前言眾所周知,“跳一跳”在前幾個(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'); ....
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,我們可以去掉這些東西。 jsonapp-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)容,例如:
此外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)容的還原。 工具
參考鏈接
本文由看雪論壇 qwertyaa 原創(chuàng) 轉(zhuǎn)載請(qǐng)注明來(lái)自看雪社區(qū) 往期熱門(mén)閱讀:
點(diǎn)擊閱讀原文/read, 更多干貨等著你~ |
工作日 8:30-12:00 14:30-18:00
周六及部分節(jié)假日提供值班服務(wù)