3
1

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 CTF 14 Quals Rev WriteUp

3
Posted at

SECCON14 Qualsに参加した。
全体176位だった。
Rev問題を2つ解けたのでそれだけ書く。

Breaking Out

There is something at stage 100.

提供ファイル

  • game.js
    • 難読化されたjs
  • index.html
    <!doctype html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>Phaser Breakout</title>
      <style>
        :root {
          color-scheme: dark;
        }
        body {
          margin: 0;
          min-height: 100vh;
          display: flex;
          flex-direction: column;
          align-items: center;
          background: radial-gradient(circle at 20% 20%, #0f172a 0, #0b0f18 45%, #06080f 100%);
          color: #e6edf3;
          font-family: "Segoe UI", "Trebuchet MS", "Helvetica Neue", sans-serif;
          text-align: center;
        }
        h1 {
          margin: 18px 0 6px;
          letter-spacing: 0.03em;
          font-weight: 700;
        }
        #game-container {
          margin-top: 8px;
          box-shadow: 0 12px 28px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 255, 255, 0.04);
          border-radius: 12px;
          overflow: hidden;
        }
        .hud {
          margin: 12px 0 18px;
          opacity: 0.9;
          max-width: 880px;
          line-height: 1.4;
        }
        a {
          color: #8be9fd;
        }
      </style>
    </head>
    <body>
      <h1>Breakout</h1>
      <div id="game-container"></div>
      <div class="hud">
        <p>Move the paddle with the arrow or A/D keys. Press Space to launch the ball. Clear every brick to win!</p>
      </div>
      <script src="https://cdn.jsdelivr.net/npm/phaser@3/dist/phaser.min.js"></script>
      <script type="module" src="./game.js"></script>
    </body>
    </html>
    

解法

1. encrypted_data.txtの抽出

game.jsnew Phaser[(a0_0x9638eb(0x27c))](a0_0x2556e2);にブラウザでブレークポイントを設定してそこまで実行する。

// 暗号化データを取得してファイルとしてダウンロードする
(function() {
    // 難読化解除関数を取得
    const decoder = (typeof a0_0x9638eb !== 'undefined') ? a0_0x9638eb : 
                    (typeof a0_0x3fa8 !== 'undefined') ? a0_0x3fa8 : null;

    if (!decoder) {
        alert("デコーダー関数が見つかりません。リロードしてブレークポイントを確認してください。");
        return;
    }

    // データ取得
    const data = decoder(0x1fc);

    // Blobを作成してダウンロードリンクをクリックさせる
    const blob = new Blob([data], {type: 'text/plain'});
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'encrypted_data.txt';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
})();

上記のコードをコンソールで実行してencrypted_data.txtを抽出する。

2. game.jsの解析

強く難読化されていますが、以下の重要な情報が確認できる。

  • bricks配列:初期ブロックの値が定義されている
  • 暗号化処理:Base64 → RC4 → 圧縮(gzip/zlib)の順で暗号化されている
  • キー計算:bricks配列の値からRC4キーを計算している

game.jsを解析すると、キーは以下のロジックで計算されている。

bricks = [0xe31329f4, 0x9bcfbc46, 0x3ffe057, 0x9a1b1dca, 0x66fa61da,
          0xf6f2f5c5, 0x74074c6c, 0xa37be577, 0x58162ae2, 0x2113426]

acc1 = 0x13572468
acc2 = 0x24681357
acc3 = 0xa000a

for val in bricks:
    val = val & 0xFFFFFFFF
    acc1 = (acc1 + val) & 0xFFFFFFFF
    rot = ((val << 7) & 0xFFFFFFFF) | (val >> 25)
    acc2 = (acc2 + rot) & 0xFFFFFFFF
    acc3 = (acc3 + (val ^ 0x9e3779b9)) & 0xFFFFFFFF

key = f"{acc1:08x}{acc2:08x}{acc3:08x}"

3. 復号処理の実装

  1. Base64デコード
  2. RC4復号(計算したキーを使用)
  3. Gzip/Zlib展開

復号処理を上記の順で行った。

import base64
import zlib
import gzip
import os

def calculate_key(bricks):
    """ブロックの値からキーを計算"""
    acc1 = 0x13572468
    acc2 = 0x24681357
    acc3 = 0xa000a

    for val in bricks:
        val = val & 0xFFFFFFFF
        acc1 = (acc1 + val) & 0xFFFFFFFF
        rot = ((val << 7) & 0xFFFFFFFF) | (val >> 25)
        acc2 = (acc2 + rot) & 0xFFFFFFFF
        acc3 = (acc3 + (val ^ 0x9e3779b9)) & 0xFFFFFFFF

    return f"{acc1:08x}{acc2:08x}{acc3:08x}"

def decrypt_level(encrypted_b64, key, level=0):
    """再帰的に復号化を実行する関数"""
    import json

    indent = "  " * level
    print(f"{indent}[Level {level}] Starting decryption...")
    print(f"{indent}[Level {level}] Key: {key}")

    try:
        # Base64 Decode
        encrypted_bytes = base64.b64decode(encrypted_b64)
        print(f"{indent}[Level {level}] Encrypted bytes length: {len(encrypted_bytes)}")

        # RC4 Decrypt
        decrypted_rc4 = rc4_decrypt(encrypted_bytes, key)
        print(f"{indent}[Level {level}] Decrypted RC4 bytes length: {len(decrypted_rc4)}")

        # Decompress (gzip format)
        try:
            decompressed = gzip.decompress(bytes(decrypted_rc4))
        except:
            try:
                decompressed = zlib.decompress(decrypted_rc4, -15)
            except:
                decompressed = bytes(decrypted_rc4)

        # Try to parse as JSON
        try:
            data = json.loads(decompressed.decode('utf-8'))

            # Check if there's a flag field
            if 'flag' in data:
                print("\n" + "="*50)
                print(f"[*] FLAG FOUND: {data['flag']}")
                print("="*50 + "\n")
                return data['flag']

            # Check if this is the final level (next is empty or doesn't exist)
            if 'next' not in data or not data['next']:
                print(f"{indent}[Level {level}] Final level reached!")

                # Check if there's a layout field (ASCII art flag)
                if 'layout' in data:
                    print(f"{indent}[Level {level}] Found layout field, extracting flag from ASCII art...")
                    layout = data['layout']
                    # Print the layout for manual inspection
                    print("\n" + "="*50)
                    print("[*] ASCII ART FLAG:")
                    print("="*50)
                    for line in layout:
                        print(line)
                    print("="*50 + "\n")

                    # Try to extract text from ASCII art
                    # Convert X to 1 and . to 0, then try to decode as ASCII
                    flag_text = ""
                    for line in layout:
                        for char in line:
                            if char == 'X':
                                flag_text += "1"
                            elif char == '.':
                                flag_text += "0"

                    # Try binary to ASCII conversion
                    try:
                        # Split into 8-bit chunks
                        binary_chunks = [flag_text[i:i+8] for i in range(0, len(flag_text), 8)]
                        ascii_flag = "".join([chr(int(chunk, 2)) for chunk in binary_chunks if len(chunk) == 8])
                        if any(c.isprintable() and c.isalpha() or c == '{' or c == '}' or c == '_' for c in ascii_flag):
                            print(f"[*] Extracted flag from binary: {ascii_flag[:100]}")
                    except:
                        pass

                    return layout

                # Return the full data as fallback
                return data

            # Check if there's a 'next' field to decrypt
            if 'next' in data and data['next']:
                print(f"{indent}[Level {level}] Found 'next' field, attempting to decrypt next level...")

                # Calculate new key from specials
                if 'specials' in data and data['specials']:
                    special_values = [spec['value'] for spec in data['specials']]
                    next_key = calculate_key(special_values)
                    print(f"{indent}[Level {level}] Calculated next level key: {next_key}")

                    # Recursively decrypt the next level
                    return decrypt_level(data['next'], next_key, level + 1)
                else:
                    print(f"{indent}[Level {level}] No 'specials' field found in data")
                    return None
            else:
                # No 'next' field, try to decode as text
                text = decompressed.decode('utf-8')
                if 'SECCON' in text or 'FLAG' in text or 'flag' in text.lower():
                    print("\n" + "="*50)
                    print(f"[*] FLAG FOUND IN TEXT: {text}")
                    print("="*50 + "\n")
                    return text
                else:
                    print(f"{indent}[Level {level}] Decrypted data (first 200 chars): {text[:200]}")
                    return text
        except json.JSONDecodeError:
            # Not JSON, try to decode as text
            text = decompressed.decode('utf-8')
            if 'SECCON' in text or 'FLAG' in text or 'flag' in text.lower():
                print("\n" + "="*50)
                print(f"[*] FLAG FOUND IN TEXT: {text}")
                print("="*50 + "\n")
                return text
            else:
                print(f"{indent}[Level {level}] Decrypted text (first 200 chars): {text[:200]}")
                return text
    except Exception as e:
        print(f"{indent}[Level {level}] Decryption failed: {e}")
        import traceback
        traceback.print_exc()
        return None

