使用Syzygy进行二进制重写,第一部分
引言
二进制插桩与分析一直是我非常着迷的主题。无论是通过clang在编译时进行,还是使用Pin或DynamoRIO等动态二进制插桩框架在运行时进行。但我一直寻找的是一个能够静态插桩PE映像的框架,一个设计类似clang的框架,可以编写各种“过程”(passes)来实现不同的功能:映像转换、代码块分析等。直到几个月前,我还不知道有任何公开且健壮的项目提供这种能力(即能够插桩像Chrome这样的大型现实世界程序)。
在这篇文章中(我知道已经很久没更新了!),我将介绍syzygy转换工具链,重点介绍其插桩器,并概述该框架的功能、限制以及如何自己编写转换过程。作为示例,我将通过两个简单的例子:一个生成调用图的分析过程,以及一个重写受/GS保护二进制中__report_gsfailure
函数的转换过程。
目录
- 引言
- Syzygy
- 调试会话
- CallGraphAnalysis
- SecurityCookieCheckHookTransform
- 最后的话
Syzygy
介绍与历史
syzygy是Google编写的一个标记为“转换工具链”的项目。它包含一系列各种实用程序:instrument.exe
是调用各种转换过程并将其应用于二进制文件的应用程序,grinder.exe
、reorder.exe
等。简而言之,该框架能够(非详尽列表):
- 读取和写入PDB文件,
- “分解”使用MSVC构建的PE32二进制文件(借助完整的PDB符号),
- 汇编Intel x86 32位代码,
- 反汇编Intel x86 32位代码(通过Distorm),
- “重新链接”插桩后的二进制文件。
你可能在2013年5月的Chromium博客文章中简要听说过这个项目:Testing Chromium: SyzyASAN, a lightweight heap error detector。如你所知,AddressSanitizer是一种编译时插桩工具,旨在检测C/C++程序中的内存错误。简而言之,AddressSanitizer跟踪程序内存的状态,并在运行时插桩内存操作(读取/写入/堆分配/堆释放)以确保它们“安全”。例如,在正常情况下,在静态大小的堆栈缓冲区上读取越界一个字节很可能不会导致崩溃。AddressSanitizer的工作是检测此问题并向用户报告。
目前在Windows平台上没有真正的等效技术。唯一可用的有助于检测内存错误的技术是Page Heap。尽管今天clang for Windows正在工作(Chrome宣布Windows版本的Chrome现在使用clang),但在2013年情况并非如此。因此,Google构建了SyzyASAN,这是一个旨在检测PE32二进制文件中内存错误的转换过程的名称。此转换构建在syzygy框架之上,你可以通过instrument.exe
工具使用它插桩二进制文件。上述的一个结果是,框架必须足够健壮和准确以插桩Chrome;因此代码经过大量测试,这对我们来说非常棒(它几乎也是唯一可用的文档0:-))!
编译
为了设置开发环境,你需要遵循特定步骤来安装所有chromium构建/开发工具。depot_tools
是包含正确构建各种chromium项目所需一切内容的包;它包括Python、GYP、Ninja、git等。
安装depot_tools
后,只需执行以下命令来获取代码并编译它:
1
2
3
4
5
6
|
> set PATH=D:\Codes\depot_tools;%PATH%
> mkdir syzygy
> cd syzygy
> fetch syzygy
> cd syzygy\src
> ninja -C out\Release instrument
|
如果你想了解更多信息,建议阅读此wiki页面:SyzygyDevelopmentGuide。
术语
项目中使用的术语起初可能有些误导或混淆,因此现在是描述关键术语及其含义的好时机:BlockGraph
基本上是一个块的容器。BlockGraph::Block
可以是代码块或数据块(如IMAGE_NT_HEADERS
)。每个块都有各种属性,如标识符、名称等,并且属于一个部分(如PE部分)。这些属性大多是可变的,你可以自由使用它们,它们将在重新链接输出映像时被后端拾取。除了作为块的高级容器外,BlockGraph
还跟踪可执行文件中的部分。块还有引用者和引用的概念。引用基本上是从块foo到块bar的链接;其中bar是被引用者。引用者可以看作交叉引用(在IDA的意义上):foo将是bar的引用者。这两个关键概念在构建转换时非常重要,因为它们还允许你更快地遍历图。例如,将引用者转移到另一个块也是非常容易的操作(并且非常强大)。
起初让我困惑的是,它们的块名称并不是我们所知的基本块。相反,它是一个函数;一组基本块。另一个使用的关键概念称为SourceRanges
。由于块可以组合或拆分,它们被设计为照顾自己的地址空间映射,从原始映像的字节到块中的字节。
最后,我们所知的基本块容器是BasicBlockSubGraph
(我稍后在文章中简要提到)。
哦,最后一件事:插桩器基本上是分解输入二进制文件(可比作前端)的应用程序,将解构的二进制文件(函数、块、指令)呈现给转换过程(可比作中端)进行修改,最后是后端部分重建你的插桩二进制文件。
调试会话
为了更清楚 - 因为我喜欢调试会话 - 我认为花一些时间在调试器中实际查看各种结构以及它们如何映射到我们知道的代码是值得的。让我们采用以下C程序并在调试模式下编译(不要忘记使用以下链接器标志启用完整PDB生成:/PROFILE
):
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#include <stdio.h>
void foo(int x) {
for(int i = 0; i < x; ++i) {
printf("Binary rewriting with syzygy\n");
}
}
int main(int argc, char *argv[]) {
printf("Hello doar-e.\n");
foo(argc);
return 0;
}
|
使用以下命令将其扔到你最喜欢的调试器中 - 我们将使用afl转换作为示例转换来分析我们可用的数据:
1
|
instrument.exe --mode=afl --input-image=test.exe --output-image=test.instr.exe
|
并设置此断点:
1
|
bm instrument!*AFLTransform::OnBlock ".if(@@c++(block->type_ == 0)){ }.else{ g }"
|
现在是时候检查与上面函数foo
关联的块了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
0:000> g
eax=002dcf80 ebx=00000051 ecx=00482da8 edx=004eaba0 esi=004bd398 edi=004bd318
eip=002dcf80 esp=0113f4b8 ebp=0113f4c8 iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
instrument!instrument::transforms::AFLTransform::OnBlock:
002dcf80 55 push ebp
0:000> dx block
[+0x000] id_ : 0x51
[+0x004] type_ : CODE_BLOCK (0)
[+0x008] size_ : 0x5b
[+0x00c] alignment_ : 0x1
[+0x010] alignment_offset_ : 0
[+0x014] padding_before_ : 0x0
[+0x018] name_ : 0x4ffc70 : "foo"
[+0x01c] compiland_name_ : 0x4c50b0 : "D:\tmp\test\Debug\main.obj"
[+0x020] addr_ [Type: core::detail::AddressImpl<0>]
[+0x024] block_graph_ : 0x48d10c
[+0x028] section_ : 0x0
[+0x02c] attributes_ : 0x8
[+0x030] references_ : { size=0x3 }
[+0x038] referrers_ : { size=0x1 }
[+0x040] source_ranges_ [Type: core::AddressRangeMap<core::AddressRange<int,unsigned int>,core::AddressRange<core::detail::AddressImpl<0>,unsigned int> >]
[+0x04c] labels_ : { size=0x3 }
[+0x054] owns_data_ : false
[+0x058] data_ : 0x49ef50 : 0x55
[+0x05c] data_size_ : 0x5b
|
以上显示了块中可用的不同属性;我们可以看到它名为foo
,具有标识符0x51
,大小为0x5B
字节。
它还有一个引用者和3个引用,它们可能是什么?根据我上面的解释,我们可以猜测引用者(或交叉引用)必须是主函数,因为它调用了foo
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
0:000> dx -r1 (*((instrument!std::pair<block_graph::BlockGraph::Block *,int> *)0x4f87c0))
first : 0x4bd3ac
second : 48
0:000> dx -r1 (*((instrument!block_graph::BlockGraph::Block *)0x4bd3ac))
[+0x000] id_ : 0x52
[+0x004] type_ : CODE_BLOCK (0)
[+0x008] size_ : 0x4d
[+0x00c] alignment_ : 0x1
[+0x010] alignment_offset_ : 0
[+0x014] padding_before_ : 0x0
[+0x018] name_ : 0x4c51a0 : "main"
[+0x01c] compiland_name_ : 0x4c50b0 : "D:\tmp\test\Debug\main.obj"
[+0x020] addr_ [Type: core::detail::AddressImpl<0>]
[+0x024] block_graph_ : 0x48d10c
[+0x028] section_ : 0x0
[+0x02c] attributes_ : 0x8
[+0x030] references_ : { size=0x4 }
[+0x038] referrers_ : { size=0x1 }
[+0x040] source_ranges_ [Type: core::AddressRangeMap<core::AddressRange<int,unsigned int>,core::AddressRange<core::detail::AddressImpl<0>,unsigned int> >]
[+0x04c] labels_ : { size=0x3 }
[+0x054] owns_data_ : false
[+0x058] data_ : 0x49efb0 : 0x55
[+0x05c] data_size_ : 0x4d
|
关于引用要记住的一点是,它们不仅仅是指向块的指针。引用确实引用了一个块(废话),但它还有一个与此块关联的偏移量,以精确指向数据被引用的位置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
// Represents a reference from one block to another. References may be offset.
// That is, they may refer to an object at a given location, but actually point
// to a location that is some fixed distance away from that object. This allows,
// for example, non-zero based indexing into a table. The object that is
// intended to be dereferenced is called the 'base' of the offset.
//
// BlockGraph references are from a location (offset) in one block, to some
// location in another block. The referenced block itself plays the role of the
// 'base' of the reference, with the offset of the reference being stored as
// an integer from the beginning of the block. However, basic block
// decomposition requires breaking the block into smaller pieces and thus we
// need to carry around an explicit base value, indicating which byte in the
// block is intended to be referenced.
//
// A direct reference to a location will have the same value for 'base' and
// 'offset'.
//
// Here is an example:
//
// /----------\
// +---------------------------+
// O | B | <--- Referenced block
// +---------------------------+ B = base
// \-----/ O = offset
//
|
现在让我们看看与foo
块关联的引用。如果你仔细看块,引用集的大小是3…它们可能是什么?
一个用于printf
函数,一个用于传递给printf
的字符串的数据块?
第一个引用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
0:000> dx -r1 (*((instrument!std::pair<int const ,block_graph::BlockGraph::Reference> *)0x4f5640))
first : 57
second [Type: block_graph::BlockGraph::Reference]
0:000> dx -r1 (*((instrument!block_graph::BlockGraph::Reference *)0x4f5644))
[+0x000] type_ : ABSOLUTE_REF (1) [Type: block_graph::BlockGraph::ReferenceType]
[+0x004] size_ : 0x4
[+0x008] referenced_ : 0x4ce334
[+0x00c] offset_ : 0
[+0x010] base_ : 0
0:000> dx -r1 (*((instrument!block_graph::BlockGraph::Block *)0x4ce334))
[+0x000] id_ : 0xbc
[+0x004] type_ : DATA_BLOCK (1)
[...]
[+0x018] name_ : 0xbb90f8 : "??_C@_0BO@LBGMPKED@Binary?5rewriting?5with?5syzygy?6?$AA@"
[+0x01c] compiland_name_ : 0x4c50b0 : "D:\tmp\test\Debug\main.obj"
[...]
[+0x058] data_ : 0x4a11e0 : 0x42
[+0x05c] data_size_ : 0x1e
0:000> da 0x4a11e0
004a11e0 "Binary rewriting with syzygy."
|
第二个引用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
0:000> dx -r1 (*((instrument!std::pair<int const ,block_graph::BlockGraph::Reference> *)0x4f56a0))
first : 62
second [Type: block_graph::BlockGraph::Reference]
0:000> dx -r1 (*((instrument!block_graph::BlockGraph::Reference *)0x4f56a4))
[+0x000] type_ : PC_RELATIVE_REF (0) [Type: block_graph::BlockGraph::ReferenceType]
[+0x004] size_ : 0x4
[+0x008] referenced_ : 0x4bd42c
[+0x00c] offset_ : 0
[+0x010] base_ : 0
0:000> dx -r1 (*((instrument!block_graph::BlockGraph::Block *)0x4bd42c))
[+0x000] id_ : 0x53
[+0x004] type_ : CODE_BLOCK (0)
[...]
[+0x018] name_ : 0x4ffd60 : "printf"
[+0x01c] compiland_name_ : 0x4c50b0 : "D:\tmp\test\Debug\main.obj"
[...]
|
第三个引用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
0:000> dx -r1 (*((instrument!std::pair<int const ,block_graph::BlockGraph::Reference> *)0x4f5a90))
first : 83
second [Type: block_graph::BlockGraph::Reference]
0:000> dx -r1 (*((instrument!block_graph::BlockGraph::Reference *)0x4f5a94))
[+0x000] type_ : PC_RELATIVE_REF (0) [Type: block_graph::BlockGraph::ReferenceType]
[+0x004] size_ : 0x4
[+0x008] referenced_ : 0x4bd52c
[+0x00c] offset_ : 0
[+0x010] base_ : 0
0:000> dx -r1 (*((instrument!block_graph::BlockGraph::Block *)0x4bd52c))
[+0x000] id_ : 0x54
[+0x004] type_ : CODE_BLOCK (0)
[...]
[+0x018] name_ : 0xbb96c8 : "_RTC_CheckEsp"
[+0x01c] compiland_name_ : 0x4c5260 : "f:\binaries\Intermediate\vctools\msvcrt.nativeproj_607447030\objd\x86\_stack_.obj"
[...]
|
完美 - 这和我们猜测的差不多!最后一个是编译器添加的运行时错误检查。
让我们仔细看看第一个引用。references_
成员是一个偏移量和引用实例的哈希表。
1
2
|
// Map of references that this block makes to other blocks.
typedef std::map<Offset, Reference> ReferenceMap;
|
偏移量告诉你块中确切的位置有引用;在我们的例子中,我们可以看到第一个引用在距块基址偏移57处。如果你快速启动IDA并浏览此地址,你会看到它指向PUSH操作码