小程序模板網(wǎng)

教你如何反編譯微信小程序

發(fā)布時(shí)間:2018-05-08 14:15 所屬欄目:小程序開(kāi)發(fā)教程

前言

眾所周知,“跳一跳”在前幾個(gè)月很火,并且出現(xiàn)了包括通過(guò)規(guī)則匹配/機(jī)器學(xué)習(xí)得到關(guān)鍵點(diǎn)坐標(biāo)后模擬點(diǎn)擊和通過(guò)源碼獲知加密方式偽造請(qǐng)求等方法。后者提到了如何獲取含有源碼的程序包 wxapkg ,以及使其能夠在微信開(kāi)發(fā)者工具中具體步驟(見(jiàn)參考鏈接1)。

當(dāng)時(shí)我在對(duì)其他微信小程序應(yīng)用進(jìn)行嘗試的時(shí)候發(fā)現(xiàn),他們不同于小游戲,解包后的文件并不能通過(guò)簡(jiǎn)單增改就直接在微信開(kāi)發(fā)者工具中運(yùn)行,于是對(duì)小程序源代碼=>wxapkg包內(nèi)文件的具體轉(zhuǎn)換關(guān)系進(jìn)行了一定研究。

正文

由前文知,我們可以通過(guò)查看 Android 手機(jī)中的 /data/data/com.tencent.mm/MicroMsg/{User}/appbrand/pkg({User} 為當(dāng)前用戶的用戶名,類似于2bc**************b65)文件夾,獲取最近使用過(guò)的微信小程序所對(duì)應(yīng)的 wxapkg 包文件。

通過(guò)簡(jiǎn)單分析知,這個(gè)包由文件名+文件內(nèi)容起始地址及長(zhǎng)度信息開(kāi)頭,且各個(gè)文件明文存放在包內(nèi),通過(guò)類似于 https://gist.github.com/feix/32ab8f0dfe99aa8efa84f81ed68a0f3e 的腳本(這一個(gè)腳本處理包內(nèi)二進(jìn)制文件時(shí)有個(gè)小 bug ,將第78行的 w 改成 wb 即可),我們可以輕易獲取包內(nèi)文件。(具體解包細(xì)節(jié)可見(jiàn)于參考鏈接3)

但是這個(gè)包中的文件內(nèi)容主要如下:

app-config.json

app-service.js

page-frame.html

其他一堆放在各文件夾中的.html文件

和源碼包內(nèi)位置和內(nèi)容相同的圖片等資源文件

微信開(kāi)發(fā)者工具并不能識(shí)別這些文件,它要求我們提供由wxml/wxss/js/wxs/json組成的源碼才能進(jìn)行模擬/調(diào)試。

js

注意到app-service.js中的內(nèi)容由

define('xxx.js',function(...){
//The content of xxx.js
});require('xxx.js');
define('yyy.js',function(...){
//The content of xxx.js
});require('yyy.js');
....


組成,很顯然,我們只要定義自己的define函數(shù)就可以將這些 js 文件恢復(fù)到源碼中所對(duì)應(yīng)的位置。當(dāng)然,這些 js 文件中的內(nèi)容經(jīng)過(guò)壓縮,即使使用 UglifyJS 這樣的工具進(jìn)行美化,也無(wú)法還原一些原始變量名。

wxss

所有在 wxapkg 包中的 html 文件都調(diào)用了setCssToHead函數(shù),其代碼如下

var setCssToHead = function(file, _xcInvalid) {
    var Ca = {};
    var _C = [...arrays...];
    function makeup(file, suffix) {
        var _n = typeof file === "number";
        if (_n && Ca.hasOwnProperty(file)) return "";
        if (_n) Ca[file] = 1;
        var ex = _n ? _C[file] : file;
        var res = "";
        for (var i = ex.length - 1; i >= 0; i--) {
            var content = ex[i];
            if (typeof content === "object") {
                var op = content[0];
                if (op == 0) res = transformRPX(content[1]) + "px" + res; else if (op == 1) res = suffix + res; else if (op == 2) res =makeup(content[1], suffix) + res;
            } else res = content + res;
        }
        return res;
    }
    return function(suffix, opt) {
        if (typeof suffix === "undefined") suffix = "";
        if (opt && opt.allowIllegalSelector != undefined && _xcInvalid != undefined) {
            if (opt.allowIllegalSelector) console.warn("For developer:" + _xcInvalid); else {
                console.error(_xcInvalid + "This wxss file is ignored.");
                return;
            }
        }
        Ca = {};
        css = makeup(file, suffix);
        var style = document.createElement("style");
        var head = document.head || document.getElementsByTagName("head")[0];
        style.type = "text/css";
        if (style.styleSheet) {
            style.styleSheet.cssText = css;
        } else {
            style.appendChild(document.createTextNode(css));
        }
        head.appendChild(style);
    };
};

