Featured image of post CISCN & CCB 2025 部分逆向方向 WP

CISCN & CCB 2025 部分逆向方向 WP

wasm-login

某人本想在2025年12月第三个周末爆肝一个web安全登录demo,结果不仅搞到周一凌晨,他自己还忘了成功登录时的时间戳了,你能帮他找回来吗?

提交格式为flag{时间戳正确时的check值}。是一个大括号内为一个32位长的小写十六进制字符串。

附件:https://pan.baidu.com/share/init?surl=HbRVmm1xNPqsK4oytlt5zg password:GAME

打开附件后,发现核心文件为 Wasm 格式(WebAssembly),首先尝试使用wabt工具集进行逆向还原,分别生成了wasm.wat(文本格式 Wasm)和wasm.c(还原的 C 语言代码)。但还原后的.c文件存在大量未定义标识符,无法直接编译运行,且代码量庞大,逐行分析效率极低。

随后注意到附件中存在release.wasm.map文件(Wasm 源码映射文件),该文件可直接转换为 JS 格式进行分析,通过映射文件快速定位到核心认证逻辑代码,大幅降低逆向成本。

通过release.wasm.map转换后的 JS 代码,提取到核心登录认证函数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
134
135
136
137
138
/**
 * 执行完整的登录认证流程:
 * 1. 对密码进行Base64编码
 * 2. 获取当前时间戳
 * 3. 构建JSON消息
 * 4. 使用HMAC-SHA256进行签名
 * 5. 返回最终的JSON字符串
*/
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}\"}`;
  //"username":"admin","password":""}
  //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 stringToUint8Array(str: string): Uint8Array {
  const arr = new Uint8Array(str.length);
  for (let i = 0; i < str.length; ++i) {
    arr[i] = str.charCodeAt(i);
  }
  return arr;
}

@inline
function fill(ptr: usize, value: u8, length: u32): void {
  const finalPtr = ptr + length;
  while(ptr < finalPtr) {
    store<u8>(ptr, value);
    ptr++;
  }
}

@inline
function ArrayBufferToUint8Array(input: ArrayBuffer): Uint8Array{
  const res = new Uint8Array(input.byteLength);
  const inputPtr = changetype<usize>(input)
  for (let i = 0; i < input.byteLength; ++i) {
    res[i] = load<u8>(inputPtr + i);
  }
  return res;
} 
/**
 * 使用HMAC-SHA256算法对消息进行签名
 * @param message 待签名的消息
 * @param secret 密钥(时间戳)
 * @returns 签名后的Base64字符串
*/
function signMessage(message: string, secret: string): string {
  const messageBytes = String.UTF8.encode(message);
  const secretBytes = String.UTF8.encode(secret);

  const signatureBytes = hmacSHA256(secretBytes,messageBytes);
  
  return encode(ArrayBufferToUint8Array(signatureBytes));
}    

// 实现 HMAC-SHA256 函数
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) {
    // 如果密钥长度超过块大小,对密钥进行哈希处理
      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;
}

通过搜索authenticate相关调用,发现了release.jsindex.html中的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  const memory = exports.memory || imports.env.memory;
  const adaptedExports = Object.setPrototypeOf({
    authenticate(username, password) {
      // assembly/index/authenticate(~lib/string/String, ~lib/string/String) => ~lib/string/String
      username = __retain(__lowerString(username) || __notnull());
      password = __lowerString(password) || __notnull();
      try {
        return __liftString(exports.authenticate(username, password) >>> 0);
      } finally {
        __release(username);
      }
    },
  }, exports);

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { authenticate } from "./build/release.js";
...
// 调用 WASM 中的 authenticate 函数
const authResult = authenticate(username, password);
const authData = JSON.parse(authResult);
...
simulateServerRequest(authData)
              .then(response => {
                if (response.success) {
                  // 登录成功
                  alert('登录成功!');
...
function simulateServerRequest(data) {
          return new Promise(resolve => {
            // 模拟网络延迟
            setTimeout(() => {
              // 实际应用中这里应该是真实的 API 请求
              // 这里仅作演示,使用本地判断
              const check = CryptoJS.MD5(JSON.stringify(data)).toString(CryptoJS.enc.Hex);
              console.log('Debug - Data sent to server:', data);
              console.log('Debug - Check value:', check);
              if (check.startsWith("ccaf33e3512e31f3")){
                resolve({ success: true });
....
  • release.js中对authenticate函数进行了 Wasm 内存管理封装,确保 JS 与 Wasm 之间的字符串参数正常传递;
  • index.html中明确了登录成功的判断条件:将authenticate返回的 JSON 数据标准化后(JSON.stringify(JSON.parse(authResult))),通过CryptoJS.MD5计算得到 32 位十六进制校验值check,该值必须以ccaf33e3512e31f3开头才判定为登录成功。

这里需要注意原本release.js中的hmacSHA256等函数==并非标准的实现==,直接复刻易出现偏差,因此采用JS 脚本直接导入 Wasm 封装函数的方式,复用原始加密逻辑,仅对时间戳进行遍历爆破。(原本爆破时间很长,此处为得到时间戳后的缩短的时间范围)

 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
import { createHash } from "node:crypto";
import { authenticate } from "./build/release.js";
const P = "ccaf33e3512e31f3";
const u = "admin", p = "admin";
const T = (x) => (/^\d+$/.test(x) ? +x : new Date(x).getTime());
const s = T(process.argv[2] ?? "2025-12-22T00:00:00+08:00");
const e = T(process.argv[3] ?? "2025-12-22T01:00:00+08:00");

for (let t = s; t <= e; t++) {
  Date.now = () => t;
  try {
    const authResult = authenticate(u, p);
    const j = JSON.stringify(JSON.parse(authResult));
    const h = createHash("md5").update(j).digest("hex");
    if (h.startsWith(P)) {
      console.log(`MD5哈希值:${h}`);
      console.log(`正确时间戳(毫秒):${t}`);
      console.log(`标准化JSON:${j}`);
      console.log(`对应日期:${new Date(t).toLocaleString()}`);
      break;
    }
  } catch (err) {
    continue;
  }
}

爆破结果

babygame

请找出隐藏的Flag。请注意只有收集了所有的金币,才能验证flag。

附件:https://pan.baidu.com/share/init?surl=KI0XixtMhXvUFap3Ey4c6g password:GAME

使用反编译工具GDRE_tools-v2.4.0-windows 得到资源,可以查看代码

如果没有注意到AB代换,通关游戏也能获得正确的密钥,然后直接使用cyberchef的aes解密就能得到flag

参考学习文献:

JavaScript Source Map 详解

Webassembly逆向手法

wasm逆向

调试 C/C++ WebAssembly | Chrome DevTools | Chrome for Developers

https://github.com/WebAssembly/wabt

前途似海,来日方长。


<