参考资料:
2024最新版Node.js下载安装及环境配置教程(非常详细)
JS逆向之wasm算法(JS二进制)
JS逆向之wasm逆向(二)

准备复现
记录一次wasm逆向
JS逆向之wasm逆向(二进制)
【JS逆向百例】酷某音乐 wasm 逆向
某视频解析网站js逆向分析——学会wasm文件类型逆向
某某电影网页wasm逆向思路

引言

最近整理题库时,遇到了wasm形式的加密,即浏览器调用wasm的函数实现加解密,而我们无法像调试js一样逆向,所以有一定难度,记录一下学习过程。

前提

是一个小程序,再获取题目时对题目和选项进行了加密,一开始以为是普通的 AES 或者 DES
还想用调试看看小程序代码,结果开了调试直接闪退,重装几次微信都是这样,后面才知道或许就是因为这个wasm
还好这个小程序有网页版,加密跟小程序一样,要是只有小程序的话我估计是搞不出来了

前面尝试了很多方法,小程序调试用不了,就用传统的解包看代码吧,发现了有这个wasm,于是去网上找教程该怎么办
一般是需要用nodejs来加载这个wasm,我尝试了很多次也没成功。

后面发现了网页版,调试起来就方便多了

分析

网址:aHR0cHM6Ly93d3cuYXFsZWFybi5jb20vcHJhY3RpY2UvIy9pbmRleA==
打开网页,分析下js,不难发现Rh函数即为需要的解密函数
解密函数

一步一步跟,发现下面这个函数定义的有些问题,都与Xr有关

1
2
3
4
Qe._get_secret = (e, t, n) => (Qe._get_secret = Xr.e)(e, t, n);
Qe._malloc = e => (Qe._malloc = Xr.f)(e);
Qe._free = e => (Qe._free = Xr.g)(e);
Qe._decrypt = (e, t, n, o) => (Qe._decrypt = Xr.h)(e, t, n, o);

这个Xr就是wasm里的函数,后面分析时也能看出导出函数的名字就是efgh

然后就继续找教程看看怎么实现这个功能吧

找了很多文字教程都没成功,快要放弃时,在b站搜到了一个视频,很快就写出来了

JS逆向之wasm算法(JS二进制)
还有他的博客
JS逆向之wasm逆向(二)

实现

首先是如何加载这个wasm文件并获取里面的函数,这就需要借助nodejs来实现了
百度一个教程,很详细,装起来很快,2024最新版Node.js下载安装及环境配置教程(非常详细)

网站里的一般写法是通过fetch去加载wasm的代码
加载wasm
加载wasm时有两种加载方法,第一种是读取本地文件,第二种是读取网络文件传网络请求的参数,需要传两个参数,可以参考这篇WebAssembly教程

1
2
3
4
5
6
7
8
9
WebAssembly.instantiate(bufferSource, importObject);
WebAssembly.instantiateStreaming(source, importObject)
//参数
//source
//一个 Response 对象或一个会兑现为 Response 的 promise,其表示你想要传输、编译和实例化的 Wasm 模块的底层源。

//importObject 可选
//包含一些想要导入到新创建的 Instance 中的值的对象,例如函数或 WebAssembly.Memory 对象。每个已编译模块的声明导入必须有一个匹配属性,否则抛出 WebAssembly.LinkError 异常。

在这个网站里,传入了n,这就需要扣代码了
不难发现,jie函数里的n是第三个参数,也就是qie函数里的e,可以发现在第一行已经定义了,顺着扣就好了,没什么难度
初试化wasm

由于没学过js,不太懂这个异步函数promise,尝试了很多遍之后,发现可以直接在then里写代码,定义好各种加密函数,然后在需要的时候调用这个Rh就行了
注意在原来的js里,加载完wasm后要执行jie函数里的n,也就是t,里面执行了很多函数并且返回了,虽然没按照他这种形式复现出来,但通过尝试,发现只有cy赋值这个有用,其他的对结果没有影响。
Rh函数

后面调用这个Rh函数就能正常解密了,由于不太会js,所以再用nodejs作为后端进行解密,python来爬虫

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
//初始化wasm
var promise_test = qie();

const server = http.createServer();

server.on('request',(req,res)=>{
// 当请求参数传递时触发data事件
// 当请求参数传递结束时触发end事件
// 所以需要data和end都写才能获取到完整数据
let postParams = ''
req.on('data',(parmas)=>{
postParams += parmas
})
req.on('end',()=>{
promise_test.then((F)=>{
var result = F(postParams);
console.log(result)
res.end(result)
// 下面这个是js对结果进行解码,有些符号解不出来,后面用python解了
// res.end(unescape(result.replace(/&#x/g, '%u').replace(/;/g, '')))
})
})
})
//监听端口号为3000窗口
server.listen(3000,(err)=>{
if( !err ){
console.log('服务器已经开启 我们可以通过http://127.0.0.1:3000 来访问');
}
})

