Loading... 在逆向一个app时,遇到了一种协议,传输的数据怪怪的,查了一下是Protobuf。直接开干! **使用的软件:charles** # 什么是 Protobuf ? Protobuf 是 Google 开发的一套数据存储传输协议,跟 xml 和 json 一样的,都是用来储存和传输数据的。 因为 Protobuf 能够把数据压缩得很小,所以传输数据就比 xml 和 json 快几倍,Protobuf 解析数据的速度也比它两快,所以在数据网络传输上,用 Protobuf 而不用 json 就有点受欢迎了。 不过 Protobuf 储存、压缩、传输效率比 json 好,付出的代价就是用法麻烦,不像 json.loads() json.dumps() 一下就搞定了这么简单。Protobuf 有一套自己的语法。不了解 Protobuf 协议语法和用法的话也无法反解数据。 ## 优缺点 ![](http://type.zimopy.com/usr/uploads/2024/01/582039037.jpg) ## protoc安装 1.安装 protoc :[Protoc下载地址](https://link.jianshu.com/?t=https://github.com/google/protobuf/releases "Protoc下载地址"),可以根据自己的系统下载相应的 protoc,windows 用户下载 win32/64 版本。 2.**配置 protoc 到系统的环境变量中**,执行如下命令查看是否安装成功: ```sh $ protoc --version # 如果正常打印 libprotoc 的版本信息就表明 protoc 安装成功 ``` 3.安装 ProtoBuf 相关的 python 依赖库 ```sh pip install grpcio grpcio-tools protobuf ``` # 编写案例 ## 案例1 创建一个叫person_pb2.proto的文件,内容如下 ```yaml syntax = "proto3"; package example; message person { int32 id = 1; string name = 2; } message all_person { repeated person Per = 1; } ``` ### 编译 ```sh protoc -I=. --python_out=. person.proto #直接编译到当前目录 protoc --proto_path=src --python_out=build/gen src/foo.proto src/bar/baz.proto#官方命令 ``` ### python中使用 protobuf 进行序列化与反序列化 **main.py:** ```python #! /usr/bin/env python # -*- coding: utf-8 -*- import person_pb2 # 为 all_person 填充数据 pers = person_pb2.all_person() p1 = pers.Per.add() p1.id = 1 p1.name = 'xieyanke' p2 = pers.Per.add() p2.id = 2 p2.name = 'pythoner' # 对数据进行序列化 data = pers.SerializeToString() # 对已经序列化的数据进行反序列化 target = person_pb2.all_person() target.ParseFromString(data) print(target.Per[1].name) # 打印第一个 person name 的值进行反序列化验证 ``` ## 案例2 文件名为 girl.proto ```yaml // syntax 负责指定使用哪一种 protobuf 服务 // 注意:syntax 必须写在非注释的第一行 syntax = "proto3"; // 包名, 这个目前不是很重要, 你删掉也是无所谓的 package girl; // 把 UserInfo 当成 Python 中的类 // name 和 age 当成绑定在实例上的两个属性 message UserInfo { string name = 1; // = 1表示第1个参数 int32 age = 2; } ``` ### 编译 ```sh protoc -I=. --python_out=. girl.proto #直接编译到当前目录 protoc --proto_path=src --python_out=build/gen src/foo.proto src/bar/baz.proto#官方命令 ``` ### python调用 ```python import orjson import girl_pb2 # 在 protobuf 文件中定义了 message UserInfo # 那么我们可以直接实例化它,而参数则是 name 和 age # 因为在 message UserInfo 里面指定的字段是 name 和 age user_info = girl_pb2.UserInfo(name="satori", age=17) # 如果不使用 protobuf,那么我们会选择创建一个字典 user_info2 = {"name": "satori", "age": 17} # 然后来看看序列化之后的结果 # 调用 SerializeToString 方法会得到序列化之后的字节串 print(user_info.SerializeToString()) """ b'\n\x06satori\x10\x11' """ # 如果是 json 的话 print(orjson.dumps(user_info2)) """ b'{"name":"satori","age":17}' """ ``` 可以看到使用 protobuf 协议序列化之后的结果要比 json 短,平均能得到一倍的压缩。序列化我们知道了,那么如何反序列化呢? ### 反序列化 ```python import orjson import girl_pb2 # 依旧是实例化一个对象,但是不需要传参 user_info = girl_pb2.UserInfo() # 传入序列化之后的字节串,进行解析(反序列化) user_info.ParseFromString(b'\n\x06satori\x10\x11') print(user_info.name) # satori print(user_info.age) # 17 # json 也是同理,通过 loads 方法反序列化 user_info2 = orjson.loads(b'{"name":"satori","age":17}') print(user_info2["name"]) # satori print(user_info2["age"]) # 17 ``` ## 在服务端之间传输 protobuf ```sh // 文件名:girl.proto syntax = "proto3"; package girl; message Request { string name = 1; int32 age = 2; } message Response { string info = 1; } ``` 一个 protobuf 文件中可以定义任意个 message,在生成 Python 文件之后每个 message 会对应一个同名的类。然后我们执行之前的命令,生成 Python 文件。 接下来使用 Tornado 编写一个服务: ```python from abc import ABC from tornado import web, ioloop import girl_pb2 class GetInfoHandler(web.RequestHandler, ABC): async def post(self): # 拿到客户端传递的字节流 # 这个字节流应该是由 girl_pb2.Request() 序列化得到的 content = self.request.body # 下面进行反序列化 request = girl_pb2.Request() request.ParseFromString(content) # 获取里面的 name 和 age 字段的值 name = request.name age = request.age # 生成 Response 对象 response = girl_pb2.Response( info=f"name: {name}, age: {age}" ) # 但 Response 对象不能直接返回,需要序列化 return await self.finish(response.SerializeToString()) app = web.Application( [("/get_info", GetInfoHandler)] ) app.listen(9000) ioloop.IOLoop.current().start() ``` 整个过程很简单,和 JSON 是一样的。然后我们来访问一下 ```python import requests import girl_pb2 # 往 localhost:9000 发请求 # 参数是 girl_pb2.Request() 序列化后的字节流 payload = girl_pb2.Request( name="古明地觉", age=17 ).SerializeToString() # 发送 HTTP 请求,返回 girl_pb2.Response() 序列化后的字节流 content = requests.post("http://localhost:9000/get_info", data=payload).content # 然后我们反序列化 response = girl_pb2.Response() response.ParseFromString(content) print(response.info) """ name: 古明地觉, age: 17 """ ``` 所以 protobuf 本质上也是一个序列化和反序列化协议,在使用上和 JSON 没有太大区别。只不过 JSON 对应的 Python 对象是字典,而 protobuf 则是单独生成的对象。 ## 数据类型 但是类型我们需要说一下,之前用到了两个基础类型,分别是 string 和 int32,那么除了这两个还有哪些类型呢? ### 基础类型 ![](http://type.zimopy.com/usr/uploads/2024/01/927746258.jpg) 以上是基础类型,当然还有复合类型,我们一会单独说,先来演示一下基础类型。编写 .proto 文件: ```sh // 文件名:basic_type.proto syntax = "proto3"; package basic_type; message BasicType { // 字段的名称可以和类型名称一致,这里为了清晰 // 我们就直接将类型的名称用作字段名 int32 int32 = 1; sint32 sint32 = 2; uint32 uint32 = 3; fixed32 fixed32 = 4; sfixed32 sfixed32 = 5; int64 int64 = 6; sint64 sint64 = 7; uint64 uint64 = 8; fixed64 fixed64 = 9; sfixed64 sfixed64 = 10; double double = 11; float float = 12; bool bool = 13; string string = 14; bytes bytes = 15; } ``` 然后我们来生成 Python 文件,命令如下: ```sh python3 -m grpc_tools.protoc --python_out=. -I=. basic_type.proto ``` 执行之后,会生成 basic_type_pb2.py 文件,我们测试一下: ```python import basic_type_pb2 basic_type = basic_type_pb2.BasicType( int32=123, sint32=234, uint32=345, fixed32=456, sfixed32=789, int64=1230, sint64=2340, uint64=3450, fixed64=4560, sfixed64=7890, double=3.1415926, float=2.71, bool=True, string="古明地觉", bytes=b"satori", ) # 定义一个函数,接收序列化之后的字节流 def parse(content: bytes): obj = basic_type_pb2.BasicType() # 反序列化 obj.ParseFromString(content) print(obj.int32) print(obj.sfixed64) print(obj.string) print(obj.bytes) print(obj.bool) parse(basic_type.SerializeToString()) """ 123 7890 古明地觉 b'satori' True """ ``` ### 复合类型 repeat 和 map repeat 和 map 是一种复合类型,可以把它们当成 Python 的列表和字典。 ```sh // 文件名:girl.proto syntax = "proto3"; package girl; message UserInfo { // 对于 Python 而言 // repeated 表示 hobby 字段的类型是列表 // string 则表示列表里面的元素必须都是字符串 repeated string hobby = 1; // map<string, string> 表示 info 字段的类型是字典 // 字典的键值对必须都是字符串 map<string, string> info = 2; } ``` 导入测试一下 ```python import girl_pb2 user_info = girl_pb2.UserInfo( hobby=["唱", "跳", "rap", "🏀"], info={"name": "古明地觉", "age": "17"} ) print(user_info.hobby) print(user_info.info) """ ['唱', '跳', 'rap', '🏀'] {'name': '古明地觉', 'age': '17'} """ ``` ### 避坑 结果正常,没有问题。但需要注意:对于复合类型而言,在使用的时候有一个坑。 ```python import girl_pb2 # 如果我们没有给字段传值,那么会有一个默认的零值 user_info = girl_pb2.UserInfo() print(user_info.hobby) # [] print(user_info.info) # {} # 对于复合类型的字段来说,我们不能单独赋值 try: user_info.hobby = ["唱", "跳", "rap", "🏀"] except AttributeError as e: print(e) """ Assignment not allowed to repeated field "hobby" in protocol message object. """ # 先实例化,然后单独给字段赋值,只适用于基础类型 # 因此我们需要这么做 user_info.hobby.extend(["唱", "跳", "rap", "🏀"]) user_info.info.update({"name": "古明地觉", "age": "17"}) print(user_info.hobby) print(user_info.info) """ ['唱', '跳', 'rap', '🏀'] {'name': '古明地觉', 'age': '17'} """ ``` ## message 的嵌套 通过标识符 message 即可定义一个消息体,大括号里面的则是参数,但参数的类型也可以是另一个 message。换句话说,message 是可以嵌套的。 ```sh // 文件名:girl.proto syntax = "proto3"; package girl; message UserInfo { repeated string hobby = 1; // BasicInfo 定义在外面也是可以的 message BasicInfo { string name = 1; int32 age = 2; string address = 3; } BasicInfo basic_info = 2; } ``` 生成 Python 文件,导入测试一下。 ```python import girl_pb2 # 在 protobuf 文件中,BasicInfo 定义在 UserInfo 里面 # 所以 BasicInfo 在这里对应 UserInfo 的一个类属性 # 如果定义在全局,那么直接通过 girl_pb2 获取即可 basic_info = girl_pb2.UserInfo.BasicInfo( name="古明地觉", age=17, address="地灵殿") user_info = girl_pb2.UserInfo( hobby=['唱', '跳', 'rap', '🏀'], basic_info=basic_info ) print(user_info.hobby) """ ['唱', '跳', 'rap', '🏀'] """ print(user_info.basic_info.name) print(user_info.basic_info.age) print(user_info.basic_info.address) """ 古明地觉 17 地灵殿 """ ``` ## 枚举类型 再来聊一聊枚举类型,它通过 enum 标识符定义。 ```sh // 里面定义了两个成员,分别是 MALE 和 FEMALE enum Gender { MALE = 0; FEMALE = 1; } ``` 这里需要说明的是,对于枚举来说,等号后面的值表示成员的值。比如一个字段的类型是 Gender,那么在给该字段赋值的时候,要么传 0 要么传 1。因为枚举 Gender 里面只有两个成员,分别代表 0 和 1。 而我们前面使用 message 定义消息体的时候,每个字段后面跟着的值则代表序号,从 1 开始,依次递增。至于为什么要有这个序号,是因为我们在实例化的时候,可以只给指定的部分字段赋值,没有赋值的字段则使用对应类型的零值。那么另一端在拿到字节流的时候,怎么知道哪些字段被赋了值,哪些字段没有被赋值呢?显然要通过序号来进行判断。 ### 编写 .proto 文件。 ```sh // 文件名:girl.proto syntax = "proto3"; package girl; // 枚举成员的值必须是整数 enum Gender { MALE = 0; FEMALE = 1; } message UserInfo { string name = 1; int32 age = 2; Gender gender = 3; } message Girls { // 列表里面的类型也可以是 message 定义的消息体 repeated UserInfo girls = 1; } ``` 输入命令生成 Python 文件,然后导入测试: ```python import girl_pb2 user_info1 = girl_pb2.UserInfo( name="古明地觉", age=17, gender=girl_pb2.Gender.Value("FEMALE")) user_info2 = girl_pb2.UserInfo( name="芙兰朵露", age=400, # 传入一个具体的值也是可以的 gender=1) girls = girl_pb2.Girls(girls=[user_info1, user_info2]) print(girls.girls[0].name, girls.girls[1].name) print(girls.girls[0].age, girls.girls[1].age) print(girls.girls[0].gender, girls.girls[1].gender) """ 古明地觉 芙兰朵露 17 400 1 1 """ ``` 枚举既可以定义在全局,也可以定义在某个 message 里面。 ## .proto 文件的互相导入 .proto 文件也可以互相导入,我们举个例子。下面定义两个文件,一个是 people.proto,另一个是 girl.proto,然后在 girl.proto 里面导入 people.proto。 ```sh /* 文件名:people.proto */ syntax = "proto3"; // 此时的包名就很重要了,当该文件被其它文件导入时 // 需要通过这里的包名,来获取内部的消息体、枚举等数据 package people; message BasicInfo { string name = 1; int32 age = 2; } /* 文件名:girl.proto */ syntax = "proto3"; // 导入 people.proto, import "people.proto"; message PersonalInfo { string phone = 1; string address = 2; } message Girl { // 这里的 BasicInfo 是在 people.proto 里面定义的 // people.proto 里面的 package 指定的包名为 people // 所以这里需要通过 people. 的方式获取 people.BasicInfo basic_info = 1; PersonalInfo personal_info = 2; } ``` 然后执行命令,基于 proto 文件生成 Python 文件,显然此时会有两个 Python 文件。 > python3 -m grpc_tools.protoc --python_out=. -I=. girl.proto > python3 -m grpc_tools.protoc --python_out=. -I=. people.proto ```python import girl_pb2 import people_pb2 basic_info = people_pb2.BasicInfo(name="古明地觉", age=17) personal_info = girl_pb2.PersonalInfo(phone="18838888888", address="地灵殿") girl = girl_pb2.Girl(basic_info=basic_info, personal_info=personal_info) print(girl.basic_info.name) # 古明地觉 print(girl.basic_info.age) # 17 print(girl.personal_info.phone) # 18838888888 print(girl.personal_info.address) # 地灵殿 ``` # 文档 中文文档:`https://colobu.com/2017/03/16/Protobuf3-language-guide/` 官方文档:`https://protobuf.dev/reference/python/python-generated/` # 项目情况 ## 抓包情况 ![image.png](http://type.zimopy.com/usr/uploads/2024/01/1609873096.png) ## 查看请求信息 ```js 1 { 1: "6288218576422" } ``` ## 改设置看清晰的数据 鼠标在请求的链接上左击,修改viewer mapping ![image.png](http://type.zimopy.com/usr/uploads/2024/01/2829654441.png) ## 再看请求头 ![image.png](http://type.zimopy.com/usr/uploads/2024/01/3461050348.png) ```js type_url: "\n\r6288218576422" ``` ## 请求验证 另外他还有一个验证信息,也就是返回值 ![image.png](http://type.zimopy.com/usr/uploads/2024/01/2985518374.png) ```json 18: { 1: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkaXNwbGF5UmVhc29uIjoiRVhQRVJJTUVOVCIsImNhcHRjaGFLZXlJZCI6IkI1QjA3IiwiZXhwIjoxNzA0NjAxOTUxLCJhdXRoVHlwZSI6InNtcyIsImF1dGhJZCI6IjYyODgyMTg1NzY0MjIiLCJyZWZyZXNoVG9rZW4iOiIifQ.XTj4np_Kx3naYYVbuPwvTtJ0s5bBtK-Qf6g6S-QsbME" 2: "44717a7f5e6710c51.1681322902|r=us-west-2|pk=B5B07C8C-F93F-44A8-A353-4A47B8AD5238|at=40|sup=1|rid=36|ag=101|cdn_url=https%3A%2F%2Fclient-api.arkoselabs.com%2Fcdn%2Ffc|lurl=https%3A%2F%2Faudio-us-west-2.arkoselabs.com|surl=https%3A%2F%2Fclient-api.arkoselabs.com|smurl=https%3A%2F%2Fclient-api.arkoselabs.com%2Fcdn%2Ffc%2Fassets%2Fstyle-manager" } ``` 这个就稍微复杂一点了,不过呢已经构建好了 # 介绍完毕,开始编写proto ## 构造 第一个请求 ```bash # 文件名 persong.proto syntax = "proto3"; message Phone{ string type_url=1; } ``` ## python 调用赋值 ```python import person_pb2 headers = { 'content-type': 'application/x-protobuf', 'accept-encoding': 'gzip' } my_seg_phone = person_pb2.Phone() my_seg_phone.type_url = "\n\r6288218576422" data = my_seg_phone.SerializeToString() requests.post("https://api.kkk.com/v3/auth/login", data=data, headers=headers) ``` ## 构造第二个请求 里面的18是服务端要解析的数字,一定要对应好,否则解析错误 ```sh syntax = "proto3"; message TT{ map<string, string> data = 18; } ``` ## python调用 ```python my_seg_tt = renzheng_pb2.TT(data={"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkaXNwbGF5UmVhc29uIjoiRVhQRVJJTUVOVCIsImNhcHRjaGFLZXlJZCI6IkI1QjA3IiwiZXhwIjoxNzA0NjAxOTUxLCJhdXRoVHlwZSI6InNtcyIsImF1dGhJZCI6IjYyODgyMTg1NzY0MjIiLCJyZWZyZXNoVG9rZW4iOiIifQ.XTj4np_Kx3naYYVbuPwvTtJ0s5bBtK-Qf6g6S-QsbME": "44717a7f5e6710c51.1681322902|r=us-west-2|pk=B5B07C8C-F93F-44A8-A353-4A47B8AD5238|at=40|sup=1|rid=36|ag=101|cdn_url=https%3A%2F%2Fclient-api.arkoselabs.com%2Fcdn%2Ffc|lurl=https%3A%2F%2Faudio-us-west-2.arkoselabs.com|surl=https%3A%2F%2Fclient-api.arkoselabs.com|smurl=https%3A%2F%2Fclient-api.arkoselabs.com%2Fcdn%2Ffc%2Fassets%2Fstyle-manager"}) print(my_seg_tt) data = my_seg_tt.SerializeToString() ``` 当时我写的是,这样是错误的 ```python renzheng_pb2.TT(data={1:"dfsdf",2: "sdfsdffsdf"}) ``` 借鉴: https://blog.csdn.net/mijichui2153/article/details/99585860 https://www.leyeah.com/index.php/article/detailed-examples-using-protobuf-python-699945 最后修改:2024 年 01 月 08 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