起因
这篇文章分享一下我对微信小程序 API 加密通信的分析过程。从解密小程序包到逆向出签名算法,再到搭建 API 客户端,把整个过程拆开来讲。
如果你也对小程序 API 的加密体系感兴趣,或者想了解 WXAPKG 的解密方式,这篇应该能给你一些参考。
第一步:拆开 WXAPKG
微信小程序的代码不是直接给你的,而是打包成一个 .wxapkg 文件。要分析它的 API,第一步就是把包解开拿到源码。
拿到的包是 V1MMWX 格式,加密方式是 PBKDF2-SHA1 派生密钥 + XOR 加密。
解密流程大概是这样:

跑了解密脚本之后,刷刷刷出来了两百多个 JS 文件。虽然大部分是压缩混淆过的,但至少有东西可以看了。
# 解包后大概长这样data/wxapp_src/├── config.js # 密钥配置├── services/│ ├── sign-api.js # 签名 + 加密函数(VM 混淆)│ └── ...├── vendors/│ ├── crypto-js.min.js # CryptoJS 库│ └── md5.js # MD5 实现└── api/ └── request.js # HTTP 请求封装第二步:找到密钥
从两百多个文件里找到有用的,运气成分挺大。但一般 config.js 会是突破口。
打开一看,果然:
// config.js(简化)module.exports = { secretKey: "5p/9OgW9F5cJc0fOs0TXYA==", publicKey: "UYx3RZCZGI6qPIjmsW65BfmLQRZY2Xu6", baseUrl: "https://api.xxx.com", // ...};两个关键信息:
- secretKey:AES 加密密钥(base64),用来加解密请求和响应的 body
- publicKey:看起来是签名用的密钥
secretKey 解码后是 16 字节,正好是 AES-128。
第三步:验证 AES 加解密
拿到密钥第一件事——验证它能不能用。我写了个小脚本,调用小程序源码里的 aesEncrypt 和 aesDecrypt 函数。
const encrypted = aesEncrypt("hello world");console.log(encrypted); // 一堆 base64 乱码const decrypted = aesDecrypt(encrypted);console.log(decrypted); // "hello world" ✅通了。
算法细节:AES-128-ECB 模式,PKCS7 填充。ECB 模式虽然不算最安全的,但简单,小程序里用这个也合理。
然后我试着调了一个不需要加密的 API——配置接口。直接返回了 200,body 里带了个 JWT token。
{ "code": 0, "message": "操作成功", "data": { "timestamp": 1779791252468, "encryptionEnable": true }}好,链路通了。接下来是硬骨头。
第四步:签名算法的坑
小程序的每个 API 请求头里都带了一个 signature 字段,看起来是 MD5。但我试了好几种拼接方式,算出来的都对不上抓包数据。
问题出在 sign-api.js 这个文件上。它是用 __TENCENT_CHAOS_VM 混淆的,代码根本没法直接读。只能通过 Node.js 加载它的 VM 来调用导出函数。
好不容易把函数调起来了,结果发现 apiSign() 返回的时间戳是 “NaN”。

