首页
论坛
课程
招聘
[原创]X86内核笔记_1_CPU保护模式
2021-6-3 14:50 4910

[原创]X86内核笔记_1_CPU保护模式

2021-6-3 14:50
4910

0608:补充CPU预读取知识
0609:补充段寄存器知识点
0610:补充跨段跳转知识点
0611:补充调用门知识点
0628:补充任务门、任务段、分页模式知识点
0630:补充CPU缓存及TLB知识点
0701:保护模式完结-补充控制寄存器知识点

 

目录

0.CPU指令预读取

https://zhuanlan.zhihu.com/p/32718424 可以看一下这个链接了解一下CPU知识和几个著名的漏洞。

 

文章中提到了 指令预测 技术 。 在执行长跳转(跨段跳转)时会将 缓存的指令 清空。 所以会造成执行速度减慢。这句话现在可能体会不到。 具个具体例子:

1
2
3
4
5
HOOK绘制做方框透视。   为什么你的框刷新速度慢跟不上人物模型的位置? 
因为你的HOOK代码用到了JMP,而且多数情况是个跨段跳转。  这种JMP会将缓存的指令清空。造成执行速度减缓。
而对于 绘制 这种频率极高的行为,这种速度减缓会被放大,也就造成了框子跟不上模型的现象。
 
而call指令无论跳多远都不会清空缓存,因此hook时使用call而不是jmp会优化绘制速度。让你的框子不会刷新过慢。

1.保护模式简介

CPU分为实模式、保护模式、虚拟8086模式,大多数操作系统运行在保护模式下

 

保护模式具有两个特点:段、页 ,保护模式真正保护的是数据结构、寄存器、指令

 

实模式:16位汇编,访问的都是物理地址,非常危险。

 

保护模式:保护的是对内存的访问,相对实模式安全。段层面保护,页层面保护。

 

保护模式具体资料可以在 Intel白皮书第三卷 中查看。

2.段寄存器

CPU共有八个段寄存器 : ES CS SS DS FS GS LDTR TR ,OD可见前6个,但GS段寄存器windows并未使用(32位下)。

 

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

 

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

 

LDT:局部描述符表 LDTR寄存器指向LDT段描述符。段描述符具体是什么在后文说明。

 

当执行一条指令如mov dword ptr ds:[0x12345678],eax时 , 真正访问的地址: ds.base + 0x12345678

 

当执行一条指令时,真正执行的是cs.base+EIP处的指令

 

当访问堆栈时,真正访问的是ss.base+Addr地址。

 

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

 

可见部分为Selector成员(选择子)。

 

具体结构如下图:

 

image-20210402155523289

 

image-20210402155536561

 

读段寄存器指令:mov ax,es 只能读16位(可见部分)

 

写段寄存器指令:mov ds,ax 写了96位的。

 

段寄存器可以用mov指令读写,但是LDTR和TR除外。

 

证明隐藏的80位的存在:(下表属性是根据实际分析时总结出来的。)

 

image-20210402160030863

段寄存器属性探测

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

练习

完成属性探测代码,感受段寄存器属性的存在。

3.段描述符

上文说过段寄存器有96位,其中16位的可见部分称为段选择子。剩下的80位哪里来?从GDT中查出来的

GDT 全局描述符表

GDT是一块内存,是CPU设计中要求操作系统提供的一款内存。这块内存是操作系统在启动时填充的。使用windbg的命令可以查看gdt表的地址:

1
2
3
4
5
6
7
8
9
gdtr寄存器(windbg伪寄存器,是windbg通过sgdt lgdt指令获取的,为了方便用户,才模拟了一个寄存器叫gdtr,实际是没有这个寄存器的) : 
存两个值,一个是GDT表在哪里,一个是GDT表有多大   48位  有32位存储在哪里,16位存储大小
r gdtr  r查看gdtr寄存器的前32位也就是位置
r gdtl  r查看gdtr寄存器的后16位也就是大小  都查gdtr
dd  xxxx       4字节查看内存
dq  XXXX       8字节查看内存
dq  xxxx Lnum    查看固定数量元素的内存
 
GDT表中存储着段描述符,每一个段描述符8个字节

LDT 局部描述符表

与GDT作用一样,但是在windows中很少(也可能是没有)使用LDT。后文会介绍到什么时候查LDT。目前只做个了解。知道有这么个表即可。

段描述符结构

image-20210402171835868

 

当执行mov ds,ax这种对段寄存器的赋值时,不是简单的给可见部分赋值。也会为80位隐藏部分赋值。

 

因此cpu会查表来取那80位的数据,根据ax的值决定查找GDT还是LDT,查找表的什么位置,查多少数据

 

ax决定了gdt中的第几个段描述符,ax这种2字节的数据,称为段选择子

练习

