您的位置:首页 > 娱乐 > 明星 > WebAssembly 案例分析与爬取实战

WebAssembly 案例分析与爬取实战

2024/12/23 11:04:50 来源:https://blog.csdn.net/qq_39217312/article/details/141116580  浏览:    关键词: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 pywasmruntime = 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 requestsBASE_URL = 'https://spa14.scrape.center'
TOTAL_PATE = 10runtime = pywasm.load('./Wasm.wasm')
for i in range(TOTAL_PATE):offset = i * 10sign = 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 Compilerstore = 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 Compilerstore = Store(engine.JIT(Compiler))
module = Module(store, open('Wasm.wasm', 'rb').read())
instance = Instance(module)BASE_URL = 'https://spa14.scrape.center'
TOTAL_PAGE = 10runtime = pywasm.load('./Wasm.wasm')
for i in range(TOTAL_PAGE):offset = i * 10sign = 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())

运行结果一样

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com