1、保护模式简介 CPU分有:实模式、保护模式、虚拟8086模式,大多数操作系统都运行在保护模式下。
保护模式主要是用来保护寄存器、数据结构、指令,实际上也就是保护寄存器,因为cpu的数据都存放在寄存器中。
保护模式的特点:段和页。
保护模式具体资料可以在 Intel白皮书第三卷 中查看。
2、段寄存器 2.1 段选择子 CPU一共有八个段寄存器:ES CS SS DS FS GS LDTR TR ,OD可见前6个,但GS段寄存器windows并未使用(32位下)。
如果运行在实模式下,则只有前四个有用。
如果是64位,则使用GS而不是FS。
当执行下列汇编代码时
Text
实际上cpu所“看到的”代码如下:
1 2 3 4 mov dword ptr ds:[0x12345678 ],eax
💡 ds段寄存器通常时用来存放要访问数据的段地址。cs段寄存器表示要执行的代码。ss段寄存器表示堆栈的段地址。[…]则表示一个内存单元,比如ds:[1],cs:[1],ss:[1]。——王爽《汇编语言》
段寄存器结构: 共96位, 16位可见,80位不可见。
1 2 3 4 5 6 struct SegMen { WORD Selector; WORD Attributes; DWORD Base; DWORD Limit; };
读段寄存器指令:mov ax,es 只能读16位 (可见部分)
写段寄存器指令:mov ds,ax 写了96位的 。
段寄存器可以用mov指令读写,但是LDTR和TR除外。
加载段描述符至段寄存器的指令共有三种:
其中选择子(Selector)有如下结构:
打开OD,随便加载一个程序可以看到段寄存器对应的选择子。
以fs的选择子为例进行解析:
Text 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 fs=0x0053 => 0000 0000 0101 0011 由于前面两个字节为0,因此单独拿后面两个字节讲解。 0101 0011 根据上面给出的Selector表可以对这两个字节划分。 01010 0 11 值 | 含义 - ------------------------------------------------ 01010 | 欲查表的索引号,此处为十进制5 0 | 欲查哪一块表;0->GDT 1->LDT 11 | 哪一环的权限(RPL),此处为十进制3,因此为3环 根据公式 addr = GDT + 8 * index计算后可得到fs的段描述符 addr = 0xfffff88004590000 + 8 * 5 = 0xfffff88004590028 2: kd> dq fffff88004590028 fffff880`04590028 **00cff300`0000ffff** 0020fb00`00000000 fffff880`04590038 00000000`00000000 04008b58`f0000067 fffff880`04590048 00000000`fffff880 ff40f3fd`f000bc00 fffff880`04590058 00000000`00000000 00cf9a00`0000ffff fffff880`04590068 00000000`00000000 00000000`00000000 fffff880`04590078 00000000`00000000 00000000`00000000 fffff880`04590088 00000000`00000000 00000000`00000000 fffff880`04590098 00000000`00000000 00000000`00000000
GDT:全局描述表(Global Description Table),在操作系统加载完毕后就存在的一快内存。实际上就是一个数组,每一个元素就是一个描述符,多个组合一起就构成了全局描述符表。而每一个描述符共64位,包含了以下的这些信息:段基址、段长度、属性。段寄存器通过解析选择子后得到索引后在GDT中跳转获取对应描述符。
LDT:局部描述表(Local Description Table),与GDT功能一致,但不能单独存在,只能嵌套在GDT中。
GDT可以使用windbg的命令可以查看gdt表的地址:
Text 1 2 3 4 5 6 7 gdtr寄存器(windbg伪寄存器,是windbg通过sgdt lgdt指令获取的,为了方便用户,才模拟了一个寄存器叫gdtr,实际是没有这个寄存器的) : 存两个值,一个是GDT表的首地址,一个是GDT表的大小(字节为单位) 48位 r gdtr r查看gdtr寄存器的地址 r gdtl r查看gdtr寄存器的大小 都查gdtr dd xxxx 4字节查看内存 dq XXXX 8字节查看内存 dq xxxx Lnum 查看固定数量元素的内存
通过指令sgdt获取。其中共获取到6个字节,前两个字节位gdt寄存器的大小,后面四个字节为gdt的地址。
1 2 3 4 5 6 7 8 9 10 11 12 #include "stdafx.h" #include <stdlib.h> int _tmain(int argc, _TCHAR* argv[]) { unsigned char var[6 ]={0 }; _asm{ sgdt var } printf ("%x,%x\n" ,*(unsigned int *)&var[2 ],*(unsigned short *)&var[0 ]); return 0 ; }
2.2 段描述符 段描述符有如下结构:
将gdt的一个段描述符0x00cff300 0000ffff进行拆分可得到如下
Text 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 base:0x00000000 attr:0x0cf3(前面的0是补齐2字节) limit:0xfffff(前面的0是补齐4字节) attr的属性又可以细分如下: 0xc = 1100 ---------------------------- G:1;1->limit以4k对齐,limit = ( limit + 1 ) * 4096 - 1 0->字节对齐,limit = limit D/B:1 0:0 AVL:0 0xf3 = 1111 0011 ----------------------------- P:1 DPL:11 S:1 Type:0011(3)
G位 段对齐粒度。 也就是决定了Limit大小的一个位。
在上文填充段寄存器隐藏部分时,Limit在描述符中只有5个16进制位表示,剩下的3个16进制位就需要看G位。
当G为0时,整个段将以字节对齐,Limit大小单位为字节,所以精确到1。Limit直接就是段长。段寄存器中的Limit高位补0。
当G为1时,整个段将以4KB对齐,Limit大小单位为4KB,所以段的末尾处一定是以FFF结尾。段寄存器中的Limit低位补FFF。
D/B位 =0 表示是16位的单位,=1 表示32位的单位。
如果是代码段的描述符,那么称为D;如果是数据段的描述符,称为B。
大段或者小段,分为三种情况:
对CS段来说:
为1时,默认为32位寻址。
为0时,默认为16位寻址。
前缀67改变寻址方式。
对SS段来说:
为1时,隐式堆栈访问指令(PUS H POP CALL RETN等)修改的是32位寄存器ESP
为0时,隐式堆栈访问指令(PUSH POP CALL RETN等)修改的是16位寄存器SP
对于向下扩展的数据段:
为1时,段上限大小为4GB(2的32次方)。 为0时 段上限大小为64KB(2的16次方)。
P位 有效位 1:描述符有效 0:描述符无效
当描述符无效时,任何尝试加载该描述符、访问该描述符对应的段间地址都会报错。
DPL位 能访问段描述符的权限。
💡 RPL:发出请求的权限等级。
CPL:当前请求的权限等级。一般特指为CS的RPL。
DPL:能否访问段描述符的权限等级。
三者的联系可总结为:在CPL的权限下,以RPL的权限去访问DPL权限。
S位 Text 1 描述符类型位。 为0时,是系统段描述符。 为1时,是代码或数据段描述符。具体类型需要搭配type属性来判断。
Type位 由S位决定了具体是代码段还是数据段描述符
数据段: A位:数据段是否被访问过位,访问过为1,未访问过为0 段描述符是否被加载过 W位:数据段是否可写位,可写为1,不可写为0 E位:向下扩展位,0向上扩展:段寄存器.base+limit区域可访问。1向下扩展:除了base+limit以外的部分可访问。
代码段: A位:代码段是否被访问过位,访问过为1,未访问过为0 段描述符是否被加载过 R位:代码段是否可读位,可读为1,不可读为0。(但R位为0,代码段照样可以读) C位:一致位。1:一致代码段(0环的函数在3环可以调用)。 0:非一致代码段(各调各的)
(小于8是16位的系统描述,大于8是32位)
针对E位的理解与实验:
左边为向上扩展,右边为向下扩展。红色代表可以访问,绿色不可访问
Tip:段描述符有没有4G,首先是看E位的拓展方向,其次是看Base和limit之间的大小。
首先构造一条段描述符.
Text 1 2 3 4 5 6 7 00cff700`0000ffff base:00000000 attr:0cf7 limit:fffff ;由于G位是1所以这里实际为(0xfffff+1) * 0x1000 - 1 = 0xFFFFFFFF E位:0111,向下扩展
通过windbg的e[b|d|D|f|p|q|w] address [Values]指令可以修改GDT中保存的段描述符。修改8003f090(GDTR = 8003f000),然后输出gdt查看前20个描述表,dq address l20。(注意是小写L,不是数字1)。
实验一:ds 1 2 3 4 5 6 7 8 9 10 11 12 13 #include "stdafx.h" unsigned char var = 0 ; int _tmain(int argc, _TCHAR* argv[]) { _asm{ mov ax,0x93 mov ds,ax mov dword ptr ds:[var],0x20 }printf ("%X\n" ,var);return 0 ; }
运行出错。
**分析:**异常断在了赋值var的地方。首先在描述符中attr为0x0cf7,Type=0111表示为向下扩展、可读写、已访问。根据上图可知,如果E位为向下扩展,则base+limit这段区域是无法访问的即(0x00000000-0xffffffff),因此在写数据时发生了错误。修复方法为将E位修改为向上扩展(E=0),或者将limit修改为一个小范围值,使得其他区域的内存可以访问(在demo2实现)。
实验成功!
实验二:ss 首先将0x8003f090修改回0x00cff700`0000ffff。
1 2 3 4 5 6 7 8 9 10 11 12 #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) {unsigned char var = 0 ; _asm{ mov ax,0x93 mov ds,ax mov dword ptr ds:[var],0x20 }printf ("%X\n" ,var);return 0 ; }
运行同样报错。
**分析:**需要注意的是中断打在了printf上,说明我们上边的ASM代码没问题!那么一个新问题来了,为什么demo1中中断打在了var赋值上?虽然我们显式使用了ds段来描述变量var,但仍存在一个问题,变量var属于堆栈地址 ,所以实际上mov dword ptr ds:[var]被编译器翻译成了mov dword ptr ss:[var]
因为我们没有修改ss段,所以上面对ss段的操作没问题。根据单步执行,可以发现中断的位置是printf函数。
根据我们构造的描述符可知,base=0,limit的范围为0xFFFFFFFF,然后又属于向下扩展,所以0x0-0xFFFFFFFF的区域无法访问,printf中必然有用到ds段的数据,但这些数据有没有访问权限,因此访问中断了。修复方法为将limit设置为一个很小的范围,这样其他的区域就可以访问了。比如0x1FFF。(00c0f700`00000001)
实验完成!
使用windbg的dg segment命令可以快速查看段寄存器对应的段描述符。
2.3 段寄存器证明 读的时候只能读到16位(选择子),但写的时候却写入了96位。如何证明剩下的80位是否存在?
1 2 3 4 5 6 7 8 int main (int argc,char * argv[]) { int var = 0 ; __asm{ mov ax,ss mov ds,ax mov dword ptr ds:[var],eax } }
1 2 3 4 5 6 7 8 int main (int argc,char * argv[]) { int var = 0 ; __asm{ mov ax,cs mov ds,ax mov dword ptr ds:[var],eax } }
1 2 3 4 5 6 7 8 9 int main (int argc,char * argv[]) { int var = 0 ; __asm{ mov ax,fs mov gs,ax mov eax,gs:[0 ] mov dword ptr gs:[var],eax } }
1 2 3 4 5 6 7 8 9 10 int main (int argc,char * argv[]) { int var = 0 ; __asm{ mov ax,fs mov gs,ax mov eax,gs:[0x1000 ] mov dword ptr gs:[var],eax } }
2.4 段寄存器权限 数据段:
mov ds,ax //当执行 mov ds,ax 时,CPU先解析段选择子0020,然后去GDT表找段描述符,检查段描述符P位是否有效,然后检查S位,确认是数据段或代码段,然后检查TYPE域确认是数据段,然后看DPL是否能够访问.只要上述条件都满足,则mov指令执行成功,只要有一条不满足,mov失败。
实验一:CPL=3 RPL=3 DPL=3 修改GDT+0x48为 00cff300`0000ffff
1 2 3 4 5 6 7 8 9 10 11 12 #include "stdafx.h" int var = 0 ;int _tmain(int argc, _TCHAR* argv[]) { _asm{ mov ax,0x4B mov ds,ax mov dword ptr[var],10 } return 0 ; }
运行正常!
实验二:CPL=3 RPL=0 DPL=3 修改GDT+0x48为 00cff300`0000ffff
1 2 3 4 5 6 7 8 9 10 11 12 #include "stdafx.h" int var = 0 ;int _tmain(int argc, _TCHAR* argv[]) { _asm{ mov ax,0x48 mov ds,ax mov dword ptr[var],10 } return 0 ; }
运行正常!
实验三:CPL=3 RPL=3 DPL=0 修改GDT+0x48为 00cf9300`0000ffff
1 2 3 4 5 6 7 8 9 10 11 12 #include "stdafx.h" int var = 0 ;int _tmain(int argc, _TCHAR* argv[]) { _asm{ mov ax,0x4B mov ds,ax mov dword ptr[var],10 } return 0 ; }
运行失败!
实验四:CPL=3 RPL=0 DPL=0 修改GDT+0x48为 00cf9300`0000ffff
1 2 3 4 5 6 7 8 9 10 11 12 #include "stdafx.h" int var = 0 ;int _tmain(int argc, _TCHAR* argv[]) { _asm{ mov ax,0x48 mov ds,ax mov dword ptr[var],10 } return 0 ; }
运行失败!
总结: 数据段下RPL<=DPL && CPL<=DPL(数值上),实验四失败是因为此时运行的环境为3环。
代码段(跨段跳转-不提权): 跳转指令有call、jmp两种,格式如下
Text 1 2 3 4 5 CALL FAR CS:EIP JMP FAR CS:EIP ;CALL/JMP FAR 0x20:0x004183D7 ;0x20为新的cs寄存器,通过拆分新的cs寄存器得到段描述符后根据其base+0x004183D7进行跳转。
为了避免干扰,需要关闭增量链接和随机地址。
长跳转与短跳转 Text 1 2 3 4 5 6 汇编写法: call/jmp far cs:eip C++写法: char buf[6]={78,56,34,12,0x4b,0}; call/jmp fword ptr [buf] // call 4b:12345678
长跳转的压栈与短跳转(普通的call)压栈略有区别。短跳转会将下一行代码的地址入栈后进行跳转;而长跳转会将当前cs和下一行代码的地址入栈再跳转。
执行前
执行后
因此ret指令已经不适合长跳转的返回,取而代之的是retf。
实验一:CPL=3 RPL=3 DPL=3 构造描述符00cffb00`0000ffff(type:1011,非一致代码段)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include "stdafx.h" void __declspec(naked) test() { _asm{ retf } }int _tmain(int argc, _TCHAR* argv[]) { char buf[6 ]={0 ,0 ,0 ,0 ,0x4b ,0 }; *(int *)&buf[0 ]=(int )test; _asm{ call fword ptr [buf] } return 0 ; }
运行正常!
实验二:CPL=3 RPL=0 DPL=3 构造描述符00cffb00`0000ffff(type:1011,非一致代码段)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include "stdafx.h" void __declspec(naked) test() { _asm{ retf } }int _tmain(int argc, _TCHAR* argv[]) { char buf[6 ]={0 ,0 ,0 ,0 ,0x48 ,0 }; *(int *)&buf[0 ]=(int )test; _asm{ call fword ptr [buf] } return 0 ; }
运行正常,但会发现cs并没有被修改为0x48。
原因:长跳转时有那么一个计算 RPL|DPL -> 0|3 = 3 => 4B
实验三:CPL=3 RPL=3 DPL=0 构造描述符00cf9b00`0000ffff(type:1011,非一致代码段)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include "stdafx.h" void __declspec(naked) test() { _asm{ retf } }int _tmain(int argc, _TCHAR* argv[]) { char buf[6 ]={0 ,0 ,0 ,0 ,0x4b ,0 }; *(int *)&buf[0 ]=(int )test; _asm{ call fword ptr [buf] } return 0 ; }
运行失败!
实验四:CPL=3 RPL=0 DPL=0 构造描述符00cf9b00`0000ffff(type:1011,非一致代码段)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include "stdafx.h" void __declspec(naked) test() { _asm{ retf } }int _tmain(int argc, _TCHAR* argv[]) { char buf[6 ]={0 ,0 ,0 ,0 ,0x48 ,0 }; *(int *)&buf[0 ]=(int )test; _asm{ call fword ptr [buf] } return 0 ; }
运行失败!
实验五:CPL=3 RPL=3 DPL=3 (retf) 构造描述符00cffb00`0000ffff(type:1011,非一致代码段)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include "stdafx.h" void __declspec(naked) test() { _asm{ retf } }int _tmain(int argc, _TCHAR* argv[]) { char buf[6 ]={0 ,0 ,0 ,0 ,0x4b ,0 }; *(int *)&buf[0 ]=(int )test; _asm{ call fword ptr [buf] } return 0 ; }
在retf处下断点,然后修改保存的cs为18(0环)。
然后运行,发现异常。
总结: 跨段代码无法进行提权(提权需要门)。
然后火哥是这么说的!!!!!
3、调用门 门,通往新世界的通道。与长跳转类似也是通过call far cs:eip(jmp不行)进行调用。当cs对应的段描述符的S=0时,CPU会识别这个描述符是一个门,每个门格式不同。调用门格式如下:
实验一:提权R3进入R0环 构造一个3环->0环的描述符1234EC00·00085678
Text 1 2 3 4 5 offset in segment:0x12345678 segment selector:0x0008 -> 1 0 00 (0环权限,查GDT,index=1) p:1 dpl:3 param count:0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include "stdafx.h" void __declspec(naked) test() { _asm{ int 3 retf } }int _tmain(int argc, _TCHAR* argv[]) { char buf[6 ]={0 ,0 ,0 ,0 ,0x4b ,0 }; printf ("%p\n" ,test); *(int *)&buf[0 ]=(int )test; _asm{ call fword ptr buf; } return 0 ; }
在printf下断点,查看函数test地址后填充到门描述符。
然后单步执行调用门后int 3断点被执行。
使用u[f] addr [-lxxxx]查看汇编,其中f可以直接查看函数的所有汇编。
说明此时已经跨段提权进入0环。输入g命令继续执行,此时发现r3层异常中断,原因为int 3造成,去掉int 3即可正常运行。
与长跳转不同的是,由于调用门为R3进入到R0,由于两个权限的地址范围和权限不同,因此调用门在call的时候会将ss、esp、cs、下一行代码地址,进行入栈。
实验二:调用0环函数(DbgPrint) 首先使用uf nt!DbgPrint获取函数地址
然后添加函数声明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #include "stdafx.h" typedef int (__cdecl *fnDbgPrint) (const char * _Format, ...) ; fnDbgPrint myDbgPrint = (fnDbgPrint)0x83e4fc60 ;const char msg[]="fuking man!!!" ;void __declspec(naked) test() { _asm{ pushfd pushad push fs mov ax, 0x30 mov fs, ax; lea eax, [msg] push eax call myDbgPrint add esp, 4 pop fs popad popfd retf } }int _tmain(int argc, _TCHAR* argv[]) { char buf[6 ]={0 ,0 ,0 ,0 ,0x4b ,0 }; printf ("%p\n" ,test); *(int *)&buf[0 ]=(int )test; _asm{ call fword ptr buf; } return 0 ; }
打开dbgView进行监视。
运行R3程序。
成功输出,但是不知道为啥DbgView没捕获到。
补充:设置完DbgView了之后重新打开就行了。
4、中断门
硬件叫做中断,软件叫做异常。
中断门,CPU执行如下的指令:INT N,查询的是另外一张表,这张表叫IDT表。
表的含义与调用门基本一致。这里面的D代表了default默认是1。windbg同样也提供了类似GDT表查询的指令,r idtr、r idtl。
使用dq idtr查看idt表。
其中每个中断描述符代表了一个中断函数。常见R3层的int 3指令对应的是83e5ee00·00084fc0,拆分后得到的中断函数(Offset)为0x83e54fc0,使用windbg查看该地址的反汇编。
int3 与 int 3作用相同,都是查询IDT表index为3的描述符。但int3只有一个字节(0xCC),int 3占两个字节(CD 03)
windbg同样提供了!idt n指令用于查看对应中断序号的中断函数。
实验一:构造中断门
关闭增量链接和随机地址。
首先获取函数地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include "stdafx.h" void __declspec(naked) test() { _asm{ iretd } }int _tmain(int argc, _TCHAR* argv[]) { printf ("%p\n" ,test); getchar(); return 0 ; }
然后构造描述符。
1 2 3 4 5 6 offset:0x00401000 segment selector:0008 (0 环) P:1 DPL:3 (确保3 环有权限访问该中断描述符) =0040 EE00`00081000
然后在idtr+0x100处写入我们的描述符。
代码中添加int的调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include "stdafx.h" void __declspec(naked) test() { _asm{ int 3 iretd } }int _tmain(int argc, _TCHAR* argv[]) { printf ("%p\n" ,test); getchar(); _asm{ int 0x20 } return 0 ; }
运行后,windbg中断。
输入g命令继续执行,此时发现r3层异常中断,原因为int 3造成,去掉int 3即可正常运行。
实验二:堆栈影响 重新运行实验一的代码,在执行int 0x20前,观察寄存器和段寄存器的值。
然后继续执行。
可以看到中断门先后压入了ss、esp、efl、cs、下一条语句的地址。因此进入中断门的堆栈结构如下:
实验三:IF 将代码修改为如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include "stdafx.h" int val = 0 ;void __declspec(naked) test() { _asm{ pushfd pop eax mov dword ptr [val],eax int 3 iretd } }int _tmain(int argc, _TCHAR* argv[]) { printf ("%p\n" ,test); getchar(); _asm{ pushfd int 0x20 popfd } printf ("%x\n" ,val); getchar(); return 0 ; }
运行后windbg断下,输入dds esp查看堆栈
堆栈值未变,那么输入uf 401000查看一下函数的反汇编.
dd一下变量val的值。
神奇的发现堆栈中保存的efl与获取到的efl不一样!!!!!!原因是,int 3同时也是进入中断门,因此当前堆栈保存的是int 3的efl。分别将0x46和0x246转换为二进制。
Text 1 2 0x046 = 0000 0100 0110 0x246 = 0010 0100 0110
可以发现第九位有区别,第九位在eflags文档中为IF为,中断启用标志。
大概意思就是我们自己的int 0x20中断后无法对鼠标或者键盘之类的外设输入进行响应,但是int 3可以。
Text 1 2 cli; //清除Efl的IF位 sti; //设置Efl的IF位
然后火哥说中断门会清空VM、TF、NF、IF位。
VM(Vitual-8086 Mode):虚拟8086模式,这个是为了16位兼容,在C:\windows下除了有system32还有一个是system(实模式)。当这个为1时表示运行在一个虚拟的16位系统(可分页可分段,访问的不是物理地址)。
TF(Trap Flag):单步位(相当于OD的F7,F8不算单步,因为他跳过了call),如果为1表示下一行代码执行时会发生异常。
NT(Nested Task):任务嵌套位。为1时有上一层要返回。
中断门之所以会清空这三个标志位是因为防止中断嵌套
可以理解为清空IF是为了防止其他中断打断;清空TF是防止在执行中断门里边的代码时一直异常;清空NF位是为了防止执行完就会返回(有点还不太了解,因为还没学到任务门);清空VM位不是很理解。
调用门与中断门的区别是,调用门能被可屏蔽中断打断。
5、陷阱门 陷阱门的格式与中断门一致,唯独不同的地方就是Type位。
实验一:VM、TF、IF、NT 将中断门的描述符改为0040EF00`00081000
然后执行代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include "stdafx.h" int val = 0 ;void __declspec(naked) test() { _asm{ pushfd pop eax mov dword ptr [val],eax iretd } }int _tmain(int argc, _TCHAR* argv[]) { printf ("%p\n" ,test); getchar(); _asm{ pushfd int 0x20 popfd } printf ("%x\n" ,val); getchar(); return 0 ; }
可以看到此时EFL为246.
说明它禁止响应可屏蔽中断。
IDT中没有用到陷阱门。
然后中断门和陷阱门的区别就是:中断门由于IF为为0,表明不会被其他中断打断,而陷阱门有可能会被打断。
6、任务段 任务段(TSS:Task-State Segment)描述的是一个任务环境,用于进程和线程的环境的切换。TSS是一种结构数据且保存在内存中。内存的位置被描述在GDT中。
由于任务太过依赖于GDT表中的任务段描述和内存块,因此Windows 和Linux 都没有采用任务段(不想被CPU限制)。
TSS结构如下:
TSS是最小104字节的内存。
Text 1 2 3 4 5 6 7 Avoid placing a page boundary in the part of the TSS that the processor reads during a task switch (the first 104 bytes). The processor may not correctly perform address translations if a boundary occurs in this area. During a task switch, the processor reads and writes into the first 104 bytes of each TSS (using contiguous physical addresses beginning with the physical address of the first byte of the TSS). So, after TSS access begins, if part of the 104 bytes is not phy 避免在TSS中处理器在任务切换期间读取的部分(前104字节)。如果该区域出现边界,处理器可能无法正确执行地址转换。在一个任务开关,处理器读写每个TSS的前104(0x68)个字节(使用连续的物理以TSS的第一个字节的物理地址开头的地址)。因此,在TSS访问开始后,如果部分在这104个字节不是物理连续的,处理器将访问不正确的信息而不生成一个页面错误异常。
在windbg中使用命令dt structName [addr]来查看结构体数据,查看TSS命令如下:
Text
TSS同样有str和ltr指令。str用于获取、ltr用于加载。但不同的是,tr保存的是一个选择子。windbg中使用命令r tr,获取选择子。
0x28拆分得到index后可寻址到描述TSS的描述符,也可以使用dg命令。
描述TSS的描述符结构如下:
描述符的结构与段描述符基本一致,需要注意的有两个地方,一个是Type位的B表示的是Busy,即当前任务是否处于忙碌状态(是否在执行),1表示忙碌,0表示非忙碌。Base表示Tss这块内存数据保存的地址。
此时重新使用dt命令解析TSS。
可以看到Flags为0x8b,b=1011,B=1表示busy。
实验一:构造任务段
关闭增量链接、关闭地址随机
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 #include "stdafx.h" #include <windows.h> struct _KiIoAccessMap { UCHAR DirectionMap[32 ]; UCHAR IoMap[8196 ]; }; struct _KTSS { USHORT Backlink; USHORT Reserved0; ULONG Esp0; USHORT Ss0; USHORT Reserved1; ULONG NotUsed1[4 ]; ULONG CR3; ULONG Eip; ULONG EFlags; ULONG Eax; ULONG Ecx; ULONG Edx; ULONG Ebx; ULONG Esp; ULONG Ebp; ULONG Esi; ULONG Edi; USHORT Es; USHORT Reserved2; USHORT Cs; USHORT Reserved3; USHORT Ss; USHORT Reserved4; USHORT Ds; USHORT Reserved5; USHORT Fs; USHORT Reserved6; USHORT Gs; USHORT Reserved7; USHORT LDT; USHORT Reserved8; USHORT Flags; USHORT IoMapBase; struct _KiIoAccessMap IoMaps [1]; UCHAR IntDirectionMap[32 ]; }; struct _KTSS tss = { 0 }; __declspec(naked) void a () { __asm { int 3 ; iretd; } }char esp3[0x2000 ] = { 0 };char esp0[0x2000 ] = { 0 };int main () { char trcode[2 ] = { 0 }; __asm { str trcode; } memset (esp3,0xCC ,0x2000 ); printf ("tss地址:%x\n" , &tss); tss.Eax = 0 ; tss.Ecx = 0 ; tss.Edx = 0 ; tss.Ebx = 0 ; tss.Ebp = 0 ; tss.Esi = 0 ; tss.Edi = 0 ; tss.Cs = 0x8 ; tss.Ss = 0x10 ; tss.Ds = 0x23 ; tss.Esp = (ULONG)(esp3+0x2000 -8 ); tss.Esp0 = (ULONG)(esp0+0x2000 -8 ); tss.Ss0 = 0x10 ; tss.Fs = 0x30 ; tss.Eip = (ULONG)a; DWORD dwCr3 = 0 ; printf ("请输入CR3:" ); scanf_s("%x" , &dwCr3); tss.CR3 = dwCr3; printf ("CR3:%x" , tss.CR3); printf ("func:%x esp0:%x esp3:%x" , a, tss.Esp0, tss.Esp); system("pause" ); char bufcode[6 ] = { 0 ,0 ,0 ,0 ,0x48 ,0 }; __asm { call fword ptr bufcode; } system("pause" ); return 0 ; }
运行,查看函数a的地址和程序的CR3。
构造TSS描述符。
Text 1 2 3 4 5 6 7 8 0000E940`503020ab base:405030 limit:0x20ab DPL:11 ->3环可以访问到 type:1001 -> B = 0 P:1 G:0
继续运行后发现windbg断下,然后使用uf查看汇编。
可以看到已经成功执行。此时输入r tr,发现索引已经为实验测试的0x48,使用dg解析。
可以看到Flags已经被设置为忙碌状态,9->b。使用dt重新查看tss结构。
寄存器此时都为我们自定义的,但会发现此时的ESP并不为ESP0。。
这是因为我们是构造了任务段,而不是通过r3切换到R0,当使用了中断门、调用门、陷阱门时才会进行切换。
输入g,继续运行,发现蓝屏。
实验二:分析蓝屏原因 重新运行实验一的代码,并在call任务段的位置下断点。
重新运行,断下后转到反汇编。
可以看到call后的返回地址为0x4011cc,然后继续运行后windbg断下,输入r tr查看获取TSS选择子后进行解析。
查看原始TSS。
会发现原始的TSS结构保存着真实的返回地址!!!
Text 1 2 3 iretd: 1)、NT如果为1,找到TSS的Previous Task Link,替换寄存器后返回(比如EIP) 2)、如果NT为0,则从堆栈返回。(由于我们的堆栈在初始化时全是0xcccccc,因此返回到一个不存在的地址,就蓝屏了。)
因此如果添加了int 3断点,则需要把eflags给恢复回来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 __declspec(naked) void a () { __asm { int 3 ; pushfd; pop eax; or eax, 0x4000 ; push eax; popfd; iretd; } }
7、任务门
关闭增量链接、关闭随即地址
任务门结构图如下:
Windows-双重异常 Windows使用的任务门主要有作为不可屏蔽中断和双重异常。
双重异常:系统处理异常时触发的异常。Windows使用了任务门在实现双重异常是为了在触发双重异常时,可以将当前环境保存到TSS中,让开发者有信息进行调试、排查问题(系统无法解决双重异常,因此将触发时的环境保存在TSS中,然后反馈给开发者,让开发者自行解决)。
Text
实验一:构造任务门 选择0x48的位置来存放TSS。
构造任务门,写入idt+0x100
Text
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 #include "stdafx.h" #include <windows.h> struct _KiIoAccessMap { UCHAR DirectionMap[32 ]; UCHAR IoMap[8196 ]; }; struct _KTSS { USHORT Backlink; USHORT Reserved0; ULONG Esp0; USHORT Ss0; USHORT Reserved1; ULONG NotUsed1[4 ]; ULONG CR3; ULONG Eip; ULONG EFlags; ULONG Eax; ULONG Ecx; ULONG Edx; ULONG Ebx; ULONG Esp; ULONG Ebp; ULONG Esi; ULONG Edi; USHORT Es; USHORT Reserved2; USHORT Cs; USHORT Reserved3; USHORT Ss; USHORT Reserved4; USHORT Ds; USHORT Reserved5; USHORT Fs; USHORT Reserved6; USHORT Gs; USHORT Reserved7; USHORT LDT; USHORT Reserved8; USHORT Flags; USHORT IoMapBase; struct _KiIoAccessMap IoMaps [1]; UCHAR IntDirectionMap[32 ]; }; struct _KTSS tss = { 0 }; __declspec(naked) void a () { __asm { int 3 ; pushfd; pop eax; or eax, 0x4000 ; push eax; popfd; iretd; } }char esp3[0x2000 ] = { 0 };char esp0[0x2000 ] = { 0 };int main () { char trcode[2 ] = { 0 }; __asm { str trcode; } memset (esp3,0xCC ,0x2000 ); printf ("tss地址:%x\n" , &tss); tss.Eax = 0 ; tss.Ecx = 0 ; tss.Edx = 0 ; tss.Ebx = 0 ; tss.Ebp = 0 ; tss.Esi = 0 ; tss.Edi = 0 ; tss.Cs = 0x8 ; tss.Ss = 0x10 ; tss.Ds = 0x23 ; tss.Esp = (ULONG)(esp3+0x2000 -8 ); tss.Esp0 = (ULONG)(esp0+0x2000 -8 ); tss.Ss0 = 0x10 ; tss.Fs = 0x30 ; tss.Eip = (ULONG)a; DWORD dwCr3 = 0 ; printf ("请输入CR3:" ); scanf_s("%x" , &dwCr3); tss.CR3 = dwCr3; printf ("CR3:%x" , tss.CR3); printf ("func:%x esp0:%x esp3:%x" , a, tss.Esp0, tss.Esp); system("pause" ); __asm { pushfd int 0x20 popfd } system("pause" ); return 0 ; }
构造TSS
Text
可以发现已经断下。
输入g继续执行。
8、101012分页 Windows x86模式下有29912分页和101012分页,其中默认为29912分页。
设置101012分页 使用EasyBCD工具设置系统配置为如下:
重启即可。
实验一:线性地址转物理地址 确保系统当前分页为101012模式!!!
1 2 3 4 5 6 7 8 9 #include "stdafx.h" const char val[] ="hello world" ;int _tmain(int argc, _TCHAR* argv[]) { printf ("%p\n" ,val); getchar(); return 0 ; }
首先获取变量val的逻辑地址。
然后将地址0040312c以101012格式拆分。
Text 1 2 3 4 5 6 7 8 0040312c 0000 0000 0100 0000 0011 0001 0010 1100 (不足的用0补充) 0000 0000 0001(0x1) -> PDT中PDE的索引号 0000 0000 0011(0x3) -> PTT中PTE的索引号 0001 0010 1100(0x12c) -> 页内偏移(物理页)
使用windbg获取当前进程的CR3。
小知识:如果!process 0 0遍历出来进程的CR3末尾三个数不为0,则说明是29912分页,如果为0则是101012分页。
windbg中查看物理地址需要在命令前加上!。
将刚刚拆分得到的数值进行与cr3计算。
最后一次计算不用*4是因为最后得到的66c44000是物理页,里边存放的是数据或者代码。前面两次 *4是因为要寻找PDE和PTE项。
cr3:控制寄存器,里面存放页基址。一共有4096个字节的大小。
为什么一个页的大小为4096字节?原因是在29912或者101012中,后面的12位表示为页内偏移;当12为全部为1时,页内最大偏移为0xFFFF+1(加上偏移0),也就是4096个字节。
CPU拆分地址的操作由模块MMU(Memory Manager Unit),相当于软件的函数。但因为每次寻址时都需要对线性地址拆分,因此操作系统使用了叫做TLB缓存的东西。
TLB中保存着线性地址(前20位)和物理页的对映关系,如果匹配到线性地址(前20位)就可以迅速找到物理页。
通过物理页与线性地址后12位的偏移组合得到最终的物理地址。
如果在TLB中找不到线性地址和物理页的映射(TLB miss),则会操作MMU模块将线性地址拆分后存入TLB缓存中。
实验二:将同一个线性地址转成物理地址
关闭随机地址。变量设置为全局或者静态。
将实验一的程序编译后,同时运行两个。
可以看到线性地址一致,但CR3不同。
可以看到两个线性地址虽然相同,但是对应的物理地址不同。可以得出结论,同一个线性地址可以被映射为多个物理地址。
9、探索0地址
关闭增量和随即地址。
实验一:0地址挂物理页 1 2 3 4 5 6 7 8 9 10 11 12 #include "stdafx.h" int var = 100 ;int _tmain(int argc, _TCHAR* argv[]) { printf ("%p\n" ,&var); int * a = (int *)0 ; getchar(); printf ("%d\n" ,*a); getchar(); return 0 ; }
输出var的地址,然后进行101012拆分。
Text 1 2 3 4 5 0x00405000 0000 0000 0001(0x1) 0000 0000 0101(0x5) 0000 0000 0000(0x0)
然后通过CR3寻找物理页。
同样的方法,寻找0地址的物理页。
发现0地址没有pte,将变量的pte挂上。
继续运行程序。
实验二:页内偏移对齐 将实验一的全局变量改为局部变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { int var = 100 ; printf ("%p\n" ,&var); int * a = (int *)0 ; getchar(); printf ("%d\n" ,*a); getchar(); return 0 ; }
可以看到var的地址已经不是000结尾了,然后按照实验一的方法对0地址挂上pte。
Text 1 2 3 4 5 6 7 0x0012ff28 0000 0000 0001 0010 1111 1111 0010 1000 0000 0000 0000(0x0) 0001 0010 1111(0x12F) 1111 0010 1000(0xF28)
继续运行。
发现得到的内容不对。原因:变量var的页内偏移是0xf28,所以var的值为0x6b181000+0xf28的内容。但0地址拆分后得到的页内偏移也是0,读取到的数据是0x6b181000+0x0的内容。
如果要读到正确数据,需要将0地址的页内偏移变成0xF28。
1 2 3 4 5 6 7 8 9 10 11 12 13 #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { int var = 100 ; printf ("%p\n" ,&var); int offsets = (int )&var & 0xfff ; int * a = (int *)offsets; getchar(); printf ("%d\n" ,*a); getchar(); return 0 ; }
实验三:0地址实现shellcode执行 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include "stdafx.h" #include <windows.h> char shellcode[]={ 0x6a ,0 , 0x6a ,0 , 0x6a ,0 , 0x6a ,0 , 0xb8 ,0 ,0 ,0 ,0 , 0xff ,0xd0 , 0x83 ,0xc4 ,0x0c , 0xc3 };typedef int (*fnMessageBoxA) () ;int _tmain(int argc, _TCHAR* argv[]) { *(int *)&shellcode[9 ]=(int )MessageBoxA; printf ("%p\n" ,shellcode); getchar(); fnMessageBoxA msgbox = (fnMessageBoxA)0 ; msgbox(); getchar(); return 0 ; }
输出shellcode地址后拆分,将PTE赋值给0地址。
继续运行,弹出信息框。
CPU的分页单位是4k,也就是一个页。
操作系统分页是64k。
当我们申请内存时,返回的其实是拥有64k大小的内存地址,并且如果只申请不用,还有可能并不会挂物理页。
当我们再次申请内存时,如果上一次申请的64k的页内存中有未使用的内存,则会返回内存对应的地址。
还有就是0x0-0x10000的地址为无权限,是操作系统为了保护访问异常数据时出错,是一种保护机制。比如int *p=0,访问p时就会出错。另外r3和r0之前还有一块64k的空间也是无法使用的,这个区域隔离了用户和内核空间;防止用户程序跨越到内核空间中。
10、页属性
P位:有效位,1为有效,0为无效。
R/W:是否可读可写,0为可读,1为可读可写。
U/S:实际上是R3/R0,1为R3可访问;0为R3不可访问,R0可访问。
D:是否被写过,0为没有,1为被写入过。
A:是否被读过,0为没有,1为被读过。
PAT:是否存在下一个PTE,1为存在;0为不存在,如果为0则代表下一个是一个物理页。
G:是否为全局页,如果为1则表明TLB不进行刷新缓存(不绝对,只是有概率刷新)。
PS:物理页大小。为0则下一个页为4kb大小(小页),为1则下一个页为4mb大小(大页)。
实验一:R/W位 1 2 3 4 5 6 7 8 9 10 11 12 13 #include "stdafx.h" #include <windows.h> int _tmain(int argc, _TCHAR* argv[]) { char * p="12345" ; printf ("%p\n" ,p); system ("pause" ); p[0 ]='5' ; printf ("%s\n" ,p); system ("pause" ); return 0 ; }
由于字符串”12345”是一个常量,因此下边的修改行为存在异常。
使用CFF_Explorer查看.rdata区域的属性位0x40000040,为只读内存。表明系统在拉起该进程时为此段申请的内存属性为只读,因此无法进行修改。
将属性修改为0xC0000040后保存在运行。
运行成功。
Text 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 标志(属性块) 常用特征值对照表: [值:00000020h] [IMAGE_SCN_CNT_CODE // Section contains code.(包含可执行代码)] [值:00000040h] [IMAGE_SCN_CNT_INITIALIZED_DATA // Section contains initialized data.(该块包含已初始化的数据)] [值:00000080h] [IMAGE_SCN_CNT_UNINITIALIZED_DATA // Section contains uninitialized data.(该块包含未初始化的数据)] [值:00000200h] [IMAGE_SCN_LNK_INFO // Section contains comments or some other type of information.] [值:00000800h] [IMAGE_SCN_LNK_REMOVE // Section contents will not become part of image.] [值:00001000h] [IMAGE_SCN_LNK_COMDAT // Section contents comdat.] [值:00004000h] [IMAGE_SCN_NO_DEFER_SPEC_EXC // Reset speculative exceptions handling bits in the TLB entries for this section.] [值:00008000h] [IMAGE_SCN_GPREL // Section content can be accessed relative to GP.] [值:00500000h] [IMAGE_SCN_ALIGN_16BYTES // Default alignment if no others are specified.] [值:01000000h] [IMAGE_SCN_LNK_NRELOC_OVFL // Section contains extended relocations.] [值:02000000h] [IMAGE_SCN_MEM_DISCARDABLE // Section can be discarded.] [值:04000000h] [IMAGE_SCN_MEM_NOT_CACHED // Section is not cachable.] [值:08000000h] [IMAGE_SCN_MEM_NOT_PAGED // Section is not pageable.] [值:10000000h] [IMAGE_SCN_MEM_SHARED // Section is shareable(该块为共享块).] [值:20000000h] [IMAGE_SCN_MEM_EXECUTE // Section is executable.(该块可执行)] [值:40000000h] [IMAGE_SCN_MEM_READ // Section is readable.(该块可读)] [值:80000000h] [IMAGE_SCN_MEM_WRITE // Section is writeable.(该块可写)]
重新运行程序,并使用windbg查看地址的pde/pte。
PDE的属性为0x867-> 1000 0110 0111,其中R/W为1表明为可读可写,但是PTE的属性0x225-> 0010 0010 0101,R/W为0,表明为只读,将PTE的R/W改为1.
继续运行。
执行成功。
实验二:U/W位 1 2 3 4 5 6 7 8 9 10 11 12 13 #include "stdafx.h" #include <windows.h> int _tmain(int argc, _TCHAR* argv[]) { int p=0x80b93800 ; printf ("%p\n" ,p); system ("pause" ); *(int *)p=0x100 ; printf ("%s\n" ,p); system ("pause" ); return 0 ; }
R3中默认不可访问高位地址,因此代码运行时会异常。使用windbg查看GDT的页属性。由于高位地址在内存中共享,因此随意随意获取一个进程的CR3进行查看!process 0 0 system。
PDE的页属性0x063->0000 0110 0011,U/S为0表明只有R0可访问;PTE的页属性0x163->0001 0110 0011,U/S为0表明只有R0可访问.将PDE和PTE的U/S修改为1.
运行。
执行成功。
11、页基址 操作系统启动流程:BIOS(实模式)->NtLdr(构建保护模式)->操作系统(管理内存)。
x86模式下,虚拟内存有4GB大小,其中高2G是内核共享,该2G中被划分为不同的部分,用来做不同的管理。
管理4GB大小的内存需要以下大小的空间:
Text 1 ( 总大小 \ 页表大小 )* 指针单位 = ( 4GB \ 4K ) * 4 = 4mb
10-10-12模式下,有这样的指向 PDE->PTE->物理页;换句话来说,PTE由PDE管理,PDE由CR3管理。但由于这些都是物理地址,因此操作系统是怎么获取这些物理地址并管理的咧?
微软在管理内存时设计了一个特殊的基址0xC0000000,也就是页表基址。因为要管理4GB内存,因此页表基址的范围为0xC0000000~0xC0400000。这块区域中保存了系统中进程的PDE和PTE。
PDE保存的位置可以通过如下计算:
Text 1 2 3 0xC0000000 \ 4G * 4MB = 0xC0000000 \ 0x100000000 * 0x400000 = 0xC \ 0x10 * 0x400000 = 0xC * 0x40000 = 0x300000 BASE_PDE = 0xC0000000 + 0x300000 = 0xC0300000
而PTE则保存在0xC0000000~0xC02FFFFC。
所以对于所有PDE和PTE有如下公式:
Text 1 2 内存的PTE = 0xC0000000 + PTI*4 内存的PDE = 0xC0300000 + PDI*4
PTI和PDI分别为虚拟地址的PTE和PDE的索引。
实验一:验证页表基址 1 2 3 4 5 6 7 8 9 10 #include "stdafx.h" #include <windows.h> int _tmain(int argc, _TCHAR* argv[]) { int x=0 ; printf ("%p\n" ,&x); system ("pause" ); return 0 ; }
将输出的地址进行拆分0x0031f7c0
0000 0000 0000 0x0
0011 0001 1111 0x31f
0111 1100 0000 0x7c0
然后获取对应的PDE和PTE
PDE=0x4b96a867
PTE=0x4b675867
使用页表基址获取PDE和PTE:
PDE=0xC0300000+0x0=0xC0300000
PTE=0xC0000000+0x31f*4=0xC0000C7C
切换进程环境读取这两个内存。(由于自写的程序为R3,无法访问高位地址。因此切换到system.exe进程,反正都是共享)。
可以看到是获取正确的。因此操作系统可以通过页表基址获取到所有内存的PTE和PDE,然后进行管理。
实验二:页表基址获取自身CR3 微软有一个巧妙的设计,每一个PDE/PTE的0xC00位置都指向了自身,这样既满足了页表基址的管理范围,又实现了通过构造特殊地址来获取自身的CR3。
将0xC00构造为一个地址0xC0000000,拆分后得到
0x300
0x0
0x0
可以看到此时读取到的位置与CR3相同,由于10-10-12模式下拆3次读取是CPU的机制,因此可以构建这么一个地址0xC0300C00(地址范围在PDE范围内),拆分后如下:
0x300
0x300
0xc00
就可以巧妙地得到了自身的CR3。正常读取该地址,效果也相同。
实验三:逆向101012的MmIsAddressVaild 对于10-10-12模式的内核程序为ntoskrnl.exe;2-9-9-12为ntoskrnlpa.exe。
进入函数MmIsAddressValid进行分析
补充:后边学了29912后, & 80实际上是判断PS位,101012分页下页大小为4kb(小页),因此下边的cmp是判断如果ps=1则返回false.
12、29912分页 由于101012分页最大管理的内存为4G(2^102^10 2^12=4GB),在4GB无法满足后(迎接64时代),Intel开始这设计了新的分页模式,既2-9-9-12,又称PAE(物理地址扩展)分页。
原理 页大小依旧为4kb,也就是2^12;既要能管理更多内存,又要向下兼容4GB内存管理,所以只能扩大地址总线长度为8,因此PTE和PDE的保存数量变成了4096 / 8 = 512(2^9),剩余的两位用于保存一个叫做PDEPTE(Page Directory Entry Page Table Entry,页目录页表入口)的索引。
PDPTE PDEPTE是新引入的项,总共有4个(2^2),且每个项占8字节。在29912下,CR3不再是直接指向PDE而是指向该表项。
0-12位为属性位。
PDE
当PS=1时是大页,35-21位是大页的物理地址,这样36位的物理地址的低21位为0,这就意味着页的大小为2MB,且都是2MB对齐。
2MB哪里来的呢?2-9-9-12,后面的9和12合并成了一个大页,所以是21位,也就是2的21次方,所以是2MB。
PAT:Page Attribute Table,页属性表,当PDE的PS为0的时候就有没这一项,原因就是这个位是针对页的,目录当然没有。
XD/NX标志位 该位也叫做(DEP数据执行保护),在PAE分页模式下,PDE与PTE的最高位为XD/NX位。Intel 中称为XD,AMD 中称为NX,即No Excetion。 段的属性有可读、可写和可执行,页的属性有可读、可写。当RET执行返回的时候,如果把堆栈里面的数据指向一段提前准备好的数据 (把数据当作代码来执行,漏洞都是依赖这点,比如SQL注入也是),那么就会产生任意代码执行 的后果所以,Intel就在这方面做了硬件保护,设置了一个不可执行位 – XD/NX位 。当XD=1时,软件产生了溢出也没有关系,即使EIP蹦到了危险的“数据区”,也是不可以执行的
实验一:手动寻找物理地址 1 2 3 4 5 6 7 8 9 10 11 12 13 #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { char test[]="hello world!" ; printf ("%p\n" ,test); printf ("%s\n" ,test); getchar (); printf ("%s\n" ,test); getchar (); return 0 ; }
将输出的地址进行划分。
Text 1 2 3 4 5 0x001FF978 0000 0000 0000 0*8 0000 0000 0000 0*8 0001 1111 1111 1ff*8 1001 0111 1000 978
尝试修改字符串。
回到R3,再次输出。
内容已改变这里其实是改错了,应该用的是eb,这里用了ed,所以输出了e。
这里也可以看到最后找到的PTE地址头部为8,即1000,地址最高位为1,表明当前XD=1,数据为不可执行。
实验二:逆向29912的MmIsAddressVaild
总结:如果PDE为大页则直接返回true,否则常规检查PTE。
额外指令学习:
Text 1 2 3 4 5 6 7 kd> x nt!*IsAddress* 84008ea2 nt!PopIsAddressRangeValid (@PopIsAddressRangeValid@8) 84008ea2 nt!IopIsAddressRangeValid (_IopIsAddressRangeValid@8) 840913c8 nt!MiIsAddressValid (_MiIsAddressValid@8) 8400df4c nt!MmIsAddressValid (_MmIsAddressValid@4) *代表模糊匹配
Text 1 2 3 4 5 kd> !pte 84008ea2 VA 84008ea2 PDE at C0300840 PTE at C0210020 contains 001C3063 contains 04008121 pfn 1c3 ---DA--KWEV pfn 4008 -G--A--KREV
13、PAT\PCD\PWT CPU缓存 CPU缓存是介于CPU和物理内存之间的临时存储器。它容量比内存小得多,但读写速度比内存要快得多。越强大的CPU,CPU缓存大小越大。与TLB不同,TLB是线性地址和物理地址间的映射缓存。 而CPU缓存是物理地址和地址内的数值(内容)间的映射缓存。TLB+CPU缓存搭配可以大大加快读取速度。CPU读写内存时,会先去CPU缓存中寻找该物理地址,如果缓存中存在该物理地址,则读写全部对CPU缓存操作。
CPU缓存分为3级,L1,L2,L3。
L1缓存速度最快,容量最小,一个核一个。
L2缓存比L1容量大,速度略慢。一个核一个。
L3缓存比L2容量大,速度最慢。所有核共享一个。
在读写内存时,会依次从L1->L2->L3进行查找。若在L2中找到了,会将缓存更新到L1(提升下次访问速度)。若在L3中找到了,会将缓存更新到L1 L2。所以L1一直都在变。
缓存类型 Intel定义了如下类型;
UC:无缓存
WC:组合写(直写+回写),写入缓存,什么时候写入物理内存,由CPU决定。
WT:直写,即写到物理内存也写入缓存。
WP:写保护,置1后写内存直接异常。这里的写保护是局部,而CR4中的WP为全局
WB:回写,先写入缓存,过一段时间再写入物理内存。
UC-:-号代表弱,表示有时候会有缓存,有时候没有。
一块内存的缓存类型可以通过PAT-PCD-PWT进行组合判断。
MSR寄存器
MSR中保存着对PAT属性的定义。可以自己自定义
windbg使用rdmsr addr来查看保存的值。
拆分后得到如下:
PAT7
PAT6
PAT5
PAT4
PAT3
PAT2
PAT1
PAT0
00
07
01
06
00
07
01
06
当PAT=0,PCD=0,PWT=0,查右边第0个,也就是PAT*,得到6。6对应到上边的Table 11.10就是WB。
14、TLB TLB(Translation Lookaside Buffer,转换后备缓冲区),tlb保存的是一种<线性地址,物理地址>的映射关系,系统每次在读取地址数据时如果都需要进行拆分,那么带来的消耗巨大,因此Intel设计了一种缓存的形式来缓解开销。当我们读取某个地址时,CPU会首先查询这一块表是否存在映射关系,如果不存在则进行拆分后读取数据并返回,然后将线性地址与物理地址的映射关系保存到TLB表中,如果下一次在读取该地址,则会在TLB中查询对应的物理地址。极大的提高了效率。数据和代码指令各有有自己的TLB。
需要注意的是
Text 1 2 3 4 不同的CPU,TLB大小不同。只要Cr3发生变化,TLB立即刷新,一核一套TLB 由于操作系统的高2G映射基本不变,因此如果Cr3改了,TLB刷新的话,重建高2G以上很浪费。 所以PDE和PTE中有个G标志位(当PDE为大页时,G标志位才起作用),如果G位为1,刷新TLB时将不会刷新PDE/PTE G位为1的页,当TLB写满时,CPU根据统计信息将不常用的地址废弃,保留最常用的地址
TLB也分有下列种类:
物理页分为普通页(4KB)、大页(2MB/4MB),物理页又分为指令和数据。因此分为4种TLB
缓存一般页表(4KB)的指令页表缓存(Instruction-TLB)
缓存一般页表(4KB)的数据页表缓存(Data-TLB)
缓存大尺寸页表(2MB/4MB)的指令页表缓存(Instruction-TLB)
缓存大尺寸页表(2MB/4MB)的数据页表缓存(Data-TLB)
查找流程:
1、线性地址–>TLB缓存
2、没有找到 则 线性地址–>物理帧缓存(page struct cache)–>PDE页帧–>PTE页帧(PTE并没有被缓存 保存。
3、都没有则 线性地址–>PDPTE–>PDE->PTE。
INVLPG指令
简单说该指令用于删除某线性地址在TLB中的记录。
Text 1 invlpg dword ps:[0] ;删除0地址在tlb中的缓存
以下实验均为101012分页
实验一:CR3刷新TLB 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 #include <stdio.h> #include <windows.h> DWORD x, y, z;void __declspec(naked) PageOnNull () { __asm { push ebp mov ebp, esp sub esp, 0x100 push ebx push esi push edi } DWORD* pPTE; DWORD* pNullPTE; pNullPTE = (DWORD*)0xC0000000 ; pPTE = (DWORD*)(0xC0000000 + (0x50000000 >> 10 )); *pNullPTE = *pPTE; x = *(DWORD*)0 ; pPTE = (DWORD*)(0xC0000000 + (0x60000000 >> 10 )); *pNullPTE = *pPTE; y = *(DWORD*)0 ; __asm { mov eax, cr3 mov cr3, eax } z = *(DWORD*)0 ; __asm { pop edi pop esi pop ebx mov esp, ebp pop ebp iretd } }int main (int argc, char * argv[]) { DWORD* p5 = (DWORD*)VirtualAlloc ((LPVOID)0x50000000 , 4 , MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); DWORD* p6 = (DWORD*)VirtualAlloc ((LPVOID)0x60000000 , 4 , MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); if (p5 != (DWORD*)0x50000000 || p6 != (DWORD*)0x60000000 ) { printf ("Error alloc!\n" ); return -1 ; } *p5 = 0x1234 ; *p6 = 0x5678 ; __asm { int 0x20 } printf ("1. 读 0 地址数据:\n" ); printf ("*NULL = 0x%x \n\n" , x); printf ("2. 给 0 地址重新挂上物理页\n\n" ); printf ("3. 重新读取 0 地址数据:\n" ); printf ("*NULL = 0x%x \n\n" , y); printf ("4. 刷新 TLB \n\n" ); printf ("5. 再次读取 0 地址数据:\n" ); printf ("*NULL = 0x%x \n" , z); return 0 ; }
设计中断门:
Text 1 2 3 4 5 6 7 offset: 0x001213c0 P:1 DPL:3 TYPE:0xE Selector:1000 0012ee00`000813c0
写入描述符。
运行效果
可以发现,在x被赋值完成后,即使0地址被挂上了新的物理页,再对y进行赋值,x和y输出的值是相同的。但是在Cr3刷新后,0地址没有被挂上新的物理页,对z进行赋值后,z却输出了新的值。这是因为Cr3刷新前,0地址第一次被x访问时,线性地址与物理地址的对应关系被写入了TLB中,因此在对y赋值时,TLB的记录没有被刷新,访问的还是原来的物理页。
实验二:修改pte的G位禁止刷新TLB 由于需要给G位置1,因此这里使用windbg进行辅助。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 #include "stdafx.h" #include <windows.h> DWORD x, y, z;void __declspec(naked) PageOnNull () { __asm { push ebp mov ebp, esp sub esp, 0x100 push ebx push esi push edi } DWORD* pPTE; DWORD* pNullPTE; pNullPTE = (DWORD*)0xC0000000 ; pPTE = (DWORD*)(0xC0000000 + (0x50000000 >> 10 )); *pNullPTE = *pPTE; *pNullPTE = *pNullPTE | 0x100 ; x = *(DWORD*)0 ; pPTE = (DWORD*)(0xC0000000 + (0x60000000 >> 10 )); *pNullPTE = *pPTE; y = *(DWORD*)0 ; __asm { mov eax, cr3 mov cr3, eax } z = *(DWORD*)0 ; __asm { pop edi pop esi pop ebx mov esp, ebp pop ebp iretd } }int main (int argc, char * argv[]) { DWORD* p5 = (DWORD*)VirtualAlloc ((LPVOID)0x50000000 , 4 , MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); DWORD* p6 = (DWORD*)VirtualAlloc ((LPVOID)0x60000000 , 4 , MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); if (p5 != (DWORD*)0x50000000 || p6 != (DWORD*)0x60000000 ) { printf ("Error alloc!\n" ); return -1 ; } *p5 = 0x1234 ; *p6 = 0x5678 ; __asm { int 0x20 } printf ("1. 给0x50000000的PTE的G位赋值为1\n" ); printf ("2. 挂载并读 0 地址数据:\n" ); printf ("*NULL = 0x%x \n\n" , x); printf ("3. 给 0 地址重新挂上物理页\n\n" ); printf ("4. 重新读取 0 地址数据:\n" ); printf ("*NULL = 0x%x \n\n" , y); printf ("5. 刷新 TLB \n\n" ); printf ("6. 再次读取 0 地址数据:\n" ); printf ("*NULL = 0x%x \n" , z); return 0 ; }
运行效果。
实验结果证明G=1时,TLB不刷新。
实验三:INVLPG刷新TLB 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 #include "stdafx.h" #include <windows.h> DWORD x, y, z;void __declspec(naked) PageOnNull () { __asm { push ebp mov ebp, esp sub esp, 0x100 push ebx push esi push edi } DWORD* pPTE; DWORD* pNullPTE; pNullPTE = (DWORD*)0xC0000000 ; pPTE = (DWORD*)(0xC0000000 + (0x50000000 >> 10 )); *pNullPTE = *pPTE; x = *(DWORD*)0 ; pPTE = (DWORD*)(0xC0000000 + (0x60000000 >> 10 )); *pNullPTE = *pPTE; y = *(DWORD*)0 ; __asm{ invlpg dword ptr ds:[0 ] } z = *(DWORD*)0 ; __asm { pop edi pop esi pop ebx mov esp, ebp pop ebp iretd } }int main (int argc, char * argv[]) { DWORD* p5 = (DWORD*)VirtualAlloc ((LPVOID)0x50000000 , 4 , MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); DWORD* p6 = (DWORD*)VirtualAlloc ((LPVOID)0x60000000 , 4 , MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); if (p5 != (DWORD*)0x50000000 || p6 != (DWORD*)0x60000000 ) { printf ("Error alloc!\n" ); return -1 ; } *p5 = 0x1234 ; *p6 = 0x5678 ; __asm { int 0x20 } printf ("1. 读 0 地址数据:\n" ); printf ("*NULL = 0x%x \n\n" , x); printf ("2. 给 0 地址重新挂上物理页\n\n" ); printf ("3. 重新读取 0 地址数据:\n" ); printf ("*NULL = 0x%x \n\n" , y); printf ("4. 刷新 TLB \n\n" ); printf ("5. 再次读取 0 地址数据:\n" ); printf ("*NULL = 0x%x \n" , z); return 0 ; }
运行效果
实验四:CR4刷新TLB CR4中第八位PGE位
由于VS不支持CR4,因此使用硬编码
Text 1 2 009D126E | 0F20E0 | mov eax,cr4 | 009D1271 | 0F22E0 | mov cr4,eax |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 #include "stdafx.h" #include <windows.h> DWORD x, y, z;void __declspec(naked) PageOnNull () { __asm { push ebp mov ebp, esp sub esp, 0x100 push ebx push esi push edi } DWORD* pPTE; DWORD* pNullPTE; pNullPTE = (DWORD*)0xC0000000 ; pPTE = (DWORD*)(0xC0000000 + (0x50000000 >> 10 )); *pNullPTE = *pPTE; x = *(DWORD*)0 ; pPTE = (DWORD*)(0xC0000000 + (0x60000000 >> 10 )); *pNullPTE = *pPTE; y = *(DWORD*)0 ; __asm{ __emit 0x0F ; __emit 0x20 ; __emit 0xe0 ; mov ebx,0x80 ; not ebx; and eax,ebx; __emit 0x0F ; __emit 0x22 ; __emit 0xe0 ; } z = *(DWORD*)0 ; __asm { pop edi pop esi pop ebx mov esp, ebp pop ebp iretd } }int main (int argc, char * argv[]) { DWORD* p5 = (DWORD*)VirtualAlloc ((LPVOID)0x50000000 , 4 , MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); DWORD* p6 = (DWORD*)VirtualAlloc ((LPVOID)0x60000000 , 4 , MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); if (p5 != (DWORD*)0x50000000 || p6 != (DWORD*)0x60000000 ) { printf ("Error alloc!\n" ); return -1 ; } *p5 = 0x1234 ; *p6 = 0x5678 ; __asm { int 0x20 } printf ("1. 读 0 地址数据:\n" ); printf ("*NULL = 0x%x \n\n" , x); printf ("2. 给 0 地址重新挂上物理页\n\n" ); printf ("3. 重新读取 0 地址数据:\n" ); printf ("*NULL = 0x%x \n\n" , y); printf ("4. 刷新 TLB \n\n" ); printf ("5. 再次读取 0 地址数据:\n" ); printf ("*NULL = 0x%x \n" , z); return 0 ; }
运行效果:
15、控制寄存器
注意: 控制寄存器中有些位一旦置1,则代表对应功能直接启用。 有些位只有置1了,对应功能才可以被启用,具体启不启用看细化到PTE之类上面的控制位。 所以学习控制寄存器属性时需要留意。
CR1、CR5、CR6 操作系统不用
Cr0(全局控制器)
Protection Enabled:保护启用位,为1时是保护模式 ,为0时是实模式。 1时仅启用段保护机制。
Paging:页保护启用位(分页机制位),为1时代表启用分页保护机制。 为0时不启用分页保护机制(线性地址=物理地址)。
Text 1 PE=0 PG=0 处理器工作在实模式下 (由于实模式无法使用,因此CPU提供了一个虚拟8086模式,也叫虚拟实模式)PE=1 PG=0 处理器工作在保护模式下,但只有段机制的保护,没有页机制的保护PE=0 PG=1 处理器工作在实模式下。 由于PE为0,所以PG位即使为1也不会开启页保护。同时会触发一个 一般保护异常(GP:General-protection exception)。PE=1 PG=1 处理器工作在保护模式下,同时开启了段机制保护与页机制保护。
Write Protect:写保护位,当WP为1时,超级特权用户(0环)不可以向用户层只读地址写入数据。x86下置1可直接修改所有只读数据,x64引入VT后,修改CR0操作可能会被拦截且触发蓝屏。
Text 1 CPL<3时,此时为特权层,用户层地址A(US=1)对应的页为只读页。当WP为0时,特权层程序可以对地址A进行写的操作。当WP为1时,特权层程序无法对地址A进行写的操作。
与数学运算相关,不用了解。
Task Switched: 任务切换位。当call入任务门时,TS位置1。当从任务门中返回时,TS位置0。
Cache Disable : 缓存禁用位。 当置1时,所有缓存全部禁用。相当于缓存的总开关。
Alignment Mask:对齐位。为1时,启用对齐检查。为0时,关闭对齐检查。
Cr1(保留) Cr1寄存器在X86架构中为保留状态,并未使用。
Cr2(缺页异常地址) 当程序执行发生缺页异常时(E号中断),CPU会将触发了缺页异常的线性地址写入Cr2寄存器。供异常处理函数(E号中断)使用。
如 00401000:mov eax,[12345678],若12345678地址无效,则CR2中存12345678. 若401000地址无效,则CR2存00401000.
Cr3(PDBR) 页目录表基址。
PCD:PageLevelCacheDisabled,缓存禁用位。 为1时,禁用页表缓存。该位仅在CR0.PG=1且CR0.CD=0时才有效果。
PWT:PageLevelWriteThrough,页直写位。 为1时,页表使用直写缓存,为0时页表使用回写缓存。
PCD和PWT不同来源的不同影响:
当访问一个32位分页模式(101012)下的PDE时,PCD与PWT取自CR3寄存器。
当访问一个PAE模式(29912)下的PDE时,PCD与PWT取自PDPTE相关寄存器
当访问一个PTE时,PCD与PWT取自对应的PDE。
当访问一个从线性地址翻译过来的物理地址时,PCD与PWT取自与PTE或PDE。
Cr4(个性化控制器)
VME:Virtual-8086 Mode Extensions,虚拟8086模式扩展位。置1时,启用虚拟8086模式的中断和异常处理。置0时,不启用。
PVI:Protected-mode Virtual Interrupts,虚拟8086中断位。置1时,启用VIF(virtual interrupt flag)位。置0时,VIF位无效。
TSD:Time Stamp Disable,时间戳禁用位。置1时,只有特权级用户才可以执行RDTSC指令。置0时,所有用户都可以执行RDTSC指令。 该指令用于获取Tick值。
DE:Debugging Extensions,调试扩展位。置1时,调试寄存器DR4 DR5启用。置0时,DR4 DR5保留。DR4 DR5启用时作为DR6 DR7使用。
PSE:Page Size Extensions,页尺寸扩展位。置1时,PDE的PS位才有效果。置0时,PDE的PS位作废。
PAE:Physical Address Extensions,物理地址扩展位。 为1时,29912分页。为0时,101012分页。
MCE:Machine-Check Enable,机器检查启用位。置1时,会检查硬件连接。置0时,不会检查硬件连接。
PGE:Page Global Enable,全局页启用位。置1时,PDE PTE的G位才有效果。否则无效果;0时会刷新TLB。
PCE:Performance-Monitoring Counter Enable,性能监控计数器启用位。置1时,3环可以执行RDPMC指令。否则只能在特权级执行。
VMXE:VMX-Enable,VT标志位。为1时,代表处于VT模式下。为0时,未处于VT模式。特权级为-1
SMXE:SMX-Enable,更安全模式位(Safer-mode)。为1时,处于SM模式下。否则未处于。特权级为-2
SMEP和SMAP:SuperModeExecuteProtect,特权执行保护。为1时,特权级不能执行US=1的代码。SuperModeAccessProtect,特权访问保护。为1时,特权级不能访问US=1的数据。
在64位中,CR0.AM不再作为扩展位存在,而是控制SMEP与SMAP。当AM=0时,SMEP和SMAP失效。