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

2021-02-25 知識小集

在上一篇文章《微信小程序「反編譯」實戰(一):解包》中,我們詳細介紹了如何獲取某一個小程序的 .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(){};



global.__wcc_version__='v0.6vv_20180125_fbi';
global.__wcc_version_info__={"customComponents":true,"fixZeroRpx":true,"propValueDeepCopy":false};


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";
 
});




define("app.js", function(...) {
 "use strict";
 
});
require("app.js");


__wxRoute = 'pages/index/index';
__wxRouteBegin = true;
define("pages/index/index.js", function(...){
 "use strict";
 
});
require("pages/index/index.js");

在這個文件中,原有小程序工程中的每個 JS 文件都被 define 方法定義聲明,定義中包含 JS 文件的路徑和內容,如下:

define("path/to/xxx.js", function(...){
 "use strict";
 
});

因此,我們同樣很容易提取這些 JS 文件源碼,並恢復至相應的路徑位置中。當然,這些 JS 文件中的內容經過混淆壓縮,我們可以使用 UglifyJS 這樣的工具進行美化,但仍很難還原一些原始變量名,不過基本不影響正常閱讀和使用。

page-frame.html

在小程序中使用 WXML 文件描述頁面的結構,WXSS 文件描述頁面的樣式。工程中有一個 app.wxss 文件用於定義一些全局的樣式,會自動被 import到各個頁面中;另外每個頁面也都分別包含 page.wxml 和 page.wxss 用於描述其頁面的結構和樣式;同時,我們也會自定義一些公共的 xxxCommon.wxss 樣式文件和公共的 xxxTemplate.wxml 模板文件供一些頁面復用,一般在各自頁面的 page.wxss 和 page.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
     };
     
     
     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) {
       
     }
     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()
     var eps = 1e-4;
     function transformRPX(number, newDeviceWidth) {
       
     }
     var setCssToHead = function(file, _xcInvalid) {
       
     }
     setCssToHead([])();
     setCssToHead([...]);
     var __pageFrameEndTime__ = Date.now()
         </script>
 </head>
 <body>
   <div></div>
 </body>
</html>

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

其中最核心的是 $gwx 和 setCssToHead 這兩個方法。

$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 代碼並設置到 HTML 的 Head 中,同時,它還將所有被 import 引用的 .wxss 文件(公共 xxxCommon.wxss樣式文件)所對應的樣式數組內嵌在該方法中的 _C 變量中,並標記哪些文件引用了 _C 中數據。另外在 page-freme.html 文件的末尾,調用了該方法生成全局 app.wxss 的內容設置到 Head 中。

因此,我們可以在每個調用 setCssToHead 方法的地方提取相應 .wxss 的內容並還原。

對於 page-freme.html 文件中 $gwx 和 setCssToHead 這兩個方法更詳細的分析,可以參考這篇文章 https://bbs.pediy.com/thread-225289.htm 。

此外,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([...])()
 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.json 和 app.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 中提取並還原各頁面的 .wxml 和 app.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-tree 等 node.js 包,所以你可能需要 npm install xxx 安裝這些依賴包才能正確執行。

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

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

以上,大功告成!

總結

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

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

後續

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

參考連結