在学习段选择子之前,使用windbg查看GDT表的数据。 结合上图结构,拆分40个段描述符记录它们每个属性的值。不需要知道这些属性代表什么,该练习仅为了熟悉描述符结构。

4.段选择子

段选择子是一个遵守特定结构的16位的数,这个数决定了去GDT/LDT表中查哪一个段描述符,段选择子结构如下图:(段选择子的RPL一定要<=对应段描述符的DPL,否则试图使用该选择子加载对应段描述符的行为将由于权限不足而失败)

 

image-20210402173447380

练习

打开OD随便加载个程序,找到CS DS SS ES GS FS对应的描述符。

5.加载段描述符至段寄存器

加载段描述符至段寄存器的指令共有三种:

  • mov ss,ax 使用mov指令

  • les lss lds lfs lgs 修改对应的段寄存器

  • cs不能通过上述指令改变,否则会导致EIP的改变,必须保证cs与eip一起改。后文会讲解如何修改CS并在需要时提升权限。

段选择子填充

段寄存器不可见的80位是由对应的段描述符的以下部分填充的:

 

WORD Selector //16位 就是段选择子的值

 

WORD Attribute //16位 从段描述符的G位到Type位 刚好16位

 

DWORD Base //32位 段描述符的3个Base,顺序在图里标注了高低位。

 

DWORD Limit //32位 段描述符的2个SegLimit,G位为0,单位为字节,高位补0。G位为1,低位补FFF。

练习

打开OD随便加载个程序,找到CS DS SS ES GS FS的80位不可见部分。

6.段描述符属性

P位

有效位 1:描述符有效 0:描述符无效

 

当描述符无效时,任何尝试加载该描述符、访问该描述符对应的段间地址都会报错。

G位

段对齐粒度。 也就是决定了Limit大小的一个位。

 

在上文填充段寄存器隐藏部分时,Limit在描述符中只有5个16进制位表示,剩下的3个16进制位就需要看G位。

 

当G为0时,整个段将以字节对齐,Limit大小单位为字节,所以精确到1。Limit直接就是段长。段寄存器中的Limit高位补0。

 

当G为1时,整个段将以4KB对齐,Limit大小单位为4KB,所以段的末尾处一定是以FFF结尾。段寄存器中的Limit低位补FFF。

S位

描述符类型位。 为0时,是系统段描述符。 为1时,是代码或数据段描述符。具体类型需要搭配type属性来判断。

Type域

决定了具体是代码段还是数据段描述符

 

当S为1时,type针对代码或数据段,具体含义如下:

1
2
3
4
5
6
7
8
数据段:
A位:数据段是否被访问过位,访问过为1,未访问过为0  段描述符是否被加载过
W位:数据段是否可写位,可写为1,不可写为0
E位:向下扩展位,0向上扩展:段寄存器.base+limit区域可访问。1向下扩展:除了base+limit以外的部分可访问。
代码段:
A位:代码段是否被访问过位,访问过为1,未访问过为0  段描述符是否被加载过
R位:代码段是否可读位,可读为1,不可读为0
C位:一致位。1:一致代码段。   0:非一致代码段  具体后文解释

image-20210406103616472
当S为0时,Type针对系统段描述符

1
下图的每个类型会在后文详细说明。此处只留个印象即可。

image-20210406104706585

D/B位

大段或者小段,分为三种情况:

 

对CS段来说:

 

为1时,默认为32位寻址。 为0时,默认为16位寻址。

 

对SS段来说:

 

为1时,隐式堆栈访问指令(PUS H POP CALL RETN等)修改的是32位寄存器ESP

 

为0时,隐式堆栈访问指令(PUSH POP CALL RETN等)修改的是16位寄存器SP

 

对于向下扩展的数据段:

 

为1时,段上限大小为4GB。 为0时 段上限大小为64KB

练习

分别更改ES SS DS几个段寄存器对应段描述符的各个属性,感受下当某个属性被更改时,会对代码执行发生什么影响?

7.段权限检查

CPU分级(与操作系统无关)

0环 1环 2环 3环 特权指令只能运行在0环

 

我们常说的驱动就是运行在0环权限下。也是最高权限,所以驱动才会那么牛逼。

 

而常见的一些exe dll等我们直观感受的软件,代码都是运行在3环,权限最低。

CPL-当前特权级别

当前特权级:CS和SS段选择子的后两位。之所以称为3环程序就是CPL为3

 

CS和SS段选择子的后两位永远相同!(X86规定的)

 

所以所谓的你程序是几环的就是看CS SS,而所谓的提权就是改CS SS,只要有一种方法能改掉CS SS,那就是提权。

DPL-描述符特权级别

描述符特权级别:访问该段所需要的特权级别。

1
mov ds,ax  若ax所指向的段描述符级别为0,且当前CPL为3,那么这行指令执行会失败。

RPL-请求特权级别

请求特权级别:段选择子中的后两位,可以随意指定。

 

