微信安卓客户端逆向分析

前言

微信6.5.4版APK(2017-1-20更新版本)

更多微信开发技术
https://github.com/WeMobileDev/article

工具:apktool、IDA、dex2jar、xsearch、jd-gui、Luyten、jadx、keytool、jarsigner

(2017/12/8修改) 工具:Apktool / ShakaApktool、smali、baksmali、IDA、dex2jar、xsearch、jd-gui、Luyten、jadx、keytool、jarsigner

文件结构分析

众所周知,安卓的APK文件就是MIME为ZIP的压缩包。我们可以修改文件后缀名为zip,使用WinRAR打开来查看APK的文件结构。

通过查看微信APK的文件结构,我们发现存在三个不同的dex文件,分别是classes.dex、classes2.dex、classes3.dex。dex文件是Android平台上(Dalvik虚拟机)的可执行文件,也叫作Dalvik字节码文件。通过Dalvik字节码我们不能直接查看原来的逻辑代码,但可以借助其它工具来帮助查看。

1
2
3
4
5
assets: 存放资源文件
lib:存放.so动态链接库
META-INF:存放工程的属性文件,例如签名文件
AndroidManifest.xml:Android工程的基础配置属性文件
resources.arsc:主资源文件,相当于资源索引表

因此我们逆向的重点通常放在dex文件上,这里我们需要两个工具:apktool、dex2jar。

apktool可以对整个apk文件进行反编译,命令:java -jar apktool.jar d weixin.apk

反编译后得到如下文件,其中smali、smali_classes2、smali_classes3下分别存放classes.dex、classes2.dex、classes3.dex文件反编译后的smali文件

smali文件就是Dalvik虚拟机内部执行的核心代码文件,里面存放着虚拟机指令,它有自己的一套语法。但是smali文件阅读起来并不那么直观,因此我们需要另外提取那三个dex文件,使用dex2jar将dex文件转换成jar文件,使用相应软件打开jar文件,生成更加方便阅读的java伪代码。

逆向大致思路

对于Android代码文件的整个变化过程,大致上可以看作,java转换成smali文件,所有smali文件可以打包成一个jar文件,jar文件可以转换成dex文件,dex文件就存放在apk文件里。而多个dex文件是运用了dex分包技术,不同dex的逻辑代码可以相互调用,但是不会出现重复。

我们可以结合jar和smali文件,对微信代码进行逆向分析。综合使用三个jar文件阅读器jd-gui、Luyten、jadx,使用阅读器自带的文本查找功能定位jar代码。另外综合使用XSearch对所有smali文件进行文本查找,方便定位smali代码。

逆向分析需要进行smali注入又称smali插桩,它是在保证被测程序原有逻辑完整性的基础上在程序中插入一些探针,通过探针的执行并抛出程序运行的特征数据,通过对这些数据的分析,可以获得程序的控制流和数据流信息,进而得到逻辑覆盖等动态信息,从而实现测试目的的方法。这个方法需要对原代码进行修改,需要先定位到相应的smali文件,再按照语法修改虚拟机指令。

smali文件修改后需要把smali文件转换回dex文件,命令:java -jar apktool.jar s smali

(2017/12/8修改) smali文件修改后需要把smali文件转换回dex文件,使用如下命令

1
2
ShakaApktool: java -jar apktool.jar s [目录或smali文件]
Smali: java -jar smali-[版本号].jar a [目录或smali文件]

使用WinRAR将dex文件重新放回到apk文件中,覆盖更新原有文件。修改后的apk原有签名将会被破坏,需要重新签名才能被安装到Android设备上。删除apk文件里的META-INF文件夹,删除原有签名文件。使用keytool生成自签证书,命令:keytool -genkey -alias android.keystore -keyalg RSA -validity 20000 -keystore android.keystore。然后使用jarsigner对apk进行签名,命令:jarsigner -verbose -keystore android.keystore -signedjar weixin_signed.apk weixin.apk android.keystore

apk文件签名后,需要使用adb install weixin_signed.apk命令安装修改后的apk至Android设备上,安装前需要使用adb uninstall com.tencent.mm命令卸载原有apk。

