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.jsのnew 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. 復号処理の実装
- Base64デコード
- RC4復号(計算したキーを使用)
- 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圧縮されています。以下のスクリプトでデコードしする。
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 は以下の条件を満たす必要がある:
- すべての
eqExprsの式がtrueを返す - すべての
maskExprsの式がtrueを返す
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生成
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}