WebAssembly 案例分析与爬取实战

WebAssembly 简介

WebAssembly 是一种可以使用非 JS 编程语言编写代码并且能在浏览器上运行的技术

借助 Emscripten 工具,我们能将 C/C++ 文件转成 wasm 格式的文件, JS 可以直接调用该文件执行其中的方法

这样做的好处如下:

一些核心逻辑(比如 API 参数的加密逻辑) 使用 C/C++ 实现,这样这些逻辑就可以"隐藏" 在编译生成的 wasm 文件中, 其逆向难度比 JS 更大

一些逻辑基于 C/C++ 编写的, 有更高的执行效率, 这使得以各种语言编写的代码都可以以接近原生的速度在 Web 中运行

对于这种类型的网站,我们一般会看到网站会加载一些 wasm 后缀的文件,这就是 WebAssembly 技术常见的呈现形式, 即原生代码被编译成了 wasm 后缀的文件, JS 通过调用 wasm 文件得到对应的计算结果, 然后配合其他 JS 代码实现页面数据的加载和页面的渲染

案例介绍

网站: https://spa14.scrape.center

我们按照常规的方法,加载首页,然后通过 Nerwork 面板分析 Ajax 请求,可以看到,这里就找到了第一页数据的 Ajax 请求, limit , offset 参数用来控制分页, sign 参数用来做校验,它的值是一个数字。通过观察后面几页的内容,我们发现 sign 的值一直在变化

因此,这里关键就是找到 sign 值的生成逻辑, 我们再模拟请求即可

我们这里设置一个 Ajax 断点, 在 Sources 面板的 XHR/fetch Breakpins 这里添加一个断点, 内容为 /api/movie , 就是在请求加载数据的时候进入断点

然后翻页,就可以看到页面执行到断点的位置就停了下来

这里我们通过 Call Stack 找到构造逻辑, 经过简单的查找和推测, 我们可以判断逻辑入口在 onFetchData 方法里

我们可以看到, params 有三个参数,分别是 limit , offset , sign 这和 Ajax 请求一致。

如果在平时可以继续添加断点,进一步验证其正确性

这里的关键参数就是 sign 了, 可以看到它的值是用变量 e 表示的,而 e 的生成代码就在上面

var n = (this.page - 1) * this.limit

, e = this.$wasm.asm.encrypt(n, parseInt(Math.round((new Date).getTime() / 1e3).toString()));

可以看到,它通过调用了 this.$wasm.asm 对象的 encrypt 方法传入了 n 和一个时间戳构造出来的,接下来我们进一步调试, 在

var n = (this.page - 1) * this.limit

添加断点,重新刷新页面,运行到该断点停下来

这相当于 JS 上下文处于 onFetchData 方法内部,所以我们可以访问方法内部的所有变量, 比如 this , this.$wasm 等

接下来我们就在 Watch 面板添加一个 this.$wasm , 先看看它是什么对象

可以看到,这个 this.$wasm 对象里面又定义了很多对象和方法,其中包括了 asm 对象。因为代码中又调用了 asm 对象的 encrypt 来产生 sign , 所以我们进一步看看 asm 对象, encrypt 方法都是什么? 我们可以看到 asm 对象里面又包含了几个对象和方法, 比较重要的就是 encrypt 方法了, 其中它的 [[ function ]] 指向另一个位置, 名称是 Wasm.wasm:0xd9 。因为我们就是想知道这个方法内部是什么逻辑,所以直接点击进入

可以看到我们进入了一个不是 JS 代码的位置,文件名称叫作 Wasm.wasm ,在代码中我们可以看到 encrypt 字样

