吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 11603|回复: 85
收起左侧

[Android 原创] 使用python+aiohttp还原某度某吧私信的websocket+protobuf+RSA/AES加密流程

    [复制链接]
Starry-OvO 发表于 2022-6-13 20:51
本帖最后由 Starry-OvO 于 2022-9-18 21:08 编辑

本人是一名大学生,因为某吧吧务管理有自动发送私信的需求,我就利用课余时间逆向研究了某吧app的私信功能。刚准备研究的时候,我在github和吾爱破解都搜了一下,没找到相关内容,这篇文章算是补个漏吧

先给个Unlicense开源仓库地址https://github.com/Starry-OvO/aiotieba还望各位能赏个star

jadx反编译应该算是很常规的逆向手段了,而且说实话我没写过java也不太了解java,只能靠C++的开发经验来推测代码的用途,我这篇文章的参考价值可能更多地在python实现上

如下图,用网页端打开某吧私信页的时候可以明显看到一个status_code=101的websocket建立请求,并且发送和接收私信都是在这个websocket上实现,我就大胆猜测app的私信功能也是基于websocket实现的

010-网页端websocket

打开模拟器和wireshark开始抓包。然后打开私信界面

020-app私聊界面

wireshark抓到了websocket建立请求,追踪这个请求的TCP流

030-wireshark追踪流

如下图所示,可以发现该请求的Host和网页端的一致,都是im.tieba.baidu.com:8000

040-抓包ws建立请求

观察一下client发给server的第一个websocket包,如下图所示。可以看到很多明文的字符串,但也夹杂着一些不能ascii解码的内容,出于直觉我猜测这是protobuf的编码效果

050-分析ws第1帧

再看后续双方通信的websocket包,完全不包含明文,我推测这是某种加密的效果

051-分析ws第2帧

052-分析ws第3帧

jadx反编译某吧极速版app

060-jadx打开极速版

搜索字符串im.tieba.baidu.com并跳转到下图位置

070-jadx搜索imtieba

搜索一下该变量被调用的位置,如下图红框所示。蓝框标出的是伏笔,和后续的RSA加密有关

080-jadx跟踪url

跟踪这个链接,我们可以找到Socket的初始化方法

090-找到socket初始化函数

注意到上图红框里的MessageManager,因为它有个类方法sendMessage(如下图),再综合其他一系列特征,比如创建并行化任务队列的相关方法,我判断出这应该是负责收发包的类

100-注意到MessageManager

分析一下上图红框里的函数e

120-sendMessage进入发包函数

通过上图所示的分析,再结合类成员变量(如mData、mCmd等)的内容,我判断出Message应该是一个保存请求参数的类,而MessageTask保存了任务类别相关的信息

130-分析发包函数

mCmd是初始化Message的一个关键参数,需要特别注意它的用途,如下图所示

140-注意cmd的作用

根据下图分析,我推测Message的类成员变量mData保存了请求相关的数据

150-注意mData的作用

下面跟踪一下setData方法

160-跟踪setData

找到了一个纯虚成员函数encode

180-准备跟踪encode

因为这个方法的具体实现在Message的子类中,我们只能回到initSocket来找找其他线索

会动态调试的也可以考虑在这个initSocket方法附近下断点

190-回到initSocket

上图所示的ResponseOnlineMessage是被注册到MessageManager的第一个MessageTask对应的Message子类,我判断它应该是被用于接收client发给server的第一个websocket包的返回数据

如下图所示,点开这个Message我们找到了protobuf流程的特征

210-发现protobuf

如下图所示,通过UpdateClientInfoResIdl提供的线索,我们可以进一步找到同一级文件夹下UpdateClientInfoReqIdl的protobuf字段定义

根据名称和字段定义我判断Req是请求proto,而Res是响应proto

220-找到UpdateClientInfoReqIdl字段定义

其中UpdateClientInfoReqIdl.DataReq的proto定义如下图所示

230-找到UpdateClientInfoReqIdl字段定义

下一步就是查找protobuf字段被装填的位置,这可以通过查找Builder被调用的位置来实现,如下图所示

250-寻找protobuf数据装填位置

我们找到了UpdateClientInfoMessage的类方法encode,它正是对上文提到的Message类的纯虚方法encode的具体实现

如下图所示,看来这个encode系列方法所实现的功能就是装填protobuf字段

260-明确装填方式并寻找上级调用

同时找到该encode方法被调用的位置getData,再找getData方法的上级调用,如下图所示

270-寻找上级调用