相關焦點

  • 兩個步驟讓你獲取任何微信小程序源碼!
    ,半個月學習下來,很想實戰一下踩踩坑,於是就仿寫了一個滴滴他們家的青桔單車小程序的前端實現,過程一言難盡,差不多兩周時間過去了,發現小程序的坑遠比想像的要多的多!!在實際練手中,完全是黑盒的,看到人家上線的小程序的效果,純靠推測,部分效果在絞盡腦汁後能做出大致的實現,但是有些細節,費勁全力都沒能做出來。很想一窺源碼,查看究竟,看看大廠的前端大神們是如何規避了小程序的各種奇葩的坑。於是就想到獲取到小程序地源文件,然後再對其進行反編譯還原為原始碼,來作為學習參考。我百度了各種關於小程序地反編譯教程,但是感覺都不太適合像我這樣地初學小白,踩了挺多坑。
  • Java代碼的編譯與反編譯
    2、用編譯程序產生目標程序的動作。 編譯就是把高級語言變成計算機可以識別的2進位語言,計算機只認識1和0,編譯程序把人們熟悉的語言換成2進位的。 編譯程序把一個源程序翻譯成目標程序的工作過程分為五個階段:詞法分析;語法分析;語義檢查和中間代碼生成;代碼優化;目標代碼生成。主要是進行詞法分析和語法分析,又稱為源程序分析,分析過程中發現有語法錯誤,給出提示信息。
  • 實戰Python3.7+64位 Exe 反編譯
    本文轉載自【微信公眾號:MicroPest,ID:gh_696c36c5382b】,經微信公眾號授權轉載,如需轉載與原文作者聯繫一、生成python3.7+64位Exe程序在反之前要先編一個。python編譯後的二進位文件Python源碼編譯的結果就是PyCodeObject(簡稱「代碼對象」),每個作用域會編譯出一個對應的代碼對象,其中名為co_code的PyStringObject保存著代碼對象的字節碼。一個Python源文件就是一個模塊。
  • 記一次小程序加解密測試
    1.前言由於小程序的便捷性,越來越多的應用遷移到了了小程序上,由此伴隨著小程序上線前的日常滲透測試工作也開始增加。
  • 微信小程序實戰篇:實現抖音評論效果
    我們在寫小程序的時候經常會遇到彈出層的效果而現有官網提供的跳轉方法多數是不支持參數傳遞的。
  • 微信小程序開發《影視小程序》從0實戰-無需懂代碼
    最近圈子裡很火爆的就是微信小程序了···不知道大家是否有注意,影視類一直是比較容易獲得認可的資源種類。總有那麼一個時間點,你想看某部電影或者電視,結果發現一是沒資源,二是要付費。今天免費分享怎麼從0開始搭建所有資原始碼請在GZH「副業研習圈」獲取需要準備的東西:1、伺服器2、備案域名3、註冊微信小程序
  • java程序編譯後會產生什麼
    什麼是反編譯   計算機軟體反向工程(Reverse engineering)也稱為計算機軟體還原工程,是指通過對他人軟體的目標程序(可執行程序)進行「逆向分析、研究」工作,以推導出他人的軟體產品所使用的思路、原理、結構、算法、處理過程、運行方法等設計要素,某些特定情況下可能推導出原始碼。
  • 科普:除了源碼下載,微信小程序還有哪些開發方式?
    製作一個微信小程序真的有那麼難?那要看你用的是什麼方式了。這種方式你要先下載微信官網開發者工具,新建一個小程序項目,填入小程序的 AppID(需先在微信公眾平臺申請好),勾選 「創建 QuickStart 項目」,你就得到了你的第一個小程序了,點擊頂部菜單編譯就可以在微信開發者工具中預覽你的第一個小程序。具體注意事項需要參考官方配置文檔。
  • 漫畫:如何給女朋友解釋什麼是編譯與反編譯
    但是不同型號的計算機其機器語言是不相通的,按著一種計算機的機器指令編制的程序,不能在另一種計算機上執行。因為機器語言是使用二進位表示的,所以編出的程序全是些0和1的指令代碼。由於彙編更接近機器語言,能夠直接對硬體進行操作,生成的程序與其他的語言相比具有更高的運行速度,佔用更小的內存,因此在一些對於時效性要求很高的程序、許多大型程序的核心模塊以及工業控制方面大量應用。機器語言和彙編語言,二者是幾乎很少或者完全沒有做任何語法抽象的,這種語言我們通常稱之為低級語言,這種更加接近硬體,而且是不可以在不同硬體間移植的。
  • 微信影視小程序從0開始實戰-無需懂代碼
    最近圈子裡很火爆的就是微信小程序了··· 不知道大家是否有注意,影視類一直是比較容易獲得認可的資源種類。 總有那麼一個時間點,你想看某部電影或者電視,結果發現一是沒資源,二是要付費。
  • android反編譯和防止反編譯的方法
    【IT168技術】android基於java的,而java反編譯工具很強悍,所以對正常apk應用程式基本上可以做到100%反編譯還原。  因此開發人員如果不準備開源自己的項目就需要知道怎樣防止反編譯和反編譯他人的項目來學習。
  • 便民超市小程序源碼,生鮮、超市等實體店開發微信小程序的意義
    超市商城小程序的開啟,可謂是打開一個全新的渠道,通過手機就能夠迅速的選購自己的產品,完全不需要浪費更多的時間在產品的甄選以及路途的奔波上。便民超市小程序源碼,生鮮、超市等實體店開發微信小程序的意義在哪裡?
  • 介紹一款好用的java反編譯工具 - jd-gui
    在排查問題時,很多時候我們需要查看這些二方包和三方包的源碼。二方包是公司內部其它部門的產出物,雖然折騰些但協調下一般還是能查看到源碼的;三方包如果是開源的項目,也能找到源碼;但對於非開源項目的 jar 包,如果想查看源碼,就不得不用到反編譯工具了。
  • 微信小程序中如何使用WebSocket實現長連接(含完整源碼)
    1、前言微信小程序提供了一套在微信上運行小程序的解決方案,有比較完整的框架、組件以及 API,在這個平臺上面的想像空間很大。騰訊雲研究了一番之後,發現微信支持 WebSocket 還是很值得玩味的。這個特性意味著我們可以做一些實時同步或者協作的小程序。
  • SAST Weekly|靜態反編譯軟體IDA使用簡介
    說到反編譯,很多人想到的可能是修改,破解。但是實際上軟體的逆向工程在我們生活中有很多*正當*的應用。這期weekly將給大家簡要介紹下利用IDA的反編譯和在漢化上的應用。    如果要讀取軟體在做什麼,顯然只靠讀內存是不行的(人均手讀機器碼),我們還需要一定的輔助工具,把機器碼翻譯成近似於自然語言的彙編語言,以及將彙編翻譯成類c的偽代碼(pseudocode),來讓我更好的理解程序在做什麼,程序要做什麼以及現在的狀態。做這一類的軟體有很多,但是功能數目不同,就好像編程時IDE的代碼補全和糾錯能力等。反編譯軟體也有不同的擴展功能。
  • 如何反編譯Android 5.0 framework
    為了更好的適配,我們不得不對framework層進行反編譯,在Android更新到5.0後,開發人員對framework的反編譯也出現了新的變化。以前分散在framework文件夾根目錄裡的那些odex文件全部集中在了framework文件夾中的arm(或arm64)子文件夾中,而且通過正常的反編譯發現這些odex並不是像5.0以前一樣是我們所需要的東西。
  • 從零開始:微信小程序新手入門寶典
    ;二:微信小程序官方地址1:官方工具:https://mp.weixin.qq.com/debug/wxadoc/dev/devtools/download.html?  《二》開發組件使用初步,配置  《三》APP生命周期  《四》頁面生命周期和參數傳遞  《五》布局基礎  程式設計師實戰系列:  程式設計師開發實戰系列《一》註冊、預覽小程序  程式設計師開發實戰系列《二》微信小程序架構篇  程式設計師開發實戰系列
  • 【CUDA學習筆記】第八篇:源碼編譯OpenCV+CUDA模塊(完整源碼打包一次成功編譯)
    1、OpenCV+CUDA+Contrib的源碼編譯2、OpenCV+CUDA+Contrib的測試3、源碼級相關難下載文件的集成下載(附連結)1、OpenCV+CUDA+Contrib的源碼編譯1、cmake軟體打開,選擇opencv源碼文件夾
  • 腳本類惡意程序分析技巧匯總
    前方內容較多,請準備好花生瓜子python樣本分析對於python打包的exe,如何反編譯得到源碼成了最關鍵的問題。那麼我們首先來將一個py文件打包成exe,了解一下這個過程和python打包的exe的特徵打包一個hello world首先安裝python2.7,記得添加環境變量。
  • 面試官:Linux下如何編譯C程序?
    本文轉載自【微信公眾號:嵌入式大雜燴】,經微信公眾號授權轉載,如需轉載與原文作者聯繫Windows下常用IDE來編譯,Linux下直接使用gcc來編譯,編譯過程是Linux嵌入式編程的基礎,也是嵌入式高頻基礎面試問題