当使用选择子来加载段描述符时,会检查CPL DPL RPL三者。

数据段的权限检查

1
2
mov ax,000B        //RPL = 3
mov ds,ax        //假设DPL = 0

假设上述代码运行在0环,也就是CPL=0,成功条件为:CPL<=DPL且RPL<=DPL,所以代码会执行失败。

练习

修改DS CS对应的DPL,看看OD中内存访问、代码执行会有什么影响?

8.代码跨段跳转

必须保证CS与EIP同时修改,因此没有lcs这种只修改cs的指令。

 

同时修改CS与EIP的指令:

1
JMP FAR、CALL FAR、RETF、INT、IRETED

案例:

 

JMP FAR 0x20:0x004183D7

1
2
3
4
5
6
7
8
1.0x20为段选择子,拆分后RPL=0 TI=0 INDEX=4,因此查GDT表,索引为4,请求特权级为0
2.查GDT找对应段描述符。由于是修改CS段,所以不是所有段描述符都可以,四种情况可以跳转:代码段、调用门、TSS任务段、任务门    此处为了练习我们只规定其必须使用代码段描述符。
3.权限检查 如果是非一致代码段  CPL== DPL 且 RPL<=DPL   严格检查权限则使用非一致代码段
         如果是一致代码段  CPL>=DPL  不会破坏内核数据的可以使用一致代码段
4.将段描述符加载到CS段寄存器中。
5.将SC.base+Offset写入EIP,然后执行CS:EIP处的代码
 
直接对代码段进行JMP或CALL,无论目标是否为一致代码段,CPL都不会发生改变。只能通过类似调用门的方式改变CPL。
1
2
3
4
5
一致代码段:
    也就是共享的段,特权级高的程序不允许访问特权级低的数据:内核态不允许访问用户态数据
    特权级低的程序可以访问特权级高的数据:用户态可以访问内核态数据,但特权级依然是用户级别。
非一致代码段:
    普通代码段,只允许同级访问。禁止不同级别的访问。

练习

使用jmp far跳转到自己的裸函数,裸函数内使用jmp far跳回来。要求不能崩溃报错。

9.代码跨段跳转实验(3环测试)

  • 构造一个DPL为3的非一致代码段的段描述符,使用JMP FAR指定一个RPL为3的段选择子,可以执行
  • 构造一个DPL为0的非一致代码段的段描述符,使用JMP FAR指定一个RPL为3的段选择子,不可以执行
  • 构造一个DPL为0的一致代码段的段描述符,使用JMP FAR指定一个RPL为3的段选择子,可以执行
  • 构造一个DPL为0的一致代码段的段描述符,使用JMP FAR指定一个RPL为0的段选择子,可以执行

10.长调用与短调用

CALL FAR较为复杂,因为对堆栈产生了影响。

短调用:

常规call调用,调用后将下一条指令地址压入堆栈,并将ESP-4,然后修改EIP为指定地址。

 

RETN将栈顶数据赋给EIP,并将ESP+4

长调用(跨段不提权,call代码段)

指令格式: CALL FAR CS:EIP

 

拆分CS段选择子,找到一个段描述符,这个段描述符是一个代码段。新EIP为该代码段的base+call指令后面的EIP

 

call时会将调用者的CS首先压入堆栈,然后压入返回地址。同时修改CS。(RPL若与CPL不同,会被CPU强制修正。)

长调用(跨段不提权,call门)

指令格式: CALL FAR CS:EIP (EIP是废弃的,通过指定的段选择子来决定跳到哪)

 

拆分CS段选择子,找到一个段描述符,这个段描述符必须是个调用门。

 

跨段不提权的长调用与短调用不同的是,call时会将调用者的CS首先压入堆栈,长返回retf时在弹出EIP后再将CS弹出

长调用(跨段并提权,call门)(堆栈切换)

指令格式: CALL FAR CS:EIP (EIP是废弃的,通过指定的段选择子来决定跳到哪)

 

拆分CS段选择子,找到一个段描述符,这个段描述符必须是个调用门。

 

涉及到权限变化后会发生堆栈的切换。从原调用者的堆栈A到目标堆栈B。

 

CALL后会将调用者的SS ESP CS 返回地址依次压入到堆栈B中。在retf时依次弹出回原调用者。

 

堆栈B和SS来源于TSS,每当发生提权调用时CPU就会从TSS中取出堆栈B和SS的值,而这两个值是Windows操作系统赋进去的。每个线程有两份堆栈,一份3环的一份0环的。

提权时的新堆栈从哪来

提权方式共有:调用门 任务门 中断门 陷阱门 快速调用 这几种。

 

其中快速调用需要CPU的MSR寄存器支持。

 

除了快速调用以外,其他的提权方式SS 和 ESP都是从TSS中获取。

 

快速调用的SS是通过CS+8计算得到,ESP在MSR寄存器里存着。

