Flatbuffers 简介
FlatBuffers 是一个开源的、跨平台的、高效的、提供了多种语言接口的序列化工具库,实现了与 Protocal Buffers 类似的序列化格式
FlatBuffers 通过 Scheme 文件定义数据结构,在官方网站 FlatBuffers: Tutorial 中可以找到如下的测试案例:
1 | namespace MyGame.Sample; |
- Table:每个字段都有一个名称,一个类型和一个可选的默认值(默认为 0 / NULL)
- Structs:只包含标量或其他结构(没有默认值,其字段也不会被添加或者弃用)
- Types:包括标量类型和非标量类型
- 标量类型的字段有默认值
- 非标量的字段
(string/vector/table/enums/structs)
如果没有值的话,默认值为 NULL
- Enums:定义一系列命名常量,默认的第一个值是 “0”
- Unions:一个 Unions 中可以放置多种类型,共同使用一个内存区域
- 可以声明一个 Unions 字段,该字段可以包含对这些类型中的任何一个的引用,即这块内存区域只能由其中一种类型使用
- 另外还会生成一个带有后缀
_type
的隐藏字段,该字段包含相应的枚举值,从而可以在运行时知道要将哪些类型转换为类型
- Root Type:声明了序列化数据的根表
参考:深入浅出 FlatBuffers 之 Schema (halfrost.com)
Flatbuffers Table
Table 中每个字段都是可选 optional 的,这种机制可以令 Flatbuffers 前向和后向兼容
假设当前 schema 如下:
1 | table { a:int; b:int; } |
在后添加字段:
1 | table { a:int; b:int; c:int; } |
- 旧的 schema 读取新的数据结构会忽略新字段 c 的存在(不会浪费存储空间),新的 schema 读取旧的数据,将会取到 c 的默认值
在前添加字段:
1 | table { c:int a:int; b:int; } |
- 在前面添加新字段是不允许的,因为这会使 schema 新旧版本不兼容
指定字段 ID 序列:
1 | table { c:int (id: 2); a:int (id: 0); b:int (id: 1); } |
- 可以手动指定 ID 排序/分组,只要顺序与旧版本相同也是可以兼容的
删除字段:
1 | table { a:int (deprecated); b:int; } |
- 不能直接从 schema 中删除字段(否则会破坏源代码),可以将需要删除的字段标记为 deprecated
- 旧的 schema 读取新的数据结构会获得 a 的默认值(因为它不存在)
- 新的 schema 代码不能读取也不能写入 a,它们将忽略该字段
更改字段:
1 | table { a:uint; b:uint; } |
- 如果旧数据不包含任何负数,这将是安全的,如果包含了负数,这样改变会出现问题
- 不能修改字段名称与字段默认值(否则会破坏所有使用此版本 schema 的代码)
Flatbuffers Structs
Structs 只包含标量或其他结构,只要确定以后就不会进行任何更改
Structs 使用的内存少于 Table,并且访问速度更快,它们总是以串联方式存储在其父对象中,并且不使用虚表
Structs 不提供前向/后向兼容性,占用内存更小
Flatbuffers 内存布局
测试样例:
1 | namespace Test.Sample; |
1 |
|
在 CreateMonster
处打断点,然后 GDB 调试打印数据如下:
1 | 02:0010│ 0x5555555702b0 ◂— 0x9c00000060 /* '`' */ |
- 首先在 Flatbuffers 的缓冲区中,数据是倒序排列的
- 从下往上看:在字符串之后,有4字节的
\x00
用于分割 data 和 size,又有4字节的空间用于指示字符串的大小 - 位于缓冲区最上方的控制信息是实时更新的
Flatbuffers 逆向分析
下面先展示一个 IDA 逆向分析出来的伪代码:
1 | v3 = flatbuffers::AlignOf<unsigned long>(); |
这里我们重点分析反序列化的部分:
1 | const flatbuffers::String *__cdecl Test::Sample::Monster::name(const Test::Sample::Monster *const this) |
程序的反序列化会大量使用 flatbuffers::Table::GetXXXX
的函数模板,从这些函数中可以看出一些 Scheme 文件的信息
1 | const flatbuffers::String *__cdecl Test::Sample::Monster::name(const Test::Sample::Monster *const this) |
- 第二个参数的数字可以体现该条目在 Scheme 文件中的位置
1 | int32_t __cdecl Test::Sample::Weapon::value(const Test::Sample::Weapon *const this) |
- 从传参个数可以判断该条目是否为标量类型(有3个参数即是标量类型,第3个参数为其默认值)
在逆向分析 flatbuffers 的过程中,Verifier 可以暴露大量信息(Verifier 是 flatbuffers 的检查器),它的内部结构如下:
1 | if ( !flatbuffers::Verifier::Check(this, this->size_ > 0xB) ) |
最后一个函数 Verify 可以泄露有关 Scheme 的信息:
1 | if ( flatbuffers::Table::VerifyTableStart(this, verifier) |
- VerifyString 用于分析 string 类型
- VectorVerifyVectorOfTables 用于分析 Vector 类型
1 | bool __cdecl Test::Sample::Weapon::Verify(const Test::Sample::Weapon *const this, flatbuffers::Verifier *verifier) |
- VerifyField 的第4个参数表示该标量类型条目的大小