本题目标:抓取这5页的数字,计算加和并提交结果

WebAssembly 是新一代的Web虚拟机标准,C/C++ 程序可以通过 Emscripten 工具链编译为 WebAssembly 二进制格式 .wasm,进而导入网页中供 JavaScript 调用——这意味着使用 C/C++ 编写的程序将可以直接运行在网页中。

以下是一个非常简单的 " hello world " WebAssembly 模块(WAT格式):
- (module
- (func (result i32)
- (i32.const 42)
- )
- (export "helloWorld" (func 0))
- )
编写一个从屏幕坐标转换为内存偏移的函数,这是一个最小的测试用例:
- test("offsetFromCoordinate", () => {
- expect(wasm.offsetFromCoordinate(0, 0)).toBe(0);
- expect(wasm.offsetFromCoordinate(49, 0)).toBe(49 * 4);
- expect(wasm.offsetFromCoordinate(10, 2)).toBe((10 + 2 * 50) * 4);
- });
下面是函数实现与导出:
- (func $offsetFromCoordinate (param $x i32) (param $y i32) (result i32)
- get_local $y
- i32.const 50
- i32.mul
- get_local $x
- i32.add
- i32.const 4
- i32.mul
- )
-
- (export "offsetFromCoordinate" (func $offsetFromCoordinate))
WebAssembly 函数与其他语言的函数非常相似,它们具有声明无,一个或多个类型化参数和可选返回值的签名,上述函数采用两个 i32 入数(坐标)并返回单个 i32 结果(存储偏移量),函数体包含许多指令( WebAssembly 有大约 50 条不同的指令),这些指令是按顺序执行的。
WebAssembly 指令在堆栈上运行,考虑到上述函数中的每一步,它解释如下:
当函数执行完成时,堆栈上只剩下一个值,它将成为函数的返回值。
WebAssembly 相关可参考:教你手写 WASM
一般情况下,JavaScript 逆向分为三步:
接下来开始正式进行案例分析:
F12 打开开发者人员工具,刷新网页进行抓包,在 Network 中可以看到数据接口为 20?page=1&XXX,响应预览中可以看到当前页面各数字数据:
在 Payload 负载中可以看到有三个请求参数 page、m 和 t,初步推断 page 为页码,t 为时间戳,需进一步跟栈分析:
在该数据接口的 Initiator 中跟栈进入到 send 中:
点击左下角 { } 格式化文件,send 位于 jquery.min.js:formatted 文件的第 3801 行,在此处打下断点,刷新网页,会在此处断住,向上跟栈到 request 中:
同样格式化文件,可以看到三个请求参数在 20:formatted 文件第 783 行的 list 中定义:
- t = Date.parse(new Date());
- var list = {
- "page": window.page,
- "sign": window.sign(window.page + '|' + t.toString()),
- "t": t,
- };
所以我们需要进一步跟进到 sign 方法定义的位置,看看具体是什么加密方式:
跳转到了 index_bg.js 文件的第 144 行, 格式化后跳转到了第 202 行,这里很明显用 wasm 编写的,在第 210 行打下断点进行调试,可以看到 content 为传入的参数,getStringFromWasm0(r0, r1) 返回了加密结果,wasm 中 getStringFromWasm0 方法能获取内存中指定位置,长度的数据,经调试 r1 为定值 32,所以 sign 的长度为 32 位:
逐个参数分析,跟进到 retptr 在 wasm 文件中的位置,了解其含义:

- (func $__wbindgen_add_to_stack_pointer (;752;) (export "__wbindgen_add_to_stack_pointer") (param $var0 i32) (result i32)
- local.get $var0
- global.get $global0
- i32.add
- global.set $global0
- global.get $global0
- )
再跟进到 ptr0 的 passStringToWasm0 函数中,传入了三个参数 arg、malloc 和 realloc,在第 188 行打下断点调试分析:

len0 值为 15,由 WASM_VECTOR_LEN 传入,即 content 字符串的长度,同样定义在 passStringToWasm0 函数中:
WASM_VECTOR_LEN = offset;
经分析,retptr 为指针地址,ptr0 为内存地址,打断点,从第 207 行进入 wasm 文件,可以看到明显的sign 模块 (export "sign"),后面传了三个参数进来,wasm 文件指针依次向下传值:

ctrl + f 搜索 sign 关键词,看其值是如何生成的,搜索出了 38 个结果,在每个包含 sign 关键字的函数此处打下断点,此处断住后可以观察到,var2 即 content 参数的长度,为 15:

进入下一个断点,会跳到 $match_twenty::sign::MD5::hash::hd3cc2e6ebf304f6f 函数中,此时 var2 的长度变成了 31,跟我们之前分析的 sign 长度近似,证明这个函数中对 content 进行了加密处理,导致字符串长度出现了变化:

此时的 var1 为 1114192,var2 为 31,接着继续跳到下一个断点,跳不动的就将该处断点取消掉,一直跳转到最后一个断点,即 index_bg.js?:formatted 文件的第 210 行 return 处,此时将 var1 和 var2 作为参数传递到 getStringFromWasm0 方法中,在控制台打印 getStringFromWasm0(1114192, 31),会输出一段明文值,且与 content 内容及其相似:

该函数名为 MD5,不妨试试将这串内容通过 MD5 进行加密后的值与 sign 值作对比:


可以看到值是一样的,即 MD5 加密,并经过加盐处理,python 代码如下:
sessionid 要改为自己的:
- import re
- import time
-
- import requests
- import hashlib
-
- headers = {
- "user-agent": "yuanrenxue,project"
- }
-
- cookies = {
- "sessionid": " your sessionid "
- }
-
- url = "https://match.yuanrenxue.com/api/match/20"
-
-
- def main():
- num_add_total = 0
-
- for page_num in range(1, 6):
-
- timestamp = str(int(time.time() * 1000))
- sign = hashlib.md5((str(page_num) + "|" + timestamp + "D#uqGdcw41pWeNXm").encode()).hexdigest()
-
- params = {
- "page": page_num,
- "sign": sign,
- "t": timestamp
- }
-
- response = requests.get(url, headers=headers, cookies=cookies,
- params=params)
-
- num_add = 0
- for i in range(10):
- value = response.json()['data'][i]
- num = re.findall(r"'value': (.*?)}", str(value))[0]
- num_add += int(num)
-
- num_add_total += num_add
-
- print(num_add_total)
-
-
- if __name__ == '__main__':
- main()