保护模式介绍

什么是保护模式?

x86 CPU的3个模式:实模式、保护模式和虚拟8086模式。

为什么不直接讲x64的?

x86是由Intel推出的一种复杂指令集,能够生产支持这种指令集CPU公司主要是Intel和AMD.

AMD在1999年的时候,拓展了这套指令集,称为x86-64,后改名为AMD64,Intel也兼容了这个产品,称为Intel 64.但AMD64和Intel64几乎是一样的,所以在很多资料中统称为x64.这套指令集是对x86的拓展,向下兼容的.

保护模式有什么特点?

段的机制

页的机制

学习保护模式有什么用?

真正理解内核是如何运作的

段寄存器结构

1、什么是段寄存器?

当我们用汇编读写某一个地址时:

1
mov dword ptr ds:[0x123456]

eax我们真正读写的地址是:

1
ds.base + 0x123456

ES CS SS DS FS GS LDTR TR 共8个

2、段寄存器的结构

image-20240923131745478

3、段寄存器的读写

3.1 读段寄存器

1
2
3
4
5
比如:MOV AX,ES    只能读16位的可见部分

读写LDTR的指令为:SLDT/LLDT

读写TR的指令为:STR/LTR

3.2 写段寄存器

1
比如:MOV DS,AX   写时是写96位  

段寄存器属性探测

1、段寄存器成员简介

段寄存器 Selector Attribute Base Limit
ES 0023 可读、可写 0 0xFFFFFFFF
CS 001B 可读、可执行 0 0xFFFFFFFF
SS 0023 可读、可写 0 0xFFFFFFFF
DS 0023 可读、可写 0 0xFFFFFFFF
FS 003B 可读、可写 0x7FFDE000 0xFFF
GS - - - -

2、探测Attribute

1
2
3
4
5
6
7
int var = 0;					
__asm
{
mov ax,ss //cs不行 cs是可读 可执行 但不可写
mov ds,ax
mov dword ptr ds:[var],eax
}

3、探测Base

1
2
3
4
5
6
7
8
9
10
int var = 1;					
__asm
{
mov ax,fs
mov gs,ax
mov eax,gs:[0] //不要用DS 否则编译不过去
mov dword ptr ds:[var],eax

//mov edx,dword ptr ds:[0x7FFDF000]
}

4、探测Limit

1
2
3
4
5
6
7
8
9
10
int var = 1;					
__asm
{
mov ax,fs
mov gs,ax
mov eax,gs:[0] //不要用DS 否则编译不过去
mov dword ptr ds:[var],eax

//mov edx,dword ptr ds:[0x7FFDF000]
}

段描述符与段选择子

段描述符分类

GDT(全局描述符表)

LDT(局部描述符表)

当我们执行类似MOV DS,AX指令时,CPU会查表,根据AX的值来决定查找GDT还是LDT,查找表的什么位置,查出多少数据.

image-20240923132339229

段描述符

image-20240923132548637

段选择子

段选择子是一个16位的段描述符,该描述符指向了定义该段的段描述符.

image-20240923132759152

RPL:请求特权级别

TI:

1
2
TI=0  查GDT表
TI=1 查LDT表

Index:处理器将索引值乘以8在加上GDT或者LDT的Base地址,就是要加载的段描述符

image-20241007124724810

加载段描述符至段寄存器

除了MOV指令,我们还可以使用LES、LSS、LDS、LFS、LGS指令修改寄存器

CS不能通过上述的指令进行修改,CS为代码段,CS的改变会导致EIP的改变,要改CS,必须要保证CS与EIP一起改,后面会讲

1
2
3
4
5
char buffer[6];					
__asm
{
les ecx,fword ptr ds:[buffer] //高2个字节给es,低四个字节给ecx
}

[!NOTE]

注意:RPL<=DPL(在数值上)

段描述符属性

P位和G位

1、P位

1
2
3
P = 1 段描述符有效

P = 0 段描述符无效

2、G位(当前寻址上限)

G = 0 Limit单位是bytes

1
Limit = 000 FFFFF 前面加000

G = 1 Limit单位是以4KB

1
Limit = FFFFF FFF 后面加FFF

S位和TYPE域

1、S位

S = 1 代码段或者数据段描述符

S = 0 系统段描述符

2、TYPE域

image-20240923135446213

分析举例

image-20240923135549083

数据段(DS)说明

Access 访问位,表示该位最后一次被操作系统清零后,该段是否被访问过.每当处理器将该段选择符置入某个段寄存器时,就将该位置1.

Write 是否可写

E 扩展方向 (向上拓展红色部分图一,向下拓展红色部分图二)

image-20240923135655239

代码段(CS)说明

A 访问位

R 可读位

C 一致位

1
2
3
C = 1 一致代码段

C = 0 非一致代码段

系统段(SS)描述符

当S=0时,该段描述符为系统描述符.系统描述符有分为以下类型

image-20240923140023146

DB位

DB位:当前实际地址最多大小

1、CS段

1
D = 1 采用32位寻址方式	

image-20240924200830741

整个地址空间是4KB

1
2
D = 0 采用16位寻址方式	
前缀67 改变寻址方式

image-20240924200922757

整个地址空间是64K

2、SS段

1
2
3
D = 1 隐式堆栈访问指令(如:PUSH POP CALL) 使用32位堆栈指针寄存器ESP	

D = 0 隐式堆栈访问指令(如:PUSH POP CALL) 使用16位堆栈指针寄存器SP

3、向下拓展的数据段

1
D = 1 段上线为4GB
1
D = 0 段上线为64KB

image-20240924201125839

总结

  • 段描述符中的DB位控制操作数的默认大小,DB=0时默认操作数为16位,DB=1时默认操作数为32位。
  • 寄存器宽度的改变通过改变DB位实现,不影响寄存器本身的宽度。
  • CS段描述符的DB位控制代码段的默认操作数大小,影响push指令的默认操作数。
  • SS段描述符的DB位改变堆栈寄存器的寻址方式,从32位变为16位

段权限检查

CPU分级

image-20240924203127817

如何查看程序处于几环?

CPL(Current Privilege Level) :当前特权级

CS和SS中存储的段选择子后2位

例如:23 == 0010 0011 2b == 0010 1011 11==3 那么在三环

image-20240924203902095

DPL(Descriptor Privilege Level) 描述符特权级别

image-20240924203206796

DPL存储在段描述符中,规定了访问该段所需要的特权级别是什么.

通俗的理解:如果你想访问我,那么你应该具备什么特权.

举例说明:

1
2
3
mov DS,AX  如果AX指向的段DPL = 0  

但当前程序的CPL = 3 这行指令是不会成功的

RPL(Request Privilege Level) 请求特权级别

RPL是针对段选择子而言的,每个段的选择子都有自己的RPL

image-20240924203307380

举例说明:

1
2
3
Mov ax,0008	与	Mov ax,000B 	//段选择子  		

Mov ds,ax Mov ds,ax //将段描述指向的是同一个段描述符,但RPL是不一样的.

数据段的权限检查

1
2
3
4
5
比如当前程序处于0环,也就是说CPL=0

Mov ax,000B //1011 RPL = 3

Mov ds,ax //ax指向的段描述符的DPL = 0