找到了函数encodeInBackground,以及熟悉的老朋友mData

该函数通过java反射来调用protobuf-Builder的toByteArray方法,将proto容器转换为bytes

280-找到encodeInBackground

再找encodeInBackground方法的上层调用,找到方法a

290-找到打包方法

这个a方法非常重要,简单分析如下

300-分析打包方法-改

深入函数e,发现一个函数g.f

310-e函数分析

点进去发现这是一个gzip压缩的方法

320-e函数分析gzip

接着分析a函数里的u.a函数,发现这是一个简单的AES加密方法

330-u a函数分析AES

既然是AES加密我们就得找密钥

如下图所示,类成员变量Yn就是AES密钥,该密钥通过函数g里的函数调用u.be(eo)生成

340-AES密钥

为了搞清楚生成的时机,还得看看函数g在何处被调用,如下图所示

350-寻找g函数被调用的位置

发现函数g只在Socket初始化的时候被调用了一次

因为函数g的入参和AES加密无关,这个getPublicKey我们后面再看

360-找到g函数被调用的位置

下面分析一下生成AES密钥的函数u.be的入参eo

370-分析eo

点进函数eo发现这是个返回36字节长的随机bytes的函数

而且他这个子串分割写得好像有点问题,子串长度与int i无关,而是一个固定值36

380-eo随机生成36bytes

然后就用sha1生成一个AES密钥,salt是bytes数组ahI

390-生成AES密钥

现在我们就完全知道函数a中的函数e和函数u.a的用途了,解析如下

400-阐释上层函数-改

然后是对a.a函数的分析,如下图

400-分析a a函数-改

为了验证上述猜测,我们保存一下websocket的首个请求

420-保存字节流

看头部的9字节,第1个字节为0x08,意味着数据未经过AES加密和gzip压缩;后4个字节转换到十进制就是熟悉的cmd=1001

430-分析9字节头部

用hexediter删去头部9字节后保存,再用protoc反解剩余内容,发现确实是protobuf

440-分析protobuf

熟悉某吧客户端爬虫的能很轻松地看出来,这些protobuf字段基本都是固定的客户端标识,比较常规,除了一个secretKey需要额外分析

先看secretKey字段的赋值位置

450-寻找secretKey的生成方式

然后搜索类属性secretKey被赋值的位置

460-寻找secretKey的生成方式

然后看setSecretKey方法被调用的位置

470-寻找secretKey的生成方式

再研究类方法oJ,发现它其实就是个取出类属性Yo的方法

480-寻找secretKey的生成方式

类属性Yo又是在函数g中通过函数u.b生成

进入u.b可以知道这就是一个利用publicKey对输入bytes数组做RSA加密的函数

而输入的bytes数组其实就是从36字节长的随机字符串eo逐字节转换而来

这个eo也被用于AES密钥的生成,之前的分析也提到过

490-RSA生成secretKey

回到上一层函数我们发现publicKey是通过函数u.q(bArr)生成

点进函数q我们发现这其实就是一个利用bArr生成公钥的方法

500-RSA导入公钥

通过不断地往上级翻找,我们可以发现bArr来源于getRSAPublicKey函数

520-找到g的调用位置

点进这个函数,成功回收伏笔,把这串硬编码的东西base64decode就能得到RSA公钥了

530-找到RSA公钥的来源

现在回头分析这个发送私信时产生的websocket包,第1字节0x88说明该包经过AES加密而未经gzip压缩

550-重新分析ws帧

利用后四个字节给出的cmd编号0x0320c9=205001我们可以搜索出发送私信功能所对应的Message子类

560-找出私信Message

进一步可以找出发送私信使用的protobuf字段赋值方法

570-找出私信protobuf

以及字段定义

580-找出私信protobuf

自行编写proto文件还原必需字段,这是客户端信息上报的proto

590-编写客户端信息上报protobuf

这是私信的proto。然后用protoc编译为python格式脚本

595-编写私信protobuf

还需要分析websocket的解包方法,因为大部分工作都和上面重复,我这里就只放个核心分析了

需要注意的是你client发送的时候用的是什么log_id,server响应的时候返回的就是什么log_id

600-分析websocket解包函数

到这里我们已经能完全理解某吧私信的工作流程了

首先是包格式

每个包的头部第1字节包含了是否AES加密(第8bit是否为1)、是否gzip压缩(第7bit是否为1)、是否包含extraData(第4bit是否为1)的信息

第2345字节为int32的cmd编号,用来标明该包是用于什么功能的,譬如1001对应客户端信息上报而205001对应私信,当服务端主动推送时这个cmd编号就能起到作用

