返回介绍

16.2 IDA 应用编程接口

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

IDA 的 API 由<SDKDIR>/include 目录中的头文件定义。关于可用的 API 函数,并没有一个完整的目录(不过 Steve Micallef 在他的插件编写指南中已经收集了一部分 API 函数)。最初,许多潜在的 SDK 程序员很难接受这一事实。实际上,对于“我如何使用 SDK 做……?”这个问题,从来没有简单的答案。要想获得这类问题的答案,可以采取两种途径:将这些问题粘贴到 IDA 的用户论坛,或者搜索 API 文档尝试自己找到答案。你可能会问,该搜索哪些文档呢?当然是头文件文档!虽然这些文档并不是最便于搜索的文档,但是,其中确实包含 API 的所有功能。这时,你可以使用 grep 工具(或者一个适当的替代工具,最好是内置在编程编辑器中)。不过,关键是你要知道搜索什么内容,因为这并不总是非常明显。

有一些方法可以帮助你缩小搜索范围。首先,利用你所掌握的 IDC 脚本语言知识,使用关键字及从 IDC 中获得的函数名称,设法在 SDK 中找到类似的功能。但是,令人非常沮丧的是,虽然许多 SDK 函数的用途与 IDC 函数的用途完全相同,但这些函数的名称却很少相同。这使得程序员需要学习两组 API 调用,一组用于 IDC ,一组用于 SDK 。为了解决这个问题,我们在附录 B 中提供了一个完整的列表,其中列出了 IDC 函数及用于执行这些函数的对应的 SDK 6.1 操作。

缩小 SDK 相关搜索范围的第二种方法是熟悉各种 SDK 头文件的内容和作用(更为重要)。通常,头文件会根据函数类型为相关的函数和数据结构分组。例如,处理用户交互的 SDK 函数归入 kernwin.hpp 文件。如果通过 grep 之类的搜索无法确定你需要的功能,那么,了解与该功能有关的头文件信息,将可以帮助你缩小搜索范围,减少需要深入分析的文件的数量。

16.2.1 头文件概述

虽然 SDK 的 readme.txt 文件概括介绍了大多数常用的头文件,但是,在这一节中,我们重点说明其他一些与这些文件有关的信息。首先,绝大多数头文件使用.hpp 作为后缀,但也有一些文件使用.h 作为后缀。在命名将要包含在文件中的头文件时,这很可能会导致细小的错误。其次,ida.hpp 是 SDK 的主要头文件,该文件应包括在所有与 SDK 有关的项目中。最后,SDK 利用预处理指令阻止用户访问那些 Hex-Rays 认为危险的函数(如 strcpysprint )。有关所有这些函数的完整内容,请参考 pro.h 头文件。要恢复对这些函数的访问,在将 ida.hpp 包含在你自己的文件中之前,必须定义 USE_DANGEROUS_FUNCTIONS 宏,如下所示:

#define USE_DANGEROUS_FUNCTIONS  
#include 

如果没有定义 USE_DANGEROUS_FUNCTIONS 宏,将导致一个构建错误,其大致意思是: dont_ use_snprintf 是一个未定义的符号(如果尝试使用 snprintf 函数)。为了“补偿”对这些所谓的危险函数的限制,SDK 为每个函数定义了更加安全的替代函数,这些函数通常采用 qstr XXXX 的形式,如 qstrncpyqsnprintf 。这些更加安全的函数也是在 pro.h 文件中声明的。

同样,SDK 还限制用户访问许多标准文件输入/输出变量和函数,如 stdinstdoutfopenfwritefprintf 。这种限制部分是由于 Borland 编译器的局限性所致。对于这些函数,SDK 同样为它们定义了替代函数,它们一般为 qXXX 的形式,如 qfopenqfprintf 。如果你需要访问标准文件函数,那么,在包含 fpro.h 文件之前,你必须定义 USE_STANDARD_FILE_FUNCIONS 宏 。(fpro.h 文件由 kernwin.hpp 包含,后者又由其他几个文件包含。)