数据段的权限检查:

1
CPL <= DPL  并且 RPL <= DPL (数值上的比较)

注意:代码段和系统段描述符中的检查方式并不一样

总结

1
2
3
4
5
CPL  CPU当前的权限级别

DPL 如果你想访问我,你应该具备什么样的权限

RPL 用什么权限去访问一个段

代码跨段执行流程

段寄存器:ES,CS,SS,DS,FS,GS,LDTR,TR

段寄存器读写:除CS外,其他的段寄存器都可以通过MOV,LES,LSS,LDS,LFS,LGS指令进行修改

1、代码间的跳转(段间跳转 非调用门之类的)

段间跳转,有2种情况,即要跳转的段是一致代码段还是非一致代码段,也就是看C等于0还是1

同时修改CS与EIP的指令

JMP FAR / CALL FAR / RETF / INT /IRETED

[!NOTE]

注意:只改变EIP的指令 JMP / CALL / JCC / RET

2、执行流程

CPU如何执行这行代码? JMP 0x20:0x004183D7

(1) 段选择子拆分

1
2
3
4
0x20 对应二进制形式 0000 0000 0010 0000	
RPL = 00
TI = 0
Index = 4

(2) 查表得到段描述符

1
2
3
4
5
TI = 0 所以查GDT表

Index = 4 找到对应的段描述符

四种情况可以跳转:代码段、调用门、TSS任务段、任务门

(3) 权限检查

1
2
3
如果是非一致代码段,要求:CPL == DPL 并且 RPL <= DPL

如果是一致代码段,要求:CPL >= DPL

(4) 加载段描述符

1
通过上面的权限检查后,CPU会将段描述符加载到CS段寄存器中.

(5) 代码执行

1
CPU将 CS.Base + Offset 的值写入EIP 然后执行CS:EIP处的代码,段间跳转结束.

3、总结

对于一致代码段:也就是共享的段

  • 特权级高的程序不允许访问特权级低的数据:核心态不允许访问用户态的数据
  • 特权级低的程序可以访问到特权级高的数据,但特权级不会改变:用户态还是用户态

对于普通代码段:也就是非一致代码段

  • 只允许同级访问
  • 绝对禁止不同级别的访问:核心态不是用户态,用户态也不是核心态.

直接对代码段进行JMP 或者 CALL的操作,无论目标是一致代码段还是非一致代码段,CPL都不会发生改变.如果要提升CPL的权限,只能通过调用门.

3、实验

3.1 构造段描述符

找一个非一致代码段描述符,复制一份,写入到GDT表中

1
2
3
4
5
6
7
8
9
10
11
kd> r gdtr
gdtr=8003f000
kd> dq 8003f000
8003f000 00000000`00000000 00cf9b00`0000ffff
8003f010 00cf9300`0000ffff 00cffb00`0000ffff
8003f020 00cff300`0000ffff 80008b04`200020ab
8003f030 ffc093df`f0000001 0040f300`00000fff
8003f040 0000f200`0400ffff 00000000`00000000
8003f050 80008954`b1000068 80008954`b1680068
8003f060 00009302`2f40ffff 0000920b`80003fff
8003f070 ff0092ff`700003ff 80009a40`0000ffff

00cffb00`0000ffff 数据写如某个P位为0的位置

1
kd> eq 8003f048 00cffb00`0000ffff(用内核工具查看新增是否成功)

3.2 OD中进行测试

在OD中,执行跨段跳转 JMP FAR 004B:0041840D

成功的话,CS段寄存器就会修改成4B

image-20240927211027897

3.3 修改段描述符的权限级别,并再次在OD中进行测试

将00cffb00`0000ffff 改为00cf9b00`0000ffff在OD中,执行跨段跳转 JMP FAR 004B:0041840D

3.4 将该段描述符的属性更改为一致代码段.

将00cffb00`0000ffff 改为00cf9f00`0000ffff在OD中,执行跨段跳转 JMP FAR 004B:0041840D

如果是一致代码段,要求:CPL >= DPL (当前同样可以访问)

CPL:3

DPL:0

4、总结

1、为了对数据进行保护,普通代码段是禁止不同级别进行访问的。用户态的代码不能访问内核的数据,同样,内核态的代码也不能访问用户态的数据.

2、如果想提供一些通用的功能,而且这些功能并不会破坏内核数据,那么可以选择一致代码段,这样低级别的程序可以在不提升CPL权限等级的情况下即可以访问.

3、如果想访问普通代码段,只有通过“调用门”等提示CPL权限,才能访问。

长调用与短调用

我们通过JMP FAR可以实现段间的跳转,如果要实现跨段的调用就必须要学习CALL FAR,也就是长调用.

CALL FARJMP FAR要复杂,JMP并不影响堆栈,但CALL指令会影响.

1、短调用

指令格式:CALL 立即数/寄存器/内存

image-20240926223503339

发生改变的寄存器:ESP EIP

2、长调用(跨段不提权)

指令格式:CALL CS:EIP(EIP是废弃的)

image-20240926223600844

发生改变的寄存器:ESP EIP CS

3、长调用(跨段并提权)

指令格式:CALL CS:EIP(EIP是废弃的)

image-20240926223620406

image-20240926223639418

发生改变的寄存器:ESP EIP CS SS

4、总结

  1. 跨段调用时,一旦有权限切换,就会切换堆栈.
  2. CS的权限一旦改变,SS的权限也要随着改变,CS与SS的等级必须一样.
  3. JMP FAR 只能跳转到同级非一致代码段,但CALL FAR可以通过调用门提权,提升CPL的权限.

调用门

1、调用门执行流程

指令格式:CALL CS:EIP(EIP是废弃的)

执行步骤:

  1. 根据CS的值 查GDT表,找到对应的段描述符 这个描述符是一个调用门.
  2. 在调用门描述符中存储另一个代码段段的选择子.
  3. 选择子指向的段 段.Base + 偏移地址 就是真正要执行的地址.

2、门描述符

image-20240928111147890

(16-31位)offset+(0-15位)offset = 跳转到函数执行的位置

3、构造一个调用门(无参 提权)

1
2
3
4
5
6
7
8
9
10
11

