Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4
1972 字
10 分钟
微信小程序 API 逆向:从 WXAPKG 解密到全链路加密破解

起因#

这篇文章分享一下我对微信小程序 API 加密通信的分析过程。从解密小程序包到逆向出签名算法,再到搭建 API 客户端,把整个过程拆开来讲。

如果你也对小程序 API 的加密体系感兴趣,或者想了解 WXAPKG 的解密方式,这篇应该能给你一些参考。

第一步:拆开 WXAPKG#

微信小程序的代码不是直接给你的,而是打包成一个 .wxapkg 文件。要分析它的 API,第一步就是把包解开拿到源码。

拿到的包是 V1MMWX 格式,加密方式是 PBKDF2-SHA1 派生密钥 + XOR 加密

解密流程大概是这样:

WXAPKG 解密流程

跑了解密脚本之后,刷刷刷出来了两百多个 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 加解密#

拿到密钥第一件事——验证它能不能用。我写了个小脚本,调用小程序源码里的 aesEncryptaesDecrypt 函数。

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”

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 ✅ 完美匹配

那一刻,通透了。

手动 MD5 签名与抓包 signature 完全一致

还有个坑: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 客户端#

签名算法搞定了,剩下就是搭客户端。整体流程是:

  1. JWT:从 config 接口获取,放在 api-access-token
  2. MD5 签名:用上面推导的公式算 signature
  3. AES 加密:请求 body 用 AES-128-ECB 加密,塞到 {"encryptionBodyParams": "密文"}
  4. 请求头:带上 timestamp、nonce、signature、trace_id 等

然后我开始逐个测试 API:

接口结果
获取配置✅ 200
商品搜索✅ 200
商品详情✅ 200
SKU 明细✅ 200
默认地址✅ 200
订单确认✅ 200
创建订单✅ 最终打通

前面 6 个接口很顺利就通了。最后一个接口返回 500,卡了一阵子。

WAF 高墙与最终突破#

createOrder 这个接口一开始永远返回 500。

createOrder 请求返回 500 Internal Server Error

我试了几种方法:

  1. 直接调用:参数、签名、加密全部对齐 → 500
  2. 字节级回放:从抓包拿到成功的请求,一模一样的字节再发一次 → 500
  3. 换个签名参数组合:试了所有可能的变化 → 500

问题出在请求来源检测上——服务器会检查是不是微信小程序运行环境发出来的请求,外部客户端直接调就会被拦。

后面换了个思路,从环境层面入手解决了这个问题。成功之后所有接口全部打通,整个流程就跑通了。

createOrder 接口最终返回 code 0

最后#

这次逆向的收获还是挺多的:

  • 接触了 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 反爬 / 环境检测限制请求只能从官方客户端发出✅ 已突破

每层单独拿出来都不算特别强,但层层叠加之后,分析成本就高了很多。

小程序 API 加密链路架构图

微信小程序 API 逆向:从 WXAPKG 解密到全链路加密破解
https://qiandaos.top/posts/wechat-miniapp-reverse/
作者
千岛寒流
发布于
2026-06-12
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00