- UID
 - 2198
 
 注册时间2005-6-29
阅读权限255
最后登录1970-1-1
副坛主 
    
 
 
 
该用户从未签到  
 | 
 
标 题: 【原创】ring 3级32位x86 cpu仿真 
作 者: linxer 
时 间: 2007-04-08,16:47 
链 接: http://bbs.pediy.com/showthread.php?threadid=42343 
 
【  标题  】 ring 3级32位x86 cpu仿真 
【  作者  】 linxer 
【  Q Q   】 3568599 
【  声明  】 俺系初级选手,高手略过。失误之处敬请诸位大侠赐教! 
 
题外话:本文系本人最近编码的总结,目前初步完成了ring 3级别x86 cpu的仿真,但由于这个东西还跟很多模块关联在一起,这些模块我都还没有写,因此,我写的这个虚拟的ring 3 x86 cpu的代码也还没有进行功能性等测试,目前只是能编译过去而已。本文旨在说明如何仿真,不在于show代码,不过为了说明问题,会引用一些代码,这些代码来源于那些未测试的代码,有意往下看的,请抱着一颗发掘代码bug的心,抱歉! 
 
前段时间已经发过一篇关于如何识别x86机器码的文章,机器码识别出来了,接下来就是要交给仿真的cpu执行,这里给出一个相对简单的cpu的实现。 
 
首先说下,这里为什么只仿真ring 3级别的一些功能?这是由于仿真的cpu上不能安装OS决定的,如果要用到一些ring 0的功能那怎么办呢,就只能仔细分析这个ring 0的功能,通过其它方式仿真出来了。 
 
这个仿真的cpu包含以下三个部分:cpu环境,寻址系统和指令解析系统。 
 
一.cpu环境 
 
必要的宏定义 
 
#define u8 unsigned char 
#define s8 char 
 
#define u16 unsigned short int 
#define s16 short int 
 
#define u32 unsigned int 
#define s32 int 
 
1. 8个普通寄存器在x86上按如下顺序索引 
typedef enum tagCommonRegIndex 
{ 
  EAX = 0, 
  ECX, 
  EDX, 
  EBX, 
  ESP, 
  EBP, 
  ESI, 
  EDI 
}CommonRegIndex; 
 
2. 6个段寄存器在x86上按如下顺序索引 
typedef enum tagSegmentRegIndex 
{ 
  ES = 0, 
  CS, 
  SS, 
  DS, 
  FS, 
  GS 
}SegmentRegIndex; 
 
3. 定义标志寄存器有用位的索引 
这里并没有按照intel CPU的格式来定义,主要是出于效率考虑,因为程序执行过程中有大量的跳转语句,这些语句都要用到条件位,用这种方式比用一个unsigned long来定义标志寄存器,每次可以节省一个&操作,由于在仿真的cpu上执行程序,效果是个大问题,因此这里在效率问题上也是“寸土必争”的;另外,我还把常用的条件位放在一起了,这也与x86 cpu不同,这里只要是为了减少cache miss情况,不过应该收效甚微的 
 
这种定义方式就注定了标志寄存器操作指令的特殊性,幸好与标志寄存器相关的指令只要仿真四条(pushf/sahf/popf/lahf) 
typedef enum tagFlagReg 
{ 
  CF = 0, 
  ZF, 
  SF, 
  OF, 
  DF, 
  PF, 
  AF, 
  TF, 
  IF 
}FlagReg; 
 
4. 普通32位寄存器结构声明 
用这个结构可以比较方便的访问普通寄存器里包含的"小寄存器",比如说eax中的ax/ah/al 
typedef union tagCommonReg 
{ 
  s32 nAll; 
  s16 _x; 
  struct tag8CommonReg 
  { 
    s8 _l; 
    s8 _h; 
  }_8; 
}CommonReg, *PCommonReg; 
 
5. cpu环境 
typedef struct tagCPUEnvernment 
{ 
  CommonReg  commonReg[8];  //8个普通32位寄存器 
  u32    eip;    //eip寄存器 
  CommonReg  segReg[6];  //段寄存器,这里其实用s16就可以啦,段寄存器虽然有48bit,但是我们可见的只有16bit 
  u8    flagReg[9];  //要用到的9个标志寄存器位 
}CPUEnvernment, *PCPUEnvernment; 
 
为了以下的应用,这里定义一个cpu环境变量: 
CPUEnvernment g_cpu; 
 
 
二.寻址系统 
 
这里不含寄存器的寻址,因为寄存器的寻址可以很轻松搞定,一般都在opcode和ModR/M包含了。这里特指内存寻址。 
 
对32位的cpu来说,含有16位寻址和32位寻址,具体用的是那种寻址方式,由每条指令的寻址大小前缀码指定,不过现在好像都是用32位寻址啦。 
 
1. 16位寻址 
 
