微信小程序「反編譯」實戰(二):源碼還原

知識小集是一個團隊公衆號,主要定位在移動開發領域,分享移動開發技術,包括 iOS、Android、小程序、移動前端、React Native、weex 等。每週都會有 原創 文章分享,咱們的文章都會在公衆號首發。歡迎關注查看更多內容。javascript

原文連接css

在上一篇文章《微信小程序「反編譯」實戰(一):解包》中,咱們詳細介紹瞭如何獲取某一個小程序的 .wxapkg 包,以及分析了 .wxapkg 包的結構,最後經過腳本解壓獲取包中的文件:小程序「編譯」後的代碼文件和資源文件,可是因爲這些文件大部分被混淆了,可讀性不好,因此本文將進一步分析,儘量地把 .wxapkg 包的內容還原爲「編譯」前的內容。html

注:本文包含一部分源碼分析,因爲手機屏幕較小,閱讀體驗可能不佳,建議在電腦上瀏覽。前端

特別感謝:下文使用的還原工具來自於 GitHub 上的開源項目 wxappUnpacker,在此特別感謝原做者的無私貢獻。java

概覽

咱們知道,前端 Web 網頁編程採用的是 HTML + CSS + JS 這樣的組合,其中 HTML 是用來描頁面的結構,CSS 用來描述頁面的樣子,JS 一般用來處理頁面邏輯和用戶的交互。相似地,在小程序中也有一樣的角色,一個小程序工程主要包括以下幾類文件:node

  • .json 後綴的 JSON 配置文件
  • .wxml 後綴的 WXML 模板文件
  • .wxss 後綴的 WXSS 樣式文件
  • .js 後綴的 JavaScript 腳本邏輯文件

例如「知識小集」的小程序源碼工程結構以下:git

然而,根據上一篇文章介紹,對「知識小集」小程序的 .wxapkg 解包後獲得以下文件:github

主要包括 app-config.json, app-service.js, page-frame.html, *.html, 資源文件 等,但這些文件已經被「編譯混淆」並從新整合壓縮,微信開發者工具並不能識別它們,咱們沒法直接對它們進行調試/編譯運行。web

因此,咱們先嚐試分析一下從 .wxapkg 提取出來的各個文件內容的結構及其用途,而後介紹如何用腳本工具把它們一鍵還原爲「編譯」前的源碼,並在微信開發者工具中跑起來。npm

文件分析

本節主要以「知識小集」小程序的 .wxapkg 解包後的源碼文件爲例,進行分析。

你也能夠跳過本節的分析,直接看下一節介紹用腳本「反編譯」還原源碼。

app-config.json

小程序工程主要包括工具配置 project.config.json,全局配置 app.json 以及頁面配置 page.json 三類 JSON 配置文件。其中:

project.config.json 主要用於對開發者工具進行個性化配置以及包括小程序項目工程的一些基礎配置,因此它不會被「編譯」到 .wxapkg 包中;

app.json 是對當前小程序的全局配置,包括了小程序的全部頁面路徑、界面表現、網絡超時時間、底部 tab 等;

page.json 用於對每個頁面的窗口表現進行配置,頁面中配置項會覆蓋 app.jsonwindow 中相同的配置項。

所以「編譯」後的文件 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 這樣的工具進行美化,但仍很難還原一些原始變量名,不過基本不影響正常閱讀和使用。

page-frame.html

在小程序中使用 WXML 文件描述頁面的結構,WXSS 文件描述頁面的樣式。工程中有一個 app.wxss 文件用於定義一些全局的樣式,會自動被 import 到各個頁面中;另外每一個頁面也都分別包含 page.wxmlpage.wxss 用於描述其頁面的結構和樣式;同時,咱們也會自定義一些公共的 xxxCommon.wxss 樣式文件和公共的 xxxTemplate.wxml 模板文件供一些頁面複用,通常在各自頁面的 page.wxsspage.wxml 中去 import

當「編譯」小程序後,全部的 .wxml 文件和 app.wxss 及公共 xxxCommon.wxss 樣式文件的將被整合到 page-frame.html 文件中,而每一個頁面的 page.wxss 樣式文件,將分別單獨在各自的路徑下生成一個 page.html 文件。

