栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

IDA Pro权威指南读书笔记——数据类型和数据结构

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

IDA Pro权威指南读书笔记——数据类型和数据结构

目录

数据类型和数据结构

函数原型的设置:

识别数据结构的用法:

数组成员访问:

结构体成员访问:

创建 IDA 结构体:

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

编辑结构体成员

使用结构体模板:

导入新的结构体:

解析 C 结构体声明:

解析 C 头文件:

使用标准结构体:

IDA TIL 文件:

加载新的 TIL 文件:

共享 TIL 文件:

C++ 逆向工程基础:

this 指针:

虚函数和虚表:

对象生命周期:

名称改编:

运行时类型识别:

继承关系:


数据类型和数据结构

函数原型的设置:

如果一个变量作为 IDA 拥有原型的函数的参数,在分析阶段,IDA 会尽其所能推断出该变量的数据类型。如有可能,IDA 会对该变量使用一个从函数原型中提取出的正式名称,而不是为其生成默认的哑名。

举例:IDA 拥有原型的 connnect 函数的反汇编代码:

IDA 的函数原型传播与设置:

IDA 传播来自函数原型的类型信息的能力并不仅限于 IDA 类型库中包含的库函数。只要你明确设置函数的类型信息,IDA 就可以传播你的数据库中任何函数的正式参数名称和数据类型。

在初始分析阶段, 如果通过类型传播没有得出其他结论,IDA 会向所有函数参数分配哑名和一般类型 int。

可以使用 Edit 》Functions 》Set Function Type 命令或者在函数名称上右击鼠标并在上下文菜单中选择 Set Function Type (或使用热键 Y ) 来设置函数的类型。

设置函数原型举例:

附录:

将光标定位在 IDA 窗口中的任何名称上,IDA 将显示一个类似于工具提示条的弹出窗口,其中包含目标位置多过 10 行的反汇编代码。如果该名称为库函数名,窗口中通常包信用于调用该序数的原型。

识别数据结构的用法:

基本数据类型能与 CPU 寄存器或指令操作数的大小很自然地适应,但是复合数据类型 ( 如数组和结构体 ) 所包含的各数据项,则需要更加复杂的指令序列。

数组成员访问:

关于数组使用的重要总结:

只有当变量被用作数组的索引时,我们才最容易确定数组的存在。要访问数组中的元素,首先需要用索引乘以数组元素的大小,计算出相应元素的偏移量,然后将得到的偏移量与数组的基址相加, 得到数组元素的访问地址。

遗憾的是,如我们在下一节所述, 在使用常量索引值访问数组元素时,它们很少能够证明数组的存在,并且看起来与用于访问结构体成员的代码非常类似。

传统意义上的数组指包含同一数据类型的连续元素的连续内存块。用数组中元素的数量乘以每个元素的大小,即可直接计算出数组的大小。

使用 C 语句,以下数组:

所占用的最小字节的计算方法为:

各数组元素通过索引值进行访问,这个索引值可能是变量或常量,如下面这些数组引用所示:

1. 全局分配的数组

重要总结:

第一:

使用常量索引访问全局数组时,在对应的反汇编代码清单中,对应的数组元素将以全局变量的形式出现。换句话说,,反汇编代码清单基本上不提供任何数组存在的证据。

第二:

使用可变索引值将带领我们来到数组的开头,因为在计算要访问的数组元素的具体地址时,我们需要用数组的基址加上相应的偏移量,这时基址即呈现出来 (如四处所示 )。4 处的计算提供了另外一条有关数组的关键信息。通过观察与数组索引相乘的那个数 (这里为4),我们知道了数组中各元素的大小 (不是类型)

如果一个数组在程序的全局数据区内分配 ( 例如,在 .data 或 .bss 节 ),编译器在编译时可获知该数组的基址。由于基址固定,编译器可以计算出使用固定索引访问的任何数组元素的固定地址。

举例:下面同时使用固定偏移量和可变偏移量访问一个全局数组:

尽管这个程序只有一个全局变量,但 1 、2 和 3 处的反汇编行似乎表明,它使用了 3 个全局变量。3 处对偏移量的计算 (eax * 4 ) 暗示全局数组 dword_40B720 存在的唯一线索,不过数组的名称与 1 处的全局变量的名称相同。

多个全局变量的由来:

基于 IDA 分配的哑名, 我们知道,全局数组由从地址 0040B720 开始的 12 个字节组成。在编译过程中,编译器使用了固定索引 ( 0、1、2 ) 来计算数组中对应元素的具体地址 (0040B720、0040B724 和 0040B728 ),它们使用 1、2 和 3 处的全局变量来引用。

使用上一章讨论的 IDA 数组格式化操作 (Edit 》Array 命令),可将 dword_40B720 转换成一个三元素数组,从而得到下面的反汇编行。

如下所示,格式化数组后体现了数组中偏移量的使用:

2. 栈分配的数组

当数组作为栈局部变量分配时,由于编译器在编译时无法获得绝对地址,即使是使用常量索引的访问也必须在运行时进行计算。但是编译器几乎以完全相同的方式处理栈分配的数组和全局分配的数组。

举例:下面使用栈局部变量 ebp+ 的形式访问栈分配数组:

 

和全局数组例子类似,这个函数也使用了 3 个变量,但是是局部变量 ( var_10、var_5C 和 var_8 ),而不是一个包含 3 个整数的数组。