练习

门描述符后面学,先练习call代码段。

 

使用call far调用自己的裸函数,裸函数内使用jmp far跳回来。要求不能崩溃报错、跳回后原寄存器的数值不能被改变。

11.调用门(无参)

指令格式: CALL FAR CS:EIP (EIP是废弃的,通过指定的段选择子来决定跳到哪)

 

image-20210406165257166

 

根据CS的值查GDT得到门描述符,门描述符中的SegmentSelector指向一个代码段描述符。代码段描述符的base+门描述符的Offset才是要执行的代码的位置。

 

最终CS的值是调用门描述符内的选择子的值。

 

构造门描述符时,DPL一定要为3,否则无法访问门描述符,这样就连敲门的权利的没有了何谈进入门内提权。

 

调用门描述符内的选择子指向的代码段描述符的DPL为0时会自动提权。(代码3环,RPL任意,门3环,门内的代码段0环,则会自动提权)

12.调用门(有参)

参数个数写入门描述符中的ParamCount中,参数手动push,执行结束后通过retf 0xC之类的命令返回。注意堆栈平衡。

 

参数存储在CS和ESP之间,如 push 1 , push 2, call far xxxx, 堆栈内部情况为:返回地址、原CS、2、1、原ESP、原SS

练习

  1. 使用call调用门进0环。
  2. 使用call调用门从3环跳3环。
  3. 使用jmp调用门进0环。
  4. 使用jmp调用门从3环跳3环。
  5. 使用任意方法进入0环,使用jmp调用门从0环回3环。
  6. 使用任意方法进入0环,使用jmp调用门从0环跳到0环。
  7. 使用任意方法进入0环,使用call调用门从0环回3环。
  8. 使用任意方法进入0环,使用call调用门从0环跳到0环。

调用门总结

call可以调用调用门。
对应代码段比CPL权限高时,自动提权,堆栈存储EIP CS 参数 ESP SS。
对应代码段比CPL权限低时,会触发异常,蓝屏。
权限相同时,执行正常,权限不变。
jmp可以跳调用门。
如果权限相同,则执行没问题,堆栈里没有值。
如果涉及提权,则会C005访问异常,代码仍在3环,不会蓝屏。
如果涉及降权,则会蓝屏。

13.中断门

Windows中的提权方式并未使用调用门,而是使用了中断门(旧CPU)。现在的新CPU全部使用快速调用。

 

中断门调用使用int+中断描述符索引指令来进入,返回时通过iretd返回。 中断门无法携带参数。

 

断电走2号中断(IDT第三个),双重错误走8号中断

IDT表

中断描述符表。 r idtr r idtl 。 IDT表中全部存储系统段描述符。其中分为三类,任务门描述符,中断门描述符,陷阱门描述符。

 

image-20210407111837181

 

1110为32位中断门 0110为16位中断门

中断 返回

相比较调用门,中断门的压栈数据多了一个EFLAGS寄存器。

 

当未发生权限转换(无堆栈切换)时,堆栈中的数据是: 返回地址,EFLAGS,原CS

 

当发生权限转换(堆栈转换)时,堆栈中的数据是:返回地址,原CS,EFLAGS,原ESP,原SS

 

在中断门中,不可以通过RETF返回,转而使用IRET/IRETD指令返回。

练习

修改中断号为3的中断门的选择子,达到hook int3的效果。当int3指令执行时随便打印一句话。

 

提示:注意fs和cs。要保证hook代码执行完毕后的现场和刚进0环时的现场一模一样,否则KiTrap03回3环时会有问题。

 

体会CPU指令缓存的作用.

14.陷阱门

type为1111时是32位陷阱门,0111时是16位陷阱门。陷阱门执行指令为INT+IDA索引号。与中断门相同。返回指令也相同。不同点是中断门执行时会将EFLAG寄存器中的IF位清零,而陷阱门不会。

 

IF:中断允许标志位。当为1时,CPU会响应可屏蔽中断请求。为0时,CPU不会响应可屏蔽中断请求。

 

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

 

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

 

image-20210407140410433

15.任务段(TSS)

TSS是一块内存,104个字节,存储着大量的寄存器的值,结构如下图

 

image-20210407144513847

TSS的作用

Intel的设计思想:想通过TSS来实现任务的切换,因为利用TSS可以一次性更换很多寄存器从而实现快速的任务切换。

 

操作系统的设计思想:觉得TSS不够好,所以windows自己实现了一套任务切换(线程切换)。

 

最终作用:可以一次性替换很多寄存器,由于TSS表中有CS SS,所以可以用于提权。

TSS的位置

CPU中有一个段寄存器叫TR(TaskRegister),TR有96位,其中16位可见部分为选择子,可以找到GDT表中的一个段描述符,通过该描述符加载TR段寄存器中后80位。TR寄存器的Base指示了TSS表的位置。Limit指示了TSS表有多大。

 

