声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除!
逆向目标
目标:某游快爆微信小程序
目标页面:游戏论坛
逆向参数:sign
工具总结
| 工具 | 用途 | 链接 |
|---|---|---|
| Reqable | 抓包工具,自带 MCP 服务器 | 官网 | GitHub 仓库 |
| Qoder | AI 智能体客户端,串联所有工具(也可换成 Claude Code / Codex / Cursor 等) | 官网 |
| reverse-skill | 逆向技能路由包,让 AI 具备完整的逆向分析能力 | GitHub 仓库 |
| First | 微信小程序安全调试工具(Frida + CDP + MCP),解包、Hook、调试一体化 | GitHub 仓库 |
逆向分析
前两篇我们逆向的是 APP 里的 so,又是 Frida 反调试绕过、又是 OLLVM 解混淆,属于硬碰硬。这一篇换个小程序试试------并且全部由 AI 配合工具链完成。
一、工具配置
工欲善其事,必先利其器。这次的核心思路是:把抓包工具、调试工具通过 MCP 协议接入 AI。
1. Reqable:抓包 + MCP 服务器
Reqable 是一个抓包工具(官网,GitHub 仓库),它自带 MCP 服务器,能让 AI 直接读取抓包记录、生成 curl、重放请求等等。
打开 Reqable,在"调试"菜单里找到 MCP 选项:

点击后,弹出手动配置 MCP 的窗口,核心信息就一个------MCP 服务器的可执行文件路径,以及对应的 JSON 配置:

配置 JSON 长这样:
json
{
"mcpServers": {
"reqable": {
"command": "D:\\App\\reqable\\Reqable\\mcp-server.exe",
"args": []
}
}
}
可选参数有三个:
| 参数 | 说明 |
|---|---|
| --host | Reqable 所在设备的 IP 地址,默认本机 127.0.0.1 |
| --port | Reqable 的代理端口,默认自动获取(通常是 8888) |
| --scope | 注册哪些工具:minimal 仅常用工具(默认),all 注册全部工具 |
2. 给 AI 接上 Reqable
我这里用的 AI 客户端是 Qoder(官网),大家也可以换成自己习惯的 Agent。在 Qoder 的连接器页面,选择"通过 JSON 导入",把刚才的配置粘贴进去:

导入成功后,AI 就具备了"控制 Reqable "的能力。
3. 安装 reverse-skill
接下来安装 reverse-skill(逆向技能路由包,20k+ stars)。这是一个专门给 AI 用的技能包,里面封装了几十条逆向相关的工作流,覆盖 APK 逆向、JS 签名、协议分析、渗透测试等方向,兼容 Claude Code /Codex / Cursor 等主流 AI 客户端。
安装很简单:把仓库地址发给 AI,让它自己装就行(装进自己的 skills 目录,然后加载)。

安装这个主要是让AI有一个基本的逆向思路,不会胡乱跑。
其实这个skill本身是没有小程序逆向部分的,不过有前端JS逆向的部分,两者思路基本是相通的,所以拿过来用一下。
4. First:小程序安全调试工具
小程序逆向和 APP 逆向不一样,我们没法直接对小程序做动态调试。这里用到的是 First (GitHub 仓库,基于 WMPFDebugger 二次开发,感兴趣的可以了解一下)。
它通过 Frida 注入微信进程,再用 CDP(Chrome DevTools Protocol)协议桥接 ,让浏览器里的 DevTools 直接调试小程序,支持 wxapkg 解密解包、wx.* API 捕获、云函数动态捕获、路由枚举等。自 v1.1.0 起闭源,最新版直接去 GitHub Releases 下载编译好的版本 (Releases 地址)。
注意它对微信版本有要求,仓库主页有 WMPF 版本支持列表,目前推荐微信 4.1.10,可以按列表去下载对应版本。
打开 First,点击启动,它会自动注入微信:

注意运行日志,Frida 注入成功后会提示 you can now open any miniapps,并给出一段 DevTools 调试链接。
First 同样自带 MCP 服务器(端口 4554),在 First 的 MCP 页面开启服务,然后把它的配置也导入 Qoder:

