[pwn] protobuf学习与逆向
介绍
一个轻量的跨平台数据结构协议。
说人话就是json-pro,或者说zip-pro。zip不也是不论在手机、电脑、macos都能解压吗,,
当然运转起来和zip不同,毕竟这个是为了程序间收发信息而发明的,而zip只是为了打包数据,只是一个不太恰当的类比。
快速安装
sudo apt install protobuf-compiler libprotobuf-dev
protobuf支持c++、java、python、go等多种语言,但是不支持c语言。
https://github.com/protobuf-c/protobuf-c
不过这里有一个社区成员自发维护的proto-c实现,要支持c语言额外安装这个即可
(注意make之前先checkout到最新的release tag)。
基本语法
定义消息类型
syntax = "proto2";
message SearchRequest {
optional string query = 1;
optional int32 page_number = 2;
optional int32 results_per_page = 3;
}
-
文件的第一行指定您正在使用 protobuf 语言规范的 2023 版。
edition(或 proto2/proto3 的syntax)必须是文件的第一个非空、非注释行。- 如果未指定
edition或syntax,协议缓冲区编译器将假定您正在使用 proto2。
-
SearchRequest消息定义指定了三个字段(名称/值对),每种你想包含在此类消息中的数据都对应一个字段。每个字段都有一个名称和一个类型。
定义字段
定义字段类型

