x86保护模式
1、保护模式简介
CPU分有:实模式
、保护模式
、虚拟8086
模式,大多数操作系统都运行在保护模式下。
保护模式主要是用来保护寄存器、数据结构、指令,实际上也就是保护寄存器,因为cpu的数据都存放在寄存器中。
保护模式的特点:段和页。
- 实模式:16位系统DOS,访问的都是物理地址,不安全。
- 保护模式:将物理地址隔阂后,使用一种线性的虚拟地址来访问,相对实模式来说比较安全。并用段和页的特点来维护虚拟地址。
保护模式具体资料可以在 Intel白皮书第三卷 中查看。
2、段寄存器
2.1 段选择子
CPU一共有八个段寄存器:ES CS SS DS FS GS LDTR TR ,OD可见前6个,但GS段寄存器windows并未使用(32位下)。
如果运行在实模式下,则只有前四个有用。
如果是64位,则使用GS而不是FS。
当执行下列汇编代码时
1 | mov [0x12345678],eax |
实际上cpu所“看到的”代码如下:
1 | mov dword ptr ds:[0x12345678],eax |
💡 ds段寄存器通常时用来存放要访问数据的段地址。cs段寄存器表示要执行的代码。ss段寄存器表示堆栈的段地址。[…]则表示一个内存单元,比如ds:[1],cs:[1],ss:[1]。——王爽《汇编语言》
段寄存器结构: 共96位, 16位可见,80位不可见。
1 | struct SegMen{ |
读段寄存器指令:mov ax,es 只能读16位(可见部分)
写段寄存器指令:mov ds,ax 写了96位的。
段寄存器可以用mov指令读写,但是LDTR和TR除外。
加载段描述符至段寄存器的指令共有三种:
- mov ss,ax 使用mov指令
- les lss lds lfs lgs修改对应的段寄存器
- cs不能通过上述指令改变,否则会导致EIP的改变,必须保证cs与eip一起改。后文会讲解如何修改CS并在需要时提升权限。
其中选择子(Selector)有如下结构:
打开OD,随便加载一个程序可以看到段寄存器对应的选择子。
以fs的选择子为例进行解析:
1 | fs=0x0053 => 0000 0000 0101 0011 |
GDT:全局描述表(Global Description Table),在操作系统加载完毕后就存在的一快内存。实际上就是一个数组,每一个元素就是一个描述符,多个组合一起就构成了全局描述符表。而每一个描述符共64位,包含了以下的这些信息:段基址、段长度、属性。段寄存器通过解析选择子后得到索引后在GDT中跳转获取对应描述符。
LDT:局部描述表(Local Description Table),与GDT功能一致,但不能单独存在,只能嵌套在GDT中。
- windbg获取GDT
GDT可以使用windbg的命令可以查看gdt表的地址:
1 | gdtr寄存器(windbg伪寄存器,是windbg通过sgdt lgdt指令获取的,为了方便用户,才模拟了一个寄存器叫gdtr,实际是没有这个寄存器的) : |
- r3代码获取GDT
通过指令sgdt获取。其中共获取到6个字节,前两个字节位gdt寄存器的大小,后面四个字节为gdt的地址。
1 |
|
2.2 段描述符
段描述符有如下结构:
将gdt的一个段描述符0x00cff300 0000ffff
进行拆分可得到如下
1 | base:0x00000000 |
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位
描述符类型位。 为0时,是系统段描述符。 为1时,是代码或数据段描述符。具体类型需要搭配type属性来判断。
Type位
由S位决定了具体是代码段还是数据段描述符
- 当S=1,type表示为数据段的描述
数据段: 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:非一致代码段(各调各的)
- 当S=0,type表示为系统描述,门
(小于8是16位的系统描述,大于8是32位)
针对E位的理解与实验:
左边为向上扩展,右边为向下扩展。红色代表可以访问,绿色不可访问
Tip:段描述符有没有4G,首先是看E位的拓展方向,其次是看Base和limit之间的大小。
首先构造一条段描述符.
1 | 00cff700`0000ffff |
通过windbg的e[b|d|D|f|p|q|w] address [Values]
指令可以修改GDT中保存的段描述符。修改8003f090(GDTR = 8003f000),然后输出gdt查看前20个描述表,dq address l20
。(注意是小写L,不是数字1)。
实验一:ds
1 | //code1: |
运行出错。
分析:异常断在了赋值var的地方。首先在描述符中attr为0x0cf7,Type=0111表示为向下扩展、可读写、已访问。根据上图可知,如果E位为向下扩展,则base+limit这段区域是无法访问的即(0x00000000-0xffffffff),因此在写数据时发生了错误。修复方法为将E位修改为向上扩展(E=0),或者将limit修改为一个小范围值,使得其他区域的内存可以访问(在demo2实现)。
实验成功!
实验二:ss
首先将0x8003f090修改回0x00cff700`0000ffff。
1 |
|
运行同样报错。
分析:需要注意的是中断打在了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位是否存在?
- Attribute探测
1 | int main(int argc,char* argv[]){ |
1 | int main(int argc,char* argv[]){ |
- Base探测
1 | int main(int argc,char* argv[]){ |
- Limit探测
1 | int main(int argc,char* argv[]){ |
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 |
|
运行正常!
实验二:CPL=3 RPL=0 DPL=3
修改GDT+0x48为 00cff300`0000ffff
1 |
|
运行正常!
实验三:CPL=3 RPL=3 DPL=0
修改GDT+0x48为 00cf9300`0000ffff
1 |
|
运行失败!
实验四:CPL=3 RPL=0 DPL=0
修改GDT+0x48为 00cf9300`0000ffff
1 |
|
运行失败!
总结:
数据段下RPL<=DPL && CPL<=DPL(数值上)
,实验四失败是因为此时运行的环境为3环。
代码段(跨段跳转-不提权):
跳转指令有call、jmp两种,格式如下
1 | CALL FAR CS:EIP |
为了避免干扰,需要关闭增量链接
和随机地址
。
长跳转与短跳转
1 | 汇编写法: |
长跳转的压栈与短跳转(普通的call)压栈略有区别。短跳转会将下一行代码的地址入栈后进行跳转;而长跳转会将当前cs和下一行代码的地址入栈再跳转。
执行前
执行后
因此ret指令已经不适合长跳转的返回,取而代之的是retf
。
实验一:CPL=3 RPL=3 DPL=3
构造描述符00cffb00`0000ffff(type:1011,非一致代码段)
1 |
|
运行正常!
实验二:CPL=3 RPL=0 DPL=3
构造描述符00cffb00`0000ffff(type:1011,非一致代码段)
1 |
|
运行正常,但会发现cs并没有被修改为0x48。
原因:长跳转时有那么一个计算 RPL|DPL -> 0|3 = 3 => 4B
实验三:CPL=3 RPL=3 DPL=0
构造描述符00cf9b00`0000ffff(type:1011,非一致代码段)
1 |
|
运行失败!
实验四:CPL=3 RPL=0 DPL=0
构造描述符00cf9b00`0000ffff(type:1011,非一致代码段)
1 |
|
运行失败!
实验五:CPL=3 RPL=3 DPL=3 (retf)
构造描述符00cffb00`0000ffff(type:1011,非一致代码段)
1 |
|
在retf处下断点,然后修改保存的cs为18(0环)。
然后运行,发现异常。
总结:
跨段代码无法进行提权(提权需要门)。
然后火哥是这么说的!!!!!
- JMP和CALL 只能用于同权限,或者往高权限跳。(实验一到实验五我是没看出来这个,但是火哥说了就先记住!嘤嘤嘤~~~~)
- retf 和 iretd 只能用于同权限,或者往低权限返回。(根据实验五可以推测出来)
3、调用门
门,通往新世界的通道。与长跳转类似也是通过call far cs:eip(jmp不行)
进行调用。当cs对应的段描述符的S=0时,CPU会识别这个描述符是一个门,每个门格式不同。调用门格式如下:
P:表示该描述符是否有效。
DPL:当前描述符的权限。
Type:1100,表明是一个调用门。
ParamCount:调用参数个数。
Segment Selector:门的选择子。
Offset in Segment:门的偏移,跳转地址:门的选择子.base + 偏移。
实验一:提权R3进入R0环
构造一个3环->0环的描述符1234EC00·00085678
1 | offset in segment:0x12345678 |
1 |
|
在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 |
|
打开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 |
|
然后构造描述符。
1 | offset:0x00401000 |
然后在idtr+0x100处写入我们的描述符。
代码中添加int的调用。
1 |
|
运行后,windbg中断。
输入g命令继续执行,此时发现r3层异常中断,原因为int 3造成,去掉int 3即可正常运行。
实验二:堆栈影响
重新运行实验一的代码,在执行int 0x20前,观察寄存器和段寄存器的值。
然后继续执行。
可以看到中断门先后压入了ss、esp、efl、cs、下一条语句的地址。因此进入中断门的堆栈结构如下:
实验三:IF
将代码修改为如下:
1 |
|
运行后windbg断下,输入dds esp
查看堆栈
堆栈值未变,那么输入uf 401000
查看一下函数的反汇编.
dd一下变量val的值。
神奇的发现堆栈中保存的efl与获取到的efl不一样!!!!!!原因是,int 3同时也是进入中断门,因此当前堆栈保存的是int 3的efl。分别将0x46和0x246转换为二进制。
1 | 0x046 = 0000 0100 0110 |
可以发现第九位有区别,第九位在eflags文档中为IF为,中断启用标志。
可屏蔽中断请求:如键盘输入,鼠标点击都是一次可屏蔽中断请求。
不可屏蔽中断请求:CPU必须立即无条件响应的请求,如电源断电。
大概意思就是我们自己的int 0x20中断后无法对鼠标或者键盘之类的外设输入进行响应,但是int 3可以。
1 | cli; //清除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 |
|
可以看到此时EFL为246.
说明它禁止响应可屏蔽中断。
IDT中没有用到陷阱门。
然后中断门和陷阱门的区别就是:中断门由于IF为为0,表明不会被其他中断打断,而陷阱门有可能会被打断。
6、任务段
任务段(TSS:Task-State Segment)描述的是一个任务环境,用于进程和线程的环境的切换。TSS是一种结构数据
且保存在内存中。内存的位置被描述在GDT中。
由于任务太过依赖于GDT表中的任务段描述和内存块,因此Windows 和Linux 都没有采用任务段(不想被CPU限制)。
TSS结构如下:
- ESP0、SS0:从R3切换至R0时,切换的堆栈数据。
- EIP:任务的地址地址。
- 通用寄存器、段选择子、EFLAGS:自定义。
- CR3:来源R3
- Previous Task Link:上一个TSS的选择子。
TSS是最小104字节的内存
。
1 | Avoid placing a page boundary in the part of the TSS that the processor reads during a task switch (the first 104 |
在windbg中使用命令dt structName [addr]
来查看结构体数据,查看TSS命令如下:
1 | dt _KTSS |
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 |
|
运行,查看函数a的地址和程序的CR3。
构造TSS描述符。
1 | 0000E940`503020ab |
继续运行后发现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结构保存着真实的返回地址!!!
1 | iretd: |
因此如果添加了int 3断点,则需要把eflags给恢复回来。
1 | __declspec(naked) void a() |
7、任务门
关闭增量链接、关闭随即地址
任务门结构图如下:
Windows-双重异常
Windows使用的任务门主要有作为不可屏蔽中断
和双重异常
。
双重异常:系统处理异常时触发的异常。Windows使用了任务门在实现双重异常是为了在触发双重异常时,可以将当前环境保存到TSS中,让开发者有信息进行调试、排查问题(系统无法解决双重异常,因此将触发时的环境保存在TSS中,然后反馈给开发者,让开发者自行解决)。
1 | int 0x8 |
实验一:构造任务门
选择0x48的位置来存放TSS。
构造任务门,写入idt+0x100
1 | 0000e500`00480000 |
1 |
|
构造TSS
1 | 0000e940`50300068 |
可以发现已经断下。
输入g继续执行。
8、101012分页
Windows x86模式下有29912分页和101012分页,其中默认为29912分页。
设置101012分页
使用EasyBCD工具设置系统配置为如下:
重启即可。
实验一:线性地址转物理地址
确保系统当前分页为101012模式!!!
1 |
|
首先获取变量val的逻辑地址。
然后将地址0040312c
以101012格式拆分。
1 | 0040312c |
使用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位的偏移组合得到最终的物理地址。
当写数据时
为了避免一直访问物理页(内存条),造成资源开销大,因此使用了L1、L2、L3缓存,也就是俗称的一级、二级、三级缓存。将数据写到缓存中,每隔一个时钟周期后才将缓存中的数据写到内存条上,这个操作叫做WB(写回绕,Write Back)
其中L1是所有CPU共享,L2、L3等是每个CPU都有一个
。
如果在TLB中找不到线性地址和物理页的映射(TLB miss),则会操作MMU模块将线性地址拆分后存入TLB缓存中。
实验二:将同一个线性地址转成物理地址
关闭随机地址。变量设置为全局或者静态。
将实验一的程序编译后,同时运行两个。
可以看到线性地址一致,但CR3不同。
可以看到两个线性地址虽然相同,但是对应的物理地址不同。可以得出结论,同一个线性地址可以被映射为多个物理地址
。
9、探索0地址
关闭增量和随即地址。
实验一:0地址挂物理页
1 |
|
输出var的地址,然后进行101012拆分。
1 | 0x00405000 |
然后通过CR3寻找物理页。
同样的方法,寻找0地址的物理页。
发现0地址没有pte,将变量的pte挂上。
继续运行程序。
实验二:页内偏移对齐
将实验一的全局变量改为局部变量。
1 |
|
可以看到var的地址已经不是000结尾了,然后按照实验一的方法对0地址挂上pte。
1 | 0x0012ff28 |
继续运行。
发现得到的内容不对。原因:变量var的页内偏移是0xf28,所以var的值为0x6b181000+0xf28的内容。但0地址拆分后得到的页内偏移也是0,读取到的数据是0x6b181000+0x0的内容。
如果要读到正确数据,需要将0地址的页内偏移变成0xF28。
1 |
|
实验三:0地址实现shellcode执行
1 |
|
输出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 |
|
由于字符串”12345”是一个常量,因此下边的修改行为存在异常。
使用CFF_Explorer查看.rdata区域的属性位0x40000040,为只读内存。表明系统在拉起该进程时为此段申请的内存属性为只读,因此无法进行修改。
将属性修改为0xC0000040后保存在运行。
运行成功。
1 | 标志(属性块) 常用特征值对照表: |
重新运行程序,并使用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 |
|
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大小的内存需要以下大小的空间:
1 | ( 总大小 \ 页表大小 )* 指针单位 = ( 4GB \ 4K ) * 4 = 4mb |
10-10-12模式下,有这样的指向 PDE->PTE->物理页;换句话来说,PTE由PDE管理,PDE由CR3管理。但由于这些都是物理地址,因此操作系统是怎么获取这些物理地址并管理的咧?
微软在管理内存时设计了一个特殊的基址0xC0000000,也就是页表基址。因为要管理4GB内存,因此页表基址的范围为0xC0000000~0xC0400000。这块区域中保存了系统中进程的PDE和PTE。
PDE保存的位置可以通过如下计算:
1 | 0xC0000000 \ 4G * 4MB = 0xC0000000 \ 0x100000000 * 0x400000 = 0xC \ 0x10 * 0x400000 = 0xC * 0x40000 = 0x300000 |
而PTE则保存在0xC0000000~0xC02FFFFC。
所以对于所有PDE和PTE有如下公式:
1 | 内存的PTE = 0xC0000000 + PTI*4 |
PTI和PDI分别为虚拟地址的PTE和PDE的索引。
实验一:验证页表基址
1 |
|
将输出的地址进行拆分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^10*2^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位为属性位。
- P位:有效位,0无效,1有效。
- PWT:Page Write Through。直写,后边TLB会讲到。
- PCD:Page Cache Disable。禁止写缓存,后边TLB会讲到。
- Avail:操作系统用,CPU不用。
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 |
|
将输出的地址进行划分。
1 | 0x001FF978 |
尝试修改字符串。
回到R3,再次输出。
内容已改变这里其实是改错了,应该用的是eb,这里用了ed,所以输出了e
。
这里也可以看到最后找到的PTE地址头部为8,即1000,地址最高位为1,表明当前XD=1,数据为不可执行。
实验二:逆向29912的MmIsAddressVaild
总结:如果PDE为大页则直接返回true,否则常规检查PTE。
额外指令学习:
- x:搜索导入表
1 | kd> x nt!*IsAddress* |
- !pte:显示虚拟地址的PTE和PDE基地址
1 | kd> !pte 84008ea2 |
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
。
LA:线性地址
PA:物理地址
ATTR:在10-10-12分页模式下:ATTR = PDE属性 & PTE属性。在2-9-9-12分页模式下:ATTR = PDPTE属性 & PDE属性 & PTE属性
LRU:统计信息。由于TLB的大小有限,因此当TLB被写满、又有新的地址即将写入时,TLB就会根据统计信息来判断哪些地址是不常用的,从而将不常用的记录从TLB中移除。
需要注意的是
1 | 不同的CPU,TLB大小不同。只要Cr3发生变化,TLB立即刷新,一核一套TLB |
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中的记录。
1 | invlpg dword ps:[0] ;删除0地址在tlb中的缓存 |
以下实验均为101012分页
实验一:CR3刷新TLB
1 |
|
设计中断门:
1 | offset: 0x001213c0 |
写入描述符。
运行效果
可以发现,在x被赋值完成后,即使0地址被挂上了新的物理页,再对y进行赋值,x和y输出的值是相同的。但是在Cr3刷新后,0地址没有被挂上新的物理页,对z进行赋值后,z却输出了新的值。这是因为Cr3刷新前,0地址第一次被x访问时,线性地址与物理地址的对应关系被写入了TLB中,因此在对y赋值时,TLB的记录没有被刷新,访问的还是原来的物理页。
实验二:修改pte的G位禁止刷新TLB
由于需要给G位置1,因此这里使用windbg进行辅助。
1 |
|
运行效果。
实验结果证明G=1时,TLB不刷新。
实验三:INVLPG刷新TLB
1 |
|
运行效果
实验四:CR4刷新TLB
CR4中第八位PGE位
由于VS不支持CR4,因此使用硬编码
1 | 009D126E | 0F20E0 | mov eax,cr4 | |
1 |
|
运行效果:
15、控制寄存器
注意: 控制寄存器中有些位一旦置1,则代表对应功能直接启用。 有些位只有置1了,对应功能才可以被启用,具体启不启用看细化到PTE之类上面的控制位。 所以学习控制寄存器属性时需要留意。
CR1、CR5、CR6 操作系统不用
Cr0(全局控制器)
- PE
Protection Enabled
:保护启用位,为1时是保护模式 ,为0时是实模式。 1时仅启用段保护机制。
- PG
Paging
:页保护启用位(分页机制位),为1时代表启用分页保护机制。 为0时不启用分页保护机制(线性地址=物理地址)。
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 处理器工作在保护模式下,同时开启了段机制保护与页机制保护。 |
- WP
Write Protect
:写保护位,当WP为1时,超级特权用户(0环)不可以向用户层只读地址写入数据。x86下置1可直接修改所有只读数据,x64引入VT后,修改CR0操作可能会被拦截且触发蓝屏。
1 | CPL<3时,此时为特权层,用户层地址A(US=1)对应的页为只读页。当WP为0时,特权层程序可以对地址A进行写的操作。当WP为1时,特权层程序无法对地址A进行写的操作。 |
- MP EM ET NE NW
与数学运算相关,不用了解。
- TS
Task Switched
: 任务切换位。当call入任务门时,TS位置1。当从任务门中返回时,TS位置0。
- CD
Cache Disable
: 缓存禁用位。 当置1时,所有缓存全部禁用。相当于缓存的总开关。
- AM
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模式。特权级为-1SMXE:
SMX-Enable,更安全模式位(Safer-mode)
。为1时,处于SM模式下。否则未处于。特权级为-2SMEP和SMAP:
SuperModeExecuteProtect,特权执行保护
。为1时,特权级不能执行US=1的代码。SuperModeAccessProtect,特权访问保护
。为1时,特权级不能访问US=1的数据。
在64位中,CR0.AM不再作为扩展位存在,而是控制SMEP与SMAP。当AM=0时,SMEP和SMAP失效。