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指令帮助手册。