第一步:构造大概的值
DPL:3环 == 11
0000 0000 0000 0000 1110 1100(调用门固定值) 0000 0000 == 0000ec00
0000 0000 0000 1000 0000 0000 0000 0000 == 00080000
0000ec00`00080000
第二步:拿到方法地址
401000
0040ec00`00081000
第三步:eq 80b99048 0040ec00`00081000
第四步:运行程序

3.1 代码测试

步骤一:代码测试,并观察堆栈与寄存器的变化记录执行前的寄存器值:SS ESP CS

执行前:

image-20240929101518189

执行后:

1
2
3
4
5
6
7
eax=00000048 ebx=7ffd9000 ecx=00000000 
edx=00000001 esi=00000000 edi=0012fe5c
eip=00401000 esp=a69f5ca0 ebp=0012fe5c iopl=0
nv up ei pl nz na pe nc

cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000206

变化:

1
2
3
4
5
CS:1b->08 

ESP:12fd7c->a682bca0(高地址)

SS:23->10

堆栈:

image-20240929102708081

步骤二:在测试代码中加入特权指令并读取高2G内存

测试代码:

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
BYTE GDT[6] = {0};
DWORD dwH2Gvalue;
void _declspec(naked) getRegister(){
__asm{
pushad
pushfd

mov eax,0x80b9900c
mov ebx,[eax]
mov dwH2Gvalue,ebx
sgdt GDT //获取GDT表地址

popfd
popad

retf
}

}
void printfRegister(){
DWORD GDT_ADDR = *(PDWORD)(&GDT[2]);
WORD GDT_LIMIT = *(PWORD)(&GDT[0]);
printf("%x %x %x\n",dwH2Gvalue,GDT_ADDR,GDT_LIMIT);
}

int testfun1(){
__asm{
mov ebx,ebx
mov ebx,ebx
}

char buf[6];

*(DWORD*)&buf[0] = 0x12345678;
*(WORD*)&buf[4] = 0x48;

__asm{
call fword ptr[buf]
}
printfRegister();
getchar();
return 0;
}


int _tmain(int argc, _TCHAR* argv[])
{
testfun1();
return 0;
}

image-20240929104053905

4、构造一个调用门(有参 提权)

调用门描述符:

0040EC03`00081030 (三个参数)

1
kd>eq 8003f048 0040EC03`00081030

测试代码:

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
// demo03.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <windows.h>

DWORD x,y,z;

void _declspec(naked) getRegister(){
__asm{
pushad
pushfd

mov eax,[esp+0x24+0x8+0x8]
mov dword ptr ds:[x],eax

mov eax,[esp+0x24+0x8+0x4]
mov dword ptr ds:[y],eax

mov eax,[esp+0x24+0x8+0x0]
mov dword ptr ds:[z],eax

popfd
popad

retf 0xc
}

}
void printfRegister(){
printf("%x %x %x\n",x,y,z);
}

int testfun1(){

char buf[6];

*(DWORD*)&buf[0] = 0x12345678;
*(WORD*)&buf[4] = 0x68;

__asm{
push 1
push 2
push 3
call fword ptr[buf]
}
printfRegister();
getchar();
return 0;
}


int _tmain(int argc, _TCHAR* argv[])
{
testfun1();
return 0;
}

成功获取到3个传入的参数

image-20240930110250565

5、总结

  1. 当通过门,权限不变的时候,只会PUSH两个值CS和返回地址新的CS的值由调用门决定
  2. 当通过门,权限改变的时候,会PUSH四个值SS、ESP、CS和返回地址 新的CS的值由调用门决定 新的SS和ESPTSS提供
  3. 通过门调用时,要执行哪行代码有调用门决定,但使用RETF返回时,由堆栈中压入的值决定,这就是说,进门时只能按指定路线走,出门时可以翻墙(只要改变堆栈里面的值就可以想去哪去哪)
  4. 可不可以再建个门出去呢?也就是用Call 当然可以了 前门进 后门出

中断门

使用场景

  • 系统调用
1
大家在开发应用程序时都会使用到Windows提供的API,这些API在执行的过程中需要从3环一步一步进入到0环,这一过程就是系统调用,在这个过程中也用到了中断门(一些比较老的API使用的是中断门,但是新API中使用的都是快速调用)
  • 调试
1
大家使用OD调试程序时候会使用断点,断点本质上就是在你选中的这一行中写入一个字节0xCC,也就是INT 3指令,这个指令就是用来执行中断门的。中断门也有一张表,我们称之为IDT(中断描述符表),与GDT一样,IDT也是由一系列描述符组成的,每个描述符占8字节,需要注意的是IDT表中的第一个元素不是NULL(GDT是)

1、IDT

IDT即中断描述符表,同GDT一样,IDT也是由一系列描述符组成的,每个描述符占8个字节。但要注意的是,IDT表中的第一个元素不是NULL。在windbg中查看IDT表的基址和长度

image-20240930130413487

2、IDT表的构成

IDT表可以包含3种门描述符:

  • 任务门描述符
  • 中断门描述符
  • 陷阱门描述符

3、中断门描述符结构

image-20240930130535215

8-12位的值是01110则是一个中断门

函数执行:401030

0040ee00`00081030

4、构造一个中断门

1
2
3
4
5
6
7
//INT index 指令触发寻找中断门
//当前(写入的地址-idt首地址)/ 8 = index索引 16进制
__asm{
push fs
INT 0x22
pop fs
}

5、中断执行

INTX
其中,X是索引,X*8+IDT的基址就是具体的中断门描述符

6、中断返回

INT N指令:

  1. 没有权限切换时,会向堆栈PUSH 3个值,分别是:CS EFLAG EIP(返回地址)

  2. 有权限切换时,会向堆栈PUSH 5个值,分别是:SS ESP EFLAG CS EIP(返回地址)

    image-20241002205508651

在中断门中,不能通过RETF返回,而应该通过IRET/IRETD指令返回

陷阱门

1、陷阱门描述符结构

image-20240930130753905

8-12位的值是01111则是一个陷阱门

2、构造一个陷阱门

构造一个陷阱门(0040EF00 00081030)

0000ef00 00080000

0000 0000 0000 0000 1110 1111 0000 0000

写入到IDT表中

