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位下)。

Untitled

如果运行在实模式下,则只有前四个有用。

如果是64位,则使用GS而不是FS。

当执行下列汇编代码时

1
mov [0x12345678],eax

实际上cpu所“看到的”代码如下:

1
2
3
4
mov dword ptr ds:[0x12345678],eax
//ds.base+0x12345678
//cs.base+0x12345678
//ss.base+0x12345678

💡 ds段寄存器通常时用来存放要访问数据的段地址。cs段寄存器表示要执行的代码。ss段寄存器表示堆栈的段地址。[…]则表示一个内存单元,比如ds:[1],cs:[1],ss:[1]。——王爽《汇编语言》

段寄存器结构: 共96位, 16位可见,80位不可见。

Untitled_1

1
2
3
4
5
6
struct SegMen{  
WORD Selector;//16位
WORD Attributes;//16位
DWORD Base;//32位
DWORD Limit;//32位
};

读段寄存器指令: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)有如下结构:

Untitled

打开OD,随便加载一个程序可以看到段寄存器对应的选择子。

Untitled_2

以fs的选择子为例进行解析:

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中。

  • windbg获取GDT

GDT可以使用windbg的命令可以查看gdt表的地址:

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    查看固定数量元素的内存

Untitled_3

  • r3代码获取GDT

通过指令sgdt获取。其中共获取到6个字节,前两个字节位gdt寄存器的大小,后面四个字节为gdt的地址。

Untitled_1

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;
}

Untitled_2

2.2 段描述符

段描述符有如下结构:

Untitled_3

将gdt的一个段描述符0x00cff300 0000ffff进行拆分可得到如下

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次方)。

Untitled_4

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:非一致代码段(各调各的)

Untitled_5

  • 当S=0,type表示为系统描述,门

(小于8是16位的系统描述,大于8是32位)

Untitled_17

针对E位的理解与实验:

左边为向上扩展,右边为向下扩展。红色代表可以访问,绿色不可访问

左边为向上扩展,右边为向下扩展。红色代表可以访问,绿色不可访问

Tip:段描述符有没有4G,首先是看E位的拓展方向,其次是看Base和limit之间的大小。

首先构造一条段描述符.

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)。

Untitled_7


实验一:ds

1
2
3
4
5
6
7
8
9
10
11
12
13
//code1:
#include "stdafx.h"
unsigned char var = 0; //全局变量
int _tmain(int argc, _TCHAR* argv[])
{
_asm{
mov ax,0x93 //10010 011 -> index = 18 * 8 = 144 (0x90)
mov ds,ax
mov dword ptr ds:[var],0x20
}
printf("%X\n",var);
return 0;
}

运行出错。

Untitled_8

分析:异常断在了赋值var的地方。首先在描述符中attr为0x0cf7,Type=0111表示为向下扩展、可读写、已访问。根据上图可知,如果E位为向下扩展,则base+limit这段区域是无法访问的即(0x00000000-0xffffffff),因此在写数据时发生了错误。修复方法为将E位修改为向上扩展(E=0),或者将limit修改为一个小范围值,使得其他区域的内存可以访问(在demo2实现)。

Untitled_9

Untitled_10

实验成功!


实验二: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;
}

运行同样报错。

Untitled_11

分析:需要注意的是中断打在了printf上,说明我们上边的ASM代码没问题!那么一个新问题来了,为什么demo1中中断打在了var赋值上?虽然我们显式使用了ds段来描述变量var,但仍存在一个问题,变量var属于堆栈地址,所以实际上mov dword ptr ds:[var]被编译器翻译成了mov dword ptr ss:[var]

Untitled_12

Untitled_13

因为我们没有修改ss段,所以上面对ss段的操作没问题。根据单步执行,可以发现中断的位置是printf函数。

Untitled_14