结语

总的来说不太难,只是之前没接触过wasm以及js,不知道怎么加载wasm以及如何处理promise,耽误了不少时间
以下是完整的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
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
147
148
149
150
151
152
153
154
155
156
157
158
159
const fs = require("fs")
const http = require('http')
/*
配套函数
*/
var Vi;
var Na = 0;
var Qe = typeof Qe < "u" ? Qe : {};
function Vie() {
var e = cy.buffer;
Qe.HEAP8 = new Int8Array(e),
Qe.HEAP16 = new Int16Array(e),
Qe.HEAPU8 = Dp = new Uint8Array(e),
Qe.HEAPU16 = new Uint16Array(e),
Qe.HEAP32 = new Int32Array(e),
Qe.HEAPU32 = new Uint32Array(e),
Qe.HEAPF32 = new Float32Array(e),
Qe.HEAPF64 = new Float64Array(e)
}
function Die(e) {
var t;
Na++,
(t = Qe.monitorRunDependencies) == null || t.call(Qe, Na)
}
function vy(e) {
var n;
(n = Qe.onAbort) == null || n.call(Qe, e),
e = "Aborted(" + e + ")",
Ii(e),
dy = !0,
e += ". Build with -sASSERTIONS for more info.";
var t = new WebAssembly.RuntimeError(e);
throw t
}
var Uie = (e, t, n) => Dp.copyWithin(e, t, t + n)
, Yie = e => {
vy("OOM")
}
, Gie = e => {
Dp.length,
Yie()
}
, Xie = {
b: Uie,
a: Gie
};
function jie(e, t, n, o) {
const r = fs.readFileSync('decrypt.wasm');
let b = WebAssembly.instantiate(r,n);
return b.then(function(e) {
let Xr = e.instance.exports; //wasm的导出函数
console.log(Xr)
var cy = Xr.c;
Qe._get_secret = (e, t, n) => (Qe._get_secret = Xr.e)(e, t, n);
Qe._malloc = e => (Qe._malloc = Xr.f)(e);
Qe._free = e => (Qe._free = Xr.g)(e);
Qe._decrypt = (e, t, n, o) => (Qe._decrypt = Xr.h)(e, t, n, o);
function Rh(e) {
if (Qe == null)
return "";
Qe.HEAPU8 = Dp = new Uint8Array(cy.buffer)
let t = Qe.HEAPU8
, n = new TextEncoder("utf-8")
, o = new TextDecoder("utf-8");
const r = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let a = "";
for (let m = 0; m < 256; m++)
a += r.charAt(Math.floor(Math.random() * r.Length));
let l = n.encode(a);
const s = Qe._malloc(l.length + 1);
for (let m = 0; m < l.length; m++){
t[s + m] = l[m];
}
let u = Math.floor(new Date().getTime() / 1e3)
, c = Qe._get_secret(s, u, l.length);

if (c == 0)
return "";
let f = n.encode(e);
const d = Qe._malloc(f.length + 1);
for (let m = 0; m < f.length; m++)
t[d + m] = f[m];
let p = 0
, v = 0;
try {
if (p = Qe._decrypt(d, s, c, f.length),
p == 0)
return "";
for (v = 0; v < t.length && t[p + v] != 0; v++)
;
return o.decode(t.subarray(p, p + v))
} catch (m) {
return m
} finally {
s != 0 && Qe._free(s),
c != 0 && Qe._free(c),
d != 0 && Qe._free(d),
p != 0 && Qe._free(p)
}
}
return Rh
}
)
}
function qie() {
var e = {
a: Xie
};
function t(o, r) {
return Xr = o.exports,
cy = Xr.c,
Vie(),
Hie(Xr.d),
Fie(),
Xr
}
Die();
function n(o) {
t(o.instance)
}
if (Qe.instantiateWasm)
try {
return Qe.instantiateWasm(e, t)
} catch (o) {
return Ii(`Module.instantiateWasm callback failed with error: ${o}`),
!1
}
return jie(Vi, "Al", e, n)
}

//初始化wasm
var promise_test = qie();

const server = http.createServer();

server.on('request',(req,res)=>{
// 当请求参数传递时触发data事件
// 当请求参数传递结束时触发end事件
// 所以需要data和end都写才能获取到完整数据
let postParams = ''
req.on('data',(parmas)=>{
postParams += parmas
})
req.on('end',()=>{
promise_test.then((F)=>{
var result = F(postParams);
console.log(result)
res.end(result)
// 下面这个是js对结果进行解码,有些符号解不出来,后面用python解了
// res.end(unescape(result.replace(/&#x/g, '%u').replace(/;/g, '')))
})
})
})
//监听端口号为3000窗口
server.listen(3000,(err)=>{
if( !err ){
console.log('服务器已经开启 我们可以通过http://127.0.0.1:3000 来访问');
}
})