前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

微信小程序反编译实战 从 wxapkg 包还原可运行源码

首页2021-04-20 15:30:41Front-End
小程序反编译wxapkg

接手过一个交接得很潦草的项目,线上小程序还在跑,源码只剩半个仓库,最后一次提交是一年前,构建产物和代码对不上。找前同事要,人早就离职了。这种时候唯一还能拿到完整逻辑的地方,就是微信客户端本地缓存的那份小程序包。

这篇是我把一个带 6 个分包的小程序从模拟器里整包捞出来、还原成能在微信开发者工具里跑起来的源码目录的完整记录。全程用的是开源工具 wxappUnpacker,中间那几百行看着吓人的日志我会拆开讲,它们其实把小程序的打包结构说得很清楚。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • wxapkg 是一种什么格式,头部那几个字段分别代表什么
  • 为什么小程序的包里没有 wxml 文件,反编译到底在还原什么
  • 用安卓模拟器 + RE 文件管理器 / adb 两条路把包拉到本地
  • 主包怎么解,输出日志里的每一段在做什么事
  • 分包为什么必须带 -s 参数,解完怎么合并回主包
  • 导入微信开发者工具时那些必须关掉的校验
  • miniprogram_npm 合并失败等常见报错怎么处理
  • 这件事的合规边界在哪里

# 一、先弄明白 wxapkg 到底是个什么文件

先说结论,wxapkg 不是 zip,也不是 tar,用解压软件打开只会得到一堆乱码。它是微信自己定义的一种只读打包格式,结构简单到有点朴素,一个固定长度的头部 + 一张文件索引表 + 所有文件内容首尾相接。

头部信息在后面的日志里能直接看到:

Header info:
  firstMark: 0xbe
  unknownInfo:  0
  infoListLength:  15360
  dataLength:  2960164
  lastMark: 0xed
@前端进阶之旅: 代码已经复制到剪贴板

firstMark 固定是 0xbe,lastMark 固定是 0xed,这两个魔数是校验用的。解包工具第一件事就是读这两个字节,对不上直接判定文件不是 wxapkg,或者被加密改造过。infoListLength 是索引表的字节长度,索引表里存的是每个文件的路径名、在数据区的起始偏移和长度。dataLength 是数据区总长度。

所以解包这一步本身没什么技术含量,按索引表把 dataLength 那段字节切开写到磁盘上就完事了。真正麻烦的是切开之后的事。

你打开解出来的目录会发现一件很反直觉的事,里面根本没有 wxml 文件,也没有一堆分散的 js 文件,只有几个巨大的文件躺在根目录:

文件 里面装的是什么
app-service.js 所有页面、组件、工具模块的逻辑层代码,被打包器串成了一个大文件
page-frame.html 或 app-wxss.js 所有 wxss 样式,被编译成了 JS 里的字符串数组
xxx.html / app-config.json 每个页面的 wxml 结构,被编译成了名为 $gwx 的一堆函数

这就是小程序双线程架构的直接后果。逻辑层跑在 JSCore(iOS)或 V8(安卓)里,视图层跑在 WebView 里,两边通过 setData 通信。wxml 不是模板字符串,它在构建期就被编译成了生成虚拟 DOM 的 JS 函数;wxss 也不是 CSS 文件,它被编译成了带 rpx 换算逻辑的 JS 数组。

那反编译在做什么就清楚了,它做的不是「解压」,是把这三次编译逆着走一遍:

  • 把 app-service.js 按模块边界切回一个个 .js 文件(日志里的 splitJs)
  • 把 $gwx 函数还原成 wxml 标签结构(日志里的 Decompile ... wxml)
  • 把 JS 数组里的样式还原成 wxss,并猜出原来的 @import 关系(日志里的 Guess wxss)

后面日志里三段名字奇怪的输出,对应的就是这三件事。如果你对小程序本身的运行机制还不太熟,可以先看看我之前整理的 小程序开发知识梳理,理解双线程之后再回来看这篇会顺很多。

# 二、环境准备