根据我们构造的描述符可知,base=0,limit的范围为0xFFFFFFFF,然后又属于向下扩展,所以0x0-0xFFFFFFFF的区域无法访问,printf中必然有用到ds段的数据,但这些数据有没有访问权限,因此访问中断了。修复方法为将limit设置为一个很小的范围,这样其他的区域就可以访问了。比如0x1FFF。(00c0f700`00000001)

Untitled_15

Untitled_16

实验完成!


使用windbg的dg segment命令可以快速查看段寄存器对应的段描述符。

Untitled_4

Untitled_5

2.3 段寄存器证明

读的时候只能读到16位(选择子),但写的时候却写入了96位。如何证明剩下的80位是否存在?

Untitled_6

  • Attribute探测
1
2
3
4
5
6
7
8
int main(int argc,char* argv[]){
int var = 0;
__asm{
mov ax,ss                //ss可读可写
mov ds,ax                //ds可读可写
mov dword ptr ds:[var],eax        //ds此时为ss,不报错,说明两个段寄存器权限相同
}
}
1
2
3
4
5
6
7
8
int main(int argc,char* argv[]){
int var = 0;
__asm{
mov ax,cs            //cs可读可执行不可写
mov ds,ax            //ds可读可写
mov dword ptr ds:[var],eax        //ds此时为cs,写入时报错,说明Attribute属性存在
}
}
  • Base探测
1
2
3
4
5
6
7
8
9
int main(int argc,char* argv[]){
int var = 0;
__asm{
mov ax,fs            //fs 的 base为TEB  用ds编译不过去
mov gs,ax            //gs 的 base为0
mov eax,gs:[0]        //gs此时为fs,写入不出错,说明Base属性存在  fs.base+0
mov dword ptr gs:[var],eax
}
}
  • Limit探测
1
2
3
4
5
6
7
8
9
10
int main(int argc,char* argv[]){
int var = 0;
__asm{
mov ax,fs            //fs 的 base为TEB  用ds编译不过去
mov gs,ax            //gs 的 base为0
mov eax,gs:[0x1000]        //写入出错,超过了fs的limit,说明Limit属性存在 fs.base+0x1000
//mov eax,ds:[0x7FFDF000+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 //RPL=3 CPL=3 DPL=3
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 //RPL=0 CPL=3 DPL=3
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 //RPL=3 CPL=3 DPL=0
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 //RPL=3 CPL=3 DPL=0
mov ds,ax
mov dword ptr[var],10
}
return 0;
}

运行失败!

总结:

数据段下RPL<=DPL && CPL<=DPL(数值上),实验四失败是因为此时运行的环境为3环。

代码段(跨段跳转-不提权):

跳转指令有call、jmp两种,格式如下

1
2
3
4
5
CALL FAR CS:EIP
JMP FAR CS:EIP

;CALL/JMP FAR 0x20:0x004183D7
;0x20为新的cs寄存器,通过拆分新的cs寄存器得到段描述符后根据其base+0x004183D7进行跳转。

为了避免干扰,需要关闭增量链接随机地址

image-20221028142955249

image-20221028142922784

长跳转与短跳转

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和下一行代码的地址入栈再跳转。

image-20221028171858280

执行前

image-20221028172347806

执行后

image-20221028172310947

因此ret指令已经不适合长跳转的返回,取而代之的是retf

image-20221028172807035

实验一: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}; //前四个字节为跳转的地址,后两个字节为新的CS
*(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}; //前四个字节为跳转的地址,后两个字节为新的CS
*(int*)&buf[0]=(int)test;
_asm{
call fword ptr [buf]
}
return 0;
}

运行正常,但会发现cs并没有被修改为0x48。

image-20221028184231219

原因:长跳转时有那么一个计算 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}; //前四个字节为跳转的地址,后两个字节为新的CS
*(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}; //前四个字节为跳转的地址,后两个字节为新的CS
*(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}; //前四个字节为跳转的地址,后两个字节为新的CS
*(int*)&buf[0]=(int)test;
_asm{
call fword ptr [buf]
}
return 0;
}

在retf处下断点,然后修改保存的cs为18(0环)。

image-20221028191429965

然后运行,发现异常。

image-20221028191518915

总结:

跨段代码无法进行提权(提权需要门)。

然后火哥是这么说的!!!!!

  • JMP和CALL 只能用于同权限,或者往高权限跳。(实验一到实验五我是没看出来这个,但是火哥说了就先记住!嘤嘤嘤~~~~)
  • retf 和 iretd 只能用于同权限,或者往低权限返回。(根据实验五可以推测出来)

3、调用门

门,通往新世界的通道。与长跳转类似也是通过call far cs:eip(jmp不行)进行调用。当cs对应的段描述符的S=0时,CPU会识别这个描述符是一个门,每个门格式不同。调用门格式如下:

image-20221028194713225

  • P:表示该描述符是否有效。

  • DPL:当前描述符的权限。

  • Type:1100,表明是一个调用门。

  • ParamCount:调用参数个数。

  • Segment Selector:门的选择子。

  • Offset in Segment:门的偏移,跳转地址:门的选择子.base + 偏移。

实验一:提权R3进入R0环

构造一个3环->0环的描述符1234EC00·00085678

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地址后填充到门描述符。

image-20221028200858575

image-20221028200959253

然后单步执行调用门后int 3断点被执行。

image-20221028201116857

使用u[f] addr [-lxxxx]查看汇编,其中f可以直接查看函数的所有汇编。

image-20221028201232089

说明此时已经跨段提权进入0环。输入g命令继续执行,此时发现r3层异常中断,原因为int 3造成,去掉int 3即可正常运行。

与长跳转不同的是,由于调用门为R3进入到R0,由于两个权限的地址范围和权限不同,因此调用门在call的时候会将ss、esp、cs、下一行代码地址,进行入栈。

image-20221028202041627

image-20221028202349714

实验二:调用0环函数(DbgPrint)

首先使用uf nt!DbgPrint获取函数地址

image-20221028202522357

然后添加函数声明。

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 //保存R3的fs
mov ax, 0x30//切换R0的fs
mov fs, ax;

lea eax, [msg]
push eax
call myDbgPrint
add esp, 4

pop fs //恢复R3的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进行监视。

image-20221028203140417

运行R3程序。

image-20221028203401508

成功输出,但是不知道为啥DbgView没捕获到。

补充:设置完DbgView了之后重新打开就行了。

4、中断门

硬件叫做中断,软件叫做异常。

中断门,CPU执行如下的指令:INT N,查询的是另外一张表,这张表叫IDT表。

image-20221029154208996

表的含义与调用门基本一致。这里面的D代表了default默认是1。windbg同样也提供了类似GDT表查询的指令,r idtrr idtl

image-20221029154542770

使用dq idtr查看idt表。

image-20221029154614726

其中每个中断描述符代表了一个中断函数。常见R3层的int 3指令对应的是83e5ee00·00084fc0,拆分后得到的中断函数(Offset)为0x83e54fc0,使用windbg查看该地址的反汇编。

int3 与 int 3作用相同,都是查询IDT表index为3的描述符。但int3只有一个字节(0xCC),int 3占两个字节(CD 03)

image-20221029155527981

windbg同样提供了!idt n指令用于查看对应中断序号的中断函数。

image-20221029155648779

实验一:构造中断门

关闭增量链接和随机地址。

首先获取函数地址。

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 //中断门使用的是iretd,d代表dword,32位。实模式(16)
}
}

int _tmain(int argc, _TCHAR* argv[])
{
printf("%p\n",test);
getchar();
return 0;
}

image-20221029161147825

然后构造描述符。

1
2
3
4
5
6
offset:0x00401000
segment selector:00080环)
P:1
DPL:3(确保3环有权限访问该中断描述符)

=0040EE00`00081000

然后在idtr+0x100处写入我们的描述符。

image-20221029161254790

代码中添加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 //0x100 / 0x8 = 0x20
}
return 0;
}