閱讀這段代碼可知,它把 wxss 代碼拆分成幾段數(shù)組,數(shù)組中的內(nèi)容可以是一段將要作為 css 文件的字符串,也可以是一個(gè)表示 這里要添加一個(gè)公共后綴 或 這里要包含另一段代碼 或 要將以 wxss 專供的 rpx 單位表達(dá)的數(shù)字換算成能由瀏覽器渲染的 px 單位所對(duì)應(yīng)的數(shù)字 的數(shù)組。

同時(shí),它還將所有被 import 引用的 wxss 文件所對(duì)應(yīng)的數(shù)組內(nèi)嵌在該函數(shù)中的 _C 變量中。

我們可以修改setCssToHead,然后執(zhí)行所有的setCssToHead,第一遍先判斷出 _C 變量中所有的內(nèi)容是哪個(gè)要被引用的 wxss 提供的,第二遍還原所有的 wxss。值得注意的是,可能出于兼容性原因,微信為很多屬性自動(dòng)補(bǔ)上含有-webkit-開(kāi)頭的版本,另外幾乎所有的 tag 都加上了wx-前綴,并將page變成了body。通過(guò)一些 CSS 的 AST ,例如 CSSTree,我們可以去掉這些東西。

json

app-config.json 中的page對(duì)象內(nèi)就是其他各頁(yè)面所對(duì)應(yīng)的 json , 直接還原即可,余下的內(nèi)容便是 app.json 中的內(nèi)容了,除了格式上要作相應(yīng)轉(zhuǎn)換外,微信還將iconPath的內(nèi)容由原先指向圖片文件的地址轉(zhuǎn)換成iconData中圖片內(nèi)容的 base64 編碼,所幸原來(lái)的圖片文件仍然保留在包內(nèi),通過(guò)比較iconData中的內(nèi)容和其他包內(nèi)文件,我們找到原始的iconPath。

wxs

在 page-frame.html 中,我們找到了這樣的內(nèi)容

f_['a/comm.wxs'] = nv_require("p_a/comm.wxs");
function np_0(){var nv_module={nv_exports:{}};nv_module.nv_exports = ({nv_bar:nv_some_msg,});return nv_module.nv_exports;}
 
f_['b/comm.wxs'] = nv_require("p_b/comm.wxs");
function np_1(){var nv_module={nv_exports:{}};nv_module.nv_exports = ({nv_bar:nv_some_msg,});return nv_module.nv_exports;}
 
f_['b/index.wxml']={};
f_['b/index.wxml']['foo'] =nv_require("m_b/index.wxml:foo");
function np_2(){var nv_module={nv_exports:{}};var nv_some_msg = "hello world";nv_module.nv_exports = ({nv_msg:nv_some_msg,});returnnv_module.nv_exports;}
f_['b/index.wxml']['some_comms'] =f_['b/comm.wxs'] || nv_require("p_b/comm.wxs");
f_['b/index.wxml']['some_comms']();
f_['b/index.wxml']['some_commsb'] =f_['a/comm.wxs'] || nv_require("p_a/comm.wxs");
f_['b/index.wxml']['some_commsb']();

可以看出微信將內(nèi)嵌和外置的 wxs 都轉(zhuǎn)譯成np_%d函數(shù),并由f_數(shù)組來(lái)描述他們。轉(zhuǎn)譯的主要變換是調(diào)用的函數(shù)名稱都加上了nv_前綴。在不嚴(yán)謹(jǐn)?shù)膱?chǎng)合,我們可以直接通過(guò)文本替換去除這些前綴。