def solve():
    print("[*] Starting Solver...")

    # ---------------------------------------------------
    # 1. キー計算(初期ブロックから)
    # ---------------------------------------------------
    bricks = [
        0xe31329f4, 0x9bcfbc46, 0x3ffe057, 0x9a1b1dca, 0x66fa61da,
        0xf6f2f5c5, 0x74074c6c, 0xa37be577, 0x58162ae2, 0x2113426
    ]

    key = calculate_key(bricks)
    print(f"[+] Calculated Initial Key: {key}")

    # ---------------------------------------------------
    # 2. ファイルから暗号化データを読み込む
    # ---------------------------------------------------
    filename = 'encrypted_data.txt'

    if not os.path.exists(filename):
        print(f"[-] Error: '{filename}' が見つかりません。")
        print("    ブラウザからダウンロードしたファイルを同じフォルダに置いてください。")
        return

    with open(filename, 'r') as f:
        # 余計な空白や改行を除去
        ENCRYPTED_DATA_B64 = f.read().strip().replace("\n", "").replace("\r", "")

    print(f"[+] Loaded encrypted data from file ({len(ENCRYPTED_DATA_B64)} chars)")

    # ---------------------------------------------------
    # 3. 復号処理(再帰的)
    # ---------------------------------------------------
    flag = decrypt_level(ENCRYPTED_DATA_B64, key, level=0)

    if flag:
        print(f"\n[+] Final result: {flag}")
    else:
        print("[-] Could not extract flag")

def rc4_decrypt(data, key_str):
    key_bytes = key_str.encode('utf-8')
    S = list(range(256))
    j = 0
    for i in range(256):
        j = (j + S[i] + key_bytes[i % len(key_bytes)]) % 256
        S[i], S[j] = S[j], S[i]

    i = 0
    j = 0
    res = bytearray()
    for char in data:
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        k = S[(S[i] + S[j]) % 256]
        res.append(char ^ k)
    return res

if __name__ == "__main__":
    solve()

出力(一部抜粋)

crypted bytes length: 382
                                                                                                                                                                                                    [Level 98] Decrypted RC4 bytes length: 382
                                                                                                                                                                                                    [Level 98] Final level reached!
                                                                                                                                                                                                    [Level 98] Found layout field, extracting flag from ASCII art...

==================================================
[*] ASCII ART FLAG:
==================================================
XXXXXXX.XX.....XXXX.X.X...XXXXXXX
X.....X.......XX.X..XXXX..X.....X
X.XXX.X.X..XXXXX....X...X.X.XXX.X
X.XXX.X.XX.X....X...XXX.X.X.XXX.X
X.XXX.X.XXXX.X...XXXX...X.X.XXX.X
X.....X..X..X.X....X..X.X.X.....X
XXXXXXX.X.X.X.X.X.X.X.X.X.XXXXXXX
.........X.X.XX..X..X.XXX........
XXXX..X.X....XX.XXX.XX...X..XXX.X
XXX.X....X.....XX...XXXX.X...X...
....XXX.X.....XX.XX.X.XXXX.X..XXX
...X.X..X..XXXXX....X.X...XXXX..X
XXXXXXX..X.X........X.XX....X.X..
XX.XXX.XX.XX.X...XX.....XX.X..X.X
X..X..X.XX..X.X..X.XX.X..X..X.X..
.XX..X.XXX..XX.XX.X..XX..XXX.XXXX
.XXXXXX..X.X...XX.XXXXXXXXXXX.X.X
XX.X....X.X.XX.X...XX..X..X.X.XX.
X...XXX...XXX..X....X....X...X.X.
.XXXXX.XXXX.X...XXXX...X....X..XX
XX.X..X..XXXXX...XX..X..X..X.....
XX.XX.......XXX..XX.X..XX.X......
...XXXX..XXXXX.........X.XXXXX.XX
.X.X.X.XXX.....X..XX.......X....X
X..XX.X..XX.X.X....X....XXXXXX...
........XXXXX.XXXX.X.XX.X...X..XX
XXXXXXX...XX.X..X.XXXX.XX.X.X....
X.....X..XXX..XXX....XXXX...XXX..
X.XXX.X...X.X.X.X.....XXXXXXX.X..
X.XXX.X.XXX...X...XXXX.XX..XXX..X
X.XXX.X.XX...XXX..X.X.XX.XX......
X.....X.X..X.XXXXXXXX.X...XX....X
XXXXXXX.X....XX..XXXX..X.XXXX.X..
==================================================

