Patriot CTF 2024 Upsolve + writeup未満のメモ
あとで見つけたんですが問題が勉強になりそうだったので。
https://pctf.competitivecyber.club/challenges
目次
- 目次
- crypto
- forensics
- pwn
- rev
- [Solved] Password Protector (100点)
- [Solved] Revioli, Revioli, give me the formeoli (100点)
- [Solved] Puzzle Room (100点)
- [Solved] Packed Full Of Surprises (270点)
- [未] VM-ception: Layers of the Lost Byte (388点)
- [Solved, 多分嘘] AI? PRNG (394点)
- [未] Rust Lock (396点)
- [未] Not another vm reversing problem (406点)
- [未] Full Of Bugs (452点)
- [未] GO To Sleep (478点)
- misc
- web
- osint
crypto
[Solved] idk cipher (100点)
ード読んで逆算
[Solved] Bigger is Better (100点)
Wiener's attack
[Solved] Hard to Implement (190点)
送信文 + flagの文字列結合に対してAESしている。CBモードの脆弱性で16byte blockの値が場所によらず一緒になるので、パディング駆使しつつ、フラグの先頭から1文字ずつ特定していく。
[Solved] Bit by Bit (354点)
逆算コードを書く。さらに隠されたkeyを特定するんですが、16文字飛ばしで1文字にキーのうち1byteが対応しているということ、復元される文は恐らくASCIIであることを仮定するとASCIIになるようなバイト値の候補はかなり限られる。その中でそれぞれもっともそれっぽいやつ(俺は最もアルファベットが含まれるものを選ぶようにした)をバイト値として確定させるとキーが特定できる。
[未] One for you, one for me (390点)
[未] High Roller (390点)
[未] Protected Console (462点)
[未] Textbook Schnorr right?? (478点)
[未] Scrambler V2 (482点)
[未] Melting Tux (490点)
forensics
[Solved]Simple Exfiltration (100点)
ICMPのTTLだけ見る
[Solved] Bad Blood (100点)
基本Claudeに聞きまくってそれっぽい答え投げ続けてたら解けた... クソみたいな解き方だけどpowershellの勉強にはなった。
- Q1,Q2: Claudeに聞きまくってたら候補になりそうなシェルスクリプトのファイル名がいくつか出てきたので全部投げて試す。
- Q3: EventID=4104じゃないやつを眺めてたらWinRMというのが出てきたのでそれを投げたらあってた
- Q4: 攻撃ログの断片みたいなのをClaudeにこれはどのC2 Frameworkによるものですか?と聞いたらCovenantと教えてくれた。
[未] Slingshot (280点)
[Solved] A Dire Situation (328点)
バイナリを眺めるとjpegっぽいがjpegのヘッダのない部分が現れるので、それっぽい部分にjpegのヘッダ(ffd8)を付け足してあげて、末尾のffd9を見つけてそこまでバイナリ抽出してjpegとして保存するとflag画像が現れる
[未] Structured Annuity (362点)
[未] Suspicious Drive (410点)
[未] Secret Note (470点)
[未] Abnormal Maybe Illegal (484点)
pwn
[未] Not So Shrimple Is It (224点)
[未] Navigator (306点)
[未] Shellcrunch (362点)
[未] Strings Only (420点)
[未] Flight Script (464点)
[未] sanitizer (476点)
[未] babyxss (478点)
[未] DirtyFetch (484点)
rev
[Solved] Password Protector (100点)
pycをdisassembleした結果をClaudeに食わせてデコンパイルして見通しをよくした後、手動で逆算コードを書く。
[Solved] Revioli, Revioli, give me the formeoli (100点)
Ghidraをかまして読むとフィボナッチ数列の最初の15項目に何か毛を足したものが答えであることがわかる
[Solved] Puzzle Room (100点)
CUI上で移動式のパズルを解く。正しい経路がflagを解くためのキーそのものになっている。与えられたソースがかなりメンテナブルな感じかつ実際8方向移動できるのに結局ほぼ移動できる場所がないので、深さ優先探索で解けるように改変して動かしたら一瞬で解けた。 どちらかというとPPC
[Solved] Packed Full Of Surprises (270点)
コードを読むのはかなり大変なので諦め(※)、表層的にわかることはないか調査。 (※) 追記 正規ルートではUPXで圧縮されていることに気づくのが初動だったようです。これに気づかなかったせいで全く読めなかった
strace ./encrypt
するとflag.txtを読み込んでflag.txt.encを書き込んでいることはただちにわかる。
少し入力を摂動させて遊んで見るとバイト毎に暗号化が独立していることがわかる。のでflag.txt.encに合うよう全探索。 exec.shはいいとして、途中でこういう雑なお願いをClaudeさんにやるとコードを完成させてくれた。

