微信安卓协议分析笔记

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_21051503/article/details/79746742

一、查资料

        网上没找到SDK可以分析,关于微信安卓协议的文章也比较少,比较有用的是<微信交互协议和加密模式研究>,这篇论文里介绍了微信使用RSA2048与AES-CBC-128结合的加密算法以及使用protobuf编码格式传输数据;<微信协议简单调研笔记>帖子里提到微信使用长短链接结合的网络通讯方式以及基于sync key的消息同步机制;<基于TLS1.3的微信安全通信协议mmtls介绍>详细讲解了类似于https中TLS作用的mmtls协议;<微信安卓客户端逆向分析>通过举例拦截聊天记录详细演示了安卓微信客户端分析的过程.

二、准备工作

        工具除了安卓反编译常用打包解包软件以及查看java代码的软件,还可以用XSearch方便在大量smali汇编文件中快速搜索字符串,Wireshark用于抓包,Android Studio动态调试smali汇编代码,IDA静态分析so文件,WinHex用于编辑二进制文件.
       微信底层使用自家开源的跨平台通讯库mars,该库包含xlog模块,详尽记录了微信几乎所有主要函数调用流程,开启log可以极大地方便动态分析,研究mars库也有助于理解微信与服务器交互流程.
       由于mmtls无法中间人攻击,所以抓包无法获取短链接HTTP通讯的明文数据;考虑到兼容性微信允许在不使用mmtls的情况下通讯,所以关闭mmtls有助于协议分析.
       由于长链接含有包头不利于分析,考虑到稳定性微信允许在不使用长链接的情况下通讯,所以禁用长链接有利于协议分析.

三、二次打包客户端

        微信允许非官方签名的客户端运行,但是会校验签名并上报异常数据.最近因使用非官方客户端封号的风控策略越来越严格,运行二次打包客户端时要做好被封号准备,或者使用XP插件Hook代码,或者patch掉获取软件签名的代码或者禁掉上传异常数据的封包,总之这不是本文的重点.如果反编译工具直接打包失败,可以只重新编译dex文件,然后替换到原来的apk中.
       从mars库的Xlog文档中可以知道setConsoleLogOpen接口负责开启控制台log,在反汇编代码中搜索这句代码的调用"->setConsoleLogOpen",找到XLogSetup.smali文件直接修改Lcom/tencent/mars/xlog/Xlog;->setConsoleLogOpen的调用参数即开启log.搜索getLogLevel最终可以在Lcom/tencent/mm/sdk/platformtools/下面找到输出Log等级的定义,从6改为0即可打印所有等级的log.logcat里可以通过"MicroMsg""mars"两个tag来分别过滤java层和native层的log.
       运行开启Log的客户端,过滤tag为"mmtls"的日志,可以找到"Java_com_tencent_mars_mm_MMLogic_setMmtlsCtrlInfo"函数打印的log:
1
I/mars::mmext(12496): [com_tencent_mars_mm_MMLogic_Java2C.cc, Java_com_tencent_mars_mm_MMLogic_setMmtlsCtrlInfo, 299]:j_use_mmtls=1
       从函数名可以猜出这是java调用jni控制开启mmtls的接口,在反汇编代码中搜索";->setMmtlsCtrlInfo"找到调用的代码,修改参数为0关闭mmtls.
       从mars库中可以看出,长链接使用mars/stn/src/longlink.cc文件的LongLink::__RunConnect函数连接服务器,解压apk搜索字符串"longlink.cc"可知mars库编译出的文件为libwechatnetwork.so,在二进制文件中结合IDA patch掉该函数返回值可以禁用长链接;从mars文档也可以看出上层需要调用mars::stn::MakesureLonglinkConnected();启用长链接,在smali代码中搜索调用";->makesureLongLinkConnected"的代码注释掉也可关闭长链接.

四、抓包

        使用安卓模拟器或用手机连接电脑开启的wifi,运行二次打包处理的客户端,使用Wireshark工具即可抓到微信短链接的HTTP协议封包了.比如登录包可以抓到使用POST方法向/cgi-bin/micromsg-bin/manualauth 发送了1183字节长度的数据,数据是被加密的:


       <微信安卓客户端逆向分析>文章中提到数据是被libMMProtocalJni.so这个库加密的,使用IDA查看该库的jni接口:

可以看到"Java_com_tencent_mm_protocal_MMProtocalJni_pack"很可能是封包加密的接口,在smali反汇编代码中搜索调用该接口的代码"MMProtocalJni;->pack"如下:

根据log tag"RemoteReq"以及"reqToBuf using protobuf ok"可以猜测这里就是protobuf明文加密封包的地方.接下来可以使用AS在smali这句代码上下断点观察参数与返回值,如果配置调试环境有困难,可以在smali代码中插log将参数与返回值打印并保存下来,方便后面分析.其中第二个参数类型是PByteArray,这是自定义的类型,需要打印的字节数组为该对象的value成员变量;使用Log.d在控制台打印时默认有4000字节的长度限制,超过的部分无法输出,插入log时注意将长log拆分多行输出.
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
30
31
#参数1:logtag,参数2:需要打印的bytes
.method public static logX(Ljava/lang/String;[B)V
    .locals 8
    .prologue
    if-eqz p1, :cond_1
    #如果参数p1是PByteArray,要取其成员变量value
    #iget-object v1, p1, Lcom/tencent/mm/pointers/PByteArray;->value:[B
    move-object v1, p1
    #使用平台自带的将bytes转成string的函数,每个版本混淆函数名略有不同
    invoke-static {v1}, Lcom/tencent/mm/sdk/platformtools/bh;->bp([B)Ljava/lang/String;
    move-result-object v1
    invoke-virtual {v1}, Ljava/lang/String;->length()I
    move-result v2
    const v4, 0x0
    #超过4000字节长度的log,分行输出
    const v3, 0xfa0
    :goto_0
    if-ge v4, v2, :cond_1
    move v5, v4
    add-int/2addr v5, v3
    if-le v5, v2, :cond_0
    move v5, v2
    :cond_0
    invoke-virtual {v1, v4, v5}, Ljava/lang/String;->substring(II)Ljava/lang/String;
    move-result-object v6
    invoke-static {p0, v6}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
    move v4, v5
    goto :goto_0
    :cond_1
    return-void
.end method
准备好抓包环境并开启log,向任意好友发送一条消息,对比Wireshark抓到的发送的二进制数据与log中记录的该函数执行后第二个参数pByteArray.value的值发现一模一样,而该函数的入参可以看到有发送消息的明文数据,说明这就是微信的组包函数,实现 Java_com_tencent_mm_protocal_MMProtocalJni_pack 函数是分析微信协议的核心.
        用同样的方法分析可知"Java_com_tencent_mm_protocal_MMProtocalJni_unpack"是解包函数,函数执行后第1个参数pByteArray中保存解密后的protobuf数据.
        登录包由于涉及密钥协商,使用"Java_com_tencent_mm_protocal_MMProtocalJni_packHybrid"函数组包.
        把抓到的完整HTTP数据保存下来,自己写代码模拟向相同地址端口POST相同登录包数据,发现每次都返回相同的数据,说明登录包可以被重放攻击.

五、协议分析

         Http post的数据如下:

        重复登录几次对比封包数据,发现封包数据的前几字节基本是固定的,而且含有大量的连续"00"数据。由于加密后的数据通常无规律,所以一般情况下这些"00"附近的数据都是未加密的数据,而在封包前面出现的未加密数据一般是包含封包重要信息的封包头.
        首先查看这些二进制数据对应的ASCII数据,并未发现有意义的字符串;接下来只能靠经验"猜"每个字节代表的意义,然后尝试修改该字节重发封包验证.由于有详尽的log,这里可以直接根据log中的关键数据直接与二进制数据对比分析其意义,这将大大提高分析的成功率.比如,登录包组包log片段如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
D/MicroMsg.Jni( 2889): [common_function.h, CLogScope, 78]:--> Enter protocal_packHybrid
V/MicroMsg.Jni( 2889): [MMProtocalJniImpl.cpp, protocal_packHybrid, 170]:cookie length=15
V/MicroMsg.Jni( 2889): [MMProtocalJniImpl.cpp, protocal_packHybrid, 172]:cookie attached
V/MicroMsg.Jni( 2889): [MMProtocalJniImpl.cpp, protocal_packHybrid, 179]:rsa length=174
V/MicroMsg.Jni( 2889): [MMProtocalJniImpl.cpp, protocal_packHybrid, 181]:rsa attached
V/MicroMsg.Jni( 2889): [MMProtocalJniImpl.cpp, protocal_packHybrid, 188]:aes length=1610
V/MicroMsg.Jni( 2889): [MMProtocalJniImpl.cpp, protocal_packHybrid, 190]:aes attached
V/MicroMsg.Jni( 2889): [MMProtocalJniImpl.cpp, protocal_packHybrid, 197]:keye length=6
V/MicroMsg.Jni( 2889): [MMProtocalJniImpl.cpp, protocal_packHybrid, 199]:keye attached
V/MicroMsg.Jni( 2889): [MMProtocalJniImpl.cpp, protocal_packHybrid, 206]:keyn length=512
V/MicroMsg.Jni( 2889): [MMProtocalJniImpl.cpp, protocal_packHybrid, 208]:keyn attached
V/MicroMsg.Jni( 2889): [MMProtocalJniImpl.cpp, protocal_packHybrid, 215]:pkey length=16
V/MicroMsg.Jni( 2889): [MMProtocalJniImpl.cpp, protocal_packHybrid, 217]:pkey attached
D/TLV_LOG ( 2889): [common_function.h, CLogScope, 78]:--> Enter EncodeHybirdEncryptPack
V/TLV_LOG ( 2889): [mmpack.cpp, bool EncodeHybirdEncryptPack(Comm::SKBuffer&, Comm::SKBuffer&, unsigned int, unsigned char*, emMMFunc, unsigned int, Comm::SKBu, 178]:writing head, g_clientVer: 637864504, flag:7
D/TLV_LOG ( 2889): [mmpack.cpp, bool EncodeHybirdEncryptPack(Comm::SKBuffer&, Comm::SKBuffer&, unsigned int, unsigned char*, emMMFunc, unsigned int, Comm::SKBu, 207]:EncodeHybirdEncryptPack fgflag:1, cver:1001, calgo:2, clen:1180, cdlen:1180, ealog:7
I/TLV_LOG ( 2889): [skbuffer.cpp, EnsureExpandSize, 58]:EnsureExpandSize 34 to 1214 increase 1180
V/TLV_LOG ( 2889): [mmpack.cpp, bool EncodeHybirdEncryptPack(Comm::SKBuffer&, Comm::SKBuffer&, unsigned int, unsigned char*, emMMFunc, unsigned int, Comm::SKBu, 219]:packing done, uin=0, func=701, ret=0
D/TLV_LOG ( 2889): [common_function.h, ~CLogScope, 83]:<-- Exit EncodeHybirdEncryptPack
D/MicroMsg.Jni( 2889): [common_function.h, ~CLogScope, 83]:<-- Exit protocal_packHybrid
        log中"g_clientVer: 637864504"这句是打印客户端版本号 ,637864504二进制为26050A38,可以看到这与二进制封包数据中第3到第6字节一致,结合该版本的发布版本号"6510"更加确定封包头部的这4字节就是代表当前客户端的大、小版本号,这也印证了封包前面几个字节的数据是未加密的猜想.
        对于封包第一字节,通常固定是"bf",只有少部分不以"bf"开头(如截图中的登录包),log中也没找到该字节的含义,这种情况就只能反复发包测试验证是否必须以"bf"开头的封包才合法。目前测试下来是否包含该字节不影响通信.
        封包头剩余数据无法直接猜出意义,只能静态分析libMMProtocalJni.so中组包函数代码,必要时可以使用IDA下断点调试参数及栈,如果搭建动态调试so环境有困难,可以尝试调试PC版本微信的WeChatWin.dll,最近版本该dll都没有加壳.mars是跨平台的组件,PC版和android版底层通信代码差不多.
        从上面的log片段中找到关键词"writing head"定位到组包函数函数:EncodeHybirdEncryptPack



最终组包函数片段:
从组包函数可以看出有些字节的数据是由2个4bits的数据组合起来的,有些数据是转网络字节序后再经过处理的,处理的函数如图:

        这是整数压缩算法.
        尝试将包头中的数据使用整数压缩算法还原后,"BD 05"对应整数701,对应log中的
1
V/TLV_LOG ( 2889): [mmpack.cpp, bool EncodeHybirdEncryptPack(Comm::SKBuffer&, Comm::SKBuffer&, unsigned int, unsigned char*, emMMFunc, unsigned int, Comm::SKBu, 219]:packing done, uin=0, func=701, ret=0
        "8C 09"对应整数1164,对应log中的clen(log是之前抓的,与截图中的封包不对应,实际log中应是clen: 1164 , cdlen: 1164 )
1
D/TLV_LOG ( 2889): [mmpack.cpp, bool EncodeHybirdEncryptPack(Comm::SKBuffer&, Comm::SKBuffer&, unsigned int, unsigned char*, emMMFunc, unsigned int, Comm::SKBu, 207]:EncodeHybirdEncryptPack fgflag:1, cver:1001, calgo:2, clen:1180, cdlen:1180, ealog:7
        对比log可以猜出包头大部分字节的含义,第一个字节对应的代码:

        前6bits代表包头长度,后2bits表示是否使用压缩算法,在log中搜索"calgo",

        可以看出clen大于cdlen时calgo为1,即使用压缩算法; clen等于cdlen时calgo为2,即不使用压缩算法.
        从文档中前人已经分析出微信登录使用rsa算法协商秘钥后续使用aes加密通信数据,log中也明确记录了加密算法和密钥,如图:
1
2
3
4
5
6
7
8
9
10
V/MicroMsg.Jni( 2889): [MMProtocalJniImpl.cpp, protocal_packHybrid, 179]:rsa length=174
V/MicroMsg.Jni( 2889): [MMProtocalJniImpl.cpp, protocal_packHybrid, 181]:rsa attached
V/MicroMsg.Jni( 2889): [MMProtocalJniImpl.cpp, protocal_packHybrid, 188]:aes length=1610
V/MicroMsg.Jni( 2889): [MMProtocalJniImpl.cpp, protocal_packHybrid, 190]:aes attached
V/MicroMsg.Jni( 2889): [MMProtocalJniImpl.cpp, protocal_packHybrid, 197]:keye length=6
V/MicroMsg.Jni( 2889): [MMProtocalJniImpl.cpp, protocal_packHybrid, 199]:keye attached
V/MicroMsg.Jni( 2889): [MMProtocalJniImpl.cpp, protocal_packHybrid, 206]:keyn length=512
V/MicroMsg.Jni( 2889): [MMProtocalJniImpl.cpp, protocal_packHybrid, 208]:keyn attached
V/MicroMsg.Jni( 2889): [MMProtocalJniImpl.cpp, protocal_packHybrid, 215]:pkey length=16
V/MicroMsg.Jni( 2889): [MMProtocalJniImpl.cpp, protocal_packHybrid, 217]:pkey attached
        rsa的公钥n,e是内置在客户端里的,分为157和158两个版本:

         aes的密钥在log中只能看到一个key,没有iv.在腾讯开源项目mars中的aes_crypt.c文件中的aes_cbc_encrypt加密函数中,iv和key是相同的:

        在libMMProtocalJni.so也可以找到对应的代码:

        将明文protobuf数据和加密key从log中拷贝出来用aes在线加解密工具验证,加解密结果与log中一致,表明微信使用aes-cbc-128加密算法通讯.
        在log中搜索aes加密key的二进制数据,寻找该key第一次出现的位置,附近很可能有密钥计算的相关信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
I/MicroMsg.AutoAuth( 2182): [, , 2182]:summerauth end type: 701, ret:[0,0][null]
D/[LogI]( 2182): summerauth end type: 701, ret:[0,0][null]
D/[LogD]( 2084): summerauth decodeAndRetriveAccInfo type:701
D/[LogI]( 2084): summerauth decodeAndRetriveAccInfo type:701, hashcode:196488030, ret:0, stack[[sdk.platformtools.v:i(262)][s.ap:b(661)][w.s:BQ(186)][network.q$a:onTransact(156)]]
D/[LogI]( 2084): decodeAndRetriveAccInfo authResultFlag:4 UpdateFlag:1 
D/[LogD]( 2084): summerauth svr ecdh key len:57, nid:713 sessionKey len:32, sessionKey: 9d d4 f8 c6 65 fe 03 ab 1b f5 03 ca 06 4d eb 0a a9 b0 05 38 3f 0f d4 79 c7 1a 2e 63 72 65 77 1e
D/[LogD]( 2084): summerauth svrPubKey len:57 value: 04 6d 02 6d e2 94 22 48 00 fb c3 ec 1c 63 45 bb 84 c8 97 3f 8e 35 4b ed ce a3 d7 68 71 51 99 35 aa 2b 03 9d 1b c4 e2 f0 24 f5 fc 99 39 0e 8f ff a9 f9 6a 34 10 98 8d bb 11 prikey len:328, values: 30 82 01 44 02 01 01 04 1c 2e b1 e1 70 b0 c0 9f 3a a8 f3 8d 15 1e 50 30 80 b1 ed 93 be 03 e9 50 c9 f8 8a 80 7d a0 81 e2 30 81 df 02 01 01 30 28 06 07 2a 86 48 ce 3d 01 01 02 1d 00 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 00 00 00 00 00 00 00 00 00 00 00 01 30 53 04 1c ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff fe ff ff ff ff ff ff ff ff ff ff ff fe 04 1c b4 05 0a 85 0c 04 b3 ab f5 41 32 56 50 44 b0 b7 d7 bf d8 ba 27 0b 39 43 23 55 ff b4 03 15 00 bd 71 34 47 99 d5 c7 fc dc 45 b5 9f a3 b9 ab 8f 6a 94 8b c5 04 39 04 b7 0e 0c bd 6b b4 bf 7f 32 13 90 b9 4a 03 c1 d3 56 c2 11 22 34 32 80 d6 11 5c 1d 21 bd 37 63 88 b5 f7 23 fb 4c 22 df e6 cd 43 75 a0 5a 07 47 64 44 d5 81 99 85 00 7e 34 02 1d 00 ff ff ff ff ff ff ff ff ff ff ff ff ff ff 16 a2 e0 b8 f0 3e 13 dd 29 45 5c 5c 2a 3d 02 01 01 a1 3c 03 3a 00 04 be b6 80 3e 74 d5 02 35 32 1b e4 ec 9e fc 71 ad 96 ff 2f 00 cd 21 e1 45 84 70 da 6e 9d 98 35 ad 2d 57 5a 49 08 70 8c 5f c0 01 f2 95 67 7a 07 a0 c2 fe 16 42 2d e3 d0 7c
W/mars::comm( 2182): [platform_comm.cc, getCurSIMInfo, 303]:getCurSIMInfo error return null
D/[LogD]( 2182): receive kv logid:11108, isImportant: false,isReportNow: false
D/[LogD]( 2182): receive group id size:3, isImportant:false
D/[LogD]( 2182): receive kv logid:11110, isImportant: false,isReportNow: false
D/[LogI]( 2084): summerauth ComputerKeyWithAllStr ret:0, agreedECDHKey len: 16, values: 8c 1c 1a d8 a2 0b c4 41 0f a1 27 74 97 2c c9 b9
D/[LogD]( 2084): summerauth aesDecrypt sessionKey len:32, value: 9d d4 f8 c6 65 fe 03 ab 1b f5 03 ca 06 4d eb 0a a9 b0 05 38 3f 0f d4 79 c7 1a 2e 63 72 65 77 1e, session len:16, value: 55 6b 7a 52 79 24 55 57 79 56 4b 4c 4f 6d 45 6e
D/[LogD]( 2084): summerauth decode session key succ session: 55 6b 7a 52 79 24 55 57 79 56 4b 4c 4f 6d 45 6e
        最后一行log中的 session就是除登录包外的aes解密key.
        根据关键词"summerauth decode session key succ session"定位代码片段:

        从java代码中可以看出,最终打印出的session是用MMProtocalJni.aesDecrypt做aes解密获得的,解密key是MMProtocalJni.computerKeyWithAllStr返回的,结合log可以看出 computerKeyWithAllStr的参数有svrPubKey,prikey.从函数返回值agreedECDHKey的命名可以看出这是ECDH密钥交换算法,在log中搜索 svrPubKey和 prikey二进制数据第一次出现的位置, svrPubKey在登陆封包返回的解密数据中, prikey出现在:
1
D/[chao-LogD]( 2084): summerecdh nid:713 ret:0, pub len: 57, pri len:328, pub: 04 be b6 80 3e 74 d5 02 35 32 1b e4 ec 9e fc 71 ad 96 ff 2f 00 cd 21 e1 45 84 70 da 6e 9d 98 35 ad 2d 57 5a 49 08 70 8c 5f c0 01 f2 95 67 7a 07 a0 c2 fe 16 42 2d e3 d0 7c, pri: 30 82 01 44 02 01 01 04 1c 2e b1 e1 70 b0 c0 9f 3a a8 f3 8d 15 1e 50 30 80 b1 ed 93 be 03 e9 50 c9 f8 8a 80 7d a0 81 e2 30 81 df 02 01 01 30 28 06 07 2a 86 48 ce 3d 01 01 02 1d 00 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 00 00 00 00 00 00 00 00 00 00 00 01 30 53 04 1c ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff fe ff ff ff ff ff ff ff ff ff ff ff fe 04 1c b4 05 0a 85 0c 04 b3 ab f5 41 32 56 50 44 b0 b7 d7 bf d8 ba 27 0b 39 43 23 55 ff b4 03 15 00 bd 71 34 47 99 d5 c7 fc dc 45 b5 9f a3 b9 ab 8f 6a 94 8b c5 04 39 04 b7 0e 0c bd 6b b4 bf 7f 32 13 90 b9 4a 03 c1 d3 56 c2 11 22 34 32 80 d6 11 5c 1d 21 bd 37 63 88 b5 f7 23 fb 4c 22 df e6 cd 43 75 a0 5a 07 47 64 44 d5 81 99 85 00 7e 34 02 1d 00 ff ff ff ff ff ff ff ff ff ff ff ff ff ff 16 a2 e0 b8 f0 3e 13 dd 29 45 5c 5c 2a 3d 02 01 01 a1 3c 03 3a 00 04 be b6 80 3e 74 d5 02 35 32 1b e4 ec 9e fc 71 ad 96 ff 2f 00 cd 21 e1 45 84 70 da 6e 9d 98 35 ad 2d 57 5a 49 08 70 8c 5f c0 01 f2 95 67 7a 07 a0 c2 fe 16 42 2d e3 d0 7c
        根据log tag "summerecdh"定位代码:

        从java代码中可以看出 prikey是 libMMProtocalJni.so 库的jni函数generateECKey返回的.
        学习mars库时恰巧发现ECDH相关的这几个jni函数都是开源的:
        生成ECC密钥对的函数在ecdh_util.cpp中:

        计算共享密钥的函数在ecdh_crypt.c中:

        ECC参数nid在上面log中可以看到为713(即secp224r1).
        上面得到的session只能用于后续通信做aes加解密时的key,由于登录包是第一个封包,此时还未得到session,因此解密登录包的密钥需要单独计算.
        在log中搜索解密登录包的key二进制数据第一次出现的位置,发现在packHybrid的 protobuf 参数中,说明解密登录包的key是发包前本地生成发到服务器的.
        整个登录流程客户端共有2个随机参数:解密登录包的key和ecc密钥对,其余参数账号密码、硬件信息等是固定不变的。因此若每次登陆生成相同的解密key和ecc密钥对,则理论上登录请求数据可以完全相同(假设RSA加密后也生成相同的数据),将此登录包的二进制数据保存并记录与之对应的key和ecc prikey,直接发送即可 免去组包 完成登录;若在代码中动态替换掉进程的key和ecc prikey以及guid,则有可能实现在非授权设备上登录账号.
        通信协议草图如下:


(未完待续......)
demo

展开阅读全文

没有更多推荐了,返回首页