根据 1、2 和 3 处使用的常量可以得知它引用的是 stack_array 数组的 3 个元素,该数组的第一个元素位于 var_10 ( 内存地址最低的局部变量 ) 所在的位置。

多个栈局部变量的由来:

先看 stack_array[1],它在数组中的 4 字节位置,或者在 var_10 之后的 4 字节位置。在栈帧里,编译器选择分配 ebp-0x10 处的 stack_array。编译器知道, stack_array[1] 的地址为 ebp-0x10+4 (可简化为 ebp-0x0C )。

结果,IDA 将其作为局部变量引用显示。最终, 与全局分配的数组类似,使用常量索引值会隐藏有栈分配的数组存在这一事实。唯有 4 处的数组访问表明,var_10 是数组中的第一个元素,而不是一个简单的整数变量。此外,4 处的反汇编代码清单也有助于我们得出结论:数组中各元素的大小为 4 字节。

3. 堆分配的数组

堆分配的数组是使用一个动态内存分配函数(如 C 中的 malloc 或 C++ 中的 new ) 分配的。

从编译器的角度讲,处理堆分配的数组的主要区别在于, 它必须根据内存分配函数返回的地址值,所以对数组的所有引用都是基于这个地址的,特别的,该基址会不停赋值给其它常用寄存器来反复横跳。

举例:下面使用单个堆基址局部变量 + 实时计算的数字偏移来引用堆分配数组:

数组的起始地址 ( 由 EAX 寄存器中的 malloc 返回 ) 存储在局部变量 heap_array 中。

每一次访问数组时,首先必须读取 heap_array 的内容(对应蓝色箭头部分),以获得数组的基址,然后再在它上面加上一个偏移值,计算出数组中对应元素的地址。引用 heap_array[0]、heap_array[1] 和 heap_array[2] 需要的偏移量分别为 0、4 和 8 字节,如 1、2 和 4 处所示。

引用 heap_array[idx] 数字 4 处的操作与前面的例子最为相似, 它在数组中的偏移量通过将数组索引与数组元素大小相乘计算得出。

对堆分配的数组而言, 传递给内存分配函数的参数 (0x0C 在 5 处传递给了 malloc ) 即表示分配给数组的字节总数,用这个数除以元素大小(本例为 4 字节,如 1、2 和 3 处的偏移量所示 ),即可得到数组中元素的个数。上面的例子分配了一个包含 3 个元素的数组。

结构体成员访问:

与数组成员的区别:

区分的要点就是数组的 全局/局部 变量的间隔是一定的(同类型),而结构体之间的间隔通常是不一样的(不同类型且对齐)!!所以判断时先判断是不是数组,在根据间隔判断是不是结构体。

结构体中的数据字段是通过名称访问,而不是像数组那样通过索引访问,但是字段名称被编译器转换成了数字偏移量。结果在反汇编代码清单中,访问结构体字段的方式看起来与使用常量索引访问数组元素的方式极其相似。

下面的结构体定义将用在随后的例子中:

分配结构体所需的最小空间,由分配结构体中的字段所需的空间总和决定。但是默认情况下, 编译器会设法将结构体字段与内存地址对齐,以最有效地读取和写入这些字段。

例如,4 字节的整数字段将与能够被 4 整除的偏移量对齐,而 8 字节的双字则与能够被 8 整除的偏移量对齐。造成的结果是结构体实际大小大于字段大小的总和。

通过使用编译器选项来要求特定的成员对齐,可将结构体压缩到最小空间。Microsoft Visual C/C++ 和 GNU gcc/g++ 都将 pack 杂注 (pragma ) 视为控制结构体字段对齐的一种方法。

同时,GNU 编译器还使用 packed 属性来控制结构体对齐 ( 在每个结构体的基础上 )。 要求结构体字段进行 1 字节对齐(对应粉色箭头),编译器会将结构体压缩到最小空间。就我们例子中的结构体而言,这样做将得到 Minimum offset 一列中的偏移量和结构体大小。值得注意的是, 如果以这种方式对齐数据,一些 CPU 的性能更加优良; 但是,如果有些边界上的数据并未对齐,CPU 可能会产生异常。

1.全局分配的结构体

举例:下面同时使用固定偏移量和可变偏移量访问一个全局分配结构体:(与全局分配数组间隔不同)

2. 栈分配的结构体

举例:下面使用栈局部变量 ebp+ 的形式访问栈分配数组:(与栈分配数组间隔不同)

3. 堆分配的结构体

举例:下面使用单个堆基址局部变量 + 实时计算的数字偏移来引用堆分配数组:(与堆分配数组间隔不一样)

如果要求结构体进行 1 字节对齐,对其进行压缩。则该程序的反汇编代码清单为:

4. 结构体数组

在处理嵌套结构体时,偏移量就相当于二维数组一样一次偏移一个维度大小。

举例 heap_struct 指向一个包含 5 个 ch8_struct 元素的结构体数组:

访问 1 处的 field1 所需的操作包括:

用索引值乘以数组元素的大小 (这里为结构体的大小 ),然后加上 field1 这个字段的偏移量。从反汇编代码中,我们可以推断出 数组/结构体 的大小( 24 ),数组/结构体 中元素的个数 ( 120/24=5 )。

