返回介绍

2.2 摘要工具

发布于 2024-10-11 21:05:40 字数 9674 浏览 0 评论 0 收藏 0

由于我们的目标是对二进制程序文件进行逆向工程,因此,在对文件进行初步分类后,需要用更高级的工具来提取详尽的信息。本节讨论的工具不仅能识别它们所处理的文件的格式,更重要的是还能够理解某一特定的文件格式,并且能够解析它们的输入文件,提取出这些输入文件所包含的非常特别的信息。

2.2.1 nm

将源文件编译成目标文件时,编译器必须嵌入一些全局(外部)符号的位置信息,以便链接器在组合目标文件以创建可执行文件时,能够解析对这些符号的引用。除非被告知要去除最终的可执行文件中的符号,否则,链接器通常会将目标文件中的符号带入最终的可执行文件中。根据 nm 手册的描述,这一实用工具的作用是“列举目标文件中的符号”。

使用 nm 检查中间目标文件(扩展名为.o 的文件,而非可执行文件)时,默认输出结果是在这个文件中声明的任何函数和全局变量的名称。 nm 实用工具的样本输出如下所示。

idabook#    gcc -c ch2_example.c 

 
idabook#    nm ch2_example.o 


          U __stderrp
          U exit
          U fprintf
00000038  T get_max
00000000  t hidden
00000088  T main
00000000  D my_initialized_global
00000004  C my_unitialized_global
          U printf
          U rand
          U scanf
          U srand
          U time
00000010  T usage
idabook#

从中可以看到, nm 列出了每一个符号以及与符号有关的一些信息。其中的字母表示所列举的符号的类型。前面的例子中出现了以下字母,下面逐一解释。

U ,未定义符号,通常为外部符号引用。

T ,在文本部分定义的符号,通常为函数名称。

t ,在文本部分定义的局部符号。在 C 程序中,这个符号通常等同于一个静态函数。

D ,已初始化的数据值。

C ,未初始化的数据值。

说明  大写字母表示全局符号,小写字母则表示局部符号。请参阅 nm 手册了解有关字母代码的详细解释。

如果使用 nm 列举可执行文件中的符号,将会有更多信息显示出来。在链接过程中,符号被解析成虚拟地址(如有可能)。因此,这时运行 nm ,将可获得更多信息。下面是使用 nm 处理一个可执行文件所得到的部分输出。

idabook#  gcc -o ch2_example ch2_example.c
idabook#  nm ch2_example
          <. . .>
          U exit
          U fprintf
080485c0  t frame_dummy
08048644  T get_max
0804860c  t hidden
08048694  T main
0804997c  D my_initialized_global
08049a9c  B my_unitialized_global
08049a80  b object.2
08049978  d p.0
          U printf
          U rand
          U scanf
          U srand
          U time
0804861c  T usage
idabook#

在这个例子中,一些符号(如 main )被分配了虚拟地址,链接过程引入了一些新的符号(如 frame_dummy ),另一些符号(如 my_unitialized_global )的类型发生了改变,其他符号由于继续引用外部符号,仍旧为未定义符号。在这个例子中,我们检测的文件属于动态链接二进制文件,为此,未定义的符号将在 C 语言共享库中定义。欲了解更多有关 nm 的信息,请参阅 nm 手册。

2.2.2 ldd

创建可执行文件时,必须解析该文件引用的任何库函数的地址。链接器通过两种方法解析对库函数的调用: 静态链接 (static linking)和 动态链接 (dynamic linking )。链接器的命令行参数决定具体使用哪一种方法。一个可执行文件可能为静态链接、动态链接,或二者兼而有之1

1. 欲了解更多有关链接的信息,请参阅 John Levine 的著作 Linkers and Loaders (San Francisco:Morgan Kaufmann,2000 )。

如果要求使用静态链接,链接器会将应用程序的目标文件和所需的库文件组合起来,生成一个可执行文件。这样,在运行时就不需要确定库代码的位置,因为它已经包含在可执行文件中了。静态链接的优点包括:函数调用更快一些;发布二进制文件更加容易,因为这时不需要对用户系统中库函数的可用性做出任何假设。其缺点包括:生成的可执行文件更大;如果库组件发生改变,对程序进行升级会更加困难,因为一旦库发生变化,程序就必须重新链接。从逆向工程的角度看,静态链接使问题更加复杂。在分析一个静态链接二进制文件时,要回答“这个二进制文件链接了哪些库”和“这些函数中哪一个是二进制函数”可不那么容易。我们将在第 12 章讨论在对静态链接代码进行逆向工程时遇到的挑战。