运行后,windbg中断。

image-20221029162514739

输入g命令继续执行,此时发现r3层异常中断,原因为int 3造成,去掉int 3即可正常运行。

实验二:堆栈影响

重新运行实验一的代码,在执行int 0x20前,观察寄存器和段寄存器的值。

image-20221029162813573

然后继续执行。

image-20221029162956265

可以看到中断门先后压入了ss、esp、efl、cs、下一条语句的地址。因此进入中断门的堆栈结构如下:

image-20221030123121116

实验三: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 //获取efl
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查看堆栈

image-20221030122710511

堆栈值未变,那么输入uf 401000查看一下函数的反汇编.

image-20221030122744380

dd一下变量val的值。

image-20221030122813632

神奇的发现堆栈中保存的efl与获取到的efl不一样!!!!!!原因是,int 3同时也是进入中断门,因此当前堆栈保存的是int 3的efl。分别将0x46和0x246转换为二进制。

1
2
0x046 = 0000 0100 0110
0x246 = 0010 0100 0110

可以发现第九位有区别,第九位在eflags文档中为IF为,中断启用标志。

image-20221029164026531

  • 可屏蔽中断请求:如键盘输入,鼠标点击都是一次可屏蔽中断请求。

  • 不可屏蔽中断请求:CPU必须立即无条件响应的请求,如电源断电。

大概意思就是我们自己的int 0x20中断后无法对鼠标或者键盘之类的外设输入进行响应,但是int 3可以。

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位。

image-20221030124607785

实验一:VM、TF、IF、NT