json
{
"mcpServers": {
"first-miniapp": {
"url": "http://127.0.0.1:4554/sse"
}
}
}
然后打开任意小程序,验证一下 First 是否工作正常:复制控制台里的链接:
devtools://devtools/bundled/inspector.html?ws=127.0.0.1:62000
粘贴到浏览器地址栏,如果打开了 DevTools 调试界面,说明 First 启动成功:

5. 连接器就绪
回到 Qoder,确认三个连接器全部开启:QoderWork(本机执行能力)、reqable(抓包数据)、first-miniapp(小程序调试):

工具全部安装好了,接下来我们开始。
二、抓包
打开 Reqable,然后在微信里打开好游快爆小程序,进入香肠派对的游戏论坛,随便滑动一下列表。回到 Reqable,可以看到抓到了一条 POST 请求:

请求信息如下:
| 项目 | 内容 |
|---|---|
| 请求地址 | https://m.3839.com/miniapp/api.php?m=api&c=topic&a=section_topic&v=1.1 |
| 请求方式 | POST(application/x-www-form-urlencoded) |
| 接口作用 | 获取某个游戏版块(section_id)的帖子列表,按编辑时间排序、分页 |
| 关键参数 | sign:32 位十六进制字符串,典型 MD5 hex 特征 |
请求体里的参数:
| 参数 | 值 | 说明 |
|---|---|---|
| version | 1.0.0 | 接口版本 |
| timestamp | 1787542341 | 请求时间戳 |
| type | 1 | 登录态类型 |
| token | xxxxx | 登录凭证 |
| uid | xxxxx | 用户 ID |
| section_id | 11631 | 游戏版块 ID(香肠派对) |
| topic_list_type | all | 帖子列表类型 |
| sort | edit_time | 排序方式 |
| last_id | 0 | 分页游标 |
| cursor | 0 | 分页游标 |
| screen_id | 0 | 屏幕来源 |
我们要分析的就是 sign 参数。
三、交给 AI
1. 一句话下指令
在 Qoder 里输入指令:
加载 📦 reverse-skill;使用 🧩 reqable 熟悉请求;使用 🧩 first-miniapp 尝试破解 sign 参数。

然后,就是泡杯茶静静的等待了~~
2. 分析结果
大约 15 分钟后,结果出来了。
AI 做的事分两步:先通过 first-miniapp 对小程序解包,拿到 JS 源码 ;再通过 reqable 读取抓包记录,拿到真实参数。两者一对照,直接在源码里定位到了签名函数。
签名函数定位在 app-service.js 第 10562 行 ,是一个叫 l() 的通用请求封装函数,所有接口请求都会走它。核心逻辑如下(为便于阅读,做了格式化):
javascript
var l = function (e) {
// 盐值,硬编码在源码里
var r = "miniapp_kgb77Mdac046167ec7bd4d3cde27ad6NBV";
// 基础参数 {version, timestamp} + 登录态 {type, token, uid} + 业务参数
var c = Object.assign({version: "1.0.0", timestamp: parseInt(Date.now() / 1e3)}, t, ...业务参数);
delete c.sign; // 删掉旧的 sign
c = h(c); // 空值参数置空(本例参数全非空,可跳过)
// 按 key 字典序排序,拼成 key=value&key=value 的串
var u = Object.keys(c),
l = u.sort().map(e => e + "=" + c[e]).join("&");
l += r; // 末尾直接拼盐
c.sign = Hash("md5").update(l).digest("hex"); // 整串做 MD5
formPost(e, c); // 表单提交
};
大概就是:把除 sign 外的所有参数,按参数名排序,拼成 key=value&key=value 的串,末尾加上盐,整串做 MD5。
用抓包里的真实参数复算验证一下:
cursor=0&last_id=0&screen_id=0§ion_id=11631&sort=edit_time×tamp=1787542341&token=【脱敏】&topic_list_type=all&type=1&uid=【脱敏】&version=1.0.0
+ miniapp_kgb77Mdac046167ec7bd4d3cde27ad6NBV
得到 e0a1d6b18a3ecc8f4f63c2c8f82b438a------与抓包里的 sign 完全一致。
AI 还顺手发现了两个彩蛋:同一个源码里,login 接口的签名用的不是这个盐,而是 9C198906FAD3D528EB5A504D77065913,且 key 是倒序拼接;另一个接口也用了不同的盐。不同接口的签名格式不一样,不要搞混了。
3. AI 的经验总结
AI 还总结了一套"学到什么":

