众所周知,“跳一跳”在前几个月很火,并且出现了包括通过规则匹配/机器学习得到关键点坐标后模拟点击和通过源码获知加密方式伪造请求等方法。后者提到了如何获取含有源码的程序包 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
组成的源码才能进行模拟/调试。
注意到app-service.js
中的内容由
组成,很显然,我们只要定义自己的define
函数就可以将这些 js 文件恢复到源码中所对应的位置。当然,这些 js 文件中的内容经过压缩,即使使用 UglifyJS 这样的工具进行美化,也无法还原一些原始变量名。
所有在 wxapkg 包中的 html 文件都调用了setCssToHead
函数,其代码如下
阅读这段代码可知,它把 wxss 代码拆分成几段数组,数组中的内容可以是一段将要作为 css 文件的字符串,也可以是一个表示 这里要添加一个公共后缀 或 这里要包含另一段代码 或 要将以 wxss 专供的 rpx 单位表达的数字换算成能由浏览器渲染的 px 单位所对应的数字 的数组。
同时,它还将所有被 import 引用的 wxss 文件所对应的数组内嵌在该函数中的 _C 变量中。
我们可以修改setCssToHead
,然后执行所有的setCssToHead
,第一遍先判断出 _C 变量中所有的内容是哪个要被引用的 wxss 提供的,第二遍还原所有的 wxss。值得注意的是,可能出于兼容性原因,微信为很多属性自动补上含有-webkit-
开头的版本,另外几乎所有的 tag 都加上了wx-
前缀,并将page
变成了body
。通过一些 CSS 的 AST ,例如 CSSTree,我们可以去掉这些东西。
app-config.json 中的page
对象内就是其他各页面所对应的 json , 直接还原即可,余下的内容便是 app.json 中的内容了,除了格式上要作相应转换外,微信还将iconPath
的内容由原先指向图片文件的地址转换成iconData
中图片内容的 base64 编码,所幸原来的图片文件仍然保留在包内,通过比较iconData
中的内容和其他包内文件,我们找到原始的iconPath
。
在 page-frame.html 中,我们找到了这样的内容
可以看出微信将内嵌和外置的 wxs 都转译成np_%d
函数,并由f_
数组来描述他们。转译的主要变换是调用的函数名称都加上了nv_
前缀。在不严谨的场合,我们可以直接通过文本替换去除这些前缀。
相比其他内容,这一段比较复杂,因为微信将原本 类 xml 格式的 wxml 文件直接编译成了 js 代码放入 page-frame.html 中,之后通过调用这些代码来构造 virtual-dom,进而渲染网页。
首先,微信将所有要动态计算的变量放在了一个由函数构造的z
数组中,构造部分代码如下:
其实可以将[[id],xxx,yyy]
看作由指令与操作数的组合。注意每个这样的数组作为指令所产生的结果会作为外层数组中的操作数,这样可以构成一个树形结构。通过将递归计算的过程改成拼接源代码字符串的过程,我们可以还原出每个数组所对应的实际内容(值得注意的是,由于微信的Token
解析程序采用了贪心算法,我们必须将连续的}
翻译为} }
而非}}
,否则会被误认为是Mustache
的结束符)。下文中,将这个数组中记为z
。
然后,对于 wxml 文件的结构,可以将每种可能的 js 语句拆分成 指令 来分析,这里可以用到 Esprima 这样的 js 的 AST 来简化识别操作,可以很容易分析出以下内容,例如:
此外wx:if
结构和wx:for
可做递归处理。例如,对于如下wx:if
结构:
相当于将以下节点放入{parName}
节点下(z[{id1}]
应替换为对应的z
数组中的值):
具体实现中可以将递归时创建好多个block
,调用子函数时指明将放入{name}
下(_({name},{son})
)识别为放入对应{block}
下。wx:for
也可类似处理,例如:
对应(z[{id1}]
应替换为对应的z
数组中的值):
调用子函数时指明将放入{fakeRoot}
下(_({fakeRoot},{son})
)识别为放入{name}
下。
除此之外,有时我们还要将一组代码标记为一个指令,例如下面:
对应于{parName}
下添加如下节点:
还有import
和include
的代码比较分散,但其实只要抓住重点的一句话就可以了,例如:
对应与(其中的x
是直接定义在 page-frame.html 中的字符串数组):
而include
类似:
对应与:
可以看到我们可以在处理时忽略前后两句话,把中间的_ic
和_ai
处理好就行了。
通过解析 js 把 wxml 大概结构还原后,可能相比编译前的 wxml 显得臃肿,可以考虑自动简化,例如:
可简化为:
这样,我们完成了几乎所有 wxapkg包 内容的还原。
wcc-v0.5vv_20180626_syb_zp
后通过只加载z
数组中需要的部分来提高小程序运行速度,这也会导致仅考虑到上述内容的解包程序解包失败,这一更新的主要内容如下:
对于上述变更,将获取z
数组处修改并添加对_rz
_2z
_mz
_1z
_oz
的支持即可。
需要注意的是开发版的z
数组转为如下结构:
探测到为开发版后应将获取到的z
数组仅保留数组中的第二项。
以及含分包的子包采用 gz$gwx{$subPackageId}_{$id}
命名,其中{$subPackageId}
是一个数字。
对于上述内容的转换,我写了一个可以直接使用的"反编译"工具(https://github.com/qwerty472123/wxappUnpacker),可以直接自动处理 wxapkg 包。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2018-9-22 22:55
被qwertyaa编辑
,原因: fix a bug