多数情况下,每个 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 。这个文件包含前面讨论的文件输入/输出替代函数,如 qfopen

  • frame.hpp 。这个头文件包含用于操纵栈帧的函数。

  • funcs.hpp 。这个头文件包含用于处理经过反汇编的函数的函数和数据结构,以及用于处理 FLIRT 签名的函数。

  • gdl.hpp 。这个文件为使用 DOT 或 GDL 生成图形声明支持例程。

  • ida.hpp 。这个文件是处理 SDK 所需的主要头文件,其中包含 idainfo 结构的定义和全局变量 inf 的声明,还包含许多字段,一些字段提供与当前数据库有关的信息,另一些字段则由配置文件设置初始化。

  • idp.hpp 。这个文件包含一些结构体的声明,这些结构体构成处理器模块的基础。描述当前处理器模块的全局变量 ph 和描述当前汇编器的全局变量 ash 也在这个文件中定义。

  • kernwin.hpp 。这个文件声明用于处理用户交互和用户界面的函数。这个文件还声明了 SDK 中替代 IDC 的 AskXXX 函数的函数,以及用于设置显示位置和配置热键关联的函数。

  • lines.hpp 。这个文件声明用于生成格式化的彩色反汇编行的函数。

  • loader.hpp 。这个文件包含分别用于创建加载器模块和插件模块的 loader_tplugin_t 结构体的声明,以及用于加载文件和激活插件的函数的声明。

  • name.hpp 。这个文件声明用于操纵已命名位置(相对于结构或栈帧中的名称,它们分别在 stuct.hpp 和 funcs.hpp 中声明)的函数。

  • netnode.hpp 。网络节点是通过 API 访问的最低级的存储结构。通常,IDA 的用户界面完全隐藏了网络节点细节。这个文件包含 notnode 类的定义及用于网络节点低级操纵的函数。

  • pro.h 。这个文件包含任何 SDK 模块所需的顶级类型定义和宏。不需要明确将这个文件包含在你的项目中,因为 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 的一个子类)的声明, segment_t 类用于描述二进制文件的各节(如 .text.data 等)。这个文件还声明了用于处理段的函数。

  • struct.hpp 。这个文件声明 struc_t 类以及操纵数据库中的结构的函数。

  • typeinf.hpp 。这个文件声明用于处理 IDA 类型库的函数。另外,在这个文件中声明的函数还可用于访问函数签名,包括函数返回类型和参数序列。

  • ua.hpp 。这个文件声明在处理器模块中大量使用的 op_tinsn_t 类。这个文件还声明了用于反汇编各条指令,以及为每个反汇编行的各个部分生成文本的函数。

  • xerf.hpp 。这个文件声明添加、删除和遍历代码和数据交叉引用所需的数据类型和函数。

上面介绍了 SDK 自带的大约一半的头文件。但是,在你深入学习 SDK 时,建议你不但要了解这个列表中的文件,还要熟悉其他所有的头文件。已发布的 API 函数带有 ida_export 标记。SDK 自带的链接库仅导出带有 ida_export 标记的函数。请不要因为使用 idaapi 而产生误解,因为它仅仅表示一个函数只有在 Windows 平台上才使用 stdcall 调用约定。有时候,你可能会遇到一些不带 ida_export 标记的函数,你不能在模块中使用这些函数。

16.2.2 网络节点

IDA 的许多 API 以 C++ 类为基础创建,它模拟一个经过反汇编的二进制文件的各节。另一方面, netnode 类则有些神秘,因为它似乎与二进制文件的结构(如节、函数、指令等)没有任何直接关系。

网络节点是 IDA 数据库中最低级和最通用的数据存储机制。作为一名模块程序员,你很少需要直接操作网络节点。许多较为高级的数据结构均隐藏了一个事实,即它们最终都需要依靠网络节点永久存储在数据库中。nalt.hpp 文件详细说明了在数据库中使用网络节点的一些方法。例如,通过这个文件,我们知道,与二进制文件导入的共享库和函数有关的信息存储在一个名为 import_node 的网络节点中(是的,网络节点也有名称)。网络节点还是 IDC 全局数组的永久存储机制。