1
eq>8003f500 0040EF00`0008103

执行陷阱门

1
2
3
__asm{
INT 0x32
}

3、陷阱门与中断门的区别

中断门执行时,将IF位(中断标志)清零,但陷阱门不会。

任务段

在调用门、中断门与陷阱门中,一旦出现权限切换,那么就会有堆栈的切换。而且,由于CS的CPL发生改变,也导致了SS也必须要切换。

切换时,会有新的ESP和SS(CS是由中断门或者调用门指定)这2个值从哪里来的呢?TSS (Task-state segment ),任务状态段.

1、TSS的结构

image-20240930210259383

大小104字节

2、TSS的作用

不要把TSS与“任务切换”联系到一起

TSS的意义就在于可以同时换掉”一堆”寄存器

3、TR段寄存器取值

CPU如何找到TSS呢? 通过TR段寄存器,base找到TSS内存 Limit指的是TSS内存大小,TR段寄存器值来自于GDT表,GDT表里的TSS段描述符

image-20240930210450817

4、TSS段描述符(TSS Descriptor)

image-20240930210641733

5、TR寄存器读写

  1. 将TSS段描述符加载到TR寄存器

指令:LTR

说明:

  • 用LTR指令去装载的话仅仅是改变TR寄存器的值(96位) 并没有真正改变TSS

  • LTR指令只能在系统层使用

  • 加载后TSS段描述符会状态位会发生改变

6、修改TR寄存器

  • 在Ring0 我们可以通过LTR指令去修改TR寄存器
  • 在Ring3 我们可以通过CALL FAR 或者 JMP FAR指令来修改

用JMP去访问一个代码段的时候,改变的是CS和EIP :

1
2
JMP 0x48:0x123456  如果0x48是代码段  
执行后:CS-->0x48 EIP-->0x123456

用JMP去访问一个任务段的时候:

如果0x48是TSS段描述符,先修改TR寄存器,在用TR.Base指向的TSS中的值修改当前的寄存器

注意:

如果你用JMP的话Previous Task Link的值不变NT位不变,没有人给你填充初始值多少现在就多少,如果用Call那就会写成原来那个TSS段的选择子NT位置1

NT = 0 IRET(堆栈取 中断返回)

NT = 1 IRET (TSS:PTL 不是中断返回)

7、实现任务切换

  1. 构造完整的TSS
  2. 构造TSS段描述符
  3. 使用CALL FAR/JMP FAR指令修改TR寄存器
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
#include <stdio.h>
#include <windows.h>

int o_eax, n_esp;
short n_cs, n_ss;

void __declspec(naked) GetValue() {
// 将寄存器保存到全局变量中
_asm {
mov o_eax, eax
mov n_esp, esp
mov ax, cs
mov n_cs, ax
mov ax, ss
mov n_ss, ax
mov eax, o_eax
iretd
}
}

void main()
{
char stack[100] = {0}; // 栈
char buffer[6] = {0x0, 0x0, 0x0, 0x0, 0x4B, 0x0}; // 选择子
int iCr3 = 0;

printf("Input: ");
scanf("%x", &iCr3);
getchar();

DWORD tss[0x68] = {
0x00000000, // Previous Task Link
// 不同权限对应的ESP、SS,如果不涉及到权限切换所以可以将这些寄存器的值全部填0
0x00000000, // ESP0
0x00000000, // SS0
0x00000000, // ESP1
0x00000000, // SS1
0x00000000, // ESP2
0x00000000, // SS2
(DWORD)iCr3, // Cr3,与页的知识有关,必须要赋值
0x00401020, // EIP,下一次执行代码的位置,必须要赋值,在代码中就是GetValue函数的地址
0x00000000, // EFLAGS
0x00000000, // EAX
0x00000000, // ECX
0x00000000, // EDX
0x00000000, // EBX
(DWORD)stack, // ESP,任务切换时也需要切换栈,所以在代码中我们可以声明一个数组,将其地址作为一块栈
0x00000000, // EBP
0x00000000, // ESI
0x00000000, // EDI
0x00000023, // ES
0x00000008, // CS,切到0环的代码段描述符
0x00000010, // SS,CS与SS需要保持一致
0x00000023, // DS
0x00000030, // FS,切到0环就是0x30,3环就是0x3B
0x00000000, // GS,Windows没有使用这个段寄存器所以永远是0
0x00000000, // LDT,填0
0x20ac0000 // IO_MAP,Windows2000以后不用了,默认值
};

_asm {
push fs
call far fword ptr[buffer] // 长调用
pop fs
}

// 输出寄存器的值证明完成了任务切换
printf("ESP: %x, CS: %x, SS: %x \n", n_esp, n_cs, n_ss);

getchar();
}

根据TSS段描述符的结构构造描述符,首先获取TSS的地址,在代码中下断点然后查看地址即可:其地址为0x0012fd70

我们就得出对应TSS段描述符中Base的值,接着Limit就是TSS的大小0x68,DPL为3(3环程序访问)

Type域为0x9即表示当前描述符没有被加载过。

在Windbg中断点输入

1
kd> !process 0 0

指令找到当前的程序获取Cr3的值填入到程序中

最终得出TSS段描述符为:

构造TSS段描述符:XX00e9XX`XXXX0068

1
2
3
4
tss:0x0012fc2c
eq 8003f0c0 0000e912`fc2c0068

cr3:bf3915e0 getValue 4113d0

接着我们使用eq指令在Windbg中向GDT表中写入我们构造好的TSS段描述符:

image-20241007130858507

通过windbg寄存器我们可以看到EIP,ESP,CS,SS变成了我们设置的值

image-20241006233017065

任务门

1、任务门描述符

image-20241007103357569

1
2
构造任务门:0000 e500 00c3 0000
>eq 8003f500 0000e500`00c30000

2、任务门执行过程:

  1. INT N

  2. 查IDT表,找到中断门描述符

  3. 通过中断门描述符,查GDT表,找到任务段描述符

  4. 使用TSS段中的值修改寄存器

  5. IRETD返回

3、代码

段选择子一定要计算正确

1
2
3
4
idt 0x20 构造中断门
eq 8003f500 0000e500`004b0000
gdt 0x4b 构造任务门 0100 1011 RPL:3
eq 8003f048 0000e912`fd700068
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
// demo07.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <stdio.h>
#include <windows.h>

DWORD dwOK,dwESP,dwCS;

void __declspec(naked) GetValue() {
// 将寄存器保存到全局变量中
dwOK = 1;
_asm {
mov eax,esp
mov dwESP,eax
mov ax,cs
mov word ptr[dwCS],ax

iretd
}
}

int main()
{
char stack[100] = {0}; // 栈
char buffer[6] = {0x0, 0x0, 0x0, 0x0, 0xC0, 0x0}; // 选择子
int iCr3 = 0;
printf("Input: ");
scanf("%x", &iCr3);
getchar();

DWORD tss[0x68] = {
0x00000000, // Previous Task Link
// 不同权限对应的ESP、SS,如果不涉及到权限切换所以可以将这些寄存器的值全部填0
0x00000000, // ESP0
0x00000000, // SS0
0x00000000, // ESP1
0x00000000, // SS1
0x00000000, // ESP2
0x00000000, // SS2
(DWORD)iCr3, // Cr3,与页的知识有关,必须要赋值
(ULONG)GetValue, // EIP,下一次执行代码的位置,必须要赋值,在代码中就是GetValue函数的地址
0x00000000, // EFLAGS
0x00000000, // EAX
0x00000000, // ECX
0x00000000, // EDX
0x00000000, // EBX
(DWORD)stack, // ESP,任务切换时也需要切换栈,所以在代码中我们可以声明一个数组,将其地址作为一块栈
0x00000000, // EBP
0x00000000, // ESI
0x00000000, // EDI
0x00000023, // ES
0x00000008, // CS,切到0环的代码段描述符
0x00000010, // SS,CS与SS需要保持一致
0x00000023, // DS
0x00000030, // FS,切到0环就是0x30,3环就是0x3B
0x00000000, // GS,Windows没有使用这个段寄存器所以永远是0
0x00000000, // LDT,填0
0x20ac0000 // IO_MAP,Windows2000以后不用了,默认值
};
printf("TSS:%x\n",&tss);
getchar();
_asm {
PUSH fs
INT 0X20 // 长调用
POP fs
}
// 输出寄存器的值证明完成了任务切换
printf("ESP: %x, CS: %x, dwOK: %x \n", dwESP, dwCS, dwOK);
getchar();
return 0;
}

image-20241007123514913

分页

1、4GB空间

image-20241008202635838

2、物理地址

地址分为:线性地址、有效地址、物理地址

指令:

1
MOV eax,dword ptr ds:[0x12345678]

0x12345678是属于有效地址

ds.base+0x12345678 = 线性地址

image-20241008202850335

3、物理在哪

每个进程都有一个CR3,(准确的说是都一个CR3的值,CR3本身是个寄存器,一个核,只有一套寄存器)

CR3指向一个物理页,一共4096字节

image-20241008203008475

4、设置分页方式

image-20241008203120205

noexecute 改成 execute

5、如何寻找物理地址(10-10-12为例)

