案例目标
本题目标:抓取这5页的数字,计算加和并提交结果
WebAssembly
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 指令在堆栈上运行,考虑到上述函数中的每一步,它解释如下:
- get_local $y:将 $y 参数的值推入堆栈。
- i32.const 50:推入常数值 50
- i32.mul:从堆栈中弹出两个值,将它们相乘,然后将结果推入堆栈
- get_local $x:将 $x 参数的值推入堆栈。
- etc
当函数执行完成时,堆栈上只剩下一个值,它将成为函数的返回值。
WebAssembly 相关可参考:教你手写 WASM
常规 JavaScript 逆向思路
一般情况下,JavaScript 逆向分为三步:
- 寻找入口:逆向在大部分情况下就是找一些加密参数到底是怎么来的,关键逻辑可能写在某个关键的方法或者隐藏在某个关键的变量里,一个网站可能加载了很多 JavaScript 文件,如何从这么多的 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,
};
- page:页码
- t:在第 782 行定义,为时间戳
- sign:页码、|、时间戳转换为字符串,这个相加后经过 sign 方法加密后的结果
所以我们需要进一步跟进到 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 代码如下:
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()