整条链路需要四样东西,缺一样都走不下去。

  1. Node.js(反编译工具是纯 Node 写的)
  2. 微信开发者工具(最后验证成果用)
  3. PC 端安卓模拟器,我用的是 网易 MuMu,装完开箱即用,不用自己刷 root
  4. 反编译工具本身

为什么非要模拟器?因为小程序包是被微信下载到客户端本地的,iOS 上根本拿不到沙盒目录,Windows/Mac 版微信的小程序缓存也是加密过的。安卓 + root 是目前门槛最低的一条路,而模拟器天生就带 root 开关,比拿真机刷机省事太多。

模拟器装好后,在它自带的应用商店里直接搜这两个包装上:

在模拟器应用商店搜索并安装微信和 RE 文件管理器

  • 微信,登录后在模拟器里打开你要研究的那个小程序
  • RE 文件管理器,必须先在「设置 - 权限」里把 root 打开,否则它进不了微信的私有数据目录

在 RE 文件管理器里开启 root 权限

这里有个坑要注意,root 开关打开之后 RE 文件管理器第一次访问 /data 会弹一个授权框,必须点允许并勾选「记住」,不然每进一层目录弹一次,能烦死人。

最后是 反编译工具 wxappUnpacker。这是社区里流传最广的那个 fork,相比原版多了一个 bingo.sh 一键脚本,省掉了手动依次调 wuWxapkg.js、wuJs.js、wuWxss.js、wuWxml.js 的麻烦。

cd wxappUnpacker

npm install
@前端进阶之旅: 代码已经复制到剪贴板

npm install 装的是 esprima、escodegen、uglify-es、cssbeautify、js-beautify 这一票 AST 解析和美化的包。说实话这套依赖年代比较久,我当时是在 Node 12 上跑通的,后来在更高版本的 Node 上碰到过安装失败,具体见后面「踩坑」那一节。

# 三、把 wxapkg 从模拟器里捞出来

拿包这一步的核心是「让微信主动把包下全」,然后再去它的缓存目录里翻。

操作顺序是这样的:

  • 打开模拟器里的微信并登录
  • 在下拉小程序面板里找到目标小程序,或者扫码进
  • 打开小程序,等首屏加载完
  • 每个页面都手动点一遍,尤其是那些藏在二级入口里的页面
  • 打开 RE 文件管理器,进到微信的缓存目录去找包

第四步是最容易被跳过、也最容易翻车的一步。小程序默认按分包加载,你不点进那个页面,对应的分包压根不会下载到本地,最后解出来的项目就是缺胳膊少腿的。你想想看,一个电商小程序的下单流程往往整个塞在一个 subPackage 里,你只在首页转了一圈,捞回来的就只有主包。

包在这个路径下面:

/data/data/com.tencent.mm/MicroMsg/<32位用户哈希>/appbrand/pkg
@前端进阶之旅: 代码已经复制到剪贴板

中间那段 32 位十六进制是当前登录微信号的目录哈希,每个账号不一样,直接照抄我的路径是找不到的,得进 MicroMsg 目录自己看。如果里面有多个哈希目录,挑修改时间最新的那个,那是当前登录的账号。

用 RE 文件管理器进入 data/data 目录

定位到 com.tencent.mm 下的 MicroMsg 目录

进入用户哈希目录下的 appbrand 目录

pkg 目录下按修改时间排列的 wxapkg 包

pkg 目录里会躺着一堆命名规律很怪的 .wxapkg 文件,像 _-1215506245_427.wxapkg 这种。这里不用猜,按修改时间倒序排,你刚才操作的那个小程序的包一定挤在最前面几个,文件最大的那个通常就是主包,剩下几个小的是分包。

拿到手之后,最土但最稳的传输方式是直接在模拟器微信里发给文件传输助手:

选中压缩文件发送给好友传到电脑上

# 用 adb 一次性拉走整个目录

上面那套点点点的流程包一多就很崩溃,包名还带下划线和负号,手动挑容易挑漏。更省事的做法是走 adb,直接把整个 pkg 目录拖到本地。

先确认设备是不是连上了:

adb get-state
@前端进阶之旅: 代码已经复制到剪贴板