10-10-12 一共32位,把一段地址拆分成三段10位 10位 12位

用notepad为例

第一步先在notepad中写入一个hello world字符串,然后使用CE进行搜索拿到字符串在进程中的地址

image-20241008214655861

第二步拆分地址

1
2
3
4
000AA750 hello world字符串在进程中地址
第一级:0000 0000 00 0
第二级:00 1010 1010 AA * 4 = 2A8
第三级:750

第三步windbg进行查看找到cr3的值

1
2
3
4
5
DirBase:314b7000
第一级:!dd cr3+0 = 314b7000+0
第二级:!dd 3192d067+2A8 后面三位是属性使用时改成000
第三级:!dd 319ed000+750
!db 319ed000+750 可验证

image-20241008215310139

PDE/PTE

PDE和PTE介绍

1、PDE与PTE

image-20241008221138790

2、指向相同物理页

  1. PTE可以没有物理页,且只能对应一个物理页
  2. 多个PTE也可以指向同一个物理页

3、实验

查分0地址,观察PTE是否有物理页

image-20241010194913603

向0地址读写数据

没有挂物理页之前向0地址写入不了数据

image-20241010195415642

手动挂物理页

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
x的线性地址:0x1245052
先拆分
0000 0001 00 10
10 0100 0101 245 * 4 = 914
052

DirBase: 553aa000
第一级:
kd> !dd 553aa000
#553aa000 54ad6067 54a95067 00000000 00000000
#553aa010 00000000 00000000 00000000 00000000
#553aa020 00000000 00000000 00000000 00000000
#553aa030 00000000 00000000 00000000 00000000
#553aa040 00000000 00000000 00000000 00000000
#553aa050 00000000 00000000 00000000 00000000
#553aa060 00000000 00000000 00000000 00000000
#553aa070 00000000 00000000 00000000 00000000

第二级:
kd> !dd 54ad6000 + 914
#54ad6914 54df5067 00000000 00000000 00000000
#54ad6924 00000000 00000000 00000000 00000000
#54ad6934 00000000 00000000 00000000 5576d067
#54ad6944 5556f067 553b0067 00000000 00000000
#54ad6954 00000000 00000000 00000000 00000000
#54ad6964 00000000 00000000 00000000 00000000
#54ad6974 00000000 00000000 00000000 0bb31025
#54ad6984 0bb32025 00000000 00000000 00000000

查询下0地址
kd> !dd 54ad6000
#54ad6000 00000000 00000000 00000000 00000000
#54ad6010 00000000 00000000 00000000 00000000
#54ad6020 00000000 00000000 00000000 00000000
#54ad6030 00000000 00000000 00000000 00000000
#54ad6040 54e97067 00000000 00000000 00000000
#54ad6050 00000000 00000000 00000000 00000000
#54ad6060 00000000 00000000 00000000 00000000
#54ad6070 00000000 00000000 00000000 00000000


把当前x变量的物理页挂到0地址上
kd> !ed 54ad6000 54df5067

image-20241010200641755

PDE/PTE 属性

1、物理页的属性

物理页的属性 = PDE属性 & PTE属性

image-20241010211858304

image-20241010211917981

2、P位

PDE和PTE的P位如果为0,则当前的物理页是无效的,反之

3、R/W位

R/W = 0 只读

R/W = 1 可读可写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "stdafx.h"
#include <windows.h>

int main(int argc, char* argv[])
{
char* str = "hello world";
printf("%x\n",str);
getchar();

DWORD dwAddr = (DWORD)str;

*(char*)dwAddr = 'M';

printf("%s\n",str);
return 0;
}

已知上面代码在正常情况下str是只读的,后续是无法对它进行任何的修改操作的。会报错内存访问异常

image-20241010212435527

现在我们对R/W位进行查询

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
0x420f94
Dirbase:7f4fd000
0000 0000 01
00 0010 0000 20*4=80
f94
第一级:
kd> !dd 7f4fd000
#7f4fd000 7f57b067 7f7ba067 00000000 00000000
#7f4fd010 00000000 00000000 00000000 00000000
#7f4fd020 00000000 00000000 00000000 00000000
#7f4fd030 00000000 00000000 00000000 00000000
#7f4fd040 00000000 00000000 00000000 00000000
#7f4fd050 00000000 00000000 00000000 00000000
#7f4fd060 00000000 00000000 00000000 00000000
#7f4fd070 00000000 00000000 00000000 00000000
第二级:
kd> !dd 7f7ba000 + 80
#7f7ba080 7c7e9025 00000000 00de4067 7f895067
#7f7ba090 7fe65067 7eb52027 00000000 00000000
#7f7ba0a0 00000000 00000000 00000000 00000000
#7f7ba0b0 00000000 00000000 00000000 00000000
#7f7ba0c0 7f8dc067 7f7dd067 7f5de067 7f8df067
#7f7ba0d0 7f960067 7fe61067 00de2067 7fbe3067
#7f7ba0e0 00000000 00000000 00000000 00000000
#7f7ba0f0 00000000 00000000 00000000 00000000

可以看到7c7e9025对应的二进制是

1
0111 1100 0111 1110 1001 0000 0010 0101  而第二位R/W:0 说明是只读权限

现在我们修改7c7e9025->7c7e9027

1
0111 1100 0111 1110 1001 0000 0010 0111
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kd> !dd 7f7ba000 + 80
#7f7ba080 7c7e9025 00000000 00de4067 7f895067
#7f7ba090 7fe65067 7eb52027 00000000 00000000
#7f7ba0a0 00000000 00000000 00000000 00000000
#7f7ba0b0 00000000 00000000 00000000 00000000
#7f7ba0c0 7f8dc067 7f7dd067 7f5de067 7f8df067
#7f7ba0d0 7f960067 7fe61067 00de2067 7fbe3067
#7f7ba0e0 00000000 00000000 00000000 00000000
#7f7ba0f0 00000000 00000000 00000000 00000000
kd> !ed 7f7ba080 7c7e9027
kd> !dd 7f7ba000 + 80
#7f7ba080 7c7e9027 00000000 00de4067 7f895067
#7f7ba090 7fe65067 7eb52027 00000000 00000000
#7f7ba0a0 00000000 00000000 00000000 00000000
#7f7ba0b0 00000000 00000000 00000000 00000000
#7f7ba0c0 7f8dc067 7f7dd067 7f5de067 7f8df067
#7f7ba0d0 7f960067 7fe61067 00de2067 7fbe3067
#7f7ba0e0 00000000 00000000 00000000 00000000
#7f7ba0f0 00000000 00000000 00000000 00000000

现在看看效果

image-20241010213609008

已经成功!!!!

4、U/S 位

U/S = 0 特权用户

U/S = 1 普通用户

5、P/S位

只对PDE有意义,PS == PageSize的意思

当PS==1的时候 PDE直接指向物理页 无PTE,低22位是页内偏移。也就是说少一级偏移。

6、A位

是否被访问(读或者写)过

A=1 访问过

[!NOTE]

即使只访问一个字节也会导致PDE或者PTE置1

7、D位

脏位:是否被写过

0:没有被写过

1:被写过

页目录表基址

1、拆分C0300000

