内核-①保护模式
①--------------------------------------保护模式-段的机制 -----------------------------------
学习保护模式的原因
学习了保护模式,可以不通过系统提供的API而直接访问高2G内存。如:读取进程,可以不使用系统API在3环读取进程信息
内核情景分析 毛德操
windows内核原理实现 潘爱民
1 论证段寄存器96位
段寄存器一共有八个:ES CS SS DS FS GS LDTR TR
_asm{
mov ax,cs
mov ds,ax
mov dword ptr ds:[地址],eax //---》报错,cs段不可写入
}
_asm{
mov ax,ss
mov ds,ax
mov dowrd ptr ds:[地址],eax //---》ss段可以写入
}10
1
_asm{2
mov ax,cs3
mov ds,ax4
mov dword ptr ds:[地址],eax //---》报错,cs段不可写入5
}6
_asm{7
mov ax,ss8
mov ds,ax9
mov dowrd ptr ds:[地址],eax //---》ss段可以写入10
}Base属性:
_asm{
mov ax,fs
mov gs,ax
mov eax,gs:[0] //---》[0]地址,正常情况不能读也不能写的,但是此时却可以执行成功,
//说明真正访问的地址是fs.Base+0,相当于mov eax,ds:[0x7ffde000],这个地址是可以读的
}6
1
_asm{2
mov ax,fs3
mov gs,ax4
mov eax,gs:[0] //---》[0]地址,正常情况不能读也不能写的,但是此时却可以执行成功,5
//说明真正访问的地址是fs.Base+0,相当于mov eax,ds:[0x7ffde000],这个地址是可以读的6
}Limit属性:
mov ax,fs
mov gs,ax
mov eax,gs:[0x1000]【相当于:mov eax,dword ptr ds:[0x7ffde000+0x1000]】,但是ds段的长度是8个F,fs的长度只有3个F
点击运行,会发现报错,因为0x1000的长度已经超过了0xFFF,在fs的段里面,不能找到0x1000这个地址4
1
mov ax,fs2
mov gs,ax3
mov eax,gs:[0x1000]【相当于:mov eax,dword ptr ds:[0x7ffde000+0x1000]】,但是ds段的长度是8个F,fs的长度只有3个F4
点击运行,会发现报错,因为0x1000的长度已经超过了0xFFF,在fs的段里面,不能找到0x1000这个地址2. 段寄存器的结构
段寄存器以结构体方式表现
struct SegMent
{
WORD Selector; //16为Selecter,表示可见的16位(段选择子)
WORD Attributes; //16位Atrribute,表示读写执行属性,段描述符的8~23位
DWORD Base; //32位Base,表示当前段的起始位置
DWORD Limit; //32位Limit,表示当前段的长度
}8
1
段寄存器以结构体方式表现2
struct SegMent3
{4
WORD Selector; //16为Selecter,表示可见的16位(段选择子)5
WORD Attributes; //16位Atrribute,表示读写执行属性,段描述符的8~23位6
DWORD Base; //32位Base,表示当前段的起始位置7
DWORD Limit; //32位Limit,表示当前段的长度8
}3. 读写段寄存器
读:
段寄存器在读取的时候只能读取可见的16位mov, ax,es
写:
读取段寄存器时只能读取可见的16位,但是写入的时候可以写入96位,(段寄存器的值是通过段选择子来填充的)
mov es,ax
给的16位数是随便写的吗:当我们执行mov ds,ax的时候,CUP会查表,根据ax(段选择子)的值来决定查找GDT还是LDT,查找表的什么位置,查出多少数据 (详见段选择子)
当写寄存器的时候,只给了16位,剩下的80位从哪里来?
96位:
其中16位:段选择子其中80位:根据段选择子找到8字节(64位)的段描述符填充,G为标志位,一个G可以顶12位G=0填充0x000,G为1填充0xFFF,其他位有些是重复使用了(详见段描述符)
4. 段描述符与段选择子
4.1 GDT表(全局描述符)
同一台计算机上的所有程序共享一个GDT表,通过kd>r gdtr得到GDT表地址,当我们执行mov ds,ax的时候,CUP会查表,根据ax的值来决定查找GDT表还是LDT表,查找表的什么位置,查出多少数据实验:查看GDT表
打开windebug,kd>窗口输输入:①r gdtr【查看GDT表的位置】 ②r gdtl【查看GDT表的大小】 ③dq ①得到的地址gdtr寄存器:48位的寄存器,存放内容:①GDT这张表的位置(32位),②GDT表的大小(16位)
得到GDT表,里面存放的是段描述,每一个段描述符是8个字节,高32位对应的是前面4字节的值,低32位对应的是后面4字节的值
④dq 地址 L40【显示更多内容】
LDT表(局部描述符)
4.2 段描述符
typedef struct _DESCRIPTOR
{
unsigned int limit1 :16; // 段限长 [0-15] //--------低地址--------
unsigned int base1 :16; // 段基址 [0-15]
unsigned int base2 : 8; // 段基址 [16-23]
unsigned int TYPE : 4; // 类型
unsigned int S : 1: // 系统段0 \ 用户段1
unsigned int DPL : 2; // 段特权级别
unsigned int P : 1; // 有效位
unsigned int limit2 : 4; // 段限长 [16-19]
unsigned int AVL : 1; // 保留
unsigned int L : 1; // 保留
unsigned int DB : 1; // 默认大小
unsigned int G : 1; // 限长的单位
unsigned int base3 : 8; // 段基址 [24-31] //--------高地址--------
} DESCRIPTOR, *PDESCRIPTOR;GDT是一张表(GDTR是一个寄存器),里面存放的是段描述符,每一个段描述符是8个字节
AVL--- 供系统软件使用
BASE--段基址
D/B---- 默认操作大小 (0 = 16-bit segment; 1 = 32-bit segment)
DPL----描述符特权级别Descriptor privilege level
G------ Granularity
LIMIT-段长度Segment Limit
P--------当前段Segment present
S--------描述符类型Descriptor type (0 = system; 1 = code or data)
TYPE--段类型Segment type 段描述符是如何填充段寄存器的:(FS例外)WORD Selector:由段选择子填充WORD Attributes:对应段描述符高4字节的8-23位,刚好16位DWORD Base:高8部分(24~31):段描述符高4字节的24~31位中8部分(16~23):段描述符高4字节的00~07位低16部分(0~15):段描述符低4字节的16~31位DWORD Limit:高位部分(16~19位):段描述符高4字节的16~19位低16位部分(0~15位):段描述符低4字节的0~15位上面2部分总共才20位但是段寄存器的Limit是32位的--》看段描述符的G位G=0时,Limit为000FFFFFG=1时,Limit为FFFFFFFFP位:通过指令将段描述符加载到寄存器的时候第一件事就是检查P位,P为0将不会再继续,P为1会继续其他检查
P=1:段描述符有效;P=0:段描述符无效
G位:粒度,G=1:以4kb为单位,Limit为FFFFFFFF(FFFFF*4KB+FFF);
G=0:以字节为单位,Limit为000FFFFF(FFFFF*1);
DLP位:DPL存储在段描述符中,规定了访问该段所需要的特权级别是什么 (段描述符第5位数拆解后的后两位)
情况①值为0:00;情况②值为3:11
S位:当S位为1时,有以下两种情况:①代码段描述符 ②数据段描述符
当S位为0时,有以下情况:系统段的描述符
Type域:下面分析
DB位:下面分析4.2.2 判断数据段和代码段练习:通过段选择子填充段寄存器
mov es,0x1B
①拆解段选择子:00011 0 11 :倒数第3位为0,查GDT表
②找到段描述符:GDT表中第[3]个段描述符:00cffb00`0000ffff
高4字节00cffb00: 00000000 1100111111111011 00000000 低4字节0000ffff: 00000000 00000000 11111111 11111111
③根据段描述符查表:00cffb00`0000ffff
段寄存器
Selector: 0x001B [RPL = 3 、TI = 0 、Index = 3]
Attributes: 1100111111111011 = CFFB
Base: 00+00+0000 = 00000000
Limit: G=1, FFFFF+FFF
④拼凑段寄存器:转成16进制:FFFFFFFF00000000CFBB001B 共96位
4.2.3 系统段 调用门描述符:
S位为0,Type为1100,这就是系统段。真正跳转的地址是:段选择子指向的段描述符的Base+跳转地址
4.2.4 Type域
当S位不同时,Tpye域有不同的意义4.2.2.1 S位为1时,Type的意义
- S位为1,说明当前的段寄存器时数据段或代码段,参考下图
数据段(0):
- 第[11]位为0(Type中的第一位)
- Type的最高位为0时,Type的值一定小于8 (Tpye的二进制最大值:0111) -->段描述符的第6位数小于8,说明是数据段
- A:段描述符的第[8]位,A (访问位)
- A为0:段描述符没有加载过,没有被访问过
- A为1:段描述符被使用过,被访问过
- W:段描述符的第[9]位,W (可写位)
- W为0:不可写
- W为1:可写
- E:段描述符的第[10]位
- E为0,向上拓展,表示段述符地址的有效范围是从Base开始+Limit(下图红色区域)
- E为1:向下拓展,表示段描述符地址的有效范围是除了从Base开始+Limit以外的地方(下图的红色区域)
![]()
代码段(1)
- 第[11]位为1(Type中的最高位)
- Type的第一位为1时,Type的值一定大于等于8 (Tpye的二进制最小值:1000) -->段描述符的第6位数大于等于8,说明是代码段
- A:段描述符的第[8]位,A (访问位)
- A为0:段描述符没有加载过,没有被访问过
- A为1:段描述符被使用过,被访问过
- R:段描述符的第[9]位,R
- R为0:不可读
- R为1:可读
- C:段描述符的第[10]位,C (一致位)
- C=0:非一致代码段(CPL=DPL且RPL<=DPL)只能同环访问
- C=1:一致代码段(CPL>=DPL,3环可访问)也称为共享的段 (CPL>DPL时,是3环访问,CPL=DPL时,是0环访问),低特权可以访问高特权
4.2.2.2 S位为0时,Type的意义
当S位为0时,【描述符是系统段】,S+DPL+P不等于9或F (第5位数)
4.2.2.3 Type小结
- 1.代码段:第5位为9或F( 1+11+1 或 1+00+1 )
- 第6位大于8(1000)
- 2.数据段:第5位为9或F( 1+11+1 或 1+00+1 )
- 第6位小于8(1000)
- 3.系统段:第5位不等于9或F
- 4.Type
- 第一位=1时,代码段:
- 1 1 1 1
- 代码 一致 可读 被使用过
- 第一位=0时,数据段:
- 0 1 1 1
- 数据 向下拓展 可写 被访问
4.2.5 D/B位
高4字节的第[22]位
情况一:对CS段的影响(D位)。
- D=1:默认采用32位寻址方式
- D=0:默认采用16位寻址方式
- 前缀67 改变默认寻址方式
情况二:对SS段的影响(B位)。
- B=1:隐式堆栈访问指令 (如:call,push,pop,隐式修改esp)
- 使用32位的堆栈指针寄存器
- B=0:隐式堆栈访问指令 (如:call,push,pop,隐式修改sp)
- 使用16位的堆栈指针寄存器
情况三:向下拓展的数据段
- D=1:段的上线为4GB(Limit是在这个范围之内的)
- 数据段向下拓展的范围为:0~Base + Base+Limit~4GB(上图红色部分)
- D=0:段的上线为4KB(Limit是在这个范围之内的)
- 数据段向下拓展的范围为:0~Base + Base+Limit~4KB(上图红色部分)
4.3 段选择子
typedef struct _SELECTOR
{
unsigned short index: 13; // index 存在于 GDT 或 LDT 中的下标
unsigned short TI : 1; // TI 当前查 GDT(0) 还是 LDT(1)
unsigned short RPL : 2; // RPL 请求权限级别
} SELECTOR, *PSELECTOR;mov ax,fsmov ds,ax【这里的as就是一个段选择子】段选择子是一个16位的段描述符,该描述符指向了定义该段的段描述符001B: 0000 0000 0001 1011低2位:请求特权级别第3位:0时查GDT表,为1时查LDT表。(Window下一般都是GDT表)第4~15位:指向段描述符(下标)第[高12位的值]个段描述符。(也可以这样计算:值*8+GDT表的基址)拆分段选择子:23
0000 0000 0010 0011
4.5 段权限检查
4.5.1 区分:RPL、CPL、DPL
RPL:请求特权级别;mov ax,001B mov ds,ax 看我们提供的001B,转成二进制后的最后2位 //RLP是针对段选择子而言的,可以自己随便写CPL:CPU当的特权级 看CS段SS段 //当前程序CS和SS段的权限(代码执行一定会用堆栈,就一定会用SS)DPL:访问该段所需要的特权级别 看段描述符的DPL字段 //DPL存储在段描述符中,规定了【访问该段所需要的特权级别是什么】
4.5.2 当前程序的特权级(CPL)
CS和SS中,存储的【段选择子后2位】 如:001B: 0000 0000 0001 1011 后两位为11,值为3-->当前的程序处于3环
0008: 0000 0000 0000 0100 后两位为00,值为0-->当前程序处于0环
4.5.3 数据段权限检查
①CPL<=DPL (当前程序权限是否足够) 并且RPL<=DPL (段选择子请求的权限是否足够) (数值越大,权限越小)
4.5.4 代码段的权限检查
(假设此处是代码段,不是调用门之类的) 区分是一致代码段还是非一致代码段①非一致代码段要求:CPL=DPL且RPL<=DPL②一致代码段要求: CPL>=DPL
满足上面权限才可访问练习:
- kd> r gdtr
- gdtr= 8003f 000
- kd> dq 8003f 000
- 得到8个段描述符:
- 在3环能加载的数据段有哪些?
- 在0环能加载的数据段有哪些?
- 详细描述这下面代码的执行过程:
- mov ax,0x23
- mov ds,ax
5 代码间的跳转(远跳)
(远跳转 JMP FAR;段间的跳转不是调用门之类的)
段间跳转,有2种情况:①一致代码段②非一致代码段
同时修改EIP和CS的指令JMP FAR指令(段跳转):当跨段的时候,不能仅仅修改Eip,因为Eip和CS段时一起的
应该使用以下指令修改:JMP FAR/CALL FAR/RETF/INT/IRETED
跳转指令:JMP 0x20:0x004189D71.段选择子拆分
0x20:0000 0000 0010 0000
RPL=00 【请求级别】
TI=0 【选择GDT表】
index=100=4 【第[4]个段描述符】2.查表得到段描述符
判断是否是代码段【S位,Type最高位】
只有代码段、调用门、TSS任务段、任务门才可以跳转(除了代码段,其他都是系统段描述符的)3.权限检查
假设此处是代码段,不是调用门之类的
区分是一致代码段还是非一致代码段
非一致代码段要求:CPL=DPL且RPL<=DPL
一致代码段要求: CPL>=DPL
满足上面权限才可访问4.加载段描述符
通过前面的权限检查后,CPU会将段描述符加载到CS段寄存器中5.代码执行
CUP将CS.Base+Offset(0x4189D7)的值写入EIP,然后执行CS:EIP处的代码,段间跳转结束
5.1 OD实现远跳
使用OD实现对EIP和CS的修改完成跳转
| 计算出段选择子 指令:JMP FAR 段选择子:77D4EAAB 如: JMP FAR 20:77D47AAB 要保证JMP FAR 后面的段选择子最终指向的会是代码段,自己在GDT表中插入一个代码段描述符(可以直接复制cs段的段描述符),然后用这个代码段描述符的下标计算出段选择子 | 修改完之后,EIP和CS都被修改了,代码也执行到77D4EAAB了,只是段选择子改之前是CS即23,改之后是计算得来的段选择子 |
5.2 3环跳转0环
实现3环跳转到0环的代码段
| 假设需要跳转段描述符是00cffb00`0000ffff,此时,是代码段,并且是非一致代码段(b=1011) 正常跳转跳不过去,因为非一致代码段权限检查要求同环,但是我们可以修改为一致代码段,尝试跳转 1.我们可以通过Windebug在GDT表中修改段描述符,只是将该段描述符的属性改成一致代码段 ①将代码段修改成系统段(第五位改成9)-->S+DPL+P==1+00+1==1001 ②将非一致代码修改成一致代码段(第6位改成f,大于8,且第二为也是为1,1111) 00CFFB00 0000FFFF修改成00CF9F00 0000FFFF 【该段寄存器可由低权限访问高权限】 2.计算出段描述符的段选择子:Index+TI+RPL, TI是GDT表,故TI为0 RPL是00权限,故RPL为00 Index是GDT表中第几个段描述符 3.然后将指令JMP FAR 段选择子:跳转地址,(段选择子是第2步得到的段选择子) 4.执行代码 JMP FAR 段选择子:跳转地址 此时,执行代码的程序只是一个应用层的程序CPL是3,但是它要跳转的代码段的DPL是00(第1步的①修改的) 如果是非一致代码段(同环才可访问),是不可以跳转的,但是我们在第1步的②中将非一致代码段修改成了一致代码段(允许低权限访问高权限的代码段) 执行跳转之后,发现可以跳转成功,EIP和CS也都修改了。说明成功由低权限跳转到跳转到了系统的非一致代码段 |
5.3 远跳转总结
5.3.1 JMP FAR步骤:
- 拆分段选择子
- 查找段描述符
- 权限检查
- 加载段描述符
- 执行代码:Base+偏移
5.3.2 一致代码段
(也就是共享的段 ,给低权限用的)
- 权限低的可以访问权限高的
- 权限高的不可以访问权限低的
- 作用:
- 当3环需要访问0环的数据或代码时,但是这些数据或代码即是被修改也不会对系统内核造成破坏,此时就可以使用一致代码段,这样当访问或修改时,就不需要提权,进0环,返回3环等操作了
5.3.3 非一致代码段
(也就是普通代码)
- 只允许同环访问,CPL必须等于DPL,3环的不能访问0环的 (保证安全)
- 直接对代码进行JMP或CALL操作,无论目标是一致代码段还是非一致代码段,CPL的权限都不会发生改变,如果想要提升CPL的权限,只能通过"调用门"
6. 跨段调用
6.1 跨段不提权,长调用
(指令格式:CALL CS:EIP,EIP是被废弃的)
- 流程:
- ①查表,通过CS找到段描述符
- ②权限匹配
- ③若匹配成功,将段门描述符加载到CS段
- ④一旦执行成功,CS(段)就被换了。但是返回的时候,还需要返回原来的CS。所以原CS也会被push进栈
- 跨段不提权:当前的CPL等于要跳转的段的DPL
- CS:段选择子,通过它查找GDT表的一个段描述符,这个段描述符必须是一个调用门,通过调用门计算出需要执行的代码地址
- 堆栈变化:
- 小结:CALL FAR不提权调用,通过CS在GDT表中找到调用门描述符,发生改变的寄存器:ESP EIP CS
6.2 跨段提权,长调用
(指令格式:CALL CS:EIP,EIP是被废弃的)
- 流程:
- ①查表,通过CS找到段描述符
- ②权限匹配
- ③若匹配成功,将段门描述符加载到CS段
- ④一旦执行成功,CS(段)就被换了。并且权限也提升了,CS权限提升,SS的权限也必须一起提升。而返回的时候,还需要返回原来的CS。而SS变化了,就不能再使用原来的堆栈了。所以原CS和SS还有ESP也会被push进栈。
- 跨段并提权:当前CPL权限大于要跳转的段的DPL权限
- CS:段选择子,通过它查找GDT表的一个段描述符,这个段描述符必须是一个调用门,通过调用门计算出需要执行的代码地址
- 堆栈变化:
| CALL执行后:(此处假设是从3环跳到0环) 此时,现在的堆栈已经不是原来3环的堆栈了,现在是0环的堆栈 ①将调用者的SS压入0环的栈 ②将调用者的ESP压入0环的栈 ③将调用者的CS压入0环的栈 ④将返回值地址压入0环的栈 堆栈发生了切换,SS发生了切换,CS发生了切换 RETF执行后 ①返回地址出栈 ②调用者CS出栈 ③调用者ESP出栈 ④调用者SS出栈 发生改变的寄存器: CS CS改变。由CALL后面跟的段选择子决定变成什么 EIP 进入到0环了,需要执行的EIP不是之前的EIP了,是0环的EIP。由CALL后面跟的段选择子指向的段描述符的Base+offset决定 SS CS改变,SS一定也要改变。由TSS决定变成什么 ESP 不是同一个堆栈了,ESP会改变。由TSS决定变成什么 | 调用 |
返回 |
6.3 长调用总结
- 跨段调用时,一旦有权限的切换,就会有堆栈的切换
- CS的权限一旦发生改变,SS的权限也要随着改变,CS与SS的等级必须一致 (这就是为什么要把CS压入栈的原因,因为CS换了,CS换了SS也跟着换了)
- JMP FAR无法跳转到不同环的非一致代码段,CALL FAR可以通过调用门提权,提升CPL的权限
7. 调用门提权
7.1 调用门
S位为0,Type为1100。真正调转的地址是:段选择子指向的段描述符的Base+跳转地址
- 调用门的DPL应该是3,否则我们的程序将没有权限访问调用门。而调用门中的段选择子则可以的权限则可以是0,也可以是3,是0的时候,就是提权了,是3则是跨段不提权。堆栈图如6.1和6.2
CALL CS:EIP(EIP是废弃的)
执行步骤:
- 根据CS的值,查找GDT表,找到对应的段描述符,这是一个调用门
- 在调用门描述符中存储了另外一个代码段的段选择子,这个段选择子应该要指向代码段。CS最终的值就是这个段选择子
- 选择指向的段,段.Base+偏移地址(门描述符的高四字节的高16位和低四字节的低16位) 就是真正要执行的地址
7.3 实验:构造调用门
实现跨段并提权,指令:CALL FAR CS:EIP
准备:在虚拟机中运行代码,在物理机的windbg中修改虚拟机的GDT表,调试虚拟机的内核
7.3.1 无参数
7.3.1.1 验证堆栈图试验
- 构造一个调用门段描述符
1.高4字节,跳转地址的高16位 - 0000,跳转地址暂时不知道,写0。也可以直接在mian函数中写一段代码,获取了代码地址再写入进来
- 1+11+0+1100=EC
- P:当前描述符有效,填1
- DPL:填11,程序是从3环跳转到0环的,当前的CPL是11,没有权限访问00的段描述符
- 0:默认是0
- 1100:Type字段,1100表示当前描述符是调用门
- 0000+EC
- 00
- 第一个0是5-7位,默认是0;第二个0,代表不传参数
- 0000+EC+00
- 是一个段选择子,它指向了我们真正要执行的代码段
- 不提权:001B(段选择子后两位11,指向GDT表第[3]个段描述符,该段描述符的第五位为9或F;3环代码段,DPL为11)
- 提权:0008(段选择子后两位00,指向GDT表第[1]个段描述符,该段描述符的第五位为9或F;0环代码段,DPL为00)
- 0000+EC+00+0008 此时段选择子的权限是0环的
- 0000,偏移,目前不知道,填0。也可以直接在mian函数中写一段代码,获取了代码地址再写入进来
- 得到调用门段描述符:0000EC00 00080000
- 替换GDT表的段描述符
打开windbg,进入GDT表,将第10个段描述符换成第1步获取得到的段描述符(第10个是windows没有用到的段描述符,替换掉不会蓝屏)
1.r gdtr 得到GDT表地址8003f000
2.dq 8003f000 得到第10个段描述符的地址:
3.eq 8003f048 0000EC00 `00080000 将段描述符修改成第1步得到的段描述符- 此时,本机的GDT表已经增加了一个段描述符,第10个就是增加的
- 此时,替换了之后,一旦执行CALL FAR CS:EIP起来,就会执行CS指向的偏移,也就是上面的1和5两段偏移相加的地址。不管这个地址是在高2GB还是在低2GB,只要执行起来,它就是0环的权限。如果这段代码有int3断点,又是双击调式环境下,开着windng,那么就会断在windbg的0环中。
- 编写代码测试
//长调用的格式:CALL FAR CS:EIP(EIP是废弃的) //1.编写需要调用的函数 void __declspec(naked) GetRegsiter(){ _asm { int 3 //int 3是中断的意思 retf //注意是长调用,需要使用长返回 } } //__declspec(naked)是告诉编译器,函数代码的汇编语言是为自己所写的,不需要编译器添加任何代码,所以结尾处一定要字节写返回 //2.编写main函数以及格式 int main(){ char buff[6]; *(dword*)buff[0]=0x12345678; *(word*)buff[4]=0x48;//0x48是通过在GDT表添加的段描述符的位置计算出来的 _asm{ call fword ptr[buff] } getchar(); return 0; }- 修复GDT表新增的段描述符的地址
- 此时已经有了需要跳转的函数的地址,下断点,看反汇编,得到函数GerRegsiter函数的地址,并将其拆分之后重新赋值给GDT表的第10个段描述符
- 执行代码,验证堆栈
- 在调用函数前下个断点,查看CS,SS,ESP,EIP,用于与call之后之后做对比,验证跨段并提权的堆栈图
- 会在int 3的时候断在windbg,先输入g,让程序继续执行,查看CS SS EIP ESP并记录
- 重新执行代码,断在windbg后,查看CS,SS,EIP,ESP与之前是否一致,验证堆栈图
- 此时,可以查看当前0环的堆栈,在windbg点击查看寄存器,查看esp,然后跳转到esp的地址,查看堆栈
- 此时的堆栈变化与跨段提权并调用的堆栈相符,说明提成功
7.3.1.2 获取高2GB的内核内存
- 1.构造调用门段描述符
1.高4字节,跳转地址得高16位 - 0000,跳转地址暂时不知道,写0
- 1+11+0+1100=EC
- P:当前描述符有效
- DPL:填11,程序是从3环跳转到0环的,当前的CPL是11,没有权限访问00的段描述符
- 0:默认是0
- 1100:Type字段,1100表示当前描述符是调用门
- 0000+EC
- 00
- 第一个0是5-7位,默认是0
- 第二个0,代表不传参数
- 0000+EC+00
- 是一个段选择子,它指向了我们真正要执行的代码段
- 不提权:001B(段选择子后两位11,指向GDT表第[3]个段描述符,该段描述符的第五位为9或F;3环代码段,DPL为11)
- 提权:0008(段选择子后两位00,指向GDT表第[1]个段描述符,该段描述符的第五位为9或F;0环代码段,DPL为00)
- 0000+EC+00+0008 此时段选择子的权限是0环的
- 0000,偏移,目前不知道,填0
- 0000+EC+00+0008+0000
- 2.替换GDT表的段描述符
打开windbg,进入GDT表,将第10个段描述符换成第1步获取得到的段描述符(第10个是windows没有用到的段描述符,替换掉不会蓝屏)
1.r gdtr 得到GDT表地址8003f000
2.dq 8003f000 得到第10个段描述符的地址:
3.eq 8003f048 0000EC00 `00080000 将段描述符修改成第1步得到的段描述符
此时,本机的GDT表已经增加了一个段描述符,第10个就是增加的- 3.编写代码测试
//长调用的格式: CALL FAR CS:EIP(EIP是废弃的) //1.编写需要调用的函数 DWORD dwH2GBValue; BYTE GDT[6]; void __declspec(naked) GetRegsiter() { _asm { pushad pushfd mov eax,0x8003f00c //读取高2GB内存 mov ebx,[eax] mov dwH2GBValue,ebx //sgdt GDT //将gtdr寄存器的内容放到6字节的GDT数组中,这个指令也可以在3环执行 popfd popad retf } } //2.编写main函数以及格式 int main(){ char buff[6]; *(dword*)&buff[0]=0x12345678; *(word*)&buff[4]=0x48;//0x48是通过在GDT表添加的段描述符的位置计算出来的 _asm{ call fword ptr[buff] //跨段调用并提权 } printf("%x",dwH2GBValue); getchar(); return 0; }- 4.修复GDT表新增的段描述符的地址
- 此时已经有了需要跳转的函数的地址,下断点,看反汇编,得到函数GetRegsiter函数的地址,并将其拆分之后重新赋值给GDT表的第10个段描述符
- 5.执行代码,验证读取高2G内存
7.3.2 有参数
- 1.构造有参的调用门描述符
- 0000+EC+03++0008+0000 (03-->传递3个参数)
- 2.修改GDT表
- 将新构造的调用门描述符添加到GDT表中的第10个描述符
- eq 8003f048 0000EC03`00080000
- 3.编写代码
//长调用的格式: CALL FAR CS:EIP(EIP是废弃的) //1.编写需要调用的函数 DWORD x,y,z; void __declspec(naked) GetRegsiter(){ _asm { pushad pushfd mov eax,[esp+0x24+0x8+0x8] //pushad:esp+0x20; //pushfd:esp+0x4 //长调用call,esp+8 mov dword ptr ds:[x],eax mov eax,[esp+0x28+0x8+0x4] mov dword ptr ds:[y],eax mov eax,[esp+0x28+0x8+0] mov dword ptr ds:[z],eax popfd popad retf 0xc //平衡堆栈,否则会蓝屏 } } //2.编写main函数以及格式 int main(){ char buff[6]; *(dword*)&buff[0]=0x12345678; *(word*)&buff[4]=0x48;//0x48是通过在GDT表添加的段描述符的位置计算出来的 _asm{ push 1 push 2 push 3 call fword ptr[buff] } printf("%x %x %x",x,y,z); getchar(); return 0; }- 4.获取跳转地址,修复GDT表
- 下断点获取函数地址,拆分后
- 高位放GDT表中门描述符的高4字节的高16位
- 低位放GDT表中门描述符的低4字节的低16位
- 5.执行函数,验证输出
7.4 总结
- 0.调用门使用步骤
- 构造门描述符,门描述符中的段选择子是一个代码段,这个代码段是0环还是3环可以由我们自己决定(填写后两位)
- 门描述符中的代码段的Base+偏移是我们真正要跳转的地址(地址就是函数地址,代码段提权的话,可以做0环操作)
- 将门描述符写到GDT表
- CALL FAR的时候,通过CALL 段选择子,在GDT表中找到门描述符,通过门描述符完成调用函数
- 1.调用门跨段不提权时,0环堆栈只会PUSH两个参数,CS和返回地址,原CS在PUSH后会被替换。新的CS是调用门的段选择子,此时CPL就会变成调用门提供的代码段的CPL,从而完成CPL提权
- 2.调用门跨段并提权时,0环堆栈会PUSH四个参数,CS,ESP,SS,返回地址,新的CS由调用门提供,SS和ESP由TSS提供
- 3.通过调用门时,调用的代码是新的CS.Base+偏移,由调用门决定。但是RETF返回的时候,出栈的值可以由我们自己写,也就是说进去得到时候需要通过调用门,但是返回的时候,返回到哪里可以由我们决定,只需要修改堆栈的返回地址
- 4.我们也可以选择进入调用门之后,不返回,自己再使用一个call调用门来返回
- 5.有参使用调用门的时候,CALL前PUSH参数,调用完成之后平衡堆栈:retf 0xN
8. IDT表
IDT表的构成
- 任务门描述符
- 中断门描述符
- 陷阱门描述符
中断门的五六位通常是ee或者8e,任务门通常是85,陷阱门通常是8f都是随着Type位的改变而改变
9. 中断门
- 1.IDT表中存储的都是系统段描述符
- 2.IDT表第一个元素不是NULL
- 3.windbg查看IDT表:r idtr
- 4.查看idt表有多大:r idtl
9.1 中断门
9.1.1 中断门段描述符

