【软件逆向-第十九届全国大学生信息安全竞赛初赛 Reverse部分(么有vm那题)】此文章归类为:软件逆向。

页面里有一个“登录”按钮,点击会调用 WASM 里的 authenticate 函数。
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 的值:
check = MD5(JSON.stringify(authData))
check 必须以 "ccaf33e3512e31f3" 开头
因此我们需要 找到一个正确的时间戳,让这个 check 符合前缀要求。
最终提交格式:
flag{正确的check值}通过一下powerShell命令,提取assembly_index.ts assembly_base64.ts
$map = Get-Content -Raw -Path .\release.wasm.map | 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 | ConvertFrom-Json
$idx = $map.sources.IndexOf('assembly/base64.ts')
$map.sourcesContent[$idx] | Out-File -Encoding ascii .\assembly_base64.ts
在 assembly_index.ts 中看到:
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";
}
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));
}
// adapted from 108K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4y4@1i4K6u0W2k6$3W2@1K9s2g2T1i4K6u0W2j5$3!0E0i4K6u0r3d9Y4g2K6P5X3y4*7j5h3E0Q4x3V1j5$3x3$3f1$3k6o6W2W2x3o6q4V1k6h3y4U0z5o6f1H3k6r3f1H3x3H3`.`.
/**
* base64 encoding/decoding
*/
// @ts-ignore: decorator
@lazy
const PADCHAR = "=";
// @ts-ignore: decorator
@lazy
// const ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
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;
}
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;
}
在 assembly_base64.ts 里定义了一个字母表:
ALPHA = "NhR4UJ+z5qFGiTCaAIDYwZ0dLl6PEXKgostxuMv8rHBp3n9emjQf1cWb2/VkS7yO"
标准 Base64 是:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
这里被完全替换,所以 不能直接用普通 Base64。我们需要 按这个字母表重新实现 Base64 编码。
ipad = key XOR 0x76
opad = key XOR 0x3C
<!-- 测试账号 admin 测试密码 admin -->
username = "admin" password = "admin"
自定义 Base64 编码密码 admin
生成 message JSON:
{"username":"admin","password":"编码后的密码"}用 timestamp 做 key,做自定义 HMAC-SHA256
对 HMAC 结果再做自定义 Base64,得到 signature
最终 JSON:
{"username":"admin","password":"...","signature":"..."}check = MD5(JSON.stringify(finalJSON))
判断 check 是否以 ccaf33e3512e31f3 开头
从文件时间可推测:(题目描述说 爆肝到周一凌晨)
release.js:2025/12/22 00:29
release.wasm:2025/12/22 00:57
index.html:2025/12/22 01:06
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;
}
}最终找到的正确时间戳:
1766334550699
对应的 check:
ccaf33e3512e31f36228f0b97ccbc8f1
因此 flag:
flag{ccaf33e3512e31f36228f0b97ccbc8f1}flag{ccaf33e3512e31f36228f0b97ccbc8f1}利用项目直接解包:dbfK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6s2c8q4u0q4g2r3!0G2L8s2y4Q4x3V1k6Y4k6s2y4V1k6h3y4G2L8i4m8Q4x3V1k6J5k6h3I4W2j5i4y4W2M7#2)9J5c8Y4c8S2k6#2)9J5c8Y4j5J5i4K6u0W2y4q4)9J5k6e0l9`.
extends CenterContainer
@onready var flagTextEdit: Node = $PanelContainer / VBoxContainer / FlagTextEdit
@onready var label2: Node = $PanelContainer / VBoxContainer / Label2
static var key = "FanAglFanAglOoO!"
var data = ""
func _on_ready() -> void :
Flag.hide()
func get_key() -> String:
return key
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")@onready var fan = $"../Fan"
var score = 0
func add_point():
score += 1
if score == 1:
Flag.key = Flag.key.replace("A", "B")
fan.visible = trued458af702a680ae4d089ce32fc39945dfunc 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")import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
# 初始key
initial_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()flag{wOW~youAregrEaT!}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("}");
}
// f
v5 = 0; v4 = 1;
for (i = 0; i < a1; ++i) {
v2 = v4;
v4 = (v5 + v4) & 0xF; // modulo 16
v5 = v2;
}
return K[v5];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())flag{10632674-1d219-09f29-14769-f60219a24}aeeK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6E0j5h3&6V1K9h3q4F1N6q4)9J5c8V1N6G2f1X3g2e0P5h3#2Q4x3U0k6F1j5Y4y4H3i4K6y4n7i4K6t1$3L8X3u0K6M7q4)9K6b7W2)9J5y4X3&6T1M7%4m8Q4x3@1u0s2L8#2u0W2f1%4W2E0i4K6u0W2k6i4S2W2i4K6t1$3L8X3u0K6M7q4)9K6b7X3E0%4L8%4u0C8k6i4u0Q4x3U0k6F1j5Y4y4H3i4K6y4n7i4K6t1$3k6%4c8Q4x3@1u0Q4x3U0k6F1j5Y4y4H3i4K6y4n7k6$3!0J5k6h3I4G2k6#2)9J5k6i4c8^5N6q4)9J5y4X3&6T1M7%4m8Q4x3@1t1J5i4K6t1$3k6%4c8Q4x3@1u0Q4x3U0k6S2L8i4m8Q4x3@1t1I4
以如下json形式可以得到下述函数地址
{
"Start": 6656832,
"End": 6657248,
"PackageName": "main",
"FullName": "main.main"
},main.main = 0x659340 iupHvc2q4.* 是主业务包 - Run 驱动会话:newproc + chanrecv - fuzkMtvzPreC 是收包解析链:recv → 校验 → AES‑GCM → zstd → protobuf - d4k0A9zcOh / lOPJLbCSscj / owQoGI8e / l8peMOAQbbXL 是发包链:protobuf → zstd → write - OnJCbKpp = AES‑GCM 校验 - HaNDRB_IhET = magic 生成
int __fastcall main(int argc, const char **argv, const char **envp)
{
...
if (&retaddr <= *(qword*)(g + 16)) morestack();
if (global_argc <= 1) panicIndex(); // args数量检查
v15 = *(qword*)(argv_struct + 24); // argv相关
v18 = *(qword*)(argv_struct + 16);
v14 = unknown_libname_91(); // 取到运行时/配置结构
...
v6 = runtime_newobject(); // new H1eV17y(连接/会话对象)
v6[3] = v15; // 保存参数
...
v6[2] = v18;
v6[4] = v16;
v6[5] = &qword_9CB440;
v6[6] = v8;
sub_4136E0(v14); // 初始化
sub_4D0320(2); // 可能设置并发/线程数
v10 = runtime_newobject(); // 创建 goroutine closure
*v10 = sub_6594E0; // main.main.func1(goroutine入口)
v10[1] = v11; // 捕获 H1eV17y
sub_4511A0(); // runtime.newproc -> 启动 goroutine
runtime_chanrecv1_0(); // 等待 channel 信号
return sub_6573C0(); // Stop()
}
//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;
}
//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;
}
//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;
}
//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。函数iupHvc2q4.HaNDRB_IhET(0x6584A0)构建一个8字节的魔数值:
byte_98F158[i] ^ 0x99
地址0x98F158处的字节为:
DC CD AA CB D7 CC D4 C1
与0x99异或后得到:
ET3RNUMX
这与tcp.pcap中每个TCP负载开头的魔数匹配。
函数iupHvc2q4.OnJCbKpp(0x658DE0)验证解密后的数据。它调用sub_6584A0()(魔数)和sub_658FA0()(memcmp)。还通过混淆的包名调用AES相关例程:
SoyKwS7R.JabRj3ChL -> AES密钥设置
HpkfE6vaP2b.EojfYcyL -> AES-GCM初始化
静态的32字节密钥位于0x98F740:
78 66 71 47 63 56 6a 72 4f 57 70 35 74 55 47 43 50 46 51 71 34 34 38 6e 50 44 6a 49 4c 54 65 37
ASCII表示:
xfqGcVjrOWp5tUGCPFQq448nPDjILTe7
这是一个32字节密钥
从数据包结构和fuzkMtvzPreC中的解析逻辑可知,每条消息格式为:
[8字节魔数 "ET3RNUMX"] [4字节大端长度] [12字节随机数nonce] [密文 || 认证标签]
AES-GCM解密后,负载是zstd压缩的protobuf消息。
步骤:
从pcap文件中重组TCP流。
通过魔数和长度分割消息。
使用密钥xfqGcVjrOWp5tUGCPFQq448nPDjILTe7进行AES-GCM解密。
zstd解压明文。
解密后的会话显示客户端执行shell命令,服务器返回输出。其中一个输出包含一个base32字符串:
MZWGCZ33MI3WGNJYG4YDALJSMIYDCLJUMRSDILJYGUZDMLLBGRQTIN3BGY2WCMLBHF6QU===
解码后得到flag(只需要一条会话就能重建)。
读取并解析 pcap 解析网络协议层 TCP 重组(把分片拼回完整字节流) 用自定义帧格式拆包 AES‑GCM 解密 zstd 解压 搜索并解码 flag
import struct
from pathlib import Path
from collections import defaultdict
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import zstandard as zstd
import base64
pcap_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 pkts
def 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, payload
def 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, payload
def 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] = data
MAGIC = 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 msgs
zctx = 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)) flag{b7c58700-2b01-4dd4-8526-a4a47a65a1a9}相关文件暂时不知道能不能放 wp都不知道能不能放 缓几天再看哦
更多【软件逆向-第十九届全国大学生信息安全竞赛初赛 Reverse部分(么有vm那题)】相关视频教程:www.yxfzedu.com