要内存寻址的指令,指令一定会有modR/M字节,这个字节会标识该如何寻址,在16位寻址的指令中,是没有sib字节的,这样使得16位寻址仿真起来也相对简单些。比如,当mod=00,R/M=000的时候,它表示[bx + si],我们可以由g_cpu.commonReg[EBX]._x + g_cpu.commonReg[ESI]._x得到程序执行过程中用到的虚拟地址。具体情况可以参考Intel官方手册第二卷的Intel Instruction Set Reference(A-M)部分的36页。 
 
2. 32位寻址 
 
32位寻址比16位寻址要复杂的多,要32内存寻址的指令中一定会有modR/M字节,可能会有sib字节。 
 
在R/M不为100的时候,表示指令中不含sib字节,它的情况跟16位寻址没有什么差别,比如,当mod=00,R/M=000的时候,它表示[eax],我们可以由g_cpu.commonReg[EAX].nAll得到程序执行过程中用到的虚拟地址。具体情况可以参考Intel官方手册第二卷的Intel Instruction Set Reference(A-M)部分的37页。 
 
在R/M=100的时候,那么指令就含有sib字节了,sib字节主要用来支持一些象数组样的寻址,比如说sib=01 000 001,由它表示的是[ecx + eax * 2],我们可以由g_cpu.commonReg[ECX].nAll + g_cpu.commonReg[EAX].nAll * 2得到sib字节表示的虚拟地址,然后在和mod字段配合,加上一定的偏移,就可以得到最终的虚拟地址了。具体情况可以参考Intel官方手册第二卷的Intel Instruction Set Reference(A-M)部分的38页。 
 
3. 地址转换 
由上面得到的地址都是虚拟地址,那怎么获得这个虚拟地址实际表示的操作数呢,即怎样获得它在真实cpu上的虚拟地址呢,这个转换应该可以说比较简单的,在程序在仿真cpu上执行前,我们要有个PE load的过程,在这个过程中,我们可以知道,这个pe文件的ImageBase跟真实加载到内存中的起始地址的差额,用这个差额就可以完成这种地址转换了。 
 
 
三.指令解析系统 
 
写这个模块是最枯燥的,不过这也取决于你要虚拟多少条指令,基本上是每个指令要有一个专门的解析函数,因此工作量是挺大的,体力活! 
 
对指令的解析可以用两种方法来做到: 
 
1. 关键地方内嵌汇编 
因为对条件位的设置比较麻烦,我们可以用真实的cpu来搞定这些工作,这样轻松简洁,出错概率也减小不少,拿opcode=0x3c来说,它是cmp指令,它的一个参作数在eax中,第二个参作数是一个立即数 
 
s32 cmp_rac_imm_fun() 
{ 
  s32 lFirst; 
  s32 lSecond; 
  s16 sFirst; 
  s16 sSecond; 
  s8 cFirst; 
  s8 cSecond; 
 
  u8 cCF = 0; //这里目前只仿真了4个条件位,初始默认为0 
  u8 cOF = 0; 
  u8 cSF = 0; 
  u8 cZF = 0; 
   
  if(操作数大小是4) //将最可能执行到的条件放前面,以提高效率 
  { 
    lFirst = g_cpu.commonReg[EAX].nAll; 
    lSecond = 立即数; 
    _asm 
    { 
      mov esi, lFirst; 
      cmp esi, lSecond; 
      jnc cmp_rac_imm_cf_1; 
      mov cCF, 1; 
cmp_rac_imm_cf_1: 
      jno cmp_rac_imm_of_1; 
      mov cOF, 1; 
cmp_rac_imm_of_1: 
      jne cmp_rac_imm_zf_1; 
      mov cZF, 1; 
cmp_rac_imm_zf_1: 
      jns cmp_rac_imm_sf_1; 
      mov cSF, 1; 
cmp_rac_imm_sf_1: 
    } 
  } 
  else if(操作数大小是2) 
  { 
    sFirst = g_cpu.commonReg[EAX]._x; 
    sSecond = 立即数; 
    _asm 
    { 
      mov si, sFirst; 
      cmp si, sSecond; 
      jnc cmp_rac_imm_cf_2; 
      mov cCF, 1; 
cmp_rac_imm_cf_2: 
      jno cmp_rac_imm_of_2; 
      mov cOF, 1; 
cmp_rac_imm_of_2: 
      jne cmp_rac_imm_zf_2; 
      mov cZF, 1; 
cmp_rac_imm_zf_2: 
      jns cmp_rac_imm_sf_2; 
      mov cSF, 1; 
cmp_rac_imm_sf_2: 
    } 
  } 
  else if(操作数大小是1) 
  { 
    cFirst = g_cpu.commonReg[EAX]._8._l; 
    cSecond = 立即数; 
    _asm 
    { 
      mov dh, cFirst; 
      cmp dh, cSecond; 
      jnc cmp_rac_imm_cf_3; 
      mov cCF, 1; 
cmp_rac_imm_cf_3: 
      jno cmp_rac_imm_of_3; 
      mov cOF, 1; 
cmp_rac_imm_of_3: 
      jne cmp_rac_imm_zf_3; 
      mov cZF, 1; 
cmp_rac_imm_zf_3: 
      jns cmp_rac_imm_sf_3; 
      mov cSF, 1; 
cmp_rac_imm_sf_3: 
    } 
  } 
   
  g_cpu.flagReg[ZF] = cZF; //修正仿真cpu中的某些条件位 
  g_cpu.flagReg[OF] = cOF; 
  g_cpu.flagReg[CF] = cCF; 
  g_cpu.flagReg[SF] = cSF; 
   
  g_cpu.eip += 指定大小; //eip指向下条待执行指令 
   
  return 0; 
} 
 