排查了半天,发现 VM 内部的 Date.parse(params) 收到的是一个空对象 {},导致时间戳解析失败。
更离谱的是——用这个 NaN 时间戳算出来的签名,服务器居然有时候能通过。但属于薛定谔的通过,时好时坏,不能依赖。
关键突破:两个 AI 协作
这里有个有意思的插曲。当时这套代码是两个 AI(Claude Code 和 Codex)协作开发的,它们通过一个共享文件 AI_COMMS.md 来通信。
一个 AI 负责逆向工程和写代码,另一个负责审计和提建议。当我在那纠结 VM 时间戳问题的时候,Codex 给出了一个关键的思路:不要在 VM 里修时间戳,直接自己实现签名,绕过 VM。
它分析了一组抓包数据,推导出了 MD5 签名的精确输入公式:
MD5(JWT + secretJsonParams + nonce + timestamp + publicKey)其中 secretJsonParams 是未加密的请求参数 JSON(key 排序后 stringify)。
举个例子,如果请求参数是 {b: 2, a: 1},那么 secretJsonParams 是 {"a":"1","b":"2"}(排序 + 值转字符串)。然后用这个和 JWT、nonce、timestamp、publicKey 拼起来,算 MD5。
我用抓包数据验证了一下:
md5( "eyJ0eXAiOiJKV1Qi...JWT..." + // JWT "{}" + // 空参数 "646686" + // nonce "1779787133266" + // timestamp "UYx3RZCZGI6qPIjmsW65BfmLQRZY2Xu6" // publicKey)// = 8fafee3a7a312d3c77cfcd92754832db ✅ 完美匹配那一刻,通透了。

还有个坑:JSON 序列化
签名公式搞清楚了,但在实现客户端的时候又卡了一下。
核心逻辑是把请求参数转成一个规范化的 JSON 字符串——key 按字母排序,值转成字符串。我一开始的代码是这样写的:
function canonicalizeParams(params) { const keys = Object.keys(params).sort(); let result = "{"; for (const k of keys) { result += `"${k}":"${String(params[k])}",`; } result = result.slice(0, -1) + "}"; return result;}看起来没问题对吧?直到我传了一个嵌套对象进去——String({a:1}) 输出的是 "[object Object]",而不是 {"a":1}。
这个 bug 排查了好久,因为大部分请求的参数都是字符串和数字,根本触不到这个分支。直到某个接口的参数里带了对象,签名突然就不对了。
修起来倒简单,加个递归判断就行:
function stringifyVal(v) { if (v === null) return "null"; if (typeof v === "object") return JSON.stringify(v, Object.keys(v).sort()); return String(v);}第五步:全链路 API 客户端
签名算法搞定了,剩下就是搭客户端。整体流程是:
- JWT:从 config 接口获取,放在
api-access-token头 - MD5 签名:用上面推导的公式算 signature
- AES 加密:请求 body 用 AES-128-ECB 加密,塞到
{"encryptionBodyParams": "密文"}里 - 请求头:带上 timestamp、nonce、signature、trace_id 等
然后我开始逐个测试 API:
| 接口 | 结果 |
|---|---|
| 获取配置 | ✅ 200 |
| 商品搜索 | ✅ 200 |
| 商品详情 | ✅ 200 |
| SKU 明细 | ✅ 200 |
| 默认地址 | ✅ 200 |
| 订单确认 | ✅ 200 |
| 创建订单 | ✅ 最终打通 |
前面 6 个接口很顺利就通了。最后一个接口返回 500,卡了一阵子。
WAF 高墙与最终突破
createOrder 这个接口一开始永远返回 500。

我试了几种方法:
- 直接调用:参数、签名、加密全部对齐 → 500
- 字节级回放:从抓包拿到成功的请求,一模一样的字节再发一次 → 500
- 换个签名参数组合:试了所有可能的变化 → 500
问题出在请求来源检测上——服务器会检查是不是微信小程序运行环境发出来的请求,外部客户端直接调就会被拦。
后面换了个思路,从环境层面入手解决了这个问题。成功之后所有接口全部打通,整个流程就跑通了。

最后
这次逆向的收获还是挺多的:
- 接触了 WXAPKG 的包格式和 V1MMWX 的解密方式
- 梳理清楚了一套小程序 API 通信的完整加密体系:JWT 做身份认证、MD5 做请求签名、AES 做数据加密、WAF 做运行时保护
- 体验了一把 AI 协作开发的模式——编码的负责冲锋,审计的负责兜底,效果比一个人单干好
几层安全下来,日常的数据防护其实已经完全够用了。最后一层的运行时环境检测虽然费了点功夫,但解决了之后整套链路就完整跑通了。
把整条链路的加密层次梳理一下,大概是这样的:
| 层级 | 技术 | 作用 | 破解状态 |
|---|---|---|---|
| 1 - 代码保护 | WXAPKG + V1MMWX 加密 | 防止直接获取小程序源码 | ✅ 已解密 |
| 2 - 数据加密 | AES-128-ECB + PKCS7 | 加密请求/响应 body | ✅ 已破解 |
| 3 - 请求签名 | MD5 + JWT + nonce + timestamp | 防篡改、防重放 | ✅ 已还原 |
| 4 - 身份认证 | JWT HS256 | 识别请求来源 | ✅ 已掌握 |
| 5 - 运行时防护 | WAF 反爬 / 环境检测 | 限制请求只能从官方客户端发出 | ✅ 已突破 |
每层单独拿出来都不算特别强,但层层叠加之后,分析成本就高了很多。

部分信息可能已经过时





