1.3 反汇编引擎的工作原理

通过以上的小例子,相信读者已经发现OllyDBG和IDA都有一个很重要的功能:反汇编。现在为大家讲解一下反汇编引擎的工作原理。

在X86平台下使用的汇编指令对应的二进制机器码为Intel指令集—Opcode。

Intel指令手册中描述的指令由6部分组成,如图1-13所示。

图 1-13 Intel指令结构图

结构图说明如下。

Instruction Prefixes:指令前缀

指令前缀是可选的,作为指令的补助说明信息存在,主要用于以下4种情况。

重复指令:如REP、REPE\REPZ

跨段指令:如MOV DWORD PTR FS:[XXXX],0

将操作数从32位转为16位:如MOV AX, WORD PTR DS:[EAX]

将地址从16位转为32位:如MOV EAX, DWORD PTR DS:[BX+SI]

Opcode:指令操作码

Opcode为机器码中的操作符部分,用来说明指令语句执行什么样的操作,如某条汇编语句是MOV、JMP还是CALL。Opcode为汇编指令语句的主要组成部分,是必不可少的。对Opcode的解析也是反汇编引擎的主要工作。

汇编指令助记符与Opcode是一一对应的关系。每一条汇编指令助记符都会对应一条Opcode码,但由于操作数类型不同,所占长度也不相同,因此对于非单字节指令来说,解析一条汇编指令单凭Opcode是不够的,还需要Mode R/M、SIB、Displacement的帮助,才能够完整地解析出汇编信息。

Mode R/M:操作数类型

Mode R/M是辅助Opcode解释汇编指令助记符后的操作数类型。R表示寄存器,M表示内存单元。Mode R/M占一个字节的固定长度,如图1-14所示。第6、7位可以描述4种状态,分别用来描述第0、1、2位是寄存器还是内存单元,以及3种寻址方式。第3、4、5位用于辅助Opcode。

SIB:辅助Mode R/M,计算地址偏移

SIB的寻址方式为基址+变址,如MOV EAX, DWORD PTR DS:[EBX+ECX*2],其中的ECX、乘数2都是由SIB来指定的。SIB的结构如图1-15所示。SIB占1个字节大小,第0、1、2位用于指定作为基址的寄存器;第3、4、5位用于指定作为变址的寄存器;第6、7位用于指定乘数,由于只有两位,因此可以表示4种状态,这4种状态分别表示乘数为1、2、4、8。

Displacement:辅助Mode R/M,计算地址偏移

Displacement用于辅助SIB,如MOV EAX, DWORD PTR DS:[EBX+ECX*2+3]这条指令,其中的“+3”是由Displacement来指定的。

Immediate:立即数

用于解释指令语句中操作数为一个常量值的情况。

图 1-14 Mode R/M图解

图 1-15 SIB图解

反汇编引擎通过查表将由以上6种方案组合而成的机器指令编码,解释为对应的汇编指令,从而完成了机器码的转换工作。本节将介绍一款成熟的反汇编引擎Proview的开源代码,其源码片段如代码清单1-3所示。

代码清单1-3 Proview的源码片段


//机器码解析函数

/*

DISASSEMBLY结构说明

typedef struct Decoded

{

char Assembly[256];//汇编指令信息

char Remarks[256];//汇编指令说明信息

char Opcode[30];//Opcode机器码信息

DWORD Address;//当前指令地址

BYTE OpcodeSize;//Opcode机器码长度

BYTE PrefixSize;//指令前缀长度

}DISASSEMBLY;

*/

void Decode(DISASSEMBLY*Disasm,

char*Opcode,

DWORD*Index)

