- C++反汇编与逆向分析技术揭秘(第2版)
- 钱林松 张延清
- 2529字
- 2021-09-27 17:05:09
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指令帮助手册。