打造自己的反汇编引擎Intel指令编码学习报告.docx
《打造自己的反汇编引擎Intel指令编码学习报告.docx》由会员分享,可在线阅读,更多相关《打造自己的反汇编引擎Intel指令编码学习报告.docx(68页珍藏版)》请在冰点文库上搜索。
打造自己的反汇编引擎Intel指令编码学习报告
打造自己的反汇编引擎——Intel指令编码学习报告
作者:
egogg
时间:
2008-10-22,13:
28
写在前面:
学习Intel指令格式已经有近一个月了,本来想把整个反汇编引擎写完整之后再发布源代码和学习报告的,但是,最初的热情过后,剩下的就是辛苦劳动了,现在实在太累了,似乎有点写不下去的感觉了,所以我还是打算,边总结学习的过程,边完成整个反汇编引擎:
一方面,希望论坛里对指令解码知识感兴趣的朋友,高手给些鼓励;另一方面,希望能和这些朋友们讨论程序中的bug,讨论整个反汇编引擎的架构(这个我会在后面的学习报告中详细说明我所了解的一些架构)。
学习新的知识是一件很令人高兴和满足的事情,但是能和别人分享学习的经验,更令人快乐。
学习指令编码格式的好处有很多,我在这里提一些吧:
一、加深对指令的了解。
并不是用汇编语言写出的程序速度就一定比其他高级语言快,或者说节省空间,现在大部分的编译器做得比一般的汇编语言初学者,甚至是有一定编程经验的人都好,对一些汇编指令有所了解后,可能利用这些指令写出符合特定条件的好的代码,不管是用在shellcode还是用在关键代码的性能优化方面都有好处。
例如:
Svin的教程中就有一个题目:
用四个byte实现下列的算法:
(opcode hack)
IF ZF=1
inc eax
ELSE
mov al,40
再有,现在的高级语言因为执行效率的原因,一般都舍弃用leave和enter指令,然而这些指令有着空间的优势……等等,此外,学习了指令编码之后会对intel的寻址模式有一个更为深刻的了解。
二、学习了指令编码可以软件保护中的很多技巧如花指令等有更深刻的了解。
三、如果这些小的技巧实在是不值一提,那么如果想些一个虚拟机架构的话,就必须对这些指令有所了解。
……
由于我是一个菜鸟,所以有很多说不清楚的地方,还希望高手指正,毕竟,讨论才是学习永恒的主题。
本来打算,把这些学习报告发在新手区的,但是看到这个版块有一个专题,就发到这个地方了。
很多高手可能想自己学习研究,我会提前把我找到的所有的资料都列在附件中。
实验反汇编引擎介绍:
(原代码下载:
dasm.rar)
引擎采用了最直观,当然也是最笨拙的方法,switch...case,代码虽然不够简练,但是执行效率和整体结构还是很清楚的,代码的解析和识别只剩下力气活了。
反汇编引擎目前的进度:
基本框架已经实现,能解析的指令大约200多条,2-byte的指令还不能解析,浮点指令和mmx指令的解析都还待完成。
(2008.10.22更新:
已经能正确解析所有常用指令(除特权指令,浮点指令,mmx指令之外的指令))
(2008.10.24更新:
改正了解析C4,C5指令的一个小bug)
测试程序:
(CrackMe.exe为测试用的pe文件)
我用了不久前学习pe文件格式的时候写的pe文件解析代码,提取了.text中的数据作测试用,pe文件的解析部分代码很丑,大家感兴趣的话,可以只看反汇编部分的代码,代码没有加注释,我想我会在后续的学习报告中详细解释每一个部分的代码。
程序的运行方式:
dasmMain.exe 待解析的pe文件 >out.txt 最好重定向到文件中看,输出的结构有点多。
由于有些指令不能识别,导致实际的反汇编代码跟正确的比有些混乱,但是大部分代码还是正确的,我测试的输出结果如图:
跟olly的结果比较一下:
对得不是太齐,但是如果以后做成GUI的形式,因该没有什么问题了。
实际的效果大家可以用不同的pe文件测试,当然,程序只是读文件的.text区,没有任何分析,实际效果,大家还是要对照地址来检查。
学习资料:
1、首推Svin的教程,英文原版:
tutorialofopcodebysvin.rar都是一些保存好的网页,大家可以权当链接使用。
当然,论坛上也有翻译后的版本,大家可以找一找,不过还是推荐到原论坛看原帖。
2、The art of disassembly,英文原版:
ArtOfDisassembly.part1.rarArtOfDisassembly.part2.rarArtOfDisassembly.part3.rar,当然论坛里也有中文版的。
3、罗聪的《学习Opcode教程》:
learningopcode.rar
4、指令列表:
codetable.rar,网上不同的版本很多,但是这个是我见到的做的最好的一份,实际的解码过程也是参照这个表做的,当然同时参考的少不了:
5、Intel® 64 and IA-32 Architectures Software Developer's Manual 2A Instruction Set Reference A-M.pdf
6、Intel® 64 and IA-32 Architectures Software Developer's Manual 2B Instruction Set Reference N-Z.pdf,上面两个就不传上来了,intel的网站上就有。
上面列出来的是能找到的所有关于intel指令编码的资料了,很多教程都不完整,或者没有实现一个真实的反汇编引擎,我想这也是为什么,我想把学习和实现反汇编引擎的经验写出来的原因之一,希望我能写出一个完整的学习过程来。
第一部分:
总体介绍
反汇编引擎的目的就是要把机器码翻译成汇编语言的格式,主要的汇编格式有Intel格式、AT&T格式,一般在window环境中使用的大多数都是intel格式的汇编语言。
这里从官方手册的介绍中总体介绍这两部分的内容,只有知道机器码的格式,汇编指令的格式,才能在其上架起一座桥梁——汇编或反汇编。
这里我们习惯称汇编指令为Intruction operand,而称机器码为Intruction Opcode。
1.1 Intel汇编格式(Instruction operand)
在官方手册中intel汇编有着固定的格式:
label:
mnemonic argument1, argument2, argument3
(1) lable:
标签,表面意思就是这条指令的一个指代,实际代表着这条指令在内存中的起始位置。
(2) 助记符:
用英语代表机器码的操作,汇编器会根据这个助记符寻找合适的机器码。
(3) argument1, argument2, argument3:
实际上intel指令最多也只有三个操作码,当只有两个操作码的时候,第一个为目标操作码,第二个为源操作码。
1.2 intel机器码格式(Instruction Opcode)
汇编语言的格式反映了机器码的编码格式,直观地看,只要给汇编代码的每个部分都分配相应的字节就行了,例如:
mnemonic两个字节,argument1-3分别4个字节,这样汇编语言与机器码之间真的就是直接对应的关系了,在这两个部分转换至需要维持一张简单的表就行了。
但实际上,intel的指令体系为复杂指令系统(CISC),它这里的复杂绝非浪得虚名,由于以往的机器上内存是个很昂贵的设备,因此,intel的指令编码尽可能地利用了每一个bit,再加上兼容性的考虑,使得整个intel指令结构异常复杂。
远远不是一个部分和另一个部分简单的映射那么简单。
物理上,CPU的逻辑运算单元只操作计算机中的两个对象:
寄存器和内存。
只要给每个寄存器一个编码,那么寄存器的辨别就很容易了,但是内存呢?
物理上,内存是个一维的存储单元阵列,逻辑上内存被分成段,页之类的格式,要操作内存,那么指令就要给出操作内存的哪个(哪些)存储单元,这里“哪”指的是寻址模式,这里的“些”和“个”是指要操作的内存的大小,byte, word, dword……。
除了这两个操作对象之外,还有一种对象,那就是立即数(immediate),物理上指令执行时,这个数字是在CPU中的,也就是CPU取得的指令中,这个数就已经在那里了。
所有的指令编码都是围绕着这三个操作对象进行的,不同的是立即数不需要去找,寄存器简单的编码就行了,而内存不但需要指出其位置,还要指出其大小。
此外,还有一些辅助的操作说明,比如是否重复一些操作等等。
看一下intel的确切的指令格式:
prefix部分是指令操作的一些辅助说明,如果先不看prefix部分,其他部分的表面涵义是很明确的:
opcode编码了进行什么样的操作,跟汇编格式里面的mnemonic对应,CPU知道了什么操作之后就会寻找操作的对象,是寄存器还是内存?
ModR/M部分就给出了操作的对象,R是register,M是memory,而Mod指示了到底是寄存器还是内存。
如果ModR/M的字节数足够大的话,那么或许就不需要后面的两个部分了,实际上ModR/M只有一个字节,能编码所有的寄存器,却不能编码所有的内存寻址模式,intel使用后面两个部分来辅助ModR/M完成确切的内存定位——SIB和displacement。
寻址方式跟CPU对内存的管理密切相关,intel的寻址方式很多,但全部都编码到了SIB和displacement之中。
这部分到SIB部分再详细介绍。
内存寻址后面就跟了最后一个操作对象Immediate。
指令编码的整个结构还是很清楚的,但也可以看到,每一个部分都有小的子结构,代表着不同的涵义。
反汇编就是要读懂机器码的每个部分,然后翻译成汇编格式。
在后面的各个部分将把我对各个部分的了解都写出来。
(简单说明一下,关于汇编指令和机器码之间的对应关系[并非一一对应],可以看看Svin的教程opcode#1和罗聪的《学习Opcode》,我想我主要关注的是反汇编引擎的实现细节,这些知识是重要的,但是既然前辈高手都已经写得很清楚了,就没有必要再重复了。
如果真的想了解学习汇编指令格式,无论如何,这两份教程是一定要认真看的。
)
1.3 调试实验环境的简单说明:
就像Svin在教程的开头就写的那样,学习指令格式,最重要的就是实验,动手,看实际的效果。
实际上,要写出反汇编程序出来,首先得学会自己查表手工翻译指令。
不同反汇编引擎的结构不一样,但是都是建立在对Intel指令结构各个部分的理解之上的,而要想熟悉各个部分,必须亲自动手,要多动手(查找资料的时候看到论坛里有些朋友也想实现自己的反汇编引擎,但是不知什么原因却没有动手,其实动手后就会发现,一切都很简单,如果不求代码的优美,我这样的菜鸟都能写出一个)。
(1)指令察看:
如果想知道一个汇编指令对应的机器码,或者说一些机器码对应的汇编指令,最简单的办法就是使用现有的工具,首推Ollydbg。
Svin给出了一个简单的程序,在Ollydgb中当作“白纸”来用(当然也可以随便打开一个pe文件),可以在上面随便输入汇编指令,或机器码,查看对应的翻译。
(程序源代码和可执行程序下载:
blank.rar
可以在汇编栏双击随便输入一些指令,机器码部分就会显示相应的机器码。
或者Ctrl+E在机器码部分随便输入一些机器码,可以在汇编栏看到对应的反汇编指令,大家可以动手做一下。
(2)反编译器测试框架:
用C语言构架吧,想要测试自己的反汇编引擎是否正确工作,首先得假设一个调试环境。
可以使用shellcode中的方式,假设待反编译的指令位于一个字符串中:
Code[] = "\X90\X90\X90\X90"...然后在程序中现解析这些数据,看看效果。
目前,根据上图定义的指令格式可以写出指令的一个结构体来,所有的指令理论上都能解析并存放到这个指令结构的各个部分,这是最直观的定义。
代码:
typedef struct _INSTRUCTION
{
/* prefixes */
char RepeatPrefix;
char SegmentPrefix;
char OperandPrefix;
char AddressPrefix;
/* opcode */
unsigned int Opcode;
/* ModR/M */
char ModRM;
/* SIB */
char SIB;
/* Displacement */
unsigned int Displacement;
/* Immediate */
unsigned int Immediate;
/* Linear address of this instruction */
unsigned int LinearAddress;
} INSTRUCTION, *PINSTRUCTION;
各个部分的大小都是根据指令结构中最大的字节数定义的。
我们再定义一下,反汇编的程序Disassemble(),最直观的就是输入指令的起始地址,返回下一条指令的起始地址(很多反汇编引擎都是返回指令的长度,但返回下一条指令的起始地址更直观),把指令的解析结果放在INSTRUCTION中,把反编译出来的字符串存放在一个缓冲区中。
可以这样定义:
unsigned char *Disassemble(unsigned char *Code, PINSTRUCTION Instruction, char *InstructionStr);
InstructionStr为汇编指令格式,按照intel汇编指令格式的定义我们可以定义如下字符串prefix mnemonic operand1, operand1, operand3,然后把解析的结果分别放入这些字符串中,最后把这些字符串组组合起来就得到了最后的指令。
我们可以仿照The Art of Disassemlby中介绍的,先写出一个字节读取显示的框架程序(源代码下载:
dasm_frame.rar)。
运行结果如图所示:
下面将按照intel指令格式分五个部分
(Prefixes, Opcode, Mod/RM, SIB+Displacement, Immediate)分别介绍各个部分的结构和解析方法,最后再介绍如何利用这些部分的解析子程序解析不同的指令,最终实现一个反汇编引擎。
第三部分:
指令前缀(Prefixes)
指令前缀是解析一条指令之前所要做的第一项工作。
从某种意义上来说,指令前缀是指令操作的辅助说明信息。
什么是指令前缀
指令前缀是作为指令的辅助说明信息的,指令前缀是独立于指令之外的,最熟悉的一个恐怕就是重复指令前缀了repz, repnz...。
这些前缀作用于链式指令之前,可以在一定条件下重复这些操作,看看一个实际的例子,首先打开ollydbg,载入作为“空白纸”的blank.exe,在汇编窗口Ctrl + E输入A4这时你可以看到:
00401000 > A4 movs byte ptr es:
[edi], byte ptr [esi]
这是链式指令之中的一条movsb指令,现在再Ctrl + E输入:
F3 A4看到了吧:
00401001 F3:
A4 rep movs byte ptr es:
[edi], byte ptr [esi]
这里F3就是指令前缀(rep)。
链式指令前缀的位置和作用是很明显的,你也可以使用不同的指令(movsd, stosb等指令和repnz(F2)做一下测试。
如果不知道指令的编码,可以在汇编窗口的汇编栏,双击鼠标,输入对应的汇编指令,就可以在右边的窗口中看到对应的指令编码了)。
早期的intel处理器对内存的管理是采用分段式的,不同的分段由段寄存器来索引,所有的寻址方式跟这种管理策略密切相关。
但是如果把段寄存器信息也编入指令之中,将使寻址编码方式太过复杂,这里intel采用了一种比较简单的方法,那就是对每一种寻址方式都假设一个默认的段,这就是我们学习汇编语言初期所学到的:
bx, si, di等寄存器寻址默认的寄存器为ds;bp,sp等默认的寄存器为ss,ip默认的寄存器为cs...等等(其实默认寄存器也用到了指令编码之中,比如mul,div等指令都默认结果或被除数等为算术寄存器,这样的好处就是,可以大大减少指令大小,然而也同样破坏了指令的一致性)。
这里就存在一个问题,如果我想访问特定的段怎么办?
比如我想用bx, si, di等索引es, fs, gs等怎么办(保护模式之下,段寄存器的内容为段选择子,一般其内容都是由操作系统维护,我们现在用到的机会大大减少)?
intel使用指令前缀来解决这个问题,输入以下指令(ctrl+e输入编码,或双击输入汇编代码):
00401004 8B07 mov eax, dword ptr [edi]
[edi]默认的寻址寄存器为ds,再输入:
00401006 2E:
8B07 mov eax, dword ptr cs:
[edi]
2E在这里就是段改写指令前缀(cs),还有其他的5个段寄存器的改写指令前缀(ss-36H, ds-3EH, es-26H, fs-64H, gs-65H),你可以用这些前缀再任何有内存寻址操作的指令之前,看看效果。
当然还有其他一些指令前缀,后面将一一介绍,这里需要说明一下的是,指令前缀有一个特点,那就是它只针对特定的指令才有效。
例如前面的重复指令,只有当它们用在链式指令之前的时候才有效,你也可以强行把它用在其他指令之前,做以下实验:
00401000 > 40 inc eax
强行输入(Ctrl + E):
00401001 F3:
prefix rep:
00401002 40 inc eax
这里虽然ollydbg识别了这种前缀指令,但是其实cpu还是把它当作同一条指令来执行的,把ollydbg调试选项-〉异常-〉无效或特权指令关掉,运行这条指令。
你会看到并没有重复事件发生。
再来看看段前缀指令,在没有内存寻址的指令中试验一下:
00401003 40 inc eax
00401004 26:
40 inc eax
前缀指令被完全忽略。
intel在官方文档有中这样描述:
such use may cause unpredictable behavior。
所以在解析段前缀指令的时候,一定要主要该段前缀指令使用在何种指令之前。
指令前缀的分类:
在intel指令格式图中对指令前缀的字节数是这样说明的:
Up to four prefixes of 1 byte each (optional)。
很显然rep和repnz是不能同时使用的,intel把这些分为了一组,同样上面介绍的段改写指令也分成了一组。
一条指令之前可以同时包含多个不同的组,不同的组只能使用改组其中一条前缀。
intel指令前缀目前总共有四类,除了前面介绍的两类,还有两类关于操作数大小的指令:
操作数数大小改写指令和地址大小改写指令。
如图:
2.1 重复指令前缀(lock repz repnz)
已经介绍了repz, repnz,还有一条lock前缀,这个前缀是用在多处理器之中的,具体的用途大家可以可以查看intel官方文档的相关的说明。
2.2 段改写指令前缀(cs ds ss es fs gs)
除了上面的说明,还有一点,当指令中有两个默认段(一般会出现在指令中有两个操作对象都为内存的时候)的时候,段改写指令前缀改写哪一个?
例如:
00401000 > A5 movs dword ptr es:
[edi], dword ptr [esi]
加上段改写指令,到底哪个会变?
00401001 26:
A5 movs dword ptr es:
[edi], dword ptr es:
[esi]
结果很明显。
大家可以用其他链式指令做做实验,然后详细读一下svin关于这部分的讲述(opcode6#)。
Svin还有关于win32下段值的改变对寻址结果影响的实验(Opcode6#)。
由于这跟指令编码没有什么关系,这里只是提一下实验结果:
(1)在win32下,所有的cs, ds, ss使用的是同一个端选择子(尽管值不一样?
高手解释一下)。
(2)es的值对一些默认使用该段的指令是有影响的。
比如movs默认的源为:
ds:
[esi]目的为:
es:
[edi],在执行这些指令之前,改变es的值,将会导致不正确的结果。
(3)用户模式下,唯一用得到的段寄存器就是FS了,FS:
[0]指向SEH链,关于这个,研究过系统结构化异常处理的朋友可能都知道。
2.3 操作数大小改写指令:
(66H)
首先做一下试验:
00401006 8B06 mov eax, dword ptr [esi]
00401008 66:
8B06 mov ax, word ptr [esi]
可以看到,66H使得指令的操作数从32位变到了16位。
386时,intel把所有的通用寄存器都增加到了32位,但是intel并没有增加操作这些寄存器的指令,他们解决的办法就是直接使用同16位操作时相同的编码,同时使用操作数大小改写指令作为二者之间的区分。
这里要注意改写的涵义:
当目前cpu工作在16位模式时,66H的出现将使操作变成32位。
也就是cpu工作在16位的时候:
00401006 8B06 mov ax, word ptr [esi]
00401008 66:
8B06 mov eax, dword ptr [esi]
而当cpu工作在32位模式的时候,66H的出现就会直接使得操作数大小都变成16位。
注意,66H只是16位和32位之间的转化指令前缀。
32位和64位之间的区别我没有研究过(谁知道的话,可以说出来大家一起学习一下)。
8位和16位,32位之间的区分就有着本质的区分了。
对比一下:
00401010 8A06 mov al, byte ptr [esi]
00401006 8B06 mov eax, dword ptr [esi]
00401008 66:
8B06 mov ax, word ptr [esi]
可以看到al的指令编