将中断门的描述符改为0040EF00`00081000

image-20221030124837901

然后执行代码

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.

image-20221030124917851

说明它禁止响应可屏蔽中断。

IDT中没有用到陷阱门。

然后中断门和陷阱门的区别就是:中断门由于IF为为0,表明不会被其他中断打断,而陷阱门有可能会被打断。

6、任务段

任务段(TSS:Task-State Segment)描述的是一个任务环境,用于进程和线程的环境的切换。TSS是一种结构数据且保存在内存中。内存的位置被描述在GDT中。

由于任务太过依赖于GDT表中的任务段描述和内存块,因此Windows 和Linux 都没有采用任务段(不想被CPU限制)。

TSS结构如下:

image-20221031142520796

  • ESP0、SS0:从R3切换至R0时,切换的堆栈数据。
  • EIP:任务的地址地址。
  • 通用寄存器、段选择子、EFLAGS:自定义。
  • CR3:来源R3
  • Previous Task Link:上一个TSS的选择子。

TSS是最小104字节的内存

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命令如下:

1
dt _KTSS

image-20221031143307127

TSS同样有str和ltr指令。str用于获取、ltr用于加载。但不同的是,tr保存的是一个选择子。windbg中使用命令r tr,获取选择子。

image-20221031143448888

0x28拆分得到index后可寻址到描述TSS的描述符,也可以使用dg命令。

image-20221031143703152

image-20221031144646917

描述TSS的描述符结构如下:

image-20221031144418041

描述符的结构与段描述符基本一致,需要注意的有两个地方,一个是Type位的B表示的是Busy,即当前任务是否处于忙碌状态(是否在执行),1表示忙碌,0表示非忙碌。Base表示Tss这块内存数据保存的地址。

此时重新使用dt命令解析TSS。

image-20221031145236635

可以看到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>

//0x2024 bytes (sizeof)
struct _KiIoAccessMap
{
UCHAR DirectionMap[32]; //0x0
UCHAR IoMap[8196]; //0x20
};

//0x20ac bytes (sizeof)
struct _KTSS
{
USHORT Backlink; //0x0
USHORT Reserved0; //0x2
ULONG Esp0; //0x4
USHORT Ss0; //0x8
USHORT Reserved1; //0xa
ULONG NotUsed1[4]; //0xc
ULONG CR3; //0x1c
ULONG Eip; //0x20
ULONG EFlags; //0x24
ULONG Eax; //0x28
ULONG Ecx; //0x2c
ULONG Edx; //0x30
ULONG Ebx; //0x34
ULONG Esp; //0x38
ULONG Ebp; //0x3c
ULONG Esi; //0x40
ULONG Edi; //0x44
USHORT Es; //0x48
USHORT Reserved2; //0x4a
USHORT Cs; //0x4c
USHORT Reserved3; //0x4e
USHORT Ss; //0x50
USHORT Reserved4; //0x52
USHORT Ds; //0x54
USHORT Reserved5; //0x56
USHORT Fs; //0x58
USHORT Reserved6; //0x5a
USHORT Gs; //0x5c
USHORT Reserved7; //0x5e
USHORT LDT; //0x60
USHORT Reserved8; //0x62
USHORT Flags; //0x64
USHORT IoMapBase; //0x66
struct _KiIoAccessMap IoMaps[1]; //0x68
UCHAR IntDirectionMap[32]; //0x208c
};

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");
// printf("%x\n", sizeof(KTSS));
char bufcode[6] = { 0,0,0,0,0x48,0 };
__asm
{
call fword ptr bufcode;
}
system("pause");
return 0;
}

运行,查看函数a的地址和程序的CR3。

image-20221031150810537

image-20221031150905333

构造TSS描述符。

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

image-20221031151543103

继续运行后发现windbg断下,然后使用uf查看汇编。

image-20221031152749546

可以看到已经成功执行。此时输入r tr,发现索引已经为实验测试的0x48,使用dg解析。

image-20221031154025086

可以看到Flags已经被设置为忙碌状态,9->b。使用dt重新查看tss结构。

image-20221031155438210

寄存器此时都为我们自定义的,但会发现此时的ESP并不为ESP0。。

image-20221031155555167

这是因为我们是构造了任务段,而不是通过r3切换到R0,当使用了中断门、调用门、陷阱门时才会进行切换。

输入g,继续运行,发现蓝屏。

image-20221031152833171

实验二:分析蓝屏原因

重新运行实验一的代码,并在call任务段的位置下断点。

image-20221031154909931

重新运行,断下后转到反汇编。

image-20221031155105966

可以看到call后的返回地址为0x4011cc,然后继续运行后windbg断下,输入r tr查看获取TSS选择子后进行解析。

image-20221031155314591

查看原始TSS。

image-20221031155342795

会发现原始的TSS结构保存着真实的返回地址!!!

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;//中断会清空Efl里的任务嵌套NT位,所以要修改回去
pushfd;
pop eax;
or eax, 0x4000;
push eax;
popfd;
iretd;
}
}

7、任务门

关闭增量链接、关闭随即地址

任务门结构图如下:

image-20221101213529497

Windows-双重异常

Windows使用的任务门主要有作为不可屏蔽中断双重异常

image-20221101204659991

双重异常:系统处理异常时触发的异常。Windows使用了任务门在实现双重异常是为了在触发双重异常时,可以将当前环境保存到TSS中,让开发者有信息进行调试、排查问题(系统无法解决双重异常,因此将触发时的环境保存在TSS中,然后反馈给开发者,让开发者自行解决)。

image-20221101213120017

image-20221101213211284

1
int 0x8

实验一:构造任务门

选择0x48的位置来存放TSS。

image-20221101214425488

构造任务门,写入idt+0x100

1
0000e500`00480000

