前言眾所周知,“跳一跳”在前幾個月很火,并且出現了包括通過規則匹配/機器學習得到關鍵點坐標后模擬點擊和通過源碼獲知加密方式偽造請求等方法。后者提到了如何獲取含有源碼的程序包 wxapkg ,以及使其能夠在微信開發者工具中具體步驟(見參考鏈接1)。 當時我在對其他微信小程序應用進行嘗試的時候發現,他們不同于小游戲,解包后的文件并不能通過簡單增改就直接在微信開發者工具中運行,于是對小程序源代碼=>wxapkg包內文件的具體轉換關系進行了一定研究。 正文包由前文知,我們可以通過查看 Android 手機中的 /data/data/com.tencent.mm/MicroMsg/{User}/appbrand/pkg({User} 為當前用戶的用戶名,類似于2bc**************b65)文件夾,獲取最近使用過的微信小程序所對應的 wxapkg 包文件。 通過簡單分析知,這個包由文件名+文件內容起始地址及長度信息開頭,且各個文件明文存放在包內,通過類似于 https://gist.github.com/feix/32ab8f0dfe99aa8efa84f81ed68a0f3e 的腳本(這一個腳本處理包內二進制文件時有個小 bug ,將第78行的 w 改成 wb 即可),我們可以輕易獲取包內文件。(具體解包細節可見于參考鏈接3) 但是這個包中的文件內容主要如下: app-config.json app-service.js page-frame.html 其他一堆放在各文件夾中的.html文件 和源碼包內位置和內容相同的圖片等資源文件 微信開發者工具并不能識別這些文件,它要求我們提供由wxml/wxss/js/wxs/json組成的源碼才能進行模擬/調試。 js注意到app-service.js中的內容由 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 文件都調用了setCssToHead函數,其代碼如下 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 代碼拆分成幾段數組,數組中的內容可以是一段將要作為 css 文件的字符串,也可以是一個表示 這里要添加一個公共后綴 或 這里要包含另一段代碼 或 要將以 wxss 專供的 rpx 單位表達的數字換算成能由瀏覽器渲染的 px 單位所對應的數字 的數組。 同時,它還將所有被 import 引用的 wxss 文件所對應的數組內嵌在該函數中的 _C 變量中。 我們可以修改setCssToHead,然后執行所有的setCssToHead,第一遍先判斷出 _C 變量中所有的內容是哪個要被引用的 wxss 提供的,第二遍還原所有的 wxss。值得注意的是,可能出于兼容性原因,微信為很多屬性自動補上含有-webkit-開頭的版本,另外幾乎所有的 tag 都加上了wx-前綴,并將page變成了body。通過一些 CSS 的 AST ,例如 CSSTree,我們可以去掉這些東西。 jsonapp-config.json 中的page對象內就是其他各頁面所對應的 json , 直接還原即可,余下的內容便是 app.json 中的內容了,除了格式上要作相應轉換外,微信還將iconPath的內容由原先指向圖片文件的地址轉換成iconData中圖片內容的 base64 編碼,所幸原來的圖片文件仍然保留在包內,通過比較iconData中的內容和其他包內文件,我們找到原始的iconPath。 wxs在 page-frame.html 中,我們找到了這樣的內容 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'](); 可以看出微信將內嵌和外置的 wxs 都轉譯成np_%d函數,并由f_數組來描述他們。轉譯的主要變換是調用的函數名稱都加上了nv_前綴。在不嚴謹的場合,我們可以直接通過文本替換去除這些前綴。 wxml相比其他內容,這一段比較復雜,因為微信將原本 類 xml 格式的 wxml 文件直接編譯成了 js 代碼放入 page-frame.html 中,之后通過調用這些代碼來構造 virtual-dom,進而渲染網頁。 首先,微信將所有要動態計算的變量放在了一個由函數構造的z數組中,構造部分代碼如下: (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); 其實可以將[[id],xxx,yyy]看作由指令與操作數的組合。注意每個這樣的數組作為指令所產生的結果會作為外層數組中的操作數,這樣可以構成一個樹形結構。通過將遞歸計算的過程改成拼接源代碼字符串的過程,我們可以還原出每個數組所對應的實際內容。下文中,將這個數組中記為z。 然后,對于 wxml 文件的結構,可以將每種可能的 js 語句拆分成 指令 來分析,這里可以用到 Esprima 這樣的 js 的 AST 來簡化識別操作,可以很容易分析出以下內容,例如:
此外wx:if結構和wx:for可做遞歸處理。例如,對于如下wx:if結構: 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 } 相當于將以下節點放入{parName}節點下(z[{id1}]應替換為對應的z數組中的值): <block wx:if="z[{id1}]"> <!--content1--> </block> <block wx:elif="z[{id2}]"> <!--content2--> </block> <block wx:else> <!--content3--> </block> 具體實現中可以將遞歸時創建好多個block,調用子函數時指明將放入{name}下(_({name},{son}))識別為放入對應{block}下。wx:for也可類似處理,例如: var {name}=_v() _({parName},{name}) var {funcName}=function(..,..,{fakeRoot},..){ //content return {fakeRoot} } aDB.wxXCkey=2 _2({id},{funcName},..,..,..,..,'{item}','{index}','{key}') 對應(z[{id1}]應替換為對應的z數組中的值): <view wx:for="{z[{id}]}" wx:for-item="{item}" wx:for-index="{index}" wx:key="{key}"> <!--content--> </view> 調用子函數時指明將放入 {fakeRoot}下(_({fakeRoot},{son})) 識別為放入{name}下。 除此之外,有時我們還要將一組代碼標記為一個指令,例如下面: 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) 對應于{parName}下添加如下節點: <template is="z[{isId}]" data="z[{dataId}]"></template> 還有import和include的代碼比較分散,但其實只要抓住重點的一句話就可以了,例如: var {name}=e_[x[{to}]].i //Other code _ai({name},x[{from}],e_,x[{to}],..,..) //Other code {name}.pop() 對應與(其中的x是直接定義在 page-frame.html 中的字符串數組): <import src="x[{from}]" /> 而include類似: var {name}=e_[x[0]].j //Other code _ic(x[{from}],e_,x[{to}],..,..,..,..); //Other code {name}.pop() 對應與: <include src="x[{from}]" /> 可以看到我們可以在處理時忽略前后兩句話,把中間的_ic和_ai處理好就行了。 通過解析 js 把 wxml 大概結構還原后,可能相比編譯前的 wxml 顯得臃腫,可以考慮自動簡化,例如: <block wx:if="xxx"> <view> <!--content--> </view> </block> 可簡化為: <view wx:if="xxx"> <!--content--> </view> 這樣,我們完成了幾乎所有 wxapkg包 內容的還原。 工具
參考鏈接
本文由看雪論壇 qwertyaa 原創 轉載請注明來自看雪社區 往期熱門閱讀:
點擊閱讀原文/read, 更多干貨等著你~ |