- 高四字节
- 高16位、低四字节的低16位与调用门一致,是代码段的偏移地址,代码段取决于低四字节16~31的段选择子
- 0~7位,调用门可以传递参数,但中断门不可以传参数,为0
- Type(8~11位)为1110,12位为0,时,此时段描述符为中断门
- 中断门会把 IF 位置 0:中断门执行的时候,CPU会将标志寄存器IF位清零,进入中断门,如果还有可屏蔽中断信号到来,CUP将不理睬。
- 中断门堆栈图
- 提权
- 和调用门稍稍有点不一样的地方是,中断门提权会在堆栈中多压入一个值——EFLAGS.
- 不提权
![]()
- 如果不提权,意味着不会切换栈,所以也没有必要在栈中压入栈段选择子和栈顶指针
- 调用门返回时RETF,而中断门返回是使用IRET/IRETD
9.1.2 使用中断门
- 格式:INT 3表示:IDT表第[3]个中段描述符
- 1.构造中断门段描述符
1.高4字节,跳转地址的高16位 - 0000,跳转地址暂时不知道,写0
- 1+11+0+1110=EE
- P:当前描述符有效
- DPL:填11,程序是从3环跳转到0环的,当前的CPL是11,没有权限访问00的段描述符
- 0:S位,系统段描述符为0
- 1110:Type字段,1110表示当前描述符是中断门
- 0000+EE
- 00
- 中断门不允许传参,所以填00
- 0000+EE+00
- 是一个段选择子,它的Base加上中断门的偏移字段,指向了我们真正要执行的代码段
- 不提权:001B(段选择子后两位11,指向GDT表第[3]个段描述符,该段描述符的第五位为9或F;3环代码段,DPL为11)
- 提权:0008(段选择子后两位00,指向GDT表第[1]个段描述符,该段描述符的第五位为9或F;0环代码段,DPL为00)
- 0000+EE+00+0008 此时段选择子的权限是0环的
- 0000,偏移,目前不知道,填0
- 0000+EC+00+0008+0000
- 2.替换IDT中的中断门描述符
查IDT表发现,第0x20个中断描述符没有使用,
此处试验将第0x20也就是第[32]个中断描述符替换掉
r idtr得到8003f400
eq 8003f500 0041EE00 000813A0- 3.编写代码测试
//长调用的格式:CALL FAR CS:EIP(EIP是废弃的) //1.编写需要调用的函数 DWORD dwH2GBValue; BYTE GDT[6]; void __declspec(naked) GetRegsiter(){ _asm { pushad pushfd mov eax,0x8003f00c //读取高2GB内存 mov ebx,[eax] mov dwH2GBValue,ebx //sgdt GDT //将gtdr寄存器的内容放到6字节的GDT数组中,这个指令也可以在3环执行 popfd popad iret } } //2.编写main函数以及格式 int main(){ _asm { int 0x20 // 选择IDT表中的第[0x20]个中段描述符 } printf("%x",dwH2GBValue); getchar(); return 0; }- 4.修正IDT表的中段描述符,编译代码,查看函数地址
- 5.执行程序,查看返回值
9.1.3 总结
- 使用步骤
- 1.构造中断门段描述符
- 2.替换IDT中的中断门描述符
- 3.编写代码测试
- 4.修正IDT表的中段描述符,编译代码,查看函数地址
- 5.执行程序,查看返回值
- 中断门使用:
- 格式:INT N
- 通过IDT表找到第[N]个门描述符。(通过里面的段选择子可查GDT表)
- 通过门描述符的Base+偏移调用指定的地址
- 中断门执行的时候,CPU会将标志寄存器IF位置零,进入中断门,IF位为0时,如果还有可屏蔽中断信号到来,CUP将不理睬。
- 拓展:一旦有不可屏蔽中断信号,不管IF是什么,CUP都会马上执行
- 例如:电源断开,CUP会使用电容的电量,马上做一些保存操作
10. 陷阱门
10.1 陷阱门段描述符

