2017.11.06 15:36* 站在巨人的肩膀上——IDA PRO权威指南阅读笔记一,窗口view->open subviews 打开/关闭各种窗口 IDA文本视图(反汇编视图)地址显示格式 区域名称:虚拟地址 箭头显示格式 虚线条件跳转,实现非条件跳转 若加粗则 arg_X 函数栈帧 ;CODE XREF:_XX 交叉引用 Names窗口D:数据 A:字符串 F:常规函数,非库函数 I:共享库导入的函数名称 L:库函数 未知名称: sub_XXXXX 地址XXXX的子例程 loc_XXXXX 地址XXXX的一个指令 byte/word/dword_XXXXX 地址XXXX的8/16/32位数据 unk_XXXXX 地址XXXX的未知数据 底部消息窗口IDA开发脚本插件的输出窗口 Strings窗口从二进制文件中提取的字符串 右键->setup可配置 导出/入窗口导出窗口列出文件的入口点 导入窗口列出由被分析的二进制文件导入的所有函数 函数窗口函数名称,区域,起始位置,长度,描述函数的标记 <返回调用方(R),使用ebp(B)寄存器引用局部变量> 结构体窗口分析数据结构,双击数据结构名称展开,查看详细布局 枚举窗口 enums 可列举,定义枚举类型 段窗口 segmentation段的简单列表 签名窗口右键->应用新签名 类型库窗口,函数调用窗口(先选中函数),问题窗口等 二,反汇编导航双击导航最基本的为双击导航,任意可双击的地址均能跳转到目标 跳转快捷键 G 或Jump->Jump to Address跳转到名称或十六进制地址 导航栏前进 后退按钮实现跳转 栈帧
栈帧是程序运行时栈中分配的内存块,用于特定函数的调用 调用函数: 1.调用方将被调用函数所需的参数放入调用约定指定的位置。 2.调用方将控制权转交给被调用函数,即x86 CALL 与 MIPS JAL等指令执行。返回地址被保存到程序栈或CPU寄存器中。 3.有时,被调用的函数会配置一个栈指针,并保存调用方希望保持不变的寄存器的值。 4.被调用的函数为它可能需要的任何局部变量分配空间,一般通过调整程序栈指针在运行时栈上保留空间来完成这一任务。 5.被调用的函数可能访问调用函数传递给它的参数,可能生成一个结果。如果返回一个结果,此结果通常放置在一个特定的寄存器中,或放置到函数返回后调用方可立即访问的寄存器中。 6.函数完成操作后,任何为函数保留的栈空间将被释放。通常,逆向执行4步骤即可实现。 7.如果某个寄存器的值还为调用方保存(即3中情况),那么恢复原始值。这包括恢复调用方的栈指针寄存器。 8.被调用函数将控制权返还给调用方。主要指令x86 RET 和 MIPS JR 。根据调用约定,还可能会从程序栈中清除一个或多个参数。 9.调用方一旦重新获得控制权,它可能需要删除程序栈中的参数。这是可能需要对栈进行调整,将程序栈指针恢复到1步骤之前的值。 3,4在进入函数时执行,称为序言;6~8一般在函数结束是执行,称为尾声;5为函数主体,表示调用函数执行的全部操作。 调用约定(常用)cdecl调用约定: 调用方按从右到左的顺序将函数参数放入栈中,在被调用的函数完成其操作时,调用方负责从栈中清除参数。 stdcall调用约定: 按从右到左的顺序将函数参数放入栈中,函数结束时,被调用方负责删除栈中的参数。 fastcall调用约定: 传递给函数的前两个参数分别位于exc,edx(非程序栈),剩余其他参数类似stdcall,由于有两个参数被传递到寄存器中(传参2以上),被调用函数只需从栈中清除8字节。 c++调用约定: this指针指向用于调用函数的对象,c++并未规定如何向非静态成员函数传递this指针,不同编译器有不同解释。 栈帧示例:栈指针,就是栈顶指针 “序言”部分:
如果希望ebp作为栈指针,修改它之前必须保存ebp的当前值,并且在返回调用方时恢复ebp(保存其他寄存器如esi或edi也是这样,或推迟操作),即栈帧中没有用于存储被保存寄存器的标准位置。ebp被保存后,就可以对其修改,使它指向当前的栈位置。 “尾声”部分:
使用一个专用的栈帧,所有变量相对于栈指针寄存器的偏移量都可计算。许多时候,正偏移量用于访问函数参数,负偏移量用于访问局部变量。 调用:
“尾声”
IDA栈视图: 从开始,IDA提供了一个摘要栈视图 局部变量以var_为前缀,跟一个表示变量与被保存的栈指针之间的距离(以字节为单位)的十六进制后缀 函数参数(传入参数)名以arg_为前缀,后面跟表示其与最顶端的参数之间的相对距离的十六进制后缀,因此最顶端4字节参数名为arg_0,而随后为arg_4等
双击任一变量,即可进入详细视图 标记:dd一个储存字节 dw两个储存字节(也叫字) dd四个储存字节,双字 数据库搜索: search->text 启动文本搜索 search->sequence of bytes 搜索字节序列 : 搜索十六进制序列,应将搜索字符串指定为以空格分隔的两位十六进制值组成的列表;要搜索内嵌字符串数据,必须将搜索字符串用引号括起来 三,反汇编操作暂时不用 四,数据类型与数据结构数组成员访问:edit->array 格式化数组操作 可使数组看上去更明显(按地址顺序mov,全局数组可计算出地址) 栈分配数组 没有绝对地址,编译器无法计算地址,处理方法类似全局数组 堆分配数组 使用动态内存分配函数,所以必须根据内存分配函数返回的地址值,生成对数组的所有引用。如果能确定数组的总大小和每个元素的大小,可以计算该数组包含的元素数量。 只有当变量用作数组的索引时,才容易确定数组的存在;要访问数组中的元素,首先要用索引乘以数组元素的大小,计算出相应偏移量,与数组基址相加,得到数组元素的访问地址。 (常量索引值访问数组时,很少能确定是数组) 结构体成员访问:特点:结构体中数据字段通过名称访问,字段名称被编译器转换成数字偏移量 默认情况,编译器会设法将结构体字段与内存地址对其,最有效的读取和写入这些字段。 全局分配结构体:与全局分配数组一样,编译器编译时计算出结构体中每个成员的地址(仅从汇编无法判断使用了结构体)
栈分配结构体:根据栈布局很难识别出栈分配结构体,编译器能计算出起始位置与相关栈帧的帧指针ebp (其中栈指针:esp和帧指针:ebp)
堆分配结构体:由于结构体的地址在编译时未知,编译器别无选择,只有生成代码来计算每个字段在结构体中的正确偏移量。如果一个结构体在堆中分配,那么对编译器来说,引用该结构体的唯一线索就是指向该结构体起始地址的指针。
结构体数组:找到堆请求的字节数,数组索引数,加上数组起始地址mov xxxx ptr[ecx+eax],xx;可判断数组大小和元素个数
创建结构体:IDA之所以在分析阶段无法识别结构体,可能源于两个原因。首先,虽然IDA了解某个结构体的布局,但它并没有足够的信息,能够判断程序确实使用了结构体。其次,程序中的结构体可能是一种IDA对其一无所知的非标准结构体。在这两种情况下,问题都可以得到解决,且首先从Structures窗口下手 打开structures窗口,edit->add struct type 创建结构体 要给结构体添加新字段,将光标放在结构体定义的最后一行(包含ends那一行)并按下D键 如果需要修改字段的大小,首先将光标放在新字段的名称上,然后重复按下D键,使数据转盘上的数据类型开始循环,从而为新字段选择正确的数据大小。另外,你还可以使用Options->Setup Data Types来指定一个在数据转盘上不存在的大小。如果新字段是一个数组,右击其名称并在上下文菜单中选择Array;要更改一个结构体字段的名称,可右击该名称并在上下文菜单中选择ReName 省略一些详细帮助说明 可以使用结构体模版等,右击中,可在上下文菜单上看到Structure offset选项对指令操作数进行格式化;可以将栈和全局变量格式化成整个结构体,双击该变量,打开详细栈帧视图,然后使用Edit▶Struct Var显示一组已知结构体,重新格式化之后IDA将这个内存引用与结构体变量中的一个已定义的字段关联起来
将全局变量格式化成结构体的过程与格式化栈变量所使用的过程几乎完全相同 导入新的结构体:IDA能够解析C(而非C++)数据声明,以及整个C头文件,并自动为在这些声明或头文件中定义的结构体创建对应的IDA结构体。如果你碰巧拥有你正进行逆向工程的二进制文件的源代码,或者至少是头文件,那么,你就可以让IDA直接从源代码中提取出相关结构体 解析C结构体声明:View▶Open Subviews▶Local Types,通过INSERT键或上下文菜单中的Insert选项解析 解析C头文件:可以使用File▶Load File▶Parse C HeaderFile选择你想要解析的头文件。如果一切正常,IDA会通知你Compilation successful(编译完成)。如果解析器遇到任何问题,IDA将会在输出窗口中显示错误消息。IDA会将所有被成功解析的结构体添加到当前数据库的标准结构体列表中(具体地说,是列表的末尾)。如果新结构体的名称与现有结构体的名称相同,IDA会用新结构体布局覆盖原有结构体定义。除非你明确选择添加新的结构体,否则,新结构体不会出现在Structures窗口中 使用标准结构体:除了创建自定义结构体外,你还可以从IDA的已知结构体列表中提取出其他标准结构体,并将其添加到Structures窗口中。 首先,在Structures窗口中按下IN-SERT键。在Create structure/ union对话框中,包含一个Add standard structure(添加标准结构体)按钮。单击这个按钮,IDA将显示与当前编译器(在分析阶段检测出来)和文件格式有关的结构体主列表。这个结构体主列表中还包含通过解析C头文件添加到数据库中的结构体。 默认情况下,在创建后,文件头不会立即加载到数据库中。但是,如果你在最初创建数据库时选择Manual load(手动加载)选项,就可以将文件头加载到数据库中。加载文件头可确保只有与这些头部有关的数据类型才出现在数据库中。多数情况下,文件头不会以任何形式被格式化,因为通常程序并不会直接引用它们自己的文件头。因此,分析器也没有必要对文件头应用结构体模板 til文件处理ida中所有的数据类型和函数原型都储存在til文件中,types窗口(View->Open subview->Type Libraries)列出了当前加载til文件,并可用于加载想要使用的til文件(types窗口中按下INSERT) 还可以共享til文件(tilib) C++逆向工程基础所有非静态c++成员函数都使用this指针,任何时候调用这样一个函数,this都被初始化指向用于调用该函数的对象 vc++将this传递到ECX寄存器中,GUN g++则把this看成非静态成员函数的第一个(最左边)参数,并在调用该函数前将用于调用函数对象的地址作为最后一项压入栈中;也就是说,在调用函数之前,将一个地址转移到ECX意味着可能使用vc++编译,函数是一个成员函数,如果同一个地址被传递给两个或更多函数,那这些函数全都属于同一个类层次结构。函数初始化前使用ECX则调用方必定已经初始化ECX 虚函数,虚表: 虚函数为了重载和多态的需要,在基类中是由定义的,即便定义是空,所以子类中可以重写也可以不写基类中的函数! 编译器会为每个包含虚函数的类(或通过继承得到的子类)生成一个表,其中包含指向类中每一个虚函数的指针,这就是虚表(vtable),每个包含虚函数的类都获得另外一个数据成员(也就是虚表指针),用于在运行时指向适当的虚表。 使用虚表指针的后果,操纵IDA中的类时,必须考虑虚表指针。 下面的例子动态创建了SubClass 的一个对象,它的地址保存在BaseClass 的一个指针中。然后,这个指针被传递给一个函数(ca11_vfunc ),它使用该指针来调用vfunc3。
由于vfunc3 是一个虚函数,因此,在这个例子中,编译器必须确保调用SubC1ass :: vfunc3,因为指针指向一个SubC1ass 对象。下面ca11_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 [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 在A6处,虚表指针从结构体中读取出来,保存在EDX 寄存器中。由于参数b 指向一个SubClass对象,这里也将是SubClass 的虚表的地址。在AB处,虚表被编入索引,将第三个指针(在本位中为SubClass ::vfunc3 的地址) 读人EAX 寄存器。最后,在AE处调用虚函数。 AB处的虚表索引操作非常类似于结构体引用操作。我们可以定义一个结构体来表示一个类的虚表的布局,然后利用这个已定义的结构体来提高反汇编代码清单的可读性,如下所示:
结构体允许将虚表引用操作重新格式化成:
对象生命周期:对全局和静态分配的对象,构造函数中程序启动并进入main函数之前被调用;栈分配的对象的构造函数在对象进入声明对象的函数作用域中时被调用。一般对象一进入声明它的函数,它的构造函数就被调用,如果对象在一个块语句中声明,它的构造函数知道块被输入时才被调用(确实被输入)。如果对象在程序堆中动态分配则有二:一,调用new操作符分配对象的内存,二,调用构造函数来初始化对象。vc++与gun g++主要区别:前者可确保在调用构造函数之前new的结果不为null(空) 执行一个构造函数时,将会发生以下操作。 (1) 如果类拥有一个超类,则调用超类的构造函数。 (2) 如果类包含任何虚函数,则初始化虚表指针,使其指向类的虛表。注意,这样做可能会覆盖一个在超类中初始化的虚表指针,这实际上是希望的结果。 (3) 如果类拥有本身就是对象的数据成员,则调用这些数据成员的构造函数。 (4) 最后,执行特定于代码的构造函数。这些是程序员指定的、表示构造函数C++行为的代码。 构造函数并未指定返回类型,但由Microsoft Visual C++生成的构造函数实际上返回到EAX寄存器中的thi s 指针。无论如何,这是一个Visual C+实现细节,并不允许C++程序员访问返回值。析构函数基本上按相反的顺序调用。对于全局和静态对象,析构函数由在main函数结束后执行的清理代码调用。栈分配的对象的析构函数在对象脱离作用域时被调用。堆分配的对象的析构函数在分配给对象的内存释放之前通过delete 操作符调用。 析构函数执行的操作与构造函数执行的操作大致相同,唯一不同的是,它以大概相反的顺序执行这些操作。 (1) 如果类拥有任何虚函数,则还原对象的虚表指针,使其指向相关类的虚表。如果一个子类在创建过程中覆盖了虚表指针,就需要这样做。 (2) 执行程序员为析构函数指定的代码。 (3) 如果类拥有本身就是对象的数据成员,则执行这些成员的析构函数。 (4) 最后,如果对象拥有一个超类,则调用超类的析构函数。 通过了解超类的构造函数和析构函数何时被调用,我们可以通过相关超类函数的调用链,跟踪一个对象的继承体系。有关虚表的最后一个问题涉及它们在程序中如何被引用。一个类的虛表被直接引用,只存在两种情况: 在该类的构造函数中引用和在析构函数中引用。定位一个虛表后,可以利用IDA 的数据交叉引用功能迅速定位相关类的所有构造函数和析构函数 名称改编(名称修饰):是c++编译器用于区分重载函数的机制,为了给重载函数生成唯一的名称编译器用其他字符来修饰函数名称,用来编码关于函数的各种信息,编码后的信息描述函数的返回类型,所属的类和调用该参数所需的参数序列(类型和顺序)。名称改编是C++程序的一个编译器实现细节,本身并不属于C++语言规范,但通常IDA理解,并以注释的形式显示原始名称。 Options->Demangled Names 可选择 demangle选项 改编名称能提供大量与函数签名有关的信息,方便ida自己去识别函数 运行时类型识别(RTTI)也是编译器实现细节,不是语言问题。下面有例子: class abstract_class{ public : virtual int vfunc()= o; }; 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结构体,其中包含一个指向类名称的指针。在VC+代码中,指针指向一个微软RTTCompleteObjectLocator 结构体,其中又包含一个指向TypeDescriptor 结构体的指针。TypeDescriptor结构体中则包含一个指定多态类名称的字符数组。只有使用typeid 或dynamiCast 操作符的C++程序才需要RTT信息。多数编译器都提供一些选项,禁止不需要RTTI 的二进制文件生成RTT 。 继承关系:从RTTI出发 |
|