page-frame.html 文件的內容結構以下:

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
    <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'">
    <link rel="icon" href="data:image/ico;base64,aWNv">
    <script> // 一些全局變量的聲明 var __pageFrameStartTime__ = Date.now(); var __webviewId__; var __wxAppCode__ = {}; var __WXML_GLOBAL__ = { entrys: {}, defines: {}, modules: {}, ops: [], wxs_nf_init: undefined, total_ops: 0 }; // 小程序編譯基礎庫版本 /*v0.6vv_20180125_fbi*/ window.__wcc_version__ = 'v0.6vv_20180125_fbi'; window.__wcc_version_info__ = { "customComponents": true, "fixZeroRpx": true, "propValueDeepCopy": false }; var $gwxc var $gaic = {} $gwx = function(path, global) { // $gwx 方法定義(最核心) } var BASE_DEVICE_WIDTH = 750; var isIOS = navigator.userAgent.match("iPhone"); var deviceWidth = window.screen.width || 375; var deviceDPR = window.devicePixelRatio || 2; function checkDeviceWidth() { // checkDeviceWidth 方法定義 } checkDeviceWidth() var eps = 1e-4; function transformRPX(number, newDeviceWidth) { // transformRPX 方法定義 } var setCssToHead = function(file, _xcInvalid) { // setCssToHead 方法定義 } setCssToHead([])(); // 先清空 Head 中的 CSS setCssToHead([...]); // 設置 app.wxss 的內容到 Head 中,其中 ... 爲小程序工程中 app.wxss 的內容 var __pageFrameEndTime__ = Date.now() </script>
  </head>
  <body>
    <div></div>
  </body>
</html>
複製代碼

相比其餘文件,page-frame.html 比較複雜,微信把 .wxml 和部分 .wxss 直接「編譯」並混淆成 JS 代碼放入上述文件中,而後經過調用這些 JS 代碼來構造 Virtual-Dom,進而渲染頁面。

其中最核心的是 $gwxsetCssToHead 這兩個方法。

$gwx 用於經過 JS 代碼生成全部 .wxml 文件,其中每一個 .wxml 文件的內容結構都在 $gwx 方法中被定義好並混淆了,咱們只要傳給它頁面的 .wxml 路徑參數,便可獲取到每一個 .wxml 的內容,再簡單加工一下便可還原成「編譯」前的內容。

$gwx 中有一個 x 數組用於存儲當前小程序都有哪些 .wxml 文件,例如,「知識小集」小程序的 x 值以下:

var x = ['./pages/detail/detail.wxml', '/towxml/entry.wxml', './pages/index/index.wxml', './pages/search/search.wxml', './towxml/entry.wxml', '/towxml/renderTemplate.wxml', './towxml/renderTemplate.wxml'];
複製代碼

此時咱們能夠在 Chrome 中打開 page-frame.html 文件,而後在 Console 中輸入以下命令,便可獲得 index.wxml 的內容(輸出一個 JS 對象,經過遍歷這個對象便可還原出 .wxml 的內容)

$gwx("./pages/index/index.wxml")
複製代碼

setCssToHead 方法用於根據幾段被拆分的樣式字符串數組生成 .wxss 代碼並設置到 HTMLHead 中,同時,它還將全部被 import 引用的 .wxss 文件(公共 xxxCommon.wxss樣式文件)所對應的樣式數組內嵌在該方法中的 _C 變量中,並標記哪些文件引用了 _C 中數據。另外在 page-frame.html 文件的末尾,調用了該方法生成全局 app.wxss 的內容設置到 Head 中。

所以,咱們能夠在每一個調用 setCssToHead 方法的地方提取相應 .wxss 的內容並還原。

對於 page-frame.html 文件中 $gwxsetCssToHead 這兩個方法更詳細的分析,能夠參考這篇文章

此外,checkDeviceWidth 方法顧明思議,用於檢測屏幕的寬度,其檢測結果將用於 transformRPX 方法中將 rpx 單位轉換爲 px 像素。