网络节点由 netnode.hpp 文件全面描述。但是,从宏观角度看,网络节点是 IDA 的内部存储结构,其用途非常广泛。尽管如此,即使是 SDK 程序员也不知道它们的具体结构。为了提供一个访问这些存储结构的接口,SDK 定义了 netnode 类,它就像是这个内部存储结构的“不透明包装”。 netnode 类包含唯一一个数据成员,即 netnodenumber ,它是一个用于访问网络节点的内部表示形式的整数标识符。每个网络节点由它的 netnodenumber 唯一标识。在 32 位系统上, netnodenumber 是一个 32 位整数,可以表示 232 个唯一的网络节点。在 64 位系统上, netnodenumber 是一个 64 位整数,可以表示 264 个唯一的网络节点。多数情况下, netnodenumber 代表数据库中的一个虚拟地址,它在数据库中的每个地址与存储和某地址有关的信息所需的网络节点之间建立一个自然的对应关系。任何与一个地址有关的信息(如注释)都存储在与这个地址有关的网络节点中。

要操纵网络节点,建议你使用一个实例化的 netnode 对象调用 netnode 类的成员函数。浏览 netnode.hpp 文件,你会发现,有许多非成员函数似乎也可用于操纵网络节点。相对于成员函数,我们不鼓励使用这些函数。但是,你会注意到, netnode 类中的大多数成员函数都是某个非成员函数的“瘦包装器”(thin wrapper)。

