- 献词
- 致谢
- 前言
- 第一部分 IDA 简介
- 第 1 章 反汇编简介
- 第 2 章 逆向与反汇编工具
- 第 3 章 IDA Pro 背景知识
- 第二部分 IDA 基本用法
- 第 4 章 IDA 入门
- 第 5 章 IDA 数据显示窗口
- 第 6 章 反汇编导航
- 第 7 章 反汇编操作
- 第 8 章 数据类型与数据结构
- 第 9 章 交叉引用与绘图功能
- 第 10 章 IDA 的多种面孔
- 第三部分 IDA 高级应用
- 第 11 章 定制 IDA
- 第 12 章 使用 FLIRT 签名来识别库
- 第 13 章 扩展 IDA 的知识
- 第 14 章 修补二进制文件及其他 IDA 限制
- 第四部分 扩展 IDA 的功能
- 第 15 章 编写 IDA 脚本
- 第 16 章 IDA 软件开发工具包
- 第 17 章 IDA 插件体系结构
- 第 18 章 二进制文件与 IDA 加载器模块
- 第 19 章 IDA 处理器模块
- 第五部分 实际应用
- 第 20 章 编译器变体
- 第 21 章 模糊代码分析
- 第 22 章 漏洞分析
- 第 23 章 实用 IDA 插件
- 第六部分 IDA 调试器
- 第 24 章 IDA 调试器
- 第 25 章 反汇编器/ 调试器集成
- 第 26 章 其他调试功能
- 附录 A 使用 IDA 免费版本 5.0
- 附录 B IDC/SDK 交叉引用
7.3 基本代码转换
许多时候,对于 IDA 生成的反汇编代码清单,你会感到非常满意。但情况并非始终如此。如果你所分析的文件类型与常见编译器生成的普通二进制可执行文件相差甚大,你可能需要对反汇编分析和显示过程进行更多的控制。在分析采用自定义文件格式(IDA 无法识别)的模糊代码或文件时,情况更是如此。
IDA 提供的代码转换包括以下几类:
将数据转换为代码;
将代码转换为数据;
指定一个指令序列为函数;
更改现有函数的起始或结束地址;
更改指令操作数的显示格式。
利用这些操作的频繁程度取决于诸多因素及你的个人喜好。一般而言,如果二进制文件非常 复杂,或者说 IDA 不熟悉用于构建二进制文件的编译器所生成的代码序列,那么,IDA 在分析阶 段可能会遇到更多麻烦,因此,你也就需要对反汇编代码进行手动调整。
7.3.1 代码显示选项
你能够对反汇编代码清单所做的最简单的转换是,自定义 IDA 为每个反汇编行生成的信息数量。每一个反汇编行都可视为一个由许多部分组成的集合,毫不奇怪,IDA 就称之为 反汇编行部分 。标签、助记符和操作数始终会在反汇编行中显示。你也可以通过 Options▶General 命令打开“IDA Options”对话框,并选择“Disassembly”选项卡,为每一个反汇编行选择其他需要显示的部分(如图 7-4 所示)。
图 7-4 反汇编行显示选项
右上角的 Display disassembly line parts(显示反汇编行部分)区域提供了几个选项,可帮助你对反汇编行进行自定义。IDA 反汇编文本视图会默认选择行前缀、注释和可重复注释。下面说明其中的每一个选项。
Line prefixes(行前缀) 。行前缀是每个反汇编行的
section:address
部分。不选这个选项, 每个反汇编行将不会显示行前缀(图形视图的默认设置)。为说明这个选项,我们在后面 的反汇编代码中禁用了行前缀。Stack Pointer(栈指针) 。IDA 会详细分析每一个函数,以跟踪程序栈指针的变化。这种分析对于理解每个函数的栈帧的布局非常重要。选中栈指针选项,IDA 将会显示栈指针在每个函数执行过程中的相对变化。这样做有助于识别调用约定方面的差异(例如,IDA 可能不知道某个特殊的函数使用的是
stdcall
调用约定),或者确定对栈指针的不寻常操纵。栈指针跟踪如代码段中➊下面一列所示。在这个例子中,在第一条指令之后,栈指针改变了 4 字节;在第三条指令之后,总共改变了 0x7C 字节。在函数退出时,栈指针恢复到它的原始值(相对变化为 0 字节)。任何时候,如果 IDA 遇到一个函数返回语句,并检测到栈指针的值不为 0,这时,IDA 将标注一个错误条件,并将相关指令以红色显示。有时候,这样做可能是有意阻挠自动分析。其他情况下,这可能是由于编译器使用了 IDA 无法准确分析的“序言”和“尾声”代码。Comments (注释)和 Repeatable comments(可重复注释) 。取消任何一个选项,IDA 将不会显示相应类型的注释。如果你希望梳理一个反汇编代码清单,这些选项可能有用。
Auto comments(自动注释) 。IDA 可能会为某些指令类型自动添加注释。这种注释可以作为一种提醒,以帮助用户了解特殊指令的行为。IDA 不会为 x86
mov
等简单的指令添加注释。➋处的注释即为自动注释。用户注释优先于自动注释。因此,如果希望看到 IDA 为某一行添加的自动注释,你必须删除你添加的任何注释(常规注释或可重复注释)。Bad instructions marks(无效指令标记) 。IDA 可以标记出处理器认为合法,但一些汇编器可能无法识别的指令。未记入文档的 CPU 指令(而非非法指令)即属此类。这时,IDA 会将这种指令作为一个数据字节序列进行反汇编,并将未记入文档的指令显示为一段以
<BAD>
开头的注释。这样做的目的是生成大多数汇编程序都可以处理的反汇编代码。请参阅 IDA 帮助文档了解使用<BAD
>标记的更多详情。Numbers of opcode bytes(操作码字节数) 。大多数反汇编器都能够生成列表文件,逐个显示生成的机器语言字节,以及它们相应的汇编语言指令。IDA 支持将一个十六进制窗口与反汇编代码清单窗口同步,查看与每一个指令有关的机器语言字节。你可以指定 IDA 应为每个指令显示的机器语言字节的数量,选择性地查看与汇编语言指令混杂在一起的机器语言字节。
如果你正在反汇编的是指令大小固定的处理器的代码,那么,这个问题就相当简单。但是,对于 x86 等指令长度可变,其指令大小从 1 字节到十几字节不等的处理器来说,情况就变得复杂了。不管指令多长,IDA 都会在反汇编代码清单中为你在这里指定的字节数预留显示空间,而将反汇编代码行的剩余部分移向右边,从而为你指定的操作码字节数提供空间。在下面的代码中,操作码字节数设置为 5,➌下面的一列即说明了这一点。➍处的+号表示:根据当前设置,该位置的指令过长,因而无法完整显示。
➊ ➌ 000 55 push ebp 004 89 E5 mov ebp, esp 004 83 EC 78 sub esp, 78h ➋; Integer Subtraction 07C 8B 45 10 mov eax, [ebp+arg_8] 07C 89 45 F4 mov [ebp+var_C], eax 07C 8B 45 0C mov eax, [ebp+arg_4] 07C 89 45 A4 mov [ebp+var_5C], eax 07C C7 45 A0 0A ➍00+ mov [ebp+var_60], 0Ah 07C C6 45 A8 41 mov [ebp+var_58], 41h 07C 8B 45 A4 mov eax, [ebp+var_5C] 07C 89 44 24 04 mov [esp+4], eax 07C 8B 45 A0 mov eax, [ebp+var_60] 07C 89 04 24 mov [esp], eax 07C E8 91 FF FF FF call bar ➋; Call Procedure 07C C9 leave ➋; High Level Procedure Exit 000 C3 retn ➋; Return Near from Procedure
你还可以通过调整图 7-4 右下角的缩进值和边距,进一步自定义反汇编窗口。这些选项的任何变化都只影响当前数据库。这些选项的全局设置保存在主要配置文件/cfg.ida.cfg 中。
7.3.2 格式化指令操作数
在反汇编过程中,IDA 会做出许多决定,确定如何格式化与每条指令有关的操作数。通常,它做出的最重要决定是,如何格式化由各种指令使用的各种整数常量。除其他内容外,这些常量可表示跳转或调用指令中的相对偏移量、全局变量的绝对地址、用在算术运算中的值或者程序员定义的常量。为了使反汇编代码更具可读性,IDA 尽可能地使用符号名称,而非数字。有时候,IDA 根据被反汇编的指令(如调用指令)的上下文做出格式化决定;其他情况下,则根据所使用的数据(如访问的全局变量或栈帧中的偏移量)做出格式化决定。别的许多情况下,常量的具体使用情形可能并不十分清楚,这时,IDA 一般会将相关常量格式化成一个十六进制常量。
如果你碰巧是少数精通十六进制的人中的一个,那么,你会非常喜爱 IDA 的操作数格式化功能。右击反汇编窗口中的任何常量,打开如图 7-5 所示的上下文菜单。
图 7-5 常量格式化选项
在上图中,菜单提供的选项可将常量( 41h
)重新格式化成十进制、八进制或二进制值。由于这个例子中的常量属于 ASCII 可打印常量,菜单中还提供了一个选项,可将该常量格式化成一个字符常量。无论什么时候,只要你选择了一个特殊的选项,菜单将显示可用于替代操作数文本的具体文本。
许多时候,程序员在他们的源代码中使用已命名的常量。这些常量可能是使用了 #define
语句(或其等效语句)的结果,也可能属于一组枚举常量。遗憾的是,如果编译器已经完成对源代码的编译,它就不再可能确定源代码使用的是符号常量、文字常量还是数字常量。IDA 维护着大量与许多常见库(如 C 标准库或 Windows API)有关的已命名的常量,用户可以通过常量值的上下文菜单中的 Use standard symbolic constant(使用标准符号常量)选项来访问这些常量。在图 7-5 中,对常量 0AH 选择这个选项,将打开如图 7-6 所示的符号选择对话框。
图 7-6 符号选择对话框
根据我们尝试格式化的常量值进行过滤后,这个对话框中的常量从 IDA 的内部常量列表导入。在这个例子中,我们看到的是所有 IDA 认为与 0AH 相等的常量。如果我们确定在创建一个 X.25 类型的网络连接过程中使用了该值,那么,我们就可以选择 AF_CCITT,并最终得到下面的反汇编行:
.text:004010A2 mov [ebp+var_60], AF_CCITT
标准常量列表非常有用,可用于确定某个特殊的常量是否与一个已知的名称有关,使我们免 于在 API 文档中搜索潜在的匹配项,从而帮助我们节省大量时间。
7.3.3 操纵函数
在初步的自动分析完成之后,出于许多原因,你可能希望操纵函数。例如,IDA 无法定位一个函数调用,由于没有直接的方法到达函数,IDA 将无法识别它们。另外,IDA 可能无法正确确定函数的结束部分,需要你手动干预,以更正反汇编代码中的错误。此外,如果编译器已经将函数分割到几个地址范围,或者在优化代码的过程中,编译器为节省空间,将两个或几个函数的共同结束序列合并在一起,这时,IDA 同样无法确定函数的结束部分。
1. 新建函数
在某些情况下,你可能需要在没有函数的地方创建新函数。新函数可以由已经不属于某个函数的现有指令创建,或者由尚未被 IDA 以任何其他方式定义(如双字或字符串)的原始数据字节创建。将光标放在将要包含在新函数中的第一个字节或指令上,然后选择 Edit ▶Functions ▶Create Function,即可创建一个新函数。在必要时,IDA 会将数据转换成代码。接下来,它会向前扫描,分析函数的结构,并搜索返回语句。如果 IDA 能够找到正确的函数结束部分,它将生成一个新的函数名,分析栈帧,并以函数的形式重组代码。如果它无法找到函数的结束部分,或者发现任何非法指令,则这个操作将以失败告终。
2. 删除函数
你可以使用 Edit ▶Functions ▶Delete Function 命令删除现有函数。如果你认为 IDA 的自动分析出现错误,你可能希望删除一个函数。
3. 函数块
在由 Microsoft Visual C++编译器生成的代码中,经常可以找到函数块。编译器移动不常执行的代码段,用以将经常执行的代码段“挤入”不大可能被换出的内存页,由此便产生了函数块。
如果一个函数以这种方式被分割,IDA 会通过跟踪指向每个块的跳转,尝试定位所有相关的块。多数情况下,IDA 都能找到所有这些块,并在函数的头部列出每一个块,如下面某个函数的
反汇编代码所示:
.text:004037AE ChunkedFunc proc near .text:004037AE .text:004037AE var_420 = dword ptr -420h .text:004037AE var_41C = dword ptr -41Ch .text:004037AE var_4 = dword ptr -4 .text:004037AE hinstDLL = dword ptr 8 .text:004037AE fdwReason = dword ptr 0Ch .text:004037AE lpReserved = dword ptr 10h .text:004037AE .text:004037AE ; FUNCTION CHUNK AT ➊.text:004040D7 SIZE 00000011 BYTES .text:004037AE ; FUNCTION CHUNK AT .text:004129ED SIZE 0000000A BYTES .text:004037AE ; FUNCTION CHUNK AT .text:00413DBC SIZE 00000019 BYTES .text:004037AE .text:004037AE push ebp .text:004037AF mov ebp, esp
通过双击与函数块关联的地址(如➊处),可迅速到达该函数块。在反汇编代码清单中,IDA 通过界定其指令范围的注释和涉及其所属函数的注释来说明函数块,如下所示:
.text:004040D7 ; START OF FUNCTION CHUNK FOR ChunkedFunc .text:004040D7 .text:004040D7 loc_0040C0D7: ; CODE XREF: ChunkedFunc+72↑j .text:004040D7 dec eax .text:004040D8 jnz loc_403836 .text:004040DE call sub_4040ED .text:004040E3 jmp loc_403836 .text:004040E3 ; END OF FUNCTION CHUNK FOR ChunkedFunc
有时候,IDA 可能无法确定与函数关联的每一个块,或者函数可能被错误地识别成函数块,而非函数本身。在这种情况下,你需要创建自己的函数块,或删除现有的函数块。
要创建新的函数块,首先要选择属于该块的地址范围(不得属于现有的任何函数),并选择 Edit▶Functions ▶Append Function Tail 命令。这时,IDA 会要求你从所有已定义的函数列表中选择该函数的父函数。
说明 在反汇编代码清单中,函数块就叫做函数块;在 IDA 的菜单系统中,函数块叫做函数尾 (function tail )。
要删除现有的函数块,将光标放在要删除的块中的任何一行上,然后选择 Edit▶Functions ▶ Remove Function Tail 即可。这时,IDA 会要求你在删除选中的块之前确认该项操作。
如果函数块只会造成更多麻烦,你可以在初次将文件加载到 IDA 时,取消选择 Create function tails 加载器选项,要求 IDA 不要创建函数块。这个选项是一个加载器选项,可通过最初的文件加载对话框中的 Kernel Options (核心选项,参见第 4 章)访问。如果禁用了函数尾,你看到的主要不同是,已经包含函数尾的函数将包含指向函数边界以外区域的跳转。IDA 会在反汇编代码清单左侧的箭头窗口中用红线和箭头突出显示这些跳转。在对应函数的图形视图中,这些跳转的目标并不显示。
4. 函数特性
IDA 为它识别的每一个函数提供许多特性。如图 7-7 所示的函数属性对话框可用于编辑其中的某些特性。下面说明每一个可修改的属性。
图 7-7 函数编辑对话框
函数名称 。提供另外一种更改函数名称的方法。
起始地址 。函数中第一条指令的地址。通常,IDA 会在分析过程中,或根据创建函数时所使用的地址,自动识别这个地址。
结束地址 。函数中最后一条指令之后的地址。通常,它是函数的返回语句之后的指令的地址。多数情况下,IDA 会在分析阶段或在创建函数的过程中自动识别这个地址。如果 IDA 无法正确定位一个函数的结束部分,你就需要手动编辑这个值。记住,这个地址并不是函数的一部分,而是函数的最后一条指令之后的地址。
局部变量区 。函数的局部变量(见图 6-4)专用的栈字节数。多数情况下,IDA 会通过分析函数的栈指针的行为,自动计算出这个值。
保存的寄存器 。为调用方保存寄存器(见图 6-4)所使用的字节数。IDA 认为保存的寄存器区域放在保存的返回地址顶部、与函数有关的所有局部变量的下方。一些编译器选择将寄存器保存在函数局部变量的顶部。IDA 认为保存这些寄存器所使用的空间属于局部变量区域,而非保存的寄存器区域。
已删除字节 。已删除字节表示当函数返回调用方时,IDA 从栈中删除的参数的字节数。对
cdecl
函数而言,这个值始终为 0 。对stdcall
函数来说,这个值表示传递到栈上的所有参数(见图 6-4)占用的空间。在 x86 程序中,如果 IDA 观察到程序使用了返回指令的RET N
变体,它将自动确定这个值。帧指针增量 。有时候,编译器可能会对函数的帧指针进行调整,使其指向局部变量区域的中间,而不是指向保存在局部变量区域底部的帧指针。调整后的帧指针到保存的帧指针之间的这段距离叫做帧指针增量(frame pointer delta )。多数情况下,IDA 会在分析函数的过程中自动计算出帧指针增量。编译器利用栈帧增量进行速度优化。使用增量的目的,是在离帧指针 1 字节(带符号)的偏移量(128~+127)内保存尽可能多的栈帧变量。
还有另外一些特性复选框可用于设置函数的特性。与对话框中的其他选项一样,这些复选框通常反映的是 IDA 自动分析得到的结果。以下是这些可启用也可禁用的属性。
不返回 。函数不返回到它的调用方。如果调用这样的函数,在相关的调用指令之后,IDA 认为函数不会继续执行。
远函数 。这个属性用于在分段体系结构上将一个函数标记为远函数。在调用该函数时,函数的调用方需要指定一个段和一个偏移值。通常,是否使用远调用,应由程序中使用的内存模式决定,而不是由体系结构支持分段[例如,在 x86 体系结构上使用了 大 内存模式(相对于 平 内存模式)]决定。
库函数 。这个属性将一个函数标记为库代码。库代码可能包括静态链接库中的编译器或函数所包含的支持例程。将一个函数标记为库函数后,该函数将以分配给库函数的颜色显示,从而与非库代码区分开来。
静态函 数。除在函数的特性列表中显示静态修饰符外,其他什么也不做。
基于 BP 的帧 。这个特性表示函数利用了一个帧指针。多数情况下,你可以通过分析函数的“序言”来自动确定这一点。但是,如果通过分析无法确定给定的函数是否使用了帧指针,就可以手动选择这个特性。如果你手动选择了这个特性,一定要相应地调整保存的寄存器的大小(通常指根据保存的帧指针的大小增大)和局部变量的大小(通常指根据保存的帧指针的大小减少)。对基于帧指针的帧而言,使用帧指针的内存引用被格式化,以利用符号栈变量名称,而非数字偏移量。如果没有设置这个特性,则认为栈帧引用与栈指针寄存器有关。
BP 等于 SP 。一些函数将帧指针配置为在进入一个函数时指向栈帧(以及栈指针)的顶端。 在这种情况下,就应设置该属性。基本上,它的作用等同于将帧指针增量的大小设置为 等于局部变量区域。
5. 栈指针调整
如前所述,IDA 会尽其所能跟踪函数内每一条指令上的栈指针的变化。IDA 跟踪这种变化的准确程度,在很大程度上影响着函数的栈帧布局的准确程度。如果 IDA 无法确定一条指令是否更改了栈指针,你就需要手动调整栈指针。
如果一个函数调用了另一个使用 stdcall
调用约定的函数,就会出现上述情况,这是最简单的一种情况。如果被调用的函数位于 IDA 无法识别的共享库中(IDA 拥有与许多常用库函数的签名和调用约定有关的信息),那么,IDA 并不知道该函数使用了 stdcall
调用约定,也就无法认识到:被调用的函数会将栈指针修改后返回。因此,IDA 会为函数的剩余部分提供一个错误的栈指针值。在下面的函数调用中, some_imported_func
即位于共享库中,这正好说明了上述问题(注意,“栈指针行部分”选项已被选中):
.text:004010EB 01C push eax .text:004010F3 020 push 2 .text:004010FB 024 push 1 ➋ .text:00401102 028 call some_imported_func .text:00401107 ➊ 028 mov ebx, eax
由于 some_imported_func
使用的是 stdcall
调用约定,在返回时,它清除了栈中的 3 个参数,➊处的正确栈指针值应为 01C
。修正这个问题的一种方法,是对➋处的指令进行手动栈调整。要进行栈调整,首先应选中进行调整的地址,并选择 Edit▶Functions▶Change Stack Pointer (热键为 ALT+K),然后指定栈指针更改的字节数,在本例中为 12 。
虽然前面的例子能够解决这个问题,但这个特殊问题还有一个更好的解决办法。假如 some_imported_func
被调用了许多次,那该怎么办呢?这时,我们需要在 some_imported_ func
被调用的每一个位置进行上述栈调整。很明显,这是一个非常繁琐的任务,很容易出错。那么,我们最好是让 IDA 了解 some_imported_func
的行为。因为我们处理的是一个导入的函数,如果我们尝试导航到该函数,我们将最终导航到该函数的导入表条目,如下所示:
.idata:00418078 ; Segment type: Externs
.idata:00418078 ; _idata
.idata:00418078 extrn some_imported_func:dword ; DATA XREF: sub_401034 ↑r
尽管这是一个导入的函数,你也可以编辑有关其行为的一条信息:与该函数有关的已删除字节的数量。通过编辑这个函数,你可以指定它在返回时从栈中删除的字节数,IDA 将会“扩散”这一信息,将其应用于调用该函数的每一个位置,立即纠正每个位置的栈指针计算错误。
为了改进自动分析,IDA 融入了一些高级技术,通过一个与栈指针行为有关的线性方程系统来解决栈指针错误问题。因此,我们可能根本不会意识到,IDA 之前并不了解诸如 some_imported_func
之类的函数的详细信息。欲了解有关这些技术的更多信息,请参阅 Ilfak 的博客标题为“Simplex method in IDA Pro”的文章,地址为 http://hexblog.com/2006/06/ 。
7.3.4 数据与代码互相转换
在自动分析阶段,字节有时可能被错误地归类。数据字节可能被错误地归类为代码字节,并被反汇编成指令;而代码字节可能被错误地归类为数据字节,并被格式化成数据值。有许多原因会导致这类情况,如一些编译器将数据嵌入在程序的代码部分,或者一些代码字节从未被作为代码直接引用,因而 IDA 选择不对它们反汇编。模糊程序则特别容易模糊代码部分与数据部分之间的区别。
无论你出于什么原因希望对反汇编代码重新格式化,这个过程都相当简单。在重新格式化之前,首先必须删除其当前的格式(代码或数据)。右击你希望取消定义的项目,在结果上下文菜单中选择 Undefine(也可使用 Edit▶Undefine 命令或热键 U),即可取消函数、代码或数据的定义。取消某个项目的定义后,其基础字节将作为原始字节值重新格式化。在执行取消定义操作之前,使用“单击并拖动”操作选择一个地址范围,可以取消大范围内的定义。下面以一个简单的函数为例:
.text:004013E0 sub_4013E0 proc near .text:004013E0 push ebp .text:004013E1 mov ebp, esp .text:004013E3 pop ebp .text:004013E4 retn .text:004013E4 sub_4013E0 endp
取消这个函数的定义将得到下面这些未分类的字节,我们几乎可以以任何方式重新对它们进 行格式化:
.text:004013E0 unk_4013E0 db 55h ; U
.text:004013E1 db 89h ; ë
.text:004013E2 db 0E5h ; s
.text:004013E3 db 5Dh ; ]
.text:004013E4 db 0C3h ; +
要反汇编一组未定义的字节,右击其中的第一个字节,在上下文菜单中选择 Code(也可使用 Edit▶Code 或热键 C)。这样,IDA 将开始反汇编所有字节,直到它遇到一个已定义的项目或非法指令。在执行代码转换操作之前,使用“单击并拖动”操作选择一个地址范围,可以进行大范围代码转换操作。
将代码转换为数据的逆向操作要复杂一些。首先,使用上下文菜单不可能将代码转换为数据。你可以通过 EditData 和热键 D 来完成。要想将指令批量转换为数据,最简单的方法是取消你希望转换为数据的所有指令的定义,然后对数据进行相应的格式化。基本的数据格式化将在下一节讨论。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论