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

案例目标        

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

本题目标:抓取这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 指令在堆栈上运行,考虑到上述函数中的每一步,它解释如下:

  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 中定义:

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()
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值