image-20221101220518905

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>

//0x2024 bytes (sizeof)
struct _KiIoAccessMap
{
UCHAR DirectionMap[32]; //0x0
UCHAR IoMap[8196]; //0x20
};

//0x20ac bytes (sizeof)
struct _KTSS
{
USHORT Backlink; //0x0
USHORT Reserved0; //0x2
ULONG Esp0; //0x4
USHORT Ss0; //0x8
USHORT Reserved1; //0xa
ULONG NotUsed1[4]; //0xc
ULONG CR3; //0x1c
ULONG Eip; //0x20
ULONG EFlags; //0x24
ULONG Eax; //0x28
ULONG Ecx; //0x2c
ULONG Edx; //0x30
ULONG Ebx; //0x34
ULONG Esp; //0x38
ULONG Ebp; //0x3c
ULONG Esi; //0x40
ULONG Edi; //0x44
USHORT Es; //0x48
USHORT Reserved2; //0x4a
USHORT Cs; //0x4c
USHORT Reserved3; //0x4e
USHORT Ss; //0x50
USHORT Reserved4; //0x52
USHORT Ds; //0x54
USHORT Reserved5; //0x56
USHORT Fs; //0x58
USHORT Reserved6; //0x5a
USHORT Gs; //0x5c
USHORT Reserved7; //0x5e
USHORT LDT; //0x60
USHORT Reserved8; //0x62
USHORT Flags; //0x64
USHORT IoMapBase; //0x66
struct _KiIoAccessMap IoMaps[1]; //0x68
UCHAR IntDirectionMap[32]; //0x208c
};