当前我们用DbgView程序为例

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
DirBase: 238ef000

1100 0000 00 00 300 * 4 = C00
11 0000 0000 300 * 4 = C00
000

kd> !dd 238ef000
#238ef000 237e3067 2365a067 23a2e067 23a1c067
#238ef010 00000000 00000000 00000000 00000000
#238ef020 00000000 00000000 00000000 00000000
#238ef030 00000000 00000000 00000000 00000000
#238ef040 00000000 00000000 00000000 00000000
#238ef050 00000000 00000000 00000000 00000000
#238ef060 00000000 00000000 00000000 00000000
#238ef070 00000000 00000000 00000000 00000000

kd> !dd 238ef000 + C00 PDT
#238efc00 238ef063 236f0063 00000000 09f55163 //PDE
#238efc10 09f56163 09f57163 09f58163 09f59163
#238efc20 09f5a163 09f5b163 09f5c163 09f5d163
#238efc30 09f5e163 09f5f163 09f60163 09f61163
#238efc40 09f62163 09f63163 09f64163 09f65163
#238efc50 09f66163 09f67163 09f68163 09f69163
#238efc60 09f6a163 09f6b163 09f2c163 09f2d163
#238efc70 09f2e163 09f2f163 09f30163 09f31163

kd> !dd 238ef000 + C00 PTT
#238efc00 238ef063 236f0063 00000000 09f55163 //PTE
#238efc10 09f56163 09f57163 09f58163 09f59163
#238efc20 09f5a163 09f5b163 09f5c163 09f5d163
#238efc30 09f5e163 09f5f163 09f60163 09f61163
#238efc40 09f62163 09f63163 09f64163 09f65163
#238efc50 09f66163 09f67163 09f68163 09f69163
#238efc60 09f6a163 09f6b163 09f2c163 09f2d163
#238efc70 09f2e163 09f2f163 09f30163 09f31163

kd> !dd 238ef000
#238ef000 237e3067 2365a067 23a2e067 23a1c067
#238ef010 00000000 00000000 00000000 00000000
#238ef020 00000000 00000000 00000000 00000000
#238ef030 00000000 00000000 00000000 00000000
#238ef040 00000000 00000000 00000000 00000000
#238ef050 00000000 00000000 00000000 00000000
#238ef060 00000000 00000000 00000000 00000000
#238ef070 00000000 00000000 00000000 00000000

结论:C0300000存储的值就是PDT

如果我们要访问第N个PDE,那么有如下公式:0xC0300000 + N*4

2、页目录表基址(XP系统 10-10-12分页模式)

image-20241011170520641

3、总结

  1. 通过0xC0300000找到的物理页就是页目录表
  2. 这个物理页即是页目录表本身也是页表
  3. 页目录表是一张特殊的页表,每一项PTE指向的不是普通的物理页,而是指向其他的页表
  4. 如果我们要访问第N个PDE,那么有如下公式:0xC0300000 + N*4

页表基址

1、拆分C0000000 / C0001000

image-20241011172208229

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
DirBase: 238ef000
第一个PTT
kd> !dd 237e3000
#237e3000 00000000 00000000 00000000 00000000
#237e3010 00000000 00000000 00000000 00000000
#237e3020 00000000 00000000 00000000 00000000
#237e3030 00000000 00000000 00000000 00000000
#237e3040 23764067 00000000 00000000 00000000
#237e3050 00000000 00000000 00000000 00000000
#237e3060 00000000 00000000 00000000 00000000
#237e3070 00000000 00000000 00000000 00000000

第二个PTT
kd> !dd 2365a000
#2365a000 238ee025 23951025 23b08025 239c9025
#2365a010 23b0a025 23b8b025 23b4c025 2388d025
#2365a020 2398e025 238cf025 00000000 238ad025
#2365a030 00000000 239ef025 23830025 23931025
#2365a040 00000000 23833025 23734025 237f5025
#2365a050 236f6025 23677025 00000000 00000000
#2365a060 00000000 00000000 00000000 00000000
#2365a070 00000000 235a5025 23666025 00000000


C0000000
11 0000 0000 300 * 4 = C00
00 0000 0000 0
0

kd> !dd 238ef000 + C00
kd> !dd 238ef000 + 0
kd> !dd 237e3000
#237e3000 00000000 00000000 00000000 00000000
#237e3010 00000000 00000000 00000000 00000000
#237e3020 00000000 00000000 00000000 00000000
#237e3030 00000000 00000000 00000000 00000000
#237e3040 23764067 00000000 00000000 00000000
#237e3050 00000000 00000000 00000000 00000000
#237e3060 00000000 00000000 00000000 00000000
#237e3070 00000000 00000000 00000000 00000000
用0xC0000000查出来的是第一张PTT表

C0001000
11 0000 0000 300 * 4 = C00
00 0000 0001 1 * 4 = 0
0

kd> !dd 238ef000 + C00
kd> !dd 238ef000 + 4
kd> !dd 2365a000
#2365a000 238ee025 23951025 23b08025 239c9025
#2365a010 23b0a025 23b8b025 23b4c025 2388d025
#2365a020 2398e025 238cf025 00000000 238ad025
#2365a030 00000000 239ef025 23830025 23931025
#2365a040 00000000 23833025 23734025 237f5025
#2365a050 236f6025 23677025 00000000 00000000
#2365a060 00000000 00000000 00000000 00000000
#2365a070 00000000 235a5025 23666025 00000000
用0xC0001000查出来的是第二张PTT表

线性地址0xc0000000映射到第一个PTT表,0xC0000000到0xC0001000刚好相差0x1000相当于一个页,后续地址增量为4KB,对应不同的PTT表。 0xc0300000线性地址用于访问页目录表本身。掌握0xc0000000和0xc0300000两个地址等于掌握了进程内存的读写权限

总结:

  • 页表被映射到了从0xC00000000xC03FFFFF4M地址空间
  • 在这1024个表中有一张特殊的表:页目录表
  • 页目录被映射到了0xC0300000开始处的4K地址空间

2、总结

PDI与PTI:10-10-12 (I就是Index)

访问页目录表的公式:0xC0300000 + PDI*4

访问页表的公式:0xC0000000 + PDI*4096 + PTI*4

2-9-9-12分页

随着硬件的发展,这时候10-10-12分页的物理地址显然不够用,因此2-9-9-12分页随之产生。

1、10-10-12分页划分

  • 先确定了页的大小 4K 所以后面的12位的功能就确定了。(4096 = 2^12)
  • 当初的物理内存比较小,所以4个字节的PTE就够了,加上页的尺寸是4K 所以一个页能存储1024个 PTE 也就是2的10次方 第二个10也就确定了。(4096/4 = 1024 = 2^10)

image-20241013210854043

  • 剩下的10为PDI 10+10+12刚好32位

2、2-9-9-12分页划分

  • 页的大小是确定的,4KB不能随便改,所以12确定了
  • 如果想增大物理内存的访问范围,就需要增大PTE,考虑对齐的因素,增加到8个字节

image-20241013212749894

  • 同理PDI也是2的9次方32,2-9-9-12 还差2位 所以就再做一级 叫PDPI

3、2-9-9-12分页结构

image-20241013213455559