这里有一些类型,总之就是整数,浮点数,布尔和字符串。
除了前面那个例子里定义的标量类型,也可以是枚举或者复合类型。
在proto3里面这个有些许扩展,具体可以上官网查看。
分配字段编号
syntax = "proto2";
message SearchRequest {
optional string query = 1;
optional int32 page_number = 2;
optional int32 results_per_page = 3;
}
依然是这个例子,其中的=1和=2就是字段编号。每个字段编号必须唯一,范围是从0到2^29
指定字段基数
翻译成基数很奇怪,感觉应该类似字段属性的意思。
- 单一 (Singular):
-
optional:(推荐)optional字段处于两种可能状态之一 -
你可以用接口查询发送方有没有显式设置值,比如说接收到了默认值,但你不清楚发送方是没设置,还是设置了,刚好就是这个值。
-
建议使用
optional而不是隐式字段,以实现与protobuf版本和proto2的最大兼容性。 -
隐式:(不推荐)隐式字段没有明确的基数标签,其行为如下
-
如果字段是消息类型,则其行为与
optional字段完全相同。 -
如果字段不是消息,它有两种状态
-
字段设置为非默认(非零)值,该值已显式设置或从数据线解析。它将被序列化到数据线。字段设置为默认(零)值。它不会被序列化到数据线。事实上,您无法确定默认(零)值是已设置还是从数据线解析,还是未提供。有关此主题的更多信息,请参阅字段存在。
-
(翻译够怪的,总之参考上面
option的意思,这里和option相反)
-
repeated:此字段类型在一个格式良好的消息中可以重复零次或多次。重复值的顺序将被保留。map:这是一个键值对字段类型。有关此字段类型的更多信息,请参阅映射。
其实就是普通的optional,数组repeated,哈希表map
还有一个require被废弃了,官网上写不建议使用
字段默认值
- 对于字符串,默认值是空字符串。
- 对于字节,默认值是空字节。
- 对于布尔值,默认值是 false。
- 对于数值类型,默认值是零。
- 对于消息字段,该字段未设置。其确切值取决于语言。
- 对于枚举,默认值是第一个定义的枚举值,该值必须为 0。
当然你可以自己指定默认值。
syntax = "proto2";
message SearchRequest {
optional string query = 1;
optional int32 page_number = 2 [default = 10];
optional int32 results_per_page = 3;
}
如果发送方未指定 result_per_page,接收方将观察到以下状态
result_per_page字段不存在。也就是说,has_result_per_page()(存在器方法)方法将返回false。result_per_page的值(从“getter”返回)为10。
这里有点特别,虽然有值,但是查询这个值存不存在返回false,直接获取值又能获取到,感觉容易出小错误。
枚举类型
enum Corpus {
CORPUS_UNSPECIFIED = 0;
CORPUS_UNIVERSAL = 1;
CORPUS_WEB = 2;
CORPUS_IMAGES = 3;
CORPUS_LOCAL = 4;
CORPUS_NEWS = 5;
CORPUS_PRODUCTS = 6;
CORPUS_VIDEO = 7;
}
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
Corpus corpus = 4;
}
文档建议枚举类型的第一个值必须为0且不包含任何语义,因为兼容性问题。
像 SearchRequest.corpus 这样的枚举字段的默认值可以像这样显式覆盖
Corpus corpus = 4 [default = CORPUS_UNIVERSAL];
枚举也可以有别名,就是一个值有两个名字,像下面这样
enum EnumAllowingAlias {
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 1;
EAA_FINISHED = 2;
}
enum EnumNotAllowingAlias {
ENAA_UNSPECIFIED = 0;
ENAA_STARTED = 1;
ENAA_RUNNING = 1; // Uncommenting this line will cause a warning message.
ENAA_FINISHED = 2;
}
不过如果这么用的话,protocol buffer 编译器会生成一条警告消息(可设置allow_alias 为 true来关闭)。以及虽然所有别名值在序列化时都有效,但在反序列化时只使用第一个值。
删除枚举条目
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved FOO, BAR;
}
要用reserved。
比如说某项目里,字段3是AAA,一次更新以后字段3被删掉,但是没有reversed。过了一段时间另一个人要添加一个新字段,发现3空着的,就把3用给BBB。但是有其他模块没有及时更新到这个信息,结果就是本来接收BBB的接口会收到AAA,造成严重的问题。
用reversed把被删除的flag标记,这样如果后来的维护者重用了字段,编译就会报错。
proto序列化的不规范性
意思是对于同样的数据,proto不能保证其被序列化为相同的结果。
比如说哈希密钥验证等流程,不应该直接使用proto序列化出来的数据,因为它们可能随着版本更新发生变化。正确的姿势是反序列化提取其中的信息,然后再拿去用。
使用教程
参考了下这个
https://github.com/protobuf-c/protobuf-c/wiki/Examples
这里是c的使用教材,如果需要python或者C++可以区看官方文档
syntax = "proto2";
message msg {
optional uint32 id = 1;
optional string name = 2;
optional bytes key = 3;
}
写一个这个,然后输入protoc-c --c_out=. amessage.proto
接下来在自己的代码中引用生成的文件
#include "message.pb-c.h"
#include <stdio.h>
#include <stdlib.h>
int main() {
Msg msg = MSG__INIT;
msg.has_id = 1;
msg.id = 12345;
msg.name = "Hello, Protobuf-C!";
msg.has_key = 1;
uint8_t key_data[] = {0xDE, 0xAD, 0xBE, 0xEF};
msg.key.data = key_data;
msg.key.len = sizeof(key_data);
// Serialize the message
size_t packed_size = msg__get_packed_size(&msg);
uint8_t *buffer = malloc(packed_size);
if (!buffer) {
fprintf(stderr, "Failed to allocate memory for buffer\n");
return EXIT_FAILURE;
}
msg__pack(&msg, buffer);
// Print the serialized data
printf("Serialized Msg (hex): ");
for (size_t i = 0; i < packed_size; i++) {
printf("%02X ", buffer[i]);
}
printf("\n");
// Unpack the message
Msg *unpacked_msg = msg__unpack(NULL, packed_size, buffer);
if (!unpacked_msg) {
fprintf(stderr, "Failed to unpack message\n");
free(buffer);
return EXIT_FAILURE;
}
// Print the unpacked message
printf("Unpacked Msg:\n");
printf(" ID: %u\n", unpacked_msg->id);
printf(" Name: %s\n", unpacked_msg->name);
printf(" Key (hex): ");
for (size_t i = 0; i < unpacked_msg->key.len; i++) {
printf("%02X ", unpacked_msg->key.data[i]);
}
printf("\n");
msg__free_unpacked(unpacked_msg, NULL);
free(buffer);
return EXIT_SUCCESS;
}
pack和unpack,然后unpack可以传allocator,可以传自定义堆管理器,传null就是用malloc。
编译指令
gcc ./main.c ./message.pb-c.c -o ./main -lprotobuf-c
如果你用syntax = "proto3"会发现生成不了,因为神秘的“咕勾”在3.12版本把optional扬了,引发了设区的强烈反对,后来又加了回来。
而proto-c是社区人员维护的,最新的release版本还没对齐这个需求。
我们可以看看生成的文件里面有什么,其实也很简单。
/* Generated by the protocol buffer compiler. DO NOT EDIT! */
/* Generated from: message.proto */
/* Do not generate deprecated warnings for self */
#ifndef PROTOBUF_C__NO_DEPRECATED
#define PROTOBUF_C__NO_DEPRECATED
#endif
#include "message.pb-c.h"
void msg__init
(Msg *message)
{
static const Msg init_value = MSG__INIT;
*message = init_value;
}
size_t msg__get_packed_size
(const Msg *message)
{
assert(message->base.descriptor == &msg__descriptor);
return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message));
}
size_t msg__pack
(const Msg *message,
uint8_t *out)
{
assert(message->base.descriptor == &msg__descriptor);
return protobuf_c_message_pack ((const ProtobufCMessage*)message, out);
}
size_t msg__pack_to_buffer
(const Msg *message,
ProtobufCBuffer *buffer)
{
assert(message->base.descriptor == &msg__descriptor);
return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer);
}
Msg *
msg__unpack
(ProtobufCAllocator *allocator,
size_t len,
const uint8_t *data)
{
return (Msg *)
protobuf_c_message_unpack (&msg__descriptor,
allocator, len, data);
}
void msg__free_unpacked
(Msg *message,
ProtobufCAllocator *allocator)
{
if(!message)
return;
assert(message->base.descriptor == &msg__descriptor);
protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator);
}
static const ProtobufCFieldDescriptor msg__field_descriptors[3] =
{
{
"id",
1,
PROTOBUF_C_LABEL_OPTIONAL,
PROTOBUF_C_TYPE_UINT32,
offsetof(Msg, has_id),
offsetof(Msg, id),
NULL,
NULL,
0, /* flags */
0,NULL,NULL /* reserved1,reserved2, etc */
},
{
"name",
2,
PROTOBUF_C_LABEL_OPTIONAL,
PROTOBUF_C_TYPE_STRING,
0, /* quantifier_offset */
offsetof(Msg, name),
NULL,
NULL,
0, /* flags */
0,NULL,NULL /* reserved1,reserved2, etc */
},
{
"key",
3,
PROTOBUF_C_LABEL_OPTIONAL,
PROTOBUF_C_TYPE_BYTES,
offsetof(Msg, has_key),
offsetof(Msg, key),
NULL,
NULL,
0, /* flags */
0,NULL,NULL /* reserved1,reserved2, etc */
},
};
static const unsigned msg__field_indices_by_name[] = {
0, /* field[0] = id */
2, /* field[2] = key */
1, /* field[1] = name */
};
static const ProtobufCIntRange msg__number_ranges[1 + 1] =
{
{ 1, 0 },
{ 0, 3 }
};
const ProtobufCMessageDescriptor msg__descriptor =
{
PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC,
"msg",
"Msg",
"Msg",
"",
sizeof(Msg),
3,
msg__field_descriptors,
msg__field_indices_by_name,
1, msg__number_ranges,
(ProtobufCMessageInit) msg__init,
NULL,NULL,NULL /* reserved[123] */
};
程序输出结果:

rpc协议支持
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
rpc(Remote Produce Call)是一种通信协议,让你可以调用远程服务器上的函数,就如同调用本地的函数一样。protobuf支持这个,但是你需要自己提供rpc框架。
后面可以跟gRPC结合,详细放到新的文档写吧。
逆向
定位结构体,逆向通信协议,然后根据结构体把.proto文件重新写出来,然后用proto生成.py接口就行了。
这里有魔数可以定位。

然后就是把类型一个一个套上去,根据宏的名字把.proto反推就行了ovo
叽里咕噜说什么呢跟我结构体板子说去吧
右键parse statements全部导入,然后想要恢复哪个结构体自己填
typedef enum {
PROTOBUF_C_FIELD_FLAG_PACKED = (1 << 0),
PROTOBUF_C_FIELD_FLAG_DEPRECATED = (1 << 1),
PROTOBUF_C_FIELD_FLAG_ONEOF = (1 << 2),
} ProtobufCFieldFlag;
typedef enum {
PROTOBUF_C_LABEL_REQUIRED,
PROTOBUF_C_LABEL_OPTIONAL,
PROTOBUF_C_LABEL_REPEATED,
PROTOBUF_C_LABEL_NONE,
} ProtobufCLabel;
typedef enum {
PROTOBUF_C_TYPE_INT32,
PROTOBUF_C_TYPE_SINT32,
PROTOBUF_C_TYPE_SFIXED32,
PROTOBUF_C_TYPE_INT64,
PROTOBUF_C_TYPE_SINT64,
PROTOBUF_C_TYPE_SFIXED64,
PROTOBUF_C_TYPE_UINT32,
PROTOBUF_C_TYPE_FIXED32,
PROTOBUF_C_TYPE_UINT64,
PROTOBUF_C_TYPE_FIXED64,
PROTOBUF_C_TYPE_FLOAT,
PROTOBUF_C_TYPE_DOUBLE,
PROTOBUF_C_TYPE_BOOL,
PROTOBUF_C_TYPE_ENUM,
PROTOBUF_C_TYPE_STRING,
PROTOBUF_C_TYPE_BYTES,
PROTOBUF_C_TYPE_MESSAGE,
} ProtobufCType;
typedef enum {
PROTOBUF_C_WIRE_TYPE_VARINT = 0,
PROTOBUF_C_WIRE_TYPE_64BIT = 1,
PROTOBUF_C_WIRE_TYPE_LENGTH_PREFIXED = 2,
PROTOBUF_C_WIRE_TYPE_32BIT = 5,
} ProtobufCWireType;
struct ProtobufCAllocator;
struct ProtobufCBinaryData;
struct ProtobufCBuffer;
struct ProtobufCBufferSimple;
struct ProtobufCEnumDescriptor;
struct ProtobufCEnumValue;
struct ProtobufCEnumValueIndex;
struct ProtobufCFieldDescriptor;
struct ProtobufCIntRange;
struct ProtobufCMessage;
struct ProtobufCMessageDescriptor;
struct ProtobufCMessageUnknownField;
struct ProtobufCMethodDescriptor;
struct ProtobufCService;
struct ProtobufCServiceDescriptor;
typedef struct ProtobufCAllocator ProtobufCAllocator;
typedef struct ProtobufCBinaryData ProtobufCBinaryData;
typedef struct ProtobufCBuffer ProtobufCBuffer;
typedef struct ProtobufCBufferSimple ProtobufCBufferSimple;
typedef struct ProtobufCEnumDescriptor ProtobufCEnumDescriptor;
typedef struct ProtobufCEnumValue ProtobufCEnumValue;
typedef struct ProtobufCEnumValueIndex ProtobufCEnumValueIndex;
typedef struct ProtobufCFieldDescriptor ProtobufCFieldDescriptor;
typedef struct ProtobufCIntRange ProtobufCIntRange;
typedef struct ProtobufCMessage ProtobufCMessage;
typedef struct ProtobufCMessageDescriptor ProtobufCMessageDescriptor;
typedef struct ProtobufCMessageUnknownField ProtobufCMessageUnknownField;
typedef struct ProtobufCMethodDescriptor ProtobufCMethodDescriptor;
typedef struct ProtobufCService ProtobufCService;
typedef struct ProtobufCServiceDescriptor ProtobufCServiceDescriptor;
typedef int protobuf_c_boolean;
typedef void (*ProtobufCClosure)(const ProtobufCMessage *, void *closure_data);
typedef void (*ProtobufCMessageInit)(ProtobufCMessage *);
typedef void (*ProtobufCServiceDestroy)(ProtobufCService *);
struct ProtobufCAllocator {
void *(*alloc)(void *allocator_data, size_t size);
void (*free)(void *allocator_data, void *pointer);
void *allocator_data;
};
struct ProtobufCBinaryData {
size_t len;
uint8_t *data;
};
struct ProtobufCBuffer {
void (*append)(ProtobufCBuffer *buffer,
size_t len,
const uint8_t *data);
};
struct ProtobufCBufferSimple {
ProtobufCBuffer base;
size_t alloced;
size_t len;
uint8_t *data;
protobuf_c_boolean must_free_data;
ProtobufCAllocator *allocator;
};
struct ProtobufCEnumDescriptor {
uint32_t magic;
const char *name;
const char *short_name;
const char *c_name;
const char *package_name;
unsigned n_values;
const ProtobufCEnumValue *values;
unsigned n_value_names;
const ProtobufCEnumValueIndex *values_by_name;
unsigned n_value_ranges;
const ProtobufCIntRange *value_ranges;
void *reserved1;
void *reserved2;
void *reserved3;
void *reserved4;
};
struct ProtobufCEnumValue {
const char *name;
const char *c_name;
int value;
};
struct ProtobufCEnumValueIndex {
const char *name;
unsigned index;
};
struct ProtobufCFieldDescriptor {
const char *name;
uint32_t id;
ProtobufCLabel label;
ProtobufCType type;
unsigned quantifier_offset;
unsigned offset;
const void *descriptor;
const void *default_value;
uint32_t flags;
unsigned reserved_flags;
void *reserved2;
void *reserved3;
};
struct ProtobufCIntRange {
int start_value;
unsigned orig_index;
};
struct ProtobufCMessage {
const ProtobufCMessageDescriptor *descriptor;
unsigned n_unknown_fields;
ProtobufCMessageUnknownField *unknown_fields;
};
struct ProtobufCMessageDescriptor {
uint32_t magic;
const char *name;
const char *short_name;
const char *c_name;
const char *package_name;
size_t sizeof_message;
unsigned n_fields;
const ProtobufCFieldDescriptor *fields;
const unsigned *fields_sorted_by_name;
unsigned n_field_ranges;
const ProtobufCIntRange *field_ranges;
ProtobufCMessageInit message_init;
void *reserved1;
void *reserved2;
void *reserved3;
};
struct ProtobufCMessageUnknownField {
uint32_t tag;
ProtobufCWireType wire_type;
size_t len;
uint8_t *data;
};
struct ProtobufCMethodDescriptor {
const char *name;
const ProtobufCMessageDescriptor *input;
const ProtobufCMessageDescriptor *output;
};
struct ProtobufCService {
const ProtobufCServiceDescriptor *descriptor;
void (*invoke)(ProtobufCService *service,
unsigned method_index,
const ProtobufCMessage *input,
ProtobufCClosure closure,
void *closure_data);
void (*destroy)(ProtobufCService *service);
};
struct ProtobufCServiceDescriptor {
uint32_t magic;
const char *name;
const char *short_name;
const char *c_name;
const char *package;
unsigned n_methods;
const ProtobufCMethodDescriptor *methods;
const unsigned *method_indices_by_name;
};
叽里咕噜说什么呢跟我自动化脚本说去吧
有pbtk,实测识别不太出来
也有auto-re,实测太久没人维护(6years ago)
// todo

浙公网安备 33010602011771号