wxml

相比其他內(nèi)容,這一段比較復(fù)雜,因?yàn)槲⑿艑⒃?類 xml 格式的 wxml 文件直接編譯成了 js 代碼放入 page-frame.html 中,之后通過(guò)調(diào)用這些代碼來(lái)構(gòu)造 virtual-dom,進(jìn)而渲染網(wǎng)頁(yè)。

首先,微信將所有要?jiǎng)討B(tài)計(jì)算的變量放在了一個(gè)由函數(shù)構(gòu)造的z數(shù)組中,構(gòu)造部分代碼如下:

(function(z){var a=11;function Z(ops){z.push(ops)}
Z([3,'index']);
Z([[8],'text',[[4],[[5],[[5],[[5],[1,1]],[1,2]],[1,3]]]]);
})(z);

其實(shí)可以將[[id],xxx,yyy]看作由指令與操作數(shù)的組合。注意每個(gè)這樣的數(shù)組作為指令所產(chǎn)生的結(jié)果會(huì)作為外層數(shù)組中的操作數(shù),這樣可以構(gòu)成一個(gè)樹(shù)形結(jié)構(gòu)。通過(guò)將遞歸計(jì)算的過(guò)程改成拼接源代碼字符串的過(guò)程,我們可以還原出每個(gè)數(shù)組所對(duì)應(yīng)的實(shí)際內(nèi)容。下文中,將這個(gè)數(shù)組中記為z。

然后,對(duì)于 wxml 文件的結(jié)構(gòu),可以將每種可能的 js 語(yǔ)句拆分成 指令 來(lái)分析,這里可以用到 Esprima 這樣的 js 的 AST 來(lái)簡(jiǎn)化識(shí)別操作,可以很容易分析出以下內(nèi)容,例如:

  • var {name}=_n('{tag}') 創(chuàng)建名稱為{name}, tag 為{tag}的節(jié)點(diǎn)。

  • _r({name},'{attrName}',{id},e,s,gg)  將{name}的{attrName}屬性修改為z[{id}]的值。

  • _({parName},{name})  將{name}作為{parName}的子節(jié)點(diǎn)。

  • var {name}=_o({id},..,..,..)  創(chuàng)建名稱為{name},內(nèi)容為z[{id}]的文本節(jié)點(diǎn)。

  • var {name}=_v() 創(chuàng)建名稱為{name}的虛節(jié)點(diǎn)( wxml 里恰好提供了功能相當(dāng)?shù)奶摻Y(jié)點(diǎn)block, 這句話相當(dāng)于var {name}=_n('block'))。

  • var {name}=_m('{tag}',['{attrName1}',{id1},'{attrName2}',{id2},...],[],..,..,..)  創(chuàng)建名稱為{name}, tag 為{tag}的節(jié)點(diǎn),同時(shí)將{attrNameX}屬性修改為z[f({idX})]的值(f定義為{idX}與{base}的和;{base}初始為0,f返回的第一個(gè)正值后{base}即改為該返回值;若返回負(fù)值,表示該屬性無(wú)值)。

  • return {name}  名稱為{name}的節(jié)點(diǎn)設(shè)為主節(jié)點(diǎn)。

  • cs.*** 調(diào)試用語(yǔ)句,無(wú)視之。

此外wx:if結(jié)構(gòu)和wx:for可做遞歸處理。例如,對(duì)于如下wx:if結(jié)構(gòu):

var {name}=_v()
_({parName},{name})
if(_o({id1},e,s,gg)){oD.wxVkey=1
//content1
}
else if(_o({id2},e,s,gg)){oD.wxVkey=2
//content2
}
else{oD.wxVkey=3
//content3
}

相當(dāng)于將以下節(jié)點(diǎn)放入{parName}節(jié)點(diǎn)下(z[{id1}]應(yīng)替換為對(duì)應(yīng)的z數(shù)組中的值):

<block wx:if="z[{id1}]">
    <!--content1-->
</block>
<block wx:elif="z[{id2}]">
    <!--content2-->
</block>
<block wx:else>
    <!--content3-->
</block>