创建 IDA 结构体:

当我们发现一个程序正操纵某种数据结构时,除了全局分配我们可以在 .data 或 .bss 节中用热键 a 手动生成数组来使反汇编代码呈现 "基地址+偏移量" 外,对于栈分配和堆分配的 "数组/结构体" 我们只能用应用结构体的方法使其呈现 "基地址+字段名" 来更具可读性。

如: [ edx+ch8_struct.field5 ] 替换 [edx+10h] 

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

Structures 窗口用于创建新的结构体,其中列出任何它能够识别、并确定已被一个程序使用的结构体:

Structures 窗口的前 4 行(蓝色箭头所指处)文本用于提醒用户该窗口中可能进行的操作:添加、删除和编辑结构体。

热键 INSERT 添加时打开 Create structure/union ( 创建结构体/联合 ) 对话框:

指定结构体的名称并单击 OK 按钮后, IDA 将在 Stuctures 窗口中创建一个空结构体定义:

编辑结构体成员

为了给新结构体添加字段,你必须利用字段创建命令 D、A 和数字键盘上的星号键 (*)。 最初你只需要使用 D 命令。不过它的行为非常依赖于光标的位置。

我们建议采用下面的步骤给结构体添加字段:

(1):要给结构体添加新字段,将光标放在结构体定义的最后一行 (包含 ends 的那一行 ) 并按下了 D 键.这时,IDA 就会在结构体的末尾添加一个新字段。新字段的大小取决于你在数据转盘 (参见第 7 章 ) 上选择的第一个大小。最初,字段的名称为 field_N,这里的 N 为结构体开头到新字段 (如 field_0) 开头的数字偏移量。

(2):如果需要修改字段的大小,首先将光标放在新字段的名称上,然后重复按下 D 键,使数据转盘上的数据类型开始循环,从而为新字段选择正确的数据大小。另外,你还可以使用 Options 》Setup Data Types来指定一个在数据转盘上不存在的大小。如果新字段是一个数组,右击其名称并在上下文菜单中选择 Aray,将打开 “数组规范” 对话框 (参见第 7 章)

(3):要更改一个结构体字段的名称,单击字段名称并按下 N 键,或者右击该名称并在上下文菜单中选择 ReName,然后在输入框中输入一个名称即可。

在你定义自己的结构体时,下面的提示可能会有所帮助:(这只是辅助提示而已)

口 一个字段的字节偏移量以一个 8 位十六进制值在 Stuctures 窗口的左侧显示。

口 每次你添加或删除一个结构体字段,或更改一个现有字段的大小时,结构体的新大小都会在结构体定义的第一行反映出来。

口 你可以给一个结构体字段添加注释,就像给任何反汇编行添加注释一样。右击(或使用热键) 你希望为其添加注释的字段,在上下文菜单中选择一个注释选项即可。

口 与 Stuctures 窗口顶部的说明不同的是,只有当一个字段是结构体中的最后一个字段时,使用 U 键才能删除该字段。对于所有其他字段,按下 U 键将取消该字段的定义,这样做仅仅删除了该字段的名称,并没有删除分配给该字段的字节。

口 你必须对一个结构体定义中的所有字段进行适当的对齐。IDA 并不区分已压缩和未压缩的结构体。为将字段适当对齐,如果你需要填补字节,那么你必须负责添加这些字节。

填补字节最好作为适当大小的哑字段添加。在添加额外的字段后,你可以选择取消或保留这些字段的定义。

口 分配到结构体中间的字节只有在取消关联段的定义后才能删除,使用 Edit 》 Shrink Struct Type ( 缩小结构体类型 ) 即可删除被取消定义的字节。

口 你也可以在结构体的中间添加新的字节: 选择新字节后面的一个字段,然后使用 Edit 》Expand Struct Type ( 扩大结构体类型 ) 在选中的字段前插入一定数量的字节。

口 如果知道结构体的大小,而不了解它的布局,你需要创建两个字段。第一个字段为一个数组,它的大小为结构体的大小减去 1 个字节 (size - 1); 第二个字段应为 1 个字节。创建第二个字段后,取消第一个数组字段的定义。

这样,结构体的大小被保留下来 ,随后,当你进一步了解该结构体的布局后,你可以回过头来定义它的字段及其大小。

通过上面不走我们创建 ch8_struct 结构体(未压缩版本,使用无命名的填补字段对齐):

使用结构体模板:

有两种应用结构体模板方法可对反汇编代码清单中的结构体定义加以利用。

第一种:用已定义的结构体重新格式化反汇编代码中内存引用:

举例,单字段引用:

右击 Size,即可在上下文菜单中看到 Structure offset ( 结构体偏移) 选项,它提供多种形式对指令操作数进行格式化。这些形式全部是从包含一个偏移量为 16 的字段的结构体集合中提取出来的。

(就是这几个结构体都有16 偏移量的字段,由于自己只右击了一个字段,所以能匹配很多局部偏移字段)

举例:整体引用:

第二种:提供其他可应用于栈和全局变量的数据类型:

我们可以将栈和全局变量格式化成整个结构体,事实上这相当于前面的整体引用。

双击对应栈局部变量,在栈帧视图中用 Edit 》Struct Var ( ALT+Q ) 命令挑选对应的结构体覆盖引用即可:

未格式化前的反汇编代码:

格式化后的反汇编代码:

将全局变量格式化成结构体的过程与格式化栈变量所使用的过程几乎完全相同:

要进行格式化,选择要格式化的变量,或者表示结构体开头部分的地址,再使用 Edit > Struct Var (ALT+Q ) 选择合适的结构体类型即可。

导入新的结构体:

IDA 能够解析 C ( 而非 C++ ) 数据声明 ,以及整个个 C 头文件,并自动为在这些声明或头文件中定义的结构体创建对应的 IDA 结构体。如果你碰巧拥有你正进行逆向工程的二进制文件的部分源代码,或者至少是头文件,那么,你就可以让 IDA 直接从源代码中提取出相关结构体,从而节省大量时间。

解析 C 结构体声明:

使用 View 》 Open Subviews 》 Local Types ( 查看 》 打开子窗口 》 本地类型 ) 命令,可以打开 Local Types 子窗口,其中列出了所有解析到当前数据库中的类型。

通过 INSERT 键能为“Local Types” 窗口添加新的类型:

解析新类型时发生的错误将在 IDA 的输出窗口中显示。如果类型声明被成功解析,"Local Types” 窗口将列出该类型及其相关声明。

请注意:

IDA 解析器使用 4 字节的默认结构体成员对齐方式。如果你的结构体需要其他对齐方式,你可以包括该对齐方式,IDA 认可使用 pragma pack 指令来指定所需的结构体成员对齐方式。

添加到 Local Types (本地类型 ) 窗口中的数据类型不会立即在 Structures ( 结构体 ) 窗口中出现,有两种方法可以将本地类型声明添加到 Structures 窗口中。最简单的方法是在相关本地类型上单击鼠标右键,并选择 Synchronize to idb。

解析 C 头文件:

要解析头文件,可以使用 File 》Load File 》Parse C Header File (文件》加载文件》解析 C 头文件) 选择你想要解析的头文件。如果一切正常,IDA 会通知你 Compilation successful (编译完成) 如果解析器遇到任何问题,IDA 将会在输出窗口中显示错误消息。

IDA 会将所有被成功解析的结构体添加到当前数据库的标准结构体列表中 (具体地说, 是列表的未尾)。如果新结构体的名称与现有结构体的名称相同,IDA 会用新结构体布局覆盖原有结构体定义。除非你明确选择添加新的结构体,否则

新结构体不会出现在 Stuctures 窗口中。

在解析 C 头文件时,记住以下要点会有所帮助:

口 虽然内置解析器确实遵循 pack 杂注,但它不需要和你的编译器一样,默认对结构体成员进行对齐。默认情况下,解析器会建立 4 字节对齐的结构体。

口 解析器理解 C 预处理器 include 指令。为解析 include 指令,解析器会搜索包含被解析文件的目录(这里应该是自己 IDA 安装的目录),以及 Options 》Compiler (选项 》编译器) 配置对话框中的任何 include 目录 (包含)

口 解析器只能理解 C 标准数据类型。但是,解析器还能够理解预处理器 define 指令和 C typedef 语句。因此,如果解析器之前遇到过适当的 typedef,它将能够正确解析 unit32_t 之类的类型。

口 如果你没有源代码,那么你会发现,使用文本编辑器以 C 表示法迅速定义一个结构体布局,并解析得到的头文件或把声明粘贴为一个新的本地类型,会比使用 IDA 烦琐的手动结构体定义工具更加方便。

(下面有一个要点是在本地类型中添加结构体定义可以格式化整个汇编代码中符合的部分,而在结构体列表中添加则需要频繁地手动去匹配符合的部分)

一般而言, 要最大限度地提高成功解析一个头文件的几率,你需要使用标准 C 数据类型, 并尽可能地减少使用 include 文件,从而最大程度地简化结构体定义。记住, 在 IDA 中创建结构体时,正确布局最为重要。正确的布局更多地取决于每个字段的正确大小和结构体的正确对齐,而不只是对每个字段都使用正确的类型。比如你需要用 int 替换所有的 unit32_t,以更加正确解析一个文件。

使用标准结构体:

最初创建一个数据库时,IDA 会尝试确定与二进制文件有关的编译器和平台,并加载适当的结构体模板放入到标准结构体中。

当 IDA 在反汇编代码清单中操纵结构体时,它会在 Structures 窗口中添加相应的结构体定义。因此 Structures 窗口中显示的是应用于当前二进制文件的已知结构体的子集。

添加一个标准结构体,在 Structures 窗口中按下 INSERT 键,选择 Add standard structure (添加标准结构体) 按钮:

单击这个按钮,IDA 将显示与当前编译器 ( 在分析阶段检测出来 ) 和文件格式有关的结构体主列表。这个结构体主列表中还包含通过解析 C 头文件添加到数据库中的结构体。

选择结构体对话框如下图所示, 该对话框用于选择添加到 Structures 窗口中的结构体:

举例:使用标准结构体分析 PE 文件头:(默认情况下文件头不会立即加载到数据库中)

在最初创建数据库时选择 Manual load ( 手动加载 ) 选项,可以将文件头加载到数据库中:

得到类似于下面的未格式化的反汇编代码清单:(因为执行代码处不对文件头数据引用,所以并没有自动被格式化)

使用前面添加标准的 IMAGE_DOS_HEADER 结构体来格式化这段代码:

即得到下面的格式化代码:

IDA 会用结构体定义的字段名称来对每个对应的字段进行注释,下面是展开后的图像:

借助 DOS 头的 e_lfanew 字段值,同理格式化 IMAGE_NT_HEADER 部分的结构体,如下所示:

IDA TIL 文件:

IDA 中的所有数据类型和函数原型信息都存储在 TIL 文件中。IDA 拥有存储在 /til 目录(这个应该是 IDA 的安装目录)中的许多主要编译器和 API 的类型库信息。Types 窗口 (View 》Open subview 》Type Libraries)

列出了当前加载的 .til 文件,并可用于加载你想要使用的其他 .til 文件。IDA 将根据在分析阶段发现的二进制文件属性,自动加载类型库。正常情况下,多数用户不需要直接处理 .til 文件。

加载新的 TIL 文件:

有时候,IDA 可能无法检测到用于构建某个二进制文件的特殊编译器, 这可能是由于该二进制文件经过某种形式的模糊处理所致。这时,你可能需要在 Types 窗口中按下 INSERT键,并选择你想要加载的 .til 文件。加载一个新的 .til 文件后,该文件包含的所有结构体定义都被添加到标准结构体列表中,其类型信息也被应用于二进制文件中的某些函数,这些函数可在新加载的 .til 文件中找到匹配的原型。换句话说一旦 IDA 获知与某个函数有关的新信息,它就会自动应用这些新信息。

共享 TIL 文件:

IDA 还利用 .til 文件存储你在 Structures 窗口中手动创建(就是扔入 IDA 时多出来的那几个关联文件)的或者通过解析 C 头文件获得的任何自定义结构体定义。这些结构体存储在一个与创建它们的数据库有关的专用 .til 文件中。该文件的名称与其相关数据库的名称相同,扩展名为 .til。

例如,如果数据库名为 some_file.idb,则相应的类型库文件则为 some_file.til。在正常情况下,你根本不会看到这个文件,除非你碰巧在 IDA 中打开了上述数据库。前面我们提到过,.idb 文件实际上是一个归档文件 (类似于.tar文件),用

于保存不使用的数据库组件。打开一个数据库时,其组件文件 ( .til 文件为其中之一 ) 将被提取出来,成为 IDA 中的运行文件。

在数据库之间共享 .til 文件:(有两种共享方法)

第一种方法有些不太正规,即将 .til 文件由打开的数据库复制到另一个目录中,然后再通过 Types 窗口,在任何其他数据库中打开这个.til 文件。

第二种是一种正式的方法,即从一个数据库中提取出自定义类型信息,生成一段 IDC 脚本,用于在任何其他数据库中重建自定义结构体。使用 File 》Produce File 》Dump Type.into to IDC File (文件 》生成文件 》转储类型信息到 IDC 文件 ) 命令可生成该脚本。

但是,与第一种方法不同的是,这种方法只能转储 Structures 窗口中列出的结构体,但并不转储通过解析 C 头文件得到的结构体 (而复制 .til 文件却可以转储这类结构体)。

Hex-Rays 还提供一个名为 tilib 的独立工具,用于在 IDA 以外创建 .til文件。注册用户可以通过 Hex-RaysIDA 下载页面下载该实用工具的 .zip 文件。要安装这个工具,只需要将 zip 文件解压到 目录中即可。tilib 实用工具可用于列举现有 .til 文件的内容,或通过解析 C (而不是 C++ ) 头文件来创建新的 .til 文件。

下面的命令将列举 Visual Studio 6 (对应下图蓝色箭头部分)类型库的内容:

创建新的 .til 文件包括命名要解析的头文件及要创建的文件。你可以使用命令行选项来指定其他包含文件目录或之前解析的世文件,以解析头文件中包含的任何依赖关系。

下面的命令将创建一个包含 ch8_struct 声明的新 .til 文件,并且必须移至 /til 目录才能供 IDA 使用:

tilib 实用工具包含大量其他功能,tilib 工具随附的 README 文件详细介绍了其中一些功能,通过运行不带参数的 tilib 命令可简单了解其他功能。在版本 6.1 之前,tilib 仅提供 Windows 可执行文件,但是它生成的 .til 文件可与所有版本的 IDA 兼容。

C++ 逆向工程基础:

C++ 类是 C 结构体面向对象的扩展,因此,在结束数据结构的讨论时,我们有必要介绍一下已编译的 C++ 代码的各种特性。在这里,我们仅仅涉及几个重要问题,以及微软的 Visual C++ 和 GNU 的 g++ 之间的一些差异。

this 指针:

所有非静态 C++ 成员函数都使用 this 指针(静态就是直接 :: 来引用了)。任何时候调用这样一个函数 this 都被初始化,指向用于调用该函数的对象。

以下面的函数调用为例:

在 3 次调用 menber_func 的过程中,this 分别接受了&object1、&object2 和 p_obj 这 3 个值。我们最好是把 this 看成是传递到所有非静态成员函数的第一个隐藏参数。如第 6 章所述,Microsof Visual C++ 利用 thiscall 调用约定,并将 this 传递到 ECX 寄存器中。GNU g++ 编译器则把 this 看做是非静态成员函数的第一个 (最左边) 参数,并在调用该函数之前将用于调用本数的对象的地址作为最后一项压入栈中。

(传递优先级不同,这就是第一个区别)

(thiscall:

仅仅应用于 "C++" 成员函数。this 指针存放于 CX 寄存器,参数从右到左压。thiscall 不是关键词,因此不能被程序员指定。只不过前两个参数右寄存器 ecx 和 edx 传参(就是 rcx,rdx,r8,r9 传参))

从逆向工程的角度看,在调用函数之前,将一个地址转移到 ECX 寄存器中可能意味着两件事情。首先,该文件使用 Visual C++ 编译; 其次,该函数是一个成员函数。如果同一个地址被传递给两个或更多函数,我们可以得到结论,这些函数全都属于同一个类层次结构。

在一个函数中,在初始化之前使用 ECX 意味着调用方必定已经初始化了 ECX,并且该函数可能是一个成员函数 (虽然该函数可能只是使用了 fastcall 调用约定 )。另外,如果发现一个函数向其他函数传递 this 指针,则这些函数可能和传递 this 的函数属于同一个类。但是,如果一个函数没有把指针作为它的第一个参数,则它肯定不属于成员函数。

(fastcall:

它是通过寄存器来传送参数的(实际上,它用 ECX 和 EDX 传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈)。)

虚函数和虚表:

虚函数用于在 C++ 程序中实现多态行为。编译器会为每一个包含虚函数的类 ( 或通过继承得到的子类 ) 生成一个表,其中包含指向类中每一个虚函数的指针。这样的表就叫做虚表 ( vtable )。

此外,每个包含虚函数的类都获得另外一个数据成员,用于在运行时指向适当的虚表。这个成员通常叫做虚表指针 ( vtable pointer ),并且是类中的第一个数据成员。在运行时创建对象时,对象的虚表指针将设置为指向合适的虚表。如果该对象调用一个虚函数,则通过在该对象的虚表中进行查询来选择正确的函数。因此,虚表是在运行时解析虚函数调用的基本机制。(类-->虚表指针-->指向虚表)

以下面的 C++ 类定义为例说明虚表的作用:

在这个例子中,SubClass 是 baseClass 的一个子类:

BaseClass 由 4 个虚函数组成,而 SubClass 则包含 5 个虚函数 (BaseClass 中的 4 个函数加上一个新函数 vfunc5 )。在 BaseClass 中,其声明使用了 =0,说明 vfunc1 是一个纯虚函数。纯虚函数在它们的声明类中没有实现,并且必须在一个子类被视为具体类之前,在这个子类中被重写。换言之,没有名为 BaseClass::vfunc1 的函数,直到一个子类提供一次实现,也没有对象能够被实例化。SubClass 提供了这样一个实现,因此可以创建 SubClass 的对象。(就是一定要有一次实现才能被创建对象)

初看起来,BaseClass 似乎包含 2 个数据成员,而 SubClass 则包含 3 个成员。但是,我们前面提到,任何包含虚函数 ( 无论是本身包含还是继承得来 ) 的类也包含一个虚表指针。因此,BaseClass 类型的实例化对象实际上有 3 个数据成员,而 SubClass 类型的实例化对象则有 4 个数据成员,且它们的第一个数据成员都是虚表指针。在类 SubClass 中,虚表指针实际上由类 BaseClass 继承得来,而不是专门为类 SubClass 引入。

图 8-14 是一个简化后的内存布局:

它动态分配了一个 SubClass 类型的对象。在创建对象的过程中,编译器确保新对象的虚表指针指向正确的虚表 (本例中为类 SubClass 的虚表)。

值得注意的是,SubClass 中包含两个指向属于 BaseClass 的函数 ( BaseClass::vfunc2 和 Baseclass::vfunc4 ) 的指针。这是因为 SubClass 并没有重写任何一个函数,而是由 BaseClass 继承得到这些函数。图中还显示了纯虚函数的典型处理方法。由于没有针对纯虚函数 BaseClass: :vfunc1 的实现,因此,在 BaseClass 的虚表中并没有存储 vfunc1 (蓝色箭头指向处)的地址。这时,编译器会插入一个错误处理函数的地址,通常,该函数名为 purecall。理论上,这个函数绝不会被调用,但万一被调用,它会令程序终止。

使用虚表指针导致的一个后果是,在操纵 IDA 中的类时,你必须考虑到虚表指针。前面我们讲过,C++ 类是 C 结构体的一种扩展。因此,我可以利用 IDA 的结构体定义来定义 C++ 类的布局。对于包含虚函数的类,你必须将一个虚表指针作为类中的第一个字段。在计算对象的总大小时,也必须考虑到虚标指针。这种情况在使用 new 操作符动态分配对象时最为明显,这时,传递给 new 的大小值不仅包括类 ( 以及任何超类 ) 中的所有显式声明的字段占用的空间,而且包括虚表指针所需的任何空间。

举例:下面的例子动态创建了 SubClass 的一个对象:

它的地址保存在 BaseClass 的一个指针中。 然后,这个指针被传递给一个函数 ( call_vfunc ),它使用该指针来调用 vfunc3。

由于 vfunc3 是一个虚函数,因此在这个例子中,编译器必须确保调用的是 SubClass::vfunc3,因为指针指向一个 SubClass 对象。

下面 call_vfunc 的反汇编版本说明了如何解析虚函数调用:

在 1 处,虚表指针从结构体中读取出来,保存在 EDX 寄存器中。由于参数 b 指向一个SubClass 对象,这里也将是 SubClass 的虚表的地址。在 2 处,虚表被编入索引,将第三个指针 (在本例中为 Subclass::vfunc3 的地址 ) 读入 EAX 寄存器。最后,在 3 处调用虚函数。

值得注意的是,2 处的虚表索引操作非常类似于结构体引用操作。所以我们用前面所学来定义一个结构体来表示一个类的虚表的布局,然后利用这个已定义的结构体来提高反汇编代码清单的可读性。

定义的结构体如下:

这个结构体允许将虚表引用操作重新格式化成以下形式:

对象生命周期:

了解对象的构建和撤销机制,有助于明确对象的层次结构和嵌套对象关系,并有助于迅速确定类构造函数和析构函数。(谁是谁的基类,谁有时谁的嵌套)

构造函数的调用细节:

对全局和静态分配的对象来说,构造函数在程序启动并进入 main 函数之前被调用。栈分配(栈分配就是局部变量)的对象的构造函数在对象进入声明对象的函数作用域中时被调用。

许多时候, 对象一进入声明它的函数,它的构造函数就被调用。但是如果对象在一个块语句中声明,那么它的构造函数直到这个块被输入时才被调用 (如果它确实被输入的话)。

如果对象在程序堆中动态分配,则创建对象分为两个步骤:

第一步:调用 new 操作符分配对象的内存。

第二步:调用构造本数来初始化对象。

微软的 Visual C++ 与 GNU 的 g++ 的主要区别在于, Visual C++ 可确保在调用构造函数之前,new 的结果不为空值 (null)

执行一个构造函数时,将会发生以下操作:

(1)如果类拥有一个超类,则调用超类的构造函数。

(2) 如果类包含任何虚函数,则初始化虚表指针,使其指向类的虚表。注意,这样做可能会覆盖一个在超类中初始化的虚表指针,这实际上是希望的结果。

(3) 如果类拥有本身就是对象的数据成员,则调用这些数据成员的构造函数。(这里可以简单地理解为嵌套类)

(4) 最后,执行特定于代码的构造函数。这些是程序员指定的、表示函数 C++ 行为的代码。

构造函数并未指定返回类型,但由 Microsoft Visual C++ 生成的构造函数实际上返回到 EAX 寄存器中的 this 指针。无论如何,这是一个 Visual C++实现细节,并不允许 C++ 程序员访问返回值。

析构函数基本上按相反的顺序调用:

对于全局和静态对象,析构函数由在 main 函数结束后执行的清理代码调用。栈分配的对象的析构函数在对象脱离作用域时被调用。堆分配的对象的析构函数在分配给对象的内存释放之前通过 delete 操作符调用。

析构函数执行的操作与构造函数执行的操作大致相同,唯一不同的是,它以大概相反的顺序执行这些操作:

(1) 如果类拥有任何虚函数,则还原对象的虚表指针,使其指向相关类的虚表。如果一个子类在创建过程中覆盖了虚表指针,就需要这样做。

(2) 执行程序员为析构函数指定的代码。

(3) 如果类拥有本身就是对象的数据成员,则执行这些成员的析构函数。(这里可以简单地理解为嵌套类)

(4) 最后,如果对象拥有一个超类,则调用超类的析构函数。

通过了解超类的构造函数和析构函数何时被调用,我们可以通过相关超类函数的调用链,跟踪一个对象的继承体系。有关虚表的最后一个问题涉及它们在程序中如何被引用。

一个类的虚表被直接引用,只存在两种情况:

在该类的构造函数中引用和在析构函数中引用。

定位一个虚表后,你可以利用 IDA 的数据交叉引用功能 ( 参见第 9 章 ) 迅速定位相关类的所有构造函数和析构函数。

名称改编:

名称改编也叫做名称修饰 (name decoration ),是 C++ 编译器用于区分重载函数的机制。为了给重载函数生成唯一的名称,编译器用其他字符来修饰函数名称,用来编码关于函数的各种信息。编码后的信息通常描述函数的返回类型、函数所属的类、调用该函数所需的参数序列 (类型和有顺序 )。

名称改编是 C++ 程序的一个编译器实现细节,其本身并不属于 C++ 语言规范。因此,毫不奇怪,编译器供应商已经开发出他们自己的、通常并不互相兼容的名称改编约定。

幸好,IDA 理解 Microsof Visual C++、GNU g++ 以及其他一些编译器使用的名称改编约定。默认情况下,在程序中过到一个改编名称时, IDA 会在反汇编代码清单中该名称出现的位置以注释的形式显示该名称的原始名称。

使用 Options 》Demangied Names 打开如下图示的对话框,可以选择 IDA 的名称取消改编选项:

对话框中有 3 个主要选项,用于控制是否以注释的形式显示取消改编的名称 (demangled name ),是否对名称本身进行取消改编,或者根本不执行取消改编。(取消改编就是以原函数名的形式显示,而不是以编译器改编后的形式显示)

以注释的形式显示改编名称可得到以下代码:

同样,以名称显示改编名称得到以下代码:

其中,1 表示一个函数的反汇编代码清单的第一行,2 表示调用该函数。

