yangyunhui

功不唐捐,玉汝于成

2025长城杯 wasm-login

wasm-login

长城杯逆向题目-wasm-login

目录


刚开始写题的想法

​ 刚开始写题的时候,就面临一个问题:当我打开Index.html的时候是直接双击打开,我根本不知道file协议,我也不知道当我们看见有fetch的时候是不能用file的,所以当我输入用户名和密码的时候既没有错误报错和正确提示,我感到很疑惑,为什么html代码里明明就有关于这部分的代码,但是我却触发不了。我就去问gemini,是因为我们是需要用http协议的(具体在相关知识点中说),后来我就在终端中python -m http.sever 8000,然后在浏览器中打开http://localhost:8000。这是我遇见的第一个问题。

​ 第二个问题就是,我发现wasm文件是二进制文件,我在想要不要反编译它,虽然我最后还是反编译了,但是说实话,代码挺混乱的(wasm2c release.wasm -o release.c这里你也可以反编译成wat代码),很长很乱,我根本不知道怎么看

​ 第三个问题,到最后我知道是进行时间戳爆破,判断MD5值的开头是否和ccaf33e3512e31f3一样,但是这个时间格式是什么???因为一个空格一个横线都会导致MD5的值不同,那我们使用什么格式呢???

题解

题目 是:

1
2
3
某人本想在2025年12月第三个周末爆肝一个web安全登录demo,结果不仅搞到周一凌晨,他自己还忘了成功登录时的时
间戳了,你能帮他找回来吗?提交格式为flag{时间戳正确时的check值}。是一个大括号内为一个32位长的小写十六进
制字符串。

我们先看看题目给我们的附件

看到这里的时候我们就知道它可能是一个Js逆向

我们将这个网页打开,注意不能直接点开html文件(直接打开用的是file协议),这里我们要用http协议。

我们使用

  1. python -m http.server 8000
  2. 在浏览器中访问url:http://localhost:8000

这是正确打开网页的方式,打开网页之后我们可以看到这是一个登录页面,我们可以看看它的源代码

在代码的最后一行有

1
<!-- 测试账号 admin 测试密码 admin-->

说明我们的账号和密码都是admin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
});
}

最重要的是 CryptoJS.MD5(JSON.stringify(data)).toString(CryptoJS.enc.Hex);我们逐步分析

  • JSON.stringify(data),将data(wasm返回的对象)转化成标准json字符串,因为md5只能处理字符串或二进制数据,不能直接处理js对象
  • CryptoJS.MD5(...),对上一步得到的字符串进行MD5哈希运算
  • .toString(CryptoJS.enc.Hex)将加密后的数据转化成16进制字符串

得到该16进制字符串后,将它的开头和ccaf33e3512e31f3进行对比,如果一致则success

1
2
3
// 调用 WASM 中的 authenticate 函数
const authResult = authenticate(username, password);
const authData = JSON.parse(authResult);

我们需要查看wasm文件的内容,但是因为它是二进制编码,所以我们将它反编译成可读的wat文件,有很多方法,这里我说两种

  1. 使用在线网站
  2. 使用wasm2wat进行反编译

我们可以看到

1
(import "env" "abort" (func $env.abort (type $t6)))(import "env" "Date.now" (func $env.Date.now (type $t9)))

它调用了Date.now()

可以猜测,authenticate 函数结合用户名,密码,时间戳参数进行签名计算

我们修改Date.now()函数为我们自定义函数,对时间戳进行重放爆破,然后将authenticate 函数作为黑盒调用,寻找目标签名值。

完整exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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-22T06:10:00+08:00");

for (let t = s; t <= e; t++) {
Date.now = () => t;
const j = JSON.stringify(JSON.parse(authenticate(u, p)));
const h = createHash("md5").update(j).digest("hex");
if (h.startsWith(P)) {
console.log("命中时间戳:", t);
console.log("对应时间:", new Date(t).toString());
console.log("最终结果:", h);
break;
}
}

