首页
论坛
课程
招聘
[原创]ring 3级32位x86 cpu仿真
2007-4-8 16:47 20141

[原创]ring 3级32位x86 cpu仿真

2007-4-8 16:47
20141
【  标题  】 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的功能还很简单,还有待加强......

2022 KCTF春季赛【最佳人气奖】火热评选中!快来投票吧~

收藏
点赞0
打赏
分享
最新回复 (19)
雪    币: 521
活跃值: 活跃值 (151)
能力值: (RANK:570 )
在线值:
发帖
回帖
粉丝
笨笨雄 活跃值 14 2007-4-8 16:59
2
0
个人建议,在x86平台上仿真x86,没有必要识别所有指令,建立数组,标记带modr\m byte和string类型的opcode。重定向寻址指令就行了

牛人写代码就是快,我构思VM比你早,但是你的框架写出来了,我还在想实现思路

不过我的是用在加密上的,而不是解密
雪    币: 1654
活跃值: 活跃值 (200)
能力值: (RANK:450 )
在线值:
发帖
回帖
粉丝
linxer 活跃值 11 2007-4-8 17:20
3
0
雄大哥,不知道能否说得详细点呢,觉得你这个思路很好,阿拉想了解下,寻址重定向前后应该做些什么呢?

由于阿拉资质有限,目前还没有想明白,你这个思路能不能在我这里用,如果能用的话,又该怎么用......
雪    币: 1309
活跃值: 活跃值 (232)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
deepwater 活跃值 2007-4-8 19:34
4
0
CPUEnvernment 拼写错误, 也不一定非要改, 提醒下而已
雪    币: 521
活跃值: 活跃值 (151)
能力值: (RANK:570 )
在线值:
发帖
回帖
粉丝
笨笨雄 活跃值 14 2007-4-8 19:49
5
0
首先是解释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的代码。这样的方法不见得能提高多少性能。当然了,这跟我的方向不同,没深入想过
雪    币: 1654
活跃值: 活跃值 (200)
能力值: (RANK:450 )
在线值:
发帖
回帖
粉丝
linxer 活跃值 11 2007-4-8 21:56
6
0
学习了!
雄大哥的思路,值得好好研究下
雪    币: 45
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
vfer 活跃值 2007-4-9 19:58
7
0
说到底 就是反汇编的知识了
雪    币: 200
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
poincarele 活跃值 2007-4-10 09:01
8
0
这种体力活还是少利用open source来做吧///
雪    币: 203
活跃值: 活跃值 (18)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
shenhaiyu 活跃值 3 2007-4-10 09:14
9
0
好文章啊,赞一个
雪    币: 5536
活跃值: 活跃值 (56)
能力值: (RANK:1060 )
在线值:
发帖
回帖
粉丝
forgot 活跃值 26 2007-5-28 12:34
10
0
一定要这么苦啊,做个dll用vmp好了...

RORDBG好像就是重定向

我要提醒各位老大,如果虚拟机用来保护的话就不要出现真实的x86code...
雪    币: 2098
活跃值: 活跃值 (40)
能力值: (RANK:300 )
在线值:
发帖
回帖
粉丝
riijj 活跃值 7 2007-5-28 13:02
11
0
因为劳动量,所以一直也没有完成  

雄兄的思路很好,虽然使用真实 cpu 有代价,需要考虑安全,但的确是简单一点

forgot 兄提到 vm 不要出现真实 x86 code,是不是说不要把运行的指令真实地出现在 cpu 里运行 ?

如果所有指令都是在 vm 虚假运行,register 全是数据 (像楼主的设计) 这样当然比较难分析

或是,像 vb pcode, java byte code 的想法,根本是另一种语言的罗辑,缺点是不可以直接把现有 exe 加密
雪    币: 200
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
任愉 活跃值 2007-5-28 13:07
12
0
感觉蛮好的,咋就看不懂呢?
雪    币: 5536
活跃值: 活跃值 (56)
能力值: (RANK:1060 )
在线值:
发帖
回帖
粉丝
forgot 活跃值 26 2007-5-28 14:00
13
0
pcode 未必不能直接加密,只不过是翻译器设计的问题。
可以翻译的低级一些:
mov eax, ebx
====>
ldr32 ebx
str32 eax

mov eax, [ebx]
====>
ldr32 ebx
ldi32
str32 eax

mov ax, [ebx]
====>
ldr32 ebx
ldi32
str16 ax
pop16
雪    币: 2098
活跃值: 活跃值 (40)
能力值: (RANK:300 )
在线值:
发帖
回帖
粉丝
riijj 活跃值 7 2007-5-28 14:24
14
0
micro instruction
雪    币: 246
活跃值: 活跃值 (11)
能力值: ( LV13,RANK:410 )
在线值:
发帖
回帖
粉丝
Isaiah 活跃值 10 2007-5-28 15:37
15
0
算术运算全部用NOTAND
雪    币: 1654
活跃值: 活跃值 (200)
能力值: (RANK:450 )
在线值:
发帖
回帖
粉丝
linxer 活跃值 11 2007-5-28 21:04
16
0
我对RORDBG不是很了解,貌似,它新建了被调试进程,这样被调试进程有了自己的进程空间,重定位应该没有什么问题,我作这个的目的是,程序进程空间也自己虚拟了,基本上不直接需要windows支持,

如果我这个要重定位的话,估计重定位前要有些预处理,包括对一些内存寻址的替换,要把它映射到虚拟出来的进程空间中......

不过这个方法值得考虑
雪    币: 223
活跃值: 活跃值 (48)
能力值: ( LV9,RANK:450 )
在线值:
发帖
回帖
粉丝
netsowell 活跃值 11 2007-5-29 01:02
17
0
总有一天大家都不用x86了
雪    币: 258
活跃值: 活跃值 (11)
能力值: ( LV12,RANK:250 )
在线值:
发帖
回帖
粉丝
AZMC 活跃值 6 2007-5-29 08:32
18
0
有一本书:《虚拟机的设计与实现》,很不错哦!

里面有详细的 C 代码!

里面详细讲解了 一款虚拟机的 方方面面:语言/解释器/编译器/链接器/调试器。。。

--------------------
雪    币: 188
活跃值: 活跃值 (72)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
davidhee 活跃值 2007-7-2 11:32
19
0
都是大牛们的讨论,看不懂
雪    币: 207
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
dafeiqiqi 活跃值 2007-8-26 22:38
20
0
俺是支持linxer的
游客
登录 | 注册 方可回帖
返回