Assume GCC v3x (采用 GCC v3.x 名称 ) 复选框用于区分 g++ 2.9x 版本与 g++ 3.x 及更高版本使用的改编方案。 在正常情况下, IDA 应自动检测 g++ 编译代码中使用的命名约定。Setup short names (设置短名称) 和 Setup long names (设置长名称 ) 按钮为取消改编名称的格式化提供了细化控制,其中包括大量选项,相关信息请查询 IDA 的帮助系统。

因为改编名称能提供大量与函数签名有关的信息,它们减少了 IDA 为理解传递给函数的参数的数量和类型所需的时间。如果一个二进制文件使用了改编名称, IDA 的取消改编功能会立即揭示所有名称被改编的函数的参数类型和返回类型。相反, 如果函数并未使用改编名称,你必须花费大量时间,对进出函数的数据流进行分析,从而确定函数的签名。

(前面说过改编名称编码后的信息能描述函数的返回类型、所属的类、调用该函数所需的参数序列(类型和顺序))

运行时类型识别:

C++ 提供各种操作符,可进行运行时检测,以确定 (typeid) 和检查 (dynamic_cast ) 一个对象的数据类型。为实现这些操作,C++ 编译器必须将类型信息嵌入到一个程序的二进制文件中,并执行能够百分之百确定一个多态对象的类型的过程 ,而不管为访问该对象被取消引用的指针类型。

然而,与名称改编一样,RTTI ( Runtime Type Identification,运行时类型识别 )也是一个编译器实现细节,而不是一个语言问题,因此,编译器没有标准的方法来实现 RTTI 功能。

我们将简要介绍 Microsoft Visual C++ 与 GNU g++ 的 RTTI 实现之间的异同。具体来说,我们介绍的内容仅仅涉及如何定位 RTTI 信息,并以此为基础,了解与这些信息有关的类名称。

以下面这个利用多态的简单程序为例:

print_type 函数必须正确打印指针 p 所指向的对象的类型。在这个例子中,基于 main 函数创建了一个 concrete_class 对象这个事实, 我们立即意识到,concrete_class 必须被打印。这里我们需要回答的问题是, print_type,更具体的说是 typeid,如何知道 p 指向的对象的类型?

问题的答案非常简单。因为每个多态对象都包含一个指向虚表的指针,编译器将类的类型信息与类虚表存储在一起。具体来说,编译器在类虚表之前放置一个指针,这个指针指向一个结构体,其中包含用于确定拥有虚表的类的名称所需的信息。

在 g++代码中,这个指针指向一个 type_info 结构体,其中包含一个指向类名称的指针。在 Visual C++ 代码中,指针指向一个微软 RTTICompleteObjectLocator 结构体,其中又包含一个指向 TypeDescriptor 结构体的指针。

TypeDescriptor 结构体中则包含一个指定多态类名称的字符数组。

需要注意的是,只有使用 typeid 或 dynamic_cast 操作符的 C++ 程序才需要 RTTI 信息。多数编译器都提供一些选项,禁止不需要 RTTI 的二进制文件生成 RTTI。因此,如果 RTTI 信息碰巧丢失,你不应感到奇怪。

继承关系:

如果深入 RTTI 实现,你会发现,你可以弄清继承关系。但是,要了解继承关系,你必须理解编译器的特殊 RTTI 实现。而且,如果一个程序不使用 typeid 或 dynamic_cast 运算符,RTTI 信息可能并不存在。

缺少 RITI 信息,又该使用什么技巧来确定 C++ 类中的继承关系呢?

确定某个继承体系的最简单方法是:

观察在创建对象时被调用的超类构造函数的调用链。(这是第一种方法)

内联构造函数是这种方法成功与否的唯一最大障碍。如果使用了内联构造函数,我们就不可能知道一个超类构造函数实际上已经被调用。

分析和比较虚表是另一种用于确定继承关系的方法:

例如,如图 8-14 所示(就是 134 页的 BaseClass::vfunc2 和 BaseClass::vfun4 这两个没有被重写的函数),在比较虚表的过程中,我们注意到,SubClass 的虚表中包含两个相同的指针,它们也出现在类 BaseClass 的虚表中。为此,我们可以轻易得出结论,BaseClass 与 SubClass 之间必定存在着某种关系,

但关于谁是谁的子类,我们可以应用下面的一条或多条指导原则来判断:

口 如果两个虚表包含相同数量的条目,则与这两个虚表对应的类之问可能存在着某种继承关系。

口 如果类 X 的虚表包含的条目比类 Y 多,则 X 可能是的子类。(因为子类可以多建成员变量和成员函数)

口 如果 X 包含的条目也可以在 Y 的虚表中找到, 则必定存在下面一种关系; X 是 Y 的子类 Y 是 X 的子类,或者 X 和 Y 全都是同一个超类 Z 的子类。

口 如果 X 包含的条目也可以在类 Y 的虚表中找到,并且 X 的虚表中至少包含一个纯调用条目,而 Y 的虚表中并没有这个条目,那么 Y 是 X 的子类。

虽然上面罗列的并不全面,但是我们可以使用这些指导原则来推断图 8-14 中 BaseClass 与 SubClass 之间的关系。上面的后 3 条原则全都适用于这个例子。

仅仅根据对虚表的分析, 由最后一条原则可得出结论:

SubClass 是 BaseClass 的子类。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/921085.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号