保护模式篇——段描述符与段选择子

写在前面

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

你如果是从中间插过来看的,请仔细阅读 羽夏看Win系统内核——简述 ,方便学习本教程。

  看此教程之前,问几个问题,基础知识储备好了吗?搭建好环境了吗?上一节教程学会了吗?没有的话就不要继续了。


🔒 华丽的分割线 🔒


CPU分级

  如果要讲段描述符与段选择子,先介绍CPU分级的概念。数值上越小,权限越大。如果低权限访问高权限的东西,会导致失败。0环被内核使用,虽然1环2环存在,但Windows只用了3环注意在学习保护模式是时候不要把操作系统的概念扯进去,还没到操作系统层面。 CPU分级示意图如下:

GDT 与 LDT

  GDT是全局描述符表。LDT为局部描述符表,但Windows并没有使用它,故不再介绍,感兴趣请查询Intel白皮书。当我们执行类似MOV DS,AX指令时,CPU会查表,根据AX的值来决定查找GDT还是LDT,并找到对应的段描述符。段描述符将会在后面部分进行介绍。

  GDT表存在于内存之中。CPU要想找到它,就必须知道它的位置。于是乎CPU有一个寄存器。它被称之为GDTR,存储了GDT表的位置和大小,是一个48位的寄存器,用C语言表示如下:

struct GDTR
{
    DWORD GDTBase;    //GDT表的地址
    SHORT limit;      //GDT表的大小
}

  那么,我们如何通过WinDbg来获取GDT的地址、大小和GDT表里的成员呢?首先提一句,段描述符大小为64位。有关其操作见下图:

  r gdtr指令表示读取GDT表的地址;r gdtl指令表示读取GDT的大小;dq 8003f000 l20指令表示从0x8003f000地址(即为GDT表的地址)读取0x20个64位数据,如果没有l20,默认0x10个。这些都是以后常用的指令,需要熟练掌握。

段选择子

  段选择子结构简单,那我先介绍它。它是一个16位的描述符,指向了定义该段的段描述符(段描述符比较复杂,后面将会完整介绍)。段选择子结构如下图所示:

  它的成员解释如下:

  • RPL:请求特权级别,通俗的讲我用什么权限来请求。
  • TI:TI=0时,查GDT表;TI=1时,查LDT表。
  • Index:处理器将索引值乘以8在加上GDT或者LDT的基地址,就是要加载的段描述符。

  是不是很简单?该结构一定要牢记于心,后面将给出练习训练。

段描述符

  既然提到段描述符,那我来介绍一下它的结构如下图所示:

  段描述符有很多成员,它的成员将会在下面详细介绍,学习的时候一定要按照我介绍的顺序进行学习:

P位

  P = 1段描述符有效,P = 0段描述符无效。

Base

  Base被分成了三个部分,从图可知:Base的低16位被放到了段描述符的低四个字节,高16位被均分到段描述符的高四个字节的头和尾。把它们依次拼接起来就是完整的Base

Limit

  由图可知,把段描述符中所有的Limit拼接起来就只有20位。上一节教程说它有32位的Limit。那就是要看G位了。

G位

  如果G = 0,说明段描述符中的Limit的单位是字节,段长度Limit范围可从1B~1MB,即在20位的前面补3个0即可;如果G = 1,说明段描述符中的Limit的单位是字节为4KB,即段长度Limit范围可从4KB~4GB,在20位的后面补充FFF即可。举个例子,如果Limit拼接后的为FFFFF,如果G为0则为000FFFFF,反之为FFFFFFF

S位

  S = 1代码段或者数据段描述符,S = 0系统段描述符。

TYPE域

  TYPE域是比较复杂的成员,它表示的含义受S位的影响。

当S位为1时

  此时段描述符表示的是代码段或者数据段,如下图所示:

  对于表格中Type域的属性和含义,如下表格所示:

属性 含义 属性 含义
A 访问位 E 向下扩展位
R 可读位 W 可写位
C 一致位

  对于比较特殊的属性,我们将进一步介绍:

C位

  C = 1:一致代码段;C = 0:非一致代码段。什么是一致代码段,什么是非一致代码段,将在后面的教程进行介绍。