如果返回 error: no devices/emulators found,说明 adb 还没和模拟器握上手。模拟器其实就是个跑在本机上的安卓设备,通过一个固定端口暴露 adb 服务,各家端口号不一样:

  • 夜神模拟器:adb connect 127.0.0.1:62001
  • 逍遥安卓模拟器:adb connect 127.0.0.1:21503
  • 天天模拟器:adb connect 127.0.0.1:6555
  • 海马玩模拟器:adb connect 127.0.0.1:53001
  • 网易 MUMU 模拟器:adb connect 127.0.0.1:7555,MacOS 版是 adb connect 127.0.0.1:5555
  • genymotion 模拟器:adb connect 127.0.0.1:5555
  • 谷歌原生模拟器:adb connect <设备的IP地址>:5555

MUMU 在 Windows 和 macOS 上端口不一致这点我卡过一会儿,Mac 上照着网上抄的 7555 一直连不上,换成 5555 才通。连上之后 adb get-state 会返回 device,这时候就能拉了:

adb pull /data/data/com.tencent.mm/MicroMsg/056e5e31623203e0efe74762a2584f40/appbrand/pkg
@前端进阶之旅: 代码已经复制到剪贴板

路径里那串哈希记得换成你自己的。整个目录拉下来,主包分包一个不漏,比手动挑靠谱得多。

# 四、编译主包

包到手了,接下来是重头戏。先解主包,因为分包解包时要拿主包目录当参照。

# _-1215506245_427.wxapkg 主包
$ ./bingo.sh 小程序压缩包/_-1215506245_427.wxapkg
@前端进阶之旅: 代码已经复制到剪贴板

bingo.sh 是那个 fork 加的一键脚本,它内部就是按顺序调用 wuWxapkg.js,后者再依次触发解包、拆 JS、还原 wxss、还原 wxml。你也可以自己一个一个调,但没必要。

跑起来之后终端会疯狂滚屏:

终端里滚动输出的解包日志 解包完成后生成的项目目录结构

这几百行日志值得逐段看一遍,它把小程序的打包结构全招了。下面我按阶段拆开讲。

node /Users/poetry/Downloads/小程序反编译工具/wuWxapkg.js
Unpack file 小程序压缩包/_-1215506245_427.wxapkg...

Header info:
  firstMark: 0xbe
  unknownInfo:  0
  infoListLength:  15360
  dataLength:  2960164
  lastMark: 0xed

File list info:
  fileCount:  375
Saving files...
Unpack done.
Split app-service.js and make up configs & wxss & wxml & wxs...
deal config ok
deal js ok
deal wxss.js ok
deal css ok
=======================================================
这个小程序采用了分包
子包个数为:  6
=======================================================
@前端进阶之旅: 代码已经复制到剪贴板

# 4.1 头部校验和文件清点

第一段日志信息量其实很大。firstMark: 0xbe 和 lastMark: 0xed 对上了,说明这是一个标准未加密的 wxapkg。fileCount: 375 是索引表里登记的文件数,dataLength: 2960164 差不多 2.8MB,这就是主包的实际体积。

小程序主包有 2MB 的硬限制(当年是 2MB,后来放宽到 4MB,整包 20MB),这里 2.8MB 已经超了单包旧限额,所以开发者才不得不上分包。工具也看出来了,紧跟着打出「这个小程序采用了分包,子包个数为 6」。这行提示很关键,它等于提前告诉你,后面还有 6 个 .wxapkg 要解,主包解完只是完成了一小半。

fe
  • 一、先弄明白 wxapkg 到底是个什么文件
  • 二、环境准备
  • 三、把 wxapkg 从模拟器里捞出来
    • 用 adb 一次性拉走整个目录
  • 四、编译主包
    • 4.1 头部校验和文件清点
    • 4.2 还原自定义组件的 wxml
    • 4.3 拆分 app-service.js
    • 4.4 猜回 wxss 的引用关系
  • 五、编译分包
    • 合并回主包
  • 六、导入微信开发者工具
  • 七、这条路上我踩过的坑
  • 八、这件事的边界在哪
  • 总结
  • 参考

← 小程序插件总结 从创建到发布的完整流程微信小程序开发实践 从双线程模型到云开发的完整笔记 →