guest - flak

userland xnr jit

One ROP mitigation is Execute no Read (XnR) or Execute Only (XOM) memory. We can wait for someone to add this to our operating system kernel using paging (You Can Run But You Can’t Read: Preventing Disclosure Exploits in Executable Code PDF) or VT-x and EPT (ExOShim: Preventing Memory Disclosure using Execute-Only Kernel Code PDF). Or we can do it today in userland. This is only a partial implementation, that protects JIT pages only, but demonstrates the technique.

Briefly, the idea is that we keep execute only memory unmapped. After a page fault, we inspect the faulting address and compare it to the CPU’s program counter. If they match, that means we’re trying to execute something, which we then permit. If they don’t match, that means it’s a read operation. Somebody is trying to steal our code.

First we need a little JIT. (For amd64.)

struct codesegment {
        uintptr_t base;
        uintptr_t end;
        struct codesegment *next;
};
struct codesegment *seglisthead;

typedef int (*adder)(int);

static unsigned char addcode[] = {
        0x8d, 0x87, 0x00, 0x00, 0x00, 0x00,
        0xc3
};

adder
addifier(int num)
{
        uint8_t *codeptr;
        struct codesegment *cs;

        cs = malloc(sizeof(*cs));

        codeptr = mmap(NULL, sizeof(addcode), PROT_READ | PROT_WRITE, MAP_ANON, -1, 0);
        memcpy(codeptr, addcode, sizeof(addcode));
        memcpy(codeptr + 2, &num, sizeof(num));
        mprotect(codeptr, 4096, PROT_NONE);

        cs->base = (uintptr_t)codeptr;
        cs->end = cs->base + sizeof(addcode);
        cs->next = seglisthead;
        seglisthead = cs;

        return (int (*)(int))codeptr;
}

Nothing special. We only have one function type, an adder, but we’re going to generate super optimized versions that can add different constants really fast. We use the usual technique of allocating some memory, copying our generated code into it, patching in the constant, etc.

The important bit to notice is that after patching, instead of changing page permissions to PROT_READ | PROT_EXEC like a normal JIT, we change to PROT_NONE. This is important for the next part.

Normally accessing a PROT_NONE page causes a segfault. That’s exactly what we want, because we can trap this signal and fix up the page permission.

void
fault(int signum, siginfo_t *info, void *v)
{
        static void *curpage;
        struct sigcontext *ctx = v;
        uintptr_t addr = (uintptr_t)info->si_addr;
        uintptr_t pc = ctx->sc_rip;

        if (addr == pc) { 
                struct codesegment *cs;

                for (cs = seglisthead; cs; cs = cs->next) {
                        if (cs->base <= addr && addr <= cs->end)
                                break;
                }
                if (!cs)
                        goto bad;
                mprotect(curpage, 4096, PROT_NONE);
                curpage = (void *)(addr & ~4095);
                mprotect(curpage, 4096, PROT_READ | PROT_EXEC);
                return;
        }

bad:
        abort();
}

Getting the fault address and program counter uses a bit of platform magic. The fault address in siginfo_t is pretty standard, but the thing hiding behind the third context argument is quite variable.

After a fault, we check if this looks like an execution fault. If so, we check our list of code segments and verify the address is something we expect to execute. Wouldn’t want to allow an attacker to execute other memory! We map the currently executable page back to none, fix the about to be executed page’s permissions, and return. The kernel returns to the faulting address and this time everything works.

Marking the previous page as PROT_NONE again is important. If we leave the old page with its current permissions, we’d eventually have all the executable memory marked readable. Thus defeating the mitigation. There is a small window, of course, where the last executed page remains readable, even after we’ve returned to executing other code.

Some test code.

int
main(int argc, char **argv)
{
        struct sigaction sa;
        int i;
        adder fns[10];

        sa.sa_sigaction = fault;
        sigemptyset(&sa.sa_mask);
        sigaddset(&sa.sa_mask, SIGSEGV);
        sa.sa_flags = SA_SIGINFO | SA_RESTART;

        sigaction(SIGSEGV, &sa, NULL);

        fns[0] = addifier(1);
        fns[1] = addifier(42);
        fns[2] = addifier(100);
        for (i = 3; i < 10; i++) {
                fns[i] = addifier(arc4random() % 10000);
                printf("adder [%d] adds %d\n", i, fns[i](0));
        }

        for (i = 0; i < 20; i++) {
                int j = arc4random_uniform(10);
                printf("adder test [%d] %d -> %d\n", j, i, fns[j](i));
        }

        printf("check read access\n");
        printf("dis codeptr %x\n", *(int *)fns[0]);

        return 0;
}

Set up the signal handler. Create a bunch of adders. Run the adders. Everything looks good. Then for good measure, we attempt to peek at one of the executable functions. That’s an abort. Naughty, naughty.

adder [3] adds 4992
adder [4] adds 4448
adder [5] adds 2093
adder [6] adds 6033
adder [7] adds 9220
adder [8] adds 7007
adder [9] adds 3569
adder test [7] 0 -> 9220
adder test [7] 1 -> 9221
adder test [4] 2 -> 4450
adder test [9] 3 -> 3572
adder test [2] 4 -> 104
adder test [4] 5 -> 4453
adder test [7] 6 -> 9226
adder test [4] 7 -> 4455
adder test [6] 8 -> 6041
adder test [8] 9 -> 7016
adder test [5] 10 -> 2103
adder test [8] 11 -> 7018
adder test [8] 12 -> 7019
adder test [7] 13 -> 9233
adder test [2] 14 -> 114
adder test [9] 15 -> 3584
adder test [1] 16 -> 58
adder test [1] 17 -> 59
adder test [4] 18 -> 4466
adder test [2] 19 -> 119
check read access
Abort trap 

If you run the program enough times, however, you might get (un)lucky. The last executed function is still readable.

adder test [1] 18 -> 60
adder test [0] 19 -> 20
check read access
dis codeptr 1878d

0x8d, 0x87, 0x01, 0x00, ... Sure enough, that’s our one adder.

Posted 2017-05-29 10:05:51 by tedu Updated: 2017-05-29 10:05:51
Tagged: c openbsd programming