逆向某聊天app实现60行代码自定义发送消息图片

前段时间休假了半个月,在家无聊,也没有出去浪,像我们这种闲男,不陪lp和gf的话,肯定宅在这里。无聊,想找点事玩玩。在平时很少接触protubuf这个玩具。那天在群里闲转,就在群里问:有没有哪些app用了protobuf的,老友说QQ,微信,抖音。我这样的小白哪能去玩这么高大上的,有个群友说有个难度不太的app,然后就开始了一段梦幻的休假旅行。

因为涉及到有敏感信息,app名就叫马斯克吧。

抓包分析

登录抓包

登录返回界面如下: 896615_U5UK558YU2BM4ZF 具体抓包数据如下: 896615_KYXSCBEYFUF9H89 896615_K9WX4WEZT6EW2DV

登录分析

逆向app把这个m_e和m_d搞定。思路很多种.第一种,jadx中查看app的dex文件,搜索关键字"m_e".定位到如下内容。熟悉的aes和rsa加密。 896615_JAY7VU226HEEF9G 我使用最快的Frida hook它。用上库存里的frIDA脚本,得到如下动态信息,加密信息出来了。

896615_FJ96DV5XBNACZ7D

一般而言,我会先进行动态分析,尤其是加密类的,一般frida脚本都能马上显示出来。如果加密关键内容比如key没有正常得出,肯定再需要进一步so层的分析,那接下来的步骤就是静态分析,静态分析的步骤是先找出关键搜索内容,定位到关键位置,进行JAVA层的分析,后期的分析要么就进入 到so层,要么就会有其它第三方的框架,引入js或者lua。

这个请求提交的原始数据,我们从图中的before_doFinal可以直接看到。哪怕是加密的key和iv也出现了。

手机号和密码是我随便输入的,pwd字段明显就是md5,直接用了明文作了一次md5运算。

返回结果如下:

896615_86W77W7ZZ2YYVEF

再用python转换一下: 896615_P4VNJ2UHMY4F7ZV

分析后登录信息加密信息如下:

请求头中m_d是AES加密的,m_e是RSA加密的,服务器上有rsa的私钥,直接解密出来一个key,用这个key去参与m_d的AES解密,然后服务器就得到了m_d的原始内容了。响应中也类似一样,只是现在app做了服务器上一样的解密动作。

注册好了帐号正式登录,返回来了个人的用户信息,随后还会有两个一样加密算法的请求,获取群列表和好友列表。然后用bp开始抓不到包了。用C++harles出现connect的问题。

聊天内容抓包分析

聊天内容抓包

这个时候抓包可以用wireshark,或者tcpdump之类的工具,但这类工具抓取到的数据太杂,不利于这次的抓包分析工作,所以换另外一个自制的工具,用python写的socks5代理端,专用来抓tcp包的。

设置好了环境后,抓包。 896615_5VHQMJMPDDDRZPA

抓到了数据。这里要说明一下。抓取到的数据输出都是byte字节的。这样做很方便转换成其它需要的数据。比如抓取到的内容如下

b'\x1a\x12\x0fHeartBeatAckMsg\x1a\x07\x12\x053.

用python输出这个bytes的16进制数据

896615_73JNDC55FTNZ54K

16进制的数据,如果想转换成二进制数据,就很简单了。

聊天内容分析

因为protoc解析的是二进制数据。所以通过16进制转成二进制很方便。

打开一个hexFiend,然后把转换后的16进制数据复制到左侧栏内容: 896615_Z6ME8ZRAHFBNKWG 保存。

用protoc来解析一下看看。操作了二个不包的数据包,解析结果如下: 896615_37T5U5PWZP4SNEC

896615_CCQJ285Q4FX2PV7

有一个“好像”能解析有一个直接解析失败。什么原因呢?

马斯克是360加固的,脱壳app,然后把dex放jadx里倒腾倒腾。经过静态再经过用objection hook protubuf类,确定了是用了protobuf,而且通过搜索关键词"HeartBeatAckMsg"直接定位到了protobuf的关键类,proto文件要还原的话,有了这个类的信息就够了。但是为什么又解析不了呢??

再从jadx里面结合objection去分析,发现还用了一个框架netty,搜索了一下netty,并对它有了初步的认识,深入了解后,原来是他配合protobuf一起做到了高并发,高效率收发数据。protobuf用了netty的特点是在protobuf的数据前面多加了二个字节,表示protobuf数据的大小。

知道了原因,再尝试解析成功。 896615_84NACGNWDN8FQ6K

896615_2Z88P9BPZGM4E4V

奇怪的是,从服务器返回来的数据在解析的时候有时需要去掉一个字节,有时2个字节。比如上面的HeartBeatAckMsg

数据能够通过protoc正常解析了,开始分析jadx中的proto相关类的生成。

896615_QQEXDWYVE49TGW7

从上面直接可以分析出来proto文件里面的格式,而且跟protoc解析出来的字段序号都是一样的。其它收发消息的proto文件按同样方式即可得到。只例举一个书写好的proto文件

