【软件逆向-2025 全国大学生信息安全竞赛初赛 Reverse WriteUp】此文章归类为:软件逆向。
页面中有一个「登录」按钮,点击后会调用 WASM 中的authenticate函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | import { authenticate } from "./build/release.js"; // 初始化 WASM 模块async function initWasm() { const wasmStatus = document.getElementById('wasm-status'); const loginForm = document.getElementById('login-form'); const loginBtn = document.getElementById('login-btn'); const loginSpinner = document.getElementById('login-spinner'); const statusMessage = document.getElementById('status-message'); const errorMessage = document.getElementById('error-message'); const passwordInput = document.getElementById('password'); const togglePasswordBtn = document.getElementById('toggle-password'); try { // 初始化完成 wasmStatus.textContent = 'WASM 已加载'; wasmStatus.classList.add('text-success'); // 切换密码可见性 togglePasswordBtn.addEventListener('click', function() { const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password'; passwordInput.setAttribute('type', type); const icon = this.querySelector('i'); const text = this.querySelector('span'); if (type === 'text') { icon.classList.remove('fa-eye-slash'); icon.classList.add('fa-eye'); text.textContent = '隐藏'; } else { icon.classList.remove('fa-eye'); icon.classList.add('fa-eye-slash'); text.textContent = '显示'; } }); // 登录表单提交处理 loginForm.addEventListener('submit', async function(e) { e.preventDefault(); // 显示加载状态 loginBtn.disabled = true; loginSpinner.classList.remove('hidden'); statusMessage.classList.add('hidden'); try { const username = document.getElementById('username').value; const password = document.getElementById('password').value; // 调用 WASM 中的 authenticate 函数 const authResult = authenticate(username, password); const authData = JSON.parse(authResult); // 模拟发送到服务器 console.log('发送到服务器的数据:', authData); // 模拟服务器响应 simulateServerRequest(authData) .then(response => { if (response.success) { // 登录成功 alert('登录成功!'); } else { // 登录失败 showError(response.message || '登录失败,请重试'); } }) .catch(error => { console.error('登录错误:', error); showError('网络错误,请稍后重试'); }) .finally(() => { // 恢复按钮状态 loginBtn.disabled = false; loginSpinner.classList.add('hidden'); }); } catch (error) { console.error('WASM 处理错误:', error); showError('内部错误,请联系管理员'); // 恢复按钮状态 loginBtn.disabled = false; loginSpinner.classList.remove('hidden'); } }); // 显示错误消息 function showError(message) { errorMessage.textContent = message; statusMessage.classList.remove('hidden'); // 添加动画效果 const errorBox = statusMessage.querySelector('div'); errorBox.classList.add('animate-shake'); setTimeout(() => { errorBox.classList.remove('animate-shake'); }, 500); } // 模拟服务器请求 function simulateServerRequest(data) { return new Promise(resolve => { // 模拟网络延迟 setTimeout(() => { // 实际应用中这里应该是真实的 API 请求 // 这里仅作演示,使用本地判断 const check = CryptoJS.MD5(JSON.stringify(data)).toString(CryptoJS.enc.Hex); if (check.startsWith("ccaf33e3512e31f3")){ resolve({ success: true }); }else{ resolve({ success: false }); } }, 1000); }); } } catch (error) { console.error('WASM 加载失败:', error); wasmStatus.textContent = 'WASM 加载失败'; wasmStatus.classList.add('text-danger'); // 禁用登录按钮 loginBtn.disabled = true; loginBtn.classList.add('bg-neutral-400'); loginBtn.classList.remove('bg-primary', 'hover:bg-primary/90'); } } // 页面加载完成后初始化 WASM window.addEventListener('load', initWasm); |
前端会计算一个 check 值:
1 2 | check = MD5(JSON.stringify(authData));// 要求:check 必须以 "ccaf33e3512e31f3" 开头 |
因此要 枚举时间戳,找到一个能满足该前缀要求的 check。
最终提交格式:
1 | flag{正确的check值} |
使用 PowerShell 从 release.wasm.map 中提取 TypeScript 源码,这些源码原本就是有注释的:
1 2 3 4 5 6 7 | $map = Get-Content -Raw -Path .\[release.wasm.map](c21K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4u0W2L8r3g2S2M7$3g2Q4x3X3g2%4j5i4y4E0i4K6u0W2L8h3q4H3i4K6t1&6 | ConvertFrom-Json$idx = $map.sources.IndexOf('assembly/index.ts')$map.sourcesContent[$idx] | Out-File -Encoding ascii .\assembly_index.ts$map = Get-Content -Raw -Path .\[release.wasm.map](48dK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4u0W2L8r3g2S2M7$3g2Q4x3X3g2%4j5i4y4E0i4K6u0W2L8h3q4H3i4K6t1&6 | ConvertFrom-Json$idx = $map.sources.IndexOf('assembly/base64.ts')$map.sourcesContent[$idx] | Out-File -Encoding ascii .\assembly_base64.ts |
在 assembly_index.ts 中可以看到核心认证逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | export function authenticate(username: string, password: string): string { // 1. Base64编码密码 const encodedPassword = encode(stringToUint8Array(password)); //console.log(encodedPassword); // 2. 获取当前时间戳(毫秒) const timestamp = Date.now().toString(); //console.log(timestamp); // 3. 构建原始JSON消息 const message = `{"username":"${username}","password":"${encodedPassword}"}`; //console.log(message); // 4. 使用HMAC-SHA256签名 const signature = signMessage(message, timestamp); //console.log(signature); // 5. 构建最终JSON消息 const finalMessage = ` {"username":"${username}","password":"${encodedPassword}","signature":"${signature}"}`; return finalMessage; //return "ok";} |
签名函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | function signMessage(message: string, secret: string): string { const messageBytes = String.UTF8.encode(message); const secretBytes = String.UTF8.encode(secret); /** const messageBytesPtr = changetype<usize>(messageBytes); const secretBytesPtr = changetype<usize>(secretBytes); const hashInput = new ArrayBuffer(messageBytes.byteLength + secretBytes.byteLength); const hashInputPtr = changetype<usize>(hashInput); memory.copy(hashInputPtr, messageBytesPtr, messageBytes.byteLength); memory.copy(hashInputPtr + messageBytes.byteLength, secretBytesPtr, secretBytes.byteLength); const signatureBytes = new ArrayBuffer(32); const signatureBytesPtr = changetype<usize>(signatureBytes); init(); update(hashInputPtr, hashInput.byteLength); final(signatureBytesPtr) */ const signatureBytes = hmacSHA256(secretBytes,messageBytes); return encode(ArrayBufferToUint8Array(signatureBytes));} |
自定义 Base64:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | // @ts-ignore: decorator@lazy const PADCHAR = "=";// @ts-ignore: decorator@lazy// const ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; //这是标准base64映射表const ALPHA = "NhR4UJ+z5qFGiTCaAIDYwZ0dLl6PEXKgostxuMv8rHBp3n9emjQf1cWb2/VkS7yO";//这是本题中使用的新表 /** * Encode Uint8Array as a base64 string. * @param bytes Byte array of type Uint8Array. */ export function encode(bytes: Uint8Array): string { let i: i32, b10: u32; const extrabytes = (bytes.length % 3); let imax = bytes.length - extrabytes; const len = ((bytes.length / 3) as i32) * 4 + (extrabytes == 0 ? 0 : 4); let x = changetype<string>(__new(<usize>(len << 1), idof<string>())); if (bytes.length == 0) { return ""; } let ptr = changetype<usize>(x) - 2; for (i = 0; i < imax; i += 3) { b10 = ((bytes[i] as u32) << 16) | ((bytes[i + 1] as u32) << 8) | (bytes[i + 2] as u32); store<u16>(ptr+=2, (ALPHA.charCodeAt(b10 >> 18) as u16)); store<u16>(ptr+=2, (ALPHA.charCodeAt(((b10 >> 12) & 63)) as u16)); store<u16>(ptr+=2, (ALPHA.charCodeAt(((b10 >> 6) & 63)) as u16)); store<u16>(ptr+=2, (ALPHA.charCodeAt((b10 & 63)) as u16)); } switch (bytes.length - imax) { case 1: b10 = (bytes[i] as u32) << 16; store<u16>(ptr+=2, ((ALPHA.charCodeAt(b10 >> 18)) as u16)); store<u16>(ptr+=2, ((ALPHA.charCodeAt((b10 >> 12) & 63)) as u16)); store<u16>(ptr+=2, ((PADCHAR.charCodeAt(0)) as u16)); store<u16>(ptr+=2, ((PADCHAR.charCodeAt(0)) as u16)); break; case 2: b10 = ((bytes[i] as u32) << 16) | ((bytes[i + 1] as u32) << 8); store<u16>(ptr+=2, ((ALPHA.charCodeAt(b10 >> 18)) as u16)); store<u16>(ptr+=2, ((ALPHA.charCodeAt((b10 >> 12) & 63)) as u16)); store<u16>(ptr+=2, ((ALPHA.charCodeAt((b10 >> 6) & 63)) as u16)); store<u16>(ptr+=2, ((PADCHAR.charCodeAt(0)) as u16)); break; } return x; } |
自定义 HMAC-SHA256 关键点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | function hmacSHA256(key: ArrayBuffer, message: ArrayBuffer): ArrayBuffer { const blockSize = 64; // SHA256 ????????64 ??? // ?????? const keyPtr = changetype<usize>(key); const paddedKey = new ArrayBuffer(blockSize); const paddedKeyPtr = changetype<usize>(paddedKey); if (key.byteLength > blockSize) { // ????????????????????????????????n init(); update(keyPtr, key.byteLength); final(paddedKeyPtr); }else{ // ???????????? memory.copy(paddedKeyPtr, keyPtr, key.byteLength); fill(paddedKeyPtr + key.byteLength, 0, blockSize - key.byteLength) } //console.log(ArrayBufferToUint8Array(paddedKey).toString()); // ??? ipad ??opad const ipad = new ArrayBuffer(blockSize); const opad = new ArrayBuffer(blockSize); const ipadPtr = changetype<usize>(ipad); const opadPtr = changetype<usize>(opad); for (let i = 0; i < blockSize; i++) { store<u8>(ipadPtr + i , load<u8>(paddedKeyPtr + i) ^ 0x76); store<u8>(opadPtr + i , load<u8>(paddedKeyPtr + i) ^ 0x3C); } //console.log(ArrayBufferToUint8Array(ipad).toString()); //console.log(ArrayBufferToUint8Array(opad).toString()); // ??? innerHash const innerInput = new ArrayBuffer(ipad.byteLength + message.byteLength); const innerInputPtr = changetype<usize>(innerInput); const messagePtr = changetype<usize>(message) memory.copy(innerInputPtr, ipadPtr, ipad.byteLength); memory.copy(innerInputPtr + ipad.byteLength, messagePtr, message.byteLength); //console.log(ArrayBufferToUint8Array(innerInput).toString()); init(); update(innerInputPtr,innerInput.byteLength); //update(ipadPtr,ipad.byteLength); //update(messagePtr,message.byteLength); const innerHash = new ArrayBuffer(32); const innerHashPtr = changetype<usize>(innerHash); final(innerHashPtr); //console.log(ArrayBufferToUint8Array(innerHash).toString()); // ??? outerHash const outerInput = new ArrayBuffer(opad.byteLength + innerHash.byteLength); const outerInputPtr = changetype<usize>(outerInput); memory.copy(outerInputPtr, innerHashPtr, innerHash.byteLength); memory.copy(outerInputPtr + innerHash.byteLength, opadPtr, opad.byteLength); //console.log(ArrayBufferToUint8Array(outerInput).toString()); init(); update(outerInputPtr,outerInput.byteLength); //update(opadPtr,opad.byteLength); //update(innerHashPtr,innerHash.byteLength); const outerHash = new ArrayBuffer(32); const outerHashPtr = changetype<usize>(outerHash); final(outerHashPtr); //console.log(ArrayBufferToUint8Array(outerHash).toString()); return outerHash;} |
总结不同点:
HMAC 中的 ipad / opad 常量为 0x76 和 0x3C,不是标准的 0x36 / 0x5C。HMAC key 为字符串化的时间戳 Date.now().toString()。HTML 最后一行注释:
1 | <!-- 测试账号 admin 测试密码 admin --> |
所以可以确定:
1 2 | username = "admin"password = "admin" |
对每一个候选时间戳:
用自定义 Base64 编码密码 admin。
构造 message:
{"username":"admin","password":"编码后的密码"}
使用时间戳字符串做key,按**自定义 HMAC-SHA256** 计算签名。
对 HMAC 结果再做自定义 Base64,得到 signature。
构造最终 JSON:
{"username":"admin","password":"...","signature":"..."}
计算 check = MD5(JSON.stringify(finalJSON))。
判断 check 是否以 ccaf33e3512e31f3 开头。
根据文件时间(题目提示「爆肝到周一凌晨」对,题目描述是这样的):
release.js:2025/12/22 00:29release.wasm:2025/12/22 00:57index.html:2025/12/22 01:06推测时间戳大致落在 2025-12-22 00:00:00 ~ 02:00:00(UTC+8)之间,直接在该区间内暴力搜索即可。
Node.js 脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | const crypto = require('crypto');// 自定义 Base64 字母表const ALPHA = "NhR4UJ+z5qFGiTCaAIDYwZ0dLl6PEXKgostxuMv8rHBp3n9emjQf1cWb2/VkS7yO";function base64Custom(buf) { let out = ''; let i = 0; for (; i + 2 < buf.length; i += 3) { const b10 = (buf[i] << 16) | (buf[i + 1] << 8) | buf[i + 2]; out += ALPHA[(b10 >> 18) & 63]; out += ALPHA[(b10 >> 12) & 63]; out += ALPHA[(b10 >> 6) & 63]; out += ALPHA[b10 & 63]; } const rem = buf.length - i; if (rem === 1) { const b10 = buf[i] << 16; out += ALPHA[(b10 >> 18) & 63]; out += ALPHA[(b10 >> 12) & 63]; out += '='; out += '='; } else if (rem === 2) { const b10 = (buf[i] << 16) | (buf[i + 1] << 8); out += ALPHA[(b10 >> 18) & 63]; out += ALPHA[(b10 >> 12) & 63]; out += ALPHA[(b10 >> 6) & 63]; out += '='; } return out;}function sha256(buf) { return crypto.createHash('sha256').update(buf).digest();}// 自定义 HMAC (ipad/opad 常量不同)function hmacSHA256(key, message) { const blockSize = 64; let paddedKey = Buffer.alloc(blockSize, 0); if (key.length > blockSize) { sha256(key).copy(paddedKey, 0); } else { key.copy(paddedKey, 0); } const ipad = Buffer.alloc(blockSize); const opad = Buffer.alloc(blockSize); for (let i = 0; i < blockSize; i++) { const b = paddedKey[i]; ipad[i] = b ^ 0x76; opad[i] = b ^ 0x3C; } const innerHash = sha256(Buffer.concat([ipad, message])); const outerHash = sha256(Buffer.concat([innerHash, opad])); return outerHash;}const username = 'admin';const password = 'admin';const encodedPassword = base64Custom(Buffer.from(password, 'utf8'));const message = `{"username":"${username}","password":"${encodedPassword}"}`;const messageBytes = Buffer.from(message, 'utf8');const prefix = 'ccaf33e3512e31f3';const start = Date.parse('2025-12-22T00:00:00+08:00');const end = Date.parse('2025-12-22T02:00:00+08:00');for (let ts = start; ts < end; ts++) { const key = Buffer.from(String(ts), 'utf8'); const sigBytes = hmacSHA256(key, messageBytes); const signature = base64Custom(sigBytes); const finalMessage = `{"username":"${username}","password":"${encodedPassword}","signature":"${signature}"}`; const check = crypto.createHash('md5').update(finalMessage).digest('hex'); if (check.startsWith(prefix)) { console.log('FOUND', ts, check); break; }} |
爆破结果:
1 2 | 时间戳:1766334550699check:ccaf33e3512e31f36228f0b97ccbc8f1 |
1 | flag{ccaf33e3512e31f36228f0b97ccbc8f1} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | <!-- Brute-force timestamp helper (CTF) --> <div class="mt-6 text-center text-sm text-neutral-500"> <button id="bruteforce-btn" class="mt-2 px-4 py-2 rounded-lg bg-secondary text-white">?????</button> <div id="bruteforce-status" class="mt-2 text-neutral-400">???</div> </div><script type="module"> import { authenticate } from "./build/release.js"; const bfBtn = document.getElementById('bruteforce-btn'); const bfStatus = document.getElementById('bruteforce-status'); bfBtn.addEventListener('click', async () => { const prefix = "ccaf33e3512e31f3"; const username = "admin"; const password = "admin"; const start = Date.parse("2025-12-22T00:00:00+08:00"); const end = Date.parse("2025-12-22T02:00:00+08:00"); const oldNow = Date.now; bfStatus.textContent = "???..."; bfBtn.disabled = true; for (let ts = start; ts < end; ts++) { Date.now = () => ts; const authResult = authenticate(username, password); const data = JSON.parse(authResult); const check = CryptoJS.MD5(JSON.stringify(data)).toString(CryptoJS.enc.Hex); if (check.startsWith(prefix)) { bfStatus.textContent = `FOUND: ${ts} | ${check}`; Date.now = oldNow; bfBtn.disabled = false; return; } } Date.now = oldNow; bfStatus.textContent = "???"; bfBtn.disabled = false; });</script> |

1 | 利用项目直接解包: [d5bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6s2c8q4u0q4g2r3!0G2L8s2y4Q4x3V1k6Y4k6s2y4V1k6h3y4G2L8i4m8Q4x3V1k6J5k6h3I4W2j5i4y4W2M7#2)9J5c8Y4c8S2k6#2)9J5c8Y4j5J5i4K6u0W2y4q4)9J5k6e0m8Q4y4f1c8Q4x3U0S2Z5N6s2c8H3M7#2)9K6b7g2)9J5c8W2)9J5c8X3N6A6N6r3S2#2j5W2)9J5k6h3y4G2L8g2)9J5c8V1N6p5f1V1g2f1L8$3!0D9M7#2)9J5c8X3N6V1M7$3c8W2j5$3!0E0M7q4)9J5c8Y4u0W2L8r3g2S2M7$3g2K6i4K6u0r3N6r3q4Y4i4K6u0r3N6U0u0Q4x3X3f1@1i4K6u0W2x3q4)9J5z5b7`.`. //Godot引擎可以解包源码 |
在 scripts/flag.gd 中初始化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | extends CenterContainer@onready var flagTextEdit: Node = $PanelContainer / VBoxContainer / FlagTextEdit@onready var label2: Node = $PanelContainer / VBoxContainer / Label2static var key = "FanAglFanAglOoO!"var data = ""func _on_ready() -> void : Flag.hide()func get_key() -> String: return keyfunc submit() -> void : data = flagTextEdit.text var aes = AESContext.new() aes.start(AESContext.MODE_ECB_ENCRYPT, key.to_utf8_buffer()) var encrypted = aes.update(data.to_utf8_buffer()) aes.finish() if encrypted.hex_encode() == "d458af702a680ae4d089ce32fc39945d": label2.show() else: label2.hide()func back() -> void : get_tree().change_scene_to_file("res://scenes/menu.tscn") |
仅依赖这里的 key 还不足以得到最终 flag,因为游戏过程中 key 会被修改。
在 scripts/game_manager.gd:
1 2 3 4 5 6 7 8 9 | @onready var fan = $"../Fan"var score = 0func add_point(): score += 1 if score == 1: Flag.key = Flag.key.replace("A", "B") fan.visible = true |
也就是说,当获得第一个金币时:
1 2 3 | FanAglFanAglOoO! -> F an A gl F an A gl OoO! B B (A→B)最终 key = FbnBglFbnBglOoO! |
再次回到 flag.gd 的加密逻辑(AES-ECB):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | func submit() -> void : data = flagTextEdit.text var aes = AESContext.new() aes.start(AESContext.MODE_ECB_ENCRYPT, key.to_utf8_buffer()) var encrypted = aes.update(data.to_utf8_buffer()) aes.finish() if encrypted.hex_encode() == "d458af702a680ae4d089ce32fc39945d": label2.show() else: label2.hide()func back() -> void : get_tree().change_scene_to_file("res://scenes/menu.tscn") |
**目标:**找到一个输入,对 修改后的 key (FbnBglFbnBglOoO!)做 AES-ECB 加密,密文为
1 | d458af702a680ae4d089ce32fc39945d |
可以直接在本地用 Python 复现 Godot 的 AES 行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | import sysimport iosys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')from Crypto.Cipher import AESfrom Crypto.Util.Padding import pad, unpad# 初始keyinitial_key = "FanAglFanAglOoO!"# 得分为1后,key中的'A'被替换为'B'final_key = initial_key.replace("A", "B")print(f"初始 key: {initial_key}")print(f"最终 key: {final_key}")# 目标加密结果target_encrypted_hex = "d458af702a680ae4d089ce32fc39945d"target_encrypted = bytes.fromhex(target_encrypted_hex)print(f"\n目标密文 (hex): {target_encrypted_hex}")print(f"目标密文 (bytes): {target_encrypted}")print(f"密文长度: {len(target_encrypted)} bytes")# 使用AES-ECB模式解密try: # 创建AES cipher对象 (ECB模式) cipher = AES.new(final_key.encode('utf-8'), AES.MODE_ECB) # 解密 decrypted = cipher.decrypt(target_encrypted) # 尝试去除padding try: flag = unpad(decrypted, AES.block_size).decode('utf-8') print(f"\n[OK] 成功解密 (带padding): {flag}") except: # 如果去除padding失败,直接解码 flag = decrypted.decode('utf-8').rstrip('\x00') print(f"\n[OK] 成功解密 (无padding): {flag}") # 验证 - 重新加密看是否匹配 cipher_verify = AES.new(final_key.encode('utf-8'), AES.MODE_ECB) # Godot的AES会自动padding到16字节的倍数 flag_padded = flag.encode('utf-8') if len(flag_padded) % 16 != 0: flag_padded = pad(flag_padded, AES.block_size) encrypted_verify = cipher_verify.encrypt(flag_padded) if encrypted_verify.hex() == target_encrypted_hex: print(f"[OK] 验证成功!加密结果匹配") else: print(f"[FAIL] 验证失败") print(f" 预期: {target_encrypted_hex}") print(f" 实际: {encrypted_verify.hex()}") print(f"\n{'='*50}") print(f"FLAG: {flag}") print(f"{'='*50}")except Exception as e: print(f"\n[FAIL] 解密失败: {e}") import traceback traceback.print_exc() |
1 | flag{wOW~youAregrEaT!} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | int __fastcall main(int argc, const char **argv, const char **envp){ __int64 v3; // rax __int64 v4; // rax _BYTE v6[32]; // [rsp+0h] [rbp-50h] BYREF _BYTE v7[12]; // [rsp+20h] [rbp-30h] BYREF int v8; // [rsp+2Ch] [rbp-24h] BYREF char v9; // [rsp+33h] [rbp-1Dh] int i; // [rsp+34h] [rbp-1Ch] unsigned __int64 v11; // [rsp+38h] [rbp-18h] std::string::basic_string(v6, argv, envp); std::operator<<<std::char_traits<char>>(&std::cout, "Enter password: "); std::getline<char,std::char_traits<char>,std::allocator<char>>(&std::cin, v6); if ( (unsigned __int8)std::operator!=<char>(v6, "V3ryStr0ngp@ssw0rd") ) { v3 = std::operator<<<std::char_traits<char>>(&std::cout, "Wrong password!"); std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>); } else { std::operator<<<std::char_traits<char>>(&std::cout, "flag{"); std::ostream::flush((std::ostream *)&std::cout); v11 = 1; for ( i = 0; i <= 31; ++i ) { v9 = f(v11); std::operator<<<std::char_traits<char>>(&std::cout, (unsigned int)v9); std::ostream::flush((std::ostream *)&std::cout); if ( i == 7 || i == 12 || i == 17 || i == 22 ) { std::operator<<<std::char_traits<char>>(&std::cout, "-"); std::ostream::flush((std::ostream *)&std::cout); } v11 *= 8LL; v11 += i + 64; v8 = 1; std::chrono::duration<long,std::ratio<1l,1l>>::duration<int,void>(v7, &v8); std::this_thread::sleep_for<long,std::ratio<1l,1l>>(v7); } v4 = std::operator<<<std::char_traits<char>>(&std::cout, "}"); std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>); } std::string::~string(v6); return 0;}//c++看起来太乱了 以下代码会更清晰if (input != "V3ryStr0ngp@ssw0rd") { print("Wrong password!");} else { print("flag{"); v11 = 1; for (i = 0; i <= 31; ++i) { ch = f(v11); print(ch); if (i == 7 || i == 12 || i == 17 || i == 22) print("-"); v11 = v11 * 8 + i + 64; // 64-bit unsigned sleep(1); } print("}");}// fv5 = 0; v4 = 1;for (i = 0; i < a1; ++i) { v2 = v4; v4 = (v5 + v4) & 0xF; // modulo 16 v5 = v2;}return K[v5]; |
字符生成函数 f:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | __int64 __fastcall f(unsigned __int64 a1){ __int64 v2; // [rsp+10h] [rbp-20h] unsigned __int64 i; // [rsp+18h] [rbp-18h] __int64 v4; // [rsp+20h] [rbp-10h] __int64 v5; // [rsp+28h] [rbp-8h] v5 = 0; v4 = 1; for ( i = 0; i < a1; ++i ) { v2 = v4; v4 = ((_BYTE)v5 + (_BYTE)v4) & 0xF; v5 = v2; } return *(unsigned __int8 *)std::string::operator[](&K, v5);} |
可以看成 Fibonacci 序列模 16 后索引一个 16 字节表,再配合特定递推生成的 v。
Fibonacci 序列模 16 是周期性的,周期为 24。预先把 F(n) mod 16 的 24 项打表即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | K = "012ab9c3478d56ef"# 预计算 F(n) mod 16 的 24 周期F_MOD16 = [0, 1]for _ in range(22): # total 24 F_MOD16.append((F_MOD16[-1] + F_MOD16[-2]) & 0xF)def f(a1: int) -> str: idx = F_MOD16[a1 % 24] return K[idx]def solve(): out = ["flag{"] v11 = 1 for i in range(32): out.append(f(v11)) if i in (7, 12, 17, 22): out.append("-") v11 = (v11 * 8 + i + 64) & 0xFFFFFFFFFFFFFFFF out.append("}") return "".join(out)if __name__ == "__main__": print(solve()) |
运行结果:
1 | flag{10632674-1d219-09f29-14769-f60219a24} |
kworker 可执行文件tcp.pcaprun.sh1 | GoReSym.exe kworker > gorelog.txt 2>&1 //控制台打印不下 有800kb.... |
从生成的日志中可以定位 main.main:
{
"Start": 6656832, //10--->16即可
"Start": 6656832,
"End": 6657248,
"PackageName": "main",
"FullName": "main.main"
}
在逆向过程中,iupHvc2q4 包含核心逻辑(而且主要部分都在混淆过函数名的函数下,那种一眼就能看到干什么的函数都显得不重要了):
1 2 3 4 5 6 7 8 | main.main = 0x659340// iupHvc2q4.* 是主业务包// Run : 驱动会话(newproc + chanrecv)// fuzkMtvzPreC : 收包解析链:recv → 校验 → AES‑GCM → zstd → protobuf// d4k0A9zcOh : 发包链的一部分// OnJCbKpp : AES‑GCM 校验 / 解密// HaNDRB_IhET : 构造自定义 magic |
goroutine 调度iupHvc2q4.(*H1eV17y).Run 主要逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | //iupHvc2q4.(*H1eV17y).Run (0x6571E0) _QWORD *sub_6571E0() { ... if ((*(v0) || !Connect()) && !o6wghH0urlbA()) { // goroutine1: fuzkMtvzPreC v2 = runtime_newobject(); *v2 = sub_657360; // Run.gowrap1 -> fuzkMtvzPreC v2[1] = result; sub_4511A0(); // newproc // goroutine2: KeepAlive v5 = runtime_newobject(); *v5 = sub_657300; // Run.gowrap2 -> KeepAlive v5[1] = result; sub_4511A0(); // newproc (*(void (**)(void))(result[4] + 32LL))(); // 启动/唤醒某通道 runtime_chanrecv1_0(); // 等待 } return result; }//下是手写的伪代码if ((connect_ok || already_connected) && !o6wghH0urlbA()) { // goroutine 1:收包处理 fuzkMtvzPreC go fuzkMtvzPreC(session) // goroutine 2:心跳/keepalive go KeepAlive(session) // 等待会话结束 chan_recv(done_chan)} |
fuzkMtvzPreC1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | //iupHvc2q4.(*H1eV17y).fuzkMtvzPreC (0x657420) __int64 __fastcall sub_657420() { ... v41 = sub_6584A0(); // 生成 magic (ET3RNUMX) while (1) { (*(void (**)(void))(v54[4] + 32))(); // 等待/触发接收 result = runtime_selectnbrecv(); // 非阻塞接收 if ((_BYTE)result) break; v50 = runtime_makeslice(); // 分配接收 buffer sub_4B3F40(); // 读数据到 buffer if (!err) { v30 = sub_658FA0(); // memcmp/magic检查 if (ok) { v49 = runtime_makeslice(); // 读长度字段 sub_4B3F40(); // 继续读 if (!err) { v34 = _byteswap_ulong(*v49); // big-endian length v48 = runtime_makeslice(); // 读 payload sub_4B3F40(); // 继续读 v33 = v34 + 12; // payload长度 + nonce长度 v20 = runtime_makeslice(); if (v48 != v20 + ((-v34 >> 63) & 0xC)) sub_482C20(); // copy payload *(retval_658DE0 *)v30._r0 = sub_658DE0(); // OnJCbKpp: 校验+AES-GCM if (!decrypt_err) { v52 = runtime_newobject(); if (!sub_538B80(&off_74C2A0, v52, ...)) // protobuf 反序列化 sub_657860(); // 处理消息 } } } } } return result; }//下是手写的伪代码magic = HaNDRB_IhET(); // "ET3RNUMX"while (true) { wait_readable(); // 读取并检查 magic read_exact(sock, buf, 8); if (!check_magic(buf, magic)) continue; // 读取长度(4 字节,大端) read_exact(sock, &len_be, 4); uint32_t len = bswap32(len_be); // 读取 payload read_exact(sock, cipher_buf, len); // 尝试 AES‑GCM 解密+校验 if (!OnJCbKpp(cipher_buf, len, &plain_buf)) continue; // zstd 解压 + protobuf 反序列化 if (!protobuf_unmarshal(plain_buf, &msg)) continue; handle_msg(session, &msg);} |
HaNDRB_IhET函数 HaNDRB_IhET:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | //iupHvc2q4.HaNDRB_IhET (0x6584A0) __int64 __fastcall sub_6584A0() { ... result = runtime_makeslice(); for (i = 0; i < 8; ++i) *(byte*)(result+i) = byte_98F158[i] ^ 0x99; return result; }//下是手写的伪代码 byte_98F158 XOR 0x99 = ET3RNUMX → 用于帧头 magic。 result = make([]byte, 8);for i in range(8) { result[i] = byte_98F158[i] ^ 0x99;} |
byte_98F158 内容:
1 | DC CD AA CB D7 CC D4 C1 |
与 0x99 异或后的 ASCII:
1 | ET3RNUMX |
这与 pcap中每个应用层负载开头的 8 字节完全一致。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | //iupHvc2q4.OnJCbKpp (0x658DE0) retval_658DE0 sub_658DE0() { ... if (len >= 12 && (magic = sub_6584A0(), memcmp_ok = sub_658FA0(magic, ...)) && (v7 = bswap32(*(u32*)(buf+8)) + 12, len >= v7)) { v8 = off_99B220; // AES key 结构 v9 = qword_99B228; // AES key 长度 sub_6586C0(); // AES 初始化 if (!v8) sub_658A60(...); // AES-GCM 解密/验证 } else { sub_502B00(); // 错误路径 } return result; } |
OnJCbKpp 中调用 AES 相关函数,密钥位于只读段:
1 2 | 78 66 71 47 63 56 6a 72 4f 57 70 35 74 55 47 4350 46 51 71 34 34 38 6e 50 44 6a 49 4c 54 65 37 |
转为 ASCII:
1 | xfqGcVjrOWp5tUGCPFQq448nPDjILTe7 |
这是一条 32 字节 AES 密钥,用于 AES‑GCM。
结合逆向代码和流量分析,可得每条消息格式:
1 2 3 4 | [8 字节] 魔数 : "ET3RNUMX"[4 字节] 长度 (BE) : n[12 字节] GCM nonce[n-12 字节] GCM 密文+认证标签 |
解密后 payload 为 zstd 压缩的 protobuf。
大致流程:
tcp.pcap 解析以太网 / IP / TCP。(src_ip, src_port, dst_ip, dst_port) 聚合重组 TCP 流。ET3RNUMX,按长度字段切包。xfqGcVjrOWp5tUGCPFQq448nPDjILTe7 调用 AES‑GCM 解密。zstd解压缩明文。base32 字符串并解码。脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | import structfrom pathlib import Pathfrom collections import defaultdictfrom cryptography.hazmat.primitives.ciphers.aead import AESGCMimport zstandard as zstdimport base64pcap_path = Path(r"C:\Users\Euarno\Desktop\eternum_665a4bf7903bb6ea0b05c1fa8a447f4e\tcp.pcap")key = b"xfqGcVjrOWp5tUGCPFQq448nPDjILTe7"aesgcm = AESGCM(key)def parse_pcap(path): data = path.read_bytes() magic = struct.unpack_from('<I', data, 0)[0] endian = '<' if magic == 0xa1b2c3d4 else '>' offset = 24 pkts = [] while offset + 16 <= len(data): _, _, incl_len, _ = struct.unpack_from(endian + 'IIII', data, offset) offset += 16 if offset + incl_len > len(data): break pkts.append(data[offset:offset+incl_len]) offset += incl_len return pktsdef parse_ether(pkt): if len(pkt) < 14: return None eth_type = struct.unpack('!H', pkt[12:14])[0] return eth_type, pkt[14:]def parse_ipv4(pkt): if len(pkt) < 20: return None ver_ihl = pkt[0] ihl = (ver_ihl & 0x0F) * 4 if ver_ihl >> 4 != 4 or len(pkt) < ihl: return None total_len = struct.unpack('!H', pkt[2:4])[0] proto = pkt[9] src = pkt[12:16] dst = pkt[16:20] payload = pkt[ihl:total_len] return proto, src, dst, payloaddef parse_tcp(pkt): if len(pkt) < 20: return None src_port, dst_port = struct.unpack('!HH', pkt[:4]) seq = struct.unpack('!I', pkt[4:8])[0] data_offset = (pkt[12] >> 4) * 4 if len(pkt) < data_offset: return None payload = pkt[data_offset:] return src_port, dst_port, seq, payloaddef ip_str(b): return '.'.join(str(x) for x in b)pkts = parse_pcap(pcap_path)flows = defaultdict(list)for pkt in pkts: eth = parse_ether(pkt) if not eth: continue eth_type, payload = eth if eth_type != 0x0800: continue ipv4 = parse_ipv4(payload) if not ipv4: continue proto, src, dst, ip_payload = ipv4 if proto != 6: continue tcp = parse_tcp(ip_payload) if not tcp: continue sp, dp, seq, tcp_payload = tcp if not tcp_payload: continue keyf = (ip_str(src), sp, ip_str(dst), dp) flows[keyf].append((seq, tcp_payload))streams = {}for keyf, segs in flows.items(): segs_sorted = sorted(segs, key=lambda x: x[0]) data = b"" cur = None for seq, payload in segs_sorted: if cur is None: cur = seq data += payload cur += len(payload) continue if seq < cur: overlap = cur - seq if overlap < len(payload): data += payload[overlap:] cur += len(payload) - overlap elif seq == cur: data += payload cur += len(payload) else: data += b"\x00" * (seq - cur) data += payload cur = seq + len(payload) streams[keyf] = dataMAGIC = b"ET3RNUMX"def parse_messages(data): msgs = [] i = 0 while i + 12 <= len(data): if data[i:i+8] != MAGIC: j = data.find(MAGIC, i+1) if j == -1: break i = j continue length = struct.unpack('>I', data[i+8:i+12])[0] total = 12 + length if i + total > len(data): break payload = data[i+12:i+total] msgs.append(payload) i += total return msgszctx = zstd.ZstdDecompressor()for flow, data in streams.items(): msgs = parse_messages(data) for payload in msgs: nonce = payload[:12] ct = payload[12:] pt = aesgcm.decrypt(nonce, ct, None) try: decomp = zctx.decompress(pt, max_output_size=100000) except Exception: continue if b"MZWGCZ" in decomp: print(decomp) s = decomp.split(b"\n")[0].split(b"\x12")[-1] print(base64.b32decode(s)) |
查找 base32 字符串
1 2 3 4 5 6 7 | if b"MZWGCZ" in decomp:print(decomp)s = decomp.split(b"n")[0].split(b"x12")[-1]print(base64.b32decode(s)) |
1 | MZWGCZ33MI3WGNJYG4YDALJSMIYDCLJUMRSDILJYGUZDMLLBGRQTIN3BGY2WCMLBHF6QU=== |
将其以 base32 解码即可得到 flag:
1 | flag{b7c58700-2b01-4dd4-8526-a4a47a65a1a9} |
更多【软件逆向-2025 全国大学生信息安全竞赛初赛 Reverse WriteUp】相关视频教程:www.yxfzedu.com