Type:1001表示当前描述符是个TSS段描述符但没有加载到TR寄存器中。 1011表示已经加载到TR寄存器中。

 

image-20210407145450502

读写TR寄存器

通过LTR指定来装载TR寄存器(96位)。装载后TSS段描述符中的Type-3位会发生改变。LTR指令只能在0环权限中使用。并且仅修改TR寄存器,不修改其他寄存器。

 

通过STR指令来读取TR寄存器,但只能读取16位,也就是可见部分(段选择子)

TSS任务段切换步骤

  1. 调用 call/jmp + TSS任务段选择子
  2. CPU将调用者的寄存器存入当前TR寄存器对应的TSS中。
  3. 将TSS任务段选择子加载到TR寄存器中
  4. 将新的TR寄存器对应的TSS任务段中的所有寄存器全部替换。

模拟任务切换(一次性切换所有寄存器)

  1. 准备TSS任务段(104个字节)并将对应成员赋值。CR3通过 !process 0 0 来查看

  2. 准备TSS段描述符。指向准备好的TSS任务段。并写入GDT表中合适的位置。(G位为0,AVL为0,因为TSS任务段是以字节为单位的。)

  3. 修改TR寄存器指向TSS段描述符。由于ltr指令为特权指令,因此若想在3环实现TR寄存器修改,需要使用JMP FAR或CALL FAR。

    当JMP FAR后的段选择子指向一个TSS任务段描述符时,会首先将描述符装载到TR寄存器中,然后在根据TR.Base(TSS任务段)来修改当前寄存器的值。

JMP指令进行任务切换,TSS段中的PreviousLink不会被赋值。且NT位不变。

CALL指令进行任务切换,TSS段中的PreviousLink会被填入上一个TSS段所属TSS段描述符的段选择子。且NT位被置1.

NT位:任务嵌套位。 NT位为0时,IRET/IRETD会从堆栈中取值(中断返回)。 NT位为1时,IRET/IRETD不会再从堆栈中取值返回,而是从当前TSS的PreviousLink对应的上一个TSS中取数据进行返回。(调试模式,单步调试时,NT位会被清0,导致IRET无法从TSS中返回,造成蓝屏)

ESP和ESP0

当我们主动JMP CALL任务段或任务门时,CPU会将ESP SS替换。

 

当我们通过调用门、中断门这种提权时,CPU会去找TSS,这种可以理解为被动。这时会根据新CS的权限来选择替换ESP0 SS0、ESP1 SS1还是ESP2 SS2。

练习

使用jmp TSS任务段进入0环,使用JMP出来。

16.任务门

使用INT指令+IDT表索引的方式进入任务门。通过IDT表中对应的描述符中的TSS Segment Selector查GDT表找到TSS任务段描述符,使用IRETD返回。

 

image-20210407172917526

练习

使用任务门进1环。

17.10-10-12分页

地址的概念:

每个进程的4GB是假的,并不是每个进程占4GB。CPU将线性地址转换为物理地址。在X86架构中转换的方式有两种:10-10-12分页方式和2-9-9-12分页方式(PAE模式)。

 

mov eax,dword ptr ds:[0x12345678] 0x12345678为有效地址,ds.base+0x12345678为线性地址,通过线性地址进行拆分得到的是物理地址。

WinXP设置分页方式:

默认为2-9-9-12分页,指定为10-10-12分页方式为:将noexecute中的no删除即可

1
2
3
4
29912
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="debug" /noexecute=optin /fastdetect /debug /debugport=com2
101012
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="debug" /execute=optin /fastdetect /debug /debugport=com2

Win7~11设置分页方式:

默认为2-9-9-12分页模式(PAE模式),使用工具EasyBCD快捷修改启动引导属性。

 

image-20210625105215602

CR3寄存器简述

CR3是一个寄存器,每个核只有一个。他是唯一一个存储着物理地址的寄存器。这个物理地址指向第一级目录(PDE,共4096字节)。

 

通过拆分后的第一个10, ×4后,可以找到PDE中存储的一个地址(PTE)。

 

通过拆分后的第二个10, ×4后, 可以找到PTE中存储的一个地址(物理页)

 

通过拆分后的第三个12(偏移)加上物理页首地址,得到真正的物理地址。

拆分得到的10为索引,32位下地址宽度4字节,所以需要×4,得到的12为偏移,无需×4

