• 【JavaScript 逆向】猿人学 web 第二十题:新年挑战


    案例目标        

    网址:第二十题 新年挑战 2022新春快乐!- 猿人学

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

    WebAssembly

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

    以下是一个非常简单的 " hello world " WebAssembly 模块(WAT格式):

    1. (module
    2. (func (result i32)
    3. (i32.const 42)
    4. )
    5. (export "helloWorld" (func 0))
    6. )

    编写一个从屏幕坐标转换为内存偏移的函数,这是一个最小的测试用例:

    1. test("offsetFromCoordinate", () => {
    2. expect(wasm.offsetFromCoordinate(0, 0)).toBe(0);
    3. expect(wasm.offsetFromCoordinate(49, 0)).toBe(49 * 4);
    4. expect(wasm.offsetFromCoordinate(10, 2)).toBe((10 + 2 * 50) * 4);
    5. });

    下面是函数实现与导出:

    1. (func $offsetFromCoordinate (param $x i32) (param $y i32) (result i32)
    2. get_local $y
    3. i32.const 50
    4. i32.mul
    5. get_local $x
    6. i32.add
    7. i32.const 4
    8. i32.mul
    9. )
    10. (export "offsetFromCoordinate" (func $offsetFromCoordinate))

    WebAssembly 函数与其他语言的函数非常相似,它们具有声明无,一个或多个类型化参数和可选返回值的签名,上述函数采用两个 i32 入数(坐标)并返回单个 i32 结果(存储偏移量),函数体包含许多指令( WebAssembly 有大约 50 条不同的指令),这些指令是按顺序执行的。

    WebAssembly 指令在堆栈上运行,考虑到上述函数中的每一步,它解释如下:

    1. get_local $y:将 $y 参数的值推入堆栈。
    2. i32.const 50:推入常数值 50
    3. i32.mul:从堆栈中弹出两个值,将它们相乘,然后将结果推入堆栈
    4. get_local $x:将 $x 参数的值推入堆栈。
    5. 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 中定义:

    1. t = Date.parse(new Date());
    2. var list = {
    3. "page": window.page,
    4. "sign": window.sign(window.page + '|' + t.toString()),
    5. "t": t,
    6. };
    • 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 文件中的位置,了解其含义:

    1. (func $__wbindgen_add_to_stack_pointer (;752;) (export "__wbindgen_add_to_stack_pointer") (param $var0 i32) (result i32)
    2. local.get $var0
    3. global.get $global0
    4. i32.add
    5. global.set $global0
    6. global.get $global0
    7. )

    再跟进到 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 要改为自己的:  

    1. import re
    2. import time
    3. import requests
    4. import hashlib
    5. headers = {
    6. "user-agent": "yuanrenxue,project"
    7. }
    8. cookies = {
    9. "sessionid": " your sessionid "
    10. }
    11. url = "https://match.yuanrenxue.com/api/match/20"
    12. def main():
    13. num_add_total = 0
    14. for page_num in range(1, 6):
    15. timestamp = str(int(time.time() * 1000))
    16. sign = hashlib.md5((str(page_num) + "|" + timestamp + "D#uqGdcw41pWeNXm").encode()).hexdigest()
    17. params = {
    18. "page": page_num,
    19. "sign": sign,
    20. "t": timestamp
    21. }
    22. response = requests.get(url, headers=headers, cookies=cookies,
    23. params=params)
    24. num_add = 0
    25. for i in range(10):
    26. value = response.json()['data'][i]
    27. num = re.findall(r"'value': (.*?)}", str(value))[0]
    28. num_add += int(num)
    29. num_add_total += num_add
    30. print(num_add_total)
    31. if __name__ == '__main__':
    32. main()
  • 相关阅读:
    求以下两个代码的原理流程图怎么画
    重学了计算机网络,略有小成,经验全部分享出来
    MySQL权限与安全管理之权限表与账户管理
    「MySQL高级篇」MySQL索引原理,设计原则
    ASP.NET 中验证的自定义返回和统一社会信用代码的内置验证实现
    国产化之 .NET Core 操作达梦数据库DM8的两种方式
    电商仓储物流是什么?
    TCP的三次挥手、四次握手
    飞腾性能调优
    Autostrade per l’Italia选择LITESTAR 4D进行隧道照明设计
  • 原文地址:https://blog.csdn.net/Yy_Rose/article/details/126661696