- 高四字节
- 高16位,低四字节的低16位与调用门一致,是代码段的偏移地址,代码段取决于低四字节16~31的段选择子
- 0~7位,调用门可以传递参数,中断门不可以传参数,为0
- 8~11位为1111,12位为0,时,此时段描述符为陷阱门
- 低四字节
- 与之前的段描述符一致
- 陷阱门与中断门唯一的区别
- 中断门执行的时候,会将标志寄存器IF位清零,但是陷阱门不会
- Type为1111,中断门是1110
11. 任务段
11.1 任务门的作用
- 权限切换-->CS更换
- CS更换-->SS更换
- SS更换-->堆栈更换
- 堆栈更换-->ESP更换
- CS是我们指定的段选择子
- SS和ESP就是从TSS(任务状态段)中提供的
11.2 TSS结构
![]()
- TSS是一块内存,大小是104字节,所有寄存器都存在这里
- TSS可以一次性的替换这一堆寄存器
11.2.1 CUP寻找TSS的方法
- 1.TSS内存:通过TR(TaskRegister)段寄存器得到,TR.Base指向了TSS在哪里,TR.Limit表明TSS的大小
- 2.TR段寄存器:操作系统启动的时候,通过GDT表中加载TSS段描述符得到的
11.2.2 TSS段描述符
- (TSS段描述符在GDT表中)