实践转换物理地址:

  1. 在记事本中随便写入一串字符串,通过CE搜索字符串在内存中的位置,记录内存地址。000AB318image-20210412175209519

  2. 将该内存地址转换为二进制形式,并以10-10-12的形式拆为3部分(高位补0)

    1
    2
    000AB318 -> 0000 0000 0000 1010 1011 0011 0001 1000 
    0000 0000 00`00 1010 1011`0011 0001 1000  -0`AB`318   -0`2AC`318
  3. 获取记事本进程的CR3,通过!process 0 0遍历进程,可以看到CR3(DirBase,多次打开进程,选最后一个打印的。)18394000 image-20210413095914160

  4. 在windbg查看CR3+第一个10×4后(0)的物理内存内容。使用!dd(查看物理内存)命令而不是dd(查看线性内存)。
    image-20210413100635187

  5. 将PTT首地址的后三位清0(这三位是属性,使用时需清0)后,加上第二个10×4(2AC)得到物理页首地址。image-20210413101022588

  6. 将物理页首地址的后三位清0(同上),加上第三个12得到真实的物理地址:184CC000+318 = 184CC318。使用!db命令以字节方式查看该地址,发现了HelloWorld字样。 image-20210413101333280

18.PDE、PTE初步感受

地址转换逻辑图:

(PDE:页目录表项 PDT:页目录表 PTE:页表项 PTT:页表)

 

image-20210413101806515

10-10-12分页含义:

页目录表(PDT)与页表(PTT)都是4KB(4096字节)。但是每个项是4个字节,所以一共是1024个项,是2的10次方,需要10位二进制来存储。

 

物理页是4096个字节,但是内存宽度最低1KB,所以一共有4096个地址,是2的12次方,需要12位二进制来存储。刚好为10-10-12分页。

PTE-物理页对应关系:

一个页表项(PTE)可以不指向物理页。多个页表项(PTE)可以指向同一个物理页(不同权限)。一个页表项(PTE)只能指向一个物理页。

0地址探求:

在正向开发中,我们知道线性地址0是无法读写数据的。我们需要通过查看0的物理地址来搞清楚为什么0无法读写。image-20210413105529383

 

可以看到,0地址的PTE为0,并没有具体的物理页,也就没有一个真实的物理内存。这是0地址无法读写的一个原因。那么如何使0地址可以读写呢?

练习:挂载物理页实现0地址读写

编写代码,实现0地址读写。首先定义一个局部变量。这个变量的地址是可读写的,因此它一定有一个可读写的物理页。输出这个变量的地址。image-20210413105001431

 

将0012FF7C根据10-10-12分页方式拆分,查找物理地址。

1
0012FF7C -> 0000 0000 00`01 0010 1111 `1111 0fff 1100 -> 0`12F`F7C -> 0`4BC`F7C

image-20210413105348581

 

将局部变量a的PTE 0x26861067 写入至0的PTE中,使用!ed命令来修改物理内存。image-20210413105909179

 

让程序执行,观察控制台输出,可以看到我们成功读0地址进行了一波读写。通过该实验可以得出一个线性地址的读写由PDE和PTE共同决定。线性地址仅用于寻找PDE PTE。image-20210413105951422

19.PDE、PTE属性

PDE PTE后12位(二进制位,3个16进制位)存储着属性,两者的属性字段和属性值很多都是一样的。重复属性是与的关系,必须两者都为1,对应物理页该属性才为1。

 

image-20210413110743036

P位

有效位。两个P位都为1时该页有效,只要其中一个P位为0时该页无效。

R/W位

读写位,两者的P位都为1时代表该页内存可读可写,只要有一个为0代表只读不可写。(对于非VT而言,一块内存申请出来,它必然是可读的。)

 

实验验证:定义一个只读常量str,如下:

1
2
3
char* str = "aaaaaaaaaaaaaaa";
printf("%08x\r\n",str);
*str = 'x';   //正常情况下会报错,编译器认为这种“”字符串应该以常量形式存储。

修改str的PDE和PTE,使R/W位为1,尝试对str的数据进行修改。

US位

特权等级位,都为0时,特权用户可读写,都为1时,普通用户可读写。

 

特权用户: 0 1 2 环 普通用户:3环

 

实验验证:通过修改高2G地址的US位实现3环读写。

PS位

仅对PDE有意义,PaseSize位,页大小,当PS位为0时,为4KB小页。寻址如上述正常寻址。

 

当PS位为1时,对应物理页为大页,PDE直接指向物理页,无PTT参与。线性地址低22位为页内偏移。

A位

Access 是否被访问过位,访问过就全部置1,否则为0。 哪怕是一个字节的地址访问也会将其对应的PDE PTE的A位置1.

D位

Dirty 脏位,是否被写过位。逻辑与A位相同。

G位

全局页,当CR3发生改变后CPU将切换TLB,若G位为1,则该页在CR3发生切换时,TLB中的数据将不被刷新。

 

TLB与缓存将在后续有专题讲解。

PWT位

缓存相关,后续缓存章节会有说明,此处暂时不用了解。

PCD位

缓存相关,后续缓存章节会有说明,此处暂时不用了解。

PAT位

缓存相关,后续缓存章节会有说明,此处暂时不用了解。

9、10、11位(保留位,用于Windows内存管理)

