1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

初めてのSECCON Beginnersでのwriteup

Last updated at Posted at 2025-07-27

SECCON BEGINNERS CTF 2025 解けた問題のWriteup

今回、SECCON Beginners 2025に参加をしましたのでそのwriteupをここに供養します。
私が解けた問題は8問でした。Kali Linuxを使い今回解いたので特にexeファイルを解析するマルウェアの問題では最初悩みましたがなんとか解けました。残念ながら順位は130位台でしたが100位以内には入れなかったので今後hardの問題を解けるように頑張っていきたいです。

目次


Misc

kingyo_sukui - "金魚すくい"

  • カテゴリ: Misc
  • 難易度: Beginner

問題概要

「金魚すくい」を模したWebゲームで、画面上をランダムに動き回る文字(金魚)を正しい順番でクリックしていき、フラグを完成させる形式でした。

解法

  1. 分析:
    ページにアクセスすると、フラグを構成する文字が水槽の中を魚のように動き回っています。これらの文字を順番にクリックする必要がありますが、正しい順序は見た目だけではわかりません。

  2. ソースコードの解析:
    このようなフロントエンドの課題では、答えの手がかりがクライアントサイドのコード(HTML, CSS, JavaScript)に隠されていることが多いです。ブラウザの開発者ツール(F12キーで起動)を開き、HTMLの構造を調査します。

  3. 手がかり:
    水槽を表す <div id="tank"> の中を調べると、各文字が <div class="flag-char"> として表現されていることがわかります。さらに、各要素には data-index というカスタムデータ属性が付与されています。これがフラグ内での文字の正しい位置(インデックス)を示しています。

    <div class="flag-container" id="flag-container">
      <div class="flag-char" data-index="5">{</div>
      <div class="flag-char" data-index="3">4</div>
      <div class="flag-char" data-index="9">u</div>
      <div class="flag-char" data-index="2">f</div>
      <div class="flag-char" data-index="0">c</div>
    </div>
    
  4. フラグの復元:
    data-index の値が 0 から始まる連番になっていることから、これが順序を示していると確定できます。開発者ツールのコンソールで簡単なJavaScriptを実行し、自動的にフラグを構築します。

    コンソール用スクリプト:

    const chars = document.querySelectorAll('.flag-char');
    const flagArray = [];
    chars.forEach(char => {
      const index = parseInt(char.dataset.index, 10);
      const text = char.textContent;
      flagArray[index] = text;
    });
    const flag = flagArray.join('');
    console.log(flag); // ctf4b{n47um47ur1}
    

    このスクリプトを実行すると、コンソールに正しいフラグが出力されます。あとはゲーム画面でその順番通りに文字をクリックすればクリアとなりました。また、このような事をしなくても単純に今回の文字数の場合目視で並び替えでも解けます。


Cryptography

elliptic4b - "楕円曲線だからってそっ閉じしないで!"

  • カテゴリ: Cryptography
  • 難易度: Medium

問題概要

楕円曲線 secp256k1 上で、サーバーから与えられたy座標を持つ点 P(x,y) を見つけ、さらに Q = a*P を計算したときに P.x == Q.x かつ P.y != Q.y となる整数 a を見つけ出す問題です。

解法

  1. 数学的条件の分析:
    楕円曲線上の点 P(x, y) に対し、同じx座標を持ち、異なるy座標を持つ点は、Pの加法逆元である -P のみです。secp256k1 上では -P(x, -y mod p) と定義されます。したがって、問題の条件は Q = -P と同値です。

  2. 整数 a の特定:
    Q = a*P という関係から a*P = -P となります。楕円曲線上の点は位数 n の巡回群をなします。位数 n は点の個数を意味し、n*P = O(Oは無限遠点、つまり単位元)が成り立ちます。
    この性質を利用すると、a*P = (n-1)*P = n*P + (-1)*P = O - P = -P となります。
    よって、求めるべき整数は a = n - 1 です。nsecp256k1 の位数であり、これは既知の定数です。

  3. x座標の計算:
    残る課題は、与えられた y から x を求めることです。secp256k1 の曲線方程式は y² = x³ + 7 (mod p) です。これを x について変形すると x³ = y² - 7 (mod p) となります。
    これは、有限体 F_p における y² - 7モジュラ3乗根を求める問題です。

  4. 実装:
    以上のロジックを実装し、サーバーと通信してフラグを取得します。

    #!/usr/bin/env python3
    import socket
    from fastecdsa.curve import secp256k1
    
    def solve():
        p = secp256k1.p
        n = secp256k1.q
    
        host = '<運営の用意したターゲットサーバのドメイン>'
        port = 9999
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((host, port))
    
        data = s.recv(1024).decode()
        y = int(data.split('y = ')[1].strip())
    
        c = (pow(y, 2, p) - 7) % p
        
        # モジュラ3乗根を計算 (p ≡ 1 (mod 3))
        x = pow(c, (2 * p - 1) // 3, p)
    
        a = n - 1
    
        s.send(f"{x}\n".encode())
        s.recv(1024)
        s.send(f"{a}\n".encode())
    
        result = s.recv(4096).decode()
        s.close()
        # resultには ctf4b{1et'5_b3c0m3_3xp3r7s_1n_3ll1p71c_curv35!} が含まれています
        print(result)
    
    if __name__ == "__main__":
        solve()
    

seesaw - "アンバランスな素数"

  • カテゴリ: Cryptography
  • 難易度: Beginner

問題概要

RSA暗号で暗号化されたフラグが与えられており、公開鍵(n, e)と暗号文cが提供されています。しかし、nを構成する素数p, qの一方が非常に小さいという脆弱性があります。

解法

  1. 脆弱性の特定:
    問題のタイトル「seesaw」や「アンバランス」という言葉から、nの素因数p, qのビット長が大きく異なると推測できます。これにより、大きな素数nを素因数分解する困難性が著しく低下します。

  2. 素因数分解:
    nを小さな素数から順番に試し割りしていくことで、小さい方の素因数qを簡単に見つけることができます。

  3. RSA復号:
    pとqが判明すれば、通常のRSA復号手順に従って秘密鍵dを計算し、メッセージmを復号できます。

    • $\phi(n) = (p-1)(q-1)$
    • $d = e^{-1} \pmod{\phi(n)}$
    • $m = c^d \pmod{n}$
  4. 解き方:

    #!/usr/bin/env python3
    from Crypto.Util.number import inverse, long_to_bytes
    
    n = 13194226294733107273871988203271492928285991802015577464588709893163712981267717203695992442166063095550031912705476010253137003297915636936768263020353
    e = 65537
    c = 8555421308370356222906896862653261270524226397031629583445329825003821097466095985068493655203142555989309716135096606673026024529752464799649871570253
    
    # 試し割りで小さい素数qを見つける
    q = 33091
    p = n // q
    
    phi = (p - 1) * (q - 1)
    d = inverse(e, phi)
    m = pow(c, d, n)
    
    flag = long_to_bytes(m)
    print(f"[*] Flag: {flag.decode()}")
    

Reverse Engineering

CrazyLazyProgram1 (CLP1)

  • カテゴリ: Reverse Engineering
  • 難易度: Beginner

問題概要

CLP1.cs というC#のソースコードが与えられます。このプログラムが期待する正しいフラグをソースコードから読み解く問題です。

解法

この問題はコンパイルされたバイナリではなく、直接ソースコードを読むことで解決できます。ロジックは非常にシンプルです。

  1. ソースコードの分析:
    CLP1.cs を開くと、Main メソッド内にフラグチェックのロジックがすべて記述されています。

    using System;
    class Program {
        static void Main() {
            int len=0x23; // 10進数で35
            string flag=Console.ReadLine();
    
            if((flag.Length)!=len){ // 長さチェック
                Console.WriteLine("WRONG!");
            } else {
                // 各文字をハードコードされた16進数値と比較
                if(flag[0]==0x63&&flag[1]==0x74&& ... &&flag[34]==0x7d){
                    Console.WriteLine("YES!!!\nThis is Flag :)");
                } else {
                    Console.WriteLine("WRONG!");
                }
            }
        }
    }
    
  2. フラグの復元:
    if 文の条件式で比較されている16進数値は、各文字のASCIIコードです。これらを順番に文字へ変換することで、フラグが明らかになります。

インデックス 16進数 ASCII文字
flag[0] 0x63 c
flag[1] 0x74 t
flag[2] 0x66 f
... ... ...
すべての16進数コードを文字に変換して連結すると、最終的なフラグ `ctf4b{1_1in3r5_mak3_PG_hard_2_r3ad}` が得られます。

CrazyLazyProgram2

  • カテゴリ: Reverse Engineering
  • 難易度: Medium

問題概要

CLP2.o という64-bit ELFオブジェクトファイルが与えられます。「機械語で作ってみました」というヒントから、低レベルな解析が必要だと推測されます。

解法

  1. 初期調査:
    strings CLP2.o を実行すると、%33c という書式指定子が見つかり、フラグ長が33文字であることがわかります。

  2. 逆アセンブルによる深層解析:
    この問題の核心は、コントロールフローの平坦化 (Control Flow Flattening) と呼ばれる難読化技術です。objdump -d CLP2.o で逆アセンブルすると、<main> 関数内に多数の cmp (比較) 命令と、je (一致すればジャンプ) / jne (一致しなければジャンプ) 命令が複雑に絡み合っていることが確認できます。

  3. 実行フローの追跡とフラグ復元:
    フラグを復元するには、この複雑な制御フローを丹念に追いかける必要があります。

    • プログラムの開始点から最初の cmp 命令を見つけます。
      1b8:   cmp    BYTE PTR [rbp-0x48],0x63 ; 入力の1文字目と 'c' (0x63) を比較
      1bc:   jne    233 <main+0x233>         ; 不一致ならエラー処理へ
      1be:   je     1c9 <main+0x1c9>         ; 一致すれば次のチェックへ
      
    • je 命令のジャンプ先 (1c9) に移動し、次の cmp 命令を探します。
    • このプロセスを入力の33文字分繰り返します。各 cmp 命令で比較されている即値を順番に記録し、それらをASCII文字に変換することで、完全なフラグ ctf4b{GOTO_G0T0_90t0_N0m0r3_90t0} が復元されます。

d-compile - "C言語の次はこれ!"

  • カテゴリ: Reverse Engineering
  • 難易度: Beginner

問題概要

D言語でコンパイルされた64-bit ELF実行ファイルが与えられます。プログラムはユーザーからの入力を受け取り、それが正しいフラグであるかを判定します。正しいフラグを見つけ出すことが目的です。

解法

  1. 初期調査:
    file コマンドでD言語でコンパイルされた実行ファイルであることを特定します。strings ではフラグそのものは見つかりません。

  2. 逆アセンブルによる解析:
    解析の核心は objdump などでバイナリを静的解析することです。D言語のプログラムは、C言語の main 関数に相当するエントリーポイントとして _Dmain 関数を持ちます。この関数を重点的に解析します。
    _Dmain 関数内では、movb 命令が連続して実行されている箇所が見つかります。これは、1バイトずつ即値をスタック上に書き込む操作です。この一連の操作で、フラグ文字列をメモリ上に構築しています。

    ; objdump -d -M intel d-compile の抜粋
    mov    BYTE PTR [rbp-0x50],0x63   ; 'c'
    mov    BYTE PTR [rbp-0x4f],0x74   ; 't'
    ...
    
  3. フラグの復元:
    上記のアセンブリコードで movb 命令によって書き込まれている16進数の値を順番に抽出し、ASCII文字に変換することで、完全なフラグを復元できます。

    #!/usr/bin/env python3
    bytes_data = [
        0x63, 0x74, 0x66, 0x34, 0x62, 0x7b, 0x4e, 0x33,
        0x78, 0x74, 0x5f, 0x54, 0x72, 0x33, 0x6e, 0x64,
        0x5f, 0x44, 0x5f, 0x31, 0x61, 0x6e, 0x39, 0x75,
        0x61, 0x67, 0x33, 0x5f, 0x31, 0x30, 0x31, 0x7d
    ]
    flag = ''.join(chr(b) for b in bytes_data)
    print(f"[*] Found Flag: {flag}") # ctf4b{N3xt_Tr3nd_D_1an9uag3_101}
    

wasm_S_exp - "フラグをチェックしてくれるプログラム"

  • カテゴリ: Reverse Engineering
  • 難易度: Medium

問題概要

WebAssembly Text Format (WAT) で記述されたプログラムが与えられます。check_flag 関数がフラグの正当性を検証しますが、そのロジックをリバースエンジニアリングしてフラグを特定する問題です。

解法

  1. WATコードの構造理解:
    提供された .wat ファイルを分析すると、check_flagstir という主要な関数が見つかります。check_flag は文字を検証しますが、その順番はフラグの文字順とは一致しません。stir はメモリアドレスを計算する関数です。

  2. stir 関数の解析:
    stir 関数のロジックは数式に直すと stir(x) = 1024 + (23 + 37 * (x XOR 0x5a5a)) % 101 となります。

  3. check_flag のロジックとフラグ復元:
    check_flag 内の各チェックは、(期待される文字コード, stirへの入力値) のペアで構成されています。このロジックを逆手に取ることが解決の鍵です。

    • データ抽出: 25個のチェックから (期待される文字コード, stirへの入力値) のペアをすべて抽出します。
    • アドレス計算: 抽出した各ペアについて、stir 関数をPython等で再実装し、対応するメモリアドレスを計算します。
    • ソートと結合: (計算されたメモリアドレス, 期待される文字) のペアリストを作成し、メモリアドレスを基準に昇順でソートします。ソート後の文字を順番に連結すると、フラグが明らかになります。
    #!/usr/bin/env python3
    def stir(x):
        val = 37 * (x ^ 0x5a5a)
        val = (23 + val) % 101
        return 1024 + val
    
    def solve_flag():
        checks = [
            (0x7b, 38), (0x67, 20), (0x5f, 46), (0x21, 3), (0x63, 18), (0x6e, 119),
            (0x5f, 51), (0x79, 59), (0x34, 116), (0x5f, 19), (0x34, 11), (0x6c, 62),
            (0x57, 101), (0x33, 49), (0x35, 12), (0x62, 9), (0x34, 4), (0x41, 1),
            (0x54, 73), (0x30, 24), (0x66, 60), (0x7d, 21), (0x5f, 100), (0x31, 86),
            (0x74, 55)
        ]
        
        memory_map = []
        for expected_char_code, param in checks:
            memory_addr = stir(param)
            memory_map.append((memory_addr, chr(expected_char_code)))
        
        memory_map.sort()
        
        flag = ''.join(char for _, char in memory_map)
        print(f"[*] Reconstructed Flag: {flag}") # ctf4b{WAT_4n_345y_l0g1c!}
    solve_flag()
    

MAFC (Malware Analysis First Challenge)

  • カテゴリ: Reverse Engineering
  • 難易度: Hard

問題概要

MalwareAnalysis-FirstChallenge.exe というWindows実行ファイルと、それが暗号化した flag.encrypted ファイルが与えられました。今回はパソコンのスペックの問題でGUIツール(Ghidraなど)を使わずに実行ファイルを解析し、暗号化パラメータを特定してフラグを復号しました。

解法

  1. 初期静的解析 (stringsコマンド):
    まず、strings コマンドで実行ファイル内の印刷可能な文字列を抽出します。

    strings MalwareAnalysis-FirstChallenge.exe
    

    この出力から、Windows CryptoAPIに関連する多数の関数名や、ThisIsTheEncryptKeyIVCanObfuscation といった重要な手がかりが発見できます。これらの文字列から、マルウェアがファイルの暗号化を行っていること、そして鍵やIVの元になる文字列を推測します。

  2. 暗号化されたデータの確認 (xxdコマンド):
    次に、暗号化されたデータ flag.encrypted の中身を xxd コマンドで確認します。

    xxd flag.encrypted
    

    データが正確に64バイト(AESのブロックサイズ16バイトの4倍)であることがわかります。

  3. 詳細なバイナリ解析 (radare2の活用):
    GUIツールが使えないため、CLIベースのリバースエンジニアリングフレームワークである radare2 を使用して、実行ファイルの低レベルな解析を行います。

    r2 MalwareAnalysis-FirstChallenge.exe
    aaa                 # 全体の自動解析
    afl                 # 関数リスト表示
    / CryptDeriveKey    # 特定のCryptoAPI関数の呼び出し箇所を検索
    

    main 関数内のCryptoAPI呼び出し周辺のアセンブリコードを詳細に追跡し、関数の引数に渡されている値を特定します。

    • ハッシュアルゴリズム: CryptCreateHash の引数から CALG_SHA_256 (SHA256) であることを特定します。
    • 鍵生成アルゴリズム: CryptDeriveKey の引数から CALG_AES_256 (AES-256) であることを特定します。これにより、鍵長が32バイトであると確定し、SHA256ハッシュがそのまま鍵として使われると判断します。
    • 暗号化モード: CryptSetKeyParam の引数から CRYPT_MODE_CBC (CBCモード) であることを特定します。
    • IV (初期ベクトル): 別の CryptSetKeyParam の呼び出しから、IVが IVCanObfuscation という文字列から生成されることを特定します。これが最大のひっかけポイントで、Windows APIが文字列をUTF-16LEで扱うことに注目する必要がありました。ASCIIで16文字のこの文字列は、UTF-16LEでエンコードすると32バイトになり、その先頭16バイトがIVとして使用されていると推測します。
  4. 復号化スクリプトの作成:
    特定したすべての暗号化パラメータを基に、Pythonの pycryptodome ライブラリを使って復号スクリプトを作成してフラグを取得しました。

    #!/usr/bin/env python3
    from Crypto.Cipher import AES
    from Crypto.Hash import SHA256
    from Crypto.Util.Padding import unpad
    
    # 1. 鍵の元 (stringsから特定)
    key_str_raw = "ThisIsTheEncryptKey"
    key_str_bytes = key_str_raw.encode('utf-8')
    
    # 2. ハッシュアルゴリズム: SHA256
    h = SHA256.new(key_str_bytes)
    derived_key = h.digest() # 32バイトのハッシュをそのまま鍵として使用
    
    # 3. IV: 'IVCanObfuscation' のUTF-16LEエンコードから先頭16バイト
    IV_STRING_RAW = "IVCanObfuscation"
    iv = IV_STRING_RAW.encode('utf-16le')[:16]
    
    # --- 復号化ロジック ---
    with open('flag.encrypted', 'rb') as f:
        encrypted_data = f.read()
    
    cipher = AES.new(derived_key, AES.MODE_CBC, iv)
    decrypted_padded_data = cipher.decrypt(encrypted_data)
    
    unpadded_data = unpad(decrypted_padded_data, AES.block_size)
    flag = unpadded_data.decode('utf-8')
    
    print(f"[*] Flag: {flag}") # ctf4b{way_2_90!_y0u_suc3553d_2_ana1yz3_Ma1war3!!!}
    
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?