保护模式篇——分页基础

写在前面

  此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我

你如果是从中间插过来看的,请仔细阅读 羽夏看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分页来说,线性地址对应的物理地址是有对应关系的,它被分成了三个部分,每个部分都有它具体的含义。线性地址分配的结构如下图所示:

  第一个部分指的是PDEPDT的索引,第二部分是PTEPTT的索引,第三个部分是在PTE指向的物理页的偏移。PDT被称为页目录表,PTT被称为页表。PDEPTE分别是它们的成员,大小为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

  前面我们简单了解PDEPTE,接下来将学习它们的属性结构,结构如下:

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以上很浪费。所以PDEPTE中有个G位,如果为1,刷新TLB时将不会刷新它指向的页。

PWT 位

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

PCD 位

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

注意事项

  • PTE可以没有物理页,且只能对应一个物理页。
  • 多个PTE也可以指向同一个物理页。
  • PDEPTE重合的属性共同决定着最终物理页的属性。比如P位,如果有一个是0,那么最终的物理页就是无效的。但是PDEPTE它们的属性的影响范围是不一样的。数值上:物理页的属性 = PDE属性 & PTE属性。

页目录表基址与页表基址

  在学习本部分之前,请把练习的第一题做一下,以加深对此的印象。
  如果系统要保证某个线性地址是有效的,必须为其填充正确的PDEPTE,如果我们想填充PDEPTE那么必须能够访问。有的人会想,直接拿CR3去填写就行了,还需要页目录表基址干嘛?这里我强调一下:操作系统只能用线性地址,不能用物理地址CR3存储的是物理地址,这个是给CPU看的,不是给操作系统看的。操作系统访问它就必须知道它的线性地址才行。CPU可不帮我们挂物理页,它做不到这点,只能提供要求标准,而操作系统按照标准进行办事。于是乎页目录表基址与页表基址这两个东西就出现了。
  通过页目录表基址,操作系统可以帮我们程序挂上正确的PDE,通过页表基址挂上正确的PTE,然后指向正确的物理页。
  先说一下页目录表基址,我们先拆分一下这个线性地址:0xC0300000。请读者先自行拆分,并用虚拟机查看物理内存后,再查看下面的结果。


🔒 华丽的分割线 🔒


  我在虚拟机启动了一个notepad进程,中断操作系统转到Windbg中。查看进程可以得到它的CR3。然后我们查看它的物理地址,即可看到它指向的PDT。拆一下该线性地址,我们可以得到如下结果:

PDI -- 11 0000 0000
PTI -- 11 0000 0000
物理页偏移 -- 0x000

  为什么查询的后面加0xC00,而不是0x300呢。是因为PDEPTE的大小都是4个字节,乘上4后就是0xC00了。最后查看到,到最后内容又指向了自己。即0xC0300000存储的就是带PTE属性的CR3。我们根据当前所学的概念可以重新定义10-10-12分页的结构框图:

  同理,操作系统也需要页表基址。如果你老老实实地做了第一道题,你就知道我们找PDT的时候就已经发现了指向PTT的线性地址了,那就是0xC0000000。具体实验我就不再赘述了,最终我们得到下面的结构图:

  好了,我们思考一下有了0xC03000000xC0000000能做什么?如果我们掌握了这两个地址,就掌握了一个进程所有的物理内存读写权限。寻找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函数。

下一篇

  保护模式篇——PAE分页

posted @ 2021-10-16 15:53  寂静的羽夏  阅读(1462)  评论(0编辑  收藏  举报