动态链接与静态链接不同。使用动态链接时,链接器不需要复制它需要的任何库。相反,链接器只需将对所需库(通常为.so 或.dll 文件)的引用插入到最终的可执行文件中。因此,这时生成的可执行文件也更小一些。而且,使用动态链接时升级库代码也变得简单多了,因为只需要维护一个库(被许多二进制文件引用),如果需要升级库代码,用新版本的库替换过时的库,就可以立即更新每一个引用该库的二进制文件。使用动态链接的一个缺点在于,它需要更加复杂的加载过程。因为这时必须定位所有所需的库,并将其加载到内存中,而不是加载一个包含全部库代码的静态链接文件。动态链接的另一个缺点是,供应商不仅需要发布他们自己的可执行文件,而且必须发布该文件所需的所有库文件。如果一个系统无法提供程序所需的全部库文件,在这个系统上运行该程序将会导致错误。

下面的输出说明了一个程序的动态和静态链接版本的创建过程、生成的二进制文件的大小,以及如何使用 file 工具识别这两个二进制文件。

idabook#  gcc -o ch2_example_dynamic ch2_example.c  
idabook#  gcc -o ch2_example_static ch2_example.c –static  
idabook#  ls -l ch2_example_*  
-rwxr-xr-x  1 root  wheel    6017 Sep 26 11:24 ch2_example_dynamic  
-rwxr-xr-x  1 root  wheel  167987 Sep 26 11:23 ch2_example_static  
idabook#  file ch2_example_*  
ch2_example_dynamic: ELF 32-bit LSB executable, Intel 80386, version 1  
        (FreeBSD), dynamically linked (uses shared libs), not stripped  
ch2_example_static:  ELF 32-bit LSB executable, Intel 80386, version 1  
        (FreeBSD), statically linked, not stripped  
idabook#  

为了确保动态链接正常运行,动态链接二进制文件必须指明它需要的库文件,以及需要这些文件中的哪些特定资源。因此,与静态链接二进制文件不同,我们可轻易确定一个动态链接二进制文件所依赖的库文件。 ldd (list dynamic dependencies )是一个简单的实用工具,可用来列举任何可执行文件所需的动态库。在下面这个例子中,我们使用 ldd 确定 Apache Web 服务器所依赖的库。

idabook#  ldd /usr/local/sbin/httpd
/usr/local/sbin/httpd:
        libm.so.4 => /lib/libm.so.4 (0x280c5000)
        libaprutil-1.so.2 => /usr/local/lib/libaprutil-1.so.2 (0x280db000)
        libexpat.so.6 => /usr/local/lib/libexpat.so.6 (0x280ef000)
        libiconv.so.3 => /usr/local/lib/libiconv.so.3 (0x2810d000)
        libapr-1.so.2 => /usr/local/lib/libapr-1.so.2 (0x281fa000)
        libcrypt.so.3 => /lib/libcrypt.so.3 (0x2821a000)
        libpthread.so.2 => /lib/libpthread.so.2 (0x28232000)
        libc.so.6 => /lib/libc.so.6 (0x28257000)
idabook#

Linux 和 BSD 系统均提供 ldd 工具。在 OS X 系统上,使用 otool 工具,并带上–L 选项( otool–L 文件名),即可实现类似的功能。在 Windows 系统中,可以使用 Visual Studio 工具套件中的实用 1 工具 dumpbin 列举某文件所依赖的库,形式为: dumpbin /dependents 文件名。

2.2.3 objdump

与专用的 ldd 不同, objdump 的功能非常多样。显示与目标文件有关的信息是 objdump 的功能。这是一个相当宽泛的目标, objdump 为此提供了大量命令行选项(超过 30 个),以提取目标文件中的各种信息。 objdump 可用于显示以下与目标文件有关的信息(以及其他更多信息)。

  • 节头部 ,程序文件每节的摘要信息。

  • 专用头部 ,程序内存分布信息,还有运行时加载器所需的其他信息,包括由 ldd 等工具生成的库列表。

  • 调试信息 ,提取出程序文件中的任何调试信息。

  • 符号信息 ,以类似 nm 的方式转储符号表信息。

  • 反汇编代码清单objdump 对文件中标记为代码的部分执行线性扫描反汇编。反汇编 x86 代码时, objdump 可以生成 AT & T 或 Intel 语法,并可以将反汇编代码保存在文本文件中。这样的文本文件叫做反汇编死代码清单(dead listing ),尽管这些文件可用于实施逆向工程,但它们很难有效导航,也无法以一致且无错的方式被修改。

objdump 是 GNU binutils2 工具套件的一部分,用户可以在 Linux、FreeBSD 和 Windows(通过 Cygwin)系统中找到这个工具。 objdump 依靠二进制文件描述符库 libbfd (二进制工具的一个组件)来访问目标文件,因此,它能够解析 libbfd 支持的文件格式(ELF 、PE 等)。另外,一个名为 readelf 的实用工具也可用于解析 ELF 文件。 readelf 的大多数功能与 objdump 相同,它们之间的主要区别在于 readelf 并不依赖 libbfd 。

