前言众所周知,“跳一跳”在前几个月很火,并且出现了包括通过规则匹配/机器学习得到关键点坐标后模拟点击和通过源码获知加密方式伪造请求等方法。后者提到了如何获取含有源码的程序包 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, 更多干货等着你~ |