Windows为了节省内存,当一个地址A对应的物理页访问频率很低时,会将其数据写入文件保存,将该物理页提供给哪些访问频率高的地址。在写入文件后,会将该线性地址的PTE的P位置0。9~11位为文件索引。

 

CPU在拆分线性地址寻找物理地址时,若发现P位为0,则会执行中断表中的0xE号中断。当地址A被访问时,发现P位为0,CPU触发E号中断。操作系统在E号中断处理函数中根据9~10位寻找对应的文件。将数据读出返回。具体的应用会在内存管理章节详细讲解。image-20210414143359750

20.页目录表基址-C0300000(101012分页下)

思考:当我们申请一块内存用于读写,系统会为这块内存挂载相应的PDE 与PTE,挂载时必然会访问PDT与PTT。但我们在程序中出现的所有地址全部为线性地址,在保护模式下我们无法对任何物理地址进行操作。想要读写PDT PTT就必须将这两个表的物理地址映射成线性地址并挂载PDE PTE。这一步是我们及系统都无法做到的。 CPU为我们贴心的将PDT PTT的物理首地址映射成了线性地址(C0300000)供操作系统访问。这就是页目录表基址和页表基址。

验证基址正确性

1
C0300000 -> 400 400 00 -> C00 C00 0

image-20210413134513487

21.页表基址-C0000000+1000*index(101012分页下)

我们已经知道了C0300000就是PDT的首地址线性地址。在拆分C0300000时,第一部分C00指向了PDT首地址。第二部分的C00再次指向了PDT。通过偏移为0找到了PDT。

 

得出: 第一个C00找到了PDT,第二个C00找到了PDT所在物理页,也就是PTT。第三个偏移0指向表头。而我们又知道PDT内存储着一个个的PDE(PTT表头)。那么也就说明第二个10位数字是PDE的索引。索引×4=C00处的偏移刚好为PDT。那么0 4 8 这种偏移就是正常的PTT表头。通过这种方法,我们逆向C0300000重新组合,找到PTT基址公式。

1
2
3
4
5
C0300000 -> 300(1100 0000 00) 300(11 0000 0000) 000 -> 将第二个数替换为我们想要的PTT索引得到如下:
第一个PTT索引0300(1100 0000 00) 000(00 0000 0000) 000 -> 逆向还原得到线性地址C0000000
第二个PTT索引1300(1100 0000 00) 000(00 0000 0001) 000 -> 逆向还原得到线性地址C0001000
第三个PTT索引2300(1100 0000 00) 000(00 0000 0010) 000 -> 逆向还原得到线性地址C0002000
得出公式 PTT表基址 = C0000000+1000×index  回去验证一下,将300×1000+C0000000刚好是PDT基址。

22.PDE PTE寻址公式总结

PDT基址: C0300000 PTT基址:C0000000+1000×index 得出

 

每个PDE:C0300000+4×PDI 每个PTE:C0000000+1000×PDI+4×PTI

23.MmIsAddressValid逆向分析(10-10-12)

10-10-12分页模式下,内核文件为ntoskrnl.exe

 

29912分页模式下,内核文件为ntkrnlpa.exe

 

image-20210422135637011

 

image-20210422135753938

24.2-9-9-12分页(PAE模式-物理地址扩展)

设计思路

因为页大小依然是4KB,所以需要12个二进制位才可以找到整个4KB的每一个地址。

 

页大小固定,想要增大寻址范围,就要增加PTE的个数(一个PTE等于一个物理页)。在101012模式下,PTE最多FFFFF个。所以想要增加PTE数量就要增加PTE的长度。从4字节改为8字节,那么PTE最多就变成了FFFFFFFFFFFFF个。但PTT表的大小就会缩减一半。所以需要9个二进制位才能描述到每一个PTE。

PTT的大小不等于寻址能力大小,因为PTT内每一个PTE都可以更换。

 

PDE同理,从4字节变为8字节,数量减少一半,需要9个二进制位才能描述到每一个PDE

 

还剩2位,windows进行了扩展。扩展成了PDPT(PageDirectoryPointTable)。里面的成员叫PDPTE。成员数量为4。(2个二进制位)。每个成员占8字节。

WinXP开启PAE

在boot.ini中将execute改为noexecute后重启即可。

Win7~11开启PAE

使用EasyBCD工具快捷修改分页模式。

 

image-20210625105747946

整体结构

image-20210423101536358

PDPTE结构

image-20210423101702983

PDE结构

image-20210423101753337

 

如果PS位为1:大页,35~21位为物理页地址。低21位为页内偏移。每个页2MB大小。

PTE结构

image-20210423101827232

映射说明

PTE中35-12是物理页基址,24位,低12位补0。物理页基址+12位的页内偏移指向具体数据

XD/NX位(DEP数据执行保护):

在PAE分页模式下,PDE/PTE的最高位称为XD/NX位。不可执行位,为1时,该段内存不可执行。

 