exec.sh
#!/bin/bash # 一時ディレクトリを作成 temp_dir=$(mktemp -d) # 終了時に一時ディレクトリとその内容を削除するトラップを設定 trap 'rm -rf "$temp_dir"' EXIT # カレントディレクトリの./encryptを一時ディレクトリにコピー cp ./encrypt "$temp_dir/" # 作業ディレクトリを一時ディレクトリに変更 cd "$temp_dir" || exit 1 # 引数をflag.txtの内容として./encryptに渡し、暗号化を実行 echo -n "$1" > ./flag.txt ./encrypt # 生成されたflag.txt.encの内容を標準出力に出力 cat flag.txt.enc # 作業ディレクトリを元に戻す(オプション - トラップが一時ディレクトリを削除するため、厳密には必要ありません) cd - > /dev/null # トラップが一時ディレクトリとその内容を自動的に削除します
solve.py
import string import subprocess import binascii def run_exec(input_str): result = subprocess.run(['./exec.sh', input_str], capture_output=True, text=False) return result.stdout # ゴールとなる暗号化されたフラグ # flag.txt.e goal = bytes.fromhex("77 b8 a6 56 38 46 c2 74 2a 27 7e 48 03 68 ed cc af 29 e8 59 14 fa 6f ef 5d 7e 92 f2 57 4c") # 初期の入力文字列 current_input = "0" * 30 for i in range(len(goal)): for c in string.printable: test_input = current_input[:i] + c + current_input[i + 1:] result = run_exec(test_input) #print(result) result_bytes = result if len(result_bytes) > i and result_bytes[i] == goal[i]: current_input = test_input print(f"Found match at position {i}: {current_input}") break else: print(f"No match found for position {i}") print("Final result:", current_input)
[未] VM-ception: Layers of the Lost Byte (388点)
[Solved, 多分嘘] AI? PRNG (394点)
import subprocess
import string
def run_ai_rnd(input_str):
result = subprocess.run(['./ai_rnd'], input=input_str.decode(), capture_output=True, text=True)
return result.stdout.strip()
def create(prefix):
return prefix + b'0' * (32 - len(prefix))
def dfs(cur, ans):
if cur[-1] == ord("}"):
if ans == run_ai_rnd(cur):
print("Found!")
print(cur, len(cur))
return
cands = []
for c1 in string.digits + string.ascii_letters + string.punctuation:
nxt = cur + bytes([ord(c1)])
prefix = " ".join(run_ai_rnd(create(nxt)).split()[:len(nxt)])
# print(prefix, len(nxt))
if ans.find(prefix) == 0:
cands.append(nxt)
if len(cands) == 0:
print("Not Found")
else:
if len(cands) > 5:
return
for n in cands:
dfs(n, ans)
def main():
with open("./random_numbers.txt", 'r') as f:
ans = f.read()
dfs(b'pctf{', ans)
print(ans)
if __name__ == "__main__":
main()
このプログラムを全探索で走らせたら
Found!
b'pctf{d33p_le@rngeG}' 19
Found!
b'pctf{d33p_le@rnge)}' 19
Found!
b'pctf{d33p_le@rngNG}' 19
Found!
b'pctf{d33p_le@rngN)}' 19
Found!
b'pctf{d33p_le@rnIeG}' 19
Found!
b'pctf{d33p_le@rnIe)}' 19
Found!
b'pctf{d33p_le@rnING}' 19
Found!
b'pctf{d33p_le@rnIN)}' 19
Found!
b'pctf{d33p_le@rn&eG}' 19
Found!
b'pctf{d33p_le@rn&e)}' 19
Found!
b'pctf{d33p_le@rn&NG}' 19
Found!
b'pctf{d33p_le@rn&N)}' 19
という候補が出てきて、恐らくpctf{d33p_le@rnINGが答え(最低!)