关键

  1. Date.now = () => t;

    我们强行修改了全局的Date.now()函数

    WASM内部会调用Date.now()来获取当前时间戳,通过这一行,让WASM每运行一次都以为“现在”就是我们指定的那个毫秒,这就是重放爆破

  2. const j = JSON.stringify(JSON.parse(authenticate(u, p)));

    步骤:

    1. authenticate(u, p):让 WASM 按照我们伪造的时间 t 算出一串结果。
    2. JSON.parse(...):把结果变回对象。
    3. JSON.stringify(...):再变回标准的 JSON 字符串。

    为什么这样写:这叫 “标准化” 。为了防止 WASM 返回的字符串里多一个空格、少一个换行导致 MD5 匹配失败。我们必须保证字符串格式和服务器(网页)校验时完全一致。

我们在终端运行脚本

得到结果:flag{ccaf33e3512e31f36228f0b97ccbc8f1}

总结与反思

做题的时候,有几个问题需要改正

  1. 在冗余的代码中没有必要弄清楚每一个细节,有时候需要合理的猜测
  2. 在脚本中,我是需要爆破时间戳的,但是我一直想的是去换格式,我们是可以使用JSON.stringify(JSON.parse(authenticate(u, p)));,这里的authenticate(u, p)是一个黑盒调用,会自动生成时间戳。
  3. 当我们不太清楚函数内的逻辑时,就可以进行黑盒调用

相关知识点

file协议与Http协议的区别

file协议主要用于本地计算机中的文件,好比通过资源管理器打开文件一样,它主要是针对本地的,即file协议是访问本地的资源,并且file协议无法实现跨域。

http访问本地的html文件,相当于本机作为一台http服务器,然后通过localhost访问自己电脑上的本地服务器。

file只是请求了本地文件,将其作为静态文件打开,而http是在本地搭建了一个服务器再通过服务器去动态解析拿到文件

但是在这道题中我们为什么知道这是用http协议打开的而不是使用file协议,主要原因如下:

本道题涉及wasm,必须使用http协议(启动一个本地服务器)

  1. 跨域安全限制

    WebAssembly文件(.wasm)不能像图片那样直接加载,需要通过JavaScript的fetch()或者WebAssembly.instantiateStreaming()接口来获取数据

    • file协议: 浏览器出于安全考虑,严格限制file://协议下的脚本去读取本地文件系统中的其他文件。浏览器会认为这是”跨域请求”,从而拦截掉加载.wasm的行为。
    • http协议: 浏览器允许在同一域名(如localhost)下下载相关资源,这符合当代浏览器的安全模型。

    这道题代码内部使用了fetch,并且我们还需要AssemblyScript的标准加载器来获取release.wasm

    1. 如果使用file协议,在控制台(F12)会报错,提示无法加载wasm文件
    2. 如果使用http协议,所有文件都在服务器语境下,浏览器能正确获取文件,校验类型并运行逻辑

如果我们想快速开启http环境,可以在代码所在文件夹打开终端输入:

  • Python3: python -m http.server 8000

为什么这个exp可以不用考虑时间的格式?

我们使用“黑盒”,之所以不需要知道时间的格式,是因为在这个过程中,没有去手动”写”时间字符串,而是通过劫持系统函数,让程序自发地生成了正确的格式。

  1. 在脚本中我们使用了Date.now = () => t;,它并不接收你传进去的字符串,而是调用系统接口获取当前毫秒数。当wasm拿到这个数字之后,它会用它自带的转换逻辑去生成JSON里面的时间字段。既然WASM在你的电脑上运行和在服务器上运行的是同一个文件,那么他生成的时间格式的逻辑(比如有没有小数点,是不是13位)就一定是一模一样的。

  2. JSON.parse(...) :把这一串文本拆解回了 JavaScript 的“纯数据对象”。在对象状态下,格式是不存在的,只有键值对。

    JSON.stringify(...) :把你拆解出来的“纯数据”重新按照 Node.js 的标准打包。

好长时间没写blog了,期末考试结束了,我终于可以再次拿起键盘了hhhh