1.2 符号和剥离的二进制文件
高级源代码(如C代码)均以有意义的、人类可读的函数和变量命名为中心。编译程序时,编译器会翻译符号,这些符号会跟踪其名称,并记录哪些二进制代码和数据对应哪个符号。如函数符号提供符号从高级函数名称到第一个地址和每个函数的大小的映射。链接器在组合对象文件时通常使用此信息,例如,使用此信息来解析模块之间的函数和变量引用,并且帮助调试。
1.2.1 查看符号信息
为了让你了解符号信息,清单1-6显示了示例二进制文件中的一些符号。
清单1-6 readelf输出的a.out二进制文件中的符号
$ ❶readelf --syms a.out
Symbol table '.dynsym' contains 4 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
Symbol table '.symtab' contains 67 entries:
Num: Value Size Type Bind Vis Ndx Name
...
56: 0000000000601030 0 OBJECT GLOBAL HIDDEN 25 __dso_handle
57: 00000000004005d0 4 OBJECT GLOBAL DEFAULT 16 _IO_stdin_used
58: 0000000000400550 101 FUNC GLOBAL DEFAULT 14 __libc_csu_init
59: 0000000000601040 0 NOTYPE GLOBAL DEFAULT 26 _end
60: 0000000000400430 42 FUNC GLOBAL DEFAULT 14 _start
61: 0000000000601038 0 NOTYPE GLOBAL DEFAULT 26 __bss_start
62: 0000000000400526 32 FUNC GLOBAL DEFAULT 14 ❷main
63: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses
64: 0000000000601038 0 OBJECT GLOBAL HIDDEN 25 __TMC_END__
65: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
66: 00000000004003c8 0 FUNC GLOBAL DEFAULT 11 _init
在清单 1-6 中,我用到了 readelf
来显示符号❶。你将会在第 5 章中用到readelf
实用程序并解释其所有输出。现在,请注意,在许多不熟悉的符号中,main
函数❷有一个符号。你可以看到它指定了当二进制文件加载到内存时main
将驻留的地址(0x400526
)。输出还显示main
的代码大小(32字节),并指出你正在处理一个函数符号(类型为FUNC)。
符号信息可以作为二进制文件的一部分(正如你刚才看到的那样),或者以单独的符号文件形式转译,它有各种风格。链接器只需要基本符号,但为了调试,可以转译出更广泛的信息。调试符号提供了源代码行和二进制指令之间的完整映射关系,甚至描述了函数的参数、堆栈帧信息等。对于ELF二进制文件,调试符号通常以DWARF格式[4]生成,而PE二进制文件通常使用专有的Microsoft可移植调试(如PDB)格式。DWARF信息通常嵌在二进制文件中,而PDB则以单独的符号文件的形式存在。
正如你想象的那样,符号信息对于二进制分析非常有用。一组定义良好的函数符号可以使反汇编更加容易,这是因为可以将每个函数符号作为反汇编的起点。这样可以减少意外将数据反汇编为代码(这会导致在反汇编输出中出现伪指令)的可能性。知道二进制文件的哪些部分属于哪个函数以及调用了什么函数,使得逆向工程师更容易划分和理解代码在做什么。在许多二进制分析应用程序中,即使是基本的链接器符号(与更广泛的调试信息相对照)也已经是巨大的帮助。
如上所述,可以使用readelf
解析符号,也可以使用像libbfd
这样的库以编程方式解析符号,这些内容将在第4章中进行解释。还有诸如libdwarf
之类的库,这些库是专门为解析DWARF调试符号而设计的,但不会在本书中进行介绍。
遗憾的是,release版本的二进制文件通常不包含大量的调试信息,甚至经常剥离基本的符号信息以减小文件大小并防止进行逆向工程,尤其是在恶意软件或专有软件的情况下。这意味着,作为从事二进制分析的工作人员,通常必须处理更具挑战性的问题即在没有任何符号信息的情况下剥离二进制文件。除非另有说明,否则本书的所有内容都是在符号信息尽可能少的剥离后的二进制文件上进行分析。
1.2.2 剥离二进制文件
你可能还记得示例中的二进制文件尚未被剥离,如清单1-5中file
实用程序输出所示。显然,GCC的默认行为是不自动剥离新编译的二进制文件。如果你想知道带符号的二进制文件最终是如何被剥离的,可以使用strip
命令,如清单1-7所示。
清单1-7 剥离二进制文件
$ ❶strip --strip-all a.out
$ file a.out
a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically
linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32,
BuildID[sha1]=d0e23ea731bce9de65619cadd58b14ecd8c015c7, ❷stripped
$ readelf --syms a.out
❸ Symbol table '.dynsym' contains 4 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
如清单1-7所示,示例二进制文件现在已被剥离❶,如file输出❷所确认的那样。在.dynsym
符号表❸中只剩下少量符号。当二进制文件加载到内存中时,这些符号用于解决动态依赖关系,如对动态库的引用,但在反汇编时这些符号并没有太大的用处。所有其他的符号,包括你在清单 1-6 中看到的主函数的符号都已经消失了。