第6789字节为int32的log_id,当你同时发了很多包时,这个log_id就可以用于区分哪个响应对应哪个请求

然后是websocket建立的流程

首先http101请求建立websocket,然后发送一个初始化包,里面包含用户身份信息BDUSS以及用RSA公钥加密过的密码secretKey,server用RSA密钥解密后生成和client一致的AES密钥,后续的所有包都会用这个密钥做AES加密后再传输

简单概括就是websocket建立→非对称加密协商密钥→对称加密传输数据

后面就是用python+aiohttp还原这整套流程,用我小号给大号发私信debug的效果如下

!800-私信测试

!810-私信测试

可以看到效果非常完美,下面我会粗略解释一下我的代码

首先是打包函数,这里AES加密的padding是手动实现的,要注意int转bytes应使用大端序

820-编写websocket打包函数

然后是解包函数,要注意因为server也用了padding,这里要用bytes.rstrip把填充的东西去掉

830-编写websocket解包函数

然后是AES加密功能的实现,salt就是从源码扒出来的ahI,密钥生成方法sha1,迭代次数参考java设置为5,生成的密钥长度是32bytes也就是AES256

840-编写AES加密器和密码

然后是比较精髓的WebsocketResponse类设计,通过一个弱引用字典实现对返回数据的异步等待

每次websocket请求都会产生一个该类的实例用于存放返回数据,产生实例的同时其初始化函数__init__会将实例本身添加到一个弱引用字典

当从websocket接收到返回数据时,数据分派器ws_dispatcher会按照req_id从弱引用字典中找到对应的WebsocketResponse实例,并将数据填进去,然后set _readable_event,之后read协程就会被解阻塞,最后将数据返回

为什么要使用弱引用?这是为了让WebsocketResponse实例在被丢弃时可以被自动gc,比如出现timeout。如果使用普通字典(强引用)来保存实例,那会导致作为字典值的实例无法被自动gc,内存会随着程序的运行时间无限增长

然后这里__slots__里添加__weakref__是为了允许类实例被弱引用,添加__dict__是为了让_websocket_request_id可写

这个_websocket_request_id在每次创建实例时都会+1以保证每个实例的请求id的唯一性

850-编写WebsocketResponse类

read方法其实就是在等待_readable_event的set事件,不论有没有超时,read退出时都会将自己从弱引用字典ws_res_wait_dict里删除,意思是不再需要接收返回数据

855-编写WebsocketResponse类的read方法

然后是编写websocket的ClientSession,记得把那个http101请求里用到的headers带上

860-编写ClientSession

然后是编写websocket的数据接收与分派器,死循环等待读数据,读到之后就将数据填进WebsocketResponse实例,并通过_readable_event发出可读通知

当Client退出时这个ws_dispatcher也会被cancel掉,不需要担心什么溢出问题

865-编写websocket分派器

连接websocket通过ClientSession._ws_connect实现,一并被创建的还有分派器

870-连接websocket

init_websocket会发送初始化信息,RSA加密的步骤全在这里实现

880-发送初始化信息

最后就是私信方法的实现

890-发送私信

好了全文结束

上述提到的所有python代码(不包括debug脚本)都包含在https://github.com/Starry-OvO/aiotieba/blob/master/aiotieba/client.py里,食用方法参考项目的README.md

如果这篇文章能帮到你,请给我的开源项目点一个star,非常感谢

免费评分