syntax = "proto3";
message HeartBeatMsg{
    stRing token = 1;
    string proto_class_name = 2;
    HeartBeatMsgContent body = 3;
}
message HeartBeatMsgContent{
    string from_id = 1;           
    int32 device_type = 2;
    int32 device_slave_type = 3;
    int64 last_msg_sequence_id = 4;
    int64 last_msg_receive_time = 5;
    string version = 6;
    int64 interface_up_time = 7;
    string apiurl = 8;
    string imurl = 9;
}

有几个地方需要注意:

1 proto格式文件的版本。2 string类型的字段定义一般不需要处理,但int类型有很多种,还需要根据实际情况解析正常的数据后得到正确的结果。因为这个在jadx中分析不出来。

proto文件写好了,转换成python类

protoc --python_out=. HeartBeatMsg.proto

会在当前目录下生成一个HeartBeatMsg_pb2.py文件

然后在python中引入,解析数据

import HeartBeatMsg_pb2 #引入类
hbm = HeartBeatMsg_pb2.HeartBeatMsg()   #实例化
hbm.ParseFromString(buffer[2:])     #解析,buffer是byteArray类型,从抓到的数据中直接赋值给它,所以需要去掉2个字节
print(hbm)

得到如下结果

896615_AGUHK9JTNRZACJK

写其它proto文件和解析案例略过,有坑的地方我已经在上面说过。一个案例够其他同学研究学习了。 896615_XPDH49Q53NRGHVB

自定义消息内容发送

有了上面的分析和前期的工作,可以开始写发送消息的代码了。

首先是流程:通过http协议登录,获取群id或者好友id,然后再通过tcp协议收发消息。

为了省事,直接取用bp中抓到的token

import socket,uuid,time,google.protobuf
import AuthMsg_pb2,ChatMsg_pb2
skt = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
address = ("test.host.com",5000)
skt.connect(address)
token = {'isAuth'1'nickName''测试23242''headUrl''https://down.host.com/head/userhead.png''defaultId''801adfafcc0d1afa0d''accessToken''tk:801adfafcc0d1afa0d:adfasdfaec0faf6a9''token''1010e3d11973980c36ec8'}   #抓包获取到的token
#会话认验
def auth():
    am = AuthMsg_pb2.AuthMsg()
    am.token = token['accessToken']
    am.proto_class_name = "AuthMsg"
    am.body.from_id = token['defaultId']
    am.body.device_id = "861234123412"   #设备号
    am.body.device_type = 1;
    bnry = am.SerializeToString()   #protobuf序列化
    skt.send(addNettyHeader(bnry)); #通过skt发送内容
#聊天
def chat(news):
    msgstr = ChatMsg_pb2.ChatMsg()
    msgstr.token = userinfo['accessToken']
    msgstr.proto_class_name = "ChatMsg"
    msgstr.body.msg_id = "100-"+str(uuid.uuid1()).replace("-","")   #消息id号
    msgstr.body.mAIn_type = 6           #固定
    if(news['type']==2):    #如果发送消息是图片
        msgstr.body.slave_type = 3         #1 纯文本,表情,3 图片 2 语音
        msgstr.body.content = news['txt']
    elif(news['type']==1):
        msgstr.body.slave_type = 1
        msgstr.body.content = news['txt']
    msgstr.body.from_id = userinfo['defaultId']
    msgstr.body.from_name = userinfo['nickName']
    msgstr.body.from_head_url = userinfo['headUrl']
    msgstr.body.dest_id = news['to']  #接收消息的好友
    msgstr.body.send_time = int(time.time()*1000)
    msgstr.body.device_type = 1
    msgstr.body.device_slave_type = 11
    bnry = msgstr.SerializeToString()
    skt.send(addNettyHeader(bnry))
# 增加Netty头
def addNettyHeader(msg):
    value = len(msg)
    bits = value & 0x7f
    value >>= 7
    header_array = []
    while value:
        header_array.append(0x80 | bits)
        bits = value & 0x7f
        value >>= 7
    header_array.append(bits)
    return bytes(header_array)+msg
auth()
msg1 = {"type":1,"txt":"今天天气真好,有约吗?","to":123412}
msg2 = {"type":3,"txt":"https://img.host.com/test.jpg","to":123412}
chat(msg1)
chat(msg2)

 

发送成功了 896615_R46UQTKTEM8J7ZX

后记总结

通过这次的学习,对protobuf有了更深入的了解,虽然最终代码量少,但是分析工作做了很多,花了很长的一段时间。有个小彩蛋是马斯克上传图片是采用阿里的oss的。获取到临时的一个凭证后,通过阿里自带的oss-brower登录后,可以查看主帐号的所有Bucket 896615_WSVAVZEAWZEZ9HQ

进入bucket里面以后,测试了上传,复制,下载都可以。里面的聊天图片,视频,语音内容都能正常查看。

本次学习记录内容仅限于研究学习交流使用,有兴趣尝试玩一玩的同学,请勿做其它非法的延伸功能扩展,否则后果自负,与本文发布者、平台、参与交流同学无关。 896615_5TR3UUEDRGXFU3Z

标签