就是我们常见的DEP数据执行保护

练习:

逆向29912分页下的(ntkrnlpa.exe)MmIsAddressValid函数,找出页表基址和windows的判断方法。

25.TLB

当我们想读取一个线性地址的数值时,CPU会先读这个线性地址对应的PDE再读PTE再读物理内存。 这样就相当于多读了8字节的内存,若这个线性地址前两个字节和后两个字节不在同一物理页上,那么多读的字节数会更大。这点在29912分页模式下更为明显。为了加快读取速度,提高效率,CPU内部做了一张表用来存储已读取过得线性地址和物理地址间的映射关系。这张表的读写速度与寄存器一样快速。这张表就是TLB(Translation Lookaside Buffer)。

TLB结构:

image-20210425120725044

G位 - TLB切换:

TLB由于存储在CPU内部,因此一个核一套。当CR3发生了切换(任何对CR3赋值的指令都会触发TLB的切换,与最终值是否改变无关,如mov eax,cr3 mov cr3,eax),TLB就会被切换。操作系统中高2G(系统空间)的物理地址映射关系几乎不变。当CR3发生切换时,若将TLB中的高2G地址刷新会很浪费时间。因此PDE与PTE中的G位决定了TLB在切换时是否保留当前映射关系。1为保留,0为不保留。

 

G位(全局页位)为1时,整个页在TLB切换时都不会被刷新。

TLB种类:

物理页分为普通页(4KB)、大页(2MB/4MB),物理页又分为指令和数据。因此分为4种TLB

  • 缓存一般页表(4KB)的指令页表缓存(Instruction-TLB)
  • 缓存一般页表(4KB)的数据页表缓存(Data-TLB)
  • 缓存大尺寸页表(2MB/4MB)的指令页表缓存(Instruction-TLB)
  • 缓存大尺寸页表(2MB/4MB)的数据页表缓存(Data-TLB)

TLB实验:INVLPG

INVLPG指令可以强制将某个地址从TLB中刷新掉。而不看G位。

  1. 为0地址挂PTE,写入数据A,更改0地址的PTE,读0地址,发现还是数据A,证实了TLB存在。
  2. 为0地址挂PTE,写入数据A,更改0地址的PTE,切换CR3(mov eax,cr3 mov cr3,eax),读0地址,发现变成了数据B,证实了TLB的刷新。
  3. 为0地址挂一个G位为1的PTE,写入数据A,更改0地址的PTE,切换CR3(mov eax,cr3 mov cr3,eax),读0地址,发现还是数据A,证实了全局页存在。
  4. 为0地址挂一个G位为1的PTE,写入数据A,更改0地址的PTE,切换CR3(mov eax,cr3 mov cr3,eax),INVLPG强制刷新TLB中的数据(INVLPG dword ptr ds:[0]),读0地址,发现变成了数据B,证实了INVLPG指令的作用。

过CRC之shadow walker

TLB分为DataTLB和InstructionTLB,CRC校验开启时,目标代码由于会被执行,所以InstructionTLB会存一份,由于CRC线程会读取目标代码,所以DataTLB又会存一份。通过更改其中一个TLB内目标地址的映射缓存来实现读取和执行时指向的物理内存不是同一地址。达到欺骗CRC校验的效果。

 

但3环程序中使用的低2G地址在TLB中经常会被刷新,因此该技术显得很鸡肋。内核层高2G地址几乎不被刷新,此技术应用很强大。

过CRC之代码拷贝执行

通过层层分析,找出目标代码段上一层代码将要调用的系统API。将API后的目标代码段拷贝至自身内存。修复全局变量、IAT等数据以保证正常执行。在系统API中Hook,将EIP指向拷贝后的代码。由于CRC校验的是原始数据而我们跳回的是拷贝数据,因此可以过掉CRC。但要注意堆栈检测。

26.中断与异常

中断概念

中断通常是由CPU外部的硬件设备所触发的,供外部硬件设备通知CPU有事情需要处理。因此又叫中断请求。中断请求的目的是希望CPU能暂停当前正在执行的程序转而处理中断请求所对应的处理程序。(IDT表决定)

 

X86 CPU有两条中断线,分别是: 可屏蔽中断线(NMI) 不可屏蔽中断线(INTR), 中断描述符表(IDT) 中索引为2的门为不可屏蔽中断处理程序。

 

当可屏蔽中断请求发生时,CPU会观察EFLAG寄存器中的IF位来决定要不要处理这条中断请求。为1处理,0不处理。

 

使用CLI指令将IF位置0 ,只用STI指令将IF位置1. clean-set

中断控制器

在CPU中 ,可屏蔽中断由一块专门的芯片来管理,称为中断控制器(APIC)。其管理着每个可屏蔽中断请求对应的中断处理程序的IDT表索引。在其内部也存在一个编