Patriot CTF 2024 Upsolve + writeup未満のメモ

あとで見つけたんですが問題が勉強になりそうだったので。

https://pctf.competitivecyber.club/challenges

目次

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が答え(最低!)

[未] Rust Lock (396点)

[未] Not another vm reversing problem (406点)

[未] Full Of Bugs (452点)

[未] GO To Sleep (478点)

misc

[未] Emoji Stack (100点)

[未] Making Baking Pancakes (100点)

[未] RTL Warm up (100点)

[未] Really Only Echo (104点)

[未] Emoji Stack V2 (290点)

[未] RTL Easy (318点)

[未] Let's Play [steg]Hide & Seek (402点)

[未] Make It Rain (414点)

[未] Green Eggs and Ham (472点)

web

[未] giraffe notes (100点)

[未] Impersonate (100点)

[未] Open Seasame (170点)

[未] DOMDOM (312点)

[未] Blob (334点)

[未] KIRAN SAU PROBLEM (376点)

[未] dogdays (382点)

[未] Secret Door (424点)

[未] BDog nom (464点)

osint

[未] On The Run (100点)

[未] Give me four words, Vasily (100点)

[未] Phase One (100点)

[未] Night School (100点)

[未] Porcelain Throne (100点)

[未] Studious (100点)

[未] sixfeetunder (100点)

[未] Dirty Laundry (376点)