picoCTF 2025 writeup (Reversing)
picoCTF 2025 の Rev 問題の Writeup.
難易度 medium のみ
Quantum Scrambler
- フラグを読み込み 16 進数のネストされたリストにしたうえで scramble 関数で処理されている
- scramble のコードを読む限り逆変換は難しそう
- 試しに
picoCTF{
を scramble 関数にかけて結果を見ると, 各リストのはじめと終わりの要素がフラグになっていることが確認できた - scramble された暗号を
cipher.txt
として読み込んだうえで以下のコードで解読するとフラグが獲得できる
# load cipher with open("./cipher.txt", "r") as f: cipher = eval(f.read()) flag = "pi" # decode for i in range(1, len(cipher)-2): flag += chr(int(cipher[i][0], 16)) + chr(int(cipher[i][2], 16)) flag += chr(int(cipher[-2][0], 16)) + chr(int(cipher[-1][0], 16)) print(flag)
Chronohack
Python のトークン作成プログラムが配布される
乱数生成器により作成されるトークンの値を正しく推測することでフラグが獲得できそう
乱数生成部分は
time.time() * 1000
をシードとして設定しているプログラム中では 1 回の接続で 50 回までトークンの回答が可能
公式ドキュメント によると,
time.time()
は秒単位でタイムゾーンに依存しない時間を返すため, 問題のコードではマイクロ秒単位の基準時刻を特定することができればトークンが復元できるサーバーとの通信ラグなどがどの程度あるか不明なので, pwntools を用いて探索的にラグの秒数を特定
import time from pwn import * import random def get_random(seed: int): alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" random.seed(seed) # seeding with current time s = "" for i in range(20): s += random.choice(alphabet) return s host = $HOST port = $PORT for itr in range(500): connected_time = int(time.time() * 1000) sh = remote(host, port) delay_time = itr * 50 print("delay", delay_time) prompt = sh.recvuntil("(or exit):") print(prompt.decode()) for i in range(50): payload = get_random(connected_time + i + delay_time) sh.sendline(payload) prompt = sh.recvuntil("Bye!") print(prompt.decode()) sh.close() print("no hit!") sh.interactive()
上記コードでは 25 秒までのラグを探索的に確かめる (コネクション接続前に時刻を取得しているためローカル時刻よりサーバ側が遅れることはない)
正しいラグの値があれば
recvuntil("Bye!")
が受け取れずエラーでプログラムが終了する上記プログラムにより明らかになったラグの値 (筆者環境で 1.65 秒) を用いてフラグ獲得用のコードを走らせる
import time from pwn import * import random def get_random(seed: int): alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" random.seed(seed) # seeding with current time s = "" for i in range(20): s += random.choice(alphabet) return s host = $HOST port = $PORT connected_time = int(time.time() * 1000) sh = remote(host, port) delay_time = 1650 prompt = sh.recvuntil("(or exit):") print(prompt.decode()) for i in range(50): payload = get_random(connected_time + i + delay_time) sh.sendline(payload) sh.interactive()
- 上記コードでうまくフラグが獲得できた
Tap into Hash
- ブロックチェーンの生成スクリプトと与えられた key, 暗号化されたチェーンからデコードを行うことができればフラグが獲得できそう
- Python のコードを調べると
encrypt
関数があるため, その逆変換を試みる encrypt
関数の最後は以下の通り
for i in range(0, len(plaintext), block_size): block = plaintext[i:i + block_size] cipher_block = xor_bytes(block, key_hash) ciphertext += cipher_block
key_hash
とchipher_text
は既知であり,xor_bytes
は同じキーでもう一度処理をすると復号可能であるため, 以下のプロセスで復号
import hashlib def xor_bytes(a, b): return bytes(x ^ y for x, y in zip(a, b)) key = $KEY encrypted_chain = $CHAIN block_size = 16 key_hash = hashlib.sha256(key).digest() decoded_text = b"" for i in range(0, len(encrypted_chain), block_size): encrypted_block = encrypted_chain[i:i + block_size] decoded_block = xor_bytes(encrypted_block, key_hash) decoded_text += decoded_block print(decoded_text)
- 上記プロセスの結果にフラグが含まれていた (タイムスタンプを使った処理や各トランザクションの処理のコード部分は問題とは直接関係なかったらしい)
perplexed
- Ghidra を用いて逆コンパイル
- Check 関数においてフラグ判定用のコードがあるため, Python で書き下す
- フラグの各文字の各位について比較を行っているので, 以下のコードでデコードするとフラグが獲得できた
check_list = [] check_list.append(-0x1f) check_list.append(-0x59) check_list.append(0x1e) check_list.append(-8) check_list.append('u') check_list.append('#') check_list.append('{') check_list.append('a') check_list.append(-0x47) check_list.append(-99) check_list.append(-4) check_list.append('Z') check_list.append('[') check_list.append(-0x21) check_list.append('i') check_list.append(0xd2) check_list.append(-2) check_list.append(0x1b) check_list.append(-0x13) check_list.append(-0xc) check_list.append(-0x13) check_list.append('g') check_list.append(-0xc) decoded_pass = "" for i in range(len(check_list)): if type(check_list[i]) == str: check_list[i] = ord(check_list[i]) elif check_list[i] < 0: check_list[i] = 0xff ^ (-check_list[i] - 1) print(list(map(hex, check_list))) var_1c = 0 var_20 = 0 var_2c = 0 pass_hex = 0 for i in range(0x17): for j in range(8): if var_20 == 0: var_20 = 1 var_30 = 1 << (7 - j & 0x1f) var_34 = 1 << (7 - var_20 & 0x1f) print(var_30, var_20) if 0 < check_list[i] & var_30: pass_hex |= var_34 var_20 += 1 if var_20 == 8: var_20 = 0 var_1c += 1 decoded_pass += chr(pass_hex) print("passhex:", pass_hex, "phrase:", chr(pass_hex)) pass_hex = 0 if var_1c == 0x1b: print("end") break print(decoded_pass)
Binary Instrumentation 1
- zip ファイルが渡される
- 解凍すると Windows バイナリが出てきた (Windows セキュリティにより削除されるので, 設定から復元する必要があった)
- ヒントを見ると frida を用いる問題らしいので frida をインストール (Fridaの使い方)
- 問題文から
Sleep
関数が使われていることが予想される frida-trace
を用いてフックしてみる
frida-trace -f bininst1.exe -i Sleep
- Sleep 関数が使われていることが分かった
- 公式ドキュメント より, Sleep 関数の引数は待機時間を表す
- 作成される
__handlers__
フォルダ内の javascript ファイルを以下のように改ざんすることで, Sleep 時間を 0 に設定
/* * Auto-generated by Frida. Please modify to match the signature of Sleep. * This stub is currently auto-generated from manpages when available. * * For full API reference, see: https://frida.re/docs/javascript-api/ */ defineHandler({ onEnter(log, args, state) { log('Sleep()'); args[0] = ptr(0); console.log("sleep modified!"); }, onLeave(log, retval, state) { } });
- frida-trace を用いて実行するとフラグが Base64 形式で獲得できた
Binary Instrumentation 2
- 1 と同じように frida を用いる問題
- 問題文からファイルの作成と書き込みの関数が用いられていると推測
- ググってみると CreateFileA と WriteFile が該当しそう
- frida-trace で確認
frida-trace -f bininst2.exe -i CreateFileA -i WriteFile
- CreateFileA が用いられていた
- 公式ドキュメント より第一引数がファイル名になっているため, 以下のようにハンドラーを書き換えファイル名を調べる
/* * Auto-generated by Frida. Please modify to match the signature of CreateFileA. * This stub is currently auto-generated from manpages when available. * * For full API reference, see: https://frida.re/docs/javascript-api/ */ defineHandler({ onEnter(log, args, state) { log('CreateFileA()'); console.log("filename:", args[0].readAnsiString()); }, onLeave(log, retval, state) { } });
- ファイル名は
<Insert path here>
となっていたため, flag.txt というファイル名を与えるようにコードを書き換えた
/* * Auto-generated by Frida. Please modify to match the signature of CreateFileA. * This stub is currently auto-generated from manpages when available. * * For full API reference, see: https://frida.re/docs/javascript-api/ */ defineHandler({ onEnter(log, args, state) { log('CreateFileA()'); console.log("filename:", args[0].readAnsiString()); var newFileName = Memory.allocUtf8String('flag.txt'); args[0] = newFileName; console.log("filename:", args[0].readAnsiString()); }, onLeave(log, retval, state) { } });
- 上記の改ざんを用いて frida-trace を用いた解析を行うと, WriteFile 関数が呼び出される
- WriteFile の 公式ドキュメント より, 第二引数がファイルの中身の保存場所のポインタとなっているため, 以下のようにハンドラーを作成しファイルの中身を読み取る
/* * Auto-generated by Frida. Please modify to match the signature of WriteFile. * This stub is currently auto-generated from manpages when available. * * For full API reference, see: https://frida.re/docs/javascript-api/ */ defineHandler({ onEnter(log, args, state) { log('WriteFile()'); console.log("WriteFile was called!"); console.log("buf:", args[1].readAnsiString()); }, onLeave(log, retval, state) { } });
- 上記過程により Base64 形式のフラグが獲得できる