在 SDK 内部,网络节点可用于存储几种不同类型的信息。每个网络节点都有一个最长达 512 个字符的名称和一个最长达 1024 个字节的主值。 netnode 类的成员函数用于检索( name )或修改( rename )网络节点的名称。其他成员函数可按整数( set_long 、long_value )、字符串( set、valstr )或任意二进制大对象( set、valobj1 处理网络节点的主值。处理主值的方式由你所使用的函数决定。

1. 二进制大对象,即 BLOB ,通常指任何大小可变的二进制数据。

使情况更加复杂的是:除了名称和主值外,每个 netnode 还能够存储 256 个稀疏数组,其中的数组元素可以为任意大小,最大为 1024 个字节。这些数组分为 3 种相互重叠的类型。第一类数组使用 32 位索引值,最多可以保存 40 亿个数组元素。第二类数组使用 8 位索引值,最多可以保存 256 个数组元素。最后一类数组实际上是使用字符串作为密钥的散列表。无论使用哪一类数组,数组中的每个元素能接受的值最大为 1024 个字节。简言之,一个网络节点可以存储数量极其庞大的数据,现在我们只需要了解这一点是如何做到的。

或许你希望知道这些信息全都存储在什么地方,当然,你并不是唯一想了解这个问题的人!在 IDA 数据库中,网络节点的所有内容都存储在二叉树节点中。二叉树节点反过来又存储在一个 ID0 文件中,在关闭数据库时,ID0 文件又存储在一个 IDB 文件中。在 IDA 的任何显示窗口中,你都不可能看到你创建的任何网络节点内容。你可以任意操纵这些数据。因此,对于你希望用来存储调用结果的任何插件和脚本而言,网络节点是永久存储它们的理想位置。

1. 创建网络节点

关于网络节点,有一个令人迷惑的地方,即在你的一个模块中声明一个 netnode 变量,并不一定会在数据库中创建该网络节点的内部表示形式。只要满足以下其中一个条件,就可以在数据库内部创建一个网络节点。

  • 网络节点分配有一个名称。

  • 网络节点分配有一个主值。

有一个值存储在网络节点的一个内部数组中。

有 3 个构造函数可用于声明模块中的网络节点。这些函数的原型包含在 netnode.hpp 文件中,它们的应用示例如代码清单 16-1 所示。

代码清单 16-1 声明网络节点

    #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➌

在这个例子中,执行代码后,数据库中只存在一个网络节点( n3 )。如果网络节点 n1n2 之前已经创建并且填充有数据,它们可能会存在。无论之前是否存在,这时 n1 都能接受新的数据。如果 n2 并不存在,则意味着你不可能在数据库中找到名为 $ node 2 的网络节点,那么,你必须首先显式创建 n2 (➍或➎),才能将数据存储到这个节点中。如果希望保证能够在 n2 中存储数据,我们需要添加以下“安全检查”:

if (BADNODE == (nodeidx_t)n2) {  
   n2.create("$ node 2");  
}

前面的例子说明了 nodeidx_t 运算符的用法,它可以将网络节点转换成 nodeidx_tnodeidx_t 运算符只返回相关网络节点的 netnodenumber 数据成员,并可轻易将 netnode 变量转换成整数。

关于网络节点,有一点需要注意:网络节点首先必须拥有一个有效的 netnodenumber ,然后你才能在该网络节点中存储数据。如上面例子中➋处所示,和 n1 一样, netnodenumber 可以通过一个构造函数显式分配。另外,如果在构造函数中使用 create 标志(和 n3 一样,如➌处所示),或通过 create (和 n2 一样)函数创建一个网络节点,这时也可以在内部生成一个 netnodenumber 。内部分配的 netnodenumber0xFF000000 开头,并随每个新建的网络节点而递增。

在这个例子中,我们完全忽略了网络节点 n0 。当前, n0 既没有编号也没有名称。我们可以使用 create 函数,以和创建 n2 类似的方法,根据名称创建 n0 。我们也可以采用另一种形式,用一个内部生成的有效的 netnodenumber 创建一个未命名的网络节点,如下所示:

n0.create();  //assign an internally generated netnodenumber to n0

这样,我们就可以将数据存储到 n0 中,但是将来我们并没有办法检索这些数据,除非我们将分配给它的 netnodenumber 记录在某个地方,或者给 n0 分配一个名称。这表示如果网络节点与某个虚拟地址关联(类似于例子中的 n1 ),我们就可以轻松访问这个节点。对于其他所有网络节点,如果为它们分配名称,那么我们就可以对将来的所有网络节点引用进行具名查询(和例子中的 n2n3 一样)。

注意,对于已命名的网络节点,我们选择使用以“ $ ”为前缀的名称,这样做是遵循 netnode.hpp 文件中的建议,以避免与 IDA 内部使用的名称造成冲突。

2. 网络节点中的数据存储

现在,你已经知道如何创建一个可用于存储数据的网络节点。下面,我们回过头来讨论网络节点中的内部数组的存储能力。在将一个值存储到网络节点中的数组时,需要指定 5 方面的信息:一个索引值、一个索引大小(8 或 32 位)、一个待存储的值、这个值包含的字节数以及一个用于存储这个值的数组(每类 256 个数组中的一个)。索引大小参数由我们用于存储或检索数据的函数隐式指定。其他值则以参数形式传递给函数。通常,选择将一个值存储到 256 个数组中的哪一个数组的参数叫做标签(tag),它一般使用一个字符来指定(尽管并不需要如此)。网络节点的文档中列出了一些特殊的值类型,它们分别是 altval、supval 和 hashval。默认情况下,每一类值与一个特定的数组标签关联: 'A' 代表 altval, 'S' 代表 supval , 'H' 代表 hashval。第 4 类值叫做 charval ,它没有任何与之关联的数组标签。

值得注意的是,这些值类型与如何将数据存储到网络节点关联更大,而与网络节点中某个特定的数组关系不大。在存储数据时,通过指定一个备用的数组标签,你可以将任何类型的值存储到任何数组中。任何时候,你都需要记住你存储到某个特殊数组位置中的数据的类型,以便将来使用适合该数据类型的检索方法。

altval 提供了一个简单的接口,可用于存储和检索网络节点中的整数数据。altval 可存储到网络节点中的任何数组中,但默认情况下,它被存储到 'A' 数组中。不管你希望将整数存储到哪一个数组中,使用与 altval 有关的函数都将大大简化存储过程。使用 altval 存储和检索数据的代码如代码清单 16-2 所示。

代码清单 16-2 访问网络节点 altval

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

在这个例子中,你看到一种将被其他类型的网络节点值重复使用的模式,即使用 XXXset (这里为 altset )函数将一个值存储到一个网络节点中,并使用 XXXval (这里为 altval )函数从网络节点中检索这个值。如果希望使用 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 函数为数组标签参数提供默认值。

要在网络节点中存储和检索数据,supval 提供的方法最多。supval 可表示任意大小的数据,最小为 1 个字节,最大为 1024 个字节。使用 32 位索引值时,存储和检索 supval 的默认数组为 'S' 数组。但是,通过指定一个适当的数组标签值,同样可以将 supval 存储到 256 个可用数组中的任何一个。字符串是一种常见的任意长度的数据,它们可由操纵 supval 的函数进行特殊处理。代码清单 16-3 中的代码说明了如何将 supval 存储到网络节点中。

代码清单 16-3 存储网络节点 supval

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 (数据)+1,并将一个零终止符存储在该字符串数据的后面。

从 supval 中检索数据需要特别小心,因为在检索数据前,你可能并不知道该 supval 所包含的数据的数量。当你从 supval 中检索数据时,字节从网络节点被复制到一个用户提供的输出缓冲区中。如何确保输出缓冲区足够大,能够接收所有的 supval 数据呢?第一种方法是将所有 supval 数据复制到一个至少有 1024 个字节大小的缓冲区中;第二种方法是通过查询 supval 的大小,预先设置输出缓冲区的大小。有两个函数可用于检索 supval 。 supval 函数用于检索任何数据,而 supstr 函数则专门用于检索字符串数据。在使用这两个函数时,你需要指定一个指向你的输出缓冲区的指针,同时指定该缓冲区的大小。 supval 函数的返回值是复制到输出缓冲区中的字节数量,而 supstr 函数的返回值则是复制到输出缓冲区中的字符串的长度,但不包括零终止符,即使零终止符被复制到缓冲区中。这两个函数都接受一个特例,即用 NULL 指针代替输出缓冲区指针。在这种情况下, supvalsupstr 返回保存 supval 数据所需的字节数(包括任何零终止符)。使用 supvalsupstr 函数检索 supval 数据的代码如代码清单 16-4 所示。

代码清单 16-4 检索网络节点 supval

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

使用 supval ,你可以访问存储在一个网络节点中的任何数组中的任何数据。例如,通过将 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);  
}

