首页
论坛
课程
招聘
[原创]打造自己的反汇编引擎——Intel指令编码学习报告(二)
2008-10-22 13:28 21464

[原创]打造自己的反汇编引擎——Intel指令编码学习报告(二)

2008-10-22 13:28
21464
第一部分:总体介绍

    反汇编引擎的目的就是要把机器码翻译成汇编语言的格式,主要的汇编格式有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)分别介绍各个部分的结构和解析方法,最后再介绍如何利用这些部分的解析子程序解析不同的指令,最终实现一个反汇编引擎。

[招生]科锐逆向工程师培训46期预科班将于 2023年02月09日 正式开班

上传的附件:
收藏
点赞0
打赏
分享
最新回复 (16)
雪    币: 7510
活跃值: 活跃值 (412)
能力值: ( LV9,RANK:610 )
在线值:
发帖
回帖
粉丝
achillis 活跃值 15 2008-10-22 13:42
2
0
抢个沙发继续支持楼主!
雪    币: 104
活跃值: 活跃值 (10)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
Aleaxander 活跃值 1 2008-10-22 14:35
3
0
哇,这么快就有续章了。。。
再顶!
雪    币: 96
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
cherryfly 活跃值 2008-10-22 15:05
4
0
常年关注....
雪    币: 510
活跃值: 活跃值 (28)
能力值: ( LV12,RANK:220 )
在线值:
发帖
回帖
粉丝
xiep 活跃值 5 2008-10-24 10:44
5
0
(3) argument1, argument2, argument3:实际上intel指令最多也只有三个操作码,当只有两个操作码的时候,第一个为目标操作码,第二个为源操作码。

“操作码”改为“操作数”更容易理解些。
当只有两个操作码的时候,第一个为目标操作码,第二个为源操作码,这个也不一定,还受d位的影响。
雪    币: 510
活跃值: 活跃值 (28)
能力值: ( LV12,RANK:220 )
在线值:
发帖
回帖
粉丝
xiep 活跃值 5 2008-10-24 11:42
6
0
int main()
{
        unsigned int BaseAddress;
        unsigned char *pCurrent;
        unsigned char *pLast;
        INSTRUCTION stInstrcution;
        char strInstrcution[MAX_INSTRUCTION_LEN];

        BaseAddress = 0x401000;
        pCurrent = pLast = Code;

        while(pLast - Code <= sizeof(Code)) //该处地址越界
改为:
        while(pLast - Code < sizeof(Code)-1)刚好
雪    币: 64
活跃值: 活跃值 (10)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
doking 活跃值 2008-10-24 12:08
7
0
学习,很期待能其他领域也可以反汇编
雪    币: 138
活跃值: 活跃值 (58)
能力值: ( LV9,RANK:370 )
在线值:
发帖
回帖
粉丝
egogg 活跃值 9 2008-10-24 12:53
8
0
这个我知道,这里说的是操作码(还是用操作码吧,因为数总给人一种数字的感觉,至少我容易把操作数和立即数弄混,毕竟操作的对象并不数字),是指汇编格式中的操作码,而不是实际编码过程中的。
比如:mov argument1, argument2
这里位于argument1位置的为目标,而位于argument2位置的为源,这就像是固定了的几个槽,槽里可以随意放东西。这里说的就是固定了的槽的属性,而你说的d位影响的只是放在哪个槽里的问题。

这虽然只是个概念的问题,但是跟反编译器的实现是密切相关的。因为我是这样设计反编译后的汇编指令的:
prefix mnemonic argument1, argument2, argument3
这个格式是固定的(就像一个槽),解析指令的过程中,只要把各个部分填充,按照这个顺序和并起来就行了。所以我在解析过程中一些参数的传递是有考虑的,argument1就是指这个固定的argument1, argument2就是指固定的argument2,解析函数再根据d位,或其他方式来确定,哪个结果存放在哪个目标串中。

当然,这个只是我个人的设计,把指令各个部分分解,分别考虑,再合成,能简化解析过程。
雪    币: 138
活跃值: 活跃值 (58)
能力值: ( LV9,RANK:370 )
在线值:
发帖
回帖
粉丝
egogg 活跃值 9 2008-10-24 13:02
9
0
[QUOTE=xiep;525423]int main()
{
        unsigned int BaseAddress;
        unsigned char *pCurrent;
        unsigned char *pLast;
        INSTRUCTION stInstrcution;
        char strInstrcution[MAX_IN...[/QUOTE]

这个越界的问题我的确没有考虑好,你说的这个减去1刚刚好也不一定,现在把无法识别的指令都是一个字节,假如以后能解析指令了,数组中的最后一个字节刚好是一个多字节指令(比如5个字节)指令的开始,那么越界的问题还是会存在的。

所以问题的关键是,读指令之前不知道最后一条指令的长度,如果想要防止越界发生,我想可以减去指令的最大长度,sizeof(INSTRUCTION)——也就是大致为目前的INSTRUCTION的大小或者精确的(4bytes(prefix)+3bytes(Opcode)+1byte(ModRM)+1byte(SIB)+4bytes(displacement) +4bytes(immediate) = 17bytes)。
这样才正确,不过code[]的长度也必须大于17bytes才能在其中取到指令。
雪    币: 510
活跃值: 活跃值 (28)
能力值: ( LV12,RANK:220 )
在线值:
发帖
回帖
粉丝
xiep 活跃值 5 2008-10-24 13:02
10
0
哈哈,个人有个人的习惯,不过Masm汇编经常说操作数操作数,我已经习惯这个称呼了
雪    币: 110
活跃值: 活跃值 (254)
能力值: ( LV13,RANK:1050 )
在线值:
发帖
回帖
粉丝
combojiang 活跃值 26 2008-10-29 10:04
11
0
好文一定要顶。
雪    币: 208
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
ooseven 活跃值 2009-1-24 09:32
12
0
尊敬的egogg:
      拜读了您的大作打造自己的反汇编引擎——Intel指令编码学习报告,受益良多。非常感谢您将自己专研的知识与其他人分享。
   我有个疑问,不知道您能不能在百忙之中抽出点时间来辅导一下。关于您文章中提到的opcode中的s位的意思我看懂了。但是还有两个问题请您帮我分析一下
   第一:81,c0,f8  add eax,f8此时cpu是不是把f8当成8来看待?
         83,c0,f8  add eax,f8 此时cpu是不是把f8当成-8来看待?
   第二:如何控制汇编编译器,使它为指令(add eax,正或负的数值)分别生成81,或83的opcode的机器码?并且这样做有意义吗?
雪    币: 78
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
颓废的猫 活跃值 2010-1-20 16:29
13
0
学习了。。。。。
雪    币: 207
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
钟小欣 活跃值 2010-7-15 16:12
14
0
必须得顶,好好学习。
雪    币: 5
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
qyffree 活跃值 2010-12-15 12:10
15
0
学习了 谢谢楼主分享
雪    币: 172
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
三草儿 活跃值 2012-12-24 11:17
16
0
...(1) lable:标签,表面意思就是这条指令的一个指代...
...Svin给出了一个简单的程序,在Ollydgb中当作“白纸”来用...
--------------debug a bug---------------------
"lable" -- label
"Ollydgb"--Ollydbg
雪    币: 181
活跃值: 活跃值 (446)
能力值: (RANK:290 )
在线值:
发帖
回帖
粉丝
viphack 活跃值 4 2012-12-24 11:33
17
0
mark ~~~
游客
登录 | 注册 方可回帖
返回