逆向某聊天app实现60行代码自定义发送消息图片
前段时间休假了半个月,在家无聊,也没有出去浪,像我们这种闲男,不陪lp和gf的话,肯定宅在这里。无聊,想找点事玩玩。在平时很少接触protubuf这个玩具。那天在群里闲转,就在群里问:有没有哪些app用了protobuf的,老友说QQ,微信,抖音。我这样的小白哪能去玩这么高大上的,有个群友说有个难度不太的app,然后就开始了一段梦幻的休假旅行。
因为涉及到有敏感信息,app名就叫马斯克吧。
抓包分析
登录抓包
登录返回界面如下: 具体抓包数据如下:
登录分析
逆向app把这个m_e和m_d搞定。思路很多种.第一种,jadx中查看app的dex文件,搜索关键字”m_e”.定位到如下内容。熟悉的aes和rsa加密。 我使用最快的frida hook它。用上库存里的frida脚本,得到如下动态信息,加密信息出来了。
一般而言,我会先进行动态分析,尤其是加密类的,一般frida脚本都能马上显示出来。如果加密关键内容比如key没有正常得出,肯定再需要进一步so层的分析,那接下来的步骤就是静态分析,静态分析的步骤是先找出关键搜索内容,定位到关键位置,进行java层的分析,后期的分析要么就进入 到so层,要么就会有其它第三方的框架,引入js或者lua。
这个请求提交的原始数据,我们从图中的before_doFinal可以直接看到。哪怕是加密的key和iv也出现了。
手机号和密码是我随便输入的,pwd字段明显就是md5,直接用了明文作了一次md5运算。
返回结果如下:
再用python转换一下:
分析后登录信息加密信息如下:
请求头中m_d是AES加密的,m_e是RSA加密的,服务器上有rsa的私钥,直接解密出来一个key,用这个key去参与m_d的AES解密,然后服务器就得到了m_d的原始内容了。响应中也类似一样,只是现在app做了服务器上一样的解密动作。
注册好了帐号正式登录,返回来了个人的用户信息,随后还会有两个一样加密算法的请求,获取群列表和好友列表。然后用bp开始抓不到包了。用charles出现connect的问题。
聊天内容抓包分析
聊天内容抓包
这个时候抓包可以用wireshark,或者tcpdump之类的工具,但这类工具抓取到的数据太杂,不利于这次的抓包分析工作,所以换另外一个自制的工具,用python写的socks5代理端,专用来抓tcp包的。
设置好了环境后,抓包。
抓到了数据。这里要说明一下。抓取到的数据输出都是byte字节的。这样做很方便转换成其它需要的数据。比如抓取到的内容如下
b’\x1a\x12\x0fHeartBeatAckMsg\x1a\x07\x12\x053.
用python输出这个bytes的16进制数据
16进制的数据,如果想转换成二进制数据,就很简单了。
聊天内容分析
因为protoc解析的是二进制数据。所以通过16进制转成二进制很方便。
打开一个hexFiend,然后把转换后的16进制数据复制到左侧栏内容: 保存。
用protoc来解析一下看看。操作了二个不包的数据包,解析结果如下:
有一个“好像”能解析有一个直接解析失败。什么原因呢?
马斯克是360加固的,脱壳app,然后把dex放jadx里倒腾倒腾。经过静态再经过用objection hook protubuf类,确定了是用了protobuf,而且通过搜索关键词”HeartBeatAckMsg”直接定位到了protobuf的关键类,proto文件要还原的话,有了这个类的信息就够了。但是为什么又解析不了呢??
再从jadx里面结合objection去分析,发现还用了一个框架netty,搜索了一下netty,并对它有了初步的认识,深入了解后,原来是他配合protobuf一起做到了高并发,高效率收发数据。protobuf用了netty的特点是在protobuf的数据前面多加了二个字节,表示protobuf数据的大小。
知道了原因,再尝试解析成功。
奇怪的是,从服务器返回来的数据在解析的时候有时需要去掉一个字节,有时2个字节。比如上面的HeartBeatAckMsg
数据能够通过protoc正常解析了,开始分析jadx中的proto相关类的生成。
从上面直接可以分析出来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个字节
(hbm)
得到如下结果
写其它proto文件和解析案例略过,有坑的地方我已经在上面说过。一个案例够其他同学研究学习了。
自定义消息内容发送
有了上面的分析和前期的工作,可以开始写发送消息的代码了。
首先是流程:通过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)
发送成功了
后记总结
通过这次的学习,对protobuf有了更深入的了解,虽然最终代码量少,但是分析工作做了很多,花了很长的一段时间。有个小彩蛋是马斯克上传图片是采用阿里的oss的。获取到临时的一个凭证后,通过阿里自带的oss-brower登录后,可以查看主帐号的所有Bucket
进入bucket里面以后,测试了上传,复制,下载都可以。里面的聊天图片,视频,语音内容都能正常查看。
本次学习记录内容仅限于研究学习交流使用,有兴趣尝试玩一玩的同学,请勿做其它非法的延伸功能扩展,否则后果自负,与本文发布者、平台、参与交流同学无关。