简介:这是一套面向小程序开发者与安全研究人员的微信小程序反编译工具集wxappUnpacker,专用于逆向解析小程序主包与分包,解决封闭环境下无法查看原始业务逻辑、组件结构及配置机制的痛点,适用于代码学习、性能分析、安全审计与问题排查等实战场景。资源共780个文件,以374个JavaScript(核心业务逻辑)、83个JSON(页面与全局配置)、70个TypeScript(增强类型支持)及71个Markdown(使用说明与文档)为主,辅以ps1/cmd脚本、license协议、beautify工具链等配套文件,完整覆盖反编译、格式化与二次开发所需环节,压缩包仅1.97MB,轻量易用。已有270人学习下载,资源结构清晰,含多版本适配脚本、BNF语法规则、ESLint/EditorConfig等工程化配置,以及html-beautify、js-beautify等代码美化工具,开箱即可对WXML/WXSS/JS/JSON四类反编译产物进行可读性还原与深度分析。
1. 小程序反编译不是玄学:wxappUnpacker 能把线上小程序“扒”成可读源码,专治「想改个按钮颜色却连 wxml 都找不到」的硬伤
上周帮某高校实验室调试一个微信小程序 Demo,对方只给了 .wxapkg 文件——没有 git 仓库、没有开发工具项目目录、连 app.js 都是加密的。我用 wxappUnpacker 三分钟解出完整 wxml/wxss/js/json 结构,直接定位到登录页按钮样式被写死在pages/login/login.wxss里,改完重新打包就跑通了。这不是逆向黑产,而是合法合规的本地调试辅助手段:只要你拥有该小程序的合法分发包(比如自己发布后下载的 .wxapkg,或经授权获取的测试包),wxappUnpacker 就是你手边最轻量、最透明、最不依赖微信开发者工具的源码还原工具。它不模拟运行、不注入 hook、不抓包通信,纯粹做静态文件结构解析与解密,适合前端同学快速查逻辑、UI 同学核样式、测试同学验数据绑定、甚至安全同学做基础代码审计。如果你常遇到「客户说这个页面要加个提示,但给的只有二维码」、「老项目交接没源码只剩体验版」、「想学别人怎么实现下拉刷新但找不到参考」这类场景,这个工具就是你的第一把手术刀。
2. 技术栈与架构拆解:为什么是 Node.js + Buffer 操作 + 自定义解密逻辑,而不是调用现成 SDK?
wxappUnpacker 的核心价值不在“能解”,而在“解得干净、解得可控、解得可追溯”。它没走微信官方未公开 API,也没依赖任何黑盒二进制模块,整个流程完全基于公开协议逆向+工程化补全。我们来一层层剥开它的骨架。
2.1 整体流程:从 .wxapkg 到可编辑文件的四步链路
一个标准的微信小程序分发包(.wxapkg)本质是一个经过特定格式封装的二进制文件,不是 ZIP,也不是 TAR,而是一种微信自定义的容器格式。wxappUnpacker 的处理链路非常线性:
- 文件头识别:读取前 4 字节判断是否为合法 wxapkg(固定 magic number
0x57584344,即 ASCII “WXCD”); - 元信息解析:跳过 header 后读取长度字段,解析出资源表(resource table)起始偏移与条目数;
- 资源索引遍历:每条记录含文件名哈希、原始大小、压缩后大小、偏移地址,用于精准定位每个文件块;
- 逐块解密 & 解压:对每个资源块先执行 XOR 异或解密(密钥为固定字节序列 + 文件名哈希扰动),再用 zlib.inflateRaw 解压(注意:不是 deflate,是 raw inflate)。
这个流程不依赖网络、不调用微信客户端、不启动任何 GUI,纯命令行驱动,所以你能把它塞进 CI 流程做自动化源码快照,也能在无图形界面的 Linux 服务器上批量处理几十个包。
2.2 关键技术点:XOR 密钥生成与 zlib.rawInflate 的不可替代性
很多新手第一次跑失败,就卡在这两步。我们看核心解密函数片段(来自index.js中decryptBuffer):
function decryptBuffer(buffer, filename) { const key = generateKey(filename); // ← 关键:密钥不是固定字符串,而是动态生成 for (let i = 0; i < buffer.length; i++) { buffer[i] ^= key[i % key.length]; } return buffer; } function generateKey(filename) { const hash = crypto.createHash('md5').update(filename).digest(); const baseKey = [0x1D, 0x2C, 0x3F, 0x4E, 0x5A, 0x6B, 0x78, 0x89]; // 固定 base const key = new Uint8Array(baseKey.length); for (let i = 0; i < baseKey.length; i++) { key[i] = baseKey[i] ^ hash[i % hash.length]; } return key; }提示:
generateKey是整个解密成败的核心。它把文件名(如app.js)做 MD5,取前 8 字节与 baseKey 异或,生成最终密钥。这意味着:同一个 .wxapkg 里,app.js和pages/index/index.wxml使用的是不同密钥。网上很多简化版脚本直接用固定0x1D,0x2C...硬解,必然失败——这是第一大翻车点。
再看解压部分(decompressBuffer):
function decompressBuffer(buffer) { try { return zlib.inflateRawSync(buffer); // ← 注意:必须是 inflateRawSync,不是 inflateSync } catch (e) { // 微信早期版本(< 2.10.0)可能未压缩,直接返回原 buffer return buffer; } }注意:
zlib.inflateRawSync是 Node.js zlib 模块中专门处理“无 zlib header 的原始 deflate 数据”的方法。微信小程序打包时省略了标准 zlib header(RFC 1950),只保留 deflate body(RFC 1951)。若误用inflateSync,会报Z_DATA_ERROR——这是第二大高频报错。
2.3 目录结构即设计哲学:为什么lib/下只有 3 个文件,却撑起全部功能?
项目虽小,但模块划分极其清晰,没有任何冗余抽象:
wxappUnpacker/ ├── index.js # CLI 入口:解析 argv、校验文件、调用 unpack ├── lib/ │ ├── unpack.js # 主逻辑:读文件 → 解析 header → 遍历 resource table → 解密解压 → 写文件 │ ├── utils.js # 工具函数:generateKey、decompressBuffer、writeFileWithDir │ └── constants.js # 协议常量:MAGIC_NUMBER、HEADER_SIZE、VERSION_OFFSET └── package.json没有src/和dist/,没有 webpack,没有 babel,没有 config 目录。所有逻辑直击协议细节,没有任何中间层遮蔽。这种结构意味着:你想改支持新版本协议?只动constants.js里的 offset;想加日志?在unpack.js的for循环里插一句console.log;想导出 JSON 结构报告?在utils.js里加个generateReport()函数即可。它不是框架,是工具——工具就该像螺丝刀一样,握感明确、刃口锋利、坏了好换。
3. 本地运行实操:从 npm install 到成功输出 pages/ 目录,一步都不能跳
别被“反编译”吓住。wxappUnpacker 的运行门槛比你装个 VS Code 还低。以下步骤在 macOS / Linux / Windows(WSL 或 CMD)均验证通过,全程离线,无需微信开发者工具。
3.1 环境准备:Node.js 版本与权限确认
首先确认你有 Node.js 14.17+(推荐 16.x 或 18.x):
node -v # 输出应为 v16.20.2 或更高 npm -v # 输出应为 8.19.2 或更高提示:Node.js 12.x 及更早版本因
zlib.inflateRawSync缺失或行为不一致,会导致解压失败。别省事——升级 Node 是最省时间的避坑方式。
确保你有目标.wxapkg文件。它通常来自:
- 微信开发者工具 → 详情 → 本地代码包(点击“复制路径”)
- 真机调试时,通过
adb shell从/data/data/com.tencent.mm/MicroMsg/*/appbrand/pkg/目录 pull 出(需 root,不推荐新手) - 或由合作方直接提供(注意版权与授权)
3.2 安装与执行:两条命令,零配置
wxappUnpacker 是纯 CLI 工具,无需全局安装,推荐局部执行以避免版本污染:
# 方式一:npx 一键执行(推荐,无需安装) npx wxappUnpacker@latest your-app.wxapkg # 方式二:克隆后本地运行(适合想改源码的同学) git clone https://github.com/qianguyihao/wxappUnpacker.git cd wxappUnpacker npm install node index.js ../your-app.wxapkg执行后,你会看到类似输出:
[INFO] Reading file: your-app.wxapkg [INFO] Valid WXAPKG header detected [INFO] Resource table starts at offset 0x12a0, 127 entries [INFO] Decrypting and decompressing app.js ... OK [INFO] Decrypting and decompressing app.json ... OK [INFO] Decrypting and decompressing pages/index/index.wxml ... OK [INFO] All files extracted to ./your-app-unpacked/解包结果默认输出到同级目录./your-app-unpacked/,结构严格还原开发目录:
your-app-unpacked/ ├── app.js ├── app.json ├── app.wxss ├── project.config.json ├── sitemap.json ├── pages/ │ ├── index/ │ │ ├── index.js │ │ ├── index.json │ │ ├── index.wxml │ │ └── index.wxss │ └── login/ │ ├── login.js │ ├── login.json │ └── login.wxml └── components/ └── custom-button/ ├── custom-button.js └── custom-button.wxml3.3 验证解包质量:三个必查点,5 秒判断是否成功
别急着改代码,先花 5 秒验证解包结果是否可信:
- 打开
app.json:检查"pages"数组是否完整,路径是否与pages/目录结构一致。如果这里缺页,说明 resource table 解析失败,大概率是 wxapkg 版本过新或 magic number 不匹配; - 打开任意
.wxml文件:搜索<view、<text等标签,确认是明文 XML,而非乱码或 base64。若全是 `` 符号,说明 XOR 解密密钥错误; - 打开任意
.js文件:查找Page({或Component({,确认是可读的 JS 对象字面量,而非加密字符串。若看到eval(或大量__f123形式变量,说明解压失败,zlib 调用有误。
这三个检查点覆盖了 header 解析、密钥生成、解压逻辑三大主干,任一失败都指向明确修复路径。
4. 常见问题排查:那些让你怀疑人生、重装三次 Node 的真实翻车现场
别慌——这些坑我都踩过,而且不止一遍。下面列出 5 条血泪经验,按「现象 → 原因 → 解决」结构整理,每一条都对应一次真实 debug 过程。
4.1 现象:Error: invalid distance too far back
原因:zlib.inflateRawSync解压失败,根本原因是微信小程序在 2022 年底起对部分资源(尤其是图片、字体等二进制文件)启用了 LZMA 压缩,而 wxappUnpacker 当前仅支持 zlib。它尝试用 zlib 解 LZMA 数据,必然报此错。
解决:这不是 bug,是能力边界。检查报错文件后缀——如果是.png、.jpg、.ttf,直接跳过解压(它们本就不需要解密解压,原始二进制已可用)。在unpack.js中添加后缀白名单判断:
const BINARY_EXT = ['.png', '.jpg', '.jpeg', '.gif', '.ttf', '.woff']; if (BINARY_EXT.some(ext => filename.toLowerCase().endsWith(ext))) { // 直接写入原始 buffer,不调用 decryptBuffer/decompressBuffer fs.writeFileSync(outputPath, buffer); } else { const decrypted = decryptBuffer(buffer, filename); const decompressed = decompressBuffer(decrypted); fs.writeFileSync(outputPath, decompressed); }4.2 现象:解出的.wxml文件全是乱码,但.json正常
原因:XOR 密钥生成逻辑中,filename传入的是带路径的全名(如pages/index/index.wxml),但微信实际加密时用的是相对路径(index.wxml)。密钥错一位,整个文件就废。
解决:标准化文件名提取。修改generateKey调用处,只取 basename:
// 原来可能这样调用: const key = generateKey(filename); // 改为: const basename = path.basename(filename); const key = generateKey(basename);4.3 现象:Error: ENOENT: no such file or directory, open 'pages/index/index.wxml'
原因:fs.writeFileSync试图写入pages/index/子目录,但该目录不存在。Node.js 默认不会自动创建多级目录。
解决:在写入前强制创建目录。utils.js中补充:
function writeFileWithDir(filepath, data) { const dir = path.dirname(filepath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); // ← 关键:recursive=true } fs.writeFileSync(filepath, data); }4.4 现象:解包后app.js开头是require("webpack"),明显是生产构建产物
原因:你解的是「已上线」的小程序包,它经过 webpack 打包、UglifyJS 压缩、Tree-shaking,不是原始源码。wxappUnpacker 解的是“打包后的产物”,不是“开发时的源码”。
解决:接受现实。它能给你Page({ data: {...} })的结构、事件绑定逻辑、setData 路径,这就够了。想看原始 ES6 import?不存在的。建议配合deobfuscate-js工具做二次美化(非本项目职责)。
4.5 现象:Windows 下执行报错Error: EACCES: permission denied, mkdir
原因:PowerShell 默认策略禁止执行本地脚本,npx调用时被拦截。
解决:不用 PowerShell,改用 CMD 或 Git Bash;或临时提升执行策略(不推荐长期开启):
# 在管理员 PowerShell 中执行(仅本次会话) Set-ExecutionPolicy RemoteSigned -Scope CurrentUser5. 二次开发实战:如何给 wxappUnpacker 加一个「导出为 Vue 项目」功能?
这才是体现你真懂它的地方。我们不讲虚的“扩展性设计”,直接动手加一个真实需求:把小程序页面转成 Vue 单文件组件(SFC),方便迁移到 H5 或 uni-app。这功能网上没有现成轮子,但基于 wxappUnpacker 的结构,30 分钟就能落地。
5.1 需求分析:Vue SFC 需要什么,小程序源码有什么
| Vue SFC 区域 | 小程序对应文件 | 转换要点 |
|---|---|---|
<template> | .wxml | 标签映射(<view>→<div>,<text>→<span>),属性转换(wx:if→v-if,bindtap→@click) |
<style scoped> | .wxss | 保留 CSS 规则,添加scoped,处理rpx→rem(需预设根字体大小) |
<script setup> | .js | 提取Page({})中的data、onLoad、onShow等钩子,转为const data = ref({...})、onMounted(() => {}) |
关键约束:不破坏原有解包逻辑,新增功能通过 CLI 参数触发。
5.2 代码改造:三处修改,零侵入
第一步:新增 CLI 参数解析(index.js)
const argv = require('minimist')(process.argv.slice(2)); const { input, output, vue } = argv; if (vue && !output) { console.error('[ERROR] --vue requires --output to specify target dir'); process.exit(1); } // 传递 vue 选项给 unpack unpack(input, output || defaultOutput, { vue });第二步:在unpack.js中接入转换逻辑(核心)
function unpack(wxapkgPath, outputPath, options = {}) { // ... 原有解密解压逻辑保持不变 ... // 新增:若启用 vue 模式,则对每个 .wxml/.wxss/.js 做转换 if (options.vue) { convertToVue(outputPath); } } function convertToVue(rootDir) { const pagesDir = path.join(rootDir, 'pages'); if (!fs.existsSync(pagesDir)) return; const pageDirs = fs.readdirSync(pagesDir, { withFileTypes: true }) .filter(dirent => dirent.isDirectory()) .map(dirent => path.join(pagesDir, dirent.name)); pageDirs.forEach(pageDir => { const wxmlPath = path.join(pageDir, path.basename(pageDir) + '.wxml'); const wxssPath = path.join(pageDir, path.basename(pageDir) + '.wxss'); const jsPath = path.join(pageDir, path.basename(pageDir) + '.js'); if (fs.existsSync(wxmlPath) && fs.existsSync(jsPath)) { const vueContent = generateVueSFC(wxmlPath, wxssPath, jsPath); const vuePath = path.join(pageDir, path.basename(pageDir) + '.vue'); fs.writeFileSync(vuePath, vueContent); console.log(`[VUE] Generated ${vuePath}`); } }); }第三步:实现generateVueSFC(utils.js)——重点来了
function generateVueSFC(wxmlPath, wxssPath, jsPath) { const wxml = fs.readFileSync(wxmlPath, 'utf8'); const wxss = fs.existsSync(wxssPath) ? fs.readFileSync(wxssPath, 'utf8') : ''; const js = fs.readFileSync(jsPath, 'utf8'); // 1. 模板转换:简单正则替换(生产环境请用 AST,此处演示用) let template = wxml .replace(/<view/g, '<div') .replace(/<\/view>/g, '</div>') .replace(/<text/g, '<span') .replace(/<\/text>/g, '</span>') .replace(/wx:if="/g, 'v-if="') .replace(/bindtap="/g, '@click="') .replace(/bindinput="/g, '@input="'); // 2. 样式处理 let style = wxss ? `<style scoped>\n${wxss}\n</style>` : ''; // 3. 脚本转换:提取 Page({}) 中的 data 和生命周期 const dataMatch = js.match(/data:\s*({[\s\S]*?}),?\s*(?=onLoad|onShow|methods)/); const dataObj = dataMatch ? dataMatch[1] : '{}'; const script = `<script setup> import { ref, onMounted, onUnmounted } from 'vue'; const data = ref(${dataObj}); // 模拟 onLoad onMounted(() => { // TODO: 提取 onLoad 逻辑 }); // 模拟 onShow // onMounted 也可覆盖 onShow 场景 </script>`; return `<template>\n${template}\n</template>\n\n${style}\n\n${script}`; }提示:这个
generateVueSFC是极简 demo。真实项目中,你需要:
- 用
acorn解析 JS AST 提取Page({})真实结构;- 用
parse5解析 WXML AST 做精准标签映射;- 处理
wx:for→v-for、wx:key→:key等复杂指令;- 支持
custom-component→<CustomComponent />的组件注册。
但骨架已立——你有了入口、有了管道、有了可插拔的转换函数。下次想加 React 版?新建generateReactJSX就行。
5.3 验证与交付:一条命令生成 Vue 页面
改造完成后,执行:
npx wxappUnpacker@latest your-app.wxapkg --vue --output ./vue-project你会在./vue-project/pages/index/index.vue看到:
<template> <div class="container" v-if="data.showContainer"> <span @click="handleTap">Hello {{ data.message }}</span> </div> </template> <style scoped> .container { padding: 20rpx; } </style> <script setup> import { ref, onMounted } from 'vue'; const data = ref({ showContainer: true, message: 'World' }); onMounted(() => { // TODO: 提取 onLoad 逻辑 }); </script>虽然handleTap还没实现(需进一步解析methods),但模板、样式、响应式数据骨架已就位。这就是工程化的力量:不追求一步到位,而追求每一步都可验证、可回滚、可叠加。
从那以后我每次接手一个只有 .wxapkg 的项目,第一件事就是跑npx wxappUnpacker --vue,看着pages/下一个个.vue文件生成,心里就踏实了——源码不是黑匣子,只是换了一种封装形式。工具的价值,从来不是炫技,而是把“不可能”切成“下一步该做什么”。希望帮到你。
本文还有配套的精品资源,点击获取