PDPTE:Page-Directory-Point-Table Entry页目录指针表项 每项占8个字节

image-20241022115028877

第四项PDPTE指向的是第四张PDT表时,第四张PDT表的前四项又分别指向PDPTT表的PDPTE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
总结如下:	
1.第三个PDPTE指向了一个PDT表,此表的前四项 指向了PDPTE的每一个元素
2.根据计算 C0600000 的最大索引,不会超过 C0604000
mov edi, edi
.text:0043C92A 55 push ebp
.text:0043C92B 8B EC mov ebp, esp
.text:0043C92D 51 push ecx
.text:0043C92E 51 push ecx
.text:0043C92F 8B 4D 08 mov ecx, [ebp+VirtualAddress]
.text:0043C932 56 push esi
.text:0043C933 8B C1 mov eax, ecx
.text:0043C935 C1 E8 12 shr eax, 12h
.text:0043C938 BE F8 3F 00 00 mov esi, 3FF8h
.text:0043C93D 23 C6 and eax, esi
.text:0043C93F 2D 00 00 A0 3F sub eax, 3FA00000h
线性地址 >>18 +C0600000 也就是说。得出的索引是 2^14,最大值4000H 实际应用从0开始,也就是 4000H-(1*8byte)
那么得到最后一个PDE 也就是 C06003FFE8,由此可推出

C0600000是第一个PDT表的首地址
C0601000是第二个PDT表的首地址
C0602000是第三个PDT表的首地址
C0603000是第四个PDT表的首地址

4、如何寻找物理地址(2-9-9-12为例)

2-9-9-12 一共32位,把一段地址拆分成三段2位 9位 9位 12位

用notepad为例

第一步先在notepad中写入一个hello world字符串,然后使用CE进行搜索拿到字符串在进程中的地址

image-20241008214655861

第二步拆分地址

1
2
3
4
5
000AA750 hello world字符串在进程中地址
第一部分:00 0*8
第一部分:00 0000 000 0*8
第一部分:0 1010 1010 AA * 8 = 550
第四部分:750

第三步windbg进行查看找到cr3的值

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
DirBase:123402a0
第一级:!dd 123402a0
kd> !dd 123402a0
#123402a0 288a8001 00000000 28729001 00000000
#123402b0 2886a001 00000000 287a7001 00000000
#123402c0 1d083001 00000000 1cf44001 00000000
#123402d0 1cec5001 00000000 1d142001 00000000
#123402e0 1d402001 00000000 1d383001 00000000
#123402f0 1d1c4001 00000000 1d301001 00000000
#12340300 1cc20001 00000000 1cce1001 00000000
#12340310 1cc62001 00000000 1cb5f001 00000000

第二级:!dd 288a8000+0 后面三位是属性使用时改成000
kd> !dd 288a8000
#288a8000 285e4067 00000000 28b43067 00000000
#288a8010 28696067 00000000 00000000 00000000
#288a8020 285e5067 00000000 2885e067 00000000
#288a8030 28ca1067 00000000 29088067 00000000
#288a8040 28757067 00000000 00000000 00000000
#288a8050 00000000 00000000 00000000 00000000
#288a8060 00000000 00000000 00000000 00000000
#288a8070 00000000 00000000 00000000 00000000


第三级:!dd 285e4000+550
kd> !dd 285e4000+550
#285e4550 2892b067 80000000 28821067 80000000
#285e4560 28922067 80000000 286e4067 80000000
#285e4570 28625067 80000000 28866067 80000000
#285e4580 288e7067 80000000 28a68067 80000000
#285e4590 288e9067 80000000 00000200 00000000
#285e45a0 00000200 00000000 00000200 00000000
#285e45b0 29142067 80000000 28c8d067 80000000
#285e45c0 28ed1067 80000000 28d94067 80000000

第四级:!dd 2892b000 + 750
kd> !dd 2892b000 + 750
#2892b750 00650068 006c006c 0020006f 006f0077
#2892b760 006c0072 00000064 00000000 00000000
#2892b770 00000000 00000000 00000000 00000000
#2892b780 00000000 00000000 00000000 00000000
#2892b790 00000000 0088000c 000a0005 0008014a
#2892b7a0 00000000 000350ac 000aaec8 000aa3e0
#2892b7b0 00000000 00000000 00000000 00000000
#2892b7c0 0005002e 00102341 000800cc 00000000

!db 2892b000 + 750 可验证

image-20241013214120803

开启2-9-9-12:将C:\boot.ini文件中的execute改为noexecute 重启

5、PDPTE结构

image-20241013215504128

  • PDPTE共有四项(4 = 2^2)
  • 35-12 存储的是页目录表的基址,低12位补0,共36位,即页目录基址。

6、PDE结构

image-20241014105554938

image-20241014105612693

  • PS=1时是大页,35-21位是大页的物理地址,这样36位的物理地址的低21位为0,这就意味着页的大小为2MB,且都是2MB对齐。
1
2^21 = 2MB
  • PS=0时,35-12位页表基址低12位补0,共36位
1
2^12 = 4KB

在所有的表项中(除了指向2MB页的页目录项),基地址都被视为36位物理地址的高24位,这就迫使页表和页都是4KB对齐的(这样,36位物理地址的低12位都为0)。当页目录项指向一个2MB的页时,基地址被视为36位物理地址的高15位,这就迫使2MB的页都是2MB对齐的(这样,36位物理地址的低21位为0)

7、PTE结构

image-20241014132226411

PTE中35-12是物理页基址,24位,低12位补0

物理页基址+12位的页内偏移指向具体数据

8、XD标志位

AMD中称为NX,即No Excetion

PDE/PTE结构:

image-20241014132452904

段的属性:有可读、可写和可执行

页的属性:有可读、可写

当RET执行返回的时候,如果我修改堆栈里面的数据指向一个我提前准备好的数据(把数据当作代码来执行)所以,Intel就做了硬件保护,做了一个不可执行位,XD=1时。那么你的软件溢出了也没有关系,即使你的EIP蹦到了危险的“数据区”,也是不可以执行的!在PAE分页模式下,PDE与PTE的最高位为XD/NX位

9、页目录表,页表基地址

PAE分页模式下

页目录表基址:0xC0600000
页表基址:0xC0000000

PDPTE、PDI与PTI

1
2-9-9-12  (I就是Index)

访问页目录表项(PDE)的公式:

1
0xc0600000 + PDPTI * 0x200 * 8 + PDI * 8 

访问页表项(PTE)的公式

1
0xc0000000 + PDPTI * 0x200 * 0x200  * 8 + PDI * 0x200 * 8 + PTI * 8

TLB

1、地址解析

  • 通过一个线性地址访问一个物理页。比如:一个DWORD,其实未必真正读的是4个字节,我们先读的PDE再读PTE,最后才读的4个字节的页。
  • 在2-9-9-12会读24个字节,如果跨页可能更多。为了提高效率,只能做记录。CPU内部做了一个表,来记录这些东西,这个表格是CPU内部的,和寄存器一样快,这个表格:TLB

2、TLB结构

