简介这份小程序反编译工具包围绕 wxappUnpacker 展开面向需要研究微信小程序内部实现、进行代码分析或安全审计的开发者与安全研究人员重点解决主包与分包二进制文件难以阅读的问题可将 WXML、WXSS、JS 与 JSON 等核心文件还原为便于查看和编辑的源码形式。压缩包共 780 个文件约 1.97MB以 374 个 js 文件为主体辅以 83 个 json 配置、71 个 md 说明、70 个 ts 源码及 61 个 map 映射文件并包含许可协议、命令行脚本与批处理文件便于了解工具结构、依赖关系及运行方式。已有 270 人学习下载适合希望搭建分析环境、理解反编译解析流程和分包处理思路的读者参考。其价值在于提供较完整的工具代码与配套说明可用于梳理界面标记、样式、业务逻辑和配置信息的对应关系辅助排查问题、性能优化、代码复用与安全检测使用时需注意遵守平台协议与合法合规要求。1. 拿到 .wxapkg 打不开wxappUnpacker 能还原到什么程度前段时间同事把一个.wxapkg文件发给我——前同事留下的项目仓库被清空了线上还在运行本地只剩一个从测试机导出的包。把后缀改成.zip再解压、拖进编辑器、用十六进制查看器打开看到的全是乱码。这种情况基本只有一条路用 wxappUnpacker 把小程序反编译回工程目录。它是一套 Node 脚本负责的事情并不复杂按 wxapkg 的索引表把文件逐条切出来再把编译过的 wxml、wxss 和app-service.js反解析成能读的文本。适合三类人接手遗留项目的、要做兼容性比对的、想搞清楚某个第三方组件真实行为的。前提是你对这个包有处置权限——自己开发的、公司交接的、明确授权的不要越界。2. 拆包前先看壳14 字节头部与两段索引表很多人拿到脚本后就直接执行node wuWxapkg.js报一行Error: 非法的文件就懵了。其实 wxapkg 的结构简单到可以手写解析器花十分钟看懂这层壳后面遇到的九成报错都能自己定位。这一章先不碰反编译逻辑只做一件事把一个二进制包拆成“文件清单 内容块”并且确认这个包是完整的、没有加密的、没有被截断的。2.1 头部字段与条目表的字节布局wxapkg 是一个很朴素的归档格式固定头部、索引表、数据区三段首尾相接。头部只有 14 个字节字段如下。偏移长度字段说明01firstMark固定 0xBE第一个魔数14info1保留字段通常是 054indexLen索引区长度94bodyLen数据区长度131lastMark固定 0xED第二个魔数144fileCount文件条目数量两个魔数把头部夹在中间工具启动后的第一件事就是读这两个字节只要对不上就直接退出——所以“解包失败”很多时候根本不是反编译的问题而是文件本身不对。紧接着 14 字节之后是索引表先 4 字节的文件数然后每条记录是[4字节文件名长度][文件名 UTF-8][4字节数据偏移][4字节数据长度]。注意这个偏移是相对文件开头的绝对值不是相对索引区的所以解析的时候不需要做任何累加修正直接buf.slice(offset, offset size)就能切出内容。一个可以用来体检的恒等式14 indexLen bodyLen应该接近文件总长度误差通常只有几个字节的填充。这个式子是我判断“包是不是拷全了”的第一道关比任何报错信息都直观。2.2 先跑一段体检脚本别急着解包下面这段不写任何文件只读头部、走一遍索引表把文件数和前若干条目打出来。放在解包之前跑能省掉大量来回试错。// peek.js —— 只读文件头和索引表不落盘用来判断包是否完整 const fs require(fs); const buf fs.readFileSync(process.argv[2]); const firstMark buf.readUInt8(0); // 应为 0xBE const info1 buf.readUInt32BE(1); // 保留位一般 0 const indexLen buf.readUInt32BE(5); // 索引区长度 const bodyLen buf.readUInt32BE(9); // 数据区长度 const lastMark buf.readUInt8(13); // 应为 0xED const fileCount buf.readUInt32BE(14); // 条目数 console.log(magic:, firstMark.toString(16), lastMark.toString(16)); console.log(indexLen:, indexLen, bodyLen:, bodyLen, fileCount:, fileCount); console.log(理论总长:, 14 indexLen bodyLen, 实际:, buf.length); // 走一遍条目表只打印前 20 条顺便验证偏移是否越界 let pos 18; for (let i 0; i fileCount i 20; i) { const nameLen buf.readUInt32BE(pos); pos 4; const name buf.toString(utf8, pos, pos nameLen); pos nameLen; const offset buf.readUInt32BE(pos); pos 4; const size buf.readUInt32BE(pos); pos 4; const ok offset size buf.length ? ok : 越界; console.log(i, name, offset, size, ok); }参数只有一个process.argv[2]是包路径用node peek.js ./__APP__.wxapkg跑。脚本本身没有任何副作用可以放心对着可疑文件反复跑。输出里要盯的是三处。第一处是 magic如果不是be和ed说明拿错文件或者包被二次封装了。第二处是fileCount正常小程序主包是几百到几千如果打印出上百万这种数量级基本是头部错位后面必然越界。第三处是理论总长与实际长度差几百字节以上就要怀疑文件被截断——从设备里往外拷的时候中断很常见尤其包体超过十几兆。2.3 主包、分包与目录树拼装一个完整小程序通常不止一个 wxapkg。主包里是app.json、app.js、app.wxss和主包页面分包是独立文件里面有各自的子目录页面。每个包都有自己完整的头部和索引表条目里的路径也是相对各自包根的所以解出来是几棵互相独立的树需要合并成一棵。包类型命名特征典型内容处理方式主包__APP__.wxapkg或纯数字名app.json、app.js、主包页面、组件先解输出到根目录分包__sub__名称.wxapkg分包页面、分包独有组件后解覆盖式合并到同一目录运行时骨架同上包内app-service.js、app-config.json、page-frame.html不合并单独留作拆分源合并用循环就够了不需要写额外逻辑因为分包落盘的路径本身就带子目录前缀# 主包先解落到 out/ 根目录 node wuWxapkg.js -o ./out ./__APP__.wxapkg # 分包逐个解到同一个 out/路径自带前缀直接覆盖合并 for f in ./pkg/__sub__*.wxapkg; do echo $f node wuWxapkg.js -o ./out $f done-o是输出根目录不传的话默认是包文件名去掉后缀多包场景下会生成一堆平行目录改名很麻烦所以我一般显式指定。循环里不要再加别的开关保证主包分包的解析参数完全一致否则同一批.json的缩进和编码会有细微差别后面做 diff 会很难受。合并完目录里应该能看到app.json、app.js、app.wxss、pages/、components/、utils/外加三个体积明显偏大的文件app-service.js所有页面的 JS 打成一包、app-config.json所有页面配置打成一包、page-frame.htmlWXML 的运行时容器。这三个文件是后面几章的主角——真正麻烦的反编译工作全在它们身上。3. 跑通 wxappUnpackerNode 版本、命令链与四类产物3.1 Node 版本选 12别硬上新版这套脚本的核心逻辑写于 Node 8/10 时代依赖里esprima是很老的版本源码里还有new Buffer(size)这种写法——它在 Node 10 之后被标记废弃Node 16 之后彻底移除。在 Node 18/20 上跑典型症状是启动即抛TypeError: Buffer.alloc is not a function或者new Buffer is not a constructor也可能解析到一半莫名其妙卡住。# 用 nvm 隔离一个旧环境别动系统默认的 Node nvm install 12 nvm use 12 node -v # 确认输出 v12.xWindows 上对应的是 nvm-windows或者干脆单独装一个 12.x 的安装包跑完再切回默认版本。不要图省事直接在 Node 20 上改源码grep -rn new Buffer( .全量替换成Buffer.alloc(或Buffer.from(确实能过第一关但esprima那关还是过不去老 parser 不认可选链?.、空值合并??、类字段这些 ES2020 之后的语法一碰就抛Unexpected token。所以切版本是成本最低的方案。3.2 一条命令的执行链从读索引到写文件环境就绪后跑起来只有一行。但要知道它在干什么出问题才知道往哪看。cd wxappUnpacker npm install # 装 esprima / escodegen / js-beautify 等解析依赖 node wuWxapkg.js -o ./out ./__APP__.wxapkg # 包路径必须放最后内部顺序大致分四步。第一步读头部和索引表把fileList建起来这一步不产生任何输出。第二步按扩展名分发.wxml交给wuWxml.wxss交给wuWxss.js交给wuJs.json交给wuConfig其余类型图片、字体、wasm原样 dump。第三步遇到app-service.js和app-config.json这两个聚合文件时特殊处理先存一份原始副本再按内部的define结构拆成独立文件。第四步按索引表里的路径逐级mkdir再writeFile。调试时最有用的一条经验日志如果卡在Unpacking pages/xxx/xxx.js那一行不动CPU 却拉满问题一定出在wuJs这一支上。这时按 CtrlC把wuWxapkg.js里分发到wuJs的那几行临时注释掉先拿到 wxml、wxss、json 三样JS 单独处理。分批拿产物看着不优雅但比卡在一个文件上耗半天强得多。3.3 产物逐层核对哪些能用哪些只能读解完之后别急着往开发者工具里拖先对着下面这张表过一遍心里有个预期。产物来源还原程度能不能直接用pages/*/*.wxml编译后的 wxml标签和指令基本还原复杂表达式可能残缺小修可用pages/*/*.wxss编译后的 wxssrpx 已反算存在 ±0.5 的舍入误差基本可用pages/*/*.js从 app-service.js 拆出结构完整变量名和注释全丢只能读pages/*/*.json从 app-config.json 拆出完整可直接用app.json从 app-config.json 拆出完整但 subPackages 路径需核对可用project.config.json包里没有——必须自己补重点说 JS 这一行。拆出来的是压缩过的代码经过js-beautify排版后的结果define(pages/index/index.js, function(e,t,i){...})里那三个短参数原本是require、module、exports工具会按参数位置尝试改名回来。大部分情况是对的但遇到箭头函数、解构参数、默认参数位置推断就会失准——所以改逻辑之前一定要对着页面的实际行为验一遍别信变量名。3.4 app-service.js 的 define 拆分JS 只有一半能看编译期的做法是把所有页面的 JS 塞进一个app-service.js每个模块用define(模块路径, function(require, module, exports, ...){...})包一层。拆分靠的是匹配define(加上括号配对切出模块体再写回对应路径。这个办法对绝大多数模块有效验证一下就知道漏没漏# 拆分前有多少个 define拆分后有多少个 js 文件 grep -c define( app-service.js find ./out -name *.js | wc -l两个数字应该接近差值主要是app.js这类入口文件。如果差得很多说明有模块的define第一个参数不是字符串字面量——拼出来的路径、变量、或者被压缩器改写过的形式正则都切不出来这部分会原样残留在app-service.js里得手工捞。还有一个绕不过去的现实如果项目里用了打包器第三方库会被打成__webpack_require__那一套运行时反编译出来是几百行状态机能读不能改。真正值得花时间的是自己业务代码那几段找到define(pages/xxx/xxx.js开头的那一块前后对照着看逻辑基本能还原到八成。4. WXML 与 WXSS 还原750 基准换算和组件路径修正4.1 编译后的 WXML 丢了什么、能捡回什么小程序打包时会先把 wxml 编译成一份带运行时调用的中间表示wx:if变成条件表达式{{a b}}变成表达式树block直接展开wx:for的 item/index 被重命名成极短的名字。wuWxml走的是反解析路线——把这份中间表示重新 parse 成 AST再按模板语法生成文本。能捡回来的标签层级、大部分wx:if / wx:elif / wx:else、wx:for、bind和catch事件名、{{}}里的简单表达式、静态的class和 本文还有配套的精品资源点击获取