{

/*

源码中函数说明信息略

源码中变量局部定义略

*/

//机器码格式分析略

//判断是否符合Opcode机器码格式Op为参数Opcode[0]项

switch(Op)//分析Op对应的机器码

{//部分PUSH指令分析机器码信息,对照图1-2

case 0x68:

//方式1:PUSH 4字节内存地址信息

{

//判断寄存器指令前缀

if(RegPrefix==0){

//PUSH指令后按4字节方式解释

//如当前机器码为:6800304000

//由于在内存中为小尾方式排序,因此取出内容需要重新排列数据

//此函数对指令地址加1,偏移到00304000处,将其排序为00403000

//提取出的机器指令存放在dwOp中

//转换后的地址信息保存在dwMem中

SwapDword((BYTE*)(Opcode+i+1),&dwOp,&dwMem);

//将机器指令信息转换为汇编指令信息

wsprintf(menemonic, push%08X",dwMem);

//保存汇编指令语句到Disasm结构中,用于返回

lstrcat(Disasm->Assembly, menemonic);

//组装机器码信息,用空格将指令码与操作数分离

wsprintf(menemonic,68%08X",dwOp);

//将机器码信息保存到Disasm结构中,用于返回

lstrcat(Disasm->Opcode, menemonic);

//设置指令要占用的内存空间

Disasm->OpcodeSize=5;

//设置指令前缀长度

Disasm->PrefixSize=PrefixesSize;

//对当前分析指令地址下标加4字节偏移量

(*Index)+=4;

}

else{

//PUSH指令后按2字节方式解释

//解析机器码,与以上代码相同

SwapWord((BYTE*)(Opcode+i+1),&wOp,&wMem);

//按2字节解释操作数:"push%04X"

wsprintf(menemonic,"push%04X",wMem);

lstrcat(Disasm->Assembly, menemonic);

//按2字节解释操作数:"push%04X"

wsprintf(menemonic,"68%04X",wOp);

lstrcat(Disasm->Opcode, menemonic);

//设置指令长度

Disasm->OpcodeSize=3;

//设置指令前缀长度

Disasm->PrefixSize=PrefixesSize;

//对当前分析指令地址下标加2字节偏移量

(*Index)+=2;

}

}

break;

case 0x6A:

//方式2:PUSH指令的操作数是小于等于1字节的立即数

{

//有符号数判断,负数处理

if((BYTE)Opcode[i+1]>=0x80){

//负数在内存中为补码,用0x100-补码得回原码

//"push-%02X"中对原码加负号

wsprintf(menemonic,"push-%02X",(0x100-(BYTE)Opcode[i+1]));

}

//有符号数判断,正数处理

else{

//正数直接转换

wsprintf(menemonic,"push%02X",(BYTE)Opcode[i+1]);

}

//保存汇编指令语句

lstrcat(Disasm->Assembly, menemonic);

//组装机器码信息

wsprintf(menemonic,"6A%02X",(BYTE)*(Opcode+i+1));

//保存机器码信息

lstrcat(Disasm->Opcode, menemonic);

//设置指令长度与指令前缀长度

Disasm->OpcodeSize=2;

Disasm->PrefixSize=PrefixesSize;

//对当前分析指令地址下标加2字节偏移量

++(*Index);

}

break;

//机器码格式分析略


代码清单1-3中省略了其他机器码的解析过程,只列举了汇编助记符PUSH的两种机器指令方式。通过解析Opcode指令操作码,找到对应的解析方式,将机器码重组为汇编代码。通过第一个参数DISASSEMBLY*Disasm传出解析结果。将机器码指令长度由参数Index传出,用于寻找下一个Opcode指令操作码。如何使用函数Decode对机器码进行分析见代码清单1-4.。

代码清单1-4 使用反汇编引擎解析机器码


//假设此字符数组为机器指令编码

unsigned char szAsmData[]={

0x6A,0x00,//PUSH 00

0x68,0x00,0x30,0x40,0x00,//PUSH 00403000

0x50,//PUSH EAX

0x51,//PUSH ECX

0x52,//PUSH EDX

0x53//PUSH EBX

};

char szCode[256]={0};//存放汇编指令信息

unsigned int nIndex=0;//每条机器指令的长度,用于地址偏移

unsigned int nLen=0;//分析机器码总长度

unsigned char*pCode=szAsmData;

//获取分析机器码长度

nLen=sizeof(szAsmData);

while(nLen)

{

//检查是否超出分析范围

if(nLen<nIndex)

{

break;

}

//修改pCode偏移

pCode+=nIndex;

//解析机器码,此函数实现见代码清单1-5

//参数一pCode:分析机器码首地址

//参数二szCode:返回值,保存解析后的汇编指令语句信息

//参数三nIndex:返回值,保存机器码指令的长度

//由于参数四是模拟机器码,没有对应代码地址,因此传入0

Decode2Asm(pCode, szCode,&nIndex,0);

//显示汇编指令

puts(szCode);

memset(szCode,0,sizeof(szCode));

}


通过函数Decode2Asm,启动反汇编引擎Proview,通过代码清单1-3中的分析流程,解析出对应汇编指令语句代码,并输出。PUSH寄存器指令的分析并没有在代码清单1-3中列举,分析过程大致相同,读者可查看Proview源码并自行分析。

代码清单1-5 Decode2Asm实现流程


void__stdcall

Decode2Asm(IN PBYTE pCodeEntry,//分析Opcode地址,无符号字符型指针

OUT char*strAsmCode,//传出值,保存汇编指令的语句信息

OUT UINT*pnCodeSize,//传出值,保存机器码指令的大小

UINT nAddress)//分析机器码所在地址

{

DISASSEMBLY Disasm;//此结构信息见代码清单1-3

//保存Opcode指针,用于传递函数参数

char*Linear=(char*)pCodeEntry;

//初始化指令长度

DWORD Index=0;

//设置机器码所在地址

Disasm.Address=nAddress;

//初始化Disasm

FlushDecoded(&Disasm);

//调用Decode进行机器码分析

Decode(&Disasm,

Linear,

&Index);

//保存汇编指令语句信息

strcpy(strAsmCode, Disasm.Assembly);

//组装汇编语句的字符串,从参数strAsmCode返回信息

if(strstr((char*)Disasm.Opcode,":"))

{

Disasm.OpcodeSize++;

char ch='';

strncat(strAsmCode,&ch, sizeof(char));

}

strcat(strAsmCode, Disasm.Remarks);

*pnCodeSize=Disasm.OpcodeSize;

FlushDecoded(&Disasm);

return;

}


代码清单1-5对汇编引擎Proview的使用进行了封装,以简化Decode函数的调用过程,方便使用者调用。本节源码见随书文件,在工程Disasm_Push目录下,其中Disasm、Dsasm_Functions为Proview的源码,Decode2Asm为使用封装代码。

更多关于汇编指令及其对应机器码的信息请参考Intel的指令帮助手册,读者可在Intel的官方网站下载最新版的帮助手册。另外,随书文件中还提供了一个低版本的Intel指令帮助手册。