零、背景
由于需要找出一些小程序内部的逻辑,我对小程序的理解是一个类似于react native的东西,用java script写的代码,通过一个解释引擎将其解析成安卓原生的控件,所以核心的逻辑应该是写在某个js文件内,因此就需要想办法拿到小程序的运行包以及得到运行包内的js代码。
一、寻找小程序包
1.1 初步分析
要拿到程序包,最简单的办法就是寄希望于小程序是一个下载在本地的包,这样只需要找到这个包,再对包进行分析。所以第一步就是需要确认小程序确实是一个本地可执行的包。首先随便找一个小程序,安装运行,在断网并且kill掉微信重启后发现小程序依然能运行,这样就排除掉了网络加载和内存运行两种非持久化方案,因此可以确认小程序应该是从一个本地包运行起来的了。接下就是找到整个小程序的包安装在哪里。
杀死所有微信相关进程的命令如下
root@shamu:/ # ps |grep tencent
u0_a98 2752 375 2100724 190148 ffffffff b6e58784 S com.tencent.mm
u0_a98 2834 375 1643756 79344 ffffffff b6e58784 S com.tencent.mm:exdevice
u0_a98 2905 375 1654692 78280 ffffffff b6e58784 S com.tencent.mm:push
u0_a98 3001 375 1952576 109060 ffffffff b6e58784 S com.tencent.mm:appbrand0
u0_a98 3086 375 1947448 114104 ffffffff b6e58784 S com.tencent.mm:support
u0_a98 3234 375 1951704 108988 ffffffff b6e58784 S com.tencent.mm:tools
root@shamu:/ # kill -9 2752 2834 2905 3001 3086 3234
1.2 变化确认
这里用到了BeyondCompare,通过对比安装前和安装后的/data/data/com.tencent.mm
下的各个目录大小来找到微信小程序有可能安装的目录。运行一下命令得到所有微信私有目录文件:
cp -r /data/data/com.tencent.mm/ /sdcard/
adb pull /sdcard/com.tencent.mm ~/Desktop
在得到安装前和安装后两个目录后使用Beyond Compare比较
发现MicroMsg和cache两个文件夹大小变化比较明显,cache应该不太可能。直接进入MicroMsg,在一路查找
发现.wxapkg
这样后缀的包新增了,由此确定这个包就是刚刚安装的“小程序示例”这个小程序了。
1.3 提取包
通过上面的分析,可以发现微信的小程序包是一个.wxapkg
的包,放在/data/data/com.tencent.mm/MicroMsg/账号标识/appbrand/pkg/
下,直接用下面两行命令即可拉出对应的小程序包。
cp /data/data/com.tencent.mm/MicroMsg/账号标识/appbrand/pkg/小程序包名.wxapkg /sdcard/
adb pull /sdcard/小程序包名.wxapkg ~/Desktop/
二、解析小程序包
拿到小程序包,算是比较顺利了,接下来就是对这个包进行分析了。
2.1 初步分析
一般拿到文件,直接拖到hex编辑器查看,我这里用到的是hex fiend这款软件,具体用哪个可以自己百度都大同小异。
首先一眼就可以看到,微信的小程序包应该是没有加密的,至少是某些值是没有加密的,可以直接看到一些文件的路径和名字。在对比了几个wxapkg文件之后,发现他们的前28位的开头都是OxBE ~ OxED,猜测这一段应该是有标识着文件的头信息。
大胆的猜测,微信的小程序包应该就是将图片,js,json文件全部压在一起的结构,这样的结构应该是有三部分,一段是头信息,根据头信息确定索引,最后根据索引确定文件Body体。
在大胆的猜测后,下面我们根据一些特殊文件和猜测一步一步求证找出微信小程序的大致结构。
2.2 分析格式
在反复对比几个文件后,基本确定OxBE OxED应该是类似掩码,用来标识文件的,他们之间应该是头信息
拿出一个文件数量相对比较少的wxapkg如下图:
可以发现红框内标识数字五,而刚好此文件在/WAWidget.js后就是数据段了,正好只有五个文件,因此猜测OxED后8位标识为此包含有的文件数量,在查看了几个wxapkg文件后确认。再接下来就是确认OxBE和OxED内的表示了,根据思维惯性,将他们之间切割为8位一组,发现正好分为三组,就拿上面那个只有五个文件的wxapkg来看,分别是Ox00000000, Ox0000007E, Ox000F5D8A, 0x00000000很可能是标识该文件的某些属性不好确定,因为查看了好几个文件发现值都为0。0x0000007E这个数字比较小,为126,很有可能表示索引的长度单位应该是byte,转为化十六进制表示就是252位,正好对应着文件路径后面几位,所以这个长度应该能确定是索引的长度了,那剩下的应该也就是Body的长度了。
在确定了索引的长度和Body的长度后,在特殊分析这个文件,发现就是一个一个文件的名字和后面加上两组,类似于Ox0000000A /WAPerf.js Ox0000008C Ox00001FE9,前面Ox0000000A表示长度10byte, 正好20位Ox2F574150 6572662E 6A73表示了/WAPerf.js。后面的也就很有可能是WAPerf.js这个文件的开始位置和文件长度了。这里只做一个猜测,写完程序再验证。
2.3 输出规则
根据上面的分析后列出下面这样的一个表
字段 | 属性 | 长度 | 说明 |
---|---|---|---|
头信息 | 首掩码 | 1 bytes | OxBE的头掩码 |
头信息 | 未知 | 4 bytes | 未知属性,所有拉出来的包值都为Ox00000000 |
头信息 | 索引长度 | 4 bytes | 索引的长度 |
头信息 | 文件长度 | 4 bytes | Body的长度,其实相当于文件的长度 |
头信息 | 尾掩码 | 1 bytes | OxED的尾掩码 |
头信息 | 文件数量 | 4 bytes | 本来以为这个是属于索引的,但是通过索引长度计算后,发现索引长度是从文件数量后开始计算的 |
索引 | 文件名长度 | 4 bytes | 文件名长度 |
索引 | 文件名 | 根据上一个字段确定 | |
索引 | 文件开始位置 | 4 bytes | |
索引 | 文件结束位置 | 4 bytes | |
索引 | 循环文件数量次后索引端结束 | ||
Body | 根据文件开始位置和文件结束位置算出 |
其实整个过程并没有2.2分析的那样顺利,猜测也是有很多次错误的地方,大胆猜测,小心求证,最后得出了上面的表格。
三、输出程序
核心代码如下
public WXAPPPackage parse(String path) throws IOException, InvalidWXPackageException {
FileInputStream fileInputStream = new FileInputStream(path);
fileInputStream.skip(1);
int edition = getEdition(fileInputStream);
System.out.println("Edition: " + edition);
int indexLength = getIndexLength(fileInputStream);
System.out.println("Index Length: " + indexLength);
int bodyLength = getBodyLength(fileInputStream);
System.out.println("Body Length: " + bodyLength);
fileInputStream.skip(1);
int fileCount = getFileCount(fileInputStream);
System.out.println("File Count: " + fileCount);
ArrayList<WXAPPFile> wxappFiles = new ArrayList<>();
for (int i = 0; i < fileCount; i++) {
int fileNameLength = getFileNameLength(fileInputStream);
String fileName = getFileName(fileInputStream, fileNameLength);
int fileOffset = getFileOffset(fileInputStream);
int fileSize = getFileSize(fileInputStream);
System.out.println("File Name: " + fileName + ", File offset: " + fileOffset + ", File size: " + fileSize);
WXAPPFile file = new WXAPPFile();
file.setFileNameLength(fileNameLength);
file.setFileName(fileName);
file.setFileSize(fileSize);
file.setFileStart(fileOffset);
wxappFiles.add(file);
}
WXAPPPackage wxappPackage = new WXAPPPackage();
wxappPackage.setEdition(edition);
wxappPackage.setIndexLength(indexLength);
wxappPackage.setBodyLength(bodyLength);
wxappPackage.setFileCount(fileCount);
wxappPackage.setFiles(wxappFiles);
return wxappPackage;
}
可以发现,就是根据上面总结的表格,一个一个字段的遍历,最后得到包结构体对应的对象。
具体代码可以查看
https://github.com/ZaratustraN/wxapp-parser
3.1 测试程序
可以导入到IDEA中运行测试,具体方法就不提了,我测试了10来个微信小程序包,暂时没有发现大的问题,小问题可能还有,欢迎指出提issue。
四、总结
整个过程这么顺利也出乎了我的意料之外,首先是微信居然没有对小程序包进行加密或者压缩,如果进行了加密至少不会这么顺利连dump内存和反编译的手段都没有用上。其次就是解压后发现js代码居然没有任何的保护,可以很轻易的看清楚小程序的逻辑,请求url,数据加密方式,填充方式,很容易的被攻击者模仿出请求进行攻击。
这是我第一次进行这种文件格式的破解,也有一点心得。
- 仔细观察多个文件之间的差异
- 进行大胆的猜测,并用特例法针对猜测进行求证。
- 写出程序,然后再根据程序运行的结果调整规律,最终接近最终答案
最后再贴出一次代码位置: