# 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函数里找到了以下特征


很明显这是消息信息结构体descriptor
我们确定了是protobuf协议后
我们开始第一步逆向descriptor,在ida里去找magic字段为0x28AAEEF9
在data.rel.ro字段里发现了magic

确定它的descriptor
name:devicemsg
magic为0x28AAEEF9
结构体size为0x40
字段数为4
接着分析ProtobufCFieldDescriptor结构体
也就是"actionid"

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也是如此

2 0 4
idx为2 label是PROTOBUF_C_LABEL_REQUIRED type是PROTOBUF_C_TYPE_SINT64

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国赛题

消息结构体名字为heybro

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的并不可怕,只要按照常规操作给它逆向出来就行了,很简单