(func $encrypt (;4;) (export "encrypt") (param $var0 i32) (param $var1 i32) (result i32)

local.get $var0

local.get $var1

i32.const 3

i32.div_s

i32.add

i32.const 16358

i32.add

如果你了解汇编语言的话,这里会发现有点汇编语言的味道

这其实就是 wasm 文件,这里面的逻辑其实原本是用 C++ 编写的, 通过 Emscripten 转化为 wasm 文件, 就成了现在的样子

这时候我们找下 Network 请求, 搜索 wasm 后缀文件

可以看到,这里就有 wasm 后缀的文件, 其逻辑就是刚才看到的内容。到了这里代码就看不懂了

解决方法有两种: 一种是直接把 wasm 文件反编译, 还原成 C++ 代码,这种方法上手难度大,需要了解 WebAssembly 和逆向相关的知识,另一种就是通过模拟执行的方式来直接得到加密结果

这里我们主要使用第二种方案,拿到 wasm 文件, 然后通过 Python 模拟执行的方式调用 wasm 文件, 模拟调用它的 encrypt 方法,传入对应参数即可

模拟执行

首先我们可以把文件下载下来,复制,或者使用 Overrides 另存都可以

要使用 python 模拟执行 wasm ,可以使用两个库, 一个叫作 pywasm 另一个叫作 wasmer-python 前者使用简单,后者功能强大

pywasm

这个库比较简单,其主要功能就是加载 wasm 文件, 然后用 Python 执行

安装 pip install pywasm

然后是加载文件

复制代码
import pywasm

runtime = pywasm.load("./Wasm.wasm")
print(runtime)

<pywasm.Runtime object at 0x00000163CA23C150>

这里我们调用了 pywasm 的 load 方法,直接将 wasm 文件的路径传入, 实现了 wasm 文件的读取, 返回的结果是一个 pywasm.Runtime 类型的对象

有了这个 Runtime 对象之后,我们就可以调用它的 exec 方法来模拟执行 Wasm 里面的方法

比如,在网页中我们可以看到它执行了 encrypt 方法,并传入了两个参数。我们来试一下, 要调用 wasm 的方法, 只需要调用 Runtime 对象的 exec 方法并传入对应的方法名和参数内容即可

复制代码
runtime = pywasm.load("./Wasm.wasm")
result = runtime.exec('encrypt', [1, 2])
print(result)

16359

这里我们调用了 exec 方法,第一个参数就是要调用的 wasm 中的方法名, 这里我们传入字符串 encrypt , 第二个参数是一个列表, 代表 encrypt 方法所接收的参数, 如果是两个,那么列表的长度就是 2, 参数与列表元素一 一对应即可,结果出来了,但似乎并不是我们想要的,因为参数是我们自定义的,而要想真正模拟 Ajax 请求, 就要用网站里的实参。通过逻辑分析,我们知道传入的参数其实是一个 offset 和一个时间戳

后者的实现是这样的

parseInt(Math.round((new Date).getTime() / 1e3).toString())

这时 JS 的实现,我们将其输出到控制台

输出的其实是一个时间戳,结果是数值类型, 位数是10位。 使用 python 实现同样的结果

import time

int(time.time())

最终,我们可以将爬虫逻辑实现如下

复制代码
import pywasm
import time
import requests

BASE_URL = 'https://spa14.scrape.center'
TOTAL_PATE = 10

runtime = pywasm.load('./Wasm.wasm')
for i in range(TOTAL_PATE):
    offset = i * 10
    sign = runtime.exec('encrypt', [offset, int(time.time())])
    url = f'{BASE_URL}/api/movie/?limit=10&offset={offset}&sign={sign}'
    response = requests.get(url)
    print(response.json())

{'count': 103, 'results': [{'id': 21, 'name': '黄金三镖客', 'alias': 'Il buono, il brutto, il cattivo.', 'cover':

这里省略了很多内容。。。。。。

'https://p0.meituan.net/movie/b0d986a8bf89278afbb19f6abaef70f31206570.jpg@464w_644h_1e_1c', 'categories': ['剧情', '历史', '战争'], 'published_at': '1993-11-30', 'minute': 195, 'score': 9.5, 'regions': ['美国']}, {'id': 100, 'name': '魂断蓝桥', 'alias': 'Waterloo Bridge', 'cover': 'https://p0.meituan.net/movie/58782fa5439c25d764713f711ebecd1e201941.jpg@464w_644h_1e_1c', 'categories': ['剧情', '爱情', '战争'], 'published_at': '1940-05-17', 'minute': 108, 'score': 9.5, 'regions': ['美国']}]}

这里我们先定义了 TOTAL_PAGE 是 10, 就是 10 页, 然后开始一个 for 循环遍历, i 就是 0-9 的数字 offset 就是 0, 10 ,20.....90 , sign 就是利用刚才的实现,将参数转化为 offset 变量和时间戳,最后构造出 URL 即可

wasmer-python

除了使用 pywasm 库, 我们还可以使用另一个库 wasmer-python 来完成同样的操作。 相比 pywasm , wasmer-python 的更能更为 强大,它提供了更为底层的 API 。如果遇到更为复杂的wasm 调用情形, 推荐使用 wasmer-python

安装: pip install wasmer wasmer_compiler_cranelift

注意: 2024. 8.12 , 截止今天,python 3.10 以上的版本不支持

复制代码
from wasmer import engine, Store, Module, Instance
from wasmer_compiler_cranelift import Compiler

store = Store(engine.JIT(Compiler))
module = Module(store, open('./Wasm.wasm', 'rb').read())
instance = Instance(module)
result = instance.exports.encrypt(1, 2)
print(result)

在 pychrm 中会提示,但是可以正常执行

16359

更多关于 API 参考: https://wasmerio.github.io/wasmer-python/api/wasmer

根据前面的逻辑,我们再实现一下爬取过程

复制代码
import requests
import time
import pywasm
from wasmer import engine, Store, Module, Instance
from wasmer_compiler_cranelift import Compiler

store = Store(engine.JIT(Compiler))
module = Module(store, open('Wasm.wasm', 'rb').read())
instance = Instance(module)

BASE_URL = 'https://spa14.scrape.center'
TOTAL_PAGE = 10

runtime = pywasm.load('./Wasm.wasm')
for i in range(TOTAL_PAGE):
    offset = i * 10
    sign = instance.exports.encrypt(offset, int(time.time()))
    url = f'{BASE_URL}/api/movie/?limit=10&offset={offset}&sign={sign}'
    response = requests.get(url)
    print(response.json())

运行结果一样

相关推荐
凄凄迷人3 小时前
前端基于Rust实现的Wasm进行图片压缩的技术文档
前端·rust·wasm·图片压缩
码力码力我爱你15 小时前
Vue Application exit (SharedArrayBuffer is not defined)
linux·前端·javascript·qt·vue·wasm·webassembly
我叫卷卷卷呀4 天前
rust + bevy 实现小游戏 打包成wasm放在浏览器环境运行
开发语言·rust·wasm
我码玄黄6 天前
Rust语言初探:WebAssembly 入门
开发语言·rust·wasm
Atypiape223 天前
Webpack 5 支持访问 Rust WebAssembly 线性内存
javascript·rust·wasm·webassembly
2402_8575834923 天前
Blazor WebAssembly革新:C#的Web开发新纪元
前端·c#·wasm
friklogff25 天前
【Rust光年纪】探索Rust语言中的WebAssembly利器:核心功能、安装配置与API概览
开发语言·rust·wasm
码力码力我爱你1 个月前
QT for webassembly
qt·wasm
不穿铠甲的穿山甲1 个月前
使用emcc将libOpendrive编译成wasm
wasm