参与人数 55吾爱币 +60 热心值 +49 收起 理由
ofcave + 1 + 1 大佬请问一下, apk开了代{过}{滤}理就不能联网,也抓不到包怎么办呢
skykill2018 + 1 + 1 用心讨论,共获提升!
使徒行者 + 1 用心讨论,共获提升!
victos + 1 + 1 谢谢@Thanks!
kakavspaoche + 1 + 1 热心回复!
吾之名翎 + 1 用心讨论,共获提升!
weilan + 1 + 1 我很赞同!
stone009 + 1 + 1 用心讨论,共获提升!
beichen1031 + 1 + 1 热心回复!
v.n.lee + 3 + 1 大佬的python好有高级感,再看看我的烂代码, 😂🤣
tony118 + 1 + 1 我很赞同!
technogoon + 1 + 1 热心回复!
yixi + 1 + 1 谢谢@Thanks!
sordar + 1 + 1 鼓励转贴优秀软件安全工具和文档!
丶峰宇 + 1 + 1 热心回复!
笙若 + 1 + 1 谢谢@Thanks!
丶沐箐 + 1 + 1 用心讨论,共获提升!
鸣蜩十四 + 1 + 1 用心讨论,共获提升!
Sunshine1 + 1 用心讨论,共获提升!
Purplelight + 1 + 1 用心讨论,共获提升!
q1082121 + 1 热心回复!
dout0712 + 1 + 1 我很赞同!
丨灬阿豆 + 1 鼓励转贴优秀软件安全工具和文档!
azcolf + 1 + 1 热心回复!
Establish + 1 + 1 用心讨论,共获提升!
芽衣 + 1 + 1 用心讨论,共获提升!
lu_ + 2 + 1 我很赞同!
Tom_Me + 1 + 1 我很赞同!
fengbolee + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
XMQ + 1 我很赞同!
剑来…… + 2 + 1 我很赞同!
xiaoheivo + 1 + 1 谢谢@Thanks!
zshq1 + 1 + 1 我很赞同!
-yuy- + 1 + 1 用心讨论,共获提升!
gaosld + 1 + 1 热心回复!
qcinnovative + 1 + 1 用心讨论,共获提升!
hhxk + 1 + 1 用心讨论,共获提升!
Yanggedi + 1 + 1 用心讨论,共获提升!
QingYi. + 3 + 1 我很赞同!
17798 + 1 谢谢@Thanks!
wxaz + 1 我很赞同!
泽哥 + 1 + 1 热心回复!
liu8359 + 1 + 1 用心讨论,共获提升!
小黑侠 + 1 + 1 用心讨论,共获提升!
pdcba + 1 + 1 用心讨论,共获提升!
WuJ1n9 + 1 + 1 用心讨论,共获提升!
好人不受罪 + 1 + 1 我很赞同!
5omggx + 1 + 1 用心讨论,共获提升!
开心的一逼 + 1 + 1 用心讨论,共获提升!
努力加载中 + 1 + 1 谢谢@Thanks!
owouwu + 1 + 1 用心讨论,共获提升!
Crazykim + 1 + 1 用心讨论,共获提升!
howyouxiu + 1 + 1 用心讨论,共获提升!
jjjzw + 1 + 1 用心讨论,共获提升!
503671998 + 1 + 1 用心讨论,共获提升!

查看全部评分

本帖被以下淘专辑推荐:

  • · 好帖|主题: 539, 订阅: 84

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

友纪 发表于 2022-8-1 18:12
楼主牛逼 ,,想问下

DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1d\x43ommitPersonalMsgReqIdl.proto\"\x85\x01\n\x17\x43ommitPersonalMsgReqIdl\x12.\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32 .CommitPersonalMsgReqIdl.DataReq\x1a:\n\x07\x44\x61taReq\x12\r\n\x05toUid\x18\x02 \x01(\x03\x12\x0f\n\x07msgType\x18\x03 \x01(\x05\x12\x0f\n\x07\x63ontent\x18\x04 \x01(\tb\x06proto3')

这\n\x1d\x43 一些列字节代表的是什么 是怎么获得的。。。固定的?
 楼主| Starry-OvO 发表于 2022-6-21 19:37
pc1826 发表于 2022-6-21 18:34
ws_res_wait_dict: weakref.WeakValueDictionary = weakref.WeakValueDictionary()
TypeError: 'ABCMe ...

你现在用的哪个python版本?应该是低版本weakref.WeakValueDictionary不支持类型注解的原因
我正在调试3.9的兼容性,3.8应该是不准备再支持了,因为有个str.removeprefix是3.9的新特性
torrent 发表于 2022-6-14 16:20
503671998 发表于 2022-6-14 17:02
很强了加油
jjjzw 发表于 2022-6-14 17:36
厉害啊,知识面好广
正己 发表于 2022-6-14 22:37
本帖最后由 正己 于 2022-6-14 22:40 编辑

写得很详细又很精彩,又是第一帖,加个精,期待大佬后续佳作
Crazykim 发表于 2022-6-14 22:54
作者厉害啊!
s98 发表于 2022-6-14 23:07
谢谢分享
 楼主| Starry-OvO 发表于 2022-6-15 00:00
正己 发表于 2022-6-14 22:37
写得很详细又很精彩,又是第一帖,加个精,期待大佬后续佳作

hh,感谢版主抬爱
as4202 发表于 2022-6-15 00:12
厉害,应用的技能学习了
kanxue2018 发表于 2022-6-15 00:45
期待大佬后续佳作
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-4-23 20:17

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表