具體實(shí)現(xiàn)中可以將遞歸時(shí)創(chuàng)建好多個(gè)block,調(diào)用子函數(shù)時(shí)指明將放入{name}下(_({name},{son}))識(shí)別為放入對(duì)應(yīng){block}下。wx:for也可類似處理,例如:

var {name}=_v()
_({parName},{name})
var {funcName}=function(..,..,{fakeRoot},..){
//content
return {fakeRoot}
}
aDB.wxXCkey=2
_2({id},{funcName},..,..,..,..,'{item}','{index}','{key}')

對(duì)應(yīng)(z[{id1}]應(yīng)替換為對(duì)應(yīng)的z數(shù)組中的值):

<view wx:for="{z[{id}]}" wx:for-item="{item}" wx:for-index="{index}" wx:key="{key}">
    <!--content-->
</view>

調(diào)用子函數(shù)時(shí)指明將放入 {fakeRoot}下(_({fakeRoot},{son})) 識(shí)別為放入{name}下。

除此之外,有時(shí)我們還要將一組代碼標(biāo)記為一個(gè)指令,例如下面:

var lK=_v()
_({parName},lK)
var aL=_o({isId},e,s,gg)
var tM=_gd(x[0],aL,e_,d_)
if(tM){
var eN=_1({dataId},e,s,gg) || {}
var cur_globalf=gg.f
lK.wxXCkey=3
tM(eN,eN,lK,gg)
gg.f=cur_globalf
}
else _w(aL,x[0],11,26)

對(duì)應(yīng)于{parName}下添加如下節(jié)點(diǎn):

<template is="z[{isId}]" data="z[{dataId}]"></template>

還有import和include的代碼比較分散,但其實(shí)只要抓住重點(diǎn)的一句話就可以了,例如:

var {name}=e_[x[{to}]].i
//Other code
_ai({name},x[{from}],e_,x[{to}],..,..)
//Other code
{name}.pop()

對(duì)應(yīng)與(其中的x是直接定義在 page-frame.html 中的字符串?dāng)?shù)組):

<import src="x[{from}]" />

而include類似:

var {name}=e_[x[0]].j
//Other code
_ic(x[{from}],e_,x[{to}],..,..,..,..);
//Other code
{name}.pop()

對(duì)應(yīng)與:

<include src="x[{from}]" />

可以看到我們可以在處理時(shí)忽略前后兩句話,把中間的_ic和_ai處理好就行了。

通過(guò)解析 js 把 wxml 大概結(jié)構(gòu)還原后,可能相比編譯前的 wxml 顯得臃腫,可以考慮自動(dòng)簡(jiǎn)化,例如:

<block wx:if="xxx">
    <view>
        <!--content-->
    </view>
</block>

可簡(jiǎn)化為:

<view wx:if="xxx">
    <!--content-->
</view>

這樣,我們完成了幾乎所有 wxapkg包 內(nèi)容的還原。

工具

 

參考鏈接

  1. 如何獲得微信小游戲跳一跳源碼

  2. 微信小程序(.wxapkg)解包心得以及未開(kāi)放API猜測(cè)

  3. 微信小程序源碼閱讀筆記1

本文由看雪論壇 qwertyaa 原創(chuàng) 

轉(zhuǎn)載請(qǐng)注明來(lái)自看雪社區(qū)

往期熱門(mén)閱讀:

  • 看雪學(xué)院招募看雪講師

  • Windows是如何將64位Ntdll映射到32位進(jìn)程的

  • 使用Unicorn Engine繞過(guò)混淆完成算法的調(diào)用

  • 使用 Frida 逆向分析 Android 應(yīng)用與 BLE 設(shè)備的通信

  • 記一次linux(被)入侵

點(diǎn)擊閱讀原文/read,

更多干貨等著你~



易優(yōu)小程序(企業(yè)版)+靈活api+前后代碼開(kāi)源 碼云倉(cāng)庫(kù):starfork
本文地址:http://22321a.com/wxmini/doc/course/24336.html 復(fù)制鏈接 如需定制請(qǐng)聯(lián)系易優(yōu)客服咨詢:800182392 點(diǎn)擊咨詢
QQ在線咨詢