
简介这是一套面向小程序开发者与安全研究人员的微信小程序反编译工具集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 操作 自定义解密逻辑而不是调用现成 SDKwxappUnpacker 的核心价值不在“能解”而在“解得干净、解得可控、解得可追溯”。它没走微信官方未公开 API也没依赖任何黑盒二进制模块整个流程完全基于公开协议逆向工程化补全。我们来一层层剥开它的骨架。2.1 整体流程从 .wxapkg 到可编辑文件的四步链路一个标准的微信小程序分发包.wxapkg本质是一个经过特定格式封装的二进制文件不是 ZIP也不是 TAR而是一种微信自定义的容器格式。wxappUnpacker 的处理链路非常线性文件头识别读取前 4 字节判断是否为合法 wxapkg固定 magic number0x57584344即 ASCII “WXCD”元信息解析跳过 header 后读取长度字段解析出资源表resource table起始偏移与条目数资源索引遍历每条记录含文件名哈希、原始大小、压缩后大小、偏移地址用于精准定位每个文件块逐块解密 解压对每个资源块先执行 XOR 异或解密密钥为固定字节序列 文件名哈希扰动再用 zlib.inflateRaw 解压注意不是 deflate是 raw inflate。这个流程不依赖网络、不调用微信客户端、不启动任何 GUI纯命令行驱动所以你能把它塞进 CI 流程做自动化源码快照也能在无图形界面的 Linux 服务器上批量处理几十个包。2.2 关键技术点XOR 密钥生成与 zlib.rawInflate 的不可替代性很多新手第一次跑失败就卡在这两步。我们看核心解密函数片段来自index.js中decryptBufferfunction 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...硬解必然失败——这是第一大翻车点。再看解压部分decompressBufferfunction 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 headerRFC 1950只保留 deflate bodyRFC 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 / WindowsWSL 或 CMD均验证通过全程离线无需微信开发者工具。3.1 环境准备Node.js 版本与权限确认首先确认你有 Node.js 14.17推荐 16.x 或 18.xnode -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 wxappUnpackerlatest 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 }); // ← 关键recursivetrue } 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→divtext→span属性转换wx:if→v-ifbindtap→clickstyle scoped.wxss保留 CSS 规则添加scoped处理rpx→rem需预设根字体大小script setup.js提取Page({})中的data、onLoad、onShow等钩子转为const data ref({...})、onMounted(() {})关键约束不破坏原有解包逻辑新增功能通过 CLI 参数触发。5.2 代码改造三处修改零侵入第一步新增 CLI 参数解析index.jsconst 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}); } }); }第三步实现generateVueSFCutils.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 wxappUnpackerlatest your-app.wxapkg --vue --output ./vue-project你会在./vue-project/pages/index/index.vue看到template div classcontainer v-ifdata.showContainer span clickhandleTapHello {{ 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文件生成心里就踏实了——源码不是黑匣子只是换了一种封装形式。工具的价值从来不是炫技而是把“不可能”切成“下一步该做什么”。希望帮到你。本文还有配套的精品资源点击获取