- TSS段描述符的Type:
- ①1001==9:此段描述符没有加载到TR段寄存器中
- ②1011==B:此段描述符已经加载到TR段寄存器中
- TSS段的Limit是104字节,高位Limit为0即段描述符G位为0
11.2.3 TR寄存器读写
- 写
- LTR r/m16
- 在GDT表中找到与r/m16对应的段描述符,然后将段描述符加载到TR寄存器中
- 加载完成后,被加载的TSS段描述符状态位发生改变(9变成B)
- LTR是特权指令,只能在0环执行,它只会修改TR段寄存器,不会修改TSS的104字节
- 读
- STR r/m16
- 将TR段寄存器的段选择子存储到r/m16
11.2.4 实验:修改当前环境所有寄存器
(替换一堆寄存器)
步骤:
- 1.编写代码,并准备一份104字节的缓冲区
- 2.构造TSS段描述符,Base指向缓冲区,Limit为104字节
- 3.修改GDT表,增加TSS段描述符
- 4.修改TR寄存器以及当前寄存器
- 5.执行代码,windbg下断点,输入!process 0 0 指令获取,选择DirBase字段
实现:
- 1.编写代码,并准备一份104字节的缓冲区
DWORD dwOK; DWORD dwESP; DWORD dwCS; char du[0x10]; void __declspec(naked)func(){ dwOK=1; __asm{ mov eax,esp mov dwESP,eax mov ax,cs mov dwprd ptr ds:[dwCS],cs //跳回去的代码 // // } } //eq 8003f0c0 0000e912`fdcc0068//构造段描述符 int main(){ int iCr3; printf("输入进程ID\n");//通过windbg!process 0 0 指令获取,选择DirBase字段 scanf("%x",&iCr3); DWORD szTSS[0x68]={ 0x00000000, //link //切换前,存储是的原来的段选择子,切换的时候CUP会自动填充 0x00000000,//esp0 //(DWORD)bu 0x00000000,//ss0 //0x00000000 0x00000000,//esp1 0x00000000,//ss1 0x00000000,//esp2 0x00000000,//ss2 (DWORD )iCr3,//cr3,是一个寄存器,后面会讲 0x00401020,//eip//要跳转的地址 0x00000000,//eflags 0x00000000,//eax 0x00000000,//ecx 0x00000000,//edx 0x00000000,//ebx (DWORD)bu,//esp,一个地址,指定一个切换之后的堆栈(这里是0x10个字节的堆栈) 0x00000000,//ebp 0x00000000,//esi 0x00000000,//edi 0x00000023,//es 0x00000008,//cs 去什么环就写什么权限的代码段,3环的话写0x1B 0x00000010,//ss SS必须与CS在同一个环,3环的话写0x23 0x00000023,//ds 0x00000030,//fs 切换到0环写0x30,切换到3环写0x0000003B 0x00000000,//gs 0x00000000,//ldt 0x20ac0000//可以从操作系统直接复制出来 }; char buff[6]; *(DWORD*)&buff[0]=0x12345678; *(WORD*)&buff[4]=0xC0; __asm{ call fword ptr[buff] } printf("ok=%d ESP = %x CS=%x \n",dwOK,dwESP,dwCS); }- 2.构造TSS段描述符,Base指向缓冲区,Limit为104字节
- 按照下图构造,但是G位与之前构造描述符不一样,由TSS段描述符得到的TR寄存器指向的段是TSS段,TSS段的长度是以字节为单位的,之前的都是1(4KB),现在应该改成0(字节)
- 下断点,获取iTSS数组和跳转函数的地址
- 构造完成后,将构造好的TSS段描述符写入到空白的GDT表中
TSS段描述符的Type:
①1001==9:此段描述符没有加载到TR段寄存器中
②1011==B:此段描述符已经加载到TR段寄存器中
//TSS数组地址:0012 f184 TSS段的Base就是TSS数组的地址
TSS数组的大小只有104字节,使用低四字节的低16位就足够了,Limit16~19可以设置为0了
高四字节:
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 1 0 0 1 0 0 0 1 0 0 1 0
0 0 0 0 e 9 1 2
低四字节:
f184 0068
最终得到TSS段描述符:
0000e912`f1840068
- 3.修改GDT表,增加TSS段描述符
- 进入GDT表,随便找到一个空白的段描述符此处是第10个
- 填充:
r gdtr
dq 8003f000
eq 8003f0b0 0000e912`f1840068- 此处填充的是第10个,01001 0 11
- 得到段选择子是0x4B,此时4B指向的段描述符是TSS段描述符
- 4.修改TR寄存器以及当前寄存器
- 在0环修改TR寄存器LTR r/m16
- 只能在0换用
- 只会修改TR寄存器的值
- 在3环修改TR寄存器:JMP FAR或CALL FAR
- 用 JMP 去访问一个代码段的时候
- JMP 0x48:0X12345678 如果0x48是代码段
- 执行后:CS-->0x48 EIP -->0X12345678
- 用JMP去访问一个任务段的时候:
- 如果0x48是TSS段描述符,则先取出TSS段描述符,修改TR寄存器,然后用TR.Base指向的TSS中的值修改当前的一堆寄存器
- 5.执行代码,windbg下断点,输入!process 0 0 指令获取,选择DirBase字段
11.2.5 总结
- 替换当前寄存器步骤:
- ①构造TSS:创建104字节缓冲区,并填充里面的内容(寄存器), DirBase需要!process 0 0获取
- ②构造TSS段描述符(Base,Limit)指向①的缓冲区
- ③在GDT表中增加②的TSS段描述符
- ④加载TR寄存器并修改当前寄存器
- JMP/CALL FAR指令后面跟的若是任务段的选择子,系统会通过选择
- 子将段选择子指向的TSS段描述符加载到TR寄存器中
- 然后通过TR寄存器的Base找到TSS,将TSS的内容(寄存器)替换当前的寄存器
- TSS段描述符与TR段寄存器与TSS段的关系
- TSS段在GDT表中
- TR寄存器通过TSS段描述符加载
- TSS段的地址与大小是通过TR寄存器得到
- 注意JMP和CALL的堆栈变化
12. 通过任务门访问任务段
12.1 任务门描述符
- 灰色部分是保留部分,填0就可以
- Type为0101的时候,是任务门
- 低四字节的高16位:存储的是一个TSS段的段选择子,用于在GDT表中寻找TSS段描述符
12.2 任务门执行流程
- 1. INT N
- 2. 查IDT表,找到任务门描述符
- 3. 通过任务门描述符的TSS段选择子,查找GDT表,找到任务段描述符
- 4. 将任务段描述符加载到TR寄存器中,然后使用TR寄存器Base指向的TSS中的值修改寄存器
- 5. IRETD返回
12.3 实验
- 1.编写代码,并准备一份104字节的缓冲区
- 2.构造TSS段描述符
- 3.修改GDT表
- 4.构造任务门描述符,写入到IDT表中
- 5.执行代码,windbg下断点,输入!process 0 0 指令获取,选择DirBase字段
- -------------------------------------------------代码---------------------------------------------------------------
- 1.编写代码,并准备一份104字节的缓冲区
DWORD dwOK;
DWORD dwESP;
DWORD dwCS;
void __declspec(naked)func(){
dwOK=1;
__asm{
mov eax,esp
mov dwESP,eax
mov ax,cs
mov dwprd ptr ds:[dwCS],cs
//跳回去的代码
//
//
}
}
int main(){
int iCr3;
printf("输入进程ID\n");//通过windbg!process 0 0 指令获取,选择DirBase字段
scanf("%x",&iCr3);
DWORD szTSS[0x68]={
0x00000000, //link //切换前,存储是的原来的段选择子,切换的时候CUP会自动填充
0x00000000,//esp0 //(DWORD)bu
0x00000000,//ss0 //0x00000000
0x00000000,//esp1
0x00000000,//ss1
0x00000000,//esp2
0x00000000,//ss2
(DWORD )iCr3,//cr3,是一个寄存器,后面会讲
0x00401020,//eip//要跳转的地址
0x00000000,//eflags
0x00000000,//eax
0x00000000,//ecx
0x00000000,//edx
0x00000000,//ebx
(DWORD)bu,//esp,指定一个切换之后的堆栈
0x00000000,//ebp
0x00000000,//esi
0x00000000,//edi
0x00000023,//es
0x00000008,//cs 0x0000001B,需要与SS在同一个环,cs08ss就10,cs1B,ss就23
0x00000010,//ss 0x00000023
0x00000023,//ds
0x00000030,//fs 切换到0环写0x30,切换到3环写0x0000003B
0x00000000,//gs
0x00000000,//ldt
0x20ac0000//可以从操作系统直接复制出来
};
__asm{
int 0x20
}
printf("ok=%d ESP = %x CS=%x \n",dwOK,dwESP,dwCS);
}- 2.构造TSS段描述符
- 按照下图构造,但是G位与之前构造描述符不一样,由TSS段描述符得到的TR寄存器指向的段是TSS段,TSS段的长度是以字节为单位的,之前的都是1(4KB),现在应该改成0(字节),下断点,获取iTSS数组和跳转函数的地址,构造完成后,将构造好的TSS段描述符写入到空白的GDT表中
![]()
- TSS段描述符的Type:
- ①1001==9:此段描述符没有加载到TR段寄存器中
- ②1011==B:此段描述符已经加载到TR段寄存器中
- //TSS数组地址:0012 f184 TSS段的Base就是TSS数组的地址,通过运行代码下断点得到
- TSS数组的大小只有104字节,使用低四字节的低16位就足够了,Limit16~19可以设置为0了
- 高四字节:
- 0000 e912
- 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
- 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 1 0 0 1 0 0 0 1 0 0 1 0
- 0 0 0 0 e 9 1 2
- 低四字节:
- f184 0068
- 最终得到TSS段描述符:
- 0000e912`f1840068
- 3.修改GDT表
- 进入GDT表,随便找到一个空白的段描述符
- 填充:
- r gdtr
- dq 8003f000
- eq 8003f0b0 0000e912`f1840068
- 此处填充的是第10个,01001 0 11
- 得到段选择子是0x4B,此时4B指向的段描述符是TSS段描述符
- 4.构造任务门描述符,写入到IDT表中
- 5.执行代码,windbg下断点,输入!process 0 0 指令获取,选择DirBase字段
12.4 总结
- 任务门跨表了需要在IDT表和GDT表添加描述符
- 执行流程:
- 1. INT N
- 2. 查IDT表,找到任务门描述符
- 3. 通过任务门描述符的TSS段选择子,查找GDT表,找到任务段描述符
- 4. 将任务段描述符加载到TR寄存器中,然后使用TR寄存器Base指向的TSS中的值修改寄存器
- 5. IRETD返回
- 实验步骤:
- 1.构造TSS段描述符,TSS段描述符的Base指向TSS缓冲区,Limit为104字节
- 2.将TSS段描述符写入GDT表
- 3.构建任务门描述符,任务门描述符的段选择子指向GDT表的TSS段描述符
- 4.将任务门描述符写入IDT表
- 5.使用任务门 INT N
windbg常用指令:
r:查看寄存器
dd:查看地址内容,以dword字节分组显示
dq:查看地址内容,以8字节分组显示
②--------------------------------------保护模式-页的机制 ------------------------------------
1. 10-10-12分页
1.1 为什么是10-10-12?
- 将线性地址分成10-10-12
- 2的10次方=1024 PDT有1024个PTT
- 2的10次方=1024 PTT有1024个物理页
- 2的12次方=4096 物理页有4096个字节
1.2 PDT、PDE、PTT、PTE
1.2.1 4GB内存
- 给应用程序分配的4GB内存时虚拟的,让真正需要用到内存的时候,会通过地址转换找到物理地址
1.2.2 物理地址
- 线性地址 有效地址 物理地址
- mov eax,dword ptr ds:[0x12345678]
- 有效地址:0x12345678
- 线性地址:ds.Base+0x12345678
- 通常情况下,ds这个段的Base都是0
- cup会将线性地址转换成物理地址,再从物理地址中取出数据
1.2.3 设置cup分页模式
- 10-10-12分页:C盘booit.ini中,启动项的最后一项为execute=optin时,是101012的方式
- 2-9-9-12分页:C盘booit.ini中,启动项的最后一项为noexecute=optin时,是29912的方式
1.2.4 PDT与PTT表
- 每个进程都有一个CR3寄存器,指向当前进程对应的PDT,Cr3是唯一一个存储物理地址的寄存器,而程序是无法直接使用物理地址的
- 每个进程中的数据都是通过Cr3找到物理页,然后再通过物理页进行读写的
- 通过Cr3寄存器可以实现读取目标进程的数据、写入数据到目标进程,只要将目标进程的Cr3暂时修改为本进程的Cr3即可,这样可以更加高级的完成注入(将代码写入到目标程序,比注入更高级,更加难以检测)
- 通过!PROCESS 0 0找到对应进程的Cr3(DirBase字段)
- windbg中,!dd可以读取CR3物理地址地址的内容,指向一个4096大小的页目录表(PDT)
- PDT:页目录表,也叫PDT,4096大小。PDT表中成员叫页目录项(PDE),是地址,占4字节。
- PTT:页表,也叫PTT,4096大小。PTT表中成员叫PTE,是指向物理页的地址,占4字节。
- PTE可以指向物理页,但是没有给PTE分配物理页的时候PTE也可以不指向物理页
- 多个PTE可以指向同一个物理页
- 一个PTE只能指向一个物理页
- 系统通过PTE找到真实的物理地址,通过线性地址找到PTE。线性地址是程序内的线性地址,不同程序可能会出现相同数值的线性地址,但是指向的物理地址却很可能不相同。因此每一个程序都有自己独一无二的一套PTT页表,与线性地址一一对应。
1.2.5 实验:线性地址找物理地址
- 1.在虚拟机设置为101012分页,重启
- 2.在虚拟机中启动一个记事本,写上Hello World,记事本拥有自己的4GB空间,helloworld一定在这片里面
- 3.找到HelloWorld的线性地址
- 通过Cheat Engine工具附加记事本
- 勾选Unicode,搜索HelloWorld
- 修改HelloWorld,找到线性地址AA8A0
- 4.按照101012分页的方式,找到物理地址
- 实验工具:windbg,CE
- 按照10-10-12的形式,将32位的线性地址分成三份
- 000AA8A0
- 0000000000 0010101010 8A0
- 0 AA 8A0
- 每个进程都有一个CR3寄存器,Cr3是唯一一个存储物理地址的寄存器 通过!process 0 0找到对应进程的Cr3(DirBase字段),CR3指向一个4096大小的页目录表(PDT),假设找到的PDT为146a0000
- 在PDT中,根据线性地址的第一个10,找的PDE是:PDT的地址+(10*4),PDE指向一个4069大小的页表(PTT)
- !dd 146a0000+0*4 得到PTT的地址14dc0067【PTT的后3位代表属性,用的时候需要改成000】
- 在PTT中,根据线性地址的第二个10,找的地址是:PTT的地址+(10*4),这个地址指向真正的物理页
- !dd 14dc0000+AA*4 得到真正物理页的地址13fdd067【地址的后3位代表属性,用的时候需要改成000】
- 在真正物理页中,根据线性地址的12,找的地址是:物理页的地址+(10),这个地址就是物理地址
- !dd 13fdd000+8A0 得到真正要找的物理地址,物理地址里面存的就是要找的数据
- !db 13fdd000+8A0 以字节方式查看,可以看得更清晰
- 小结
- !dd,加了一个!,意思是查看物理地址
- PDT:页目录表,也叫PDT,4096大小。PDT表中成员叫页目录项(PDE),是地址,占4字节。
- PTT:页表,也叫PTT,4096大小。PTT表中成员叫PTE,是指向物理页的地址,占4字节。
- PTE可以指向物理页,但是没有给PTE分配物理页的时候PTE也可以不指向物理页
- 多个PTE可以指向同一个物理页
- 一个PTE只能指向一个物理页
- 10-10-12
- 10:按下标查找个数,第一张表存储2的10次方=1024个地址
- 10:按下标查找个数,第二张表存储2的10次方=1024个地址
- 12:按字节查找数据,第三张表存储2的12次方=4096个字节
- 寻找物理地址步骤
- 1.获取PDT Cr3(PDT)
- 2.获取PDE(是一个PTT页) Cr3+10*4
- 3.获取PTE(是一个物理页) PTT+10*4
- 4.获取物理地址 物理页+12
1.2.6 实验:对0地址进行读写
- 正常程序的0地址都是不可写入和读取的,是因为0的线性地址为00000000
- 通过PDT+0找到PDE
- 通过PDE+0找到PTT
- 通过PTT+0找到PTE---->为0,说明0的线性地址所对应的物理页地址是空的
- 通过PTE找到内存页为空
- 会发现,线性地址000000是没有分配物理页的,既然没有分配物理页,何来读写呢?,但是我们可以手动给0这个线性地址分配一个物理页,达到可以对0线性地址读写
- 实验
- 1.定义一个变量,获取变量的线性地址,并打印出控制台,然后getchar()等待
- 2.打开windbg,通过1.2.5找到该变量线性地址的PTE对应的物理地址,并记录下来(PED和PTE的后三位是属性,最好也记录下来,若原属性是不能读写的属性,那么修改成可读写属性)
- 3.找到0的物理地址,将0线性地址的PTE修改为第2步获取的线性地址,写入操作指令:
- 指令格式:!ed PTE地址 第2步得到的PTE
- 此时,0的线性地址已经分配了一个物理页,和第2步的变量共用一个物理页。
- 4. 随意输入字符,令getchar()不再等待
- 对0线性地址进行读写操作
- *(int*)0=123;
- int n = *(int*)0;
- 原理:线性地址0:页目录表->页目录->页表->物理地址没有分配 将线性地址设置为可读写:那我们把物理地址分配上去即可
- 代码
#include <iostream>
#include <windows.h>
int main() {
int n = 100;
printf("写入数据前n的值 = %d\n", n);
printf("n的地址 = %p\n", &n);
int n0 = 0;
getchar();
//windbg修改物理页
*(int*)n0 = 2000;
printf("向0地址写入数据:%d\n写入数据后n的值 = %d\n",2000, n);
system("pause");
return 0;
}1.2.7 PDE、PTE属性
PDE和PTE的低3位也就是二进制的低12位是属性,0~11是属性位,12~31是页表基址,指向物理页的基址
- P位:代表是否有效
- R/W位:0只读,1可写
- US位: ①U/S=0,本物理页只允许特权用户访问 ②U/S=1,本物理页普通用户也可以访问 【之前学的各种门需要00权限才能读写就是因为这个原因】,修改变量的U/S位,也可以达到让3环访问0环的目的(获取一个需要读取的0环的线性地址如0x8003f00c,更改其属性)
- PS位:PS位只对PDE有意义,PS是PageSize的意思。当PS=1时,PDE直接指向物理页,而不是PTT页,没有PTE,此时10-10-12变成10-22,22位直接指向页内偏移,即 通过Cr3找到PDT页,PDT+(第一个10位*4)就是PDE,值就是物理页,PDE+22位的数值=物理地址。范围是4M,俗称大页。
- A位:当前页是否被访问过,即是是访问1字节也会导致A位置为1。未访问过则为0
- D位:当前页是否被写过,0没有被写过,1被写过
- G位:G位为1时,TLB表不会刷新G为为1的PDE或PTT (G为为1的都是高2GB地址,所有程序可以共享) 详情参考TLB一章
- PWT位:(Page Wrrte Through) PWT=1时,当将当前页的数据写入到CPU缓存的时候,要求同时将数据也写入到内存中,PWT为0则没有这个要求。
- PCD位:(Page Cache Disable) PCD=1时,当前页的数据禁止写入到缓存中,CPU需要读写的话,只能直接写入到内存中
- 例如,TLB中的页,这些页已经存在TLB缓存中了,可能不需要在写入到CPU缓存了。所以他们的PCD为1。
- PAT位:
- PWT、PCD位前置知识-->CPU缓存
- 1.CPU缓存是位于CPU与物理内存之间的临时存储器,它的容量比内存小得多,但是交换速度比内存快的多
- 2.CPU缓存可以做得很大,几K,几十K,上M的都有
- 3.CPU缓存与TLB的区别
- TLB: 线性地址 <----> 物理地址
- CPU缓存:物理地址 <----> 内容
- 系统读取某个物理页的时候:
- ①查找TLB得到物理地址
- ②查找CPU缓存得到数据
- CPU缓存越大,速度越快
- 具体细节参考Inter白皮书章节:Memory Cache Control
1.2.8 实验:实现修改只读变量
- 物理页的属性=PDE属性&PTE属性
- 定义一个只读类型的变量,在另一个线性地址指向与该变量相同的物理页,但是这个线性地址的PDE/PTE的R/W属性改成1可写属性,实现可写
- 1.定义只读变量
- 2.获取只读变量的线性地址
- 3.通过线性地址获取PDE和PTE
- 4.将PDE和PTE的R/W位属性改成1可写属性
- 5.对只读变量进行赋值操作,完成对只读变量的读写
int main(){ char* str ="hello world"; printf("线性地址:%x",(DWORD)str); //windbg修改PDE和PTE属性 getchar(); DWORD dwVal=(DWORD)str; *(char*)dwVal="M"; printf("修改后的值:%s\n",str); return 0; }
1.2.9 实验:实现访问高2GB内存
- 物理页的属性=PDE属性&PTE属性
- 确定一个高2GB的线性地址,如0x8003f00c。查看8003f00c的PDE和PTE属性,并将U/S修改为1,实现普通用户也可访问
- 1.通过线性地址获取PDE和PTE
- 2.将PDE和PTE的U/S位属性改成1普通用户可访问
- 3.对线性地址进行读取操作,完成对高2GB数据的读取
int main(){ //windbg修改PDE和PTE属性 getchar(); int Vule=0x8003f00c; printf("0x8003f00c的数据:%s\n",*(DWORD*)Vule); return 0; }- 思考
- 如果系统要保证某个线性地址是有效的,那么必须为其填充正确的PDE与PTE,如果我们想填充PDE与PTE那么必须能够访问PDT与PTT,那么存在2个问题:
- 1、一定已经有“人"为我们访问PDT与PTT挂好了PDE与PTE的物理页,我们只需要找到这个线性地址就可以了。但是这个地址在哪里?按照以往的知识,我们是通过访问Cr3寄存器得到PDT表的。但是程序并不能使用物理地址,也就是说Cr3寄存器对程序来说并不能使用,那怎么才能访问PDT和PTT表呢?除非有一个线性地址可以直接访问PDT和PTT【-->页目录表基址C0300000,C0300000存储的值就是PDT】
- 2、这个为我们挂好PDE与PTE的“人”是谁?
1.2.9 页目录表(PDT)基址
- 拆分C0300000
- 10:
- 1100000000-->300*4=C00
- 10:
- 1100000000-->300*4=C00
- 12:
- 0
- 通过!dd Cr3+C00得到PDE(后3位属性为置零后再加C00)
- 通过!dd PDE+C00得到PTT(后3位属性为置零后再加C00)
- 通过!dd PTT+0得到物理地址
- 会发现,PTT+0就是Cr3指向的页目录表
- 这样,系统就可以通过线性地址访问到一个目录页(PDT表),也是物理页,也是PDT表,也是PTT表(特殊的PTT表)
![]()
- 总结
- 1.通过0C300000找到的物理页就是PDT目录页表
- 2.这个页目录表本身也是PTT页表
- 3.PDT页目录表其实就是一张特殊的PTT表,这个表的每一项PDE或者说PTE指向的是其他PTT页表
- 在windbg中,我们可以通过Cr3得到PDT目录表
- 而在程序中,无法使用物理地址,只能通过线性地址来找到PDT表
- 而程序就是通过C0300000这个线性地址来找到目录表(PDT)
1.2.10 页表基址(PTT)
- 现在可以获取PDT目录表了,但是还必须结合PTT表才可做到访问PTE,才可以修改PTE的属性,这就需要访问PTT表了。那么就需要PTT表的基址
- 线性地址C0000000,C0001000,C0002000,...
- 验证C0000000是PTT的第一个PTE,C0001000是PTT的第二个PTE,...
- 1. 打开windbg,!process 0 0获取Cr3
- 2. !dd Cr3获取PDT表(里面存的都是PDE指向PTT,共有1024个)
- 3. ①!dd PDE1查看第一张PTT表 (后3位属性位置0)
- ②!dd PDE2查看第二张PTT表(后3位属性位置0)
- ③!dd PDE3查看第三张PTT表(后3位属性位置0)
- 只是按照PDT表的顺序查看PTT表,没有特定找哪一个PTT表
- 4.拆分C0000000
- 1100 0000 00 300*4=C00
- 0000 0000 00 0*4=0
- 找到Cr3
- Cr3+C00得到PTE
- PTE+0得到物理地址
- !dd 物理地址
- 【发现和①找到的PTT表是一致的】
- 5.拆分C0001000
- 1100 0000 00 300*4=C00
- 0000 0000 01 1*4=4
- 找到Cr3
- Cr3+C00得到PTE
- PTE+4得到物理地址
- 【发现和②找到的PTT表是一致的】

1.2.10.2 总结
1.C0000000刚好是第一个PTT表的线性地址,通过它可以找到第一个PTT表的物理地址C0001000刚好是第二个PTT表的线性地址,通过它可以找到第二个PTT表的物理地址以此类推,每隔4KB就可以访问到下一个PTT表2.C0300000刚好是第0x300个PTT表,而第0x300个PTT表却又是PDT表3.PTT被映射到从0xC0000000到0xC03FFFFF的4M地址空间中(一个PTT是大小0x1000,也就是4KB,有1024个,1024*4KB=4M)4.之前学的PDT表其实就是第C0300000个PTT表,只是为了好学习才弄了个PDT表出来-------------------------------------------------------------------------公式总结:10-10-12PDI(可理解成下标):就是第一个10对应数值,PDT表第[10]个PDEPTI(可理解成下标):就是第二个10对应数值,PTI表第[10]个PTE访问指定线性地址的目录表公式:0xC0300000 + PDI*4(进入PDT目录表 + 第几个PDE)访问指定线性地址页表公式:0xC00000000+PDI*4096+PTI*4PDI*4096表示PDT表中前[PDI]个TSS表都不是要找的PTT表,而每个表是4096的大小-------------------------------------------------------------------------
2. 2-9-9-12分页
2.1 为什么是2-9-9-12?
- 10-10-12分页模式最多只能寻找4GB的物理地址:1024*(1024*4096)
- 但是4GB的物理地址不够用,为了找到更多的物理地址,只能将PTT的位数量拓展多点
- PTT的数量由原来的12-31位变成了12-35位,多了4位
- 2的2次方=4-->PDPT表有4个PDT表
- 2的9次方=512-->PDT有512个PTT
- 2的9次方=512-->PTT有512个物理页
- 2的12次方=4096-->物理页有4096个字节
2.2 2-9-9-12寻找物理地址
2.3 PDPT、PDT、PTT
2.3.1 PDPTE
第00位:是P位,为1表示有效09~11位:是给软件使用的12~35位:指向PDT表的指针
2.3.2 PDE
2.3.2.1 PS为1时
- 首先看PDT的第7位,PS位,当PS为1时,是大页
- PS位只对PDE有意义,PS是PageSize的意思。当PS=1时,PDE直接指向物理页,而不是PTT页,没有PTE,大页的地址是21~35位决定的(15位),低21位填充0。低21位填充0,所以页的大小是2MB(10-10-12中是4MB)。
- 寻找物理地址的时候:21~35位+21位0 一共36位,才是需要找的地址
- 第12位:多了一个PAT(页属性表),与CUP相关,并不是所有CUP都支持,不支持就填0
- 第9~11位:给操作系统软件使用的
2.3.2 PS位为0时
- PS位为0时的情况:指向正常页,此时,12~35是寻找物理地址的范围,一共24位,比101012多了4位,寻找的物理地址范围:2的24次方=64GB
2.3.3 PTE
PTE中存储的是物理地址物理页基址 :12~35位+低12位填充0物理页具体数据:12~35位(物理页)+12(2-9-9-12中的12的值,偏移)
2.3.4 XD/NX位
2.3.5 总结
- 2-9-9-12模式下,变化如下:
- ①新增了PDPT表,PDPT表存储的是一个指针,指向PDT表
- ②Cr3指向的不再是PDT表,而是PDPT表
- ③PPDPT,PDT,PTT表现在不再是4字节,拓展为8字节了。PTT的12~35位是页基址,比101012多了4位
- ④PTT寻找的物理地址范围由原来的4GB(物理页寻址:2的20次方*4096)变成64GB(2的24次方*4096),但是地址空间大小还是4GB,4*512*512*4096=4GB
- ⑤PDI和PTI由原来的10位变成了9位
- ⑥多了一个XD/NE属性,对数据区进行保护
3. TLB
3.1 TBL是什么
- 在10-10-12分页模式中,如果要通过线性地址读取4个字节的数据,但是实际上却是读多了8个字节:PDE四个字节、PTE四个字节
- 极端情况:需要读取的这4个字节不在同一个物理页上,那么实际上读取的数据就更多了,要读取更多的页
- 2-9-9-12更是如此,这样的效率是非常低的,因此,引出了TLB的概念
- TLB
- CUP在内部做了一个TLB表来记录这些东西,
- 这个表在CUP内部,读取速度和寄存器一样快
- Translation Lookaside Buffer
- 转义 辅助 缓存
- TLB就是用来缓存线性地址与物理页的对用地址的
3.2 TLB结构
- ①不同的CPU这个表的大小不一样.
- ②只要Cr3变了,说明进程切换了,线性地址的对应关系不成立了,TLB会立马刷新,一核一套TLB,但是G位为1的PDT或PTT不会刷新,会继续保留在TBL,因为G位为1的页是全局页,存储的都是高2GB的通用地址,进程间都是共享的
3.3 TLB的种类
- 在X86体系的CPU里,一般都设有如下4组TLB:
- 第一组:缓存一般页表(4K字节页面)的指令页表缓存(Instruction-TLB);
- 第二组:缓存一般页表(4K字节页面)的数据页表缓存(Data-TLB);
- 第三组:缓存大尺寸页表(2M/4M字节页面)的指令页表缓存(Instruction-TLB);
- 第四组:缓存大尺寸页表(2M/4M字节页面)的数据页表缓存(Data-TLB)
3.4 实验
3.4.1 体验TLB的存在
- ①.给线性地址A挂上一个物理页B
- ②.读取线性地址A的内容
- ③.将线性地址A的物理页改成C
- ④.读取线性地址A的内容
- 此时A的线性地址已经修改为C了,但是③读取线性地址A的时候,读取的仍然是物理页B的地址,因为读取的是TLB的内容,CUP第一次使用线性地址的时候就将线性地址映射到TLB中了,而TLB此时又没有刷新,所以读取的是TLB之前缓存的物理页B
3.4.2 体验全局页的存在
- ①进程A获取一个变量的线性地址
- ②将该线性地址对应的PDT,PTT的G位修改为1
- ③进程B访问进程A的线性地址
3.4.3 使用INVLPG指令,移除TLB中的指定对应关系
- 执行 invlpg [0x00401020]强制刷新该线性地址对应的条目
思考:
一般TLB能保存的映射关系只有几百条,但是对于系统来说,需要使用到这么多线性地址,这里只保存了几百条有什么作用呢?是不是太少了?
3.4.4 总结
- ①TLB中保存的是线性地址与物理地址的映射关系,只需要在第一次使用线性地址的时候加载PDE,PTE,不需要每次都加载
- ②进程发生切换的话,TLB会刷新,G位为1页会保留下来
③--------------------------------保护模式-中断、异常、控制寄存器--------------------------------------
1. 中断
1.1 什么是中断
- 中断是指由CUP外部的输入输出设备(硬件)所触发的,意思就是外部设备通知CUP“有事情需要处理”
- 中断请求的目的是希望CUP暂时停止执行当前的程序,转去执行中断请求所对应的中断处理程序(由IDT表决定)
- 80x86有两条中断请求线:
- 非屏蔽线 NMI
- 可屏蔽线 INTR
1.2 NMI-不可屏蔽请求处理
1.3 INTR-可屏蔽中断请求
1.3.1 中断管理器
- 在硬件级,可屏蔽中断是由一块专门的芯片来管理的,通常称为中断管理器。它负责分配中断资源和管理各个中断源发出的中断请求。为了便于标识各个中断请求,中断管理器通常用IRQ(Interrupt Request)后面加上数字来表示不同的中断。
- 比如:在Windows中 时钟中断的IRQ编号为0 也就是:IRQ0
![]()
- 1. IF为0时,若还有可屏蔽中断信号发送过来,CPU将不会处理,如果自己的程序执行时不希望CPU去处理这些中断,可以用CLI指令清空EFLAG寄存器中的IF位,用STI指令设置EFLAG寄存器中的IF位
- 2.硬件中断与IDT表中的下标对应关系并非固定不变的
- 参见:APIC(高级可编程中断控制器)
- 3.中断是由硬件设备发起的
2. 异常
- 异常通常是CPU在执行指令时检测到的某些错误,比如除0、访问无效页面等。
- 常见异常处理程序
![]()
- 缺页异常
- 一旦发生缺页异常,CUP会执行IDT表中0xE的中断处理程序,此时由操作系统接管
- 情况一.PDE、PTE的P位为0时
- 假设物理页紧缺,不够用了
- 线性地址A是有效的,但是物理页不够用了,此时线性地址A无法指向物理页,操作系统会将一个有效线性地址B所指向的物理页C的内容存储到文件中,从而达到腾出一个物理页供线性地址A使用,然后将线性地址B的指向的PDE和PTE的P位更改为0
- 此时再次访问线性地址B时,PDE和PTE的P位为0,物理页无效,就会触发缺页异常,执行IDT表的0xE中断处理程序,由操作系统接管,操作系统首先检查PTE,当发现第0位(P位)、第10位、第11位为0,但是其他位不为0,系统就知道此时物理页被写入到文件中了,会在第1~4位中找到文件编号,这个编号指明了原来物理页的内容存储在哪个文件,操作系统会找到这个文件,把文件的内容再读到物理页上,然后将PDE/PTE的P位置为1,这样就可以正常访问物理页了
- 操作系统通过这中方式节省了大量的物理页
- 情况二.当PDE/PTE的属性是只读的,但程序视图写入时
3. 中断与异常的区别
- 1.中断来自于外部设备,是中断源(比如键盘)发起的,CPU是被动的.
- 2.异常来自于CPU本身,是CPU主动产生的.
- 3.INT N虽然被称为“软件中断”,但其本质是异常
- 4.EFLAG的IF位对INT N无效。
- 5.无论是由硬件设备触发的中断请求还是由CPU产生的异常,处理程序都在IDT表。
4. 控制寄存器
- 控制寄存器用于控制和确定CPU的操作模式,一共有5个
- Cr0:
- Cr1:保留
- Cr2
- Cr3:页目录表基址,101012和29912的结构是不一样的
- Cr4
4.1 Cr0
- PE:
- 用于启用保护标志,PE=1为保护模式,PE=0是实地址模式。此标志仅开启段级保护,没有启用分页机制,若要启用分页机制,PE和PG都需要=1
- PG:
- PG为1,说明开启了分页机制。设置PG=1之前,必须保证PE已经为1。
- PG=0且PE=0 处理器处于实地址模式下
- PG=0且PE=1 处理器处于没有开启分页机制的保护模式下
- PG=1且PE=0 不存在
- PG=1且PE=1 处理器处于开启了分页机制的保护模式下
- WP:
- 写保护标志。当CPL<3时,此时处于系统级
- WP=0:当前程序可以读写任意用户级物理页,只要线性地址有效
- WP=1:当前程序可以读取任意用户级物理页,但对于只读的物理页,不能写
4.2 Cr2
- 缺页异常一旦发生,CUP会将引起缺页异常的线性地址写入到Cr2中,若没有Cr2,缺页异常一旦发生,原来的线性地址就丢了
4.3 Cr4
- PAE:
- 当PAE=1时,当前是2-9-9-12分页模式,当PAE=0时,当前是10-10-12模式(操作系统在启动的时候会看booit.ini,将这个位设置为配置文件设置的)
- PSE:
- PSE位=1时,PDE的PS位才有效,PS=1才会有效,会指向大页(4MB或2MB)
- PSE为0:时,PDE的PS位无效,不管PS=1还是PS=0,都是指向小页(4KB)
- PSE相当于PDE的PS位的总开关
4.4 总结
- Ce0:保护模式、实地址模式、启用分页
- Cr1:保留
- Cr2:缺页异常保存线性地址
- Cr3:页目录表
- Cr4:101012,29912,PDE的PS的总开关







































浙公网安备 33010602011771号