# PWN Protobuf逆向 前言: 呜呜,下午逃课来做这个东西,可惜最后一次课点名md,还没搞,就跑去教室了,学校好多课都没什么用,还有好多活动,太jb垃圾了学校呜呜,哎,当初考的太差了,呜呜, 2023年CISCN 和2024年CISCN都有此协议的pwn,所以这里准备复现复现,也是为了准备下省赛的题型,protobuf 协议很简单,只要掌握了基础的知识和逆向的手段基本上问题不大,很简单,今天学的东西不是很难,,,, ## 简介 Protocol Buffers (Protobuf) 是由 Google 开发的一种高效的、语言中立的、平台中立的、可扩展的序列化结构数据的方法。它可以用于通信协议、数据存储、以及各种系统中不同组件之间的结构化数据交换。Protobuf 通过一种灵活且紧凑的二进制格式来存储数据,相比于 XML 或 JSON 等文本格式,具有更高的效率。 ### Protobuf 的核心特点: 1. **高效**:Protobuf 采用紧凑的二进制格式,具有较小的消息体积和较快的序列化/反序列化速度。 2. **跨语言支持**:Protobuf 支持多种编程语言(如 C++, Java, Python, Go, C#, JavaScript 等),方便不同语言之间的互操作。 3. **平台无关**:Protobuf 定义的消息格式可以在不同操作系统和平台之间无缝传输。 4. **可扩展性**:Protobuf 允许在不破坏现有数据结构的情况下向消息格式中添加新的字段,因此具有较好的向后兼容性。 ### 工作原理: Protobuf 通过定义一个 `.proto` 文件来描述消息格式。该文件包含数据的结构定义,如字段名称、数据类型等。然后使用 Protobuf 编译器 `protoc` 将 `.proto` 文件编译为特定语言的代码,这些代码可以用于序列化和反序列化数据。 ```c syntax = "proto3"; message Person { string name = 1; int32 id = 2; string email = 3; } ``` 在这个例子中,`Person` 是一个消息类型,包含三个字段:`name`(字符串类型)、`id`(32位整数类型)、`email`(字符串类型)。 #### 编译和使用: 1. 使用 `protoc` 编译 `.proto` 文件生成语言特定的代码(例如 Python、Java、C++ 等)。 2. 在代码中使用生成的类来序列化数据到二进制格式,或将二进制数据反序列化成对应的对象。 序列化和反序列化: ​ ```python # 示例 Python 代码 import person_pb2 # 创建一个 Person 对象 person = person_pb2.Person() person.name = "Alice" person.id = 123 person.email = "alice@example.com" # 序列化为二进制数据 data = person.SerializeToString() # 反序列化 new_person = person_pb2.Person() new_person.ParseFromString(data) print(new_person.name) # 输出 Alice ``` ### 为什么选择 Protobuf? - **高效性**:Protobuf 在性能和存储上都优于传统的文本格式(如 JSON 和 XML),特别是在需要传输大量数据时。 - **跨语言和跨平台**:无论是移动端、Web 端还是后台服务,Protobuf 都能提供一致的数据格式。 - **版本兼容**:Protobuf 允许在不破坏已存在系统的情况下对数据结构进行扩展,非常适合需要长期维护和版本演进的系统。 总的来说,Protobuf 是一种高效、灵活且跨语言的数据序列化工具,广泛应用于现代微服务架构、网络协议和大数据处理等领域。 以上是简单的知识,下面来搭建下 安装 ``` wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protobuf-cpp-3.6.1.tar.gz tar -xzvf protobuf-cpp-3.6.1 cd protobuf-3.6.1 ``` 配置编译安装 ``` ./configure make sudo make install ``` 在/usr/lib中创建软连接: ```sh cd /usr/lib sudo ln -s /usr/local/lib/libprotoc.so.17 libprotobuf.so.17 sudo ln -s /usr/local/lib/libprotoc.so.17 libprotoc.so.17 ``` protoc --version 在我们比赛当中,用的protobuf-c比较多,一般CTF PWN 中的 下载Protobuf-c项目:https://github.com/protobuf-c/protobuf-c/releases 进入Protobuf-c目录配置、编译并安装: ``` tar -xzvf protobuf-c.tar.gz cd protobuf-c ./configure && make sudo make install ``` ## 基本语法 官方文档: ```c // demo.proto syntax = "proto3"; package tutorial; message Person { string name = 1; int32 id = 2; string email = 3; enum PhoneType { PHONE_TYPE_UNSPECIFIED = 0; PHONE_TYPE_MOBILE = 1; PHONE_TYPE_HOME = 2; PHONE_TYPE_WORK = 3; } message PhoneNumber { string number = 1; PhoneType type = 2; } repeated PhoneNumber phones = 4; } message AddressBook { repeated Person people = 1; } ``` 这段代码是一个 Protobuf 的示例定义文件 (`demo.proto`),描述了一个地址簿(`AddressBook`)的数据结构以及包含在其中的人员信息(`Person`)和联系方式(`PhoneNumber`)。让我们逐行分析该文件的结构。 ### 1. `syntax = "proto3";` 指定 Protobuf 的语法版本为 `proto3`。`proto3` 是 Protobuf 的最新版本,具有更简洁的语法和一些新特性,如默认值和支持 `repeated` 字段等。 ### 2. `package tutorial;` 定义了一个命名空间 `tutorial`,这有助于避免与其他 Protobuf 定义文件中可能定义的类型发生命名冲突。它通常会作为生成的代码的包名。 ### 3. `message Person` 定义了一个消息类型 `Person`,表示一个人的信息。消息类型类似于面向对象编程中的类,包含一组字段。 #### `Person` 中的字段: - `string name = 1;` 这是一个字符串类型字段,用于存储人的姓名。字段的标签(`= 1`)指示该字段在序列化时的唯一标识符,通常是一个递增的数字。 - `int32 id = 2;` 这是一个整型字段,表示人的唯一标识符(ID)。 - `string email = 3;` 这是一个字符串类型字段,用于存储人的电子邮件地址。 #### `PhoneType` 枚举: - ``` enum PhoneType { ... } ``` 这个枚举定义了手机类型的不同类别。Protobuf 的枚举类似于常规的枚举类型,可以用来描述具有固定值的一组常量。它有以下四个值: - `PHONE_TYPE_UNSPECIFIED = 0;`:表示未指定的手机类型。 - `PHONE_TYPE_MOBILE = 1;`:表示手机号码。 - `PHONE_TYPE_HOME = 2;`:表示家庭电话。 - `PHONE_TYPE_WORK = 3;`:表示工作电话。 #### `PhoneNumber` 消息: - ``` message PhoneNumber { ... } ``` 这是一个嵌套的消息类型,用于表示电话号码的信息。它包含两个字段: - `string number = 1;`:存储电话号码。 - `PhoneType type = 2;`:使用之前定义的 `PhoneType` 枚举来指示电话号码的类型(如家庭电话、工作电话等)。 #### `repeated PhoneNumber phones = 4;` - `repeated` 是 Protobuf 中的一个关键字,表示该字段可以包含多个值。这个字段用来存储一个人的所有电话号码,类型为 `PhoneNumber`。 ### 4. `message AddressBook` 定义了另一个消息类型 `AddressBook`,用于表示一个地址簿。该消息包含一个 `repeated` 字段,用于存储多个 `Person` 对象,即这个地址簿中包含的所有人。 - `repeated Person people = 1;` 表示 `AddressBook` 可以存储多个 `Person` 对象。字段 `people` 是一个重复字段,用来表示地址簿中的所有人员。 ### 总结: 这个 Protobuf 定义文件描述了一个地址簿系统的结构,其中包括: - 每个人的基本信息(如姓名、ID、电子邮件等)。 - 每个人的电话号码,以及电话号码的类型(如手机、家庭电话、工作电话)。 - 一个包含多个人信息的地址簿。 ### 使用示例: 假设您将这段 Protobuf 定义文件(`demo.proto`)编译成相应的代码(如 Python、Java、C++ 等),您可以使用以下方式创建和操作这些消息。 例如,假设我们使用 Python,生成的类 `Person` 和 `AddressBook` 可以像这样使用: ```python import demo_pb2 # 假设生成的文件名是 demo_pb2.py # 创建 Person 对象 person = demo_pb2.Person() person.name = "Alice" person.id = 123 person.email = "alice@example.com" phone = person.phones.add() # 添加电话号码 phone.number = "123-456-7890" phone.type = demo_pb2.Person.PHONE_TYPE_MOBILE # 创建 AddressBook 并添加 Person address_book = demo_pb2.AddressBook() address_book.people.append(person) # 序列化到字符串 data = address_book.SerializeToString() # 反序列化 new_address_book = demo_pb2.AddressBook() new_address_book.ParseFromString(data) print(new_address_book) ``` ## 编译 protoc --c_out=. demo.proto 生成如下文件 - **demo.pb-c.h**:类的声明。 - **demo.pb-c.c**:类的实现。 编译为python protoc --python_out=. demo.proto 与c进行交互 生成demo_pb2, import demo_pb2 导入即可 ## 逆向 下面进入正题 我们一般第一步逆向的时候先还原结构体 Protobuf关键结构体 在生成的demo.c文件里 我们可以看到如下unpack函数 ```c Tutorial__AddressBook * tutorial__address_book__unpack(ProtobufCAllocator *allocator, size_t len, const uint8_t *data) { return (Tutorial__AddressBook *) protobuf_c_message_unpack (&tutorial__address_book__descriptor, allocator, len, data); } ``` 反序列化函数传入的是消息结构体数据的descriptor,我们最后逆的就是这个消息结构体数据 descriptor: ```c struct ProtobufCMessageDescriptor { /** Magic value checked to ensure that the API is used correctly. */ uint32_t magic; /** The qualified name (e.g., "namespace.Type"). */ const char *name; /** The unqualified name as given in the .proto file (e.g., "Type"). */ const char *short_name; /** Identifier used in generated C code. */ const char *c_name; /** The dot-separated namespace. */ const char *package_name; /** * Size in bytes of the C structure representing an instance of this * type of message. */ size_t sizeof_message; /** Number of elements in `fields`. */ unsigned n_fields; /** Field descriptors, sorted by tag number. */ const ProtobufCFieldDescriptor *fields; /** Used for looking up fields by name. */ const unsigned *fields_sorted_by_name; /** Number of elements in `field_ranges`. */ unsigned n_field_ranges; /** Used for looking up fields by id. */ const ProtobufCIntRange *field_ranges; /** Message initialisation function. */ ProtobufCMessageInit message_init; /** Reserved for future use. */ void *reserved1; /** Reserved for future use. */ void *reserved2; /** Reserved for future use. */ void *reserved3; }; ``` - magic:通常为0x28AAEEF9。 - n_fields:结构体中的字段数量。 - fields:指向一个储存字段和数据的结构体。 fields是ProtobufCFieldDescriptor类型。 继续分析ProtobufCFieldDescriptor - name:字段名。 - id:唯一字段编号。 - label:修饰符,如:required、optional、repeated。 - type:数据类型,如:bool、int32、float、double等。 以上是逆向的基础知识 下面以2023年的ciscn和2024年的ciscn来实战分析 2023 第一步先去找特征,在sub_5090函数里找到了以下特征 ![image-20241111203203292](C:\Users\70335\AppData\Roaming\Typora\typora-user-images\image-20241111203203292.png) ![image-20241111203225113](C:\Users\70335\AppData\Roaming\Typora\typora-user-images\image-20241111203225113.png) 很明显这是消息信息结构体descriptor 我们确定了是protobuf协议后 我们开始第一步逆向descriptor,在ida里去找magic字段为0x28AAEEF9 在data.rel.ro字段里发现了magic ![image-20241111203714788](C:\Users\70335\AppData\Roaming\Typora\typora-user-images\image-20241111203714788.png) 确定它的descriptor name:devicemsg magic为0x28AAEEF9 结构体size为0x40 字段数为4 接着分析ProtobufCFieldDescriptor结构体 也就是"actionid" ![image-20241111203933134](C:\Users\70335\AppData\Roaming\Typora\typora-user-images\image-20241111203933134.png) id:1 label:0 type: 4 enum表: lable enum: ```c typedef enum { /** A well-formed message must have exactly one of this field. */ PROTOBUF_C_LABEL_REQUIRED, /** * A well-formed message can have zero or one of this field (but not * more than one). */ PROTOBUF_C_LABEL_OPTIONAL, /** * This field can be repeated any number of times (including zero) in a * well-formed message. The order of the repeated values will be * preserved. */ PROTOBUF_C_LABEL_REPEATED, /** * This field has no label. This is valid only in proto3 and is * equivalent to OPTIONAL but no "has" quantifier will be consulted. */ PROTOBUF_C_LABEL_NONE, } ProtobufCLabel; ``` type enum表: ```c typedef enum { PROTOBUF_C_TYPE_INT32, /**< int32 */ PROTOBUF_C_TYPE_SINT32, /**< signed int32 */ PROTOBUF_C_TYPE_SFIXED32, /**< signed int32 (4 bytes) */ PROTOBUF_C_TYPE_INT64, /**< int64 */ PROTOBUF_C_TYPE_SINT64, /**< signed int64 */ PROTOBUF_C_TYPE_SFIXED64, /**< signed int64 (8 bytes) */ PROTOBUF_C_TYPE_UINT32, /**< unsigned int32 */ PROTOBUF_C_TYPE_FIXED32, /**< unsigned int32 (4 bytes) */ PROTOBUF_C_TYPE_UINT64, /**< unsigned int64 */ PROTOBUF_C_TYPE_FIXED64, /**< unsigned int64 (8 bytes) */ PROTOBUF_C_TYPE_FLOAT, /**< float */ PROTOBUF_C_TYPE_DOUBLE, /**< double */ PROTOBUF_C_TYPE_BOOL, /**< boolean */ PROTOBUF_C_TYPE_ENUM, /**< enumerated type */ PROTOBUF_C_TYPE_STRING, /**< UTF-8 or ASCII string */ PROTOBUF_C_TYPE_BYTES, /**< arbitrary byte sequence */ PROTOBUF_C_TYPE_MESSAGE, /**< nested message */ } ProtobufCType; ``` 发现 label是PROTOBUF_C_LABEL_REQUIRED type是PROTOBUF_C_TYPE_SINT64。这是actionid的类型, 然后msgidx也是如此 ![image-20241111204642308](C:\Users\70335\AppData\Roaming\Typora\typora-user-images\image-20241111204642308.png) 2 0 4 idx为2 label是PROTOBUF_C_LABEL_REQUIRED type是PROTOBUF_C_TYPE_SINT64 ![image-20241111204755653](C:\Users\70335\AppData\Roaming\Typora\typora-user-images\image-20241111204755653.png) 4 0 f idx为2 label是PROTOBUF_C_LABEL_REQUIRED type是bytes 自此我们就可以还原消息结构体了: ```c syntax = "proto2"; message devicemsg { required sint64 actionid = 1; required sint64 msgidx = 2; required sint64 msgsize = 3; required bytes msgcontent = 4; } ``` 然后进行编译为python protoc --python_out=. device.proto 生成device_pb2.py文件 在exp里进行交互: ```python from pwn import * import device_pb2.py elf=ELF("./pwn") p=process("pwn") def add(index,size,content): msg=device_pb2.devicemsg() msg.actionid=1 msg.msgidx=index msg.msgsize=size msg.msgcontent=content p.sendlineafter("now: ",msg.SerializeToString()) add() p.interactive() ``` 自此2023年的ciscn protobuf逆向完成 下面来看下2024年的ciscn国赛题 ![image-20241111205638841](C:\Users\70335\AppData\Roaming\Typora\typora-user-images\image-20241111205638841.png) 消息结构体名字为heybro ![image-20241111205716898](C:\Users\70335\AppData\Roaming\Typora\typora-user-images\image-20241111205716898.png) whatcon 字段 id=1 3 f required bytes 其它字段也是如此 还原结构体 ```c syntax "proto2" message heybro { required bytes whatcon = 1; required sint64 whattodo = 2; required sint64 whatidx = 3; required sint64 whatsize = 4; required uint32 whatsthis = 5; } ``` protoc --python_out=. device.proto ```python from pwn import * import device_pb2 def add_chunk(index, content): msg =Heybro_pb2.heybro() msg.whattodo = 1 msg.whatidx = index msg.whatsize = 0 msg.whatcon = content msg.whatsthis = 0 p.sendafter(b'WANT?\n', msg.SerializeToString()) ``` 总结:通过前面的逆向分析其实这种带有protobuf的并不可怕,只要按照常规操作给它逆向出来就行了,很简单