image-20241014201244043

  1. ATTR(属性):属性是PDPE PDE PTE三个属性AND起来的. 如果是10-10-12 就是PDE and PTE
  2. 不同的CPU 这个表的大小不一样
  3. 只要Cr3变了,TLB立马刷新,一核一套TLB.

操作系统的高2G映射基本不变,如果Cr3改了,TLB刷新 重建高2G以上很浪费。所以PDE和PTE中有个G标志位,如果G位为1刷新TLB时不会刷新 PDE/PTE的G位为1的页,当TLB满了,根据统计信息将不常用的地址废弃,最近最常用的保留

3、TLB种类

TLB在X86体系的CPU里的实际应用最早是从Intel的486CPU开始的,在X86体系的CPU里边,一般都设有如下4组TLB:

第一组:缓存一般页表(4K字节页面)的指令页表缓存(Instruction-TLB)

第二组:缓存一般页表(4K字节页面)的数据页表缓存(Data-TLB)

第三组:缓存大尺寸页表(2M/4M字节页面)的指令页表缓存(Instruction-TLB)

第四组:缓存大尺寸页表(2M/4M字节页面)的数据页表缓存(Data-TLB)

中断与异常

1、什么是中断

  1. 中断通常是由CPU外部的输入输出设备(硬件)所触发的,供外部设备通知CPU“有事情需要处理”,因此又叫中断请求(Interrupt Request)
  2. 中断请求的目的是希望CPU暂时停止执行当前正在执行的程序,转去执行中断请求所对应的中断处理例程(中断处理程序在哪有IDT表决定)
  3. 80x86有两条中断请求线
  • 非屏蔽中断线,称为NMI(NonMaskable Interrupt)
  • 可屏蔽中断线,称为INTR(Interrupt Require)

2、非可屏蔽中断如何处理

image-20241014201812991

非可屏蔽中断产生时,CPU在执行完当前指令后会里面进入中断处理程序非可屏蔽中断不受EFLAG寄存器中IF位的影响,一旦发生,CPU必须处理非可屏蔽中断处理程序位于IDT表中的2号位置

3、可屏蔽中断

在硬件级,可屏蔽中断是由一块专门的芯片来管理的,通常称为中断控制器.它负责分配中断资源和管理各个中断源发出的中断请求.为了便于标识各个中断请求,中断管理器通常用IRQ(Interrupt Request)后面加上数字来表示不同的中断

比如:在Windows中 时钟中断的IRQ编号为0 也就是:IRQ0

4、时钟中断

image-20241014202301365

大多数操作系统时钟中断在10-100MS之间,Windows系列为10-20MS

5、可屏蔽中断如何处理

(IDT表)中断号 IRQ 说明
0x30 IRQ0 时钟中断
0x31-0x3F IRQ1-IRQ15 其他硬件设备的中断

如果自己的程序执行时不希望CPU去处理这些中断

  • CLI指令清空EFLAG寄存器中的IF位
  • STI指令设置EFLAG寄存器中的IF位

硬件中断与IDT表中的对应关系并非固定不变的,参见:APIC(高级可编程中断控制器)

6、异常

异常通常是CPU在执行指令时检测到的某些错误,比如除0、访问无效页面等

中断与异常的区别:

1、中断来自于外部设备,是中断源(比如键盘)发起的,CPU是被动的.

2、异常来自于CPU本身,是CPU主动产生的

3、INT N虽然被称为“软件中断”,但其本质是异常。EFLAG的IF位INT N无效

7、异常处理

无论是由硬件设备触发的中断请求还是由CPU产生的异常处理程序都在IDT表

常见的异常处理程序:

错误类型 (IDT表)中断号
页错误 0xE
段错误 0xD
除零错误 0x0
双重错误 0x8

8、缺页异常

缺页异常的产生:

1、当PDE/PTE的P=0时。

2、当PDE/PTE的属性为只读,但程序试图写入的时一旦发生缺页异常,CPU会执行IDT表中的0xE号中断处理程序,由操作系统来接管。

image-20241019112656885

控制寄存器

1、控制寄存器

image-20241018121239756

控制寄存器用于控制和确定CPU的操作模式

控制寄存器类型:Cr0、Cr1、Cr2、Cr3、Cr4、Cr8

Cr0:包含系统控制标志,用于控制处理器的工作模式和状态

Cr1:保留

Cr2:包含页面错误线性地址(导致页面错误的线性地址)

Cr3:页目录表基址

Cr4:包含一组标志位,用于启用一些体系结构扩展功能,并指示操作系统或执行程序对特定处理器功能的支持情况。

Cr8:仅64位下使用

2、Cr0

image-20241018120049371

PE位:CR0的位0是启用保护(Protection Enable)标志。

PE=1保护模式

PE=0 实地址模式

1
这个标志仅开启段级保护,而并没有启用分页机制。若要启用分页机制,那么PE和PG标志都要置位。

PG位:当设置该位时即开启了分页机制。在开启这个标志之前必须已经或者同时开启PE标志。

1
2
3
4
5
PG=0且PE=0  处理器工作在实地址模式下
PG=0且PE=1 处理器工作在没有开启分页机制的保护模式下

PG=1且PE=0 在PE没有开启的情况下 无法开启PG
PG=1且PE=1 处理器工作在开启了分页机制的保护模式下

WP位:对于Intel 80486或以上的CPU,CR0的位16是写保护(Write Proctect)标志当设置该标志时,处理器会禁止超级用户程序(例如特权级0的程序)向用户级只读页面执行写操作;

1
2
3
4
当CPL<3的时候:

如果 WP=0 可以读写任意用户级物理页,只要线性地址有效.
如果 WP=1 可以读取任意用户级物理页,但对于只读的物理页,则不能写.

3、Cr2

image-20241018120805029

当CPU访问某个无效页面时,会产生缺页异常,此时,CPU会将引起异常的线性地址存放在CR2中。

4、Cr4

image-20241018120906772

PAE:1 是2-9-9-12分页0 是10-10-12分页

PSE:

image-20241018120946600

PWT/PCD

1、CPU缓存

  1. CPU缓存是位于CPU与物理内存之间的临时存储器,它的容量比内存小的多但是交换速度却比内存要快得多。
  2. CPU缓存可以做的很大,有几K、几十K、几百K 甚至上M的也有。

CPU缓存与TLB的区别:

TLB:线性地址 <—–> 物理地址

CPU缓存:物理地址 <—–> 内容

所以当拿到一个程序的线性地址查询时,先会使用TLB,然后在使用CPU缓存

2、关于PWT/PCD属性

PWT:Page Write Through

1
PWT =  1 时 写Cache的时候也要将数据写入内存中。

PCD:Page Cache Disable

1
PCD = 1时,禁止某个页写入缓存,直接写内存。	比如,做页表用的页,已经存储在TLB中了,可能不需要再缓存了

具体细节参考Intel白皮书

调试踩坑

1、32位的系统就得用老版本的32位windbg,不要用windbg perview 暂时不知道怎么解决

2、windbg老版本遇到寄存器内容无法显示

WingDbg.dll插件放windbg相同目录下

1
!WingDbg.regfix

下载地址:https://github.com/mbikovitsky/WingDbg


本站由 RuntimeBroker 使用 Stellar 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

本"页面"访问 次 | 👀总访问 次 | 总访客