几种调用约定区别
_cdecl是C的调用约定,从右向左参数依次入栈,调用者平衡堆栈。所以函数外add+x平衡堆栈
_stdcall是微软的调用约定,从右向左参数依次入栈,被调用者平衡堆栈,所以平衡堆栈为retXXX
fastcall是stdcall变体,x86下前两个参数用寄存器ecx,edx,后面跟_stdcall一致。
C++ 调用约定,非静态成员函数与标准函数不同,需要this指针,指向被调用的的对象,这个由被调用方提供给,thiscall为MS提供的C++ 调用约定,被调用者平衡堆栈,this指针在ecx中;GNU g++编译器将this看成第一个参数。其他地方跟cdecl约定相同。所以调用者平衡堆栈。
ALT+T搜索文本,CTRL+T重复搜索下一个匹配结果。
ALT+B二进制搜索,CTRL+B重复搜索下一个匹配结果。
IDA恢复重命名就是重新命名为一个空白名称。
重命名
重命名中:
名称 | 作用 |
---|---|
Local names | 局部名称,唯一性仅在函数中有效,常用作分支控制,跳转 |
public name | 公共名称1,指由二进制文件(如共享库)输出的名称。 |
可重复注释为;分号,正常注释是:冒号。
栈指针
任何时候如果IDA遇到一个函数反汇语句,检测到栈指针不为0,这时IDA将标注一个错误条件,并将相关指令以红色显示。
函数块
创建新的函数块,首先选择该块起始地址,edit->function->append Function Tail命令,从已定义函数选择一个。
如果嫌弃函数块麻烦,可以在文件初次加载到IDA时,取消选择Create function tails加载器选项。在Kernel Options里。
函数特性
在Edit function里:
结束地址:函数中最后一条指令之后的地址。通常是返回语句之后的指令的地址。记住这个地址不是函数的一部分,而是函数最后一条指令之后的地址。
保存寄存器:saved registers,为调用方保存寄存器所用的字节数。IDA认为保存的寄存器区域放在保存的的返回地址顶部,局部变量下方。
已删除字节:表示当函数反汇调用方时,IDA从栈中删除的字节数。cdecl调用时这个值恒为0。如果IDA观察到程序使用了RET N。自动确定这个值。
帧指针增量:Frame point deta有时候编译器可能会对函数帧指针进行调整,使其指向局部变量区域的中间,而不是指向保存在局部变量区域底部的帧指针。目的是在离帧1字节带符号地方(带符号)的偏移量(-128~+127)内保存尽可能你
栈指针调整:如果IDA不能更好调整栈帧。我们可以ALT+K,手动调整,比如未识别stdcall调用方式的函数,调用前后栈帧不一致。可以如此修改。或者如果此函数是个导入函数,也可以编辑有关行为,指定返回时从栈中删除的字节数。
数组编辑
Array element Width(数组元素宽度)。这个值表示各组元素的大小,它由你打开对话框时选择的数据值大小决定。
Maximum possible size(最大可能大小)。这个值由自动计算得出,他决定在在遇到另一个已定义的数据项之前,可包含数组中的元素的最大数目。你可以指定一个更大的值,但这需要随后的数据项为未定义数据项,以将他们吸收到数组中。
Number of elements(元素数量)你可以在这里指定数组的具体大小。数组占用的总大小可以用 元素数量*数组元素宽度 计算得出。
Items on a line(行中的项目)。指定每个反汇编行显示元素的数量。可以通过它减少显示数组所需要的空间。
Element width(元素宽度)。这个值仅用于格式化。当一行显示多个项目时,它控制列宽。
Use dup constant(使用重复结构)。这个选项可将相同的数据值合并起来,用一个重复说明符组合成一项。
Signed elements(有符号元素)。表示将数据显示为有符号还是无符号的值。
Display index(显示索引)。使数组索引以常规注释的方式显示。如果你需要定位大型数组中的特定数据,可以使用这个选项。
Creat as array(创建为数组)。这个选项似乎有悖于这个对话框的目的,一般我们都不会选该选项。
修改函数原型
使用Y快捷键或者Edit->function->edit,可以修改参数类型等。
关于数组再反汇编中的表现
如果是global:如果是指定固定数字下标索引,为固定值;如果是变量索引为如[eax
x](x为元素大小)
如果是栈:如果是指定固定数字下标索引,索引为[ebp+偏移],如果是变量就是[ebp+eax
x]
如果是堆:一般为先mov ecx,ebp+heap_array;mov dword ptr [ecx+eax*4],这样索引。
编辑创建结构体
利用字段创建命令D,A和数字键盘的星号*。
首先创建一个结构体。
如果想添加字段就对着最后一行(包含end的那行)按下D健。修改字段大小就是不停按D进入类型转盘。或者Options》setup Data Type指定一个不在转盘的大小。如果新字段为数组就对着字段右键选择数组。
改字段名就是快捷键N或者右键Rename。
另外
- 只有当一个字段是结构体最后一个字段时才能删除,否则按U只是取消字段定义,并没有删除空间。
- 对一个结构体定义中的所有字段做适当的对齐。填补字段最好作为适当大小的哑字段添加。
- 在取消了字段定义之后,才能点击edit->Shrink Struct Type(缩小结构体类型),删除被取消定义的结构体字段。
- 在结构体中介添加新的字节方式为:在新字节后面的一个字段使用Edit->expand struct Type添加一个新的字段。
- 如果知道一个结构体大小,不知道结构体的布局,可以创建两个字段,第一个字段为一个数组,大小为结构体大小减去一个字节(size-1),第二个字段为1字节。创建第二个字段后,取消第一个数组的定义,这样结构体大小就被保留下来了,当了解布局之后,可以回过头来定义他的字段及大小。
- 按数字键盘的“-”号可以折叠结构体的定义。
关于使用结构体模板
可以将栈和全局变量格式化成整个结构体。要将栈变量格式化成结构体,双击变量打开详细栈帧,使用edit->struct var(alt+q)显示一组已知的结构体。
关于全局变量格式格式化成结构体与栈变量几乎相同。选择要格式化的变量,或者表示结构体开头部分地址,点击上面一样的选项即可。
导入新结构体
解析C结构体声明
使用View->open subviews->Local Types,列出了所有解析到当前数据库文件的类型。可以通过INSERT解析新的类型。
IDA默认4字节对齐结构体。如果需要其他对齐方式,使用pragma pack指令。
添加完对着本地类型右键选择Synchronize to idb才能生效。
解析C头文件
使用File->Load File->Parse C Header File选择解析的头文件。
导入标准结构体
可以根据部分文本匹配,比如运行进行前缀匹配,如果知道结构体前几个字符。输入这几个字符,比如输入DOS_H
就可以看见IMAGE_DOS_HEADER,第二部使用Edit->struct VAR(ALT+Q),将_ImageBase开始的字节转换成一个IMAGE_DOS_HEADER结构体,即可得到结构化的DOS头。然后继续添加IMAGE_NT_HEADER,然后再e_lfanew处也就是偏移0x80处使用Struct VAR(ALT+Q),即可识别NT头信息。
TIL文件
IDA中所有数据类型和函数原乡信息存储在TIL文件中,通过TYPE窗口(VIEW->Open subview->Type Libraries)列出,也可以Insert手动加载需要的TIL。
共享TIL文件
当分析文件时,分析文件当前目录也会产生.TIL文件,如下图
存储着当前分析创建的结构体,如果想要共享,有两种方法:
第一种为将.til文件打开的数据块复制到另一个目录中,然后再通过Types窗口,再其他数据库打开这个.til文件。
第二种是正式的方法,从一个数据库种提取字定义的信息,生成一段IDC脚本,用于在数据库中重建字定义结构体。
但是这种方法只能转储Structures窗口列出的结构体,无法转储通过解析C头文件得到的结构体。
Hax-Rays还提供一个名为tilib工具,用于在IDA以外创建.til文件。安装使用方法为将.zip文件解压到ida目录即可。tilib可以列举现有.til文件的内容。或通过解析C头文件创建新的.til文件。
使用这个工具可以比如列出vc10.til的库内容:
创建的.til文件包括命名要解析的头文件以及要创建的.til文件,可以使用命令行指定其他包含文件目录或者之前解析的.til文件,以解析文件头中包含的任何依赖关系。下面命令为创建一个包含ch8_struc声明的新.til文件,生成的.til文件要移植ida目录的/til文件夹,才能供IDA使用
tilib -c -hch8_struct.h ch8.til
C++逆向基础
C++第一个成员为虚函数表,然后排序是按着先父类虚函数然后子类虚函数。
如果没有对纯虚函数实现,编译器会插入一个错误处理函数,通常名为purecall,理论不会被调用,一旦调用,会让程序终止。
因为有虚函数表,所以动态new分配对象时,要考虑类和超类所有显式声明的字段占用空间,还要虚表指针的空间。
纯虚函数就是必须要在子类实现的函数。
运行时类型识别
RTTI指针在虚函数指针前?
交叉引用
代码交叉引用中,函数导致交叉引用,结尾后缀是P(procedure),跳转后缀J。
数据交叉引用中,有读取,写入,以及何时被引用(偏移量交叉引用,表示引用的是某个位置的地址,不是内容),结尾后缀分别是rwo。
使用菜单view->open subview->cross-references打开为非模态,快捷键CTRL+X为模态。
交叉引用中函数调用列表窗口可以通过view->open subview->Function calls打开。
上半部分为打开窗口时光标所在函数,下半部分是当前所有此函数的调用。
Xrefs To交叉引用目标为底归上升,回溯所有以选定的符号做为目标的交叉引用,知道达到一个没有其他符号引用的符号。
Xrefs From,底归下降操作,即跟踪所有以选定符号位源头的交叉引用,如果符号是函数名,只跟踪调用引用,不跟踪数据引用。
这里是显示交叉引用列表。
IDA配置文件
主配置文件ida.cfg。
使用FLIRT签名识别库
库快速识别和鉴定技术,简称魏FLIRT。
可以通过如下,打开
创建FLIRT签名文件
flair中:
plb.txt:描述静态库解析器plb.exe的用法。
pat.txt:说明了模式文件的格式。
创建签名概述
- 获得一个希望奇创建签名文件的静态库。
- 利用其中一个FLAIR解析器为该库创建一个模式文件。
- 利用其中一个FLAIR解析器为该库创建一个模式文件。
- 运行sigmake.exe处理生成的模式文件,生成一个签名文件。
- 将签名文件复制到/sig目录。
扩充函数信息
可以先找到函数,然后Edit->Function->Edit Function,或者ALT+P,如果是导入函数就可以修改平衡堆栈的数量,手动修改函数确定平衡了几个字节。
IDS文件
.ids实际是压缩后的.idt文件,包含函数名,序号,是否使用stdcall等。
IDA的idsutils工具用于创建.ids文件。
格式为:
0 Name=xxx Pascal=y Comment=zzz //第一个是序号,为0是指的当前库的名字,Pascal是平衡的堆栈 Comment是注释
关于使用:
./dll2idt.exe ssleay32.dll //给openssl有关的ssleay32.dll创建.idt文件
使用.ids文件时,可敬ids文件连接到指定的.sig或.til文件。选择.ids文件时,IDA会使用一个名为/ida/idsnames的IDS配置文件。
libc.so.6 libc.ids + //将共享库名称与对应的.ids文件名映射起来。
xxx.ids xxx.ids + yyyy.til //将.ids与.til文件映射起来。
xxx.sig yyy.ids //x与y映射。
loaint扩展预定义注释
预定义注释保存在/ida.int文件中,修改方法为:
- 确定处理器关联的注释文件。
- 修改注释
- 运行loadint重新创建注释ida.int文件。
- 将生成的ida.int复制到IDA主目录。
IDA脚本
IDC有三种数据类型:整数(long),字符串,浮点值,对象,引用,函数指针等。
auto 局部变量声明。extern引入全局变量声明。
IDC表达式
所有整数操作为有符号数,在ida中给字符串复制直接会复制字符串,idc支持分片。
auto str= "string to slice";
auto s1,s2,s3;
s1=str[7:9]; //"to"
idc不支持switch。也不支持复合运算(+=这种)。支持try/catch。
在花括号支持声明新的变量,只要在第一个语句即可。但是不限作用域,出了花括号还能使用。
IDC中static用于引入一个用户定义的函数。函数不会指明是否有返回值,需要返回值时用return。
IDC可以用类,所有成员都是公共类。
IDA插件体系结构
编写插件
所有IDA模块包括插件都以适用于执行插件的平台的共享库组件实现,在IDA模块化体系结构下,模块不需要导出任何函数。而每个模块必须导出某个特定类的一个变量,就插件而言,这个类是plugin_t。此类字段如下:
- version。这个成员指出用于构建插件的 IDA 的版本号。通常,它被设置为在 idp.hpp 文件中声明的 IDP_INTERFACE_VERSION 常量。
-
flags。这个字段包含各种标志,它们规定 IDA 在不同的情况下该如何处理插件。这些标
志使用在 loader.hpp 文件中定义的 PLUGIN_XXX 常量的按位组合来设置。一般来说,将这个字段赋值为零就够了 - init。这是 plugin_t 类所包含的 3 个函数指针中的第一个指针。这个特殊的成员是一个指向插件的初始化函数的指针。该函数没有参数,返回一个 int。IDA 调用这个函数,允许加载你的插件。
-
term。这个成员是另一个函数指针。当插件卸载时,IDA 将调用相关函数。该函数没有参数,也不返回任何值。在 IDA卸载你的插件之前,这个函数用于执行插件所需的任何清理任务(释放内存、结束处理、保存状态等)。如果在插件被卸载时,你不需要执行任
何操作,你可以将这个字段设置为 NULL。 - run。这个成员指向一个函数,只要用户激活(通过热键、菜单项或脚本调用)你的插件,都应调用这个函数。这个函数是任何插件的核心组件,因为用户正是通过它定义插件行为的。将脚本与插件进行比较时,这个函数的行为与脚本语言的行为极相似。这个函数接受唯一一个整数参数,且不返回任何值。
- comment。这个成员是指向一个字符串的指针,这个字符串代表插件的一条注释。可以将它设置为 NULL。
- help。这个成员是指向一个字符串的指针,这个字符串充当一个多行帮助字符串。IDA 并不直接使用这个成员,可以将它设置为 NULL。
- wanted_name。这个成员是指向一个字符串的指针,这个字符串保存插件的名称。当一个插件被加载时,这个字符串被添加到 Edit->Plugins菜单中,提供一种激活该插件的方法。
- wanted_hotkey。这个成员是指向一个字符串的指针,这个字符串保存 IDA 尝试与插件关联的热键(如 ALT+F8)的名称。
二进制文件与IDA加载器模块
假设不识别PE文件。
首先根据DOS信息和PE头信息创建结构体,然后PE头的IMachine识别了处理器,mageBase基址为0x400000,显示了已加载文件映像的基本虚拟地址,利用这个信息合并到数据库中,使用Edit->segments->rebase Program菜单项,为程序第一段指定最新的基地址。
因为当一个文件以二进制模式加载时,IDA 仅创建一个段来保存整个文件,所以在当前的例子中,只有一个段存在。该对话框中的两个复选框决定在段被移动时,IDA 如何重新定位,以及IDA 是否应移动数据库中的每一个段。对于以二进制模式加载的文件,IDA将无法获知任何重定位信息。同样,由于程序中只有一个段,默认情况下,IDA 将重新设置整个映像的基址。
AddressOfEntryPoint字段指定程序进入点的相对虚拟地址(RAV)。RAV是一个相对于程序基本虚拟地址的偏移量,而程序进入点表示程序中即将执行的第一条指令的地址。在这个例子中,进入点 RAV 1000h 表示程序将在虚拟地址401000h(400000h+1000h)处开始运行。这是一条非常重要的信息,因为,对于该在数据库的什么地方开始寻找代码,这是我们获得的第一个提示。但是,在查找代码之前,需要将数据库的剩余部分与相应的虚拟地址对应起来。
PE 格式利用“节”(section)来描述文件内容与内存范围之间的对应关系。通过解析文件中每节的头部,我们可以确定数据库的基本虚拟内存布局。NumberOfSections字段指出一个PE 文件所包含的节的数量。,在 IMAGE_NT_HEADERS 结构体后面,紧跟着一个节头部结构体数组。这个数组中的每个元素都是 IMAGE_SECTION_HEADER 结构体,我们可以在 IDA 的“结构体”窗口中定义这些结构体,并将它应用于 IMAGE_NT_HEADERS 结构体后面的字节。
还需要注意 FileAlignmen和 SectionAlignment这两个字段。这两个字段说明如何对齐①文件中每节的数据,以及将数据映射到内存中时,如何对齐相同的数据。PE文件中,每节与文件中的一个 200h 字节偏移量对齐。但是,在加载到内存中时,这些节将与能够被 1000h 整除的地址对齐。在将一个可执行映像存储到文件中时,使用更
小的 FileAlignment 有利于节省存储空间,而较大的SectionAlignment值通常对应于操作系统的虚拟内存页面大小。在数据库中手动创建节时,了解节如何对齐可帮助我们避免错误。创建每节头部后,可以开始创建数据库中的其他段。对紧跟在IMAGE_NT_HEADERS 结构体后面的字节应用一个IMAGE_SECTION_HEADER模板,将生成第一个节头部,并使以下数据在示例数据库中显示出来:
Name字段表明这个头部描述的是.text 节,PointerToRawData字段(400h)指出可以找到节内容的位置的文件偏移量。需要注意的是,这个值是文件对齐值 200h 的整数倍。PE 文件中的节按文件偏移量(和虚拟地址)升序排列。由于这个节以文件偏移量 400h 为起点,我们可以得出结论:文件的第一个 400h字节包含文件头部数据。
在Edit->segments->create segment中可以手动创建一个段。对于x86二进制文件,IDA通过将段的基值向左移4个位,然后在字节上加上偏移量,从而计算出字节的虚拟地址(virtual=(base<<4)+offset)。如果不使用分段,则应使用基值零。VirtualAddress()字段(1000h)是一个RAV,它指定应加载段内容的位置的内存地址,SizeOfRawData()字段(600h)指出文件中有多少字节的数据。换句话说,这个特殊的节头部告诉我们,.text节是通过将文件偏移量400h与9FFh之间的600h个字节映射到虚拟地址 401000h 与 4015FFh 之间创建而成。
在创建.headers节时,IDA拆分最初的seg000节,构成我们指定的.headers节和一个新的seg001节,以保存seg000中的剩余字节。在数据库中,.text节的内容为seg001节的前600h个字节。我们只需要将seg001节移动到正确的位置,并确定.text节的正确大小即可。
创建.text节的第一步是将seg001移动到虚拟地址401000h处。使用Edit->Segments->MoveCurrentSegment命令为seg001指定一个新的起始地址,下一步,我们将通过Edit->Segments->CreateSegment从新移动的seg001节的前600h字节中分离出.text节。结束地址并不包含在地址范围内。
紧接着一个个段创建,记得后面的段泵轻易的将PointerToRawData字段映射到数据库中的一个偏移量。而是跟在前一个段后面。
还要给段之间空隙补零。
然后就可以在OEP将字节转化为代码。
此外加载器可以加载pacp文件,识别其中的可执行文件。
编译器变体
跳转表与分支语句
switch
borland编译
Miscrosoft vc++
gcc
定位main函数
gcc的linux
传递给__libc_start_main的第一个参数(在栈的最顶端,因而最后被压入)实际上是一个指向main的指针。有两个因素阻止IDA将loc_8048384确定为函数(它可能名为sub_8048384)。第一个因素是它从未被直接调用,因此,loc_8048384绝不会是一条调用指令的目标。第二个因素是,虽然IDA基于已识别函数的“序言”,提供了它们的“启发”(这也是sub_80483C0和sub_80483D0被确定为函数的原因,即使它们同样从未被直接调用),但是,loc_8048384(main函数)处的函数并没有使用IDA能够识别的“序言”。这段“惹事生非”的“序言”(包括注释)如下所示:
这段“序言”包含一个使用 EBP 作为帧指针的函数的传统“序言”的所有要素。首先保存调用方的帧指针,然后为当前函数设置帧指针,最后为局部变量分配空间。IDA 的问题在于,这些操作并非作为函数中的前几项操作而发生,因此 IDA 的“启发”失效。
这时,要手动创建一个函数,操作起来非常简单(Edit->Functions->Create Function),但是,你应该小心监视IDA的行为。就像它起初无法识别该函数一样,它可能同样无法确定该函数使用EBP 作为帧指针。你需要编辑这个函数(ALT+P),迫使 IDA 相信该函数使用一个基于 BP 的帧,并对专门用于保存寄存器和局部变量的栈字节的数量进行调整.
在Windows平台上,C/C++编译器的数量(因而启动例程的数量)要更多一些.下面的启动例程摘自一个gcc/Cygwin二进制文件:
这段代码与前面的Linux示例存在一些差异。但是,有一个地方相似:只有一个函数被调用,且该函数以一个函数指针作为参数。在这个例子中,sub_401120的作用与__libc_start_main相同,而sub_4010B0则是程序的main函数。
使用gcc/MinGW构建的Windows二进制文件可以使用另一种形式的start函数,如下所示:
这时,IDA同样无法识别程序的main函数。关于main函数的位置,这段代码提供了若干条线索:只有一个非库函数被调用(sub_401150),该函数似乎并未使用任何参数(而main函数应包含参数)。这时,最好的办法是继续在sub_401150中搜索main函数。sub_401150的一部分代码如下所示:
与我们前面看到的与FreeBSD有关的start函数有许多相似之处。sub_401395可能就是main函数,因为它是唯一一个使用3个参数(2、3和4)调用的非库函数,而且第三个参数(4)与库函数__p__environ的返回值有关,这使我们联想到一个事实,即main函数的第三个参数应该是一个指向环境字符串数组的指针。虽然并未显示,但这段代码在之前还调用了getmainargs库函数,以在真正调用main函数之前设置argc和argv参数,并进一步强化一个概念:main函数即将被调用。
Visual C/C++代码的启动例程简洁明了,如下所示:
通过应用启动签名,而非因为程序链接到一个包含给定符号的动态库,IDA识别出两条指令引用的库例程。IDA的启动签名能够轻松确定最初调用main函数的位置。
调试版本和发行版本二进制文件
在vs调试版本中,一个明显的不同是几乎所有函数通过jump函数(thunk函数)调用。
其他调用约定
下面的代码是一个使用非标准调用约定的函数:
根据IDA的分析,该函数的栈帧中只有一个参数(1)存在。但是,经过仔细分析,我们发现,这个函数使用了EAX寄存器(2)和CL寄存器(3),但没有进行任何初始化。据此,我们得出的唯一结论是,EAX和CL寄存器应由调用方初始化。因此,我们应把这个函数看成是一个包含3个参数的函数,而不是仅包含一个参数的函数。在调用它时,你必须特别小心,以确保它的3个参数都处在正确的位置。通过设置函数的“类型”,IDA能够指定任何函数的自定义调用约定。通过Edit->Functions->Setfunctiontype(编辑函数设置函数类型)菜单项输入函数的原型并使用IDA的_usercall调用约定,即可做到这一点。用于为上一个示例中的sub_158AC设置类型的对话框如下:
模糊代码分析
反静态分析技巧
反汇编去同步
专门破坏反汇编过程,即创造性地使用指令和数据,以阻止反汇编器找到一条或多条指令的起始地址。这种方法将令反汇编器“迷失自己”,无法生成反汇编代码清单,或者至少生成错误的反汇编代码清单。
这个例子执行了一次调用(1,使用跳转会更加方便),调用对象在现有指令的中间(2)。由于IDA认为这个函数调用将会返回,它继续反汇编地址0A04B0D6(2)处的指令(并不正确)。调用指令的真正目标——loc_A04B0D6+1(0A04B0D7)——将不会被反汇编,因为相关字节已经作为0A04B0D6处的5字节指令的一部分分配。如果注意到这种情况,剩下的反汇编代码清单应引起我们的怀疑。其他证据包括出人意料的用户空间指令(3,这里为iret②)及杂项数据类型(4)。注意,这种行为并不仅限于IDA。无论它们使用的是递归下降算法还是线性扫描算法,几乎所有反汇编器都会成为这种技巧的受害者。处理这种情况的正确方法是对包含调用目标字节的指令取消定义,然后在调用目标地址处定义一条指令,以重新同步反汇编代码清单。当然,使用交互式反汇编器可大大简化这个过程。使用IDA,将光标放在处,应用Edit->ndefine(热键U),然后将光标放在0A04B0D7地址处,应用Edit->Code(热键C),可以得到下面的代码:
从这个代码段中,我们发现,很明显,地址0A04B0D6(1)处的字节从未执行。地址0A04B0D7(2)(调用目标)的指令用于从栈上删除返回地址(来自虚假调用),然后执行继续。值得注意的是,不久之后,我们这里讨论的反逆向工程技巧又被重新利用,这次它使用的是地址0A04B0DB(3)处的一个2字节跳转指令,它实际上跳转到自身之中。这时,我们同样必须取消一条指令的定义,以到达下一条指令的开始位置。再一次应用取消定义(地址0A04B0DB处)和重新定义(地址0A04B0DC处)过程,得到下面的反汇编代码清单:
结果,跳转指令的目标是另一条跳转指令(1)。但是,反汇编器不可能跟踪这里的跳转(分析人员也会感到困惑),因为跳转的目标包含在寄存器(EAX)中,并在运行时计算。在这个例子中,鉴于在跳转之前的指令序列相对简单,确定EAX寄存器包含的值并不是非常困难。2处的pop指令将前一个例子中调用指令(0A04B0D6)的返回地址加载到EAX寄存器中,随后的指令(3)再给EAX加上10。因此,跳转指令的目标为0A04B0E0,我们必须从这个地址恢复反汇编过程。
动态计算目标地址
这个例子使用一个call语句将一个返回地址压入栈中。然后,这个返回地址直接由栈进入寄存器,再给寄存器加上一个常量值,得到最后的目标地址。最终,通过执行一个跳转指令,跳转到寄存器内容指定的位置,再到达目标地址。
右边的注释记录了每一条指令对各种CPU寄存器所做的更改。这个过程以一个获取到的值被移入栈顶部(TOS)而告终(1),从而使返回指令将控制权转交给计算得出的位置(这里为0A04B068)。这样的代码序列能够显著增加静态分析的工作量,因为分析人员必须动手运行代码,才能确定程序的具体控制流路径。
另一种技巧常用在面向Windows的恶意软件中,它配置一个异常处理程序②,并有意触发一个异常,然后在处理异常时操纵进程的寄存器的状态。下面的例子被tElock反逆向工程工具用于隐藏程序的真实控制流:
首先,这段代码使用一个调用指令(1)调用下一条指令(2),这个调用指令将0041D07F作为返回地址压入栈中,随后这个返回地址立即由栈进入EBP寄存器(2)。接下来(3),EAX寄存器被设置为EBP和46h的和,即0041D0C5,并将这个地址作为一个异常处理函数的地址压入栈中(4)。剩下的异常处理程序设置在5和6处发生,它们将新的异常处理程序链接到由fs:[0]①引用的现有异常处理程序链中。下一步是有意生成一个异常(7),这里为int3,它是调试器使用的一个软件陷阱(中断)。在x86程序中,int3指令被调试器用于实现一个软件断点。正常情况下,这时一个依附于进程的调试器将获得控制权。实际上,如果一个调试器已经依附于进程,它将有机会第一个处理异常(把它看成是一个断点)。在这个例子中,程序已做好处理异常的准备,因此,任何依附的调试器应将异常递交给程序处理。无法使程序处理异常可能会导致错误操作,甚至会使程序崩溃。如果不了解如何处理int3异常,你将无法知道这个程序下一步将如何执行。如果我们假定程序会在int3后继续执行,那么最终指令8和9将触发一个“除以零”异常。
传递给异常处理函数的第三个参数(1)是一个指向一个WindowsCONTEXT结构体(在WindowsAPI头文件winnt.h中定义)的指针。CONTEXT结构体使用异常发生时所有CPU寄存器的内容进行初始化。一个异常处理程序有机会检查和修改(如有必要)CONTEXT结构体的内容。如果异常处理程序认为它已经更正了导致异常的问题,它可以通知操作系统,允许导致异常的线程继续执行。这时,操作系统会从提供给异常处理程序的CONTEXT结构体中,为这个线程重新加载CPU寄存器,线程将恢复执行,就好像什么也没有发生一样。在上面的例子中,异常处理程序首先访问线程的CONTEXT结构体(2),以递增指令指针(3),从而移动到生成异常的指令之外。接下来,异常的类型代码[EXCEPTION_RECORD4中的一个字段]被检索(5),以确定异常的性质。这部分的异常处理程序通过将所有x86硬件调试寄存器①清零(7),处理前一个例子中生成的“除以零”错误(6)。如果不分析剩余的tElock代码,你不能立即了解清零调试寄存器的原因。在这个例子中,tElock正清除前一个操作的值,在前一个操作中,它使用调试寄存器设置了4个断点以及我们前面看到的int3。除了模糊程序的真正控制流外,清除或修改x86调试寄存器可能会对应用软件调试器(如OllyDbg)或IDA的内部调试器造成重大影响。
操作码模糊
阻止正确反汇编的一种更加有效的方法是在创建可执行文件时编码或加密具体的指令。
模糊指令对CPU没有用处,在被CPU提取并执行之前,它们必须经过去模糊处理,以恢复到原始状态。因此,程序必须至少有一个部分没有被加密,以充当启动例程。在模糊程序中,启动例程通常负责对一些或所有的剩余程序进行去模糊处理。
模糊过程的输入其实就是加壳。
反动态分析技巧
检测虚拟化
检测虚拟机 虚拟化平台包含后门式的通信通道,以方便虚拟机与主机软件之间的通信。例如,下面的4行代码可用于确定你是否在一个VMware虚拟机中运行:
如果你在虚拟机中,这段代码将导致EBX寄存器包含0x564D5868这个值。如果你不在虚拟机中,根据你使用的主机操作系统,这段代码将造成一个异常,或者不会改变EBX寄存器。这个指令序列利用了一个事实,即用户空间程序通常不使用或不允许使用x86in指令。但是,在VMware中,这个指令序列可用于检测一个特殊的通信通道,VMware客户操作系统使用这个通道与它们的主机操作系统进行通信。例如,VMwareTools使用这个通道在主机与客户操作系统之间交换数据(如剪贴板内容)。
**检测特定于处理器的行为变化。 **
完美的虚拟化很难实现。理想情况下,程序应该不能检测到虚拟化环境与本地硬件之间的任何差异。但是,这种情况很少发生。观察在本地硬件与虚拟机环境中执行的x86sidt指令的行为差异后,JoannaRutkowska开发出她的“红丸”(redpill)VMware检测技巧。
使用IDA对二进制文件进行“静态去模糊”
面向脚本的去模糊
面向模拟的去模糊
ida-x86emu可用于模拟大部分的x86指令集。x86emu插件默认使用ALT+F8热键组合激活。
一旦激活后,插件将执行许多其他操作。模拟器将为所有文件类型创建名为.stack和.heap的新数据库段,为模拟程序的操作提供运行时内存支持。在某个二进制文件中第一次激活插件时,当前的光标位置用于初始化指令指针(EIP)。对于WindowsPE二进制文件,该插件执行以下任务。
(1)创建另外一个名为.headers的程序段,重新读取输入的二进制文件,然后将MS-DOS和PE头部字节加载到数据库中。
(2)分配内存,模拟一个线程环境块(TEB)和一个进程环境块(PEB)。用合理的值填充这些结构,让被模拟的程序确信,它在真正的Windows环境中运行。
(3)为x86段寄存器分配合理的值,配置一个虚假的中断描述符表,提供最小的异常处理功能。
(4)尝试在PE文件的导入目录中定位所有被引用的DLL。对于每一个被发现的DLL,模拟器将在数据库中为它们创建额外的段,并加载该DLL的头部和导出目录。然后,用从已加载DLL的信息中获得的函数地址填充二进制文件的导入表。注意,已导入的DLL中没有任何代码被加载到数据库中。
漏洞分析
无论是在源代码还是在二进制层次上进行审核,基本的静态分析技巧包括:审核问题函数(如strcpy和sprintf)的使用,审核动态内存分配例程(如malloc和VirtualAlloc)返回的缓冲区的用法,审核如何处理通过recv、read、fgets和许多其他类似函数接收的用户提交的输入。
BugScam插件查询变量是否有缓冲区溢出。
使用IDA在事后发现漏洞
商业版BinDiff,BDS,Turbodiff,PatchDiff2可以进行二进制比较。
IDA与破解程序开发过程
实用IDA插件
collabREate促进分析同一二进制文件的多个用户之间的协作。
Class Informer主要用于逆向工程使用MicrosoftVisualStudio编译的C++ 代码。ClassInformer通过标识虚拟函数表(vtable或vftable)及RTTI信息,然后提取出相关类名称与继承信息,从而自动完成IgorSkochinsky在他的有关逆向工程MicrosoftVisualC++①的OpenRCE文章中描述的大部分工作。
MyNav添加的功能包括一个函数级(与基本的块级相反)图形浏览器(受Zynamics的BinNavi启发而开发)、其他图形功能(如显示任意两个函数间的代码路径),以及许多旨在增强IDA的调试功能的特性。在调试方面,MyNav会记录有关调试会话的信息,并允许你使用一个调试会话的结果来过滤随后的会话。在任何调试会话结束后,MyNav会显示一个图形,仅在其中突出显示那些在会话过程中执行的函数。使用MyNav提供的功能,你可以快速缩小负责程序特定操作的函数集的范围。例如,如果你对负责启动网络连接并下载某些内容的函数感兴趣,可以创建一个执行除启动网络连接以外的任何操作的会话,然后再执行另一个会话,并在其中创建一个网络连接。排除在第一个调试会话中执行的所有函数后,MyNav最终生成的图形将包含与那些负责启动网络连接的函数有关的信息。如果你试图了解那些具有庞大二进制代码的函数,这项功能会非常有用。
IdaPdf由一个IDA加载器模块和一个IDA插件模块组成,这两个模块都设计用于分析PDF文件。IdaPdf的加载器组件将识别PDF文件并将其加载到一个新的IDA数据库中。加载器负责分割PDF文件。在加载过程中,加载器将尽一切努力提取并过滤出所有PDF流对象。由于加载器模块会在加载过程完成后退出,这时就需要第二个组件(即IdaPdf插件),以提供初始加载以外的PDF分析功能。插件模块在确认已加载PDF文件后,将继续枚举文件中的所有PDF对象,并打开一个新的选项卡式窗口,其中列出每一个PDF对象。
IDA调试器
调试器通常用于执行以下两种任务:
分析与已崩溃进程有关的内存映像(内核转储)。
以一种完全受控的方式执行进程。调试会话以选择一个接受调试的进程为起点。
Debugger->Attach可以附加调试。
栈跟踪显示的是当前调用栈或函数调用序列,这些调用是为了使执行到达二
进制文件中的一个特定位置。使用Debugger->Stack Trace。
栈跟踪的最上面一行列出当前正在执行的函数名称。第二行指出调用当前函数的函数,以及
做出该调用的地址。下面的行则指出调用每一个函数的地址。调试器可以通过遍历栈并解析它遇
到的每一个栈帧,从而创建一个栈跟踪窗口。I