两个重点:一是小程序不像 APP 有 so 层原生保护,wxapkg 解包后就是明文 JS ,salt 直接硬编码在里面,难度低了一个量级;二是分析路径非常标准:抓包看参数结构 → 反编译搜 md5/sign 关键词 → 提取函数上下文 → 用真实请求复算闭环验证。
四、脚本验证
我这边让 AI 把算法用 Python 复现一遍:
接下来我需要进行验证,请你写一个 python 脚本复现此算法,参数就使用刚刚抓包抓到的值。
AI 生成的 sign_reproduce.py 如下(token/uid 已脱敏):
python
# -*- coding: utf-8 -*-
"""
好游快爆小程序 API 签名复现脚本
目标接口: POST https://m.3839.com/miniapp/api.php?m=api&c=topic&a=section_topic&v=1.1
请求格式: application/x-www-form-urlencoded
签名算法(逆向自 app-service.js 中的通用请求封装函数 l()):
1. 收集参数: 基础参数 {version, timestamp} + 登录态 {type, token, uid} + 业务参数
2. 删除值为空字符串 / null 的参数(小程序端 h() 函数干的活,本例参数全非空,可跳过)
3. 按参数名(key)字典序升序排序
4. 拼成 "k1=v1&k2=v2&..." 的字符串
5. 字符串末尾直接拼接盐值
6. 整串做 MD5,输出 32 位小写 hex,就是 sign
盐值(硬编码在小程序 JS 里):
miniapp_kgb77Mdac046167ec7bd4d3cde27ad6NBV
运行: python sign_reproduce.py
"""
import hashlib
# ---------- 配置区 ----------
# 盐值,从反编译源码 app-service.js 提取
SALT = "miniapp_kgb77Mdac046167ec7bd4d3cde27ad6NBV"
# 抓包参数(2026-08-24 11:32 Reqable 记录 ID=57, 不含 sign 本身)
params = {
"version": "1.0.0",
"timestamp": "1787542341",
"type": "1",
"token": "【脱敏:发布前替换为真实值或打码】",
"uid": "【脱敏:发布前替换为真实值或打码】",
"section_id": "11631",
"topic_list_type": "all",
"sort": "edit_time",
"last_id": "0",
"cursor": "0",
"screen_id": "0",
}
# 抓包中的真实 sign,用来比对验证
captured_sign = "e0a1d6b18a3ecc8f4f63c2c8f82b438a"
# ---------- 签名复现 ----------
def calc_sign(data: dict, salt: str) -> tuple[str, str]:
"""复现小程序端签名逻辑,返回 (sign, 拼接串)"""
# 第 3 步: 按 key 字典序排序, 第 4 步: 拼成 k=v&k=v
raw = "&".join(f"{k}={data[k]}" for k in sorted(data.keys()))
# 第 5 步: 末尾拼盐
raw += salt
# 第 6 步: 整体 MD5
sign = hashlib.md5(raw.encode("utf-8")).hexdigest()
return sign, raw
# ---------- 主流程 ----------
if __name__ == "__main__":
sign, raw_string = calc_sign(params, SALT)
print(f"参与签名的参数个数: {len(params)}")
print()
print("拼接串(排序后 k=v&k=v + 盐):")
print(raw_string)
print()
print(f"本脚本算出的 sign: {sign}")
print(f"抓包里的真实 sign: {captured_sign}")
print()
if sign == captured_sign:
print(">>> MATCH: 算法复现成功, sign 与抓包完全一致")
else:
print(">>> 不匹配!请检查参数是否与抓包一致、是否有空值参数被过滤")
运行:

11 个参数参与签名,脚本算出的 sign 和抓包里的真实 sign 一模一样,算法复现成功。
五、实战测试
为了严谨一点,我们换一个游戏的论坛,完整模拟一次带上自己 sign 的请求。
根据 AI 的分析,本接口的游戏标识参数是 section_id :11631 代表香肠派对,93 代表原神------我们就拿原神测试。
接下来你写一个 python 脚本,将 section_id 换成 93,然后模拟完整请求。

AI 生成的 sign_request.py 核心逻辑如下(签名计算函数与上面完全一致,不重复贴,只贴请求部分):
python
import gzip
import hashlib
import io
import json
import time
import urllib.request
import urllib.parse
# ---------- 配置区 ----------
SALT = "miniapp_kgb77Mdac046167ec7bd4d3cde27ad6NBV"
API_URL = "https://m.3839.com/miniapp/api.php?m=api&c=topic&a=section_topic&v=1.1"
# 业务参数: section_id 换成 93, 其余沿用抓包值(uid/token 是你自己的登录态)
params = {
"version": "1.0.0",
"timestamp": str(int(time.time())), # 当前时间戳, 动态生成
"type": "1",
"token": "【脱敏:发布前替换为真实值或打码】",
"uid": "【脱敏:发布前替换为真实值或打码】",
"section_id": "93", # <<< 换游戏: 11631 -> 93
"topic_list_type": "all",
"sort": "edit_time",
"last_id": "0",
"cursor": "0",
"screen_id": "0",
}
# 请求头, 照抄抓包(Referer 和 UA 都是小程序环境标识, 缺了可能被风控拦)
headers = {
"Host": "m.3839.com",
"User-Agent": ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 "
"MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI "
"MiniProgramEnv/Windows WindowsWechat/WMPF "
"WindowsWechat(0x63090a13) UnifiedPCWindowsWechat(0xf2541a35) XWEB/19977"),
"Content-Type": "application/x-www-form-urlencoded",
"Referer": "https://servicewechat.com/wxcce6440ef3727704/10/page-frame.html",
"Accept": "*/*",
"xweb_xhr": "1",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9",
}
# ---------- 主流程 ----------
if __name__ == "__main__":
# 1) 算签名
sign, raw_string = calc_sign(params, SALT)
params["sign"] = sign
print(f"[1] 签名计算完成: sign={sign}")
# 2) 组装表单并发送
body = urllib.parse.urlencode(params)
req = urllib.request.Request(API_URL, data=body.encode("utf-8"), headers=headers, method="POST")
print(f"[2] 发送请求: POST {API_URL}")
# 3) 接收响应(注意响应体是 gzip 压缩的, urllib 不会自动解压, 需要手动处理)
with urllib.request.urlopen(req, timeout=15) as resp:
status = resp.status
raw = resp.read()
if resp.headers.get("Content-Encoding", "").lower() == "gzip":
raw = gzip.decompress(raw)
raw = raw.decode("utf-8", errors="replace")
print(f"[3] 响应状态码: {status}")
# 4) 解析 JSON 并展示关键信息
data = json.loads(raw)
print(f" code: {data.get('code')} msg: {data.get('msg')}")
result = data.get("result") or {}
posts = result.get("data") or []
print(f" 返回帖子数: {len(posts)}")
if posts:
first = posts[0]
print(f" 首条帖子: id={first.get('id')} 内容={str(first.get('content'))[:40]}"
f" 版块sid={first.get('sid')}")
运行:

输出关键信息:code: 100、msg: ok、返回帖子数: 10、首条帖子版块 sid=93------服务端验签通过,返回的正是原神版块的数据。
测试成功,本次逆向完成。
总结
总的来说,这个小程序的加密不算很复杂,用更聪明的模型应该可以更快分析出来。
不过,AI 的发展可真的令人惊叹啊,以前要几天的工作量,现在泡杯茶的功夫就搞定了。
至此,本篇结束。