微信反编译(二)源码还原
概覽
我們知道,前端 Web 網(wǎng)頁(yè)編程采用的是?HTML + CSS + JS?這樣的組合,其中?HTML?是用來(lái)描頁(yè)面的結(jié)構(gòu),CSS?用來(lái)描述頁(yè)面的樣子,JS?通常用來(lái)處理頁(yè)面邏輯和用戶的交互。類似地,在小程序中也有同樣的角色,一個(gè)小程序工程主要包括如下幾類文件:
- .json?后綴的 JSON 配置文件
- .wxml?后綴的 WXML 模板文件
- .wxss?后綴的 WXSS 樣式文件
- .js?后綴的 JavaScript 腳本邏輯文件
例如“知識(shí)小集”的小程序源碼工程結(jié)構(gòu)如下:
然而,根據(jù)上一篇文章介紹,對(duì)“知識(shí)小集”小程序的?.wxapkg?解包后得到如下文件:
主要包括?app-config.json,?app-service.js,?page-frame.html,?*.html,?資源文件?等,但這些文件已經(jīng)被“編譯混淆”并重新整合壓縮,微信開發(fā)者工具并不能識(shí)別它們,我們無(wú)法直接對(duì)它們進(jìn)行調(diào)試/編譯運(yùn)行。
所以,我們先嘗試分析一下從?.wxapkg?提取出來(lái)的各個(gè)文件內(nèi)容的結(jié)構(gòu)及其用途,然后介紹如何用腳本工具把它們一鍵還原為“編譯”前的源碼,并在微信開發(fā)者工具中跑起來(lái)。
文件分析
本節(jié)主要以“知識(shí)小集”小程序的?.wxapkg?解包后的源碼文件為例,進(jìn)行分析。
你也可以跳過(guò)本節(jié)的分析,直接看下一節(jié)介紹用腳本“反編譯”還原源碼。
app-config.json
小程序工程主要包括工具配置?project.config.json,全局配置?app.json?以及頁(yè)面配置?page.json?三類 JSON 配置文件。其中:
project.config.json?主要用于對(duì)開發(fā)者工具進(jìn)行個(gè)性化配置以及包括小程序項(xiàng)目工程的一些基礎(chǔ)配置,所以它不會(huì)被“編譯”到?.wxapkg?包中;
app.json?是對(duì)當(dāng)前小程序的全局配置,包括了小程序的所有頁(yè)面路徑、界面表現(xiàn)、網(wǎng)絡(luò)超時(shí)時(shí)間、底部 tab 等;
page.json?用于對(duì)每一個(gè)頁(yè)面的窗口表現(xiàn)進(jìn)行配置,頁(yè)面中配置項(xiàng)會(huì)覆蓋?app.json?的?window?中相同的配置項(xiàng)。
因此“編譯”后的文件?app-config.json?其實(shí)就是?app.json?和各個(gè)頁(yè)面的配置文件的匯總,它的內(nèi)容大致如下:
{
"page": {?// 各頁(yè)面配置
"pages/index/index.html": {?// 某一頁(yè)面地址
"window": {?// 某一頁(yè)面具體配置
"navigationBarTitleText":?"知識(shí)小集",
"enablePullDownRefresh":?true
}
},
// 此處省略...
},
"entryPagePath":?"pages/index/index.html",?// 小程序入口地址
"pages": ["pages/index/index",?"pages/detail/detail",?"pages/search/search"],?// 頁(yè)面列表
"global": {?// 全局頁(yè)面配置
"window": {
"navigationBarTextStyle":?"black",
"navigationBarTitleText":?"知識(shí)小集",
"navigationBarBackgroundColor":?"#F8F8F8",
"backgroundColor":?"#F8F8F8"
}
}
}
通過(guò)與原工程?app.json?和各頁(yè)面配置?page.json?內(nèi)容的對(duì)比,我們可以得出?app-config.json?匯總文件的簡(jiǎn)單整合規(guī)律,很容易把它拆分成“編譯”前對(duì)應(yīng)的各?json?文件。
app-service.js
在小程序項(xiàng)目中?JS?文件負(fù)責(zé)交互邏輯,主要包括?app.js,每個(gè)頁(yè)面的?page.js,開發(fā)者自定義的?JS?文件和引入的第三方?JS?文件,在“編譯”后所有這些?JS?文件都會(huì)被匯總到?app-service.js?文件中,它的結(jié)構(gòu)如下:
// 一些全局變量的聲明
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(){};
// 小程序編譯基礎(chǔ)庫(kù)版本
/*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";
// ... 具體源碼內(nèi)容
});
// ...
// app.js 源碼定義
define("app.js",?function(...) {
"use strict";
// ... app.js 源碼內(nèi)容
});
require("app.js");
// 每個(gè)頁(yè)面對(duì)應(yīng)的 JS 源碼定義
__wxRoute =?'pages/index/index';?// 頁(yè)面路由地址
__wxRouteBegin =?true;
define("pages/index/index.js",?function(...){
"use strict";
// ... page.js 源碼內(nèi)容
});
require("pages/index/index.js");
在這個(gè)文件中,原有小程序工程中的每個(gè)?JS?文件都被?define?方法定義聲明,定義中包含?JS?文件的路徑和內(nèi)容,如下:
define("path/to/xxx.js",?function(...){
"use strict";
// ... xxx.js 源碼內(nèi)容
});
因此,我們同樣很容易提取這些?JS?文件源碼,并恢復(fù)至相應(yīng)的路徑位置中。當(dāng)然,這些?JS?文件中的內(nèi)容經(jīng)過(guò)混淆壓縮,我們可以使用?UglifyJS?這樣的工具進(jìn)行美化,但仍很難還原一些原始變量名,不過(guò)基本不影響正常閱讀和使用。
page-frame.html
在小程序中使用?WXML?文件描述頁(yè)面的結(jié)構(gòu),WXSS?文件描述頁(yè)面的樣式。工程中有一個(gè)?app.wxss?文件用于定義一些全局的樣式,會(huì)自動(dòng)被?import?到各個(gè)頁(yè)面中;另外每個(gè)頁(yè)面也都分別包含?page.wxml?和?page.wxss?用于描述其頁(yè)面的結(jié)構(gòu)和樣式;同時(shí),我們也會(huì)自定義一些公共的?xxxCommon.wxss?樣式文件和公共的?xxxTemplate.wxml?模板文件供一些頁(yè)面復(fù)用,一般在各自頁(yè)面的?page.wxss?和?page.wxml?中去?import。
當(dāng)“編譯”小程序后,所有的?.wxml?文件和?app.wxss?及公共?xxxCommon.wxss?樣式文件的將被整合到?page-frame.html?文件中,而每個(gè)頁(yè)面的?page.wxss?樣式文件,將分別單獨(dú)在各自的路徑下生成一個(gè)?page.html?文件。
page-frame.html?文件的內(nèi)容結(jié)構(gòu)如下:
<!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
};
// 小程序編譯基礎(chǔ)庫(kù)版本
/*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([...]);?// 設(shè)置 app.wxss 的內(nèi)容到 Head 中,其中 ... 為小程序工程中 app.wxss 的內(nèi)容
var?__pageFrameEndTime__ =?Date.now()
</script>
</head>
<body>
<div></div>
</body>
</html>
相比其他文件,page-frame.html?比較復(fù)雜,微信把?.wxml?和部分?.wxss?直接“編譯”并混淆成?JS?代碼放入上述文件中,然后通過(guò)調(diào)用這些?JS?代碼來(lái)構(gòu)造?Virtual-Dom,進(jìn)而渲染頁(yè)面。
其中最核心的是?$gwx?和?setCssToHead?這兩個(gè)方法。
$gwx?用于通過(guò)?JS?代碼生成所有?.wxml?文件,其中每個(gè)?.wxml?文件的內(nèi)容結(jié)構(gòu)都在?$gwx?方法中被定義好并混淆了,我們只要傳給它頁(yè)面的?.wxml?路徑參數(shù),即可獲取到每個(gè)?.wxml?的內(nèi)容,再簡(jiǎn)單加工一下即可還原成“編譯”前的內(nèi)容。
在?$gwx?中有一個(gè)?x?數(shù)組用于存儲(chǔ)當(dāng)前小程序都有哪些?.wxml?文件,例如,“知識(shí)小集”小程序的?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'];此時(shí)我們可以在?Chrome?中打開?page-frame.html?文件,然后在?Console?中輸入如下命令,即可得到?index.wxml?的內(nèi)容(輸出一個(gè)?JS?對(duì)象,通過(guò)遍歷這個(gè)對(duì)象即可還原出?.wxml?的內(nèi)容)
$gwx("./pages/index/index.wxml")setCssToHead?方法用于根據(jù)幾段被拆分的樣式字符串?dāng)?shù)組生成?.wxss?代碼并設(shè)置到?HTML?的?Head?中,同時(shí),它還將所有被?import?引用的?.wxss?文件(公共?xxxCommon.wxss樣式文件)所對(duì)應(yīng)的樣式數(shù)組內(nèi)嵌在該方法中的?_C?變量中,并標(biāo)記哪些文件引用了?_C?中數(shù)據(jù)。另外在?page-frame.html?文件的末尾,調(diào)用了該方法生成全局?app.wxss?的內(nèi)容設(shè)置到?Head中。
因此,我們可以在每個(gè)調(diào)用?setCssToHead?方法的地方提取相應(yīng)?.wxss?的內(nèi)容并還原。
對(duì)于?page-frame.html?文件中?$gwx?和?setCssToHead?這兩個(gè)方法更詳細(xì)的分析,可以參考這篇文章。
此外,checkDeviceWidth?方法顧明思議,用于檢測(cè)屏幕的寬度,其檢測(cè)結(jié)果將用于?transformRPX?方法中將?rpx?單位轉(zhuǎn)換為?px?像素。
rpx?的全稱是?responsive pixel,它是小程序自己定義的一個(gè)尺寸單位,可以根據(jù)當(dāng)前設(shè)備屏幕寬度進(jìn)行自適應(yīng)。小程序中規(guī)定,所有的設(shè)備屏幕寬度都為?750rpx,根據(jù)設(shè)備屏幕實(shí)際寬度的不同,1rpx所代表的實(shí)際像素值也不一樣。
*.html
上面提到,每個(gè)頁(yè)面的?page.wxss?樣式文件,“編譯”后將分別在各自的所在路徑下生成一個(gè)?page.html?文件,每個(gè)?page.html?的結(jié)構(gòu)如下:
<style></style>
<page></page>
<script>
var?__setCssStartTime__ =?Date.now();
setCssToHead([...])()?// 設(shè)置 search.wxss 的內(nèi)容
var?__setCssEndTime__ =?Date.now();
document.dispatchEvent(new?CustomEvent("generateFuncReady", {
detail: {
generateFunc: $gwx('./pages/search/search.wxml')
}
}))
</script>
在該文件中通過(guò)調(diào)用?setCssToHead?方法將?.wxss?樣式內(nèi)容設(shè)置到?Head?中,所以同樣地,我們可以根據(jù)?setCssToHead?的調(diào)用參數(shù)提取每個(gè)頁(yè)面的?page.wxss。
資源文件
小程序工程中的圖片、音頻等資源文件在“編譯”后將直接被拷貝到?.wxapkg?包中,其原始的路徑也保留不變,因此我們可以直接使用。
“反編譯”
在上一節(jié),我們完成了?.wxapkg?包幾乎所有文件內(nèi)容的簡(jiǎn)要分析。現(xiàn)在我們介紹一下如何通過(guò)?node.js?腳本幫我們還原出小程序的源碼。
在這里需要再次感謝?wxappUnpacker?作者提供的還原工具,讓我們可以“站在巨人的肩膀上”輕松地去完成“反編譯”。它的使用如下:
-
node wuConfig.js <path/to/app-config.json>?: 將?app-config.json?中的內(nèi)容拆分成各個(gè)頁(yè)面所對(duì)應(yīng)的?page.json和?app.json;
-
node wuJs.js <path/to/app-service.js>?: 將?app-service.js?拆分成一系列原先獨(dú)立的?JS?文件,并使用?Uglify-ES美化工具盡可能將代碼還原為“編譯”前的內(nèi)容;
-
node wuWxml.js [-m] <path/to/page-frame.html>?: 從?page-frame.html?中提取并還原各頁(yè)面的?.wxml?和?app.wxss及公共?.wxss?樣式文件;
-
node wuWxss.js <path/to/unpack_dir>?: 該命令參數(shù)為?.wxapkg?解包后目錄,它將分析并從各個(gè)?page.html?中提取還原各頁(yè)面的?page.wxss?樣式文件;
同時(shí),作者還提供了一鍵解包并還原的腳本,你只需要提供一個(gè)小程序的?.wxapkg?文件,然后執(zhí)行如下命令:
node wuWxapkg.js [-d] <path/to/.wxapkg>此腳本就會(huì)自動(dòng)將?.wxapkg?文件解包,并將包中相關(guān)的已被“編譯/混淆”的文件自動(dòng)地恢復(fù)原狀(包括目錄結(jié)構(gòu))。
PS: 此工具依賴?uglify-es,?vm2,?esprima,?cssbeautify,?css-tree?等?node.js?包,所以你可能需要?npm install xxx?安裝這些依賴包才能正確執(zhí)行。
更詳細(xì)的用法及相關(guān)問(wèn)題請(qǐng)查閱該開源項(xiàng)目的 GitHub repo。
最后,我們?cè)?微信開發(fā)者工具?中新建一個(gè)空小程序工程,并將上述還原后的相關(guān)目錄文件導(dǎo)入工程,即可編譯運(yùn)行起來(lái),如下圖為“知識(shí)小集”小程序的?.wxapkg?包還原后的代碼工程:
以上,大功告成!
總結(jié)
本文詳細(xì)分析了?.wxapkg?解包后的各文件結(jié)構(gòu),并介紹了如何通過(guò)腳本“一鍵還原”得到任意小程序的源碼。
對(duì)于一些簡(jiǎn)單的,且使用微信官方介紹的原生開發(fā)方式開發(fā)的小程序,用上述工具基本可以直接還原得到可運(yùn)行的源碼,但是對(duì)于一些邏輯復(fù)雜,或者使用?WePY、Vue?等一些框架開發(fā)的小程序,還原后的源碼可能會(huì)有一些小問(wèn)題,需要我們?nèi)巳馊シ治鼋鉀Q。
后續(xù)
本文對(duì)小程序源碼“編譯”后的各文件內(nèi)容結(jié)構(gòu)及用途的分析相對(duì)比較零散,而且沒有對(duì)各文件的依賴關(guān)系及加載邏輯進(jìn)行研究,后續(xù)我們?cè)賹懸恍┪恼轮v解微信客戶端是如何解析加載小程序?.wxapkg?包并運(yùn)行起來(lái)。
總結(jié)
以上是生活随笔為你收集整理的微信反编译(二)源码还原的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 程序员的日常生活
- 下一篇: 恒源智GPU云服务器