Rating:

Reversing - random_pitfalls

We are given the binary, which:

  • mmaps 64 consecutive PROT_NONE pages.
  • Randomly selects 32 of them, and mprotects them to PROT_READ|PROT_WRITE.
  • Places 40-byte random values into the first 31 of them.
  • Places flag ^ value[0] ^ ... ^ value[30] into the last one.
  • Disables all syscalls except write(1, buf, 40) and exit.
  • Reads shellcode from stdin.
  • Prepends a prologue, which clears all general purpose registers except rdi and rsi (including rsp!).
  • Calls the shellcode, passing the address of the first page as rdi and the address of the buffer as rsi.

The gist of the task is to find a way to determine whether a page is readable without relying on the OS. Indeed, we need to correctly determine which 32 pages are mapped, and xor all the corresponding 40-byte values in order to get the flag. An incorrect guess would lead to a segfault, and on the next attempt we'll see some completely new new values.

Still, the way pages are randomly chosen looks a bit weird, let's write a similar loop in python and see if the distribution it produces is biased.. no, for all practical intents and purposes, it's not.

Second guess - Spectre-like approach, when we would try to provoke speculative accesses to those pages and measure differences in timings. Sounds complicated, let's leave this as a last resort.

Third guess - a specialized machine instruction. Let's check out Volume 2 of Intel Manual. And let's start with Chapter 5 (V-Z) and move backwards, because is's fun (no, seriously, that was the solution process!). The first instruction we encounter this way is: XTEST - Test If In Transactional Execution. This particular one is irrelevant, but - What Happens To Page Faults If In Transactional Execution?

Quick test:

#include <assert.h>
#include <stddef.h>
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>

static inline int is_mapped(void *p) {
    long long rax;
    asm("mov $-1,%%rax\n"
        "xbegin 0f\n"
        "mov 0(%[p]),%%rbx\n"
        "xend\n"
        "0: mov %%rax,%[rax]\n"
        : [rax] "=r" (rax) : [p] "r" (p) : "rax", "rbx");
    return rax == -1;
}

int main() {
    assert(getpagesize() == 4096);
    void *m0 = mmap(NULL, 4096, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    assert(m0 != MAP_FAILED);
    void *m1 = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    assert(m1 != MAP_FAILED);
    *(long long *)m1 = 1;
    for (;;) {
        assert(!is_mapped(m0));
        assert(is_mapped(m1));
    }
}

The code loads -1 into rax, initiates a transaction, dereferences a pointer, and ends a transaction. When transaction is aborted for whatever reason, rax receives a reason code (-1 is not a valid reason code), otherwise it stays as is. Turns out a page fault is a completely valid reason to abort a transaction without calling an interrupt handler! That's why, by the way, *(long long *)m1 = 1 is very important in this test, pages allocated by mmap are mapped only on first access, which, if it happens inside a transaction, never reaches the #PF handler.

With that knowledge, implementing shellcode is trivial, and we get the flag: SECCON{7h1S_ch4L_Is_in5p1r3d_by_sgx-rop}. The SGX ROP article turned out to be fantastic, by the way - a very much recommended read.

Original writeup (https://github.com/mephi42/ctf/tree/master/2019.10.19-SECCON_CTF_2019/reversing-random_pitfalls).