hashval 提供了另一种访问网络节点的接口。除了与整数索引有关外,hashval 还与密钥字符串有关。使用 hashset 函数的重载版本,可以轻易地将整数数据或数组数据与一个散列密钥关联起来。如果提供合适的散列密钥, hashval、hashstrhashval_long 函数可用于检索 hashval。与 hashXXX 函数有关的标签值实际上选择的是 256 个散列表中的一个,默认散列表为 'H' 。指定 'H' 以外的标签,可以选择供替代的散列表。

我们提到的最后一个访问网络节点的接口为 charval 接口。 charvalcharset 函数提供了一种简单的方法,可以在网络节点数组中存储单字节数据。由于不存在与 charval 存储和检索有关的默认数组,因此,你必须为每一个 charval 操作指定一个数组标签。charval 存储在与 altval 和 supval 相同的数组中,charval 函数不过是 1 字节 supval 的包装器而已。

netnode 类提供的另一项功能是它能够遍历网络节点数组(或散列表)的内容。遍历通过对 altval、supval 、hashval 和 charval 有效的 XXX1stXXXnstXXXlastXXXprev 函数执行。代码清单 16-5 中的例子说明了如何遍历默认的 altval 数组( 'A' )。

代码清单 16-5 枚举网络节点 altval

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

遍历 supval 、hashval 和 charval 的方法与遍历 altval 的方法非常类似,但是,你会发现,所使用的语法因被访问的值的类型而异。例如,遍历 hashval 将返回散列密钥而非数组索引,然后再用得到的密钥检索 hashval。

网络节点与 IDC 全局数组

你可能记得,我们在第 15 章提到,IDC 脚本语言提供永久全局数组。网络节点为 IDC 全局数组提供备份存储。在你为 IDC CreateArray 函数提供名称时,字符串 $ idc_array 将被附加到这个名称前面,构成一个网络节点名称。随后,新建网络节点的 netnodenumber 将作为 IDC 数组标识符返回。IDC SetArrayLong 函数将一个整数存储到 altval( 'A' )数组中,而 SetArrayString 函数将一个字符串存储到 supval ( 'S' )数组中。当你使用 GetArrayElement 函数从 IDC 数组中检索一个值时,你提供的标签( AR_LONGAR_STR )代表 altval 和 supval 数组用于存储对应的整数或字符串数据的标签。