逆向目标

对于Android设备本地存储的微信聊天记录,网上有不少方法可以提取。这里尝试逆向分析微信APK,调试输出实时聊天记录。在此之前,我们知道聊天记录存在于网络通信数据包当中,经过之前对微信mmtls协议的研究,通过中间人获取网络明文传输数据的方法,在实际操作上难度比较大。因此,退而求其次,在客户端上进行逆向分析,得到明文数据包应该能够实现。

Android LogCat可以输出系统及其程序的log信息,那么微信也许会输出自己的log信息。同样,我们也可以使用smali插桩,输出自己想要的信息,比如明文通信数据。那么整个逆向分析的关键就是获取微信log信息。

逆向过程

探寻微信的看门人

Android APP开发者都知道,通常情况下,app的log输出由app的一个类负责管理,这个类通常会调用系统函数log()输出log信息,那么我们使用XSearch在smali文件中查找一下“log(”,得到许多匹配的结果。

我们发现有一个名为XLogSetup.smali的文件,看文件名的意思像是负责Log的Setup。使用jar代码查看器打开,查看一下它的代码。

这个文件确实是负责微信Log的Setup的,而微信的Log系统被命名为XLog。

Xlog.setConsoleLogOpen(isLogcatOpen.booleanValue());看上去这句像是负责LogCat输出Log的开关,那么我们先尝试修改这行对应的smali代码,设置函数的传参为True。

Xlog.appenderOpen(toolsLevel.intValue(), 1, cachePath, logPath, nameprefix);后面的这行调用了appenderOpen函数,这个函数位于com/tencent/mars/xlog/Xlog.smali文件。

Xlog.smali文件就是Xlog的主文件

System.loadLibrary("tencentxlog");调用了tencentxlog动态链接库,而上面提到的appenderOpen函数也被声明为native,显然这个函数是存在于动态链接库里的。

private static String decryptTag(String paramString)这里的decryptTag函数很显眼,因为LogCat的Tag和Text分别是项名和项值,而decrypt和encrypt也很能引起大家的注意。结合这两点,可以估计log的项名是被加密的。

com/tencent/mm/booter/c.class中可以看到,微信boot阶段会检查com.tentcent.mm.coolassistcom.tentcent.mm.debug.log.tag.skey,后者同于tag的解密。

每一种类型log都调用了logWrite2函数,logWrite2函数被声明为native,可以推测调用系统函数输出log由tencentxlog负责。

使用XSearch分别尝试搜索每一种类型log的函数名(logD、logE、logF、logI、logV、logW),发现v.smali每次都出现。

使用jar代码查看器查看一下com/tencent/mm/sdk/platformtools/v.class的代码

发现多次调用public static int getLogLevel()函数,每一种类型的log在输出之前需要检查loglevel(log级别),一共1到7级,1为最高级别。那么我们可以使用smali插桩,输出所有类型的Log。

插入log函数:

1
2
3
4
5
6
7
.method public static log(Ljava/lang/String;Ljava/lang/String;)V
.locals 2
.prologue
const v0, true
invoke-static {p0, p1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
return-void
.end method

删除所有调用getLogLevel函数及其判断结构的代码:

1
if ((nMo != null) && (nMo.getLogLevel() <= X))

在需要输出log的地方插入调用log函数的代码:

1
2
invoke-static {p0, p2}, Lcom/tencent/mm/sdk/platformtools/v;->
log(Ljava/lang/String;Ljava/lang/String;)V

安装修改后的APK,过滤tag:MicroMsg,可以在LogCat中看到微信输出的Log信息

捕获微信的通讯员

微信APK动态链接库里libwechatnetwork.solibMMProtocalJni.so引起我的注意,看名字前者像是负责微信的网络连接,后者像是负责微信的协议。

使用IDA6.6反编译这两个so文件

libwechatnetwork.so:结合mmtls协议和抓包分析,微信网络使用HTTP短连接和私有协议长连接,Signaling是负责推送的函数,网络传输任务作为Task进入链接库进行Queue。而且可以自定义设置远程服务器IP,获取当前网络连接状态。

libMMProtocalJni.so:分析函数名,本库进行aes加解密,密钥的协商管理(生成和销毁),数据包的封装和解封装。

我们把重点放在pack*和unpack,即数据包的封装和解封装。

使用XSearch查找所有“;->pack”和“;->unpack(”,对所有调用libMMProtocalJni.so的pack*和unpack函数进行smali插桩,log输出函数传参。

1
2
3
4
5
注:经过测试,应该重点插桩下面四个文件
com/tencent/mm/u/r.class
com/tencent/mm/u/t.class
com/tencent/mm/protocal/ab.class关键代码位于ab$b.smali
com/tencent/mm/model/ak.class关键代码位于ak$3.smali

传参有PByteArray、Int、Byte、String四种类型,这里log输出PByteArray、Byte和String三种类型,其中PByteArray为自定义类型,封装着一个Byte类型。

插桩方法:在smali中插入下面三个函数,其中com/tencent/mm/sdk/platformtools/bf;->bm(可以将Byte类型格式化为16进制String类型,com/tencent/mm/pointers/PByteArray;->value:[B可以读取PByteArray封装的Byte。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
.method public static log(Ljava/lang/String;)V
.locals 2
.prologue
const-string v0, "Test"
invoke-static {v0, p0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
return-void
.end method
.method public static log([B)V
.locals 2
.prologue
const-string v0, "Test"
move-object v1, p0
invoke-static {v1}, Lcom/tencent/mm/sdk/platformtools/bf;->bm([B)Ljava/lang/String;
move-result-object v1
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
return-void
.end method
.method public static log(Lcom/tencent/mm/pointers/PByteArray;)V
.locals 2
.prologue
const-string v0, "Test"
iget-object v1, p0, Lcom/tencent/mm/pointers/PByteArray;->value:[B
invoke-static {v1}, Lcom/tencent/mm/sdk/platformtools/bf;->bm([B)Ljava/lang/String;
move-result-object v1
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
return-void
.end method

然后在pack*和unpack对应位置插入调用log函数的代码:

1
2
3
invoke-static {v1}, Lcom/tencent/mm/u/r;->log(Ljava/lang/String;)V
invoke-static {v2}, Lcom/tencent/mm/u/r;->log([B)V
invoke-static {v3}, Lcom/tencent/mm/u/r;->log(Lcom/tencent/mm/pointers/PByteArray;)V

代码按需修改,最后修改所在函数的寄存器声明数.locals。

安装修改后的APK,过滤tag:Test,可以在LogCat中看到微信网络数据包的16进制信息,包含发送和接收的密文和明文数据。

在两台手机上分别登录两个微信账号,“测试”向“测试2”发送三条信息

LogCat输出了下面三段数据包的16进制数据

每一段的第一句分别是发送数据包的16进制明文数据

0801124d0a150a13777869645f71797131737566683030667132311221e590ace8afb4e4bda0e59ca8e5819ae5beaee4bfa1e98086e59091e58886e69e90180120d2a9b6c505289891d9fefdffffffff01

0801122f0a150a13777869645f717971317375666830306671323112085be5beaee7ac915d180120f6a9b6c50528b4d19c8e01

080112330a150a13777869645f717971317375666830306671323112073233333333333318012082aab6c50528f1e2e5f6ffffffffff01

“wxid_”开头的是微信id,0000001ch是聊天发送数据的长度,0000001dh是聊天发送数据的开始位置。

提取出聊天发送数据的16进制,分别进行分析

e590ace8afb4e4bda0e59ca8e5819ae5beaee4bfa1e98086e59091e58886e69e90

5be5beaee7ac915d

32333333333333

经过尝试,确定是UTF8编码,只需在开头插入UTF-8编码标志EFBBBF即可。

efbbbfe590ace8afb4e4bda0e59ca8e5819ae5beaee4bfa1e98086e59091e58886e69e90

efbbbf5be5beaee7ac915d

efbbbf32333333333333

结语

目前所做的逆向分析只是冰山一角,微信客户端还是有许多地方值得分析和借鉴的。

逆向分析是一个学习的过程,同时也是一个考验安全性的过程。