也说X86虚拟机(CPU仿真)
作 者: wht0395
注:本人不善言辞,而且写此文也仅算是对本人近期编码和思考的一个总结,不会太注意用词的。
前面linxer前辈有写过《ring 3级32位x86 cpu仿真》,对于x86CPU的仿真的做过一些构想和阐述。可惜,我是在完成了X86的基本指令集的解释引擎后才注意到此文章。拜读之后,觉得自己与前辈的实现思想有些许差别,所以就随便写下。
目标优先级:
1)可维护性。
2)执行效率。
3)可移植性。本引擎并非设计为在桌面平台使用
一、CPU仿真
1、CPU CONTEXT。
因为一些原因,在CPU相关的结构上,有借鉴linxer前辈的思想。
此处要提到的是,本人对于所有寄存器和寄存器索引,是按照IA32手册上的标准定义的。linxer前辈在EFLAG寄存器的标识位索引定义时并未按照标准,并且注释为了“效率”,本人到现在仍然没有理解这个“效率”问题。
2、内存寻址和访问接口。
因为模块化编程的原因,此处对于内存的操作是单独提取出来的接口。当前版本是使用一个映射接口来完成,其声明如下:
DWORD32 MmMapAddress(pVM, /*虚拟机结构*/
WORD16 Seg, /*段地址/段选择子*/
DWORD32 Address, /*线性地址。在16位寻址模式下仅仅使用低16位*/
DWORD32 Size, /*需要映射的区域大小*/
DWORD32 Privilege); /*映射的权限(读、写、执行等)*/
每当VCPU(我们仿真的虚拟CPU)要访问访问内存时,调用此接口,映射得到一个本地指针,可以对其进行相应读写操作。
在内存映射模块的内部,基本的思想是跟linxer前辈一样,区别如下
a)效率问题,对地址部分采用了HASH处理。
b)根据Privilege检查内存块属性,不符合时会触发VCPU GP类异常
备注:1)我们并未对VCPU的读写进行函数级封装,效率问题
2)内存映射部分并非本人所作,这里说的仅仅是我们的原始设计
本人其实会更倾向于对于读写进行函数级封装,虽然效率低,但可以对页属性进行较为完善的仿真。
3、指令解析部分
这个部分本人和linxer前辈区别较大。
事实上,如果看下INTEL IA32手册的(OPCODE MAP)部分,你会发现指令解析的方法和方式相当清晰:建表。
IA32体系结构下的机器码其实就是由几张表来完成的“One-byte Opcode Map”、“Two-byte Opcode Map”、“Opcode Extensions for ..."以及浮点协处理器的指令解析表格。
所以,指令解析框架很简单:建立如下的“One-byte Opcode Map”(256项)
ONE_OPCODE_TABLE_ENTRY One_byte_Opcode_Map[]={
/*0x00*/ Function1_ADD_00 , /*ADD */
/*0x01*/ Function1_ADD_01 , /*ADD */
/*0x02*/ Function1_ADD_02 , /*ADD */
/*0x03*/ Function1_ADD_03 , /*ADD */
其中FuncOne_ADD_00等都是对应的解析函数。
解析过程如下:
通过指令第一个字节查询"One-byte Opcode Map",直接CALL对应的解析函数
a)如果这条指令是单字节指令(如0x00 add),则这个解析函数其实就是这条指令的实现函数
b)否则,这个解析函数其实是另外一个分派函数(将会查询另外一个表格),此解析函数根据指令第二个字节查询“Two-byte Opcode Map” “Opcode Extensions for ..." 或者 浮点协处理指令解析表,调用下一级指令解析函数
以此类推,到最低层解析函数时,指令所对应的参数和格式已经最大程度地明确了,实现起来很简单。
选择这种解析方式的理由:
a)结构简单,利于维护。添加和修改指令都只需写/更改相应指令解析函数,然后填入对应位置即可。
b)解析速度会比switch快些。虽然switch case结构在编译后其实也可能用表格来实现(也可能用DEC/JZ之类跳转实现),但其更加依赖于编译器优化。
c)假如对这些解析表和相应的X86指令序列进行随机置乱的话,可以成为一个简单的保护代码执行引擎。
这种结构的弊端也很简单:前期工程量会稍大点,因为实现基本指令就要几百个解析函数,另外要建立好几个表格,比较累(还好可以写一些辅助工具完成)
4、中断仿真
对于中断的仿真可以以如下方式完成:
a)在指令产生异常/中断(GP/DE/DB等等)时,指令会填充CPU CONTEXT的一个INTERRUPT_INFOMATION结构,把相应的异常/中断信息保存,然后逐层返回到最高层的解析函数(查询
"One-byte Opcode Map"的那个函数)。
b)根据中断号调用相应中断处理程序。中断处理程序会根据INTERRUPT_INFOMATION结构的内容执行相应的操作。SEH之类的模拟,可以在这里完成的。
5、指令仿真
因为是采用标准C来实现指令仿真,所以有些标志位处理起来比较麻烦。
用内联汇编的方法实现指令很简单,没必要说什么。这里主要说下用C来实现实现指令模拟时会遇到的问题
a)AF位的处理。这个标志主要影响调整指令,比较麻烦,我的当前版本暂时回避了此标志,下个版本中将使用使用算法来解决此标志。
b)CF/OF位。由于暂时没有找到统一的处理办法,当前版本中,我是通过做有符号/无符号两种运算来分别设置的。
c)PF位。使用计算海明码码重的算法(LINUX源码中可以找到),起码比逐位遍历要高效不少
d) ModR/M SIB位。本部分一样是使用查表(参见IA32手册INSTRUCTION FORMAT部分)来完成解析的,解析结果是地址,保存到如下结构中
struct xxxxxxxxxxxxxxxxxxxxxx
{
/*
The total length of ModR/M、SIB(if exist) and Displacement(if exist).
Filled by GetInstructionArgs()
*/
UCHAR Length;
/*The reg field of the ModR/M byte*/
UCHAR Reg_Opcode;
/*The r/m field of the ModR/M byte*/
/*
Is EffectiveAddress a memory address or general register ?
1 -> Register. the RegIndex field is the index of the general register.
0 -> Memory address.
*/
UCHAR bIsEAReg;
union
{
UCHAR RegIndex;
ULONG Address;
}EffectiveAddress;
}
6、用于提高解析效率的指令CACHE
当前版本并未实现CACHE,但为了解析效率计划加上。实现CACHE的麻烦在于自修改指令的处理,不过因为对内存访问使用单独的接口,所以,只要映射内存时将Address、Size、Privilege与指令Cache对比即可准确判断是否要更新CACHE
7、指令模拟的正确性测试
利用微软的DEBUG API接口写测试工具,单步,内存/寄存器 对比。
二、PE运行环境的模拟
这部分我并未实际编码,所以说的估计都是错的
1、内存管理
没什么好说的,链表/数组什么的,就是要高效点。
另外,要把常用的DLL文件的地址空间、TEB/PEB也仿真了
2、API仿真
对于截获API调用并不需要在在CALL/JMP/RET等里面进行单独处理,我的思路是对于被仿真的API的虚拟地址维护一张表,在解释一条指令之前,对比EIP与API地址表,如果是API调用,则执行仿真API,否则正常解析
胡言乱语:
指令/编码层次对抗仿真的手段
1)将浮点运算/BCD码运算的结果融入到正常指令流中
2)使用一些特殊的指令参数。例如:ENTER/CPUID等虽然是常用指令,但个别类型的入口参数仿真起来很繁琐。起码不使用内联X86汇编的情况下,我是觉得很烦。
3)未公开指令。这个思路其实有点扯淡,你能得到的东西,仿真者也能得到。
3)调用不常用的API,这个是最容易想到的。
4)API的仿真是不可能把KERNEL32真的加载进去的,所以,如果读取这些地址,比如API的前N个字节,应该会导致仿真失败。
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法