附录 B 提供了一些额外的信息,说明如何在执行 IDC 函数的过程中使用网络节点,以及如何使用网络节点在数据库中存储各种信息(如注释)。

3. 删除网络节点及其数据

netnode 类还提供用于删除各数组元素、全部数组内容或全部网络节点内容的函数。删除整个网络节点的过程相当简单。

netnode n("$ idabook", 0, true);  
n.kill();                        //entire contents of n are deleted

在删除各数组元素或全部数组内容时,你必须选择适当的删除函数,因为这些函数的名称非常相似。如果选择了错误的函数,可能会导致大量数据丢失。下面带有注释的例子说明了如何删除 altval:

  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 数组中一个元素(➋)所使用的语法非常相似。如果在删除一个数组元素时,因为某种原因你没有指定一个索引,那么,最终你可能会删除整个数组。删除 supval 、charval 和 hashval 数据的函数也与之类似。

16.2.3 有用的 SDK 数据类型

IDA 的 API 定义了许多 C++ 类,专门模拟可执行文件中的各个组件。SDK 中包含大量类,用于描述函数、程序节、数据结构、各汇编语言指令以及每条指令中的各操作数。SDK 还定义了其他类,以实现 IDA 用于管理反汇编过程的工具。后一种类型的类定义数据库的一般特点、加载器模块的特点、处理器模块的特点和插件模块的特点以及每条反汇编指令所使用的汇编语法。

下面介绍了一些较为常见的通用类。在后面几章中,我们将讨论特定于插件、加载器和处理器模块的类。本节主要介绍一些类、它们的作用以及每个类中一些重要的数据成员。操纵每个类所使用的函数将在 16.2.4 节中介绍。

  • area_t (area.hpp) 。这个结构体描述一系列地址,并且是其他几个类的基类。该结构体包含两个数据成员: startEA (包括)和 endEA (不包括),它们定义地址范围的边界。该结构体还定义了一些成员函数,以计算地址范围的大小。这些函数还可以对两个区域进行比较。

  • func_t (func.hpp ) 。这个类从 area_t 继承而来,其中添加了其他一些数据字段以记录函数的二进制属性,如函数是否使用帧指针,还记录了描述函数的局部变量和参数的属性。为了进行优化,一些编译器可能会将函数分割成一个二进制文件中的几个互不相邻的区域。IDA 把这些区域叫做 (chuck )或 (tail )。 func_t 类也用于描述尾块(tail chunk )。

  • segment_t (segment.hpp)segment_t 类是 area_t 的另一个子类,其中添加了一些数据字段,以描述段的名称、段中可用的权限(可读、可写、可执行)、段的类型(代码、数据等)、一个段地址所使用的位数(16 、32 或 64 位)。

  • idc_value_t (expr.hpp ) 。这个类描述一个 IDC 值的内容,任何时候它都可能包含一个字符串、一个整数或一个浮点值。当与一个已编译模块中的 IDC 函数交互时,其类型被大量使用。

  • idainfo (ida.hpp ) 。这个结构体用于描述开放数据库的特点。ida.hpp 文件声明了唯一一个名为 infidainfo 全局变量。这个结构体中的字段描述所使用的处理器模块的名称、输入文件类型(如通过 filetype_t 枚举得到的 f_PEf_MACHO )、程序进入点( beingEA )、二进制文件中的最小地址( minEA )、二进制文件中的最大地址( maxEA )、当前处理器的字节顺序( mf )以及通过解析 ida.cfg 得到的许多配置设置。

  • struc_t (struct.hpp ) 。这个类描述反汇编代码清单中结构化数据的布局。它用于描述.Structures 窗口中的结构体以及函数栈帧的构成。 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 )、一个可能由 6 个 op_t 类型的操作数数值( Operand )构成的数组(IDA 限制每条指令最多使用 6 个操作数)。 itype 字段由处理器模块设置。对于标准的 IDA 处理器模块, itype 字段被设定为在 allins.hpp 文件中定义的一个枚举常量。如果使用第三方处理器模块,则必须从模块开发者那里获得潜在 itype 值的列表。需要注意的是, itype 字段通常与该指令的二进制操作码无关。