当然这种方法对某些指令的仿真是行不通的。 
 
2. 完全手工模拟 
还以opcode=0x3c为例,这里简单点说明,略过很多情况 
 
s32 cmp_rac_imm_fun() 
{ 
  s32 lFirst; 
  s16 sFirst; 
  s8 cFirst; 
 
  //这里为了说明问题,阿拉就不模拟对OF,CF的影响了,我怕麻烦 
   
  if(操作数大小是4) //将最可能执行到的条件放前面,以提高效率 
  { 
    lFirst = g_cpu.commonReg[EAX].nAll - 立即数; 
    g_cpu.flagReg[ZF] = (lFirst == 0); 
    g_cpu.flagReg[SF] = (lFirst < 0); 
  } 
  else if(操作数大小是2) 
  { 
    sFirst = g_cpu.commonReg[EAX]._x - 立即数; 
    g_cpu.flagReg[ZF] = (sFirst == 0); 
    g_cpu.flagReg[SF] = (sFirst < 0); 
  } 
  else if(操作数大小是1) 
  { 
    cFirst = g_cpu.commonReg[EAX]._8._l - 立即数; 
    g_cpu.flagReg[ZF] = (cFirst == 0); 
    g_cpu.flagReg[SF] = (cFirst < 0); 
  } 
   
  g_cpu.eip += 指定大小; //eip指向下条待执行指令 
   
  return 0; 
} 
 
通过以上两个方法,应该是可以搞定所有ring 3级别的指令的解析的。 
 
 
 
 
 
下面说下,如何将识别出来的机器码跟该机器码对应的解析函数关联起来:我们在x86机器码的识别过程,可以给每个识别出来的opcode一个id,而其相映的解析函数也用这个id,然后将所有解析函数放入函数指针数组中,并通过这个id索引,这样就可以从opcode快速找到相映的解析函数了。 
 
 
就写到这里吧,这个仿真cpu的功能还很简单,还有待加强......  BY:笨笨雄 
 
个人建议,在x86平台上仿真x86,没有必要识别所有指令,建立数组,标记带modr\m byte和string类型的opcode。重定向寻址指令就行了 
牛人写代码就是快,我构思VM比你早,但是你的框架写出来了,我还在想实现思路  
不过我的是用在加密上的,而不是解密 
 
 
首先是解释modr\m byte,如果是不带寄存器的寻址,直接改地址就好了 
 
如果是带寄存器寻址的,可以在代码运行缓冲区中改写这条指令。举一个简单的例子吧 
 
假设有以下代码 
 
mov eax,00400000 
xor ecx,ecx 
mov edx,dword ptr [eax+4*ecx] 
 
前两条指令不带modr\m byte的,在计算完指令长度之后,可以将一条完整的指令复制到代码执行缓冲区中。第三条指令,由于寻址是不确定的。因此在代码执行缓冲区中写入一条jmp到自己虚拟机里的。也就是缓冲区被写成下面的样子: 
 
mov eax,00400000 
xor ecx,ecx 
jmp VM_reAddr_proc 
 
然后按照实际情况的eax和ecx便可以知道确切的值了。计算实际加载地址与默认地址的差值,然后加上去不就好了。如果你担心程序自己申请内存,并对跟内存寻址的情况。那么你在模拟分配内存函数的时候,将申请地址的返回值,也加上那个差值。便可以用统一的算法解决所有寻址问题了。计算出程序原本需要访问地址,生成 
 
mov reg32,dword ptr [xxxxxxxx] 
 
注意环境保护,你可以用真实CPU的寄存器。切换堆栈也就mov esp,dword ptr [xxxxxxx]。 
 
在最初构思的时候,我是想如何提高虚拟机速率的。不过最近发现,编译器使用了大量带modr\m byte的代码。这样的方法不见得能提高多少性能。当然了,这跟我的方向不同,没深入想过  |   
 
 
 
 |