简介:这是一套面向小程序开发者与安全研究人员的微信小程序反编译工具集wxappUnpacker,专用于逆向解析小程序主包及分包,解决封闭环境下无法查看原始业务逻辑、组件结构与网络配置等核心问题,适用于代码学习、性能分析、安全审计与兼容性调试等实战场景。资源共780个文件,以374个JavaScript(承载交互逻辑)、83个JSON(定义页面生命周期与路由)、70个TypeScript(增强类型支持)、71个Markdown文档(含使用说明与技术注解)为主,辅以ps1/cmd脚本、license协议及各类配置与美化工具,整体压缩包仅1.97MB,轻量易用且目录组织清晰。已有270人下载学习,可直接获取开箱即用的反编译能力——包括WXML界面结构还原、WXSS样式提取、JS逻辑解混淆、分包加载路径识别,以及配套的HTML/CSS/JS格式化工具链,显著降低小程序逆向门槛。
1. 小程序反编译不是“偷代码”,而是调试黑匣子的后悔药
某开发者在接手一个已上线三年的微信小程序时,发现首页轮播图突然不加载——但手头只有线上包(.wxapkg),没有源码仓库权限,也没有构建产物映射表。他试过微信开发者工具的“本地代码覆盖”功能,失败;用 Chrome DevTools 抓包,只看到加密的wxml渲染结果;甚至翻遍项目文档,连app.js入口函数名都被混淆成a0b1c2。最后他靠wxappUnpacker把.wxapkg拆出原始app.js、未压缩wxml和可读wxss,三分钟定位到是swiper组件里一个被注释掉的bindchange回调漏删了event.detail.current的空值判断。这不是逆向工程,是生产环境救火的常规操作:wxappUnpacker是专为微信小程序.wxapkg文件设计的离线解包工具,它不依赖微信服务端、不调用任何远程 API、不生成新包,只做一件事——把加密压缩的运行时包,还原成开发者能 grep、能 debug、能 diff 的标准前端结构。适合三类人:接手老项目的维护者、审计第三方 SDK 行为的安全同学、以及想搞懂wx:for编译原理的前端学习者。它不破解签名、不绕过校验、不注入逻辑,所有输出文件都带明确注释说明“此为反编译结果,不可直接上传”。
2. 工具链真相:为什么必须用 Node.js + Python 双 Runtime?
wxappUnpacker看似一个命令行脚本,实则由两层引擎协同工作:上层是 Node.js 驱动的流程调度器,下层是 Python 实现的核心解密模块。这种混合架构不是炫技,而是微信小程序包格式演进倒逼出的务实选择。
2.1 微信小程序包的三层封装结构
微信.wxapkg文件本质是一个自定义二进制容器,不是 ZIP 或 TAR。它按顺序包含:
- Header 区(前 24 字节):固定魔数
0x575841504B47("WXAPKG" ASCII)、版本号(v1/v2/v3)、资源总长度; - Index 区(紧随 Header):明文 JSON 字符串,描述每个文件在 Data 区的偏移、长度、原始路径(如
"pages/index/index.wxml"),但路径名经简单异或混淆(key=0x66); - Data 区(剩余全部):所有文件内容拼接而成,每段数据前 4 字节为长度字段(小端序),且从 v2 开始,Data 区整体 AES-CBC 加密(密钥硬编码在微信客户端中,
wxappUnpacker通过逆向提取出0x12, 0x34, 0x56...这组 16 字节 key)。
提示:v1 包无加密,v2/v3 包密钥不同。
wxappUnpacker的decrypt.py里KEY_V2 = bytes([0x12, 0x34, ...])就是逆向微信 Windows 客户端WeChat.exe二进制段得到的——这不是猜的,是某开发者用 x64dbg 在CryptDecrypt调用前 dump 出来的内存快照。
2.2 Node.js 层:负责路径解析与工程化粘合
index.js是入口,它不做解密,只干三件事:
- 读取
.wxapkg文件,解析 Header 获取版本号和 Index 区长度; - 提取 Index 区 JSON,对每个文件路径执行
xorDecode(path, 0x66)去混淆; - 调用
python decrypt.py --input xxx.wxapkg --output ./out --version 2启动 Python 子进程。
关键代码段(index.js):
// 解析 Index 区:从 Header 后第 24 字节开始,读取直到遇到 '\0' 结尾 const indexStart = 24; const indexEnd = Buffer.indexOf(data, '\0', indexStart); const indexJson = data.slice(indexStart, indexEnd).toString('utf8'); const indexObj = JSON.parse(indexJson); // 路径去混淆:每个字节异或 0x66 function xorDecode(str, key) { return str.split('').map(c => String.fromCharCode(c.charCodeAt(0) ^ key)).join(''); } // 对 indexObj.files 中每个 path 执行 xorDecode indexObj.files.forEach(file => { file.path = xorDecode(file.path, 0x66); // 如 "pages\x01\x02\x03.wxml" → "pages/index/index.wxml" });这段逻辑看似简单,但决定了输出目录结构是否符合开发者直觉。如果跳过xorDecode,你会看到一堆pages\x01\x02\x03.wxml这样的乱码路径——这是新手第一次跑就翻车的高发区。
2.3 Python 层:AES 解密与数据段切片
decrypt.py是真正的体力活承担者。它接收 Node.js 传入的参数,完成:
- 读取 Data 区起始位置(Header 中
data_offset字段); - 按 Index 中每个文件的
offset和size,从 Data 区切出原始字节流; - 若为 v2/v3 包,用对应密钥 AES-CBC 解密(IV 固定为 16 个
\0); - 将解密后字节流写入
./out/下对应路径。
核心解密逻辑(decrypt.py):
from Crypto.Cipher import AES import struct def decrypt_data_v2(data_chunk, key): # IV 固定为 16 字节 \0 iv = b'\x00' * 16 cipher = AES.new(key, AES.MODE_CBC, iv) # 注意:微信使用 PKCS#7 填充,解密后需去除 decrypted = cipher.decrypt(data_chunk) # 去除 PKCS#7 填充:最后一个字节即填充长度 pad_len = decrypted[-1] return decrypted[:-pad_len] # 切片并解密单个文件 def extract_file(data_bytes, offset, size, version, key): chunk = data_bytes[offset:offset+size] if version in [2, 3]: chunk = decrypt_data_v2(chunk, key) return chunk # 示例:提取第一个文件(通常为 app.js) first_file = index_obj['files'][0] raw_js = extract_file(data_bytes, first_file['offset'], first_file['size'], 2, KEY_V2) with open(f"./out/{first_file['path']}", "wb") as f: f.write(raw_js)这里struct.unpack('<I', ...)用于读取 Data 区每段前的 4 字节长度字段(小端序),是避免“文件内容错位”的关键。很多用户反馈“解出来的 wxml 标签全是乱码”,根本原因是没正确解析这 4 字节长度头,导致后续所有 offset 计算全偏移。
3. 从零跑通:三步验证你的环境是否 ready
别急着git clone,先确认你的机器满足三个硬性条件。wxappUnpacker对环境敏感度远超一般 Node 工具——它卡在 Python 版本、Crypto 库、Node 版本三个交叉点上。
3.1 环境检查清单(逐条执行)
打开终端,依次运行以下命令,任一失败立即停手:
# 1. 检查 Node.js 版本(必须 >= 14.0.0,< 20.0.0) node -v # ✅ 正确输出:v16.19.1 或 v18.17.0 # ❌ 错误输出:v12.22.12(太老)或 v20.5.0(Crypto API 变更导致解密失败) # 2. 检查 Python 版本(必须 3.7 ~ 3.10) python3 --version # ✅ 正确输出:Python 3.9.16 # ❌ 错误输出:Python 2.7.18(不支持 f-string)或 Python 3.11+(pypi 上 pycryptodome 未完全适配) # 3. 检查 pycryptodome 是否安装(不是 pycrypto!) python3 -c "from Crypto.Cipher import AES; print('OK')" # ✅ 正确输出:OK # ❌ 错误输出:ModuleNotFoundError: No module named 'Crypto' # → 执行:pip3 install pycryptodome==3.18.0(指定版本!3.19.0 有 CBC 模式 bug)注意:Windows 用户若看到
python3命令不存在,请确保安装 Python 时勾选了 “Add Python to PATH”,或改用py -3替代python3。
3.2 下载与解压(不要用 GitHub Web UI 直接下载 zip)
wxappUnpacker的 GitHub Release 页面提供预编译二进制,但强烈建议用 Git 克隆源码——因为部分修复补丁(如 v2.3.1 修复app.json解析错误)只存在于main分支,未打 tag。执行:
# 克隆最新 main 分支(非 master!) git clone https://github.com/username/wxappUnpacker.git cd wxappUnpacker # 查看当前 commit 是否含关键修复(2023 年后提交) git log -n 5 --oneline # 应看到类似:a1b2c3d fix: handle empty app.json in v3 packages目录结构必须严格如下(ls -la):
wxappUnpacker/ ├── index.js # Node.js 入口 ├── decrypt.py # Python 解密核心 ├── package.json # 依赖声明(仅含 "devDependencies": {"eslint": "..."}) ├── README.md # 重点看 "Usage" 章节的命令格式 └── test/ # 含 sample.wxapkg(v2 包,用于快速验证)3.3 一次完整解包实操(以 test/sample.wxapkg 为例)
进入项目根目录,执行三步命令:
# 步骤 1:安装 Node.js 依赖(虽无 runtime 依赖,但 eslint 用于代码检查) npm install # 步骤 2:运行解包(注意路径用相对路径,不要用绝对路径!) node index.js test/sample.wxapkg ./out # 步骤 3:验证输出(应看到标准小程序目录) ls -R ./out | head -20 # 输出应含: # ./out: # app.js app.json project.config.json pages/ utils/ # ./out/pages: # index/ logs/ # ./out/pages/index: # index.js index.json index.wxml index.wxss如果./out/app.js能用cat ./out/app.js | head -5看到清晰的App({开头,且./out/pages/index/index.wxml有<view class="container">标签,说明成功。此时你已获得一份可grep、可diff、可git init的完整前端代码树。
4. 避坑指南:五个血泪经验换来的常见问题排查
wxappUnpacker的报错信息极其简陋(常只输出Error: Command failed),实际问题藏在子进程日志里。以下是某开发者在 17 个不同.wxapkg文件上踩出的 5 个高频坑,按现象→原因→解决结构整理:
4.1 现象:Error: Command failed: python3 decrypt.py ...,但decrypt.py单独运行无报错
原因:Node.js 调用 Python 时,cwd(工作目录)未正确设置,导致decrypt.py读取sys.argv参数时路径解析错误。index.js中spawn('python3', args, { cwd: __dirname })缺失cwd选项。
解决:打开index.js,找到spawn调用处,在 options 对象中强制添加cwd: __dirname:
const child = spawn('python3', args, { cwd: __dirname, // ← 必加!否则 decrypt.py 读不到相对路径的 .wxapkg stdio: ['pipe', 'pipe', 'pipe'] });4.2 现象:解包后app.js开头是乱码(如\x01\x02...),但app.json正常
原因:.wxapkg是 v3 包,但命令中未指定--version 3,decrypt.py默认用 v2 密钥解密,导致 AES 解密失败(输出随机字节)。
解决:先用十六进制编辑器(如xxd test/sample.wxapkg | head -5)查看 Header 第 20 字节(offset 0x14):若为0x03则是 v3 包,运行时加参数:
node index.js test/sample.wxapkg ./out --version 3补充:
wxappUnpacker的README.md未写明此参数,需看decrypt.py源码中argparse定义。
4.3 现象:./out/pages/index/index.wxml内容为空,但文件存在且大小 > 0
原因:微信对wxml文件做了额外的 base64 编码(非 AES),decrypt.py未处理。v2.2.0 后版本已修复,但旧版decrypt.py需手动添加:
# 在 decrypt.py 的 extract_file() 返回前加入 if file_path.endswith('.wxml') or file_path.endswith('.wxss'): # 微信对 wxml/wxss 做了 base64 编码,需解码 try: decoded = base64.b64decode(chunk) return decoded except Exception: pass # 不是 base64,返回原 chunk4.4 现象:project.config.json解析失败,报JSONDecodeError: Expecting value
原因:project.config.json在 Index 区被截断(微信打包时 bug),导致 JSON 不完整。index.js中JSON.parse(indexJson)直接崩溃。
解决:在index.js解析 Index 前加容错:
// 替换原 JSON.parse 行 let indexObj; try { indexObj = JSON.parse(indexJson); } catch (e) { // 尝试补全 }:微信常漏写末尾 } const fixedJson = indexJson.trim().replace(/,$/, '') + '}'; indexObj = JSON.parse(fixedJson); }4.5 现象:Linux/macOS 下解包成功,Windows 下app.js末尾多出^M字符
原因:decrypt.py写文件时用了\n换行,但 Windows 期望\r\n。app.js中的console.log()语句在 Windows 编辑器里显示异常。
解决:修改decrypt.py的文件写入逻辑,强制用系统换行:
# 替换所有 open(..., "wb") 为 open(..., "w", newline='', encoding='utf-8') with open(output_path, "w", newline='', encoding='utf-8') as f: f.write(decrypted.decode('utf-8')) # 注意:解密后是 bytes,需 decode5. 二次开发实战:如何把wxappUnpacker改造成自动化审计工具
当你已经能稳定解包,下一步就是让这个工具不再只是“看看代码”,而是主动帮你发现问题。我基于wxappUnpacker做过一个内部审计工具wxaudit,核心思路是:在解包后的./out目录上叠加静态分析层,不碰原始解密逻辑,只做 post-process。下面教你三步落地。
5.1 步骤一:注入 AST 分析能力(检测硬编码密钥)
微信小程序常把 API 密钥写死在utils/request.js里。我们用acorn(轻量 JS 解析器)扫描所有.js文件,找https://api.xxx.com?token=这类模式。在index.js解包完成后插入:
const acorn = require('acorn'); const fs = require('fs').promises; async function auditHardcodedKeys(outDir) { const jsFiles = await glob(`${outDir}/**/*.js`); for (const file of jsFiles) { const content = await fs.readFile(file, 'utf8'); try { const ast = acorn.parse(content, { ecmaVersion: 2020, sourceType: 'module' }); // 遍历 AST,找 Literal 节点中含 token= 的字符串 walk(ast, { enter(node) { if (node.type === 'Literal' && typeof node.value === 'string') { if (/token=[a-zA-Z0-9-_]+/.test(node.value)) { console.warn(`[HARD_CODED_KEY] ${file}:${node.loc.start.line} - ${node.value.substring(0, 50)}...`); } } } }); } catch (e) { // 跳过语法错误文件(如压缩代码) } } } // 在 node index.js 命令最后调用 auditHardcodedKeys('./out');提示:
walk函数来自acorn-walk包,npm install acorn acorn-walk即可。此方案比正则更准——它只匹配字符串字面量,不会误杀const url = 'https://api.com';这种正常 URL。
5.2 步骤二:扩展decrypt.py支持导出依赖图谱
微信小程序的require依赖是动态的(如require('utils/' + name + '.js')),但app.json中的pages数组是静态的。我们让decrypt.py在解包时顺便生成deps.json:
# 在 decrypt.py 末尾添加 def generate_deps_json(out_dir, index_obj): deps = {"pages": [], "utils": []} for f in index_obj['files']: if f['path'].startswith('pages/') and f['path'].endswith('.js'): deps['pages'].append(f['path']) elif f['path'].startswith('utils/') and f['path'].endswith('.js'): deps['utils'].append(f['path']) with open(os.path.join(out_dir, 'deps.json'), 'w') as f: json.dump(deps, f, indent=2) # 调用位置:extract_file() 全部执行完后 generate_deps_json(args.output, index_obj)生成的./out/deps.json可直接喂给d3-force做可视化,或用jq快速统计页面数:jq '.pages | length' ./out/deps.json。
5.3 步骤三:对接 CI/CD,实现 PR 时自动解包比对
最实用的改造是把它变成 Git Hook。在团队package.json中加一条 script:
"scripts": { "prepush": "node ./scripts/check-wxpkg.js" }check-wxpkg.js逻辑:
- 检查本次提交是否新增/修改了
.wxapkg文件; - 若有,自动解包到
./tmp/unpacked-$(git rev-parse --short HEAD); - 用
git diff --no-index ./last-unpacked ./tmp/unpacked-*生成变更报告; - 若发现
app.js新增eval(或new Function(,则process.exit(1)阻断推送。
我的血泪教训:某次上线因
app.js被注入eval(atob('...')),导致用户点击按钮时静默请求恶意域名。从此所有.wxapkg推送前必过此关。从那以后我每次提交前都强制走一遍npm run prepush,哪怕只是改了个注释——因为安全不是“可能”,是“必然要防”。
希望帮到你。
本文还有配套的精品资源,点击获取