055-wasm:爬虫练习网spa15(token 纯算还原)

网站地址:爬虫练习网 spa15

这一例是「爬虫练习网」里专门练 WebAssembly(wasm) 的一关。接口返回的数据是真的电影榜单,但请求里多了一个叫 token 的参数,不带它就 403。难点在于:生成 token 的那段 JS 被编译成了 .wasm 二进制,你在 Sources 里点开只能看到一堆字节码,读不懂。别慌------这关其实不需要你去啃 wasm 里的汇编,我们照样能把它拿下。

一、找加密参数:哪个请求带了 token

打开网站,按 F12 调出开发者工具,切到 Network(网络) 面板,勾选 Fetch/XHR,刷新页面。你会看到一个发往列表接口的请求:

复制代码
GET https://spa15.scrape.center/api/movie/?limit=10&offset=0&token=MDViMWRhOGU3MTE2NmY4ZTQ3YTRlYjcwMjgxOTBkOWQzMzA0OTBkZiwxNzkzMTUyMDQ2

(这条是我用调试工具真实抓下来的,不是编的。)

它的 Query 参数长这样:

复制代码
limit  = 10
offset = 0
token  = MDViMWRhOGU3MTE2NmY4ZTQ3YTRlYjcwMjgxOTBkOWQzMzA0OTBkZiwxNzkzMTUyMDQ2

limit、offset 是翻页用的,一眼就懂;可疑的就是那串又长又像乱码的 token。它就是本例要还原的加密参数。

二、定位 token 在哪生成:先全局搜一把

token 是拼在 URL 上发出去的,那它一定是在发请求之前,由某段 JS 算出来的。我们到 Sources(源代码) 面板里按 Ctrl+Shift+F 做全局搜索,关键词先用最直白的 token------能搜到一堆,噪音大。换个思路:既然这一关叫「wasm」,那页面里多半加载了一个 .wasm 文件,直接搜 Wasm.wasm 更准。

图里能看到:搜索框输入 Wasm.wasm,底部提示「Found 1 matching line in 1 file」,命中在 chunk-1e26c652.6d5384a8.js 里,代码是 var lt="Wasm.wasm"; function ht(t){ try{ if(t==lt&&E) return new Uint8Array(E); ... throw "both async and sync fetching of the wasm failed" } ... }。

给完全没接触过 wasm 的同学补一句白话:WebAssembly 是一种能塞进浏览器里跑的「二进制字节码」 ,性能高、但人眼读不了(打开是一堆 \x00\x61\x73\x6d)。网站把「怎么算 token」这件事编译进 wasm,就是为了让你没法像看普通 JS 那样直接读懂。它通常配一个「胶水文件」(就是命中这个 chunk),负责:把 .wasm 下载下来 → 实例化 → 把里面的函数包装成 JS 能调用的样子。

三、看胶水代码:token 是调用 wasm 的哪个函数算的

顺着命中,我们把这个 chunk 用 Ctrl+P 打开,点右下角 {} 做 Pretty print(美化) (不然全挤在一行没法看),再 Ctrl+F 搜 encrypt:

图里两处关键信息:

  1. 上面是加载 wasm 的胶水:WebAssembly.instantiateStreaming(...)、t["instantiatiewasm"] ------ 确认了「先实例化 wasm 模块」;

  2. 最底下高亮那行是重点:

    t["_encrypt"] = function() {
    return (t["_encrypt"] = t["asm"]["encrypt"]).apply(null, arguments)
    }

翻译成人话:wasm 模块实例化后,它对外暴露的函数都挂在 t.asm 上;这里把 wasm 里的 encrypt 函数取出来,包装成 t._encrypt。也就是说------token 是调用 wasm 的 encrypt 函数算出来的。

到这一步,「扣 wasm」的标准做法就出来了:既然 token = encrypt(入参),而 encrypt 是 wasm 里的一个导出函数,那我们根本不用去读懂 wasm 内部 ,只要把同一个 Wasm.wasm 文件拿到 Node 里,补一个最小的运行环境,直接调用它的 encrypt,就能得到和浏览器一模一样的 token。

四、先别急着啃 wasm:把线上 token 反解一下,看看它长什么样

