在上一篇文章 《微信小程序“反编译”实战(一):解包》 中,我们详细介绍了如何获取某一个小程序的 .wxapkg 包,以及分析了 .wxapkg 包的结构,最后通过脚本解压获取包中的文件:小程序“编译”后的代码文件和资源文件,但是由于这些文件大部分被混淆了,可读性很差,所以本文将进一步分析,尽可能地把 .wxapkg 包的内容还原为“编译”前的内容。 注:本文包含一部分源码分析,由于手机屏幕较小,阅读体验可能不佳,建议在电脑上浏览。 特别感谢:下文使用的还原工具来自于 GitHub 上的开源项目 wxappUnpacker ,在此特别感谢原作者的无私贡献。 概览我们知道,前端 Web 网页编程采用的是 HTML + CSS + JS 这样的组合,其中 HTML 是用来描页面的结构, CSS 用来描述页面的样子, JS 通常用来处理页面逻辑和用户的交互。类似地,在小程序中也有同样的角色,一个小程序工程主要包括如下几类文件:
例如“知识小集”的小程序源码工程结构如下:
然而,根据上一篇文章介绍,对“知识小集”小程序的 .wxapkg 解包后得到如下文件:
主要包括 app-config.json , app-service.js , page-frame.html , *.html , 资源文件等,但这些文件已经被“编译混淆”并重新整合压缩,微信开发者工具并不能识别它们,我们无法直接对它们进行调试/编译运行。 所以,我们先尝试分析一下从 .wxapkg 提取出来的各个文件内容的结构及其用途,然后介绍如何用脚本工具把它们 一键还原 为“编译”前的源码,并在微信开发者工具中跑起来。 文件分析本节主要以“知识小集”小程序的 .wxapkg 解包后的源码文件为例,进行分析。 你也可以跳过本节的分析,直接看下一节介绍用脚本“反编译”还原源码。 app-config.json小程序工程主要包括工具配置 project.config.json ,全局配置 app.json 以及页面配置 page.json 三类 JSON 配置文件。其中: project.config.json 主要用于对开发者工具进行个性化配置以及包括小程序项目工程的一些基础配置,所以它不会被“编译”到 .wxapkg 包中; app.json 是对当前小程序的全局配置,包括了小程序的所有页面路径、界面表现、网络超时时间、底部 tab 等; page.json 用于对每一个页面的窗口表现进行配置,页面中配置项会覆盖 app.json 的 window 中相同的配置项。 因此“编译”后的文件 app-config.json 其实就是 app.json 和各个页面的配置文件的 汇总 ,它的内容大致如下: { "page": { // 各页面配置 "pages/index/index.html": { // 某一页面地址 "window": { // 某一页面具体配置 "navigationBarTitleText": "知识小集", "enablePullDownRefresh": true } }, // 此处省略... }, "entryPagePath": "pages/index/index.html", // 小程序入口地址 "pages": ["pages/index/index", "pages/detail/detail", "pages/search/search"], // 页面列表 "global": { // 全局页面配置 "window": { "navigationBarTextStyle": "black", "navigationBarTitleText": "知识小集", "navigationBarBackgroundColor": "#F8F8F8", "backgroundColor": "#F8F8F8" } } } 通过与原工程 app.json 和各页面配置 page.json 内容的对比,我们可以得出 app-config.json 汇总文件的简单整合规律,很容易把它拆分成“编译”前对应的各 json 文件。 app-service.js在小程序项目中 JS 文件负责交互逻辑,主要包括 app.js ,每个页面的 page.js ,开发者自定义的 JS 文件和引入的第三方 JS 文件,在“编译”后所有这些 JS 文件都会被汇总到 app-service.js 文件中,它的结构如下: // 一些全局变量的声明 var __wxAppData = {}; var __wxRoute; var __wxRouteBegin; var __wxAppCode__ = {}; var global = {}; var __wxAppCurrentFile__; var Component = Component || function(){}; var definePlugin = definePlugin || function(){}; var requirePlugin = requirePlugin || function(){}; var Behavior = Behavior || function(){}; // 小程序编译基础库版本 /*v0.6vv_20180125_fbi*/ global.__wcc_version__='v0.6vv_20180125_fbi'; global.__wcc_version_info__={"customComponents":true,"fixZeroRpx":true,"propValueDeepCopy":false}; // 工程中第三方或者自定义的一些 JS 源码 define("utils/util.js", function(require, module, exports, window,document,frames,self,location,navigator,localStorage,history,Caches,screen,alert,confirm,prompt,XMLHttpRequest,WebSocket,Reporter,webkit,WeixinJSCore) { "use strict"; // ... 具体源码内容 }); // ... // app.js 源码定义 define("app.js", function(...) { "use strict"; // ... app.js 源码内容 }); require("app.js"); // 每个页面对应的 JS 源码定义 __wxRoute = 'pages/index/index'; // 页面路由地址 __wxRouteBegin = true; define("pages/index/index.js", function(...){ "use strict"; // ... page.js 源码内容 }); require("pages/index/index.js"); 在这个文件中,原有小程序工程中的每个 JS 文件都被 define 方法定义声明,定义中包含 JS文件的路径和内容,如下: define("path/to/xxx.js", function(...){ "use strict"; // ... xxx.js 源码内容 }); 因此,我们同样很容易提取这些 JS 文件源码,并恢复至相应的路径位置中。当然,这些 JS 文件中的内容经过混淆压缩,我们可以使用 UglifyJS 这样的工具进行美化,但仍很难还原一些原始变量名,不过基本不影响正常阅读和使用。 |