[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 消息定义指定了三个字段(名称/值对),每种你想包含在此类消息中的数据都对应一个字段。每个字段都有一个名称和一个类型。

定义字段

定义字段类型

e7fa3d288c2ad8c00cb4004f60bdcec0_MD5

这里有一些类型,总之就是整数,浮点数,布尔和字符串。
除了前面那个例子里定义的标量类型,也可以是枚举或者复合类型。
在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_aliastrue来关闭)。以及虽然所有别名值在序列化时都有效,但在反序列化时只使用第一个值。

删除枚举条目

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] */
};

程序输出结果:

98e8de5de4b86f3d0078042f42968d4f_MD5

rpc协议支持

service SearchService {
  rpc Search(SearchRequest) returns (SearchResponse);
}

rpc(Remote Produce Call)是一种通信协议,让你可以调用远程服务器上的函数,就如同调用本地的函数一样。protobuf支持这个,但是你需要自己提供rpc框架。

后面可以跟gRPC结合,详细放到新的文档写吧。

逆向

定位结构体,逆向通信协议,然后根据结构体把.proto文件重新写出来,然后用proto生成.py接口就行了。

这里有魔数可以定位。

1aa7578a02f006650c2fb20506019b5b_MD5

然后就是把类型一个一个套上去,根据宏的名字把.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

posted @ 2026-03-17 14:03  imiab  阅读(70)  评论(0)    收藏  举报