在第11 关中 和前面不同的是使用了 WebAssembly(简称 Wasm)技术。
WebAssembly主要是借助Emscripten编译工具,使c/c++文件能转成wasm格式的文件,javaScript可以直接调用该文件执行其中的方法。从而可将一些api参数的加密逻辑隐藏其中。
对于逆向使用了这种技术的网站的情况下,我们可以借助 python 的 pywasm库来模拟执行 wasm文件。
通过对11关的请求的调用栈溯源可以得知,加密参数M 就是通过这个方法的来的,当前这个文件就是一个wasm文件。

Pasted image 20250926175816.png (97.91 KB, 下载次数: 1)
下载附件
2025-9-26 17:59 上传
因此我们将目标 .wasm 文件另存到本地,然后使用pywasm库来模拟执行加密方法即可:

Pasted image 20250925203202.png (120.15 KB, 下载次数: 1)
下载附件
2025-9-26 17:59 上传
wasm模拟执行,可以使用以下代码:
[Python] 纯文本查看 复制代码import pywasm
def generate_param_m(num, ts):
runtime = pywasm.core.Runtime()
m = runtime.instance_from_file('./encrypt.wasm')
r = runtime.invocate(m, 'encrypt', [num, ts])
return r[0]
在第12关中,可以看到整个js 文件,除了发送请求和方法名解析的方法。就只剩下了一个 interpreter。这个方法的内部就是一个自定义的解释器,请求参数中的加密逻辑也在其中。
对于这种情况,我们挨个去debug 溯源比较麻烦,因此直接copy 这个方法下来。整个12关的逆向过程就是缺啥补啥。
在补环境的过程中,下面的这段代码可以更好的帮助我们理清调用关系:
[JavaScript] 纯文本查看 复制代码dtavm = {}
dtavm.log = console.log
function proxy(obj, objname, type) {
function getMethodHandler(WatchName, target_obj) {
let methodhandler = {
apply(target, thisArg, argArray) {
if (this.target_obj) {
thisArg = this.target_obj
}
let result = Reflect.apply(target, thisArg, argArray)
if (target.name !== "toString") {
if (target.name === "addEventListener") {
dtavm.log(`调用者 => [${WatchName}] 函数名 => [${target.name}], 传参 => [${argArray[0]}], 结果 => [${result}].`)
} else if (WatchName === "window.console") {
} else {
dtavm.log(`调用者 => [${WatchName}] 函数名 => [${target.name}], 传参 => [${argArray}], 结果 => [${result}].`)
}
} else {
dtavm.log(`调用者 => [${WatchName}] 函数名 => [${target.name}], 传参 => [${argArray}], 结果 => [${result}].`)
}
return result
},
construct(target, argArray, newTarget) {
var result = Reflect.construct(target, argArray, newTarget)
dtavm.log(`调用者 => [${WatchName}] 构造函数名 => [${target.name}], 传参 => [${argArray}], 结果 => [${(result)}].`)
return result;
}
}
methodhandler.target_obj = target_obj
return methodhandler
}
function getObjhandler(WatchName) {
let handler = {
get(target, propKey, receiver) {
let result = target[propKey]
if (result instanceof Object) {
if (typeof result === "function") {
dtavm.log(`调用者 => [${WatchName}] 获取属性名 => [${propKey}] , 是个函数`)
return new Proxy(result, getMethodHandler(WatchName, target))
} else {
dtavm.log(`调用者 => [${WatchName}] 获取属性名 => [${propKey}], 结果 => [${(result)}]`);
}
return new Proxy(result, getObjhandler(`${WatchName}.${propKey}`))
}
if (typeof (propKey) !== "symbol") {
dtavm.log(`调用者 => [${WatchName}] 获取属性名 => [${propKey?.description ?? propKey}], 结果 => [${result}]`);
}
return result;
},
set(target, propKey, value, receiver) {
if (value instanceof Object) {
dtavm.log(`调用者 => [${WatchName}] 设置属性名 => [${propKey}], 值为 => [${(value)}]`);
} else {
dtavm.log(`调用者 => [${WatchName}] 设置属性名 => [${propKey}], 值为 => [${value}]`);
}
return Reflect.set(target, propKey, value, receiver);
},
has(target, propKey) {
var result = Reflect.has(target, propKey);
dtavm.log(`针对in操作符的代理has=> [${WatchName}] 有无属性名 => [${propKey}], 结果 => [${result}]`)
return result;
},
deleteProperty(target, propKey) {
var result = Reflect.deleteProperty(target, propKey);
dtavm.log(`拦截属性delete => [${WatchName}] 删除属性名 => [${propKey}], 结果 => [${result}]`)
return result;
},
defineProperty(target, propKey, attributes) {
var result = Reflect.defineProperty(target, propKey, attributes);
dtavm.log(`拦截对象define操作 => [${WatchName}] 待检索属性名 => [${propKey.toString()}] 属性描述 => [${(attributes)}], 结果 => [${result}]`)
// debugger
return result
},
getPrototypeOf(target) {
var result = Reflect.getPrototypeOf(target)
dtavm.log(`被代理的目标对象 => [${WatchName}] 代理结果 => [${(result)}]`)
return result;
},
setPrototypeOf(target, proto) {
dtavm.log(`被拦截的目标对象 => [${WatchName}] 对象新原型==> [${(proto)}]`)
return Reflect.setPrototypeOf(target, proto);
},
preventExtensions(target) {
dtavm.log(`方法用于设置preventExtensions => [${WatchName}] 防止扩展`)
return Reflect.preventExtensions(target);
},
isExtensible(target) {
var result = Reflect.isExtensible(target)
dtavm.log(`拦截对对象的isExtensible() => [${WatchName}] isExtensible, 返回值==> [${result}]`)
return result;
},
}
return handler;
}
if (type === "method") {
return new Proxy(obj, getMethodHandler(objname, obj));
}
return new Proxy(obj, getObjhandler(objname));
}
EventTarget = function EventTarget(){}
//这里放你要代理的对象 --->
window = globalThis
//document
document = {}
// navigator
navigator = {}
//location
location = {}
//history
history = {}
//screen
screen = {}
//localStorage
localStorage = {}
localStorage = proxy(localStorage, 'localStorage')
screen = proxy(screen, 'screen')
location = proxy(location, 'location')
history = proxy(history, 'history')
window = proxy(window, 'window')
document = proxy(document, 'document')
navigator = proxy(navigator, 'navigator')
最后第13关:
我们发送网络请求,看他的调用链,点进 loadPage 方法:

Pasted image 20250925205955.png (117.9 KB, 下载次数: 1)
下载附件
2025-9-26 17:58 上传
发现者整个文件中就只有一个方法:

Pasted image 20250925210027.png (184.64 KB, 下载次数: 1)
下载附件
2025-9-26 17:58 上传
但是,按照之前关卡的逻辑,他会使用一个请求发送前的拦截器。
所以我们全局搜索‘$’。最后在jqueryxhr.js文件中可以找到这个请求发送前的拦截器:

Pasted image 20250925210202.png (232.65 KB, 下载次数: 1)
下载附件
2025-9-26 17:58 上传
这里可以将整个文件的js 都拉下来。不过在复制到本地之前可以先反混淆一下,这是我用的工具网址 https://obf-io.deobfuscate.io/。

Pasted image 20250925210806.png (155.55 KB, 下载次数: 1)
下载附件
2025-9-26 17:58 上传
复制代码后,我们在本地加上打印方法运行,发现没有任何输出。这是因为他重写了log 方法,我们注掉就好了:

Pasted image 20250925210954.png (137.75 KB, 下载次数: 1)
下载附件
2025-9-26 17:58 上传
最后我们再次运行可以发现了。
到此这关的请求参数加密逆向完成了, 最后我们组装一下方法,让其方便python调用就行了。
最后的最后,我们再发送请求的时候还有个小坑,就是他的请求入参是一个字符串且一点不能改动。
[Python] 纯文本查看 复制代码data = '{"page":"'+str(num)+'"}'
本文到此就结束了,非常感谢各位能看到这里。8~13关的代码我放在附件了。