在动手扣 wasm 之前,我习惯先把抓到的真实 token 拆一拆,往往能省一大半力气。这串 token 看着像 base64(结尾没有 = 但字符集是 base64 的样子),那就先 base64 解码:

复制代码
MDViMWRhOGU3MTE2NmY4ZTQ3YTRlYjcwMjgxOTBkOWQzMzA0OTBkZiwxNzkzMTUyMDQ2
        ↓ base64 解码
05b1da8e71166f8e47a4eb7028190d9d330490df,1793152046

结构一下就清楚了,它是「一段 40 位十六进制字符串 + 逗号 + 一段 10 位数字」拼起来再做 base64:

  • 05b1da8e71166f8e47a4eb7028190d9d330490df ------ 40 个十六进制字符,正好是 160 位,这是 SHA1 摘要的典型长度;
  • 1793152046 ------ 10 位,是秒级的 Unix 时间戳。

于是大胆猜一句:token = base64( sha1( 某段原文 ) + "," + 时间戳 )。而 wasm 的 encrypt 收的入参,从调用处看就是「接口路径」和「时间戳」两样。

五、判断是不是「标准 SHA1」:看着像不算数,必须验

「看着像 sha1」和「它真的是标准 sha1」是两回事 ,很多网站会在 sha1 前后加盐、改拼接顺序(也就是所谓的魔改)。所以这里必须做相互证明:用标准算法在本地重算一遍,和线上真实值逐字节对齐。

我试了几种拼法,命中是 sha1(url + "," + 时间戳),其中 url = "/api/movie"(注意:不带请求 URL 里那个尾部斜杠)。用 Python 标准库直接验:

复制代码
import hashlib, base64
url, ts = "/api/movie", "1793152046"
sha1_hex = hashlib.sha1((url + "," + ts).encode()).hexdigest()
token = base64.b64encode((sha1_hex + "," + ts).encode()).decode()
print(token)

真实跑出来的结果(本地重算 vs 线上抓包):

复制代码
线上真实 token : MDViMWRhOGU3MTE2NmY4ZTQ3YTRlYjcwMjgxOTBkOWQzMzA0OTBkZiwxNzkzMTUyMDQ2
纯Python复现   : MDViMWRhOGU3MTE2NmY4ZTQ3YTRlYjcwMjgxOTBkOWQzMzA0OTBkZiwxNzkzMTUyMDQ2
本地 == 线上   : MATCH: True ✅

一模一样。说明这个 wasm 里藏的就是一个没被魔改的标准 SHA1 ,配方是 sha1("/api/movie," + 时间戳)。

顺带一提,为了确认「wasm 内部确实也是这么算的」,我也用扣 wasm 的办法(把 Wasm.wasm 丢进 Node 调 encrypt)跑了一遍,喂相同的 (url, 时间戳),得到的 token 和上面、和线上完全一致。两条路殊途同归,互相印证,可以放心。

六、端到端证明:用本地造的 token 去请求,服务器认不认

前面证明的是「我能算出和线上一样的 token」,但服务器会不会因为时间戳太旧、或另有校验而拒绝?再做一步端到端:用「此刻」的时间戳本地生成一个新 token,直接打接口。

复制代码
本地新 token : ZDgyOWQzZjViOWRjZGNiOGQwMjVjN2Y1ZTc4OGQ5Yjg2YjI5MzBlOCwxNzkzMTUyNDkw
HTTP 返回    : 200   count = 104   本页条数 = 10
第一条电影   : 霸王别姬 | Farewell My Concubine | 评分 9.5

服务器照单全收,返回真实榜单。至此,相互证明(本地==线上)+ 端到端证明(服务器接受本地 token) 双证齐全,纯算还原成立。

七、完整代码(纯算版,三行核心,连 wasm 都不用跑)

一旦看清配方,最省事的写法根本不需要 Node、也不需要 wasm,纯 Python 就够:

复制代码
import base64, hashlib, time, requests

def make_token(url="/api/movie", ts=None):
    ts = str(ts) if ts is not None else str(int(time.time()))
    sha1_hex = hashlib.sha1((url + "," + ts).encode()).hexdigest()
    return base64.b64encode((sha1_hex + "," + ts).encode()).decode()