上面并没有列出 SDK 所使用的全部数据类型,它仅仅介绍了一些较为常用的类,以及这些类中的一些较为常用的字段。

16.2.4 常用的 SDK 函数

虽然 SDK 使用 C++ 编程,并定义了大量 C++ 类,但在许多时候,SDK 更倾向于使用 C 风格的非成员函数来操纵数据库中的对象。对于多数 API 数据类型,SDK 常常使用非成员函数(它们需要一个指向某个对象的指针)而不是以你期望的方式操纵对象的成员函数来处理它们。

在下面的总结中,我们将介绍许多 API 函数,它们提供的功能与第 15 章中讨论的许多 IDC 函数的功能类似。可惜,在 IDC 和 API 中,执行相同任务的函数的名称并不相同。

1. 基本数据库访问

下面的函数由 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 包含有效数据,则返回真,否则返回假。

还有其他函数可用于访问其他数据大小。需要注意的是, get_original_XXX 函数读取的是第一个初始值,它不一定是修补之前位于某个地址处的值。例如,如果一个字节值被修补两次,那么,在整个过程中,这个字节就保存了 3 个不同的值。在第二次修补后,我们可以访问当前值和初始值,但没有办法访问第二个值(它由第一个补丁设置)。

2. 用户界面函数

与 IDA 用户界面的交互由唯一一个名为 callui调度函数 处理。向 callui 传递一个用户界面请求(其中一个枚举 ui_notification_t 常量)以及该请求所需的其他参数,即可提出各种用户界面服务请求。每种请求所需的参数由 kernwin.hpp 文件指定。不过 kernwin.hpp 文件还定义了大量便捷函数(convenience function ),只是这些函数隐藏了许多直接使用 callui 的细节。下面是几个常见的便捷函数。

  • msg(char *format, ...) ,在消息窗口中打印一条格式化消息。这个函数类似于 C 的 printf 函数,接受一个 printf 风格的格式化字符串。

  • warning(char *format, ...) ,在一个对话框中显示一条格式化消息。

  • char * askstr(int hist, char *default, char * format, ...) ,显示一个输入框,要求用户输入一个字符串值。 hist 参数指出如何写入输入框中的下拉历史记录列表,并它将设置为 kernwin.hpp 定义的一个 HIST_xxx 常量。 format 字符串和任何其他参数用于构成一个提示字符串(prompt string)。

  • 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) ,使反汇编窗口跳转到指定地址。

与 IDC 脚本相比,使用 API 能够实现更多的用户界面功能,包括创建自定义单列和多列的列表选择对话框。对这些功能感兴趣的读者可以参阅 kernwin.hpp 文件,特别是 choosechoose2 函数。

3. 操纵数据库名称

下面的函数可用于处理数据库中的已命名位置。

  • 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)。

4. 操纵函数

访问与经过反汇编的函数有关的信息的 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 对象的指针, func_t 对象代表数据库中的第n 个函数,这里的 n 介于零(包括)和 get_func_qty() (不包括)之间。

  • func_t *get_next_func(ea_t addr) ,返回一个指向 strnc_t 对象的指针, strnc_t 对象描述指定地址后面的下一个函数。

  • get_func_name(ea_t addr, char *name, size_t namesize) ,将包含指定地址的函数的名称复制到函数提供的名称缓冲区中。

  • struc_t *get_frame(ea_t addr) ,返回一个指向 struc_t 对象的指针, struc_t 对象描述包含指定地址的函数的栈帧。

5. 操纵结构体

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 对象的指针,该对象表示由给定类型 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 的新结构体附加到标准结构体列表中。该结构体还被添加到 Structures 窗口的给定 index 位置。如果 index 为 BADADDR ,则该结构体被添加到 Structures 窗口的结尾部分。

  • 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 参数指定新成员占用的字节数。

6. 操纵段