rpx 的全稱是 responsive pixel,它是小程序本身定義的一個尺寸單位,能夠根據當前設備屏幕寬度進行自適應。小程序中規定,全部的設備屏幕寬度都爲 750rpx,根據設備屏幕實際寬度的不一樣,1rpx所表明的實際像素值也不同。

*.html

上面提到,每一個頁面的 page.wxss 樣式文件,「編譯」後將分別在各自的所在路徑下生成一個 page.html 文件,每一個 page.html 的結構以下:

<style></style>
<page></page>
<script> var __setCssStartTime__ = Date.now(); setCssToHead([...])() // 設置 search.wxss 的內容 var __setCssEndTime__ = Date.now(); document.dispatchEvent(new CustomEvent("generateFuncReady", { detail: { generateFunc: $gwx('./pages/search/search.wxml') } })) </script>
複製代碼

在該文件中經過調用 setCssToHead 方法將 .wxss 樣式內容設置到 Head 中,因此一樣地,咱們能夠根據 setCssToHead 的調用參數提取每一個頁面的 page.wxss

資源文件

小程序工程中的圖片、音頻等資源文件在「編譯」後將直接被拷貝到 .wxapkg 包中,其原始的路徑也保留不變,所以咱們能夠直接使用。

「反編譯」

在上一節,咱們完成了 .wxapkg 包幾乎全部文件內容的簡要分析。如今咱們介紹一下如何經過 node.js 腳本幫咱們還原出小程序的源碼。

在這裏須要再次感謝 wxappUnpacker 做者提供的還原工具,讓咱們能夠「站在巨人的肩膀上」輕鬆地去完成「反編譯」。它的使用以下:

  • node wuConfig.js <path/to/app-config.json> : 將 app-config.json 中的內容拆分紅各個頁面所對應的 page.jsonapp.json

  • node wuJs.js <path/to/app-service.js> : 將 app-service.js 拆分紅一系列原先獨立的 JS 文件,並使用 Uglify-ES 美化工具儘量將代碼還原爲「編譯」前的內容;

  • node wuWxml.js [-m] <path/to/page-frame.html> : 從 page-frame.html 中提取並還原各頁面的 .wxmlapp.wxss 及公共 .wxss 樣式文件;

  • node wuWxss.js <path/to/unpack_dir> : 該命令參數爲 .wxapkg 解包後目錄,它將分析並從各個 page.html 中提取還原各頁面的 page.wxss 樣式文件;

同時,做者還提供了一鍵解包並還原的腳本,你只須要提供一個小程序的 .wxapkg 文件,而後執行以下命令:

node wuWxapkg.js [-d] <path/to/.wxapkg>
複製代碼

此腳本就會自動將 .wxapkg 文件解包,並將包中相關的已被「編譯/混淆」的文件自動地恢復原狀(包括目錄結構)。

PS: 此工具依賴 uglify-es, vm2, esprima, cssbeautify, css-treenode.js 包,因此你可能須要 npm install xxx 安裝這些依賴包才能正確執行。

更詳細的用法及相關問題請查閱該開源項目的 GitHub repo。

最後,咱們在 微信開發者工具 中新建一個空小程序工程,並將上述還原後的相關目錄文件導入工程,便可編譯運行起來,以下圖爲「知識小集」小程序的 .wxapkg 包還原後的代碼工程:

以上,大功告成!

總結

本文詳細分析了 .wxapkg 解包後的各文件結構,並介紹瞭如何經過腳本「一鍵還原」獲得任意小程序的源碼。

對於一些簡單的,且使用微信官方介紹的原生開發方式開發的小程序,用上述工具基本能夠直接還原獲得可運行的源碼,可是對於一些邏輯複雜,或者使用 WePYVue 等一些框架開發的小程序,還原後的源碼可能會有一些小問題,須要咱們人肉去分析解決。

後續

本文對小程序源碼「編譯」後的各文件內容結構及用途的分析相對比較零散,並且沒有對各文件的依賴關係及加載邏輯進行研究,後續咱們再寫一些文章講解微信客戶端是如何解析加載小程序 .wxapkg 包並運行起來。

參考連接

相關文章
相關標籤/搜索