struct _KTSS tss = { 0 };
__declspec(naked) void a()
{
__asm
{
int 3;//中断会清空Efl里的任务嵌套NT位,所以要修改回去
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");
// printf("%x\n", sizeof(KTSS));
__asm
{
pushfd //保存R3的FD
int 0x20 //修改为中断,因为是从IDT中跳转
popfd
}
system("pause");
return 0;
}

构造TSS

1
0000e940`50300068

image-20221101221127844

可以发现已经断下。

image-20221101221219040

输入g继续执行。

8、101012分页

Windows x86模式下有29912分页和101012分页,其中默认为29912分页。

image-20221102221050831

设置101012分页

使用EasyBCD工具设置系统配置为如下:

image-20221102221555765

重启即可。

实验一:线性地址转物理地址

确保系统当前分页为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的逻辑地址。

image-20221102221412169

然后将地址0040312c以101012格式拆分。

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。

image-20221102222814542

小知识:如果!process 0 0遍历出来进程的CR3末尾三个数不为0,则说明是29912分页,如果为0则是101012分页。

windbg中查看物理地址需要在命令前加上!

image-20221102222014984

将刚刚拆分得到的数值进行与cr3计算。

image-20221102223147321

image-20221102223314124

image-20221102223456812

最后一次计算不用*4是因为最后得到的66c44000是物理页,里边存放的是数据或者代码。前面两次 *4是因为要寻找PDE和PTE项。


cr3:控制寄存器,里面存放页基址。一共有4096个字节的大小。

image-20221102224139240

为什么一个页的大小为4096字节?原因是在29912或者101012中,后面的12位表示为页内偏移;当12为全部为1时,页内最大偏移为0xFFFF+1(加上偏移0),也就是4096个字节。

CPU拆分地址的操作由模块MMU(Memory Manager Unit),相当于软件的函数。但因为每次寻址时都需要对线性地址拆分,因此操作系统使用了叫做TLB缓存的东西。

image-20221102232909532

TLB中保存着线性地址(前20位)和物理页的对映关系,如果匹配到线性地址(前20位)就可以迅速找到物理页。

  • 当读数据时

​ 通过物理页与线性地址后12位的偏移组合得到最终的物理地址。

  • 当写数据时

    为了避免一直访问物理页(内存条),造成资源开销大,因此使用了L1、L2、L3缓存,也就是俗称的一级、二级、三级缓存。将数据写到缓存中,每隔一个时钟周期后才将缓存中的数据写到内存条上,这个操作叫做WB(写回绕,Write Back)其中L1是所有CPU共享,L2、L3等是每个CPU都有一个

如果在TLB中找不到线性地址和物理页的映射(TLB miss),则会操作MMU模块将线性地址拆分后存入TLB缓存中。


实验二:将同一个线性地址转成物理地址

关闭随机地址。变量设置为全局或者静态。

将实验一的程序编译后,同时运行两个。

image-20221102233427425

可以看到线性地址一致,但CR3不同。

image-20221102234021660

image-20221102234105221

可以看到两个线性地址虽然相同,但是对应的物理地址不同。可以得出结论,同一个线性地址可以被映射为多个物理地址

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拆分。

1
2
3
4
5
0x00405000

0000 0000 0001(0x1)
0000 0000 0101(0x5)
0000 0000 0000(0x0)

然后通过CR3寻找物理页。

image-20221103215644361

同样的方法,寻找0地址的物理页。

image-20221103215710523

发现0地址没有pte,将变量的pte挂上。

image-20221103215741029

继续运行程序。

image-20221103215939831

实验二:页内偏移对齐

将实验一的全局变量改为局部变量。

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;
}

image-20221103220203339

可以看到var的地址已经不是000结尾了,然后按照实验一的方法对0地址挂上pte。

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)

image-20221103220713373

继续运行。

image-20221103220733841

发现得到的内容不对。原因:变量var的页内偏移是0xf28,所以var的值为0x6b181000+0xf28的内容。但0地址拆分后得到的页内偏移也是0,读取到的数据是0x6b181000+0x0的内容。

image-20221103221310918

如果要读到正确数据,需要将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;
}

image-20221103222041844

实验三: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, //push 0
0x6a,0, //push 0
0x6a,0, //push 0
0x6a,0, //push 0
0xb8,0,0,0,0, //mov eax,0
0xff,0xd0, //call eax
0x83,0xc4,0x0c, //add esp,c
0xc3 //ret
};

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地址。

image-20221103224204506

继续运行,弹出信息框。

image-20221103224240906


CPU的分页单位是4k,也就是一个页。

操作系统分页是64k。

当我们申请内存时,返回的其实是拥有64k大小的内存地址,并且如果只申请不用,还有可能并不会挂物理页。

当我们再次申请内存时,如果上一次申请的64k的页内存中有未使用的内存,则会返回内存对应的地址。

image-20221103225309298

还有就是0x0-0x10000的地址为无权限,是操作系统为了保护访问异常数据时出错,是一种保护机制。比如int *p=0,访问p时就会出错。另外r3和r0之前还有一块64k的空间也是无法使用的,这个区域隔离了用户和内核空间;防止用户程序跨越到内核空间中。

image-20221103230007148

image-20221103232902036

10、页属性

image-20230412100130216

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大小(大页)。

image-20230412101456731

实验一: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”是一个常量,因此下边的修改行为存在异常。

image-20230412102152104

使用CFF_Explorer查看.rdata区域的属性位0x40000040,为只读内存。表明系统在拉起该进程时为此段申请的内存属性为只读,因此无法进行修改。

image-20230412102229823

将属性修改为0xC0000040后保存在运行。

image-20230412102406660

运行成功。

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。

image-20230412104327299

PDE的属性为0x867-> 1000 0110 0111,其中R/W为1表明为可读可写,但是PTE的属性0x225-> 0010 0010 0101,R/W为0,表明为只读,将PTE的R/W改为1.

image-20230412104536369

继续运行。

image-20230412104550249

执行成功。

实验二: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; //GDT
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

image-20230412105907184

PDE的页属性0x063->0000 0110 0011,U/S为0表明只有R0可访问;PTE的页属性0x163->0001 0110 0011,U/S为0表明只有R0可访问.将PDE和PTE的U/S修改为1.

image-20230412110130511

运行。

image-20230412110222330

执行成功。

11、页基址

操作系统启动流程:BIOS(实模式)->NtLdr(构建保护模式)->操作系统(管理内存)。

x86模式下,虚拟内存有4GB大小,其中高2G是内核共享,该2G中被划分为不同的部分,用来做不同的管理。

image-20230413094816983

管理4GB大小的内存需要以下大小的空间:

1
( 总大小 \ 页表大小 )* 指针单位 = ( 4GB \ 4K ) * 4 = 4mb

10-10-12模式下,有这样的指向 PDE->PTE->物理页;换句话来说,PTE由PDE管理,PDE由CR3管理。但由于这些都是物理地址,因此操作系统是怎么获取这些物理地址并管理的咧?

微软在管理内存时设计了一个特殊的基址0xC0000000,也就是页表基址。因为要管理4GB内存,因此页表基址的范围为0xC0000000~0xC0400000。这块区域中保存了系统中进程的PDE和PTE。

PDE保存的位置可以通过如下计算:

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有如下公式:

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进程,反正都是共享)。

image-20230413104410108

可以看到是获取正确的。因此操作系统可以通过页表基址获取到所有内存的PTE和PDE,然后进行管理。

实验二:页表基址获取自身CR3

微软有一个巧妙的设计,每一个PDE/PTE的0xC00位置都指向了自身,这样既满足了页表基址的管理范围,又实现了通过构造特殊地址来获取自身的CR3。

将0xC00构造为一个地址0xC0000000,拆分后得到

0x300

0x0

0x0

image-20230413105519620

可以看到此时读取到的位置与CR3相同,由于10-10-12模式下拆3次读取是CPU的机制,因此可以构建这么一个地址0xC0300C00(地址范围在PDE范围内),拆分后如下:

0x300

0x300

0xc00

image-20230413105722619

就可以巧妙地得到了自身的CR3。正常读取该地址,效果也相同。

image-20230413105815776

实验三:逆向101012的MmIsAddressVaild

对于10-10-12模式的内核程序为ntoskrnl.exe;2-9-9-12为ntoskrnlpa.exe。

进入函数MmIsAddressValid进行分析

image-20230413113703728

image-20230413113807329

补充:后边学了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而是指向该表项。

image-20230519114908954

0-12位为属性位。

  • P位:有效位,0无效,1有效。
  • PWT:Page Write Through。直写,后边TLB会讲到。
  • PCD:Page Cache Disable。禁止写缓存,后边TLB会讲到。
  • Avail:操作系统用,CPU不用。

PDE

image-20230519151435985

image-20230519151441972

当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;
}

将输出的地址进行划分。

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

image-20230519121927102

尝试修改字符串。

image-20230519121953464

回到R3,再次输出。

image-20230519122028569

内容已改变这里其实是改错了,应该用的是eb,这里用了ed,所以输出了e

这里也可以看到最后找到的PTE地址头部为8,即1000,地址最高位为1,表明当前XD=1,数据为不可执行。

实验二:逆向29912的MmIsAddressVaild

image-20230521012611661

总结:如果PDE为大页则直接返回true,否则常规检查PTE。


额外指令学习:

  • x:搜索导入表
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)

*代表模糊匹配
  • !pte:显示虚拟地址的PTE和PDE基地址
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定义了如下类型;

image-20230524231314576

  • UC:无缓存
  • WC:组合写(直写+回写),写入缓存,什么时候写入物理内存,由CPU决定。
  • WT:直写,即写到物理内存也写入缓存。
  • WP:写保护,置1后写内存直接异常。这里的写保护是局部,而CR4中的WP为全局
  • WB:回写,先写入缓存,过一段时间再写入物理内存。
  • UC-:-号代表弱,表示有时候会有缓存,有时候没有。

一块内存的缓存类型可以通过PAT-PCD-PWT进行组合判断。

image-20230524233000843

MSR寄存器

MSR中保存着对PAT属性的定义。可以自己自定义

image-20230524233239514

windbg使用rdmsr addr来查看保存的值。

image-20230524233420397

拆分后得到如下:

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

QQ图片20230519152651

  • LA:线性地址

  • PA:物理地址

  • ATTR:在10-10-12分页模式下:ATTR = PDE属性 & PTE属性。在2-9-9-12分页模式下:ATTR = PDPTE属性 & PDE属性 & PTE属性

  • LRU:统计信息。由于TLB的大小有限,因此当TLB被写满、又有新的地址即将写入时,TLB就会根据统计信息来判断哪些地址是不常用的,从而将不常用的记录从TLB中移除。

需要注意的是

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指令

image-20230519154133640

简单说该指令用于删除某线性地址在TLB中的记录。

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; // 保存目标线性地址的 PTE 线性地址
DWORD* pNullPTE; // 0 地址的 PTE 线性地址
pNullPTE = (DWORD*)0xC0000000;

// 挂上 0x50000000 所在位置
pPTE = (DWORD*)(0xC0000000 + (0x50000000 >> 10));
*pNullPTE = *pPTE;

x = *(DWORD*)0;

// 挂上 0x60000000 所在位置
pPTE = (DWORD*)(0xC0000000 + (0x60000000 >> 10));
*pNullPTE = *pPTE;

y = *(DWORD*)0;

// 刷新 TLB
__asm {
mov eax, cr3
mov cr3, eax
}

// 再次读取 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;
}

image-20230519160245941

设计中断门:

1
2
3
4
5
6
7
offset: 0x001213c0
P:1
DPL:3
TYPE:0xE
Selector:1000

0012ee00`000813c0

写入描述符。

image-20230519161304372

运行效果

image-20230519161452057

可以发现,在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; // 保存目标线性地址的 PTE 线性地址
DWORD* pNullPTE; // 0 地址的 PTE 线性地址
pNullPTE = (DWORD*)0xC0000000;

// 挂上 0x50000000 所在位置
pPTE = (DWORD*)(0xC0000000 + (0x50000000 >> 10));
*pNullPTE = *pPTE;
//修改G位为1
*pNullPTE = *pNullPTE | 0x100;

x = *(DWORD*)0;

// 挂上 0x60000000 所在位置
pPTE = (DWORD*)(0xC0000000 + (0x60000000 >> 10));
*pNullPTE = *pPTE;

y = *(DWORD*)0;

// 刷新 TLB
__asm {
mov eax, cr3
mov cr3, eax
}

// 再次读取 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. 给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;
}