segment_t 类存储与数据库中不同段(如 .text.data )有关的信息,这些段可通过 View▶Open Subviews▶Segments 窗口查看。如前所述,各种可执行文件格式(如 PE 和 ELF )通常将 IDA 术语段称为节。下面的函数可用于访问 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 文件,获取预定义类的完整列表。如果使用分段地址( seg:offset ), startend 将被解释为偏移量而不是虚拟地址,这时, para 参数描述节的基址。如果没有使用分段地址,或者所有段以零为基数,则这个参数应设置为零。

  • 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 缓冲区中。注意,IDA 可能会对 name 进行过滤,使用一个哑字符(通常为 ida.cfg 中 SubstCha r 指定的一个下划线)替换其中的无效字符(在 ida.cfg 中没有指定为 NameChars 的字符)。

  • 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 )均是如此。这些类仅仅提供一种便捷的方法来访问一个基本数据库实体的属性。要创建、修改或删除具体的数据库对象,你必须使用适当的函数,以对数据库进行永久性更改。

7. 代码交叉引用

在 xref.hpp 中定义的许多函数和枚举常量(部分如下所示)可用于访问代码交叉引用信息。

  • get_first_cref_from(ea_t from) ,返回给定地址向其转交控制权的第一个位置。如果给定的地址没有引用其他地址,则返回 BADADDR (- 1)。

  • get_next_cref_from(ea_t from, ea_t current) ,如果 current 已经由前一个对 get_first_ cref_fromget_next_cref_from 的调用返回,则返回给定地址( from )向其转交控制权的下一个位置。如果没有其他交叉引用存在,则返回 BADADDR 。

  • get_first_cref_to(ea_t to) 返回向给定地址转交控制权的第一个位置。如果不存在对给定地址的引用,则返回 BADADDR (- 1)。

  • get_next_cref_to(ea_t to, ea_t current) ,如果 current 已经由前一个对 get_first_cref_toget_next_cref_to 的调用返回,则返回向给定地址( to )转交控制权的下一个位置。如果没有对给定地址的其他交叉引用,则返回 BADADDR 。

8. 数据交叉引用

访问数据交叉引用信息的函数(也在 xref.hpp 中声明)与用于访问代码交叉引用信息的函数非常类似。这些函数如下所示。

  • get_frist_dref_from(ea_t from) ,返回给定地址向其引用一个数据值的第一个位置。如果给定地址没有引用其他地址,则返回 BADADDR (-1)。

  • get_next_dref_from(ea_t from, ea_t current) ,如果 current 已经由前一个对 get_first_ dref_fromget_next_dref_from 的调用返回,则返回给定地址( from )向其引用一个数据值的下一个位置。如果没有其他交叉引用存在,则返回 BADADDR 。

  • get_first_dref_to(ea_t to) ,返回将给定地址作为数据引用的第一个位置。如果没有对给定地址的引用,则返回 BADADDR (-1)。

  • get_next_dref_to(ea_t to, ea_t current) ,如果 current 已经由前一个对 get_first_ dref_toget_next_dref_to 的调用返回,则返回将给定地址( to )作为数据引用的下一个位置。如果没有其他对给定位置的交叉引用,则返回 BADADDR 。

SDK 中没有与 IDC 的 XrefType 对应的函数。虽然 xref.hpp 文件声明了一个名为 lastXR 的变量,但 SDK 并不导出这个变量。如果你需要确定一个交叉引用的类型,你必须使用 xrefblk_t 结构体迭代交叉引用。我们将在下一节中讨论 xrefblk_t 结构体。

16.2.5 IDA API 迭代技巧

通常,使用 IDA API 能以几种不同的方式迭代数据库对象。在下面的例子中,我们将说明一些常用的迭代技巧。

1. 枚举函数

迭代数据库中函数的第一种技巧与使用 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);  
}

最后,我们可以采用一种较为低级的方法,利用一个由 area.hpp 文件定义的名为 areacb_t 的数据结构(也叫做 区域控制块 )。区域控制块用于维护相关的 area_t 对象的列表。一个名为 funcs 的全局 areacb_t 变量作为 IDA API 的一部分导出(在 funcs.hpp 文件中)。使用 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 指针。

2. 枚举结构体成员

在 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 ),确定该成员是一个局部变量还是一个参数。

3. 枚举交叉引用

在第 15 章中提到过,我们可以使用 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 并不能保证当前的交叉引用与一个函数调用有关。

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

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

发布评论

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