今天偶然在图书馆发现一本《IDA PRO权威指南》,我上回搜还没有,原来是图书馆查询系统还要区别大小写,哼~既然想了解机器指令是如何转为汇编指令的,那就需要了解基本的反汇编算法。
反汇编算法
算法思路
第一步,确定进行反汇编的代码区域
对于一个反汇编可执行文件,通常指令与数据混杂在一起,首先要做的就是区分它们。对于Unix系统常用的可执行和链接格式和Windows系统下的可移植可执行格式的文件,这些格式通常含有一种机制,表现为层次文件头的形式,来确定文件中包含代码和代码入口点的位置。
第二步,获取指令和操作数
找到指令的起始地址后,需要读取该地址所包含的值,并执行一次表查找,将操作码和助记符对应起来。根据不同的指令集,还需要获取该指令包含的操作数。对于一个固定长度的指令集,这个过程就比较简单;但多数情况下我们遇到的都是一些非固定长度的指令集,在这种情况下就大大增加了反汇编的难度,需要检索额外的指令字节。同时这个过程也是极为关键,很多文件正是因为这个过程最终导致错误的翻译。
第三步,格式转换,输出
获取到指令和操作数后,需要进行汇编语言等价形式的转换,并将转换的结果进行输出。在这里介绍两种常见的汇编语言格式:AT&T和Intel。
GCC采用的是AT&T的汇编格式, 也叫GAS格式(Gnu ASembler GNU汇编器), 而微软采用Intel的汇编格式。语法上主要有以下几个不同:
寄存器命名原则
在 AT&T 汇编格式中,寄存器名要加上 ‘%’ 作为前缀;而在 Intel 汇编格式中,寄存器名不需要加前缀。
源/目的操作数顺序
在 Intel 汇编格式中,目标操作数在源操作数的左边;而在 AT&T 汇编格式中,目标操作数在源操作数的右边。
常数/立即数的格式
在 AT&T 汇编格式中,用 ‘$’ 前缀表示一个立即操作数;而在 Intel 汇编格式中,立即数的表示不用带任何前缀。
操作数长度标识
在 AT&T 汇编格式中,操作数的字长由操作符的最后一个字母决定,后缀’b’、’w’、’l’分别表示操作数为字节(byte,8 比特)、字(word,16 比特)和长字(long,32比特);而在 Intel 汇编格式中,操作数的字长是用 “byte ptr” 和 “word ptr” 等前缀来表示的。
跳转指令
在 AT&T 汇编格式中,绝对转移和调用指令(jump/call)的操作数前要加上’ * ‘作为前缀,而在 Intel 格式中则不需要。
第四步,重复上述过程至结束
输出一条指令后,继续反汇编下一条指令,并重复上述过程,直至反汇编完文件中的所有指令。
线性扫描反汇编
原理:顺序分析,一条指令结束,另一条指令开始。
算法步骤
1.位置指针lpStart指向代码段开始处
2.从lpStart位置开始尝试匹配指令,并得到指令长度n
3.如果2成功,则反汇编(Intel风格或者AT&T风格)从lpStart之后n个数据;如果失败,则退出
4.位置指针lpStart赋值为lpStart+n,即上条指令的结尾
5.判断lpStart是否超过了代码段结尾处,如果超出则结束。如果不超出则继续2过程。
线性扫描算法的主要优点,在于它能够完全覆盖程序的所有代码段。线性扫描方法的一个主要缺点,是它没有考虑到代码中可能混有数据。
GNU调试器(gdb)、微软公司的WinDbg调试器和objdump实用工具的反汇编引擎均采用线性扫描算法。
递归下降反汇编
原理:根据一条指令是否被另一条指令引用来决定是否对其进行反汇编。
分类:
1.顺序流指令:直接解析它后面的下一条指令,如MOV、PUSH、POP
2.条件分支指令:解析它的所有条件路径,如JNZ
3.无条件分支指令:反汇编器会尝试定位到跳转的目标,但有可能失败(如JMP EAX,EAX在静态环境下无法确认)
4.函数调用指令:和无条件分支指令相似,如CALL EAX
5.返回指令:由于RET返回的地址实际是从栈中取得的,但反汇编器不可能访问到栈,所以就会终止
递归下降算法的主要优点是可以区别代码和数据,缺点是它无法处理间接代码路径。
IDA Pro是一种最为典型的递归下降反汇编器。
总结
通过上面的分析可以得出,在目前,没有一个反汇编器可以输出完全正确的结果,但IDA和其他的汇编器不同,它可以提供大量机会来指导和推翻它的决定,最终得到正确的反汇编。这本书还介绍了许多IDA的拓展功能,比如处理器模块就和本阶段的任务有关,之后会进行了解。