运行效果。

image-20230519164716376

实验结果证明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; // 保存目标线性地址的 PTE 线性地址
DWORD* pNullPTE; // 0 地址的 PTE 线性地址
pNullPTE = (DWORD*)0xC0000000;

// 挂上 0x50000000 所在位置
pPTE = (DWORD*)(0xC0000000 + (0x50000000 >> 10));
*pNullPTE = *pPTE;

x = *(DWORD*)0;

// 挂上 0x60000000 所在位置
pPTE = (DWORD*)(0xC0000000 + (0x60000000 >> 10));
*pNullPTE = *pPTE;

y = *(DWORD*)0;

// 刷新 TLB
__asm{

invlpg dword ptr ds:[0]
}

// 再次读取 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;
}

运行效果

image-20230521142724659

实验四:CR4刷新TLB

CR4中第八位PGE位

image-20230521143220842

image-20230521143513360

由于VS不支持CR4,因此使用硬编码

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; // 保存目标线性地址的 PTE 线性地址
DWORD* pNullPTE; // 0 地址的 PTE 线性地址
pNullPTE = (DWORD*)0xC0000000;

// 挂上 0x50000000 所在位置
pPTE = (DWORD*)(0xC0000000 + (0x50000000 >> 10));
*pNullPTE = *pPTE;

x = *(DWORD*)0;

// 挂上 0x60000000 所在位置
pPTE = (DWORD*)(0xC0000000 + (0x60000000 >> 10));
*pNullPTE = *pPTE;

y = *(DWORD*)0;

// 刷新 TLB
__asm{
//mov eax,cr4
__emit 0x0F;
__emit 0x20;
__emit 0xe0;


mov ebx,0x80;
not ebx;
and eax,ebx;

//mov cr4,eax
__emit 0x0F;
__emit 0x22;
__emit 0xe0;

}

// 再次读取 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;
}

运行效果:

image-20230521144553724

15、控制寄存器

image-20230521143220842

注意: 控制寄存器中有些位一旦置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时才有效果。

image-20230521160606649

  • PWT:PageLevelWriteThrough,页直写位。 为1时,页表使用直写缓存,为0时页表使用回写缓存。

image-20230521160642513

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失效。