保护模式篇——分页基础
写在前面
此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
你如果是从中间插过来看的,请仔细阅读 羽夏看Win系统内核——简述 ,方便学习本教程。
看此教程之前,问几个问题,基础知识储备好了吗?前面的教程学会了吗?没有的话就不要继续了。
🔒 华丽的分割线 🔒
前言
在讲解分页基础之前,我们先大体了解CPU
是如何在保护模式下访问数据的,如下图所示:
比如我们执行mov eax,ds:[0x12345678]
这句汇编指令的时候,0x12345678
这个线性地址会传递给CPU
,先查询TLB
和缓存
有没有,有的话直接取出来返回;如果没有,经过MMU(内存管理单元)
处理得到物理地址,通过固定的分页模式直接找到,取出数据返回。
分页
前面的教程讲解了段的机制,接下来将介绍页的机制。CPU为了方便管理物理内存,按照页的方式进行管理内存。可用的所有内存可以类比为一本书,而所有的内存被分为这本书的一个页。对于32位来说,它有10-10-12
分页和2-9-9-12
分页。其中10-10-12
分页最为简单,故拿其作为详细讲解,作为分页讲解的基础。
我们都了解一个进程都有4GB的虚拟地址空间,它们并不是真正的地址,而是个索引。它通过某种方式进行转换,从而指向真正的物理地址,示意图如下所示:
而虚拟地址也被称作线性地址。举个例子,比如某个进程里面我想读取一个0x12345678
,它就是线性地址,通过一些转换,找到了对应的物理地址0x10101010
,如下图所示:
每个进程都有一个CR3
,准确的说是都一个CR3
的值。CR3
本身是个寄存器,一核一套。CR3
里面放的是一个真正的物理地址,指向一个物理页,一共4096字节
,如下图所示:
对于10-10-12
分页来说,线性地址对应的物理地址是有对应关系的,它被分成了三个部分,每个部分都有它具体的含义。线性地址分配的结构如下图所示:
第一个部分指的是PDE
在PDT
的索引,第二部分是PTE
在PTT
的索引,第三个部分是在PTE指向的物理页的偏移。PDT
被称为页目录表,PTT
被称为页表。PDE
和PTE
分别是它们的成员,大小为4个字节。我们接下来将详细介绍每一个部分是咋用的。
直接纯理论的讲解挺抽象的,我们先通过一个实现初步探测一下它们的存在。注意在学习之前需要对操作系统的调试设置进行修改,因为系统默认的是2-9-9-12
分页。如下图所示修改并重启操作系统即可启用10-10-12
分页:
实验探测
这次就需要我们的CheatEngine
,它是一个强大的内存搜索工具。我们打开一个记事本,然后打开CheatEngine 6.3
,如下图所示:
然后我们按照下图所示来打开notepad
进程:
然后在记事本中写入一个字符串This is a test
,然后在CheatEngine
搜索这个字符串,注意选中Unicode
,否则字符串搜不到。
然后随便改一下记事本最后一个字符串的字母,如下图所示,即可定位到真正的存储记事本填写内容的线性地址。
找到线性地址后,打开WinDbg
,找到记事本的CR3
,如下图所示:
我们按字节读取线性地址的内存时,在WinDbg的指令是dd [地址]
。如果是物理地址的内存的话,需要在前面加一个英文叹号,即为!dd [地址]
,查询内容的流程如下图所示:
10-10-12 分页整体结构
通过实验我们了解了它们的结构,接下来将详细介绍了。根据实验结果的体验,可以给出如下图:
分页并不是由操作系统决定的,而是由CPU
决定的。只是操作系统遵守了CPU
的约定来实现的。物理页是什么?物理页是操作系统对可用的物理内存的抽象,按照4KB
的大小进行管理(Intel
是按照这个值做的,别的CPU
就不清楚了),和真实硬件层面上的内存有一层的映射关系,这个不是保护模式的范畴,故不介绍。
PDE 与 PTE
前面我们简单了解PDE
和PTE
,接下来将学习它们的属性结构,结构如下:
P 位
表示PDE
或者PTE
是否有效,如果有效为1
,反之为0
。
R/W 位
如果R/W = 0
,表示是只读的,反之为可读可写。
U/S 位
如果U/S = 0
,则为特权用户(super user),即非3环权限。反之,则为普通用户,即为3环权限。
PS位
这个位只对PDE
有意义。如果PS == 1
,则PDE
直接指向物理页,不再指向PTE
,低22位是页内偏移。它的大小为4MB
,俗称“大页”。
A 位
是否被访问,即是否被读或者写过,如果被访问过则置1
。
D 位
脏位,指示是否被写过。若没有被写过为0
,被写过为1
。
注意,下面的三个位的讲解将涉及 TLB 和控制寄存器相关知识,为了保证文章的完整性,故先介绍。之后将会详细讲解。
G 位
表示是否为全局页。它的作用是什么呢?举个例子,操作系统的进程的高2G
映射基本不变,如果Cr3
改了,TLB
刷新重建高2G
以上很浪费。所以PDE
和PTE
中有个G
位,如果为1,刷新TLB
时将不会刷新它指向的页。
PWT 位
当PWT = 1
,写缓存的时候也要将数据写入内存中。
PCD 位
当PCD = 1
时,禁止某个页写入缓存,直接写内存。比如,做页表用的页,已经存储在TLB中了,可能不需要再缓存了。
注意事项
PTE
可以没有物理页,且只能对应一个物理页。- 多个
PTE
也可以指向同一个物理页。 PDE
和PTE
重合的属性共同决定着最终物理页的属性。比如P位
,如果有一个是0,那么最终的物理页就是无效的。但是PDE
和PTE
它们的属性的影响范围是不一样的。数值上:物理页的属性 = PDE属性 & PTE属性。
页目录表基址与页表基址
在学习本部分之前,请把练习的第一题做一下,以加深对此的印象。
如果系统要保证某个线性地址是有效的,必须为其填充正确的PDE
与PTE
,如果我们想填充PDE
与PTE
那么必须能够访问。有的人会想,直接拿CR3
去填写就行了,还需要页目录表基址干嘛?这里我强调一下:操作系统只能用线性地址,不能用物理地址。CR3
存储的是物理地址,这个是给CPU
看的,不是给操作系统看的。操作系统访问它就必须知道它的线性地址才行。CPU
可不帮我们挂物理页,它做不到这点,只能提供要求标准,而操作系统按照标准进行办事。于是乎页目录表基址与页表基址这两个东西就出现了。
通过页目录表基址,操作系统可以帮我们程序挂上正确的PDE
,通过页表基址挂上正确的PTE
,然后指向正确的物理页。
先说一下页目录表基址,我们先拆分一下这个线性地址:0xC0300000
。请读者先自行拆分,并用虚拟机查看物理内存后,再查看下面的结果。
🔒 华丽的分割线 🔒
我在虚拟机启动了一个notepad进程,中断操作系统转到Windbg中。查看进程可以得到它的CR3
。然后我们查看它的物理地址,即可看到它指向的PDT
。拆一下该线性地址,我们可以得到如下结果:
PDI -- 11 0000 0000
PTI -- 11 0000 0000
物理页偏移 -- 0x000
为什么查询的后面加0xC00
,而不是0x300
呢。是因为PDE
和PTE
的大小都是4
个字节,乘上4
后就是0xC00
了。最后查看到,到最后内容又指向了自己。即0xC0300000
存储的就是带PTE
属性的CR3
。我们根据当前所学的概念可以重新定义10-10-12
分页的结构框图:
同理,操作系统也需要页表基址。如果你老老实实地做了第一道题,你就知道我们找PDT
的时候就已经发现了指向PTT
的线性地址了,那就是0xC0000000
。具体实验我就不再赘述了,最终我们得到下面的结构图:
好了,我们思考一下有了0xC0300000
和0xC0000000
能做什么?如果我们掌握了这两个地址,就掌握了一个进程所有的物理内存读写权限。寻找PDE和PTE的公式总结如下:
- 访问页目录表的公式:
0xC0300000 + PDI * 4
- 访问页表的公式:
0xC0000000 + PDI * 4096 + PTI * 4
练习
本节的答案将会在下一节进行讲解,务必把本节练习做完后看下一个讲解内容。不要偷懒,实验是学习本教程的捷径。
俗话说得好,光说不练假把式,如下是本节相关的练习。如果练习没做好,就不要看下一节教程了,越到后面,不做练习的话容易夹生了,开始还明白,后来就真的一点都不明白了。本节练习比较多,请保质保量的完成。
1️⃣ 拆两个进程的4GB
物理页。(建议阅读页目录表基址与页表基址
这个部分前完成)
2️⃣ 定义一个只读类型的变量,再另一个线性地址指向相同的物理页,通过修改PDE
/PTE
属性,实现可写。
3️⃣ 分析0x8043F00C
线性地址的PDE
属性。
4️⃣ 修改一个高2G线性地址的PDE
/PTE
属性,实现Ring3
可读。
5️⃣ 在0
线性地址挂上物理页并执行shellcode
调用MessageBox
。
6️⃣ 逆向分析MmIsAddressValid
函数。
下一篇
本文来自博客园,作者:寂静的羽夏 ,一个热爱计算机技术的菜鸟
转载请注明原文链接:https://www.cnblogs.com/wingsummer/p/15363603.html