1.7 反汇编引擎的工作原理

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

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

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

图1-35 Intel指令结构图

针对图1-35结构图进行如下说明。

1. 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]。

2. Opcode:指令操作码

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

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

3. Mode R/M:操作数类型

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

图1-36 Mode R/M结构

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

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

图1-37 SIB结构

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

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

6. Immediate:立即数

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

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

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

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

// 假设此字符数组为机器指令编码
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-4
        // 参数一 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-4 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-4对汇编引擎Proview的使用进行了封装,以简化Decode函数的调用过程,方便使用者调用。本节源码见随书文件,在工程Disasm_Push目录下,其Disasm、Disasm_Functions为Proview的源码,Decode2Asm为使用封装代码。

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