E位

  什么是向下拓展位,我们以fs为例来看一下如下示意图:

  左边表示向上拓展,右边是向下拓展。即向上拓展basebase+limit之间区域有效,其余无效;向下拓展basebase+limit之间的区域无效,其余有效。这个位针对数据段有效。

当S位为0时

  此时段描述符表示的是系统段,系统段有很多种,将会在后面的教程进行详细讲解。Type域每一个数值的含义如下图所示:

DB位

  DB位对不同的段具有不同的影响,情况如下:

1️⃣ 对CS段的影响
  D = 1采用32位寻址方式,D = 0采用16位寻址方式。

2️⃣ 对SS段的影响
  D = 1隐式堆栈访问指令(如:PUSH POP CALL)使用32位堆栈指针寄存器ESPD = 0隐式堆栈访问指令(如:PUSH POP CALL)使用16位堆栈指针寄存器SP

3️⃣ 向下拓展的数据段
  D = 1段上线为4GBD = 0段上线为64KB。至于是什么意思,我们来看下面一张图。

  红色表示向下拓展能寻址的范围。可以看出,如果D = 0,就算原来能寻址4GB,因为DB位的限制导致最大范围是64KB

DPL

  DPL(Descriptor Privilege Level),即描述符特权级别,规定了访问该段所需要的特权级别是什么。如果通俗的理解,就是:如果你想访问我,那么你应该具备什么权限

AVL

  AVL指示是否可供系统软件使用,由操作系统来使用,CPU并不使用它。

加载段描述符至段寄存器

  除了MOV指令,我们还可以使用LESLSSLDSLFSLGS指令修改寄存器。CS不能通过上述的指令进行修改,CS为代码段,CS的改变会导致EIP的改变,要改CS,必须要保证CSEIP同时改,后面会讲解。

  给一个代码模板,具体如下,作为本节的一个作业。

char buffer[6];
//自己构造一个段选择子
//自己自行赋值,你构造的的段描述符位置不一样,段选择子就不一样

_asm
{
    les ecx,fword ptr ds:[buffer] //高2个字节给es,低四个字节给ecx
}

本节练习

本节的答案将会在下一节进行讲解,务必把本节练习做完后看下一个讲解内容。不要偷懒,实验是学习本教程的捷径。

  俗话说得好,光说不练假把式,如下是本节相关的练习。如果练习没做好,就不要看下一节教程了,越到后面,不做练习的话容易夹生了,开始还明白,后来就真的一点都不明白了。本节练习比较多,请保质保量的完成,不得使用任何拆分工具

如下是从虚拟机读取的GDT表的前18个段描述符,下面的实验均按照此进行练习。

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  80008955`22000068 80008955`22680068
8003f060  00009302`2f40ffff 0000920b`80003fff
8003f070  ff0092ff`700003ff 80009a40`0000ffff
8003f080  80009240`0000ffff 00009200`00000000

1️⃣ 练习读取GDT表的位置和长度,并显示GDT表前48个段描述符。

2️⃣ 在给定的段描述符中,进行拆分练习(至少10个)。

3️⃣ 拆分如下段选择子。

002B 0023 0010 001B 003B

4️⃣ 快速辨别给定段描述符是否可用以及段基址、段长(至少10个)。

5️⃣ 从给定段描述符,请按照下面的要求进行练习(全部):

  1. 快速找出所有数据段,并分析该段属性:只读、已访问、可读可写、拓展方向
  2. 快速找出所有代码段,并分析该段属性:只执行、可读可执行、已访问、一致代码
  3. 快速找出所有系统段,并分析属性

6️⃣ 自行构造段选择子和段描述符,并用加载段描述符至段寄存器中的代码模板和要求取得成功。如果有时间同样把LSSLDSLFSLGS的实验也类比做了。

7️⃣ 如何在调试器中快速判断程序在几环权限。

8️⃣ 自学修改GDT表的相关知识,并思考如下问题。

r gdtr
dq 8003f090 00cffb00`0000ffff
r gdtr

  8003f090是GDT表中的一个段描述符的地址,更改后发现并没有更改,请思考为什么会这样。

下一篇

  保护模式篇——段权限检查与代码跨段跳转

posted @ 2021-09-20 17:43  寂静的羽夏  阅读(8001)  评论(0编辑  收藏  举报