IDA(静态分析工具)
第一部分 IDA简介
(一)反汇编简介
01 何为反汇编
在传统的软件开发模型中,程序员使用编译器,汇编器和链接器中的一个或几个创建可执行程序。为了回溯编程过程(或对程序进行逆向工程),我们使用各种工具来撤销汇编和编译过程。毫不奇怪,这些工具就叫做反汇编器和反编译器,名副其实。反汇编器撤销汇编过程,因此,我们可以得到汇编语言形式的输出结果(以机器语言作为输入).反编译器则以汇编语言甚至是机器语言为输入,其输出结果为高级语言。
反汇编就是把目标代码(目标代码可以理解成能够被CPU识别并执行的代码)转为汇编代码的过程,也可以说是把机器语言转换为汇编语言代码,低级转高级的意思,常用于软件破解(例如找到它是如何注册的,从而解出它的注册码或者编写注册机),外挂技术,病毒分析,逆向工程,软件汉化等领域。
02 为何反汇编
通常,使用反汇编工具是为了在没有源代码的情况下促进对程序的了解。需要进行反汇编的常见情况包括以下几种。
- 分析恶意软件。
- 分析闭源软件的漏洞。
- 分析闭源软件的互操作性。
- 分析编译器生成的代码,以验证编译器的性能和准确性。
- 在调试时显示程序指令。
2.1分析恶意软件
通常,恶意软件的作者很少会提供他们"作品"的源代码,除非你对付的是一种基于脚本的蠕虫。由于缺乏源代码,要准确地了解恶意软件的运行机制,你的选择非常有限。动态分析和静态分析是分析恶意软件的两种主要技术。动态分析(dynamicanalysis)是指在严格控制的环境(沙盒)中执行恶意软件,并使用系统检测实用工具记录其所有行为。相反,静态分析(staticanalysis)则试图通过浏览程序代码来理解程序的行为。此时,要查看的就是对恶意软件进行反汇编之后得
到的代码清单。
2.2 漏洞分析
整个安全审核过程划分成3个步骤:发现漏洞,分析漏洞,开发破解程序(exploit).无论是否拥有源代码,都可以采用这些步骤来进行安全审核。但是,如果只有二进制文件,你可能需要付出巨大的努力。这个过程的第一个步骤,是发现程序中潜在的可供利用的条件。一般情况下,我们可通过模糊测试等动态技术来达到这一目的,也可通过静态分析来实现(通常需要付出更大的努力).一旦发现漏洞,通常需要对其进行深入分析,以确定该漏洞是否可被利用,如果可利用,可在什么情况下利用。
至于编译器究竟如何分配程序变量,反汇编代码清单提供了详细的信息。例如,程序员声明的一个70字节的字符数组,在由编译器分配时,会扩大到80字节,知道这一点会很有用。另外,要了解编译器到底如何对全局声明或在函数中声明的所有变量进行排序,查看反汇编代码清单是唯一的办法。在开发破解程序时,了解变量之间的这些空间关系往往非常重要。最后,通过结合使用反汇编器和调试器,就可以开发出破解程序。
2.3 软件互操作性
如果仅以二进制形式发布软件,竞争对手要想创建可以和它互操作的软件,或者为该软件提供插件,将会非常困难。针对某个仅有一种平台支持的硬件而发布的驱动程序代码。就是一个常见的例子。如果厂商暂时不支持,或者更糟糕地,拒绝支持在其他平台上使用他们的硬件,那么为了开发支持该硬件的软件驱动程序,可能需要完成大量的逆向工程工作。在这些情况下,静态代码分析几乎是唯一的补救方法。通常,为了理解嵌人式固件,还需要分析软件驱动程序以外的代码。
2.4 编译器验证
由于编译器(或汇编器)的用途是生成机器语言,因此优秀的反汇编工具通常需要验证编译器是否符合设计规范。分析人员还可以从中寻找优化编译器输出的机会,从安全角度来看,还可查知编译器本身是否容易被攻破,以至于可以在生成的代码中插人后门,等等。
2.5 显示调试信息
在调试器中生成代码清单,可能是反汇编器最常见的一种用途。遗憾的是,调试器中内嵌的反汇编器往往相当简单。它们通常不能批量反汇编,在无法确定函数边界时,它们有时候会拒绝反汇编。因此,在调试过程中,为了解详细的环境和背最信息,最好是结合使用调试器与优秀的反汇编器。
03 如何反汇编
为了满足所有要求,反汇编器必须从大量算法中选择一些适当的算法,来处理我们提供的文件,反汇编器所使用算法的质量及其实施算法的效率,将直接影响所生成的反汇编代码的质量。
3.1 基本的反汇编算法
为方便初学者,首先开发一个以机器语言为输入,以汇编语言为输出的简单算法。
- 确定代码区域:找到你需要反汇编的代码部分。
- 读取并解码指令:逐字节读取机器码并转换为汇编指令。
- 格式化指令:将指令和操作数格式化为易于阅读的汇编语言格式。
- 重复处理:继续处理下一条指令,直到代码区域结束。
这四个步骤可以看作是从头到尾读懂和翻译代码的过程。**首先,确定你要从哪里开始;然后,读取和转换数据;接着,格式化这些数据使其易于理解;最后,一直重复这个过程,直到你完成了整个任务。 **
3.2 线性扫描反汇编(主要)
线性扫描反汇编算法采用一种非常简单的方法来确定需要反汇编的指令的位置:一条指令结束,另一条指令开始的地方。因此,确定起始位置最为困难。常用的解决办法是,假设程序中标注为代码(通常由程序文件的头部指定)的节所包含的全部是机器语言指令。反汇编从一个代码段的第一个字节开始,以线性模式扫描整个代码段,逐条反汇编每条指令,直到完成整个代码段。这种算法并不会通过识别分支等非线性指令来了解程序的控制流。
以下是实现线性扫描反汇编的主要步骤:
-
**初始化: **设置初始条件以开始反汇编过程。 ** **
-
**读取指令: 从代码区域读取机器码。 **
-
**解码指令: **将机器码转换为汇编语言指令。
-
**处理控制流指令: **处理指令中的控制流跳转,如条件跳转和函数调用。
-
**继续扫描: 继续扫描并反汇编剩余的机器码。 **
-
**终止条件: **确定反汇编过程何时结束。
线性扫描算法的主要优点,在于它能够完全覆盖程序的所有代码段。线性扫描方法的一个主要缺点,是它没有考虑到代码中可能混有数据。总的来说,线性扫描算法无法正确地将嵌入的数据与代码区分开来。
GNU调试器(gdb)、微软公司的WinDbg调试器和objdump实用工具的反汇编引擎均采用线性扫描算法。
3.3 递归下降反汇编(主要)
递归下降采用另外一种不同的方法来定位指令。递归下降算法强调控制流的概念,控制流根据一条指令是否被另一条指令引用来决定是否对其进行反汇编。为便于理解递归下降,我们根据指令对cpu指令指针的影响对它们分类。
- 顺序流指令
顺序流指令是那些执行后会按顺序继续执行下一条指令的指令。它们不改变程序的控制流。
示例: 这些指令包括简单的算术运算和数据传输指令:
- MOV EAX, 1(将值 1 移动到寄存器 EAX)
- ADD EBX, 2(将值 2 加到寄存器 EBX 中)
这些指令不会改变程序的控制流,因此反汇编器只需顺序读取并解码下一条指令。
- 条件分支指令
条件分支指令根据某些条件的结果(如比较结果)决定是否跳转到另一条指令,需要解析条件并更新控制流。它们使程序的控制流变得复杂。
示例:
- JE label(如果上次比较结果相等,则跳转到 label)
- JNE label(如果上次比较结果不等,则跳转到 label)
反汇编器需要解析条件判断逻辑,确定跳转条件,以及跳转目标地址。 处理时,通常会根据条件判断是否跳转,然后继续反汇编跳转目标的代码。
- 无条件分支指令
无条件分支指令总是会跳转到指定的地址, 直接改变控制流 ,无论程序的当前状态如何。
示例:
- JMP label(跳转到 label 指定的位置)
- CALL function(调用一个函数,跳转到函数的起始地址,在处理CALL指令时,与函数调用指令的操作一致,即 CALL需要处理RET返回的地址 )
反汇编器需要更新程序的控制流到新的地址(目标地址),并开始从那个地址继续反汇编。 在处理 CALL 指令时,还需注意保存当前的返回地址,以便在函数执行完后返回。
- 函数调用指令
函数调用指令用于跳转到一个函数的开始位置,并保存返回地址, 反汇编时需处理函数的入口及返回机制,以便函数执行完后能够返回到原来的位置。
示例:
- CALL function(调用 function 函数)
反汇编器需要处理函数的入口地址,并且需要处理返回地址的保存。 这可能会涉及到设置一个新的反汇编位置,并在函数结束时处理返回指令。
- 返回指令
从函数返回到调用位置,处理返回地址的恢复和继续执行的逻辑。
示例:
- RET(从当前函数返回)
汇编器需要处理返回地址的恢复,并继续从返回地址开始执行反汇编过程。 在遇到 RET 指令时,通常需要查找调用该函数的位置,并将控制流设置回那个位置。
(二)逆向与反汇编工具
01 分类工具
1.1 file
file 是一个在Unix-like系统(如Linux、macOS)中常用的命令行工具,它用于识别文件类型。它通过分析文件的特征码(magic numbers)和其他文件的特征来确定文件的实际类型,而不仅仅是根据文件扩展名。
功能
- 文件类型检测:file 可以识别多种类型的文件,包括文本文件、可执行文件、文档、图片等。
- 文件格式信息:对二进制文件,file 能够告诉你它是一个ELF文件、PE文件(Windows可执行文件)、或其他格式的文件。
基本用法
- 查看文件类型:
file filename
例子:
file example.exe
输出可能是:
example.exe: PE32 executable (GUI) Intel 80386, for MS Windows
- 查看文件的详细信息:
file -i filename
例子:
file -i example.exe
输出可能是:
example.exe: application/x-dosexec; charset=binary
适用场景
- 初步分析:在进行逆向工程前,使用 file 了解文件的类型和格式,判断其是否为可执行文件或特定的格式。
- 快速识别:确定文件是否是你期望的类型(如确定是否是PE文件)。
f1le及类似的实用工具也会出错。如果一个文件碰巧包含了某种文件格式的标记,file等工具很可能会错误地识别这个文件。使用一个十六进制文件编辑器将任何文件的前4字节修改为java的幻数序列CA FE BA BE,这时,file会将这个新修改的文件错误地识别为已编译的java类数据。同样,一个仅包含MZ这两个字符的文本文件会被误认为是一个MS-DOS可执行文件。在逆向工程过程中,绝不要完全相信任何工具所提供的结果,除非该结果得到其他几款工具和手动分析的确认,这是一个良好的习惯。
1.2 PE Tools
PE Tools是一组用于分析Windows系统中正在运行的进程和可执行文件的工具,用于分析和编辑PE(Portable Executable)格式文件。PE格式是Windows操作系统下可执行文件(EXE)、动态链接库(DLL)等的标准格式。
功能
- 查看PE文件结构:分析PE文件的各个部分,包括头部信息、节表、导入表、导出表等。
- 分析文件属性:查看文件的编译时间、入口点、节的详细信息。
- 识别可疑特征:检查文件中是否有常见的反病毒特征(如加壳工具、恶意代码迹象)。
基本用法
- 打开PE文件:启动 PE Tools,通过“文件”菜单打开一个PE文件(如EXE或DLL)。
- 查看详细信息:浏览工具提供的不同选项卡(如“头部信息”、“节表”、“导入表”)以获取有关文件的详细数据。
适用场景
- 深入分析PE文件:了解文件的内部结构,查找特定的数据段、代码段或导入的函数。
- 调试和逆向:分析文件的各种部分,确定其是否被加壳或包含恶意代码。
在进程列表中,用户可以将一个进程的内存映像转储到某个文件中,也可以使用PE Sniffer实用工具确定可执行文件由何种编译器构建,或者该文件是否经过某种已知的模糊实用工具的模糊处理。Tools菜单提供了分析磁盘文件的类似选项。另外,用户还可以使用内嵌的PE Editor实用工具查看PE文件头字段,使用该工具还可以方便地修改任何文件头的值。通常,如果想要从一个文件的模糊版本重建一个有效的PE,就需要修改PE文件头。
1.3 PEiD
PEiD 是另一款Windows工具,用于检测和分析PE文件的加壳和保护,并确定任何模糊Windows PE二进制文件的工具。它可以帮助你识别文件是否经过加壳或使用了特定的保护技术。
功能
- 识别加壳工具:检测PE文件是否使用了常见的加壳工具(如UPX、AsPack、Themida等)。
- 分析保护技术:识别文件中使用的各种保护机制,如反调试、加密技术。
- 插件支持:PEiD 支持插件,可以扩展其功能,例如支持更多的壳和保护技术的识别。
基本用法
- 扫描文件:启动 PEiD,选择“文件”菜单中的“打开”选项来加载目标PE文件。
- 查看检测结果:PEiD 会自动扫描文件,提供加壳工具和保护技术的识别结果。
适用场景
- 分析加壳文件:确定一个PE文件是否被加壳,了解其使用的加壳工具。
- 逆向和破解:在逆向工程时,识别文件的保护技术,决定如何解壳或绕过保护机制。
PEiD的许多功能与PE Tools的功能相同,包括显示PE文件头信息摘要、收集有关正在运行的进程信息、执行基本的反汇编等。
02 摘要工具
2.1 nm
nm 是一个在Unix-like系统(如Linux、macOS)中用于列出目标文件(包括可执行文件和共享库)的符号表的工具。它可以帮助你查看程序中的函数、变量等符号的定义和引用。
功能
- 列出符号:显示目标文件中所有的符号及其地址、类型等信息。
- 符号分类:区分出符号是局部的还是全局的,定义的还是未定义的。(大写字母是全局符号,小写字母是局部符号)
基本用法
- 查看符号表:
nm filename
例子:
nm my_program
输出示例:
0000000000401140 T main
0000000000601020 B global_var
扩展:
U——未定义符号,通常为外部符号引用。
T ——在文本部分定义的符号,通常为函数名称。
t ——在文本部分定义的局部符号。在c程序中,这个符号通常等同于一个静态函数。
D—— 已初始化的数据值。
C—— 未初始化的数据值。
- 查看特定符号:
nm -g filename
-g 选项只显示全局符号。
适用场景
- 分析符号:了解程序中函数和变量的定义和引用。
- 调试:帮助定位符号的定义和未定义问题。
2.2 ldd
ldd 是一个适用于Unix-like系统的用于显示一个可执行文件或共享库所依赖的动态链接库的工具。
功能
- 显示共享库依赖:列出程序在运行时需要加载的所有共享库及其路径。
基本用法
- 查看共享库依赖:
ldd filename
例子:
ldd my_program
输出示例:
linux-vdso.so.1 (0x00007ffed6fef000)
libm.so.6 => /lib64/libm.so.6 (0x00007f2c9c4c8000)
libc.so.6 => /lib64/libc.so.6 (0x00007f2c9c0e6000)
适用场景
- 分析动态依赖:查看一个程序需要哪些动态库,及其路径。
- 调试动态链接问题:确定缺少的或路径错误的共享库。
2.3 objdump
objdump 是一个在Unix-like系统中用于显示目标文件信息的工具,功能强大,可以显示详细的汇编代码、头部信息、符号表等。
功能
- 反汇编代码清单:将目标文件的二进制代码转换为汇编代码(objdump对文件中标记为代码的部分执行线性扫描反汇编)。
- 显示头部信息:ELF头部、节头部等信息。
- 节头部:程序文件每节的摘要信息。
- 专用头部:程序内存分配信息,还有运行时加载器所需要的其他信息,包括ldd等工具生成的库列表。
- 显示符号表:列出目标文件中的符号及其地址(以类似nm的方式转储符号表信息)。
- 调试信息:提取出程序文件中的任何调试信息。
基本用法
- 反汇编目标文件:
objdump -d filename
例子:
objdump -d my_program
- 显示文件头信息:
objdump -f filename
例子:
objdump -f my_program
适用场景
- 反汇编分析:查看目标文件的汇编代码,进行深入分析。
- 文件头信息检查:了解目标文件的格式和结构。
2.4 otool
otool 是一个在macOS系统中使用的工具,用于显示Mach-O格式目标文件的信息,如库依赖、符号表、反汇编代码等。
功能
- 显示Mach-O文件的信息:包括文件头信息、依赖库、符号表等。
- 反汇编:将Mach-O文件的二进制代码转换为汇编代码。
基本用法
- 显示Mach-O文件头信息:
otool -h filename
例子:
otool -h my_program
- 反汇编Mach-O文件:
otool -t filename
例子:
otool -t my_program
适用场景
- macOS下的文件分析:查看Mach-O文件的详细信息和汇编代码。
- 动态库依赖检查:查看目标文件的动态库依赖。
2.5 dumpbin
dumpbin 是一个在Windows系统中使用的工具,用于显示PE文件的详细信息,包括符号表、导入表、导出表和反汇编代码等。
功能
- 显示PE文件的详细信息:导入表、导出表、符号表和反汇编代码等。
- 反汇编:显示文件的汇编代码(通过其他工具)。
基本用法
- 显示PE文件导入表:
dumpbin /imports filename
例子:
dumpbin /imports my_program.exe
- 显示PE文件的导出表:
dumpbin /exports filename
例子:
dumpbin /exports my_program.exe
适用场景
- PE文件分析:检查PE文件的导入和导出信息。
- 符号和依赖检查:了解PE文件的符号和库依赖。
2.6 c++filt
c++filt 是一个用于解码C++程序的名称改编(name mangling)的工具,将C++编译器生成的修饰符号转换为原始符号名称。(注释:通常,一个目标文件中不能有两个名称相同的函数。为支持重载,编译器将描述函数参数类型的信息合并到函数的原始名称中,从而为重载函数生成唯一的函数名称。为名称完全相同的函数生成唯一名称的过程叫做名称改编(name mangling))
功能
- 解码C++符号:将被C++编译器修饰的符号名称(如经过name mangling的名称)转换为人类可读的形式。
基本用法
- 解码符号:
c++filt mangled_name
例子:
c++filt _Z3fooi
输出示例:
foo(int)
适用场景
- 符号解析:理解C++程序中的符号和函数名称。
- 逆向工程:帮助识别和分析C++代码中的函数和变量。
03 深度检测工具(任意格式)
3.1 strings
strings 是一个在Unix-like系统中使用的工具,用于提取二进制文件中的可打印字符串。这个工具对于逆向工程和恶意软件分析尤其有用,因为它可以帮助你发现隐藏的文本信息,比如错误消息、文件路径、网络地址等。
功能
- 提取字符串:从二进制文件中提取连续的可打印字符序列。
- 过滤和排序:按需过滤短小的或特定长度的字符串,便于分析。
基本用法
- 提取字符串:
strings filename
例子:
strings my_program.exe
- 指定最小字符串长度:
strings -n length filename
例子:
strings -n 5 my_program.exe
这将提取所有长度大于等于5的字符串。
- 输出到文件:
strings filename > output.txt
适用场景
- 查找隐藏信息:揭示程序中的隐藏文本,如错误信息、配置文件路径等。
- 恶意软件分析:发现恶意软件中的字符串,例如命令和控制服务器地址、编码方式等。
- 文档和调试:帮助开发人员和逆向工程师分析程序的调试信息。
需要记住的是:二进制文件中包含某个字符串,并不代表该文件会以某种方式使用这个字符串。
下面是使用 strings 时的一些注意事项:
- 使用 strings 处理可执行文件时,默认情况下,strings仅仅扫描文件中可加载的,经初始化的部分。使用命令行参数-a可强制strings扫描整个文件。
- strings不会指出字符串在文件中的位置。使用命令行参数-t可令 strings 显示所发现的每一个字符串的文件偏移量信息。
- 许多文件使用了其他字符集。使用命令行参数-e可使strings搜索更广泛的宇符,如16位Unicode字符。
3.2 流式反汇编器
前面介绍过很多工具可以生成二进制目标文件的死代码清单形式的反汇编代码。PE、ELF和MACH-O文件可分别使用dumpbin、objdump和otool进行反汇编。但是,它们中的任何一个都无法处理任意格式的二进制数据块。有时候,你会遇到一些并不采用常用文件格式的二进制文件,在这种情况下,你就需要一些能够从用户指定的偏移量开始反汇编过程的工具。如用于x86指令集的流式反汇编器:ndisasm和diStormp。
1. ndisasm
ndisasm 属于 nasm(Netwide Assembler)套件的一部分。它用于将机器码反汇编成汇编代码,支持各种处理器架构。
功能
- 流式反汇编:从机器码中生成汇编语言代码。
- 支持多种处理器架构:处理x86、x86_64等不同架构的机器码。
基本用法
- 反汇编机器码文件:
ndisasm -b bitness file
例子:
ndisasm -b 32 my_program.bin
其中 -b 32 指定反汇编32位机器码。
- 从指定地址开始反汇编:
ndisasm -b bitness -o offset file
例子:
ndisasm -b 64 -o 0x1000 my_program.bin
这将从偏移0x1000开始反汇编。
适用场景
- 低级分析:直接查看二进制文件的汇编代码,进行低级调试和分析。
- 教学和学习:学习机器码和汇编语言之间的关系。
- 逆向工程:分析恶意软件或破解保护机制。
2. diStorm
diStorm 支持多种处理器架构,它可以在Python和C++中使用,适用于需要在代码中进行反汇编的场景。
功能
- 高效流式反汇编:将机器码流转换为汇编代码。
- 支持多种架构:x86、x86_64、ARM等。
- 易于集成:可以与Python和C++项目集成,提供反汇编功能。
基本用法
- Python中使用:
from distorm3 import Decode
# 机器码示例
data = b"\x55\x48\x89\xe5"
for offset, size, mnemonic, operands in Decode(0, data, Distorm3.Decode32Bits):
print(f"Offset: {offset}, Mnemonic: {mnemonic}, Operands: {operands}")
- C++中使用:
#include "distorm.h"
// 机器码示例
BYTE code[] = { 0x55, 0x48, 0x89, 0xe5 };
Decode(0, code, sizeof(code), Decode32Bits, Disasm);
适用场景
- 动态分析:在动态分析工具或自动化分析工具中使用反汇编功能。
- 逆向工程:集成到逆向工程工具中,以提供反汇编支持。
- 开发和测试:用于开发新的分析工具或测试现有工具的反汇编能力。
由于流式反汇编非常灵活,因此它的用途相当广泛。例如,在分析网络数据包中可能包含shellcode 的计算机网络攻击时,就可以采用流式反汇编数据包中包含 shellcode 的部分,以分析恶意负载的行为。另外一种情况是分析那些不包含布局参考的ROM镜像。ROM中有些部分是数据,其他部分则为代码,可以使用流式反汇编器来反汇编镜像中的代码。
第二部分 IDA基本用法
(三)IDA入门
01 启动IDA
只要启动IDA,你都会看到一个初始欢迎界面,上面显示你的许可证信息摘要。初始屏幕消失后,IDA将显示另一个对话框,为你进入桌面环境提供3种选项,如图3-1所示。
图3-1所示的3个选项进人IDA桌面的方式略有不同,下面简单说明。
- New(新建)。点击“New”会打开一个标准的文件选择对话框,让你选择要分析的文件。根据你选择的文件,IDA会显示其他对话框,让你设置具体的分析选项,然后再开始加载和分析文件。
- Go(运行)。 当你点击“Go”按钮时,它会终止当前的加载过程,并将IDA重置为一个空白的工作区。接下来,如果你想要打开一个文件,你可以选择以下任意一种方法:
- 拖放文件:将二进制文件直接拖放到IDA的工作区。
- 使用菜单:通过IDA的“File”菜单选择打开文件。
请注意,IDA默认只显示特定类型的文件。如果你的文件没有显示出来,确保在“File”对话框中选择“所有文件”(All Files),以便能够看到你想要打开的文件。打开文件时,IDA会尝试自动识别文件类型,并会显示一个“Loading”对话框,你可以查看IDA选择了哪个加载器来处理你的文件。
- Previous(上一个)。 点击“Previous”按钮可以从“最近用过的文件”列表中打开之前用过的文件。这个列表显示了你最近打开的文件,可以方便你重新访问它们。默认情况下,历史记录最多显示10个文件,但你可以通过编辑配置文件将这个数量增加到100。使用这个历史记录列表,可以快速找到并重新处理你最近打开过的数据库文件。
1.1 IDA文件加载
当使用“File -> Open”命令打开一个新文件时,会出现一个加载对话框(图3-2),显示IDA可能使用的文件类型列表。这个列表会显示最适合处理你选择的文件的IDA加载器。
IDA通过尝试每一个加载器来识别文件,并生成这个列表,比如图3-3显示的是ELF加载器。如果列表中显示了多个加载器,比如Windows PE加载器(pe.idw)和MS-DOS EXE加载器(dos.ldw),这表示它们都认为可以处理这个文件。PE格式是MS-DOS EXE格式的扩展,因此多个加载器可能都适用。
列表中的最后一个选项是“Binary File(二进制文件)”,这是一个默认选项,表示IDA无法识别文件时会使用这种最基础的方法来加载文件。如果有多个加载器选择,通常选择IDA推荐的默认选项是明智的,除非你有更好的信息来选择其他加载器。
有时候,Binary File是出现在加载器列表中的唯一选项。这表示没有加载器能够识别选定的文件。这时,如果你希望继续完成加载过程,请确保根据自己对文件内容的理解,选择合适的处理器类型。
在IDA中,你可以通过“**Processor type”(处理器类型)**下拉菜单来选择反汇编过程中使用的处理器类型(图3-4)。IDA通常会自动根据文件头信息选择合适的处理器,但如果它无法正确识别,你需要手动选择。
如果你选择了“Binary File”(二进制文件)格式和某种x86处理器,那么“Loading Segment”(加载段)和“Loading Offset”(加载偏移量)字段将变得可编辑(图3-5)。由于二进制加载器无法提供内存布局信息,你需要在这两个字段中输入段和偏移量值,以设置文件内容的基址。如果在加载过程中忘记指定基址,你可以随时通过“Edit -> Segments -> Rebase Program”命令来修改IDA中的基址。
**“Kemel Options”(核心选项)**按钮用于配置特定的反汇编分析选项,IDA可利用这些选项改进递归下降过程。绝大多数情况下,默认选项提供的都是最佳的反汇编选项。
**“Processor Options”(处理器选项)**按钮用于选择选中的处理器模块的配置选项。这些选项可以帮助改进反汇编过程,但并不是所有处理器模块都适用。
其他选项复选框可帮助用户更好地控制文件加载过程。IDA的帮助文件详细介绍了这里的每一个选项。这些选项并不适用于所有输人文件类型,多数情况下,用户可以使用IDA的默认设置。
1.2 使用二进制文件加载器
如果你选择使用二进制加载器,就需要手动完成一些通常由更高级加载器自动完成的任务,因为二进制加载器没有文件头信息来引导分析。这种情况通常发生在分析从网络数据包、日志文件中提取的ROM镜像或破解程序负载时。
如果同时选择x86处理器模块和二进制加载器,将会显示如图所示的对话框,让你选择将代码作为16位模式还是32位模式处理。IDA还可以为ARM和MIPS等处理器区分16位和32位模式。
二进制文件通常不包含内存布局的信息,IDA无法从中识别这些信息。因此,如果你选择x86处理器,你需要在加载器对话框中手动输入“Loading Segment”(加载段)和“Loading Offset”(加载偏移量)来指定基址。
图3-10(选择"No"后)显示的是加载二进制文件的最后一个步骤:告诉你需要做一些额外工作。因为IDA没有文件头信息来自动区分代码和数据,所以它会提示你指定一个地址,告诉IDA将这个地址的字节转换成代码(你可以按C键来强制将字节当作代码处理)。对于二进制文件,IDA不会进行反汇编,直到你至少标记了一个字节作为代码。
02 IDA数据库文件
当你选择加载选项并点击OK按钮后,IDA会开始加载并分析你选择的可执行文件。它会创建一个数据库,这个数据库由4个文件组成,名称与可执行文件相同,但扩展名分别为:.id0
、.id1
、.nam
和 .til
。这些文件的作用如下:
- .id0文件:包含数据库的结构。
- .id1文件:标记每个程序字节的信息。
- .nam文件:与程序位置相关的索引信息。
- .til文件:存储本地类型定义的信息。
这些文件格式是IDA专用的,不易在IDA之外编辑。当你关闭项目时,这4个文件将被存档,你还可以选择将它们压缩成一个IDB文件。通常情况下,IDA的数据库指的是这个IDB文件。如果数据库正常关闭,你绝不会在工作目录中看到扩展名为.id0
、. id1
、.nam
或.til
的文件。如果工作目录中存在这些文件,则往往表示数据库被意外关闭(IDA崩溃),这时数据库可能被损坏。
值得注意的是,一旦IDA为某个可执行文件创建了数据库,它就不再需要访问原文件,除非你要使用IDA的调试功能。这对安全分析很有帮助,比如在分析恶意软件时,可以只分享IDA数据库而不是恶意文件本身。虽然IDA用来分析文件,但本质上它是一个数据库应用程序。IDA创建的数据库包含了所有分析的信息,并提供不同的视图来显示这些信息。对数据库的修改会保存在IDA中,但不会改变原始文件。IDA的强大在于它提供了多种工具来分析和操作这些数据库数据。
2.1 创建IDA数据库
在你选择一个文件并设置分析选项后,IDA会开始创建数据库。这个过程包括以下步骤:
- 加载文件:IDA使用加载器模块从磁盘加载文件,解析文件头信息,创建代码和数据块。
- 确定代码位置:加载器模块会根据文件头确定代码在内存中的布局,并配置数据库。
接下来,IDA的反汇编引擎会接管工作,逐个处理地址。处理器模块的任务是:
- 分析指令:确定每个地址的指令类型、长度及下一步执行的位置(如是否是分支)。
- 转换指令:将找到的指令转换为汇编语言并显示出来。
在IDA完成初始分析后,用户可能会在数据库中发现以下一些或全部信息。
- 编译器识别。了解构建软件所用的编译器对分析二进制文件很有帮助。IDA在加载文件时会尝试识别使用的编译器,这样可以扫描并高亮显示编译器生成的样板代码,减少需要分析的代码量。
- 函数参数和局部变量识别。 IDA会分析每个识别出的函数,检查栈指针寄存器来确定栈中的变量和函数的栈布局。然后,IDA会自动为这些变量命名,区分它们是函数中的局部变量还是传递给函数的参数。
- 数据类型信息。 IDA会在数据库中添加注释,说明常见库函数的参数位置。这些注释基于对函数和参数的了解,能节省分析人员查找API资料的时间。
2.2 关闭IDA数据库
任何时候你关闭一个数据库,无论你是完全关闭IDA,还是切换到另一个数据库,IDA都将显示一个 Save database(保存数据库)对话框,如图3-6所示。
当你第一次保存一个新数据库时,IDA会用扩展名.idb
替换输入文件的扩展名,生成数据库文件名。例如,example.exe
会变成example.idb
。如果输入文件没有扩展名,IDA会直接在文件名后加上.idb
,比如httpd
变成httpd.idb
。以下是保存选项的简要说明。
- Don’t pack database(不打包数据库)。这个选项只会更新四个数据库组件文件的更改,而不会创建
.idb
文件。在关闭数据库时,不建议使用这个选项。 - Pack database(Store) [ 打包数据库(存储)]。 选择这个选项会将4个数据库组件文件合并成一个
.idb
文件,原有的.idb
文件会被覆盖。这个选项不进行压缩,创建.idb
文件后,4个组件文件会被删除。 - Pack database(Deflate) [ 打包数据库(压缩)]。 选择这个选项会将4个数据库组件文件压缩成一个
.idb
文件。与“Pack database(Store)”选项不同的是,它会对文件进行压缩,以节省空间。 - Collect garbage(收集垃圾)。 这个选项会在关闭数据库之前,删除数据库中未使用的内存页面。如果同时选择“Deflate”选项,可以创建更小的
.idb
文件。通常只有在磁盘空间不足时才需要使用这个选项。 - Don’t SAVE the datebase(不保存数据库)。选择这个选项会放弃当前对数据库所做的所有更改,仅保留上次保存时的状态。IDA会删除4个组件文件,保留未修改的
.idb
文件,这类似于撤销你在IDA中所做的更改。
2.3 重新打开数据库
实际上,重新打开一个现有的数据库并不复杂。你只需使用IDA的文件打开功能,通常速度较快,因为IDA不需要重新分析文件,并会恢复到上次关闭时的状态。
问题是,IDA有时会崩溃,可能是因为软件本身或某个插件的错误。崩溃可能导致打开的数据库文件受损。崩溃时,IDA可能无法正确关闭数据库或删除中间文件,这会留下一个已保存的.idb
文件和可能受损的中间文件。
在这种情况下,你可以选择恢复上次保存的.idb
文件,或继续使用可能受损的数据库。如果选择“继续使用未打包的库”,可能无法完全恢复你所做的更改。
03 IDA桌面简介
IDA的默认界面如图3-6所示。
1. 工具栏区域。显示IDA的常用操作工具。你可以通过“View ->Toolbars
”命令来显示或隐藏这些工具栏,也可以用鼠标拖动工具栏调整位置。IDA的基本模式工具栏只有一排按钮,如图3-6所示。若要查看更多按钮,可以通过“View->Toolbars->Advanced mode
”切换到高级模式,这样工具栏会显示三排按钮。
2. 导航带。彩色的水平带是IDA的概况导航栏,它显示了加载文件的整个地址空间。默认情况下,导航带展示了整个二进制文件的地址范围。你可以右击导航带来缩放,调整显示的地址范围。不同颜色表示文件中的不同内容,比如数据或代码。
导航带上有一个小指示符(默认为黄色),显示当前反汇编窗口中对应的地址。将光标悬停在导航带上,会显示工具提示,指示对应的二进制文件位置。点击导航带反汇编视图将跳转到你选定的二进制文件位置。你也可以通过“Options->Colors
”命令自定义导航带的颜色。
如果你将导航带拖离IDA窗口,它会变成一个独立的工具栏。
3.标签。 IDA的界面中每个数据显示窗口都有一个标签,这些窗口展示了从二进制文件中提取的信息,代表了不同的数据库视图。大多数分析工作都是通过这些数据显示窗口来完成的。图3-6展示了三个主要的数据显示窗口:IDA-View
、Functions
和Graph Overview
。你可以通过“View->Open Subviews
”菜单打开更多的数据窗口,或者恢复那些被意外或故意关闭的窗口。
4.反汇编视图。 在IDA的反汇编视图中,有两种显示方式:图形视图(默认)和列表视图。图形视图显示函数的流程图,让你可以通过视觉化的结构图了解函数的运行情况。你可以在IDA-View窗口中用空格键在这两种视图之间切换。如果你想将列表视图设为默认视图,可以通过“Options -> General
”菜单进入设置,取消选中“Graph
”选项卡下的“Use graph view by default
”复选框(如图所示)。
5.图形概况视图。 在使用图形视图时,屏幕上可能无法一次性显示整个函数的流程图。这时,图形概况视图(只有在图形视图中才会显示)提供了一个缩略图,显示整个函数图形的结构,并用虚线矩形标出当前的显示区域。你可以在图形概况视图中点击鼠标来调整图形视图的位置。
6.输出窗口。 显示IDA的各种信息,包括文件分析的进度和用户操作的错误消息。它相当于一个控制台,用来展示系统的反馈和状态。
7.函数窗口。默认IDA显示窗口的最后一部分,我们将在第四章详细讨论这些窗口。
04 初始分析时的桌面行为
在对新文件进行初始自动分析时,桌面上会显示很多信息,帮助你了解分析进展。这些信息包括:
- 消息输出窗口:显示分析的进度消息。
- 反汇编窗口:展示初始的反汇编内容和位置。
- functions窗口:显示函数的初始数据,并随着分析的进行定期更新。
- 导航带:展示文件被识别为代码或数据的变化,代码块被识别为函数的过程,以及函数识别的情况。
- 当前位置指示符:在导航带上移动,显示当前正在分析的区域。
05 IDA桌面提示和技巧
IDA可能会显示很多信息,桌面可能变得混乱。这里有一些提示来帮助你更好地管理桌面:
- 恢复关闭的窗口:用
View -> Open Subviews
命令重新打开你不小心关闭的数据显示窗口。 - 重置布局:用
Windows -> Reset Desktop
命令将桌面恢复到默认布局。 - 保存布局:用
Windows -> Save Desktop
命令保存当前桌面布局,以备后用。 - 加载布局:用
Windows -> Load Desktop
命令打开之前保存的桌面布局。 - 修改字体:你可以在
Disassembly
窗口中调整显示字体,使用Options -> Font
命令设置字体。
(四)IDA数据显示窗口
在详细介绍IDA的子窗口之前,了解一些基本规则会很有帮助:
- 无撤销功能:IDA没有撤销功能。如果操作导致数据库文件意外更改,你需要手动恢复以前显示窗口的状态。
- 操作方式:几乎所有操作都有对应的菜单项、热键和工具栏按钮。工具栏高度和热键映射都可以自定义。
- 右键菜单:IDA提供了方便的右键菜单,虽然这些菜单不会列出所有可能的操作,但可以快速执行一些常见操作。
01 IDA主要的数据显示窗口
在IDA的默认配置中(从6.1版开始),打开一个新二进制文件时,会创建7个显示窗口,你可以通过导航带下方的标签访问这些窗口。3个主要的窗口是IDA-View窗口、函数窗口和消息输出窗口。即使这些窗口不是默认打开的,你也可以通过 View -> Open Subviews
菜单来打开它们。记住这一点,因为你可能会不小心关闭窗口。
在IDA中,Esc键非常有用:在反汇编窗口中,它像网页浏览器的“后退”按钮一样,用于返回上一个视图(详细的导航会在第五章讲解)。不过,在其他窗口中,Esc键用于关闭窗口。如果你不小心关闭了窗口,可能需要迅速重新打开它。
1.1 反汇编窗口
反汇编窗口也叫IDA-View
窗口,它是操作和分析二进制文件的主要工具。
反汇编窗口有两种视图格式:默认的图形视图和文本列表视图。你可以根据个人喜好选择使用哪种视图。如果你希望将文本列表视图设为默认视图,可以通过 Options -> General
菜单进入设置,取消选中“Graph
”选项卡下的“Use graph view by default
”选项。在反汇编窗口中,你也可以使用空格键在图形视图和列表视图之间切换。
(1)IDA图形视图
图形视图类似于程序流程图(见图4-1),它把一个函数分解成多个基本块,直观地显示函数在这些块之间的控制流程。
在IDA的图形视图中,函数块之间的流动由不同颜色的箭头表示:绿色箭头表示条件跳转的“Yes”(是的,执行分支),红色箭头表示“No”(不,不执行分支),蓝色箭头表示正常的顺序流程。在图形模式下,IDA一次显示一个函数。可以使用"CTRL+鼠标滑轮"来调整图形的大小。键盘缩放控制需要使用"CTRL+加号键"来放大,或使用"CTRL+减号键"
来缩小。如果函数很大或很复杂,图形视图可能会变得很混乱,这时可以使用“图形概况”窗口(见图4-2)。这个窗口显示整个函数的结构,并用虚线框标出当前视图的位置。你可以在概况窗口中拖动虚线框,快速调整图形视图的位置。
你可以通过以下几种方式控制IDA图形视图的显示:
- 平移:通过点击并拖动图形视图的背景,可以移动视图位置,快速定位到你需要的区域。你也可以使用“图形概况”窗口帮助定位。
- 重新调整块位置:点击并拖动块的标题栏可以移动块的位置。IDA会尽量保留块之间的连接线不变,但你可以手动调整连接线的路径。按住 Shift 键并双击连接线可以添加新的连接点。要恢复默认布局,可以右击图形并选择“Layout Graph”。
- 分组和折叠块:你可以将块分组,折叠分组后的块以减少界面混乱。折叠块特别有用,可以帮助你追踪已经分析过的块,右击块的标题栏选择“Group Nodes”可以进行分组和折叠。
- 创建其他反汇编窗口:如果需要同时查看多个函数,可以通过
Views -> Open Subviews -> Disassembly
创建新的反汇编窗口。这样打开的第一个反汇编窗口叫做IDA View-A
.随后的反汇编窗口叫做IDA View-B
,IDA View-C
,依次类推。每个窗口都是独立的,你可以在不同的窗口中查看不同的视图或图形。
需要指出的是,对于视图的控制并不仅限于这些示例。我们将在第八章介绍其他IDA图形功能。
(2)IDA文本视图
面向文本的反汇编窗口是传统的视图方式,用来查看和操作IDA生成的反汇编代码。它显示程序的完整反汇编代码清单,而图形视图一次只能显示一个函数。你只能通过文本视图窗口查看二进制文件的数据部分。图形视图中显示的信息在文本视图中也可以找到。
图4-1展示的函数的文本视图列表如图4-3所示。在这个窗口中,反汇编代码按行显示,并显示虚拟地址,格式为[区域名称]:[虚拟地址](如 text:004011c1
)。
窗口的左侧是箭头窗口,显示函数中的控制流:
- 实线箭头表示非条件跳转。
- 虚线箭头表示条件跳转。
- 粗线箭头表示逆向跳转(循环)。
在图4-3中,地址 0000000000001188
至 000000000000119C
之间的粗线箭头表示存在一个循环。
位置2显示了IDA对函数栈布局的估算,描述了栈指针及栈帧的结构,这将在第五章详细讨论。
位置3 的注释(以分号开头)是交叉引用,表示其他指令可能跳转到当前代码的位置。交叉引用将在第八章详细讲解。
1.2 函数窗口
Functions窗口用于列举IDA在数据库中识别的每一个函数。Functions窗口中的条目如下图4-4所示:
选中的这一行信息特别指出:在二进制文件中,地址 0000000000001135
对应的是 text
部分的 main
函数。这个函数长17字节(十六进制),它返回调用方(R
)并使用 EBP
寄存器(B
)来引用局部变量。有关描述函数的标记(如 R
和 B
)的更多信息,可以查看IDA的帮助文档,或者右击函数选择“Properties
”来查看和编辑标记。
与其他窗口一样,双击 functions
窗口中的条目,反汇编窗口会跳转到该函数的位置。
1.3 输出窗口
当你打开一个新文件时,IDA的输出窗口位于工作区底部,与其他窗口一起显示。输出窗口是IDA的消息控制台,用于显示与IDA执行任务相关的信息。例如,当你首次打开一个二进制文件时,输出窗口会显示分析的进度和操作详情。在使用数据库时,输出窗口会显示你执行的各种操作的状态。你可以将内容复制到剪贴板,也可以右击窗口选择菜单来清除内容。通常,输出窗口也用于显示你开发的脚本和插件的输出。
02 次要的IDA显示窗口
2.1 十六进制窗口
虽然通常称IDA中的这个窗口为“十六进制窗口”,但它实际上可以显示多种格式,并可以用作十六进制编辑器。默认情况下,它显示程序内容的标准十六进制代码,每行16个字节,以及对应的ASCII字符。你可以同时打开多个十六进制窗口,第一个叫做Hex View-A,第二个叫Hex View-B,依此类推。默认情况下,第一个十六进制窗口会与第一个反汇编窗口同步,滚动一个窗口时另一个窗口也会同步滚动,并且在反汇编窗口中选中的指令在十六进制窗口中也会高亮显示。例如,在图4-5中,光标在反汇编窗口的地址 0000000000001120
指向一个调用指令,这个指令的5个字节在十六进制窗口中都会高亮显示。
在图4-5中,右击十六进制窗口可以打开上下文菜单,从中可以设置与某个十六进制窗口同步的反汇编窗口(如果有的话)。如果取消同步选项,滚动十六进制窗口时不会同步滚动其他反汇编窗口。通过选择 Edit 菜单项,可以将十六进制窗口切换为十六进制编辑器,完成编辑后需提交或取消更改才能返回查看模式。你可以通过 Data format 菜单项选择不同的显示格式,如1、2、4、8字节的十六进制,带签名或不带签名的十进制,以及各种浮点格式。使用 Columns 菜单项可以更改显示的列数,使用 Text 选项可以打开或关闭文本块。
如果十六进制窗口中显示全是问号,这表示IDA无法识别该虚拟地址范围内的内容。这通常发生在程序的 bss
节中,该节在文件中不占用实际空间,但加载时会扩展以满足程序的存储需求。
2.2 导出窗口
导出窗口列出了文件中的所有导出点,包括程序的执行入口点和文件中导出的函数及变量。通常,这些导出点在共享库(如Windows DLL文件)中比较常见。导出窗口按名称、虚拟地址和序号(如果有)排列这些条目。对于可执行文件,导出窗口至少会有一个条目,即程序的执行入口点,通常被命名为 start
。
2.3 导入窗口
导入窗口(见图4-7)的功能与导出窗口正好相反。它列出被分析的二进制文件中导入的所有函数。只有当二进制文件使用共享库时,IDA才会用到导入窗口。静态链接的二进制文件没有外部依赖,因此不需要导入其他内容。导入窗口中的每个条目列出一个导入项目(函数或数据)及其所在的库。由于导入的函数代码在共享库中,所以窗口中的地址是导入表条目的虚拟地址。
双击导入窗口中的条目,IDA会跳转到反汇编窗口中的相应地址,例如 0000000000004040
。在十六进制窗口中,该地址的内容可能显示为 ?? ?? ?? ?? ??
(见图4-8),因为IDA无法静态分析程序在运行时实际加载的内容。导入窗口提供了类似于 objdump -T
、readelf -S
和 dumpbin /IMPORTS
的功能。
需要注意的是,导入窗口只显示二进制文件需要动态加载的符号。如果二进制文件使用如 dlopen/dlsym
或 LoadLibrary/GetProcAddress
这样的机制自行加载符号,这些符号不会出现在导入窗口中。
2.4 结构体窗口(Shift+F9)
结构体窗口用于显示IDA识别出的二进制文件中的复杂数据结构(如C语言中的结构体和联合)。在分析过程中,IDA会查找函数类型签名库,尝试将函数参数类型与程序中的内存匹配起来。比如,在图4-9中的结构体窗口中,IDA识别出了程序使用了 Elf
数据结构。
IDA得出某个数据结构(如 sockaddr
)的结论,可能是因为它发现程序调用了如 connect
的网络函数。双击结构体的名称(本例为 Elf
)可以展开并查看该结构体的详细布局,包括每个字段的名称和大小。
结构体窗口的两个主要用途是:提供标准数据结构的布局参考,以及帮助你创建自定义数据结构,用于内存布局模板。第7章将详细介绍如何定义和使用这些结构体。
2.5 枚举窗口(Shift+F10)
枚举窗口类似于结构体窗口。如果IDA检测到标准的枚举数据类型(如C语言中的 enum
),它会在枚举窗口中列出这些数据类型。使用枚举可以替代整数常量,使反汇编代码更易读。和结构体窗口一样,你也可以在枚举窗口中定义自己的枚举类型,并在反汇编的代码中使用它们。
03 其他IDA显示窗口
我们来谈谈IDA默认情况下不会自动打开的窗口。你可以通过 `View -> Open Subviews` 命令打开这些窗口,但IDA在开始时不会自动显示它们,因为这些窗口提供的信息可能不是你最初需要的。
3.1 Strings窗口(Shift+F12)
Strings 窗口是IDA中的一个内置工具,用来显示从二进制文件中提取的所有字符串及其地址。虽然从IDA 5.2版开始,Strings窗口不再默认打开,但你可以通过 View -> Open Subviews -> Strings
命令手动打开它。
在Strings窗口中,你可以双击任何字符串,IDA会跳转到该字符串所在的地址。结合使用交叉引用功能,可以帮助你找到程序中引用这些字符串的代码位置。例如,如果你看到一个字符串 SOFTWARE\Microsoft\Windows\CurrentVersion\Run
,你可以找到程序为何引用这个Windows注册表项。
每次打开Strings窗口时,IDA都会重新扫描整个数据库以提取字符串。你可以通过右击窗口并选择 Setup
来配置字符串扫描的设置(见图4-11)。默认情况下,IDA会扫描至少包含5个字符的C风格、以null结尾的ASCII字符串。
要在Strings窗口中显示除了C风格字符串以外的其他类型的字符串,你需要在 Setup Strings
窗口中重新配置IDA的扫描设置。比如,Windows程序常用Unicode字符串,Borland Delphi文件则使用2字节的Pascal字符串。
在配置时,有两个重要的选项:
- Display only defined strings(仅显示已定义的字符串):选中后,Strings窗口只会显示IDA自动或手动定义的字符串,不会扫描其他类型的字符串。
- Ignore instructions/data definitions(忽略指令/数据定义):选中后,IDA会扫描代码中的字符串,即使这些字符串被误认为是指令或数据定义。这会产生很多不相关的字符串(类似使用
strings -a
命令)。
确保正确配置这些选项,否则IDA可能不会显示所有的字符串。例如,如果没有选中“Ignore instructions/data definitions”选项,IDA可能会遗漏一些字符串。
3.2 Names窗口(Shift+F4)
Names窗口(图4-12)列出二进制文件中的所有全局名称,这些名称是对程序虚拟地址的符号描述。IDA在加载文件时,会通过符号表和签名分析生成这些名称。你可以按字母或虚拟地址排序(升序或降序)这些名称。双击Names窗口中的名称,可以快速跳转到反汇编视图中显示该名称的位置。
Names窗口中的名称使用颜色和字母编码来表示不同类型:
- F: 常规函数,IDA认为这些函数不是库函数。
- L: 库函数,IDA通过签名匹配识别的库函数。如果库函数的签名不匹配,则标记为常规函数。
- I: 导入的名称,通常是共享库的函数名称,没有代码,只是一个导入声明。
- C: 命名代码,IDA找到的已命名程序指令位置,不属于任何函数。IDA在程序的符号表中找到一个名称,但没发现对程序位置的任何调用时,就会出现这种情况。
- D: 数据,表示全局变量的位置。
- A: 字符串数据,包含已知格式的字符串,如以’\0’结尾的ASCII字符串。
在反汇编代码中,你可能会发现一些位置在Names窗口中没有显示名称。IDA会为程序中所有被引用的位置生成名称。如果符号表中已有名称,IDA会使用这些名称。如果没有,IDA会自动生成名称,通常由位置的虚拟地址和一个类型前缀组成,以确保每个名称唯一。以下是一些常见的前缀:
- sub_xxxxxx: 地址 xxxx 是一个子例程。
- loc_xxxxxx: 地址 xxxx 是一个指令位置。
- byte_xxxxxx: 地址 xxxx 处的 8 位数据。
- word_xxxxxx: 地址 xxxx 处的 16 位数据。
- dword_xxxxxx: 地址 xxxx 处的 32 位数据。
- unk_xxxxxx: 地址 xxxx 处的数据大小未知。
3.3 段窗口(Shift+F7)
段窗口显示的是在二进制文件中出现的段的简要列表。需要注意的是,这里的“段”(segment)指的是文件结构中的节(section),而不是CPU中的内存段。窗口中列出了每个段的名称、起始和结束地址,以及权限标志。起始和结束地址表示该段在程序运行时的虚拟地址范围。以下是一个Windows二进制文件的段窗口示例:
段窗口允许你查看二进制文件中的段信息。双击一个条目,IDA将跳转到该段的起始位置。右击条目会显示一个菜单,你可以用它来添加新段、删除现有段,或编辑段的属性。这些功能特别适用于分析非标准格式的文件,因为这些文件的段结构可能尚未被IDA自动识别。类似的命令行工具包括 objdump -h
、readelf -s
和 dumpbin /HEADERS
。
3.4 签名窗口(Shift+F5)
IDA使用一个大规模的签名库来识别常见的代码模式,帮助确定二进制文件可能使用了哪些编译器或库函数。签名可以识别编译器生成的常用启动代码和已知的库函数,使你能够将更多精力集中在IDA无法自动识别的代码上。签名窗口展示了IDA对当前二进制文件使用的签名。窗口显示的是IDA识别出的相关签名,Windows PE文件的签名窗口的示例如下所示:
有两种情况你需要应用其他签名:
- IDA无法识别编译器:如果IDA无法识别用于构建二进制文件的编译器,导致无法选择合适的签名,你需要手动选择适当的签名来帮助IDA进行分析。
- 缺少库的签名:如果IDA没有针对某些库的现成签名,你可能需要自己创建这些库的签名,例如为
FreeBSD 8.0
中的OpenSSL
静态库创建签名。DataRescue
提供了工具包来生成自定义签名,我们将在第11章中详细介绍如何创建这些签名。
要应用新的签名,你可以在签名窗口中按INSERT
键或右击窗口,选择“Apply new signature
”(应用新签名),然后从已安装的签名中选择需要的签名。
3.5 类型库窗口(Shift+F11)
类型库窗口与签名窗口类似,它保存了IDA收集的关于常用编译器头文件中的数据类型和函数原型的信息。通过这些头文件,IDA能够识别常用库函数所需的数据类型,并为反汇编代码提供注释,还能了解复杂数据结构的大小和布局。这些信息存储在TIL
文件中,并可以应用于分析的二进制文件。
要使用其他类型库,你可以在类型库窗口中按INSERT
键,或右击窗口选择“加载类型库”。类型库将在第12章中详细介绍。
3.6 函数调用窗口
在任何程序中,一个函数可以调用其他函数,也可以被其他函数调用。函数调用图形显示了这些调用关系。这样的图形叫做函数调用图形或函数调用树(第8章介绍如何在IDA中生成这类图形)。我们有时只对特定函数的“近邻”感兴趣,即直接调用或被调用的函数。函数调用窗口就是用来显示这些“近邻”(如果Y直接调用X,或者X直接调用Y,则称Y是X的近邻)的。打开函数调用窗口时(图4-16),IDA会列出光标所在函数的直接调用和被调用的函数。
3.7 问题窗口
问题窗口显示了IDA在反汇编过程中遇到的困难和处理方法。虽然有时你可以通过调整反汇编代码来帮助IDA,但有些问题可能无法解决。即使在处理简单的二进制文件时,也可能会遇到问题。对这些问题的处理可能需要比IDA更深入的理解,但大多数人可能无法做到这一点。
(五)反汇编导航
01 基本IDA导航
1.1 双击导航
在分析阶段,IDA会通过检查二进制文件的符号表生成符号名称,或根据文件的引用自动创建名称。反汇编窗口中的名称可以像网页中的超链接一样用来导航,但它们没有超链接那么明显,你需要双击名称才能跳转到对应的位置。
如图5-1,你可以双击名称(黄色高亮), IDA会跳转到反汇编窗口中的相应位置。
为了方便导航,IDA将交叉引用视作导航目标。交叉引用通常显示为名称和十六进制偏移值。例如,loc_1113
右边的交叉引用表示它引用了__do_global_dtors_aux+15↑j
之前的字节位置。双击交叉引用,IDA会跳转到该引用的位置。第8章将详细介绍交叉引用的使用方法。
在IDA中,另一个重要的导航功能是十六进制值的显示。如果你在窗口中看到一个十六进制值,它代表了一个合法的虚拟地址,双击这个值,IDA会跳转到对应的反汇编位置。比如,双击标图中方框的值,IDA会转到该虚拟地址的位置。但如果双击标圆框的值,则不会发生任何操作。
IDA的输出窗口也支持双击导航。如果你在消息中看到一个导航目标,双击这条消息,IDA会跳转到相关的位置。
1.2 跳转到地址(G)
有时候,你知道想要跳转的具体地址,但反汇编窗口没有提供直接的名称。在这种情况下,你可以:
- 手动滚动:使用反汇编窗口的滚动条,找到目标地址。这适用于你知道虚拟地址时,因为窗口是按地址排序显示的。
- 函数窗口搜索:如果你知道名称(如某个子程序名),可以在函数窗口按字母排序找到并双击该名称,这样比手动滚动更快。
- 搜索功能:使用IDA的
Search
菜单输入已知的位置或名称来定位目标,尽管这在已知地址时可能显得繁琐。 - 跳转到地址:使用“跳转到地址”对话框,直接输入虚拟地址进行跳转,这是最快的方法。(见图5-5)
你可以使用Jump -> Jump to Address命令或按热键G来打开“跳转到地址”对话框。你可以在对话框中输入目标地址(无论是名称还是十六进制值),然后点击“OK”,IDA将直接跳转到这个位置。对话框还会记住你之前输入的地址,并通过下拉列表显示,以便你以后快速访问这些位置。
1.3 导航历史记录
IDA的文档导航功能与网页浏览器的功能类似,可以让你快速移动到不同的位置。每次导航到新位置时,IDA会将当前位置添加到位置列表中。你可以通过菜单操作或工具栏按钮快速返回之前的位置。这里有两种常见的导航方法:
- 前进和后退功能:
- 前进功能:使用Jump -> Jump to Next Position (跳转->跳转到前一个位置) 命令或按CTRL + ENTER可以跳转到列表中的下一个位置,相当于网页浏览器中的前进按钮。
- 后退功能:使用Jump -> Jump to Previous Position(跳转->跳转到下一位置)命令或按ESC可以跳转到当前位置的前一个位置,相当于网页浏览器中的后退按钮。
- 工具栏按钮:
- 工具栏上有两个导航按钮(见图5-7),分别对应前进和后退功能,旁边还有历史记录下拉列表,让你可以迅速访问导航历史中的任意位置。
02 栈帧
栈帧(stack frame)是一个低级概念,它指的是在程序运行时为每个函数调用分配的内存块,程序员通常将可执行语句分组,划分成叫做函数(也称过程、子例程或方法)的单元。
函数在未被调用时通常不需要内存。但是,当函数被调用时,它需要内存来处理以下几方面的需求:第一,函数可能需要存储传递给它的参数;第二,函数在执行过程中可能需要临时的存储空间,这些空间通过局部变量分配,仅在函数内部使用,函数结束后这些变量不再可用。
编译器使用栈帧(也叫激活记录)来自动管理函数的参数和局部变量,这些操作对程序员不可见。在函数被调用时,编译器会将函数参数放入栈帧,并分配足够的内存来存储局部变量,同时将函数的返回地址也存储在栈帧中。栈帧还支持递归调用,因为每次递归调用都有独立的栈帧,这样可以区分当前调用和之前的调用。 下面是调用一个函数的详细操作步骤。
- 传递参数:调用方将参数放到指定位置,这可能会改变栈指针的位置。
- 转交控制权:调用方使用指令(如x86的CALL或MIPS的JAL)将控制权交给被调用的函数,同时保存返回地址。
- 设置栈帧:被调用的函数设置栈指针,并保存需要保留的寄存器值。
- 分配局部变量空间:被调用的函数为局部变量分配空间,通常是通过调整栈指针来实现。
- 执行函数操作:函数执行其任务,可能会使用传递的参数,并将结果放到特定寄存器中以供调用方使用。
- 释放局部变量空间:函数执行完毕后,释放为局部变量分配的栈空间。通常,逆向执行第4步中的操作,即可完成这个任务。
- 恢复寄存器:将之前保存的寄存器值恢复到调用方(第3步),这包括恢复帧指针寄存器。
- 返回控制权:函数使用指令(如x86的RET或MIPS的JR)将控制权返还给调用方,可能还会清除栈中的参数。
- 清理栈:调用方在重新获得控制权后,它可能需要删除程序栈中的参数。这时可能需要调整栈,恢复栈指针到函数调用前的位置。
步骤3和4通常在函数开始时进行,称为函数的序言;步骤6到8在函数结束时进行,称为函数的尾声;步骤5是函数的主体部分,它们是调用一个函数时执行的全部操作。
2.1 调用约定
了解栈帧的基本概念后,我们可以更深入地探讨它们的结构。以下以x86架构和常见编译器(如Microsoft Visual C++或GNU gcc/g++)为例:
- 创建栈帧:栈帧的创建主要涉及将函数参数存入栈中。调用函数需要将参数正确地放入栈中,否则可能会出现问题。
- 调用约定:每个函数都有一个调用约定,规定了参数如何传递。
调用约定规定了如何传递函数参数,包括参数放在寄存器、栈中,或两者结合的方式。在传递参数时,程序栈还要决定参数使用后谁负责从栈中删除这些参数:有些约定由调用函数负责,有些则由被调用函数负责。
(1)C调用约定
在x86体系结构中,许多C编译器默认使用一种叫做cdecl
的调用约定。如果默认的调用约定被重写,则C/C+程序中常用的cdecl
修饰符会迫使编译器利用C调用约定。这个约定规定:函数参数从右到左依次放入栈中,而且调用函数负责在函数执行完后清理栈中的参数。
在cdecl
调用约定中,参数从右到左放入栈中,这样第一个参数总是位于栈顶,便于访问。这种约定适用于参数数量可变的函数(如printf
)。调用函数负责清理栈中的参数,因此在函数返回后,常会看到栈指针被调整。这样做的好处是调用方知道自己传递了多少个参数,可以准确地进行调整,而被调用的函数无法预知参数数量,难以进行正确的栈调整。
(2)标准调用约定
stdcall
是微软定义的一种调用约定,在函数声明中用 _stdcall
修饰符表示。与 cdecl
调用约定类似,stdcall
也按从右到左的顺序将参数放入栈中,但有一个重要区别:stdcall
规定由被调用的函数负责清理栈中的参数。这意味着被调用的函数必须知道栈中有多少个参数,这对于参数数量固定的函数是可行的,但对于参数数量可变的函数(如 printf
)则不适用。例如,stdcall
调用约定下,如果一个函数需要三个整数参数,总共占用12个字节,函数会在返回时使用特定的 RET
指令来清理这些参数。
stdcall
调用约定的主要优点是,它使得程序在每次函数调用后不需要额外的代码来清理栈中的参数,从而可以生成体积更小、速度更快的程序。微软通常对所有由共享库(DLL)文件提供的、参数数量固定的函数使用 stdcall
调用约定。如果你需要为这些共享库生成函数原型或与之兼容的代码,请注意这一点。
(3)x86 fastcall约定
fastcall
调用约定是 stdcall
调用约定的一种变体,它通过 CPU 寄存器(而非程序栈)最多传递两个参数。具体来说,前两个参数会分别放在 ECX
和 EDX
寄存器中,其余参数则按 stdcall
的方式从右到左放在栈上。fastcall
调用约定的函数在返回时也负责清理栈中的参数。这个约定可以让程序运行更快,因为寄存器访问比栈访问更迅速。
(4)C++调用约定
在 C++ 中,非静态成员函数需要一个指针 this
来指向调用该函数的对象。不同编译器处理 this
指针的方式不同:
- Microsoft Visual C++ 使用
thiscall
调用约定,将this
指针放在ECX
寄存器中,并要求非静态成员函数在结束时清除栈中的参数。 - GNU g++ 则把
this
作为第一个隐含参数放到栈顶,其他参数按照cdecl
约定处理,调用方负责在函数返回时清理栈中的参数。
(5)其他调用约定
了解不同的调用约定非常重要,因为它们决定了如何在程序中传递参数。以下是几个关键点:
- 调用约定种类繁多:各种语言、编译器和CPU都有不同的调用约定。遇到不常见的编译器时,可能需要自己研究调用约定。
- 主流调用约定:为了确保函数能够被其他程序员轻松调用,函数通常遵循主流的调用约定。
- 内部函数优化:为了优化性能,内部函数可能使用特别的调用约定,这些约定可能只为特定程序所知。例如,Microsoft Visual C++中的
/GL
选项和GNU g++中的regparm
关键字就是用于优化的调用约定。 - 汇编语言:使用汇编语言编写的程序可以完全自定义参数传递方式。如果函数仅供内部使用,程序员可以自由选择最合适的调用约定。
- 系统调用:系统调用是请求操作系统服务的特殊函数调用。不同操作系统和CPU有不同的系统调用方式。例如,Linux系统调用使用
int 0x80
或sysenter
指令,而其他系统可能只用sysenter
。参数通常在栈上,系统调用编号在特定寄存器中。
2.2 局部变量布局
函数参数的传递方式由调用约定规定,但函数的局部变量如何存储没有统一的标准。编译器在编译时负责决定函数局部变量的存储位置。它首先计算需要多少空间来存储这些变量,然后决定这些变量是放在CPU寄存器中还是程序栈上。具体的存储方式与函数的调用方和被调用函数无关,因此从函数的源代码中通常无法得知局部变量的具体布局。
2.3 IDA栈视图
栈帧是程序运行时的一种内存结构,通过分析二进制文件中的代码,即使函数可能没有运行,我们仍可以了解每个函数的栈帧结构。IDA Pro等工具通过记录 push
和 pop
操作及其他栈指针的变化来推测栈帧的布局。这包括:
- 确定栈帧的大小:分析出为函数分配的内存空间。
- 检查是否使用专用帧指针:例如识别
push ebp
和mov ebp, esp
指令。 - 识别内存引用:找出对函数参数和局部变量的内存访问。
了解函数的行为需要了解它操作的数据类型。在查看反汇编代码时,分析栈帧的结构是理解函数数据的关键步骤。
(以后补充)
03 搜索数据库
在IDA中,要进行更广泛的搜索,可以使用搜索菜单中的选项。例如,Search -> Next Code
命令可以将光标移动到下一个指令位置。跳转菜单也提供了多种导航选项,比如 Jump -> Jump to Function
命令允许你查看所有函数并跳转到其中一个。
此外,IDA还有两种重要的通用搜索功能:文本搜索和二进制搜索。
3.1 文本搜索(ALT+T)
在IDA中,你可以使用文本搜索功能来查找反汇编列表中的特定字符串。通过 Search -> Text
(快捷键:ALT+T),你可以打开文本搜索对话框。这个对话框提供了多种搜索选项,包括使用正则表达式进行搜索。
值得注意的是,“标识符”(Identifier)搜索实际上是查找完整的单词,而不是部分匹配。例如,如果你搜索“401116”,它不会找到像“loc_401116”这样的符号。
选择“Find all occurrences”(查找所有结果),这样会在一个新窗口中显示所有匹配的搜索结果,方便你导航到每一个结果。要继续查找下一个匹配项,可以使用快捷键 CTRL+T
或选择 Search -> Next Text
(搜索->下一个文本)命令。
3.2 二进制搜索(ALT+B)
如果你需要搜索特定的字节序列(如已知的十六进制值),就应该使用IDA的二进制搜索工具,而不能使用文本搜索。二进制搜索专门用于十六进制视图窗口,而文本搜索则适用于反汇编窗口。要进行二进制搜索,使用 Search -> Sequence of Bytes
或按 ALT+B
。你可以在对话框中输入十六进制字节序列(如CA FE BA BE
)或ASCII字符串,这样IDA就会找到这些字节序列,无论你是否选择了Case-sensitive(区分大小写)的选项。
对话框")
如果你想搜索内嵌的字符串数据(如ASCII字符串),在搜索框中必须将搜索字符串用引号括起来。使用“Unicode Strings”选项可以搜索字符串的Unicode版本。
区分大小写选项在搜索时非常重要。对于普通字符串搜索,如果没有选中该选项,“hello” 也会匹配 “HELLO”。但在十六进制搜索中,如果没有选中该选项,0x41(字符A)和0x61(字符a)会被认为是匹配的。因此,在不区分大小写的十六进制搜索中,0x41 和 0x61 可能被认为是相同的字符。
使用 CTRL+B
或 Search -> Next Sequence of Bytes
(搜索->下一个字节序列) 可以搜索随后的二进制数据。你不需要在十六进制视图窗口中进行二进制搜索,IDA 允许你在反汇编窗口中指定二进制搜索。如果找到匹配的字节序列,反汇编窗口会自动跳转到相应的位置。
(六)反汇编操作
01 名称与命名(N)
在IDA中,你会看到两种类型的名称:一种是虚拟地址相关的名称,另一种是栈帧变量的名称。IDA自动生成这些名称,但它们通常没有实际意义,无法帮助你理解程序的功能。为了更好地分析程序,你可以将这些默认名称更改为更有意义的名称。IDA允许你随意修改名称,方法是点击名称并按 N
键,或右击名称选择 “Rename”。接下来几节会详细介绍如何更改栈变量和已命名位置的名称。
1.1 参数和局部变量
栈变量的名称( 像 `arg_0`、`local_4` 这样的默认名称。 )通常是反汇编代码清单中最简单的,因为它们与特定的虚拟地址无关,也不会出现在名称窗口中。它们只在特定函数内部有效,所以不同函数可以有相同的名称(如 `arg_0`)。
修改变量名称后,IDA会在当前函数中更新所有使用旧名称的地方。如果你想恢复变量的默认名称,只需在更名对话框中输入一个空白名称,IDA会自动生成默认名称。
1.2 已命名的位置
重命名一个已命名的位置或给一个未命名的位置取名与重命名栈变量的过程略有不同。虽然打开更名对话框的方法相同(按热键 N
),但操作步骤不同。与已命名的位置有关的更名对话框如图6-1所示。
该对话框显示你命名的具体地址,以及一些与该名称有关的特性。最大名称长度对应于IDA的一个配置文件(cfg/ida.cfg)中的某个值。如果你输入的名称超过了这个长度,IDA会提醒你,并询问是否要增加当前数据库的最大名称长度。如果你同意,新的长度设置只会在当前数据库中生效,其他新创建的数据库仍会使用原来的设置。
下面的特性可能与某个已命名的位置有关。
- Local names(局部名称)。仅在当前函数内部有效。例如,两个不同的函数可以有相同的局部名称,但一个函数中不能有两个相同的局部名称。局部名称通常用于标记函数内的跳转目标。
- Include in names list(包含在名称列表中)。 选择这个选项会将名称添加到名称窗口中,方便你以后快速找到这个名称的位置。默认情况下,自动生成的名称(哑名)不包含在名称窗口中。
- Public name(公共名称)。 通常用于表示由二进制文件(如共享库)输出的名称。选择这个选项可以将一个符号标记为公共名称,主要用于在反汇编代码和名称窗口中添加注释,不会影响代码本身。
- Autogenerated name(自动生成的名称)。 这个选项不影响反汇编代码本身,只是标记名称是否为自动生成的。
- Weak name(弱名称)。 指弱符号(weak symbol),这是一种特殊的公共符号,仅在没有找到相同名称的符号时使用。在IDA中标记为弱名称并不会改变反汇编代码的行为。
- Create name anyway(无论如何都要创建名称)。 通常,函数内部或全局范围内不会有重复的名称。选择这个选项可以在出现命名冲突时强制创建名称,但其具体行为可能因名称类型的不同而有所不同。
如果你在IDA中编辑一个全局名称(如函数名或全局变量),并尝试使用一个已经存在的名称,IDA会弹出一个名称冲突对话框。IDA会自动添加一个唯一的数字后缀来解决这个冲突。这个对话框会出现,不管你是否选择“Create name anyway
”选项。
如果你正编辑一个函数内的局部名称,并尝试使用一个已经存在的名称,IDA默认会拒绝这个操作。若你坚持使用这个名称,需要选择“Create name anyway
”选项,这样IDA会为该局部名称自动添加一个唯一的数字后缀。解决名称冲突的最佳方法是选择一个未被使用的名称。
1.3 寄存器名称(N)
在函数内部,你可以给寄存器重新命名。如果你希望用更具体的名称代替默认的寄存器名(如将EDX重命名为更具意义的名字),只需通过热键N或右键点击寄存器名称并选择“Rename”选项。重命名寄存器时,你实际上是提供了一个别名,IDA会用这个别名来替代寄存器的原始名称。注意,寄存器重命名仅限于函数内部的代码,函数外部的代码无法重命名寄存器。
02 IDA中的注释
IDA提供了几种注释类型,每种类型适用于不同的目的。你可以通过 Edit->Comments
命令、热键或上下文菜单来添加注释。
大多数IDA注释通常以分号开头,表示分号后的内容是注释。
2.1 常规注释(:)
最简单直接的注释是常规注释,位于汇编代码行的末尾。右击反汇编窗口的右边缘或使用冒号(:)热键,可以打开“输入注释”对话框添加注释。如果你输入了多行注释,它们会在反汇编窗口的右侧显示,每行前都有分号,并与第一个分号对齐。要编辑或删除这些注释,就必须需重新打开“输入注释”对话框。常规注释默认以蓝色显示。
2.2 可重复注释(;)
可重复注释一旦输入,将会自动出现在反汇编窗口中的许多位置。它们的颜色通常是蓝色,可能与常规注释难以区分。可重复注释的特别之处在于它们会在引用它们的位置自动显示,这与交叉引用的概念有关。默认情况下,这些回显的注释是灰色的, 常规注释只显示在代码行的右侧,而可重复注释在代码的多个位置都会出现 。可重复注释的热键是分号(;)。
如果你在一个显示可重复注释的位置添加常规注释,常规注释会覆盖原来的可重复注释,这个位置只会显示常规注释。删除常规注释后,原来的可重复注释会重新显示。
可重复注释的一种变体与字符串有关。如果自动创建了字符串变量,它会在该变量的位置显示一段虚拟的可重复注释。虚拟注释显示了字符串的内容,并且在整个数据库中自动出现。用户不能编辑这些虚拟注释,它们像普通的可重复注释一样,出现在所有引用字符串变量的地方。
2.3 在注释前与注释后
“在前注释”和“在后注释”是指分别出现在某行代码之前或之后的注释,这些注释与分号不同,不以分号开头。通过比较某行的地址与其前后指令的位置,可以区分“在前注释”和“在后注释”。如果注释在代码行地址之前,则为“在前注释”;如果在代码行地址之后,则为“在后注释”。
2.4 函数注释
为函数添加注释,这些注释会显示在函数的反汇编代码顶部,包括函数原型。要添加注释,首先选中函数名称,然后输入你想要的常规注释或可重复注释。常规注释只会在函数顶部显示一次,而可重复注释会在所有调用该函数的位置显示。当你使用“Set Function Type
”命令时,IDA会自动生成函数原型注释。
03 基本代码转换
当处理自定义文件格式或不常见的二进制文件时,IDA提供了几种工具来帮助你调整反汇编代码,包括:
- 将数据解释为代码;
- 将代码解释为数据;
- 标记特定指令序列为函数;
- 修改函数的起始或结束地址;
- 改变指令操作数的显示方式。
3.1 代码显示选项
你可以对反汇编代码做的最简单的转换是自定义每行代码的显示内容。每行反汇编代码由多个部分组成,比如标签、助记符和操作数。通过 Options -> General
命令打开“IDA Options”对话框,然后选择“Disassembly”选项卡,为每一个反汇编行选择其他需要显示的部分,如图6-3所示。
在IDA的右上角“Display disassembly line parts”(显示反汇编行部分)区域,你可以选择显示反汇编行的不同部分。默认情况下,它会显示行前缀、注释和可重复注释。下面说明其中的每一个选项。
- Line prefixes(行前缀)。行前缀 是每个反汇编行开头显示的
section:address
部分。如果不选择显示这个选项,反汇编行的开头将不会显示这些信息。 - Stack Pointer(栈指针)。栈指针用于跟踪程序栈的变化,启用这个选项后,IDA将显示每个函数中栈指针的变化情况。例如,它会显示栈指针在函数执行过程中的增减量,以及函数退出时栈指针是否恢复原值。如果IDA检测到函数返回时栈指针的值不为0,它会标记一个错误并将相关指令以红色显示。这种标记有时可能是为了防止自动分析,其他时候则可能是因为编译器生成了IDA无法准确分析的“序言”和“尾声”代码。
- Comments(注释)和Repeatable comments(可重复注释)。取消任何一个选项,IDA将不会显示相应类型的注释。
- Auto comments(自动注释)。自动注释 是IDA自动为某些特殊指令添加的注释,用来帮助解释指令的行为。IDA不会为简单指令(如
x86mov
)添加自动注释。用户自定义的注释会覆盖自动注释,因此如果希望看到IDA的自动注释,需要先删除任何注释(常规注释或可重复注释)。 - Bad instructionsmarks(无效指令标记)。无效指令(标记)是IDA用来标记那些处理器认为合法,但某些汇编器可能无法识别的指令。IDA会将这些指令作为数据字节处理,并在反汇编时用
<BAD>
注释标记它们。这样做的目的是生成大多数汇编程序能处理的反汇编代码。 - Numbers of opcode bytes(操作码字节数)。操作码字节数选项允许你设置IDA显示每个指令的机器语言字节的数量。这样,你可以在反汇编窗口中看到每条汇编指令对应的十六进制字节。
对于指令长度固定的处理器,显示操作码字节很简单。但对于像x86这种指令长度可变的处理器,指令可能从1字节到十几字节不等。IDA会在反汇编代码中为你指定的字节数预留显示空间,不论指令的实际长度,以确保足够的空间来显示这些操作码字节,并将其他部分移到右边。
3.2 格式化指令操作数
在反汇编时,IDA会决定如何格式化每条指令的操作数,特别是整数常量。这些常量可以表示跳转偏移、全局变量地址、算术运算中的值或程序员定义的常量。IDA尽量使用符号名称而非数字,以提高可读性。有时,IDA根据指令的上下文(如调用指令)来决定格式,其他时候则根据数据(如全局变量地址或栈偏移)来决定。如果常量的具体用途不明确,IDA通常会将其格式化为十六进制常量。
如果你擅长使用十六进制, 只需右击反汇编窗口中的任何常量,即可打开一个上下文菜单。
在上图中,你可以将常量(如10h
)重新格式化为十进制、八进制或二进制值。如果常量是ASCII字符,菜单还可以将其格式化为字符常量。选择不同的选项时,菜单会显示具体的替代文本。
程序员在源代码中常使用命名常量,如使用#define
语句(或其等效语句)定义的常量或枚举常量。如果编译器已经完成对源代码的编译,IDA就无法直接区分这些常量是符号常量、文字常量还是数字常量。
IDA维护了一些常见库(如C标准库或Windows API)的命名常量,用户可以通过常量值的上下文菜单中的“Use standard symbolic constant(使用标准符号常量)”选项来访问这些常量。在选择这个选项后,会弹出一个符号选择对话框(见图6-5)。
在尝试格式化常量值后,IDA会从内部常量列表中筛选出相关的常量。在这个例子中,我们看到的是所有IDA认为与0H
相等的常量。如果我们知道这个值用于创建X25网络连接,我们可以选择名为AUTH_OK
的常量,最终反汇编代码行会显示为使用该常量。 标准常量列表非常有用,可用于确定某个特殊的常量是否与一个已知的名称有关。
6.3 操纵函数
在初步的自动分析完成之后,你可能需要手动调整函数。IDA可能无法找到某些函数调用,或者错误地确定函数的开始和结束位置。比如,函数可能被分割到多个地址,或编译器为了节省空间将多个函数合并在一起,这些情况都需要你手动修正反汇编代码中的错误。
1.新建函数
当你需要在没有函数定义的地方创建一个新函数时,可以这样做:将光标放在你想创建函数的起始位置,然后选择 Edit -> Functions -> Create Function
。IDA会尝试将数据转换为代码,并分析函数结构。如果IDA找到函数的结束部分,它会生成新的函数名,并重新组织代码。如果无法找到函数结束部分或遇到非法指令,函数创建失败。
2.删除函数
要删除一个函数,可以使用 Edit -> Functions -> Delete Function
命令。如果你觉得IDA自动分析时出现了错误,可以用这个命令来删除不正确的函数。
3.函数块
在Microsoft Visual C++编译器生成的代码中,经常会看到“函数块”。编译器会将不常执行的代码移到不容易被换出的内存位置,以确保经常执行的代码始终保存在内存中,从而形成了这些函数块。
如果一个函数以这种方式被分割,IDA会通过跟踪这些块之间的跳转来找到所有相关的块。多数情况下IDA能够找到所有块,并在函数的开头列出每一个块。
有时,IDA可能找不到函数的所有块,或错误地把函数识别成块。此时,你可以手动创建或删除函数块。要创建新的函数块,选择一个地址范围(确保它不属于现有的函数),然后点击Edit -> Functions -> Append Function Tail
。之后,IDA会让你从已定义的函数列表中选择这个块所属的函数。
要删除现有的函数块,将光标放在要删除的块中的任何一行上,然后选择Edit->Functions->Remove Function Taill
即可。
如果函数块让分析变得复杂,可以在最初加载文件时取消选择Create function tails
选项,这样IDA就不会自动创建函数块。这个选项在加载对话框的Kernel Options
(核心选项) 中可以找到。禁用后,函数中将出现指向函数外部的跳转,IDA会在反汇编视图的左侧用红线和箭头标出这些跳转,但在图形视图中不会显示这些跳转的目标。
4.函数特性
IDA为它识别的每一个函数提供许多特性。如图6-6所示的函数属性对话框可用于编辑其中的某些特性。
下面说明每一个可修改的属性。
- 函数名称。提供另外一种更改函数名称的方法。
- 起始地址。函数中第一条指令的地址。
- 结束地址。函数最后一条指令之后的地址,通常是函数的返回语句之后。如果IDA自动分析无法正确识别这个结束地址,你需要手动设置它。请注意,这个地址不是函数的一部分,而是函数最后指令后的地址。
- 局部变量区。函数的局部变量专用的栈字节数。多数情况下,IDA会通过分析函数的栈指针的行为,自动计算出这个值。
- 保存的寄存器。函数调用时用于保存寄存器值的内存字节数。IDA假设这些寄存器保存在返回地址的上方和局部变量的下方。然而,一些编译器可能将这些寄存器保存在局部变量的上方。IDA会把这种情况视为局部变量区域,而不是专门的保存寄存器区域。
- 已删除字节。指函数返回时,IDA从栈中移除的参数字节数。对于
cdecl
函数,这个值通常为0,因为调用方负责清理栈。而对于stdcall
函数,这个值则表示所有传递给函数的参数占用的栈空间。如果IDA检测到使用了特定的返回指令,它会自动计算出这个值。 - 帧指针增量。有时候编译器可能会调整帧指针,使其指向局部变量区域中的特定位置,而不是指向栈帧的底部。这段距离称为帧指针增量(frame pointer delta)。大多数情况下,IDA会自动计算这个增量。使用增量的目的是在离帧指针1字节(带符号)的偏移量(-128~+127)内保存尽可能多的栈帧变量。
还有一些复选框可以用来设置函数的特性。这些选项通常是IDA自动分析的结果,你可以选择启用或禁用这些属性。
- 不返回。 函数不会回到它的调用方。
- 远函数。 这个属性标记函数为“远函数”,通常用于分段架构中。当调用这样的函数时,需要指定段和偏移值。是否使用“远调用”一般由程序的内存模式决定,而不是由硬件架构决定。
- 库函数。 这个属性将函数标记为库代码,通常是编译器提供的函数或静态链接库中的函数。标记为库函数后,该函数会以特殊颜色显示,以便与普通代码区分开来。
- 静态函数。除在函数的特性列表中显示静态修饰符外,其他什么也不做。
- 基于BP的帧。 这个特性表示函数使用了帧指针(BP寄存器)。IDA会自动识别是否使用了帧指针,如果分析失败,你可以手动选择这个特性。如果选择了这个特性,你需要相应地调整保存的寄存器大小(通常指根据保存的帧指针的大小增大)和局部变量的大小(通常指根据保存的顿指针的大小减少)。基于帧指针的栈引用会用符号名称显示,而不是数字偏移量。如果没有这个特性,IDA会认为栈引用是基于栈指针的。
- BP等于SP。 这个特性表示函数将帧指针(BP)设置为与栈指针(SP)相同,即在进入函数时,帧指针和栈指针指向栈帧的顶端。启用这个特性后,帧指针增量的大小将等于局部变量区域的大小。
5.栈指针调整
IDA 会尽力跟踪函数内每条指令对栈指针的变化,如果 IDA 无法确定某条指令是否更改了栈指针,你需要手动调整栈指针。
如果一个函数调用了另一个使用 stdcall
调用约定的函数,而这个被调用的函数位于 IDA 无法识别的共享库中,IDA 可能不知道该函数使用了 stdcall
调用约定,从而无法正确跟踪栈指针的变化。这会导致IDA 在分析时显示错误的栈指针值。
6.4 数据与代码互相转换
在自动分析阶段,字节有时可能被错误地归类。例如,数据字节可能被误当作代码字节反汇编成指令,而代码字节可能被误当作数据字节显示成数据值。这种错误可能发生在编译器将数据嵌入代码部分时,或是代码字节没有被直接引用时,因而IDA不会对它们反汇编。特别是在模糊程序中,代码和数据之间的界限可能变得更加模糊。
要重新格式化反汇编代码,首先需要删除当前的格式设置(代码或数据)。右键点击要取消定义的项目,选择 Undefine
(或使用 Edit -> Undefine
命令或快捷键 U
),即可取消对函数、代码或数据的定义。取消定义后,这些字节将重新以原始值显示。你可以通过“点击并拖动”来选择一个地址范围,批量取消定义。
要反汇编一组未定义的字节,右键其中的第一个字节,在上下文菜单中选择 Code
(或使用 Edit -> Code
命令或快捷键 C
)。这样IDA 会从这个字节开始反汇编,直到遇到已定义的项目或非法指令。在进行代码转换前,你可以通过“点击并拖动”来选择一个地址范围,批量进行代码转换。
将代码转换为数据比反汇编要复杂。你不能通过上下文菜单直接完成这个操作,而是需要使用 Edit -> Data
命令或快捷键 D
。如果你想批量转换指令为数据,最简单的方法是先取消这些指令的定义,然后对这些区域进行数据格式化。
04 基本数据转换
4.1 指定数据大小(ALT+D)
调整数据大小是修改数据最直接的方法。IDA提供了几种常见的数据类型说明符,如 db
(1字节)、dw
(2字节)和 dd
(4字节)。你可以通过“Options -> Setup Data Types
”(选项->设置数据类型) 对话框来更改数据大小。
这个对话框有两个部分。左侧有按钮,用于立即改变选中数据的大小。右侧有复选框,用于设置“数据转盘”(data carousel),这是一个包含你选择的数据类型列表。每个左侧的按钮在右侧都有对应的复选框。数据转盘中的数据类型不会立即影响显示,但当你右击数据项时,数据转盘中的类型会出现在上下文菜单中。你可以根据图6-7选中的数据类型,将数据项重新格式化为字节、字或双字数据。
数据转盘这个名字来源于快捷键D的功能。当你按下D键时,当前选中的数据项会按照数据转盘列表中的下一个数据类型重新格式化。例如,如果转盘列表中的数据类型顺序是db
、dw
、dd
,那么按下D键后,db
会变成dw
,dw
变成dd
,dd
又变回db
,这样数据类型就循环了一遍。如果你对非数据项(比如代码)使用热键D,该项将会被格式化为转盘列表中的第一个数据类型(例如db
)。
切换数据类型会改变数据项的大小,可能会使它变大、变小,或者保持不变。
- 保持不变:只有数据的显示格式发生了变化。
- 缩小:比如从4字节(
dd
)变成1字节(db
),多余的字节会变成未定义字节。 - 增大:如果你将一个小的数据项(如1字节)转换成更大的数据项(如4字节),而后面有已定义的字节,IDA会提示你是否要取消定义这些字节,以便扩展当前的数据项。这个提示信息通常是:“直接转换成数据吗?”这意味着IDA会取消定义足够多的后续字节,以满足新的数据项的需求。例如,从字节数据(
db
)转换成双字数据(dd
)时,会需要额外的3字节。
你可以为任何数据位置(包括栈上的变量)指定数据类型和大小。要更改栈上变量的大小,首先双击该变量以打开详细栈帧视图,然后调整变量的大小。
4.2 处理字符串
IDA可以识别多种字符串格式。默认情况下,它会查找并格式化C风格的字符串、以空字符结尾的字符串。要强制将数据格式化为字符串,可以通过Edit -> Strings
菜单选项选择所需的字符串风格。如果选中的字节符合这种字符串格式,IDA会将它们合并成一个字符串变量。你也可以使用快捷键A按照默认字符串风格对选中的位置进行格式化。
有两个对话框可以用来设置字符串数据格式。第一个对话框可以通过选择 Options -> ASCII String Style
命令打开。这个对话框类似于“数据类型配置”对话框。左侧的按钮可以用来在选定的位置创建特定格式的字符串,但数据必须符合所选的格式。对于以字符结尾的字符串,可以在对话框底部设置两个终止符。右侧的单选按钮用来选择按下快捷键A时的默认字符串格式。
第二个对话框用于配置与字符串相关的设置。你可以通过 Options -> General
打开这个对话框,并点击上面的 Strings
选项卡来进行配置。这个对话框允许你指定默认的字符串类型,但大多数选项与字符串的命名和显示有关。右侧的 Name generation
(名称生成) 区域只会在你选择 Generation names
(生成名称) 选项时显示。如果不启用 名称生成
,IDA 将给字符串变量分配以 asc_
开头的默认名称。
启用名称生成后,Name generation
选项会控制IDA如何为字符串变量生成名称。如果没有选择 Generate serial names
(默认设置),IDA会使用指定的前缀和从字符串中提取的字符来生成名称,生成的名称长度不会超过当前的最大名称长度。
名称的首字母要大写,在生成名称时,禁止用于名称的字符(如空格)会被省略。如果选择了 Mark as autogenerated
(标记为自动生成),生成的名称(通常是深蓝色)会和用户指定的名称(默认为蓝色)用不同的颜色显示。Preserve case
(保留大小写) 选项会保留字符串中的原始大小写,而不是转换成首字母大写。选择 Generate serial names
选项会在名称后附加数字(如 a000
、a001
、a002
),数字的长度由 Width
字段控制。
4.3 指定数组
反汇编代码通常不会提供数组的大小信息。某个已命名变量 unk_8048560
后面可能有数据声明,只有第一个元素被直接引用,表明它是数组的第一个元素。数组中的其他元素通常通过复杂的索引计算间接引用。
要创建数组,请选择数组的第一个元素(本例为 unk_8048560
),然后通过 Edit -> Array
打开“创建数组”对话框。如果选定的位置已有数据项定义,右击该项会在菜单中显示 Array
选项。数组的类型取决于第一个元素的数据类型,比如这里创建了一个字节数组。
下面是该对话框中用于创建数组的字段。
- Array element Width(数组元素宽度):这个值表示数组中每个元素的大小(这里是1字节)。它由你在对话框中选择的数据类型决定。
- Maximum possible size(最大可能大小):它决定在遇到另一个已定义的数据项之前,可包含在数组中的元素(不是字节)的最大数目。你可以指定更大的值,但这样做需要确保后面的数据项是未定义的,以便将它们包括在数组中。
- Number of elements(元素数量)。你可以在这里指定数组的具体大小。数组占用的总字节数可通过“元素数量×数组元素宽度”计算得出。
- Items on a line(行中的项目)。指定在每个反汇编行显示的元素的数量。通过它可以减少显示数组所需的空间。
- Element width(元素宽度)。这个值仅用于格式化。当一行显示多个项目时,它控制列宽。
- Use“dup”construct(使用重复结构)。这个选项可将相同的数据值合并起来,用一个重复说明符组合成一项。
- Signed elements(有符号元素)。表示将数据显示为有符号还是无符号的值。
- Display indexes(显示索引)。使数组索引以常规注释的形式显示,帮助你更容易在大型数组中找到特定数据。此选项还启用“Indexes”单选按钮,让你可以选择显示索引的格式。
- Create as array(创建为数组)。 该选项默认处于选中状态,表示你要将选择的数据项合并成一个数组。如果你只想指定一段连续的数据项,而不将它们视作一个数组,可以取消这个选项。
(七)数据类型与数据结构
IDA不仅可以传播库函数的类型信息,还可以传播你在数据库中设置的任何函数的参数名称和数据类型。初始分析阶段,IDA通常会将所有函数参数标记为默认的int
类型,并给它们分配哑名。要设置函数的实际类型信息,你需要使用Edit -> Functions -> Set Function Type
命令,或右键点击函数名称选择Set Function Type
(或者使用快捷键Y
)。这样你可以在弹出的对话框中输入正确的函数原型,如图8-1所示。
01 识别数据结构的用法
1.1 数组成员访问
在IDA中处理数组成员的访问涉及识别数组结构、定义数组、访问数组元素、以及分析代码如何操作数组。
1.全局分配的数组
全局数组通常位于数据段中。IDA会自动分析数据段,但如果需要,你可以手动检查数据段中的数据。全局数组的数据通常是连续的内存块,通过检查这些数据,观察数据的格式和大小,确定数据是否按照某种规律排列,如字节(byte
)、字(word
)或双字(dword
),有这些规律则可推断出存在数组。
要创建数组,可右击选择的地址, 从上下文菜单中选择 Edit -> Array
(或使用热键 A
),打开“创建数组”对话框。
在反汇编代码中,查找对数组的访问。通常,数组访问涉及计算索引偏移量,然后从数组的起始地址中读取数据。例如,通过 MOV EAX, [ARRAY + EBX * 4]
指令,EBX
寄存器中的值乘以元素宽度(4字节)后加到数组起始地址 ARRAY
上,访问数组元素。如果数组元素的类型发生变化,可以通过右键选择 Edit -> Array
,然后调整元素宽度来修改定义。
示例:
假设你有一个整数数组在内存地址 0x00402000
开始,每个元素为 4 bytes
(双字),长度为 100
个元素。以下是如何在IDA中处理该数组的步骤:
(1)选择起始地址0x00402000
。
(2)创建数组:
- 右击选中的地址,选择
Edit -> Array
。 - 设置 Array Element Width 为
4 bytes
。 - 设置 Maximum Possible Size 为
100
元素。 - 选择 Display Indexes 以显示索引。
- 确保 Create as Array 选项已选中。
(3)查看数组内容。
2.栈分配的数组
栈分配的数组通常位于函数的栈帧中。函数的栈帧包括函数参数、局部变量和保存的寄存器等。 栈上的数组通常通过栈帧基指针(如 EBP
或 RBP
)加上偏移量来访问。
要查看栈分配的数组, 首先查找函数的开头部分,通常会有设置栈帧基指针的指令(如 MOV EBP, ESP
) 。 代码中可能会有类似以下的指令:
MOV EAX, [EBP-0x10] ; 访问栈帧中偏移量为 -0x10 的数组元素
这种指令访问的是栈上数组的元素。偏移量通常是基于栈帧基指针的负值,表示局部变量区域中的数据。
创建数组的步骤与全局分配数组相同。
示例:
假设在函数 myFunction
中,你发现栈上分配了一个 int
类型的数组,从 EBP-0x20
开始,包含 10
个元素。以下是处理该数组的步骤:
(1)找到起始地址EBP-0x20
。
(2)创建数组:
- 右击
EBP-0x20
的地址,选择Edit -> Array
。 - 设置 Array Element Width 为
4 bytes
(因为int
通常是4 bytes
)。 - 设置 Maximum Possible Size 为
10
(表示10
个int
类型元素)。 - 选择 Display Indexes 以显示索引。
- 确保 Create as Array 选项已选中。
(3)查看数组内容。
3.堆分配的数组
堆分配的数组是通过调用分配函数(如 malloc
、calloc
、realloc
、free
等)在堆上动态分配的内存块。 可以使用IDA的 **Jump to xref**
(快捷键X) 功能查看这些函数的调用位置。
示例:
假设程序使用 malloc
分配了一个包含 10
个 int
类型元素的堆数组,下面是处理该数组的步骤:
(1)查找内存分配:找到 malloc
调用,分析其参数以确定分配了 10 * sizeof(int)
字节的内存。
(2)识别和定义数组:
- 找到
malloc
返回的指针地址。 - 右击该地址,选择
Edit -> Array
。 - 设置 Array Element Width 为
4 bytes
(因为int
通常是4 bytes
)。 - 设置 Maximum Possible Size 为
10
(表示10
个int
类型元素)。 - 选择 Display Indexes 以显示索引。
- 确保 Create as Array 选项已选中。
(3)查看数组内容。
注意,堆分配的数组在程序运行期间可能发生变化,因此在分析时需要确保使用的是正确的内存快照。 检查是否有 free
调用释放了堆内存,这可能会影响对堆数组的访问。
1.2 结构体成员访问
结构体(struct
)用于将多个数据元素(字段)组织在一起,访问结构体成员的过程包括定义结构体类型、将结构体应用于内存中的数据、以及分析如何访问和使用这些结构体。
1.全局分配的结构体
全局分配的结构体(即在程序的全局数据区域中分配的结构体)是指那些在程序启动时就已经分配好的结构体,它们的内存位置通常在程序的全局或静态数据段中。
定义一个结构体 GlobalData
,其中包含 id
(int
类型)、name
(char[20]
类型)和 value
(float
类型)字段:
struct GlobalData {
int id; // offset 0x0
char name[20]; // offset 0x4
float value; // offset 0x18
};
在数据段中,可通过偏移量或名称来定位结构体。如果你知道结构体实例的名称(如 g_data
),你可以在 **Strings**
或 **Imports**
视图中搜索该名称。 要查找全局数据, 可在内存视图中找到存储结构体实例的地址。全局数据通常在 **Data**
视图中展示,并且你可以使用十六进制视图查看原始数据。
要理解代码则在反汇编视图中找到访问结构体成员的代码。例如:
MOV EAX, [EBX] ; 访问结构体的第一个成员 id
MOV ECX, [EBX + 0x4] ; 访问结构体的第二个成员 name
MOV EDX, [EBX + 0x18] ; 访问结构体的第三个成员 value
这些指令表明如何从结构体实例中读取或写入数据。 如果结构体实例是通过指针访问的,你需要跟踪指针的计算和偏移量的应用。例如上面例子,如果 EBX
是结构体的指针,那么[EBX]
是 id
成员,[EBX + 0x4]
是 name
成员,[EBX + 0x18]
是 value
成员。
示例:
假设在IDA中你有一个全局结构体实例 g_globalData
, 在数据段中,g_globalData
实例位于内存地址 0x2000
,它定义如下:
struct GlobalData {
int id; // 0x0
char name[20]; // 0x4
float value; // 0x18
};
(1)定义结构体:定义 GlobalData
。
(2)应用结构体: 定位到地址 0x2000
; 右击该地址,选择 Set Data -> Structure
,选择 GlobalData
结构体。IDA将该内存区域解释为 GlobalData
类型。
(3)分析结构体成员: 在内存视图中,你会看到 0x2000
开始的内存区域显示为 GlobalData
结构体的字段。id
的值位于 0x2000
,name
的内容从 0x2004
开始,value
位于 0x2018
。在反汇编视图中,分析对结构体成员的访问,例如:
MOV EAX, [EBX] ; 读取 g_globalData.id
MOV ECX, [EBX + 0x4] ; 读取 g_globalData.name
MOV EDX, [EBX + 0x18] ; 读取 g_globalData.value
2.栈分配的结构体
栈分配的结构体指的是在函数内部分配的结构体,它们的生命周期仅限于函数调用期间,并且通常存储在栈上。
栈分配的结构体通常在函数的栈帧中创建,它通常会被列为局部变量。要查找函数内的结构体, 可在 **Functions**
视图中选择一个函数并查看其栈帧,栈帧通常在函数入口的 prologue
部分设置,并在 epilogue
部分清理。你可以在函数的 **Stack View**
或 **Function Frame**
视图中查看和分析局部变量 。
示例:
假设在IDA中你有一个栈分配的结构体实例 LocalData
, 在函数内部,假设结构体实例位于栈上地址 EBP-0x10
,其定义如下:
struct LocalData {
int id; // 0x0
char name[20]; // 0x4
float value; // 0x18
};
(1)定义结构体: 定义 LocalData
。
(2)应用结构体: 在函数栈视图中,定位到地址 EBP-0x10
;右击该地址,选择 Set Data -> Structure
,选择 LocalData
结构体。IDA将该内存区域解释为 LocalData
类型。
(3)分析结构体成员: 在内存视图中,你会看到 0x10
开始的内存区域显示为 LocalData
结构体的字段。对结构体成员的访问:
MOV EAX, [EBP-0x10] ; 读取栈上 `LocalData` 的 id 成员
MOV ECX, [EBP-0x10 + 0x4] ; 读取栈上 `LocalData` 的 name 成员
MOV EDX, [EBP-0x10 + 0x18] ; 读取栈上 `LocalData` 的 value 成员
3.堆分配的结构体
堆分配的结构体指的是在程序运行时通过动态内存分配函数(如 malloc
、calloc
、realloc
、new
等)分配的结构体。这些结构体的生命周期不限于函数调用期间,而是可以在程序运行期间存在,并且其地址在堆上动态分配。
要查找堆分配的结构体,你可以使用IDA的 **Cross References**
功能,查找到内存地址的所有引用(包括堆分配的结构体地址),接着选择函数调用的返回值,右击并选择 **Xrefs to**
来查看所有引用该地址的代码。
示例:
假设你在IDA中发现了一个堆分配的结构体, 并且你知道结构体在堆上的起始地址存储在 EDI
寄存器中 ,定义如下:
struct HeapData {
int id; // 0x0
char name[20]; // 0x4
float value; // 0x18
};
(1)定义结构体: 定义 HeapData
。
(2)应用结构体: 在内存视图中找到堆分配的地址(假设地址是 0x10000000
); 右击该地址,选择 Set Data -> Structure
,选择 HeapData
结构体。IDA将该内存区域解释为 HeapData
类型。
(3)分析访问: 对结构体成员的访问:
MOV EAX, [EDI] ; 读取 id 成员
MOV ECX, [EDI + 0x4] ; 读取 name 成员
MOV EDX, [EDI + 0x18] ; 读取 value 成员
4.结构体数组
结构体数组是指一组连续分配的内存区域,其中每个元素都是一个结构体。 如果某个函数通过索引访问内存位置,并且这个位置的大小与结构体的大小匹配,那么这个内存区域可能是一个结构体数组。 其他定义结构体数组等操作和步骤与以上1、2、3相同。
02 创建IDA结构体
2.1 创建一个新的结构体(或联合)(INSERT)
如果程序正使用某个结构体但IDA不了解其布局,你可以通过 **Structures**
窗口来定义该结构体的布局并将其应用到反汇编代码中。你需要在 **Structures**
窗口中创建或编辑结构体定义,IDA会自动识别并列出它能够检测到的结构体,但如果结构体不在列表中,你需要手动添加和定义它们。
**Structures**
窗口的前4行文本提示了可能进行的操作。主要操作包括添加、删除和编辑结构体。要添加结构体,可以使用 **INSERT**
热键,这会打开 **Create structure/union**
(创建结构体/联合) 对话框(如图7-3所示)。
要创建新结构体,首先在 Structure name
(结构体名称) 字段中输入结构体名称。前两个复选框用于决定新结构体在**Structures**
窗口中的显示位置,或者是否在窗口中显示新结构体。第三个复选框Creat union
(创建联合),指定你定义的是否为C风格联合结构体。结构体的大小是所有字段大小的总和,而联合的大小是最大字段的大小。点击 Add standard structure
(添加标准结构体) 按钮可以访问IDA识别的标准结构体类型。输入名称后,点击 OK
,IDA将在 **Structures**
窗口中创建一个空的结构体定义(如图7-4所示)。
2.2 编辑结构体成员
要给新结构体添加字段,你可以使用 **D**
、**A**
和数字键盘上的 *****
键。起初你只需使用 **D**
命令,不过它的效果依赖于光标的位置。 建议采用下面的步骤给结构体添加字段:
(1)要在结构体中添加新字段,将光标放在结构体定义的最后一行(包含ends
的那一行),然后按下 **D**
键。IDA 会在结构体末尾添加一个新字段。新字段的大小取决于数据转盘上选择的大小,初始名称为 field_N
,其中 N
是结构体开始到新字段如(如field_0
)的偏移量。
(2)要修改字段的大小,先将光标放在字段名称上,然后重复按 **D**
键,使数据转盘中的数据类型开始循环,从而选择正确的大小。你也可以通过 Options->Setup Data Types
来指定数据转盘上没有的大小。如果字段是数组,右击字段名称,选择 Array
,会打开“数组规范”对话框。
(3) 要更改结构体字段的名称,点击字段名称并按 **N**
键,或者右击名称选择 Rename
,然后在弹出的框中输入新名称即可。
在定义自己的结构体时,下面的提示可能会有所帮助。
- 在
**Structures**
窗口的左侧,字段的字节偏移量以8位十六进制数显示。 - 每次添加、删除结构体字段或更改字段大小时,结构体的新总大小会在结构体定义的第一行更新显示。
- 你可以给结构体字段添加注释,方法是右击(或使用热键)字段,并在上下文菜单中选择注释选项。
- 与
**Structures**
窗口顶部的说明不同的是,只有当字段是结构体中的最后一个字段时,按**U**
键才能删除它。对于其他字段,按**U**
键会取消字段的定义,仅删除字段名称,但保留分配的字节。 - 你需要确保结构体中的所有字段都正确对齐。IDA不会区分压缩和未压缩的结构体。如果需要填补字节以实现对齐,你需要手动添加这些字节。最好使用合适大小的占位符字段来填补这些字节。添加额外字段后,你可以选择保留或删除这些字段的定义。
- 在结构体中间的字节只能在取消关联字段定义后才能删除。要删除这些字节,可以使用
Edit -> Shrink Struct Type
(缩小结构体类型)命令。 - 要在结构体中间添加新的字节,可以选择希望新字节插入位置的字段,然后使用
Edit -> Expand Struct Type
(扩大结构体类型)命令。这样可以在选中的字段之前插入指定数量的字节。 - 如果你知道结构体的总大小但不清楚具体布局,可以创建两个字段:一个数组字段,其大小为结构体总大小减去1个字节(
size - 1
),和一个1字节的字段。创建完1字节字段后,取消第一个数组字段的定义。这样可以保留结构体的总大小,待你了解更多布局细节后,可以回过头来完善字段及其大小。
通过重复执行添加字段、设置字段大小以及添加填补字节等步骤,你可以在IDA中创建一个未压缩的 ch8 struct
结构体,如图7-5所示。
如果结构体定义在 Structures
窗口中占用太多空间,你可以选择任何字段并按下数字键盘上的减号键,将结构体折叠成一行简要摘要。这样可以节省空间并简化视图,一旦结构体定义完成且不需要进一步编辑,就可以进行折叠。ch8_struct
的折叠版本如图7-6所示。
大多数IDA识别的结构体都以单行摘要方式显示,因为它们通常不需要进一步编辑。你可以使用数字键盘上的加号键展开结构体定义,或者双击结构体名称来查看详细信息。
2.3 用栈帧作为专用结构体
结构体定义和函数的详细栈帧视图很相似,这是因为IDA在内部处理它们的方式相同。两者都是连续的内存块,分成多个命名字段,并且每个字段都有一个偏移量。不同的是,栈帧以帧指针或返回地址为中心,使用正负偏移量,而结构体则只使用从结构体起始位置开始的正值偏移量。
03 使用结构体模板
在反汇编代码中使用结构体定义有两种主要方法。首先,你可以重新格式化内存引用,比如 [ebx+8]
,转换成 [ebx+ch8_struct.field4]
符号式引用,从而提高它们的可读性,因为它清楚地显示了你在访问什么类型的结构体和其中的具体字段。当程序通过指针来引用结构体时,这种应用结构体模板的技术最有用。第二种应用结构体模板的方法是,提供其他可应用于栈和全局变量的数据类型。
为理解如何将结构体定义应用于指令操作数,我们把每个定义看成类似于一组枚举常量。
另一种格式化内存引用的方法是将栈和全局变量显示为完整的结构体。要做到这一点,可以双击该变量,打开详细的栈帧视图,然后使用 Edit-> Struct Var
(快捷键 ALT+Q
)命令,从已知的结构体中选择适合的结构体。
(以后补充)
04 导入新的结构体
4.1 解析C结构体声明
通过使用 View -> Open Subviews -> Local Types
(查看->打开子窗口->本地类型)命令,你可以打开一个叫做 “Local Types” 的窗口,这里会列出当前数据库中所有解析到的类型。对于刚创建的新数据库,这个窗口一开始是空的,但你可以通过按 INSERT
键或右键菜单中的 “Insert” 选项来添加和解析新的类型。得到的类型输入对话框如图7-7所示。
在解析新类型时,如果出现错误,会在IDA的输出窗口中显示。如果类型声明成功解析,它会出现在 “Local Types” 窗口中,并列出相关的声明,如图7-8所示。
IDA 默认使用 4 字节对齐方式来解析结构体成员。如果你的结构体需要不同的对齐方式,可以使用 pragma pack
指令来指定所需的结构体成员对齐方式。
添加到 “Local Types” 窗口中的数据类型不会立即出现在 “Structures” 窗口中。要将本地类型添加到 “Structures” 窗口,可以右键点击相关的本地类型,选择 “Synchronize to idb”。另外,由于新类型会自动添加到标准结构体列表中,你也可以通过其他方法将新类型导入到 “Structures” 窗口。
4.2 解析C头文件
要解析头文件,你可以通过 File -> Load File -> Parse C Header File
(文件->加载文件->解析C头文件)选择要解析的头文件。如果解析成功,IDA 会显示 “Compilation successful” 的消息。如果遇到问题,错误信息会在输出窗口中显示。
IDA 会把成功解析的结构体添加到标准结构体列表的末尾。如果新结构体的名称和已有的结构体相同,IDA 会用新结构体的定义覆盖原有结构体定义。新结构体默认不会出现在 “Structures” 窗口中,除非你特别选择将其添加。
在解析C头文件时,记住以下要点。
- 虽然内置解析器会遵循 pack 注释,但它的默认结构体成员对齐方式可能与编译器不同。默认情况下,解析器使用 4 字节对齐来构建结构体。
- 解析器可以处理 C 预处理器的
#include
指令。为了解析这些指令,解析器会在包含被解析文件的目录中搜索,同时也会查找在Options -> Compiler
(选项->编译器) 配置对话框中指定的include
目录(包含)。 - 解析器只能理解 C 标准数据类型,但它也能识别
#define
指令和C typedef
语句。因此,如果解析器遇到像uint32_t
这样的类型定义,它能够正确解析这些自定义类型。 - 如果没有源代码,使用文本编辑器快速定义一个结构体,并解析这个头文件或粘贴声明作为新的本地类型,通常比使用 IDA 的手动结构体定义工具更方便。
- 新创建的结构体只能在当前数据库中使用。如果要在其他数据库中使用它,必须重新创建这个结构体。
为了提高成功解析头文件的几率,尽量使用标准 C 数据类型,并减少 #include
文件,以简化结构体定义。在 IDA 中创建结构体时,正确的布局很重要,这不仅仅是使用正确的字段类型,还包括每个字段的大小和结构体的对齐。如果需要用 int
替换 uint32_t
以确保正确解析文件,就这样做。
05 使用标准结构体
IDA 可以识别各种库和 API 函数的数据结构。当你首次创建数据库时,IDA 会尝试确定与二进制文件相关的编译器和平台,并加载相应的结构体模板。在反汇编代码中,IDA 会根据需要在 “Structures” 窗口中添加相关的结构体定义。因此,“Structures” 窗口中显示的是与当前二进制文件相关的已知结构体。除了创建自定义结构体外,你还可以从 IDA 的标准结构体列表中选择并添加其他结构体到 “Structures” 窗口中。
要添加一个新结构体,首先在 “Structures” 窗口中按 INSERT
键。在在图7-3的 “Create structure/union” 对话框中,包含一个Add standard structure(添加标准结构体)按钮。单击这个按钮,IDA 会显示一个结构体列表,这些结构体与当前编译器(在分析阶段检测出来)和文件格式相关,包括通过解析 C 头文件添加的结构体。选择结构体对话框如图7-9所示,该对话框用于选择添加到"Structures" 窗口中的结构体。
你可以使用搜索功能来找到结构体。如果你知道结构体名称的前几个字符,只需输入这些字符,列表会自动跳到第一个匹配的结构体。选择一个结构体后,它和任何嵌套的结构体会被添加到 “Structures” 窗口中。
如果你想分析一个 Windows PE 文件的文件头,默认情况下,文件头不会自动加载到数据库中。不过,如果在创建数据库时选择了 “Manual load”(手动加载)选项,就可以将文件头加载到数据库中。加载文件头确保数据库中只包含与这些头部相关的数据类型。通常情况下,文件头不会被格式化,因为程序通常不会直接引用自己的文件头,因此分析器也不需要对文件头应用结构体模板。
在研究一个 PE 二进制文件时,你会发现文件开头是一个名为 IMAGE DOS HEADER
的 MS-DOS 头部结构体。这个结构体中的数据指向一个 IMAGE NE HEADER
结构体,它描述了 PE 文件的内存布局。对于了解 PE 文件结构的人来说,文件的前两个字节是熟悉的 MS-DOS 幻数 MZ
。
(后续补充P129)
06 IDA TIL文件
在 IDA 中,所有的数据类型和函数原型信息都存储在TIL文件中。IDA 包含许多主要编译器和 API 的类型库信息,这些文件存放在 <IDADIR>/til
目录中。你可以通过 “Types” 窗口(View -> Open subview -> Type Libraries
)查看当前加载的 .til
文件,并加载其他 .til
文件。IDA 会根据分析时发现的二进制文件属性自动加载这些类型库。一般情况下,大多数用户无需直接处理 .til
文件。
6.1 加载新的TIL文件
有时 IDA 可能无法识别用于构建某个二进制文件的特定编译器,这可能是因为文件经过了模糊处理。在这种情况下,你可以在 “Types” 窗口中按 INSERT
键,选择并加载你想要的 .til
文件。加载新的 .til
文件后,文件中的结构体定义会被添加到标准结构体列表中,类型信息也会应用到二进制文件中的函数上。如果 .til
文件中有匹配的函数原型,IDA 会自动更新这些信息。
6.2 共享TIL文件
IDA 使用 .til
文件来存储你在 “Structures” 窗口中手动创建的结构体或通过解析 C 头文件获得的自定义结构体定义。这些 .til
文件与创建它们的数据库相关联,文件名与数据库名相同,只是扩展名为 .til
。例如,如果数据库名为 some_file.idb
,那么相关的类型库文件就是 some_file.til
。通常你不会直接看到这个文件,除非你打开了相关的数据库。此外,.idb
文件实际上是一个归档文件(类似于 .tar
文件),用来存储数据库的各种组件。打开数据库时,这些组件文件会被提取并在 IDA 中使用。
在数据库之间共享 .til
文件有两种方法。第一种是将 .til
文件从一个数据库复制到另一个目录,然后在其他数据库的 “Types” 窗口中打开这个文件。第二种方法可以从一个数据库中提取自定义类型信息,生成 IDC 脚本,用于在其他数据库中重建这些自定义结构体。使用 File -> Produce File Dump Type into IDC File
(文件->生成文件->转储类型信息到IDC文件) 命令可以生成这个脚本。需要注意的是,这种方法只能转储 “Structures” 窗口中列出的结构体,而不能转储通过解析 C 头文件得到的结构体,复制 .til
文件则可以处理这些结构体。
Hex-Rays 提供了一个独立工具叫做 tilib
,用于在 IDA 外创建 .til
文件。注册用户可以从 Hex-Rays IDA 下载页面下载这个工具的 .zip
文件。安装时,只需将 .zip
文件解压到 <IDADIR>
目录中即可。tilib
工具可以列出现有 .til
文件的内容,或者通过解析 C 头文件来创建新的 .til
文件。 下面的命令将列举Visual Studio6类型库的内容:
C:\Program Files\IdaPro>tilib -1 til\pc\vc6win.til
创建新的 .til
文件时,你需要指定要解析的头文件和要生成的 .til
文件。你可以使用命令行选项来指定额外的包含目录或之前解析过的 .til
文件,以处理头文件中的依赖关系。下面的命令会创建一个包含 ch8_struct
声明的新 .til
文件。生成的 .til
文件需要移动到 <IDADIR>/til
目录,以便 IDA 使用。
C:\Program Files\IdaPro>tilib -c -hch8_struct.h ch8.til
tilib
工具有很多功能,具体可以通过随附的 README 文件了解。运行不带参数的 tilib
命令可以快速查看这些功能。在 IDA 6.1 之前,tilib
仅提供 Windows 可执行文件,但它生成的 .til
文件与所有版本的 IDA 都兼容。
07 C++逆向工程基础
7.1 this指针
在 C++ 中,所有非静态成员函数都使用 this
指针。当你调用这样一个函数时,this
指针会被初始化,指向调用该函数的对象本身。这使得成员函数能够访问和操作对象的数据成员。以下面的函数调用为例:
//object1, object2, and *p_obj are all the same type.
object1.member_func();
object2.member_func();
p-obj->member_func();
在3次调用member_func
的过程中,this
分别接受了&object1
、&object2
和p_obj
这3个值。可以把this
看成是所有非静态成员函数在调用过程中接收的第一个隐藏参数,这个参数实际上是指向调用该函数的对象的指针。Microsoft Visual C++ 使用 thiscall
调用约定,将 this
指针传递到 ECX 寄存器中。而在 GNU g++ 编译器中,this
被视为是非静态成员函数的第一个(最左边)显式参数,编译器会在调用函数之前将对象的地址作为最后一个压入栈中的参数。
如果在调用函数之前将一个地址移动到 ECX 寄存器中,通常可以得出两个重要的信息。首先,这个文件很可能是使用了 Visual C++ 编译器。其次,这个函数很可能是一个成员函数。如果同一个地址被传递给两个或更多函数,我们可以推断这些函数都属于同一个类层次结构。
在一个函数中,如果在函数初始化之前使用了 ECX 寄存器,通常意味着调用方已经初始化了 ECX。这可能表明这个函数是一个成员函数(尽管它可能仅仅使用了 fastcall
调用约定)。另外,如果发现一个函数将 this
指针传递给其他函数,那么这些函数很可能和传递 this
指针的函数属于同一个类。
使用g++编译的代码中调用成员函数的情况比较 少见。如果一个函数没有把指针作为它的第一个参数,那么可以确定它不是一个成员函数。
7.2 虚函数和虚表
虚函数用于在C++程序中实现多态行为。 编译器会为每一个包含虚函数的类(或通过继承得到的子类)生成一个虚表(vtable),这个表中存放着指向类中每个虚函数的指针。此外,每个包含虚函数的类都会得到一个额外的数据成员,称为虚表指针(vtable pointer),通常作为类的第一个数据成员。在运行时创建对象时,虚表指针会被设置为指向正确的虚表。当对象调用一个虚函数时,程序会通过虚表来查找正确的函数。因此,虚表是在运行时解析虚函数调用的基本机制。
下面举例说明虚表的作用。以下面的C++类 定义为例:
class BaseClass {
public:
BaseClass();
virtual void vfunc1() = 0;
virtual void vfunc2();
virtual void vfunc3();
virtual void vfunc4();
private:
int x;
int y;
};
class SubClass : public BaseClass {
public:
SubClass();
virtual void vfunc1();
virtual void vfunc3();
virtual void vfunc5();
private:
int z;
};
在这个例子中,SubClass
是 BaseClass
的一个子类。BaseClass
包含了4个虚函数,而 SubClass
则增加了一个新的函数 vfunc5
,使得它包含了 BaseClass
中的所有4个函数,以及额外的一个函数。在 BaseClass
中,有一个声明为 =0
的虚函数 vfunc1
,这意味着它是一个纯虚函数。纯虚函数在声明的类中没有具体实现,必须在子类中被重写后才能让这个子类被实例化。换句话说,没有名为 BaseClass::vfunc1
的函数存在,直到某个子类提供了实现。SubClass
提供了对 vfunc1
的实现,因此可以创建 SubClass
的对象。
BaseClass
看起来似乎包含2个数据成员,而 SubClass
包含3个成员。任何包含虚函数的类(无论是自身包含还是继承得来)都会额外包含一个虚表指针。因此,BaseClass
类型的实例化对象实际上有3个数据成员,而 SubClass
类型的实例化对象则有4个数据成员,它们的第一个数据成员都是虚表指针。在 SubClass
类中,虚表指针实际上是继承自 BaseClass
,而不是 SubClass
专门引入的。图7-10展示了一个简化后的内存布局,它动态分配了一个 SubClass
类型的对象。在创建对象时,编译器确保新对象的虚表指针指向正确的虚表(本例中是 SubClass
类的虚表)。
值得注意的是,SubClass
中包含两个指向属于 BaseClass
的函数(BaseClass::vfunc2
和 BaseClass::vfunc4
)的指针。这是因为 SubClass
没有重写任何一个函数,而是直接继承了 BaseClass
的这些函数。图中还展示了对纯虚函数的典型处理方式。由于在 BaseClass
中没有为纯虚函数 BaseClass::vfunc1
提供实现,所以在 BaseClass
的虚表中并没有存储 vfunc1
的地址。编译器通常会在这个位置插入一个错误处理函数的地址,一般这个函数被称为 purecall
。理论上,这个函数永远不会被调用,但如果意外调用了它,它会导致程序终止。
使用虚表指针会影响到在IDA中操作类时需要考虑的因素。我可以使用IDA的结构体定义来描述C++类的布局。对于包含虚函数的类,必须将虚表指针作为类中的第一个字段。在计算对象总大小时,必须考虑虚表指针的空间。这种情况在使用 new
操作符动态分配对象时特别显著。因为传递给 new
的大小值不仅包括类本身(以及任何超类)中所有显式声明的字段所占用的空间,还包括虚表指针所需的空间。
下面的例子动态创建了一个SubClass
的对象,并将这个对象的地址存储在一个指向BaseClass
的指针中。接着,这个指针被传递给一个名为call_vfunc
的函数。在call_vfunc
函数内部,它使用这个指针来调用对象的vfunc3
函数。
void call_vfunc(BaseClass *b) {
b->vfunc3();
}
int main() {
BaseClass *bc = new SubClass();
call_vfunc(bc);
}
由于vfunc3
是一个虚函数,因此编译器会确保调用SubClass::vfunc3
,因为指针指向一个SubClass
对象。下面是call_vfunc
函数的反汇编版本,展示了如何解析虚函数调用:
.text:004010A0 call_vfunc proc near
.text:004010A0
.text:004010A0 b = dword ptr 8
.text:004010A0
.text:004010A0 push ebp
.text:004010A1 mov ebp, esp
.text:004010A3 mov eax, [ebp+b]
.text:004010A6 mov edx, [eax]
.text:004010A8 mov ecx, [ebp+b]
.text:004010AB mov eax, [edx+8]
.text:004010AE ca11 eax
.text:004010B0 pop ebp
.text:004010B1 retn
.text:004010B1 call vfunc endp
在第8行代码,程序从结构体中读取虚表指针,并将其保存在EDX寄存器中。因为参数b指向一个SubClass
对象,所以这里的EDX将包含SubClass
的虚表地址。在第10行代码,程序通过索引获取虚表的第三个指针(在这个例子中是SubClass::vfunc3
的地址),并将其读取到EAX寄存器中。最后,在第11行代码处调用了虚函数。
值得注意的是,第10行代码处的虚表索引操作与结构体引用操作非常相似,实际上它们几乎没有区别。因此,我们可以定义一个结构体来描述一个类的虚表布局,然后在反汇编代码中使用这个结构体定义,以提升代码清单的可读性, 如下所示:
00000000 SubClass_vtable struc ; (sizeof=0x14)
00000000 vfunc1 dd ?
00000004 vfunc2 dd ?
00000008 vfunc3 dd ?
0000000C vfunc4 dd ?
00000010 vfunc5 dd ?
00000014 SubClass_vtable ends
这个结构体允许将虚表引用操作重新格式化成以下形式:
.text:004010AB mov eax,[edx+SubClass_vtable.vfunc3]
7.3 对象生命周期
了解对象的构建和销毁机制有助于理解对象如何被创建和清除,帮助分析对象之间的层次结构和嵌套对象关系,也有助于迅速找到类的构造函数和析构函数。
对全局和静态分配的对象来说,构造函数在程序启动并进入main
函数之前被调用。对于栈分配的对象,构造函数在对象进入声明它的函数作用域时被调用。通常情况下,对象在声明它的函数中时,其构造函数就会被调用。然而,如果对象在一个块语句中声明,则其构造函数直到该块被执行时才被调用(如果该块确实被执行)。如果对象在程序堆中动态分配,则创建对象分为两个步骤。第一步,调用new
操作符分配对象的内存。第二步,调用构造函数来初始化对象。微软的Visual C++和GNU的g++的主要区别在于,Visual C++确保在调用构造函数之前,new
操作符分配的结果不为空(null
)。
执行一个构造函数时,将会发生以下操作。
(1)如果类拥有一个超类,则调用超类的构造函数。
(2)如果类包含任何虚函数,则初始化虚表指针,使其指向类的虚表。注意,这样做可能会覆盖一个在超类中初始化的虚表指针。
(3)如果类拥有本身就是对象的数据成员,则调用这些数据成员的构造函数。
(4)最后,执行特定代码的构造函数。这些是程序员指定的、表示构造函数C++行为的代码。
构造函数没有指定返回类型,但在Microsoft Visual C++生成的构造函数中,实际上是将this
指针返回到EAX寄存器。这个细节是Visual C++的实现方式,普通的C++程序员无法直接访问或利用这个返回值。
析构函数的调用顺序通常与构造函数相反。全局和静态对象的析构函数由程序结束时的清理代码调用。栈上分配的对象的析构函数在对象离开其声明的作用域时被调用。而堆上分配的对象的析构函数在使用delete
操作符释放对象内存之前被调用。
析构函数和构造函数执行的操作大致相同,但它们的执行顺序通常是相反的。
析构函数执行的操作与构造函数执行的操作大致相同,唯一不同的是,它以大概相反的顺序执行这些操作。
(1)如果一个类包含虚函数,那么在还原对象时,需要确保其虚表指针指向正确的类虚表。特别是在子类创建过程中覆盖了虚表指针时,这一步就显得尤为重要。
(2)执行程序员为析构函数指定的代码。
(3)如果类拥有本身就是对象的数据成员,则执行这些成员的析构函数。
(4)最后,如果对象拥有一个超类,则调用超类的析构函数。
理解超类的构造函数和析构函数被调用的时机,可以通过它们的调用链追踪对象的继承体系。虚表在程序中被直接引用的情况主要有两种:在该类的构造函数中引用和在析构函数中引用。一旦定位了一个类的虚表,你可以利用IDA的数据交叉引用功能(参见第8章),快速找到所有引用该虚表的相关类的构造函数和析构函数。
7.4 名称改编
名称改编又称为名称修饰(name decoration),是C++编译器为了区分重载函数而使用的一种机制。为了给重载函数生成唯一的函数名称,编译器会在函数名后面添加额外的字符,来编码包括函数的返回类型、所属的类、以及调用函数所需的参数类型和顺序等信息。
名称改编是C++编译器的一项实现细节,并不属于C++语言规范的一部分。因此,不同的编译器供应商开发了它们自己的名称改编约定,这些约定通常不相互兼容。幸运的是,IDA理解并支持Microsoft Visual C++、GNU g++以及其他一些编译器使用的名称改编约定。当在反汇编代码中遇到一个改编后的名称时,默认情况下,IDA会以注释的形式显示该名称的原始(未改编)版本。你可以通过打开Options -> Demangled Names
(选项 -> 名称取消改编)对话框来选择是否启用IDA的名称取消改编选项。
对话框中有 3个主要选项,用于控制是否以注释的形式显示取消改编的名称(demangled name),是否对名称本身进行取消改编,或者根本不执行取消改编。
Assume GCC v3.x (采用GCCv3.x名称) 复选框用于区分g++ 2.9.x版本和g++ 3.x及更高版本使用的名称改编方案。通常情况下,IDA会自动检测代码中使用的g++编译器的命名约定。此外,通过’Setup short names’(设置短名称) 和’Setup long names’设置长名称) 按钮,用户可以对取消改编的名称格式进行详细控制,包括许多选项。
改编名称提供了函数签名的详细信息。当一个二进制文件使用了改编名称时,IDA的取消改编功能可以立即显示所有函数的参数类型和返回类型。
7.5 运行时类型识别
C++提供各种操作符,可进行运行时检测,以确定(typeid
)和检查(dynamic_cast
)一个对象的数据类型。为了实现这些操作,C++编译器必须将类型信息嵌入到程序的二进制文件中,并确保能够准确地确定多态对象的类型,而不管为访问该对象被取消引用的指针类型。然而,与名称改编类似,RTTI(Runtime Type Identification,运行时类型识别)也是一个编译器的实现细节,而不是语言的一部分,因此编译器没有标准的方法来实现RTTI功能。
以下面这个利用多态的简单程序为例:
class abstract_class {
public:
virtual int vfunc() = 0;
};
class concrete_class : public abstract_class {
public:
concrete_class();
int vfunc();
};
void print_type(abstract_class *p) {
cout << typeid(*p).name()<< endl;
}
int main() {
abstract_class *sc = new concrete_class();
print_type(sc);
}
print_type
函数利用typeid
运算符来确定指针p
所指向的对象的类型。这个运算符在运行时获取对象的类型信息,因此能够准确地识别出指针p
指向的具体类型。
在这个例子中,main
函数中创建了一个concrete_class
对象,并且将其地址赋给了指针p
。当调用print_type(p)
时,typeid(*p)
表达式会返回一个type_info
对象,其中包含了指针p
所指向对象的确切类型信息。
当涉及到确定一个指针所指向的多态对象的类型时,每个多态对象都包含一个指向虚表的指针。编译器将类的类型信息存储在这个虚表的前面。具体来说,编译器在类的虚表之前放置了一个指针,这个指针指向一个结构体,其中包含了用于确定具有这个虚表的类的名称所需的信息。
在使用g++编译器的代码中,这个指针指向一个type_info
结构体,该结构体包含一个指向类名称的指针。而在使用Visual C++编译器的代码中,这个指针指向一个RTTICompleteObjectLocator
结构体,该结构体再包含一个指向TypeDescriptor
结构体的指针。
TypeDescriptor
结构体包含一个存储多态类名称的字符数组。需要明确的是,只有在程序使用了typeid
或dynamic_cast
操作符时,才需要RTTI信息。大多数编译器提供了选项,可以禁用不需要RTTI的二进制文件的RTTI生成。因此,如果你发现RTTI信息丢失了,不会感到意外。
7.6 继承关系
如果没有RTTI信息可用,要理解C++类之间的继承关系可以采用分析类的虚函数表(vtable)和构造函数调用顺序来推断继承结构。如果一个程序不使用typeid
或dynamic_cast
运算符的程序可能缺少RTTI信息。
要确定一个继承体系的简单方法是观察创建对象时调用的超类构造函数的调用链。然而,内联构造函数可能会阻碍这种方法的成功,因为它们难以通过调用链追踪。
分析和比较虚表是一种确定继承关系的方法。例如图7-10所示,在比较虚表的过程中,我们可以发现在SubClass
的虚表中有两个指针与BaseClass
的虚表中相同。这表明BaseClass
和SubClass
之间肯定存在某种关系。但要确定SubClass
是BaseClass
的子类还是反过来,我们可以采用以下几条指导原则来帮助我们理解它们之间的关系。
- 如果两个虚表包含相同数量的条目,则与这两个虚表对应的类之间可能存在着某种继承关系。
- 如果类X的虚表包含的条目比类Y多,则X可能是Y的子类。
- 如果X包含的条目也可以在Y的虚表中找到,则必定存在下面一种关系:X是Y的子类,Y是X的子类,或者X和Y全都是同一个超类Z的子类。
- 如果X包含的条目也可以在类Y的虚表中找到,并且X的虚表中至少包含一个纯调用条目,而Y的虚表中并没有这个条目,那么Y是X的子类。
我们可以借助这几条指导原则来判断图7-10中BaseClass
和SubClass
之间的关系。在这个例子中,前面列举的最后三条原则都适用,但仅通过虚表分析,由最后一条原则可得出结论:SubClass
是BaseClass
的子类。
(八)交叉引用与绘图功能
如果一个二进制文件中包含大量像"ASCII
"这样的字符串,例如"Executing Denial of Service attack!"(拒绝服务攻击),你可能会怀疑其中是否隐藏了拒绝服务攻击的行为。然而,仅仅因为存在这些字符串并不意味着该程序确实会进行拒绝服务攻击。这些字符串只是表示该二进制文件偶然包含了这些特定的字符序列。 要确认这条消息是否会在实施攻击之前显示出来,你需要查找相关的代码。关键问题是:“程序从什么地方引用这个字符串?”通过回答这个问题,你可以迅速定位到使用该字符串的程序位置,并进一步确定是否存在具体的拒绝服务攻击代码。
IDA通过其强大的交叉引用功能帮助你回答这些问题。它提供了多种显示和访问交叉引用数据的工具,包括图形生成功能,以直观的方式展示代码与数据之间的关系。
01 交叉引用
IDA中的交叉引用通常简称为xref。在菜单项或对话框中,我们称这种引用为xref。对于其他引用,我们仍然使用交叉引用这一术语。
在IDA中有两种基本的交叉引用:代码交叉引用和数据交叉引用。每种引用又包含几种不同的类型。交叉引用涉及一个地址引用另一个地址,这些地址可以是代码地址或数据地址。如果你熟悉图论,可以将这些地址视为有向图中的节点,而交叉引用则是连接这些节点的边。简单来说,交叉引用就像图中的箭头指向不同的节点,图8-1可帮助我们理解代码和数据之间的关系。在这个简单的图形中,两条有向边(②)连接了3个节点( ① )。
节点通常也称为顶点(vertice),而有向边则用箭头表示它们的方向。在图8-1中,从顶部的节点可以沿着箭头到达底部的两个节点之一,但反过来从底部节点却无法到达顶部节点。
代码交叉引用可帮助IDA生成控制流图形和函数调用围形。
在进一步讨论交叉引用之前,先了解一下IDA如何在反汇编代码中显示交叉引用信息。例如,某个反汇编函数(称为register_tm_clones
)的标注行可能会显示一个常规注释,其中包含一个交叉引用,如图8-2所示。
文本中的"CODE XREF"表示这是一个代码交叉引用,而非数据交叉引用(DATA XREF)。后面的地址(这里是frame_dummy
)是交叉引用的源头地址,指示了该引用是在一个名为frame_dummy
的函数内部提出的。每个交叉引用注释的后面通常会有一个箭头,表示引用的方向。在图8-2中,下行箭头表示frame_dummy
的地址比register_tm_clones
高,因此你需要向下滚动才能找到它。相反,上行箭头表示引用地址较低,需要向上滚动才能找到它。最后,每个交叉引用注释都包含一个单字符后缀,用以说明交叉引用的类型。
1.1 代码交叉引用
代码交叉引用用于表示一条指令将控制权转移给另一条指令。IDA将这种控制权的转移方式称为流(flow)。有三种基本的流类型:普通流、跳转流和调用流。跳转流和调用流可以进一步细分,具体取决于目标地址是近地址还是远地址,不过远地址只在使用分段地址的二进制文件中才会出现。在接下来的讨论中,我们将使用以下程序的反汇编代码:
int read_it; //integer variable read in main
int write_it; //integer variable written 3 times in main
int ref_it; //integer variable whose address is taken in main
void callflow() { } //function called twice from main
int main() {
int *p = &ref_it; //results in an "offset" style data reference
*p = read_it; //results in a "read" style data reference
write_it = *p; //results in a "write" style data reference
callflow(); //results in a "call" style code reference
if (read_it == 3) { //results in "jump" style code reference
write_it = 2; //results in a "write" style data reference
}
else { //results in an "jump" style code reference
write_it = 1; //results in a "write" style data reference
}
callflow(); //results in an "call" style code reference
}
根据注释文本的描述,这个程序包含了IDA中体现所有交叉引用特性的操作。
普通流(ordinary flow)就是指程序中指令的顺序执行。简单来说,就是一条指令执行完后,接着执行下一条。除了指令在反汇编代码清单中的显示顺序外,普通流没有其他特殊的显示标志。比如,在反汇编代码中,如果指令A后面直接跟着指令B,那么A和B之间就是普通流。在代码清单中,除 ①、② 两处的指令外,其他每条指令都有一个普通流指向紧跟在它们后面的指令。
调用流(call flow)是指程序中通过如 ③ 处的call
指令跳转到函数执行的流程。通常情况下,call
指令不仅会创建一个调用流,还会在函数执行完后返回到调用位置,因此也会有一个普通流。但如果在分析时确定某个函数不会返回,那么在调用这个函数时就不会为它分配普通流。调用流通过在目标函数(流的目的地址)处显示交叉引用来表示。
callflow函数的反汇编代码清单如下所示:
在这个例子中,callflow
位置显示了两个交叉引用,表示这个函数被调用了两次。如果没有调用地址的名称,交叉引用中的地址会以调用函数中的偏移量来表示。交叉引用用后缀“p”来标识,表示它们是由函数调用产生的。
每个无条件和条件分支指令都创建一个跳转流(jump flow)。条件分支指令还会有一个普通流,用于指向如果不进入分支时的顺序执行路径。无条件分支指令则没有普通流,因为它总是跳转。⑤处的虚线表示相邻的两条指令之间没有普通流。跳转流通过跳转目标的位置(跳转的源头)显示的交叉引用来表示,如⑥处所示,与函数调用的交叉引用一样。跳转交叉引用使用后缀“j”来标识,表示它们是由跳转指令产生的。
1.2 数据交叉引用
数据交叉引用用于跟踪二进制文件访问数据的方式。它记录了数据何时被读取、写入或引用,但不涉及栈上的变量。数据交叉引用只与虚拟地址中的字节有关。下面是与前一个示例程序有关的全局变量,其中包含几个数据交叉引用。
读取交叉引用(read cross-reference)表示某个内存位置的数据被访问了。它可能来自于某个指令,也可能引用程序中的任何位置。在图8-3代码清单中,read_it
在 ⑦ 处被读取。通过交叉引用注释,我们可以知道main
函数中哪些位置访问了read_it
。读取交叉引用用后缀“r”标识。第一次读取read_it
时,它被格式化为32位双字。IDA会根据程序访问变量的方式和函数参数来推测变量的大小和类型。
在图8-3代码清单中,write_it
在 ⑧ 处被写入。IDA生成的写入交叉引用(使用后缀“w”)显示了修改write_it
的程序位置。由于write_it
的大小由32位EAX寄存器确定,IDA根据这一点推测了变量的大小。交叉引用省略号,如 ④ 处显示,表示交叉引用数量可能超出了当前限制,可以通过“Options General”对话框中Cross-references选项卡中的Number of displayed xrefs(显示的交叉引用数量)设置修改这个限制。写入交叉引用可能来自单条指令,也可能涉及程序中的其他位置。通常,针对单个指令字节的写入交叉引用表示自修改代码,这种代码通常被视为无效代码,常见于恶意软件的“去模糊例程”中。
偏移量交叉引用(offset cross-reference)表示程序引用的是某个位置的地址,而不是内容。在图8-3代码清单中,ref_it
的地址在 ⑨ 处被引用,显示了偏移量交叉引用(后缀“o”)。这种交叉引用通常由指针操作引起,比如数组访问时会用起始地址加上偏移量来定位元素。很多全局数组的起始地址和字符串数据常通过偏移量交叉引用来确定。
与仅源于指令的读取和写入交叉引用不同,偏移量交叉引用可以来源于指令或数据。例如,如果一个指针表(如虚表)指向的地址生成了偏移量交叉引用,这种交叉引用就源于程序的数据部分。
C++虚函数不会被直接引用,也不会作为调用交叉引用的目标。它们总是通过虚表间接引用,成为偏移量交叉引用的目标。虚函数可以出现在多个虚表中,因此,你不需要重写每个虚函数。通过回溯偏移量交叉引用,可以快速在程序的数据部分找到C++虚表。
1.3 交叉引用列表
要查看某个位置的所有交叉引用,有两种方法。第一种方法是选中该位置的目标地址,然后选择“View -> Open Subviews -> Cross References
”(查看 -> 打开子窗口 -> 交叉引用),即可打开该位置的完整交叉引用列表。例如,图8-7展示了变量write_it
的所有交叉引用。
在交叉引用窗口中,每一列显示了交叉引用的方向(向上或向下)、类型(前面提到的后缀)、源地址及其反汇编文本和注释。像其他地址列表窗口一样,双击任何条目会让反汇编窗口跳转到对应的源地址。打开交叉引用窗口后,它会始终显示,你可以通过反汇编代码清单上方的标题标签访问这个窗口。
第二种方法是选中你感兴趣的名称,然后在菜单中选择“Jump -> Jump to xref
”或按快捷键CTRL+X
打开对话框,显示所有引用该符号的位置。这个对话框和交叉引用子窗口几乎一样。比如,选中write_it
的一个实例(.text:00000001400117FC
)并使用CTRL+X
,就会打开图8-8中的对话框。
图8-7中的子窗口和图8-8中的对话框在使用上有几个主要区别。图8-8的对话框是模式对话框,用于选择一个引用位置并跳转到那个位置。双击列表中的位置会关闭对话框并跳转到选定位置。该对话框与交叉引用子窗口之间的第二个区别在于前者可以通过选择任何符号并使用热键或上下文菜单打开,而后者只能通过将光标放在一个交叉引用目标地址上,然后选择View->Open Subviews->Cross References
打开。换句话说,对话框允许在任何源位置打开交叉引用,而子窗口只能从目标位置打开。
交叉引用列表可用于迅速确定调用某个特殊函数的位置。例如,要查找所有strcpy
函数的调用,只需使用快捷键CTRL+X
打开交叉引用对话框,然后查看所有相关的调用。你还可以为strcpy
添加注释,然后通过该注释打开交叉引用对话框,以便更快找到所有调用。
1.4 函数调用
有一个专门显示函数调用的交叉引用列表。通过选择“View -> Open Subviews -> Function Calls
”可以打开这个窗口。窗口的上半部分列出所有调用当前函数的位置(由光标所在位置决定),下半部分则列出当前函数调用的所有其他函数。
通过查看函数调用交叉引用列表,你可以快速定位反汇编代码中的相关位置。
02 IDA绘图
2.1 IDA外部(第三方)图形
IDA使用第三方程序来显示生成的图形文件。你可以通过编辑<IDADIR>/cfg/ida.cfg
中的GRAPH_VISUALIZER
变量来配置使用的图形查看器。
当用户请求外部图形时,IDA会生成图形源文件并保存到临时文件中,然后用指定的第三方图形查看器显示图形。IDA支持两种图形语言:GDL和DOT。你可以通过编辑<IDADIR>/cfg/ida.cfg
中的GRAPH FORMAT
变量来选择使用的图形语言,值可以是DOT
或GDL
。要确保选择的图形语言与GRAPH_VISUALIZER
中指定的查看器兼容。
使用View -> Graphs
(查看 -> 图形) 子菜单可以生成5种类型的图形。可在IDA中使用的外部图形包括:
- 函数流程图;
- 整个二进制文件的调用图;
- 目标符号的交叉引用图;
- 源头符号的交叉引用图;
- 自定义的交叉引用图。
IDA可以生成并保存GDL格式的流程图和调用图供自身使用,这些选项在“File -> Produce file
”(文件 -> 生成文件)子菜单中可以找到。如果你的图形查看器支持保存显示的图形,你可以保存其他类型图形的规范文件。
(1)外部流图形
将光标放在一个函数上,选择“View Graphs -> Flow Chart
”(或按快捷键F12),IDA会生成并显示一个外部流程图。这些流程图是控制流图,它们将函数的指令划分成基本块,并用边表示块之间的流程。
图8-11是一个相对简单的函数的部分流程图。如你所见, 这个外部流程图提供的地址信息很少 ,这使得我们很难将流程图与其对应的反汇编代码清单关联起来。
(2)外部调用图
函数调用图帮助我们理解程序中函数调用的层次结构。它通过为每个函数创建一个节点,并根据函数间的调用关系连接这些节点来生成。生成调用图时,通常会递归地遍历函数调用链,直到遇到库函数为止。使用GNU gcc编译一个动态链接的二进制文件后,可以使用View -> Graphs -> Function Calls
要求IDA生成一个函数调用图。IDA利用不同的颜色类表示图形中不同类型的节点。
(3)外部交叉引用图
IDA可以为全局符号(函数或全局变量)生成两种交叉引用图:目标符号交叉引用图[View -> Graphs -> Xrefs To
(交叉引用目标)] 和源符号交叉引用图[View -> Graphs -> Xrefs From
(交叉引用源头)]。生成“交叉引用目标”图时,IDA会回溯所有引用指定符号的地方,直到找到没有其他符号引用的符号。这种图形可以帮助你了解要调用一个函数需要经过哪些其他函数。图8-12展示了通过“交叉引用目标”图形显示到达puts
函数的路径。
“交叉引用目标”图形不仅能直观地显示引用某个全局变量的所有位置,还能展示到达这些位置所需的函数调用链。这种图形是唯一能够整合数据交叉引用信息的图形。
“交叉引用源头”图形通过递归跟踪所有以选定符号为源头的交叉引用来创建。如果符号是函数名,图形将显示该函数的调用来源,但不会显示对全局变量的数据引用。如果符号是全局指针变量,则会跟踪其数据偏移量交叉引用。要以图形方式表示函数的源头交叉引用,最好绘制函数调用图,如图8-13所示。
(4)自定义交叉引用图
在IDA中,自定义交叉引用图称为用户交叉引用图(user xref chart)。它提供了最大的灵活性,可以将以某个符号为目标和源头的交叉引用合并到一幅图中。你还可以指定最大递归深度,并选择包括或排除特定符号类型。
使用“View -> Graphs -> User Xrefs Chart
”可以打开“图形定制”对话框(如图8-14所示)。在这个对话框中设置的选项会生成图形,其中指定地址范围内的每个全局符号都以节点显示。如果起始和结束地址相同,则生成的是指定符号的交叉引用图。如果地址范围不同,IDA会生成指定范围内所有非局部符号的交叉引用图。若地址范围覆盖整个数据库,生成的图形将是整个二进制文件的函数调用图。
图8-14中选择的选项为所有自定义交叉引用图的默认选项。每组选项的作用如下。
- “Starting direction”(起始方向)两个选项决定了生成图形时是搜索以选定符号为源头的交叉引用、为目标的交叉引用,还是两者都搜索。如果其他选项均使用默认设置,将起始方向设置为“交叉引用目标”(Cross references to),则生成“交叉引用目标”图;若设置为“交叉引用源头”(Cross references from),则生成“交叉引用源头”图。
- “Parameters”(参数)中的“Recursive”选项允许从选定符号开始进行递归搜索:“交叉引用源头”执行递归下降,“交叉引用目标”执行递归上升。启用“Follow only current direction”(仅跟踪当前方向)时,递归只会沿着一个方向进行。例如,如果从节点A找到节点B,递归只会深入到B,但不会继续发现B引用的其他节点。如果取消选中此选项并选择两个起始方向,图形中的每个节点将同时沿着目标和源头方向递归。
- “Recursion depth”(递归深度)选项设置了图形生成的最大递归层数。设置深度为-1则递归最深,生成最大图形。
- “Ignore”(忽略)选项允许你排除特定类型的节点,从而简化图形。比如,忽略以库函数为源头的交叉引用,可以使静态链接的二进制文件图形更简洁。这样可以确保图形中只显示重要的代码部分。
- “Print options”(打印选项)控制图形的格式化:
- Print comments(打印注释):将函数注释包含在图形节点中。
- Print recursion dots(打印递归点):显示省略号节点,表示递归超出了指定深度,可以继续递归。
2.2 IDA的集成绘图视图
在图形模式和文本模式下,IDA的反汇编视图操作基本相同。双击导航、导航历史记录等功能在两种模式下都有效。如果你导航到一个不属于函数的位置(如全局变量),反汇编窗口会自动切换到文本模式,返回到函数范围内时会切换回图形模式。在图形模式下,你可以像在文本模式中一样访问变量,双击栈变量查看详细栈帧视图,格式化指令操作数的选项也完全相同。
图形模式下用户界面的主要变化与各图形节点有关。图8-15是一个简单的图形节点及其相关的标题栏控制按钮。
从左到右,节点标题栏上的三个按钮从左到右分别用于更改节点背景颜色、分配或更改节点名称,以及访问该节点的交叉引用列表。更改颜色可以帮助标记已分析的节点或区分节点,以便突出显示你感兴趣的代码。
为节点分配颜色后,文本模式下对应的指令也会显示相同的背景色。要恢复默认颜色,可右击节点标题栏并选择“设置默认节点颜色”。标题栏中间的按钮用于为节点基本块的第一条指令地址分配名称。基本块通常是跳转指令的目标,IDA会为这些节点自动分配一个哑名。
图8-15中最右边的按钮用于查看以该节点为目标的交叉引用列表。图形视图默认不显示交叉引用注释,使用这个按钮可以直接导航到引用该节点的位置。这个交叉引用列表还会显示一个普通流条目,帮助识别节点的前任者。如果想在图形模式下查看交叉引用注释,可以通过“Options General”中的“Cross Reference”选项卡,将“Number of displayed xrefs”设置为非零值。
要降低图形的混乱程度,可以将节点分组。按住CTRL
键并单击选中多个节点的标题栏,然后右击其中一个选中的节点,选择“Group nodes”。接着,IDA会要求你输入一段文本(默认为组中第一条指令),用于显示折叠节点的名称。
(九)IDA的多种面孔
01 控制台模式IDA
1.1控制台模式的共同特性
控制台模式(console mode)指的是基于文本的DA版本在终端或shell中运行。这种模式对终端的尺寸调整和鼠标支持各有不同,因此存在一些使用上的限制,具体取决于你使用的平台和终端程序。
控制台用户界面包括窗口上方的菜单栏和下方的操作栏。菜单栏显示选项和状态,操作栏类似于工具栏。控制台可以用热键或鼠标(如果支持)进行操作。几乎所有GUI版本中的命令在控制台版本中都有对应的操作,并且保留了GUI版本的快捷键。
在控制台模式下,IDA的显示窗口位于菜单栏和命令栏之间。由于屏幕空间有限,IDA默认打开两个窗口:反汇编窗口和消息窗口。为了在控制台中模拟GUI版本的标签窗口,IDA使用TVision库的重叠窗口功能,并通过快捷键F6在窗口间切换。每个窗口都有编号,编号显示在窗口的左上角。
如果你的控制台支持鼠标,你可以拖动窗口的右下角来调整大小,拖动上边框来移动窗口。如果不支持鼠标,可以使用Window -> Resize -> Move (CTRL+F5)
命令,用箭头键移动窗口位置或调整大小。控制台版本没有图形功能,无法显示控制流箭头,但仍能打开所有GUI版本的子窗口。十六进制窗口不会单独显示,而是在反汇编窗口中通过Options -> Dump/Normal View (CTRL+F4)
命令切换。要同时打开反汇编窗口和十六进制窗口,需要额外打开另一个反汇编窗口 (View -> Open Subviews -> Disassembly)
并切换到十六进制模式,但无法同步两个窗口。
如果控制台支持鼠标,你可以像在GUI版本中一样通过双击名称或按ENTER
键来跳转到反汇编代码中的位置。对于栈变量名称,按ENTER
键会打开相关函数的详细栈帧视图。如果控制台不支持鼠标,可以使用ALT+x
组合键来实现许多功能,其中x
是屏幕上突出显示的字符。
1.2 Windows控制台
在Windows的cmd.exe
终端中,IDA的控制台版本(idaw.exe
)可以很好地运行。GUI版本是idag.exe
,64位版本分别为idaw64.exe
和idag64.exe
。要在Windows上使用IDA的鼠标功能,需要在终端中禁用QuickEdit模式。
要配置QuickEdit模式,请右击终端标题栏,选择“Properties”(属性),然后在“Options”(选项)选项卡中取消勾选“QuickEdit mode”。请确保在启动IDA之前禁用此模式,因为更改设置后不会立即生效。
在Windows中,IDA的控制台版本提供了一个“Window -> Set Video Mode”(窗口 -> 设置视频模式)菜单选项,可以在6种固定的终端大小之间切换,最大为255×100。不同于X Windows下的Linux终端,cmd.exe不能使用鼠标调整窗口大小。 即使在反汇编窗口中没有图形显示,你仍然可以使用IDA的外部绘图模式。通过选择 View -> Graphs
菜单,IDA会启动图形查看器(如qwingraph),显示生成的图形。
1.3 Linux控制台
在Linux版本的IDA(叫做idal或idal64,用于64位文件)中,你需要复制IDA密钥文件(ida.key)才能正常运行。这意味着即使你只使用Linux或OSX版本,你仍需在Windows机器上安装一次IDA。对于Unix系统,将密钥文件复制到 $HOME/.idapro/ida.key
。如果这个目录不存在,IDA会在首次启动时自动创建。
IDA包含一个名为tvtuning.txt
的文件,里面介绍了如何配置不同类型的终端,包括远程Windows终端客户端,如SecureCRT和PuTTY。
在Linux终端中,你可能会遇到热键冲突的问题,比如ALT+F
可能被终端程序占用。为解决这个问题,你可以选择使用一个不与IDA冲突的终端程序,或者在IDA的配置文件中重新设置热键,以避免与终端程序的热键冲突。如果重新设置热键,你需要在每台使用IDA的机器上更新设置,以防混淆,同时可能会影响与其他IDA用户交互。
如果你选择在Linux中使用标准文本显示,IDA控制台的大小将固定。鼠标支持取决于是否安装了GPM(Linux控制台鼠标服务器)。如果没有安装GPM,你需要在启动IDA时为TVision指定noGPM
选项,如下所示:
# TVOPT=noGPM ./idal [file to disassemble]
在控制台模式下,颜色选择较为有限。你可以通过调整颜色设置(Options -> Colors
)来确保文本与背景色区分清楚。控制台模式提供了4种预定义的颜色模板,并允许你自定义反汇编窗口中各部分的颜色,共有16种颜色选项可供选择。
在X环境中,你可能使用KDE的konsole、Gnome的gnome-terminal、xterm等终端。除xterm外,大多数终端都有自己的菜单和热键,这些可能与IDA的热键冲突。因此,使用xterm运行IDA是个好选择,尽管它的视觉效果不一定最好。KDE的konsole是推荐的Linux终端,因为它界面整洁、热键冲突少且鼠标性能流畅。
为解决X Windows控制台中的键盘和鼠标问题,Jeremy Cooper开发了一个TVision库的X11端口。使用这个改良版的TVision,你可以在X窗口中运行IDA,而无需占用整个控制台。编译并安装这个新库可能会导致VGA字体无法加载的错误。若出现这种情况,需要从http://gilesorr.com/bashprompt/xfonts/下载合适的VGA字体,并告诉X服务器字体的位置。使用本地X11端口还可以将X11窗口转发到其他机器,比如在Linux上运行IDA并通过ssh将窗口转发到Mac上。
使用Hex-Rays的TVision库远程访问Linux上的IDA时,建议配置终端软件模拟xterm,并根据tvtuning.txt
文件的说明启动IDA。例如,为确保IDA支持鼠标,启动时必须指定TVOPT=ktrack
,特别是在使用SecureCRT作为终端模拟器时。当然,也可以导出TVOPT设置,免得在每次启动DA时都需要指定这些设置。
在Linux控制台版本的IDA中查看外部图形,需在窗口环境下运行IDA,并在ida.cfg
中配置GRAPH VISUALIZER
变量指向正确的图形查看程序。IDA 6.0之前版本仅支持GDL格式图形。你可以安装GDL查看器(如aiSee),并在IDA的主配置文件ida.cfg
中设置GRAPH VISUALIZER
以启动该程序。默认配置会指定查看GDL图形的命令。默认的设置如下所示:
GRAPH VISUALIZER = "qwingraph.exe -remove -timelimit 10"
在使用qwingraph
时,remove
选项会删除输入文件,用于显示临时文件。timelimit
选项设置生成图形的最大时间(秒),若超时,则使用更简单的布局算法。IDA 6.0及以后版本的GRAPH VISUALIZER
选项在配置文件中区分了Windows和非Windows平台。编辑ida.cfg
时,确保修改正确的平台部分。如果安装了GDL查看器(如aiSee),需在GRAPH VISUALIZER
中配置该查看器的路径。使用aiSee查看器的相关设置如下:
GRAPH VISUALIZER = "/usr/local/bin/aisee"
02 使用IDA的批量模式
IDA的批量模式用于自动化任务,它启动IDA并运行指定的DC脚本,然后结束。你可以通过命令行选项控制批量模式下的处理。
GUI版本的IDA可以在批量模式下运行,无需显示图形界面,适合自动化脚本。运行Windows控制台版本 (idaw.exe和idaw64.exe) 时,会生成一个控制台窗口,批处理结束后自动关闭。如果希望禁用控制台窗口,可以将输出重定向到空设备,如cmd.exe的NUL或cygwin的/dev/null,如下所示:
C:\Program Files\Ida>idaw -B some_program.exe NUL
IDA的批量模式由以下命令行参数控制。
- 使用
-A
选项启动DA时,它会在自动模式下运行,不会显示需要用户交互的对话框。但如果你从未接受过DA的许可协议,这个对话框仍会出现。 - 使用
~C
选项会让DA删除与指定文件相关的所有现有数据库,并创建一个新的数据库。 - 使用
-S
选项可以指定IDA启动时运行的IDC脚本。运行myscript.idc的语法为-Smyscript.idc
(-S
与脚本名之间没有空格)。IDA会在<IDADIR>/idc
目录中寻找该脚本。如果安装了DAPython,也可以指定Python脚本。 -B
选项启动批量模式,相当于使用-A -c -S analysis.idc
。它会让IDA自动分析指定的文件,生成汇编代码列表(.asm文件),然后保存和关闭数据库。
-S
选项是批量模式的关键,它指定了一个脚本来终止IDA。如果脚本没有关闭IDA,程序将不会自动结束。由于旧版TVision库的限制,Linux和OSX上的批处理必须在TTY控制台中运行,不能后台处理或重定向。最新版本的TVision支持TVHEADLESS
环境变量,这样可以重定向控制台输出,例如:
TVHEADLESS=1 ./idal -B inputfile.exe /dev/null
完全脱离控制台以在后台执行需要对stdin和stderr进行额外的重定向。