[*] Extracted flag from binary: þÁê?Á§n§Â+·ZÕÛ¯GìPúªªþ¬òìNô Ç¢ ÚôâáGå°¦í£K)i(ËLï~Q¿úèV£BO½!='ÆIÀsMyðö«`!

[+] Final result: ['XXXXXXX.XX.....XXXX.X.X...XXXXXXX', 'X.....X.......XX.X..XXXX..X.....X', 'X.XXX.X.X..XXXXX....X...X.X.XXX.X', 'X.XXX.X.XX.X....X...XXX.X.X.XXX.X', 'X.XXX.X.XXXX.X...XXXX...X.X.XXX.X', 'X.....X..X..X.X....X..X.X.X.....X', 'XXXXXXX.X.X.X.X.X.X.X.X.X.XXXXXXX', '.........X.X.XX..X..X.XXX........', 'XXXX..X.X....XX.XXX.XX...X..XXX.X', 'XXX.X....X.....XX...XXXX.X...X...', '....XXX.X.....XX.XX.X.XXXX.X..XXX', '...X.X..X..XXXXX....X.X...XXXX..X', 'XXXXXXX..X.X........X.XX....X.X..', 'XX.XXX.XX.XX.X...XX.....XX.X..X.X', 'X..X..X.XX..X.X..X.XX.X..X..X.X..', '.XX..X.XXX..XX.XX.X..XX..XXX.XXXX', '.XXXXXX..X.X...XX.XXXXXXXXXXX.X.X', 'XX.X....X.X.XX.X...XX..X..X.X.XX.', 'X...XXX...XXX..X....X....X...X.X.', '.XXXXX.XXXX.X...XXXX...X....X..XX', 'XX.X..X..XXXXX...XX..X..X..X.....', 'XX.XX.......XXX..XX.X..XX.X......', '...XXXX..XXXXX.........X.XXXXX.XX', '.X.X.X.XXX.....X..XX.......X....X', 'X..XX.X..XX.X.X....X....XXXXXX...', '........XXXXX.XXXX.X.XX.X...X..XX', 'XXXXXXX...XX.X..X.XXXX.XX.X.X....', 'X.....X..XXX..XXX....XXXX...XXX..', 'X.XXX.X...X.X.X.X.....XXXXXXX.X..', 'X.XXX.X.XXX...X...XXXX.XX..XXX..X', 'X.XXX.X.XX...XXX..X.X.XX.XX......', 'X.....X.X..X.XXXXXXXX.X...XX....X', 'XXXXXXX.X....XX..XXXX..X.XXXX.X..']

.Xのアスキーアートが出力された。

4. アスキーアートの解析

アスキーアートをよく見るとQRコードであることが分かる。

#!/usr/bin/env python3
"""
ASCIIアートをQRコードとして解釈して読み取るスクリプト
"""

from PIL import Image
import numpy as np

# ASCIIアートのレイアウト(exploit.pyから取得)
layout = [
    "XXXXXXX.XX.....XXXX.X.X...XXXXXXX",
    "X.....X.......XX.X..XXXX..X.....X",
    "X.XXX.X.X..XXXXX....X...X.X.XXX.X",
    "X.XXX.X.XX.X....X...XXX.X.X.XXX.X",
    "X.XXX.X.XXXX.X...XXXX...X.X.XXX.X",
    "X.....X..X..X.X....X..X.X.X.....X",
    "XXXXXXX.X.X.X.X.X.X.X.X.X.XXXXXXX",
    ".........X.X.XX..X..X.XXX........",
    "XXXX..X.X....XX.XXX.XX...X..XXX.X",
    "XXX.X....X.....XX...XXXX.X...X...",
    "....XXX.X.....XX.XX.X.XXXX.X..XXX",
    "...X.X..X..XXXXX....X.X...XXXX..X",
    "XXXXXXX..X.X........X.XX....X.X..",
    "XX.XXX.XX.XX.X...XX.....XX.X..X.X",
    "X..X..X.XX..X.X..X.XX.X..X..X.X..",
    ".XX..X.XXX..XX.XX.X..XX..XXX.XXXX",
    ".XXXXXX..X.X...XX.XXXXXXXXXXX.X.X",
    "XX.X....X.X.XX.X...XX..X..X.X.XX.",
    "X...XXX...XXX..X....X....X...X.X.",
    ".XXXXX.XXXX.X...XXXX...X....X..XX",
    "XX.X..X..XXXXX...XX..X..X..X.....",
    "XX.XX.......XXX..XX.X..XX.X......",
    "...XXXX..XXXXX.........X.XXXXX.XX",
    ".X.X.X.XXX.....X..XX.......X....X",
    "X..XX.X..XX.X.X....X....XXXXXX...",
    "........XXXXX.XXXX.X.XX.X...X..XX",
    "XXXXXXX...XX.X..X.XXXX.XX.X.X....",
    "X.....X..XXX..XXX....XXXX...XXX..",
    "X.XXX.X...X.X.X.X.....XXXXXXX.X..",
    "X.XXX.X.XXX...X...XXXX.XX..XXX..X",
    "X.XXX.X.XX...XXX..X.X.XX.XX......",
    "X.....X.X..X.XXXXXXXX.X...XX....X",
    "XXXXXXX.X....XX..XXXX..X.XXXX.X..",
]

def ascii_to_image(layout, scale=10):
    """
    ASCIIアートを画像に変換
    X = 黒 (0), . = 白 (255)
    """
    height = len(layout)
    width = len(layout[0])

    # 画像データを作成
    img_data = np.zeros((height, width), dtype=np.uint8)

    for y, row in enumerate(layout):
        for x, char in enumerate(row):
            if char == 'X':
                img_data[y, x] = 0  # 黒
            elif char == '.':
                img_data[y, x] = 255  # 白
            else:
                img_data[y, x] = 255  # デフォルトは白

    # スケールアップ(QRコード読み取りのために拡大)
    img_data_scaled = np.repeat(np.repeat(img_data, scale, axis=0), scale, axis=1)

    # PIL Imageに変換
    img = Image.fromarray(img_data_scaled, mode='L')

    return img

def decode_qrcode(img):
    """QRコードを読み取る"""
    try:
        from pyzbar import pyzbar
        decoded = pyzbar.decode(img)
        if decoded:
            return decoded[0].data.decode('utf-8')
    except ImportError:
        pass

    try:
        import cv2
        detector = cv2.QRCodeDetector()
        data, bbox, _ = detector.detectAndDecode(np.array(img))
        if data:
            return data
    except ImportError:
        pass

    try:
        from qrcode import QRCode
        # qrcodeライブラリはデコード機能がないのでスキップ
        pass
    except ImportError:
        pass

    return None

if __name__ == "__main__":
    print("[*] Converting ASCII art to image...")
    img = ascii_to_image(layout, scale=10)

    # 画像を保存
    img.save('qrcode_from_ascii.png')
    print("[+] Saved image as 'qrcode_from_ascii.png'")

    # QRコードを読み取る
    print("[*] Attempting to decode QR code...")
    result = decode_qrcode(img)

    if result:
        print("\n" + "="*50)
        print(f"[*] FLAG FOUND: {result}")
        print("="*50 + "\n")
    else:
        print("[-] Could not decode QR code automatically.")
        print("[*] Please try using an online QR code reader with 'qrcode_from_ascii.png'")
        print("[*] Or install pyzbar: pip install pyzbar pillow")

実行

[*] Converting ASCII art to image...
[+] Saved image as 'qrcode_from_ascii.png'
[*] Attempting to decode QR code...

==================================================
[*] FLAG FOUND: SECCON{H4ve_y0u_3ver_p14yed_Atari?_SQiOIVX6HPtRekE1vTn4}
==================================================

FLAG: SECCON{H4ve_y0u_3ver_p14yed_Atari?_SQiOIVX6HPtRekE1vTn4}

Mini Bloat

solve me

この問題は、Next.jsアプリケーションの難読化されたJavaScriptからFlagを取得するReverse Engineering問題です。アプリケーションには25日分のパズルがあり、すべてのパズルを解くことでFlagが復号されます。

ファイル構造の確認

mini_bloat/
├── dist/
│   ├── index.html
│   └── _next/
│       └── static/
│           └── chunks/
│               └── app/
│                   └── page-fdcc665989738875.js  (難読化されたJavaScript)

解法

1. 難読化されたJavaScriptの解析

page-fdcc665989738875.js を確認すると、変数名が短縮され、コードが難読化されている。重要な変数を特定する:

  • Sn: Base64エンコードされたパズルデータ(gzip圧縮)
  • On: ペッパー文字列 "boolean-advent-2025-pepper"
  • Mn: Base64エンコードされた暗号化されたFlag
  • An: LocalStorageのキー名 "advent2025_solutions"

2. パズルデータの抽出とデコード

パズルデータはBase64エンコードされ、gzip圧縮されています。以下のスクリプトでデコードしする。

decode_data.js
const fs = require('fs');
const zlib = require('zlib');

// Base64デコード
const Sn_b64 = "..."; // 難読化されたJavaScriptから抽出
const compressed = Buffer.from(Sn_b64, 'base64');

// Gzip展開
const decompressed = zlib.gunzipSync(compressed);
const puzzlesData = JSON.parse(decompressed.toString('utf8'));

fs.writeFileSync('puzzles.json', JSON.stringify(puzzlesData, null, 2));

各パズルには以下の情報が含まれている:

  • day: パズルの日付(1-25)
  • eqExprs: 等式制約式の配列(JavaScript式)
  • maskExprs: マスク制約式の配列(JavaScript式)
  • inputFields: 入力フィールドの定義(通常は x

3. パズルを解く

各パズルは、32ビット符号なし整数 x (0 ≤ x < 2^32) を見つける問題。x は以下の条件を満たす必要がある:

  1. すべての eqExprs の式が true を返す
  2. すべての maskExprs の式が true を返す
solve_puzzle.js
const fs = require('fs');
const crypto = require('crypto');

// パズルデータを読み込む
const puzzlesData = JSON.parse(fs.readFileSync('puzzles.json', 'utf8'));

// 制約式を評価する関数を作成
function createEvaluator(expr) {
  return new Function('x', `return ${expr}`);
}

// パズルを解く関数(並列処理対応)
function solvePuzzle(puzzle) {
  console.log(`\n解いている: Day ${puzzle.day}`);

  // 制約式をコンパイル
  const eqEvaluators = puzzle.eqExprs.map(expr => createEvaluator(expr));
  const maskEvaluators = puzzle.maskExprs.map(expr => createEvaluator(expr));

  const maxValue = 4294967295;

  // まずランダムサンプリングで試す(高速)
  console.log('  ランダムサンプリング中...');
  for (let i = 0; i < 10000000; i++) {
    const x = Math.floor(Math.random() * (maxValue + 1));
    const xUint = x >>> 0;

    // すべての制約を満たすかチェック
    const allEqPass = eqEvaluators.every(eval => eval(xUint));
    const allMaskPass = maskEvaluators.every(eval => eval(xUint));

    if (allEqPass && allMaskPass) {
      console.log(`  ✓ 解が見つかりました: ${xUint}`);
      return xUint;
    }
  }

  // ランダムサンプリングで見つからない場合、並列全探索
  console.log('  並列全探索中...(時間がかかる場合があります)');
  const numWorkers = require('os').cpus().length;
  const chunkSize = Math.ceil((maxValue + 1) / numWorkers);

  return new Promise((resolve) => {
    let found = false;
    let completed = 0;

    for (let workerId = 0; workerId < numWorkers; workerId++) {
      const start = workerId * chunkSize;
      const end = Math.min(start + chunkSize, maxValue + 1);

      // 非同期で探索
      setImmediate(() => {
        for (let x = start; x < end && !found; x++) {
          const xUint = x >>> 0;

          // すべての制約を満たすかチェック
          const allEqPass = eqEvaluators.every(eval => eval(xUint));
          const allMaskPass = maskEvaluators.every(eval => eval(xUint));

          if (allEqPass && allMaskPass) {
            if (!found) {
              found = true;
              console.log(`  ✓ 解が見つかりました: ${xUint}`);
              resolve(xUint);
              return;
            }
          }
        }

        completed++;
        if (completed === numWorkers && !found) {
          console.log('  ✗ 解が見つかりませんでした');
          resolve(null);
        }
      });
    }
  });
}

// すべてのパズルを解く(非同期版)
(async () => {
  console.log('パズルを解き始めます...');
  const solutions = {};

  for (const puzzle of puzzlesData) {
    const solution = await solvePuzzle(puzzle);
    if (solution !== null) {
      solutions[puzzle.day] = [solution];
    } else {
      console.error(`Day ${puzzle.day} の解が見つかりませんでした`);
      process.exit(1);
    }
  }

  await generateFlag(solutions);
})();

// Flagを生成する関数
async function generateFlag(solutions) {
  console.log('\nすべてのパズルを解きました!');
  console.log('解答:', solutions);

  // Flagを生成
  console.log('\nFlagを生成中...');

// 各パズルの最初の入力値を4バイトの配列に変換
const encoder = new TextEncoder();
const pepper = 'boolean-advent-2025-pepper';
const pepperBytes = encoder.encode(pepper);

const solutionBytes = new Uint8Array(4 * puzzlesData.length);
puzzlesData.forEach((puzzle, index) => {
  const x = solutions[puzzle.day][0];
  const xUint = Math.trunc(x) >>> 0;
  const offset = 4 * index;
  solutionBytes[offset] = (xUint >>> 24) & 0xff;
  solutionBytes[offset + 1] = (xUint >>> 16) & 0xff;
  solutionBytes[offset + 2] = (xUint >>> 8) & 0xff;
  solutionBytes[offset + 3] = xUint & 0xff;
});

// SHA-256ハッシュを計算
const combined = new Uint8Array(2 * pepperBytes.length + solutionBytes.length);
combined.set(pepperBytes, 0);
combined.set(solutionBytes, pepperBytes.length);
combined.set(pepperBytes, pepperBytes.length + solutionBytes.length);

const hash = crypto.createHash('sha256').update(Buffer.from(combined)).digest();

// Mnを復号化
const MnBase64 = 'uJVY4mJFB6T9yppuCdGFmTW1O5GZ06yw4OTVml4VNOw=';
const Mn = Buffer.from(MnBase64, 'base64');

// ハッシュからキーストリームを生成(元のコードと同じロジック)
async function generateKeystream(hash, length) {
  const keystream = new Uint8Array(length);
  let offset = 0;
  let counter = 0;

  while (offset < length) {
    const counterBytes = new Uint8Array([
      (counter >>> 24) & 0xff,
      (counter >>> 16) & 0xff,
      (counter >>> 8) & 0xff,
      counter & 0xff
    ]);

    const combined = new Uint8Array(hash.length + counterBytes.length);
    combined.set(hash, 0);
    combined.set(counterBytes, hash.length);

    const chunkHash = crypto.createHash('sha256').update(Buffer.from(combined)).digest();
    const chunkLength = Math.min(chunkHash.length, length - offset);

    keystream.set(chunkHash.slice(0, chunkLength), offset);
    offset += chunkLength;
    counter++;
  }

  return keystream;
}

  try {
    const keystream = await generateKeystream(hash, Mn.length);

    // XOR復号化
    const decrypted = new Uint8Array(Mn.length);
    for (let i = 0; i < Mn.length; i++) {
      decrypted[i] = Mn[i] ^ keystream[i];
    }

    // TextDecoderでデコード
    const decoder = new TextDecoder();
    const flag = decoder.decode(decrypted);

    console.log('\n*** FLAG ***');
    console.log(flag);

    // 解答をファイルに保存
    fs.writeFileSync('solutions.json', JSON.stringify(solutions, null, 2));
    console.log('\n解答を solutions.json に保存しました');
  } catch (error) {
    console.error('Flag生成エラー:', error);
  }
}

全25日分の解:

Day 1:  1559119409 (0x5CEE4631)
Day 2:  2281820615 (0x8801D1C7)
Day 3:  3413531028 (0xCB765994)
Day 4:  3436485615 (0xCC9536EF)
Day 5:  2829004470 (0xA89206D6)
Day 6:  1389400533 (0x52D1F015)
Day 7:  1070462966 (0x3FD81576)
Day 8:  2534665693 (0x96C2FD9D)
Day 9:  305368212  (0x12356734)
Day 10: 4270731763 (0xFEC082F3)
Day 11: 1024060755 (0x3D021823)
Day 12: 3944557506 (0xEB56A082)
Day 13: 493359155  (0x1D71B5C3)
Day 14: 2601114477 (0x9B16960D)
Day 15: 2712755675 (0xA1C3471B)
Day 16: 3463169353 (0xCE135D89)
Day 17: 1603909851 (0x5F9DF35B)
Day 18: 3354656626 (0xC808D152)
Day 19: 2291380519 (0x8881AF27)
Day 20: 3228661065 (0xC08AE0E9)
Day 21: 4045939578 (0xF123AB1A)
Day 22: 2428467629 (0x90A71ACD)
Day 23: 3990651856 (0xEDA61370)
Day 24: 2239715624 (0x859CAF68)
Day 25: 3534079978 (0xD2E0B9EA)

4. Flag生成

generate_flag.js
const fs = require('fs');
const crypto = require('crypto');

// パズルデータを読み込む
const puzzlesData = JSON.parse(fs.readFileSync('puzzles.json', 'utf8'));

// 解答
const solutions = {
  1: 1559119409,
  2: 2281820615,
  3: 3413531028,
  4: 3436485615,
  5: 2829004470,
  6: 1389400533,
  7: 1070462966,
  8: 2534665693,
  9: 305368212,
  10: 4270731763,
  11: 1024060755,
  12: 3944557506,
  13: 493359155,
  14: 2601114477,
  15: 2712755675,
  16: 3463169353,
  17: 1603909851,
  18: 3354656626,
  19: 2291380519,
  20: 3228661065,
  21: 4045939578,
  22: 2428467629,
  23: 3990651856,
  24: 2239715624,
  25: 3534079978
};

// Flagを生成する関数
async function generateFlag() {
  console.log('Flagを生成中...');
  console.log(`解答数: ${Object.keys(solutions).length}`);

  // 各パズルの解を4バイトの配列に変換
  const encoder = new TextEncoder();
  const pepper = 'boolean-advent-2025-pepper';
  const pepperBytes = encoder.encode(pepper);

  const solutionBytes = new Uint8Array(4 * puzzlesData.length);
  puzzlesData.forEach((puzzle, index) => {
    const x = solutions[puzzle.day];
    if (x === undefined) {
      throw new Error(`Day ${puzzle.day} の解が見つかりません`);
    }
    const xUint = Math.trunc(x) >>> 0;
    const offset = 4 * index;
    solutionBytes[offset] = (xUint >>> 24) & 0xff;
    solutionBytes[offset + 1] = (xUint >>> 16) & 0xff;
    solutionBytes[offset + 2] = (xUint >>> 8) & 0xff;
    solutionBytes[offset + 3] = xUint & 0xff;
  });

  console.log(`Solution bytes length: ${solutionBytes.length}`);
  console.log(`Pepper bytes length: ${pepperBytes.length}`);

  // SHA-256ハッシュを計算
  // pepper + solutions + pepper の順
  const combined = new Uint8Array(2 * pepperBytes.length + solutionBytes.length);
  combined.set(pepperBytes, 0);
  combined.set(solutionBytes, pepperBytes.length);
  combined.set(pepperBytes, pepperBytes.length + solutionBytes.length);

  // Node.jsのcrypto.createHashを使用
  const hash = crypto.createHash('sha256').update(Buffer.from(combined)).digest();
  console.log(`Hash length: ${hash.length}`);

  // Mnを復号化
  const MnBase64 = 'uJVY4mJFB6T9yppuCdGFmTW1O5GZ06yw4OTVml4VNOw=';
  const Mn = Buffer.from(MnBase64, 'base64');
  console.log(`Encrypted flag length: ${Mn.length}`);

  // ハッシュからキーストリームを生成
  async function generateKeystream(hash, length) {
    const keystream = new Uint8Array(length);
    let offset = 0;
    let counter = 0;

    while (offset < length) {
      const counterBytes = new Uint8Array([
        (counter >>> 24) & 0xff,
        (counter >>> 16) & 0xff,
        (counter >>> 8) & 0xff,
        counter & 0xff
      ]);

      const combined = new Uint8Array(hash.length + counterBytes.length);
      combined.set(hash, 0);
      combined.set(counterBytes, hash.length);

      const chunkHash = crypto.createHash('sha256').update(Buffer.from(combined)).digest();
      const chunkLength = Math.min(chunkHash.length, length - offset);

      keystream.set(chunkHash.slice(0, chunkLength), offset);
      offset += chunkLength;
      counter++;
    }

    return keystream;
  }

  try {
    const keystream = await generateKeystream(hash, Mn.length);

    // XOR復号化
    const decrypted = new Uint8Array(Mn.length);
    for (let i = 0; i < Mn.length; i++) {
      decrypted[i] = Mn[i] ^ keystream[i];
    }

    // TextDecoderでデコード
    const decoder = new TextDecoder();
    const flag = decoder.decode(decrypted);

    console.log('\n*** FLAG ***');
    console.log(flag);
    console.log('*** FLAG ***\n');

    // 解答をファイルに保存
    const solutionsObj = {};
    for (let day = 1; day <= 25; day++) {
      solutionsObj[day] = [solutions[day]];
    }
    fs.writeFileSync('solutions.json', JSON.stringify(solutionsObj, null, 2));
    console.log('解答を solutions.json に保存しました');

    return flag;
  } catch (error) {
    console.error('Flag生成エラー:', error);
    throw error;
  }
}

// 実行
generateFlag().catch(console.error);
Flagを生成中...
解答数: 25
Solution bytes length: 100
Pepper bytes length: 26
Hash length: 32
Encrypted flag length: 32

*** FLAG ***
SECCON{b00l34n_4dv3nt_2025_fl4g}
*** FLAG ***

解答を solutions.json に保存しました

flag: SECCON{b00l34n_4dv3nt_2025_fl4g}

3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?