2. 参见 http://www.gnu.org/software/binutils/

2.2.4 otool

otool 可用于解析与 OS X Mach-O 二进制文件有关的信息,因此,可简单将其描述为 OS X 系统下的类似于 objdump 的实用工具。下面的代码说明了如何使用 otool 显示一个 Mach-O 二进制文件的动态库依赖关系,从而执行类似于 ldd 的功能。

idabook#  file osx_example  
osx_example: Mach-O executable ppc  
idabook#  otool -L osx_example  
osx_example:  
        /usr/lib/libstdc++.6.dylib (compatibility version 7.0.0, current version 7.4.0)  
        /usr/lib/libgcc_s.1.dylib (compatibility version 1.0.0, current version 1.0.0)  
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 88.1.5)  

otool 可用于显示与文件的头部和符号表有关的信息,并对文件的代码部分进行反汇编。欲了解更多有关 otool 功能的信息,请参阅相关手册。

2.2.5 dumpbin

dumpbin 是微软 Visual Studio 工具套件中的一个命令行实用工具。与 otoolobjdump 一样, dumpbin 可以显示大量与 Windows PE 文件有关的信息。下面的例子说明了如何使用 dumpbin 以类似于 ldd 的方式显示 Windows 计算器程序的动态依赖关系。

$ dumpbin /dependents calc.exe
Microsoft (R) COFF/PE Dumper Version 8.00.50727.762
Copyright (C) Microsoft Corporation.  All rights reserved.

Dump of file calc.exe

File Type: EXECUTABLE IMAGE

  Image has the following dependencies:

    SHELL32.dll
    msvcrt.dll
    ADVAPI32.dll
    KERNEL32.dll
    GDI32.dll
    USER32.dll

dumpbin 的其他选项可从 PE 二进制文件的各个部分提取信息,包括符号、导入的函数名、导出的函数名和反汇编代码。欲了解更多有关如何使用 dumpbin 的信息,请访问 Mircrosoft Developer Network(MSDN)3

3. 参见 http://msdn.microsoft.com/en-us/library/clh23y6c(VS.71).aspx

2.2.6 c++filt

由于每一个重载的函数都使用与原函数相同的名称,因此,支持函数重载的语言必须拥有一种机制,以区分同一个函数的许多重载版本。下面的 C++ 实例展示了一个名为 demo 的函数的几个重载版本的原型:

void demo(void);  
void demo(int x);  
void demo(double x);  
void demo(int x, double y);  
void demo(double x, int y);  
void demo(char* str);  

通常,一个目标文件中不能有两个名称相同的函数。为支持重载,编译器将描述函数参数类型的信息合并到函数的原始名称中,从而为重载函数生成唯一的函数名称。为名称完全相同的函数生成唯一名称的过程叫做名称改编(name mangling)4 。如果使用 nm 转储前面的 C++ 代码的已编译版本中的符号,将得到如下结果(有删减,以突出显示 demo 的重载版本):

4. 有关名称改编的概述,请参考 http://en.wikipedia.org/wiki/Name_mangling

idabook#    g++ -o cpp_test cpp_test.cpp 

  
idabook#    nm cpp_test | grep demo 

  
0804843c  T _Z4demoPc  
08048400  T _Z4demod  
08048428  T _Z4demodi  
080483fa  T _Z4demoi  
08048414  T _Z4demoid  
080483f4  T _Z4demov  

C++ 标准没有为名称改编方案制定标准,因此,编译器设计人员必须自己制定标准。为了译解上面列出的 demo 函数的重载版本,我们需要一个能够理解编译器(这里为 g++ )的名称改编方案的工具, c++filt 正是这样一个实用工具。 c++filt 将每个输入的名称看成是改编后的名称(mangled name ),并设法确定用于生成该名称的编译器。如果这个名称是一个合法的改编名称,那么, c++filt 就输出改编之前的原始名称;如果 c++filt 无法识别一个改编名称,那它就按原样输出该名称。

如果将上面 nm 输出的结果交给 c++filt 处理,将可以得到这些函数的原始名称,如下所示:

idabook#    nm cpp_test | grep demo | c++filt 

  
0804843c  T demo(char*)  
08048400  T demo(double)  
08048428  T demo(double, int)  
080483fa  T demo(int)  
08048414  T demo(int, double)  
080483f4  T demo()  

值得注意的是,改编名称可能包含其他与函数有关的信息,正常情况下,nm 无法显示这些信息。在逆向工程过程中,这些信息可能非常重要。在更复杂的情况下,这些额外信息中可能还包含与类名称或函数调用约定有关的信息。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文