def fetch_page(offset=0, limit=10):
    return requests.get(
        "https://spa15.scrape.center/api/movie/",
        params={"limit": limit, "offset": offset, "token": make_token()},
        headers={"Referer": "https://spa15.scrape.center/page/1",
                 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                               "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36"},
        timeout=20,
    ).json()

for page in range(3):
    d = fetch_page(offset=page * 10)
    print(f"第 {page + 1} 页:", " / ".join(x["name"] for x in d["results"][:5]))

跑起来:

复制代码
第 1 页: 霸王别姬 / 这个杀手不太冷 / 肖申克的救赎 / 泰坦尼克号 / 罗马假日
第 2 页: V字仇杀队 / 少年派的奇幻漂流 / 美丽心灵 / 初恋这件小事 / 借东西的小人阿莉埃蒂
第 3 页: 黄金三镖客 / 海洋 / 我爱你 / 阿飞正传 / 7号房的礼物

八、如果你遇到的是「真·魔改 wasm」怎么办(扣 wasm 通用法)

本例运气好,wasm 里面就是个标准 sha1,反解一下就露馅了。但很多 wasm 案例是真把自定义算法藏进了字节码,反解不出来。这时候就用第三节说的「扣 wasm」通用法:不读内部,直接把 .wasm 拿来在 Node 里跑,调它的导出函数。核心就三步:

  1. fs.readFileSync 读出 Wasm.wasm,用 WebAssembly.instantiate(buffer, imports) 实例化------imports 里补上它需要的 env.memory / env.table / wasi_snapshot_preview1.proc_exit 等(缺什么补什么,看报错);
  2. 写两个小工具:字符串 → wasm内存指针(stackAlloc + 塞字节)和 指针 → 字符串(按 \0 截断读回);
  3. 拿到 exports.encrypt,把入参(url、时间戳)转成指针丢进去,返回值再转回字符串。

这套「补最小环境 + 调导出函数」的模板,我在案例目录里也存了一份 59_wasm_gen.js,换个 wasm 站点基本改改函数名和入参就能复用。

小结

  • token = base64( sha1("/api/movie," + 秒级时间戳) + "," + 秒级时间戳 ),是标准 SHA1、无魔改;
  • wasm 只是把这段逻辑「藏」起来,读不懂字节码没关系:先反解 token 结构 常常直接看穿;看不穿就扣 wasm ,在 Node 里调它的 encrypt;
  • 判断「标准 / 魔改」不能靠猜,一定要本地重算 == 线上真实值 ,再补一步服务器接受本地 token 的端到端证明。

本例代码、真实抓包样本(_live_token_req.json)和两种生成脚本(59_纯算token.py、59_wasm_gen.js)都放在 js逆向百例/059-爬虫练习网wasm 目录下,可自行复现。

如有问题请及时提出,加油加油。

相关推荐
2601_9679791243 分钟前
0 到 1 学爬虫:用 Python 爬取网页数据指南一(附实战案例)
爬虫·python·实战案例·html解析·网页数据
姚瑞南2 小时前
【PRD】招投标商机雷达爬虫系统
人工智能·经验分享·笔记·爬虫·产品运营·产品经理
深蓝电商API2 天前
用AI生成拟人行为:让鼠标轨迹更“有人味”
人工智能·爬虫
深蓝电商API2 天前
XPath、CSS选择器、正则:什么场景用什么
爬虫·xpath·css选择器
陶醉的松鼠2 天前
SCRAPY爬虫框架使用详述
爬虫
深蓝电商API4 天前
从爬虫到Agent:数据采集的下一个形态是什么
爬虫
xiangjiaodalishi4 天前
主流 AI 爬虫 User-Agent 清单大全:分组、配置与持续维护(2026 版)
人工智能·爬虫·python
深蓝电商API5 天前
用向量数据库存储爬取内容:语义检索的高级玩法
爬虫·向量数据库
深蓝电商API5 天前
Playwright+多模态大模型:让AI自己“看”页面并决策
人工智能·爬虫·playwright