Upgrade to Pro — share decks privately, control downloads, hide ads and more …

picoCTF 2025 Writeup

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
Avatar for Asa Asa
May 17, 2025

picoCTF 2025 Writeup

Maximum (埼玉大学プログラミングサークル) で共有した picoCTF 2025 の Writeup です。
Maximum 内部向けに作ったもので、少し雑な部分もあるかもしれません。

Avatar for Asa

Asa

May 17, 2025
Tweet

Other Decks in Programming

Transcript

  1. 紹介する問題 • https://play.picoctf.org/participants/95836 • picoCTF 2025 に参加して解いた問題 (↑) を書き連ねていきます •

    解いた順です、量が量なのでざっくり書きます • 解いたけど消されてしまった問題 Shuffle (Crypto) Chalkboard (General Skills)
  2. Cookie Monster Secret Recipe [1] • Cookie Monster が秘密のクッキーレシピを隠したよ •

    見つけられるかな? • アクセスして適当にユーザー名・パスワードを打ってみると 当然ダメ
  3. Cookie Monster Secret Recipe [2] • Cookie を見てみる • 何やら

    secret_recipe という Cookie がある • 見るからに base64 なのでデコードしよう!
  4. head-dump [3] • API Docs を見てみると Diagnosing (診断) のカテゴリに GET

    /heapdump というエンドポイントがある これだろ!
  5. head-dump [4] • /heapdump にアクセスしてみると JSON がダウンロードされる • 8 MB

    もあるが、どうせ grep すると出てくるだろ!ということで grep してみるとやはり出てくる
  6. Flag Hunters [1] • 通常表示されない歌詞の部分があるよ 表示させられる? • 適当に実行してみると、 Crowd: と入力を求められる

    • コードを読んでみると、行を読んで ; で区切っていろいろしてる • RETURN [数字] とすればその行数に飛んでくれるらしい
  7. Flag Hunters [2] • そのまま RETURN 0 とするとダメ • その行が

    Crowd: RETURN 0 と書き換わるだけなので • ;RETURN 0 とするとよい • Crowd: ;RETURN 0 となり、 Crowd: と RETURN 0 の 2 つが認識される
  8. PIE TIME [1] • フラグ得られる? PIE に気を付けて! • PIE =

    Position Independent Executables • 実行ごとに絶対アドレスが変わる実行ファイル みたいな • 毎回アドレスが固定ではないので、 あらかじめ win 関数のメモリを計算して~とかはできない
  9. PIE TIME [2] • ソースコードを見てみよう! • main 関数のアドレスが出力されてる • objdump

    してみよう!(相対アドレスがわかる) • objdump -d [実行ファイル] • win 関数: 0x12a7, main 関数: 0x133d • これらの情報から win 関数のアドレスを計算しよう! • [実行時 main 関数] – 0x133d + 0x12a7 = [実行時 win 関数]
  10. Rust fixme 1 • Rust 聞いたことある? 文法エラーを直してフラグを得よう! • コンパイルエラーが出たところを直していけばよいです •

    L5: セミコロンがないよ • L18: ret という変数がないよ → 消しちゃってよさそう • L30: format は :? ではなく {:?} とする
  11. Rust fixme 2 • Rust の “所有権” 関係の問題です • 問題文の

    can I borrow that の borrow がヒントかも • 知らなくてもコンパイルエラーで出てくる通りに直せば OK • L3: string に mut をつける • L40: L3 に mut を付けた影響でこっちも mut にする • L39: 初期化部分も mut をつける
  12. Rust fixme 3 • 1, 2 同様にコンパイルエラーを直せばよいです • L22, L34:

    unsafe {} のコメントアウトを戻す • unsafe な処理 (ポインタを直でいじるとか) が含まれているので、 明示的に unsafe {} で囲わなければいけないというもの
  13. n0s4n1ty 1 [2] • 適当にファイルをアップロードしてみると The file ◦◦ has been

    uploaded Path: uploads/◦◦ と出てくる • ファイル名がそのまんま! • ところで POST 先は upload.php となっているため、 PHP が実行されていることがわかる • PHP ファイルをアップロード出来たら実行できる??
  14. SSTI1 [1] • 問題タイトルの通り、 SSTI を使いそう • SSTI = Server-Side

    Template Injection • <h1>{title}</h1> とかやったら、変数 title が反映される みたいなやつ
  15. SSTI1 [2] • はまやんさん、いつもありがとうございます https://blog.hamayanhamayan.com/entry/2021/12/15/225142 • まずはこれを試そう {{ 2*2 }}

    • 4 が出力されるので、 Flask / Jinja2 を使っていそう • RCE (Remove Code Execution) の部分にあるこれで OK {{request.application.__globals__.__builtins__.__import__(‘os’).popen(‘実行コマンド').read()}}
  16. hashcrack [2] • すごいサイト https://hashes.com/en/tools/hash_identifier • ハッシュから使われていそうなアルゴリズムを求めるやつ • パスワードリスト rockyou.txt

    • よく使われるパスワードを一覧にしたもの • https://github.com/danielmiessler/SecLists/blob/master/Passwords/Leake d-Databases/rockyou.txt.tar.gz
  17. 3v@l [3] • • どうやらさっきの TODO の keyword block は効いていそう

    • つまり os, eval, exec, ... は使えなさそう
  18. WebSockFish [3] • 観察でわかったこと • eval [評価値] で通信していそう • 評価値は自分が有利なほど低い値

    • index.html の script 見てみると sendMessage で送れそう • eval -4000000000 とか送ってみたらフラグを獲得した
  19. hash-only-1 [1] • フラグファイルを読む権限はあるけど ハッシュしか教えてくれないバイナリファイル • strings してみる • strings

    ./flaghasher • /bin/bash -c ‘md5sum /root/flag.txt’ とあるので、 このコマンドでフラグのハッシュを得ていそうだとわかる
  20. hash-only-1 [2] • 当然 /root の権限は 700(ユーザーroot 以外読み取れない) • 当然

    flaghasher には setuid フラグが立ってる • ls -l したときに所有者の実行部分 (x) が s になってればそれ • 所有者の権限で実行できる(今回はroot 権限)
  21. hash-only-1 [3] • md5sum をうまくいじれないか? • • md5sum が 777

    なので書き込めちゃう • md5sum を cat で上書きする、とかができる • cp /usr/bin/cat /usr/bin/md5sum; ./flaghasher • これでファイルの内容を取得できるようになったのでやるだけ
  22. EVEN RSA CAN BE BROKEN??? [1] • 𝑁,𝑒,𝑐 が与えられるのでRSA 暗号を解読せよとのこと

    • RSA 暗号の仕組み(ざっくり) • 素数 𝑝,𝑞 を用意して 𝑁 = 𝑝𝑞, 𝜆 𝑁 = 𝑝 − 1 𝑞 − 1 とする • 𝜆 𝑁 と素な正整数 𝑒 を選ぶと、 𝑑 ⋅ 𝑒 ≡ 1 mod𝜆 𝑁 なる 𝑑 が存在する • 𝑁,𝑒 を公開鍵、𝑑 を秘密鍵とする • 暗号化: 𝑐 = 𝑚𝑒 mod𝑁 を返す(𝑚: 暗号化したい値) • 復号: 𝑚 = 𝑐𝑑 mod𝑁 を返す • 証明はWikipedia とかを参照
  23. EVEN RSA CAN BE BROKEN??? [2] https://factordb.com/ • RSA 暗号の安全性の根拠

    • でかい数 × でかい数 の素因数分解には時間がかかるから! • 復号するには𝑑 が必要→ 𝜆 𝑁 を知りたい→ 𝑝,𝑞 を知りたい→ 𝑁 を素因数分解! • 逆に言えば、 𝑁 の素因数分解が簡単にできれば “弱い” ものとなる • すごいサイト https://factordb.com/ • でかい数の素因数分解を表示してくれる (かもしれない) サイト • 登録されてなければ表示してくれないかもしれない
  24. EVEN RSA CAN BE BROKEN??? [3] • とはいえ今回は factordb は必要ない

    • 𝑁 が偶数なので一方の素数が 2 であることがわかり、もう一方もわかる • こんなかんじの Python プログラムで復元する • 多倍長整数使えるし pow で mod とってくれるし pow で逆元計算もできる
  25. Event-Viewing [1] • Windows Log ファイルが与えらえるので解析してね • マルウェアは PC 電源オンしてログインするとすぐシャットダウンするよ

    • 従業員によるストーリーは以下の通り 1. オンラインでDL したインストーラを使ってソフトウェアをインストール 2. インストールしたソフトウェアを実行したけど何も起きず 3. ログインするたびに黒いコマンドプロンプトが起動してすぐシャットダウンする
  26. Event-Viewing [2] • Windows ユーザーならダブルクリックで開けるので開く • ほかの OS の人どうするんですかね •

    まずは「インストーラでインストール」と言われているので ソースが「MsiInstaller」のものを探すとそれっぽいのがある
  27. hash-only-2 [1] • hash-only-1 と同じ状況の模様 • とりあえず flaghasher がホームディレクトリにないので探す •

    find / -type f -name flaghasher • /配下にある、名前がflaghasherのファイル(f) を探す • /usr/local/bin/flaghasher にあるらしい
  28. hash-only-2 [2] • strings してみると今回も md5sum を呼び出しているらしい • が、当然 /usr/bin/md5sum

    は 755 で上書きできない • setuid フラグは今回もついてる • どうする?
  29. hash-only-2 [3] • PATH を上書きしよう! • $PATH は : 区切りで左から順に見られていく

    • この場合 /usr/local/sbin、 /usr/local/bin、... の順にみていく • 書き込めるディレクトリに md5sum という名前で cat の動作をするプログラムを作成し、 PATH に追加すればいい
  30. hash-only-2 [4] • 困る: テキストエディタがない • vi とか nano とか

    emacs とかやってみたが command not found • 解決: Python から直接ファイル書き込み • 幸い python3 コマンドはあるので、↓ をベタ書き • “md5sum ってファイルに「/root/flag.txt を開いてprint してね」って書いてね”の処理
  31. RED [1] • red.png を解析してね → • こういうのはプロパティに書いてあるんや! • Poem:

    Crimson heart, vibrant and bold,.Hearts flutter at your sight..Evenings glow softly red,.Cherries burst with sweet life..Kisses linger with your warmth..Love deep as merlot..Scarlet leaves falling softly,.Bold in every stroke. • ??? (大文字だけ読むとCHECK LSB になるのは当時気づいてない)
  32. Apriti sesamo [1] • ログインページにユーザー名とパスワードを入れるだけのサイト • DevTools で見てみてもそれっぽいものは見当たらない • よくわからんのでヒントを見ると、

    emacs の backup files らしい こんなの誰がわかるねん • ファイル名の末尾に ~ をつける • するとソースコードが見えた
  33. Apriti sesamo [4] • つまり、 username と pwd は違う文字列として sha1

    を衝突させなければいけない • 検索してみると、 https://shattered.io/ に SHA1 が同じになる異なる PDF ファイル 2 つが挙がっていた
  34. Apriti sesamo [5] • 得られた 2 つの PDF ファイルを送信してやればフラグ獲得 •

    ブラウザで input[type=text] にファイルは送り付けられないので、 プログラム書いて送るのがよさそう
  35. Tap into Hash [1] • ソースコード読んで復元してね • Block chain 的な処理をしている

    • SHA256 ハッシュをとって先頭 2 桁が 00 となるようにする • Block chain を – でつなげる • 16 桁ごとに key_hash と xor_bytes をとる • Block chain を 5 個つなげて、半分で切って間にフラグを埋める
  36. Tap into Hash [2] • SHA256 の桁数は (hex 表記で) 64

    桁 • 5 ブロック目の先頭 3 桁 (65 – 67 文字目) は “-00” が確定 → key hash の先頭 3 桁は 98, 0, 51 と復元される • 9 ブロック目の 2 – 4 桁も “-00” が確定 → 4 桁目まで復元される • ここから適当に復元を試みる
  37. Tap into Hash [3] • 処理を思い出す • Block chain を

    5 個つなげて、半分で切って間にフラグを埋める • chain hash は 64 × 5 + 4 = 324 文字 • 前半 162 文字 + フラグ + 後半が 162 文字 • 11 行目 3 文字目以降がフラグ(picoCTF{ ... }) 部分! • 確かに pi となっている • 10 文字目まで復元できた!
  38. Tap into Hash [4] • 下の 10 行 (ハッシュ後半部分) の

    先頭 10 桁には – が存在しない • 全体 24 行で block あたり 4 行使う → 16, 20 行目の後ろのほうに – が出てくるはず • L16 C11, L20 C12 かL16 C12, L20 C13 か... L16 C15, L20 C16 が“--” となるべき • ここから逆算してそれっぽいのを見つける
  39. Tap into Hash [5] • それっぽい (ほかの行が全部 ASCII になる) のは

    13, 14 だった → 13, 14 桁目が判明 • 処理を再び思い出すと、 padding は余る文字数 = 2 • つまり最後は ¥x02, ¥x02 となるべき → 15, 16 桁目も判明 • 文字数を考慮すると、 L14 C12 はフラグの最後 “}” となるはず → 12 桁目も判明
  40. Tap into Hash [6] • あとは 11 桁目 • フラグ以外の部分は

    SHA256 の hexdigest なので、 0-9a-f になっているはず • つまり =n89im:k>h すべてが 0-9a-f に変換される • 256 通りすべて全探索してみると [ と ¥ が候補に • あとはフラグからメタ読み • [ の場合: picoCTF{block_3SRhV... block まあわかる • ¥ の場合: picoCTF{elock_3SRhV... elock ??
  41. Quantum Scrambler [2] • コードを読むと、文字長に依存して何か起きてそう • 適当に実験してみる • “a” →

    [[‘0x61’]] • “aa” → [['0x61'], ['0x61’]] • “aaa” → [['0x61', '0x61'], ['0x61', []]] • “aaaa” → [['0x61', '0x61'], ['0x61', []], ['0x61’]] • “aaaaa” → [['0x61', '0x61'], ['0x61', [], '0x61'], ['0x61', [['0x61', '0x61']]]]
  42. Quantum Scrambler [3] • 予想: 深さ 2 の部分にある文字だけを見ればいいのでは? • “aaaaa”

    → [['0x61', '0x61'], ['0x61', [], '0x61'], ['0x61', [['0x61', '0x61']]]] • 証明: AC 深さ 4 なので無視
  43. Quantum Scrambler [4] • 一応ざっくり説明だけ • L には flag の

    hex 化 str が入っている • i をイテレーションするが、 • i–2 に i–1 を追加 • i–1 に A[0...i-2] を追加 の処理しかしていないので、元の文字列の深さは変わらない (append されることはあるがそれは copy されているだけなので)
  44. Bitlocker-1 [2] • すごいツール: John the Ripper • パスワードクラッキングツール •

    BitLocker を含むいろんな形式に対応 • https://www.openwall.com/john/
  45. Bitlocker-1 [5] • hashcat --help • 「BitLocker は -m 22100

    使えよ」 • hashcat -m 22100 ./hash.txt ./rockyou.txt • 毎度のごとく rockyou.txt を使う (スライド 37 参照) • jacqueline がパスワードだとわかった
  46. Bitlocker-1 [6] • パスワードがわかったので dd ファイルを開いてやりたい • loop デバイスを作成してやればいいらしい •

    sudo losetup --partscan --find --show bitlocker-1.dd • 削除時は sudo losetup –d /dev/loop<id>、id チェックは losetup -l • Ubuntu のエクスプローラに出てきたので見ると flag.txt がいた
  47. Guess My Cheese part 1 [2] • よくわからんのでヒントを見ると、 Affine cipher

    と言われた • Affine cipher? • 𝑎,𝑏 を適当に決めて 𝑎𝑥 + 𝑏 mod 𝑚 で定義する • 今回は大文字アルファベット 26 文字だけなので 𝑚 = 26 だと思われる • 1 ≤ 𝑎 < 𝑚, 0 ≤ 𝑏 < 𝑚 と仮定してよい (どうせ繰り返されるので)
  48. Guess My Cheese part 1 [3] • 𝑎,𝑏 を求めるために、平文を暗号化してもらう •

    適当にチーズ名を入力してみると MASCARPONE は暗号化してくれた • そこから全探索して 𝑎,𝑏 を求めると答えが出てくる
  49. perplexed [1] • Download the binary here. • file コマンド実行すると

    executable であることがわかる • strings コマンド実行してもうれしいものはない • ヒントもない • 情報なさすぎ!
  50. perplexed [2] • すごいツール: Ghidra • アメリカの国家安全保障局が作ったリバースエンジニアリングツール • デコンパイル (バイナリ

    → ソースコード) ができる • 当然変数名とかはわからないが、バイナリよりは推測しやすい • https://github.com/NationalSecurityAgency/ghidra これまで IDA 使ってたけど使いづらかったので乗り換えた
  51. perplexed [4] • パスワード入力させて • check 関数に入れて • その返り値が 1

    じゃなければ Correct ...になりそうだということがわかる
  52. perplexed [5] • check 関数 (を適当にフォーマットしたもの) • まず sVar1 ==

    0x1b に注目 • これが true じゃないと 1 が返る • よって入力値の文字数は 0x1b - 1 = 26 文字である • -1 したのは、 scanf時に Enter して ¥n が入ってしまうから
  53. perplexed [6] • 枠部分を見てみると、 param_1[local_1c] & local_34 と local_58[local_24] &

    local_30 の 一致チェックをしているらしい • local_58[local_24] & local_30 を出力して値を見てみよう!
  54. perplexed [7] • printf("%d", ((int) local_58[(int) local_24] & local_30) >

    0); • 11100001101001110001111011111000011101010010001101111011011000 01101110011001110111111100010110100101101111011111011010011101 001011111110000110111110110111110100111011010110011111110100 • 当然これだけ出されてもわかるわけはない
  55. perplexed [8] • ところで... • C の char 型は 1byte

    = 8bit • ASCII コードは 0 – 127 = 7bit • さっきの値を 7 bit ごとに区切ってみると...? • 右の図の結果に • 26 文字 + 00 となってうまくいってそう • あとは上から printf(“%c”, 0b1110000); みたいにして復元
  56. flags are stepic [4] • stepic is 何? • 「ctf

    stepic」 とかで調べてみるといろいろ出てくる • 画像にファイルを埋め込み / 取り出し ができるステガノグラフィツール • いろいろ出てくるが、いろんなところで 書かれているリンク先 (https://domnit.org/stepic/doc/) は 404 • apt install stepic で入れられるらしいので入れてみよう
  57. flags are stepic [5] • stepic を使って画像からファイルを取り出す • stepic --decode

    --image-in=upz.png --out=output.txt • 終わると output.txt ができているので、中身を見てみよう!
  58. pachinko [2] • ソースコードを読んでみてもよくわからん • いったん Web サイトにアクセスしてみることに • Submit

    Circuit ポチポチしてたらなんかゲットできた (???) • どうやら運ゲーらしい (ほんとに?)
  59. SSTI2 [1] • SSTI その 2 • 当然 1 と同一のコード

    {{request.application.__globals__.__builtins__.__import__(‘os’).popen(‘ls -lah’).read()}} は無理
  60. SSTI2 [2] • いろいろ試して処理を見てみよう! • [] が含まれるとダメ • _ や

    . は無視される → request.application.__globals__ とかは別の方法でアクセスしたい
  61. SSTI2 [3] • 調べてみると foo|attr(“bar”) は foo.bar と同じ働きらしい • ということで以下のようにしてみるもうまくいかない

    • request|attr(“application”)|attr(“¥x5f¥x5fglobals¥x5f¥x5f”) |attr(“¥x5f¥x5fbuiltins¥x5f¥x5f”)|... • ¥x5f は _ のエスケープ処理
  62. PIE TIME 2 [3] ここでは • 現在実行しているアドレスを指すポインタ • 関数内のスタックの基準を指すポインタ (ベースポインタ;

    bp) • スタックの一番上を指すポインタ (スタックポインタ; sp) を知っておけば OK 右図はあくまでイメージです ... スタックの下 bp sp スタックの上 = アドレス番地の小さいほう
  63. PIE TIME 2 [4] • call_functions() が呼ばれる • (あれば) 引数、戻りアドレス

    (Return address) が スタックに積まれる Return Address arg1 ... argN ... bp sp
  64. PIE TIME 2 [5] • 現在の bp の位置を保存しておく • 関数が返されたら

    bp も帰ってきてほしいので 元 bp address Return Address arg1 ... argN ... bp sp
  65. PIE TIME 2 [6] • bp := sp とする •

    新しく基準位置を移動 元 bp address Return Address arg1 ... argN ... bp sp
  66. PIE TIME 2 [7] • ローカル変数とかのぶんを割り当て • 今回は buffer が割り当てられる

    val Foo buffer[0] ... buffer[63] 元 bp address Return Address arg1 ... argN ... bp sp
  67. PIE TIME 2 [9] • call されて return address が

    push される • return address は call 後のアドレス (mov 部分) Return address (1441) ... bp sp
  68. PIE TIME 2 [10] • bp が push される =

    bp の位置を保存 • ちなみに endbr64 はおまじないとして無視します (興味があれば各自調べてね) 元 bp address Return address (1441) ... bp sp
  69. PIE TIME 2 [11] • bp := sp として基準位置を移動 元

    bp address Return address (1441) ... bp sp
  70. PIE TIME 2 [12] • ローカル変数とかの分を確保 • より正確には: sp のアドレスを

    0x60 引いてる 元 bp address Return address (1441) ... bp sp 0x60
  71. PIE TIME 2 [13] • return するときは • sp :=

    bp • bp を戻す • return address に戻る とかをするけど、今はそこは重要ではない 元 bp address Return address (1441) ... bp sp 0x60
  72. PIE TIME 2 [14] • 実際のコードと objdump 結果を見てみると、 buffer, val,

    foo の格納位置もわかる • なお buffer は char 64 個 = 64byte = 0x40 だけあれば十分 • 0x10 = 16byte ぶんデータが空いてるのは気にしない buffer[0] ... buffer[63] (16B の何か) 元 bp address Return address (1441) ... bp sp 0x50 buffer は bp – 0x50 の位置からスタート 0x40
  73. PIE TIME 2 [15] • 実際のコードと objdump 結果を見てみると、 buffer, val,

    foo の格納位置もわかる val buffer[0] ... buffer[63] (16B の何か) 元 bp address Return address (1441) ... bp sp 0x60 val は bp – 0x60 の位置
  74. PIE TIME 2 [16] • 実際のコードと objdump 結果を見てみると、 buffer, val,

    foo の格納位置もわかる val foo buffer[0] ... buffer[63] (16B の何か) 元 bp address Return address (1441) ... bp sp 0x58 foo は bp – 0x58 の位置
  75. PIE TIME 2 [17] https://note.com/m_yoshi3/n/n6a4f5c958c62 • フォーマット文字列攻撃について少し • printf の引数とレジスタ・スタックの対応としては、

    rdi, rsi, rdx, rcx, r8, r9, *(rsp), *(rsp+8), *(rsp+16), ... となっていく • 64bit の場合; 参考 https://note.com/m_yoshi3/n/n6a4f5c958c62 • %𝑛$p (𝑛 は数字) とすると 𝑛 番目の引数が取得できるらしい • %0$p = rdi, %1$p = rsi, %2$p = rdx, ..., %6$p = スタックの一番上
  76. PIE TIME 2 [18] • 64bit executable である ことに注意すると、 sp

    から 0x68 = 13 アドレスぶん下にある • 64bit exe = アドレスは 64bit = 8byte で計算される • 0x60 に元 bp ぶんを加えて 0x68 • つまり 6+13 = 19 で %19$p とすれば Return Address を取得できる! val foo buffer[0] ... buffer[63] (16B の何か) 元 bp address Return address (1441) ... bp sp
  77. PIE TIME 2 [19] • あとは Return Address が 0x1441、

    win 関数が 0x136a を使って 計算してやれば OK