前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >实战特权级间的跳转 -- 原理篇

实战特权级间的跳转 -- 原理篇

作者头像
用户3147702
发布2022-06-27 14:21:30
5760
发布2022-06-27 14:21:30
举报
文章被收录于专栏:小脑斧科技博客

1. 引言

经过多篇文章的介绍,我们实现了从实地址模式跳转到保护模式,并在 IA-32 硬件系统中实现了代码的编写与执行。 进军保护模式 保护模式进阶 — 再回实模式

此前的文章中,我们对保护模式特权级进行了简要的介绍,本文我们来切实的看看特权级在程序中是如何实现对内存和代码的保护的,我们又要如何在不同特权级间相互跳转。

2. 特权级

如图所示,IA-32 CPU 将内存分为 4 个特权级,数字越大,级别越小。 他的出发点是为了保护核心代码和数据,让处于低级别的应用无法访问和修改高级别的内存。 这一原则是通过不同位置的三个字段来实现的。

2.1. 当前特权级 CPL(Current Privilege Level)

他被存储在 cs 寄存器与 ss 寄存器的第 0 位和第 1 位上。 通常情况下,CPL 等于当前运行的代码所在内存段的特权级,每当程序发生跳转,处理器会自动改变 CPL 的值。

2.2. 描述符特权级 DPL(Descriptor Privilege Level)

在 LDT/GDT 描述符的属性字段里,有另一个特权级字段 DPL,他定义了 GDT/LDT 中对应对应内存段的特权级。 当代码要去访问一个内存段时,操作系统会将目标段的 DPL 与当前代码的 CPL 相比较,来决定当前代码是否可以访问目标段。

2.3. 访问特权级 RPL(Requested Privilege Level)

RPL 是位于段选择子中的特权级字段,它位于段选择子的第 0 位与第 1 位,用于在程序跳转中动态决定权限与 CPL,它实现了多次访问相同段但使用不同特权级的功能。

3. 特权级在不同段中的作用

3.1. 数据段与堆栈段

数据段与堆栈段在特权级的使用上规则非常简单,只有 CPL、RPL 都小于等于数据段的 DPL 时,才允许程序访问该数据段。 例如,我们将先前的程序中,数据段描述符的属性字段 + 60h,将原有的 DPL 0 修改为 1:

代码语言:javascript
复制
LABEL_DESC_DATA: Descriptor 0, DataLen-1, 92h + 20h

然后,我们将 RPL 修改为 3:

代码语言:javascript
复制
SelectorData equ LABEL_DESC_DATA - LABEL_GDT + 3

启动虚拟机,执行程序,我们看到程序运行崩溃了,通过查看日志,可以看到:

00073417874e[CPU0 ] load_seg_reg(DS, 0x0023): RPL & CPL must be <= DPL

这就是一般保护异常。

4. 一致代码段与非一致代码段

在介绍描述符属性时,我们已经介绍过一致代码段与非一致代码段,是通过段描述符的一致属性位来决定的,一致位为 0 表示是非一致代码段,否则为一致代码段。 如果程序要通过 jmp、call 等方式跳转到非一致代码段,当前的 CPL 必须等于目标代码段的 DPL,且 RPL 小于等于 DPL。 如果跳转目标是一致代码段,CPL 则必须大于等于 DPL,RPL 不作检查,跳转后,CPL 仍然维持原来的权限值,不会进行更新。

通常,大部分程序都被放置在非一致代码段中,防止特权级不同的程序访问,而供应用程序调用的系统调用则位于一致代码段中,虽然低特权级的应用程序可以调用,但低特权级的程序仍然维持原来的 CPL 不变,不会因此转变其特权级,达到仅供临时调用的目的。 可以看到,在代码段的访问上,限制是比较多的,完全无法实现在不同特权级的代码间自由跳转,那么,在某些情况下,我们确实需要从不同特权级的程序中跳转到目标非一致代码段,是否有办法来解决呢? 当然是有的,通过调用门就可以实现了。

5. 门描述符

5.1. 分类

门描述符可以实现不同特权级间的程序跳转,主要有下面四种门:

  1. 调用门
  2. 中断门
  3. 陷阱门
  4. 任务门

5.2. 包含的信息

本文我们主要来介绍调用门,门描述符描述了一次跳转所需要的各种信息:

  1. 需要访问的代码段
  2. 指定代码的入口地址
  3. 调用者所需的特权级
  4. 发生栈切换时拷贝的参数数量
  5. push 都按目标栈的字段大小
  6. 所描述的门是否有效

5.3. 结构

如图所示,由于历史原因,描述目标代码入口偏移地址的 4 个字节被拆到了门描述符的前两个和后两个字节,byte2、byte3 则写入了段选择子,剩下的两个字节则定义了一些属性与参数数量等信息。 门描述符的属性与段描述符的属性是一致的:

  1. P 位 — 存在位,P=1 表示段在内存中存在,P=0 表示段不在内存中
  2. DPL — 描述符特权级,可以是 0、1、2 或 3,数字越小,特权级越大
  3. S 位 — S=1 表示该段为数据段/代码段描述符,S=0 表示该段为系统段/门描述符
  4. TYPE — 共有 4 位,由低到高分别是: 访问位、读写位、一致位、执行位,分别表示是否已访问、是否可写、是否是一致代码段、是否可执行

6. 调用门的使用

通过上面门描述符结构的展示,我们可以看到,调用门描述符是由段选择子、目标代码偏移地址以及门属性、参数数量四部分构成的。 段选择子和目标代码偏移地址共同确定了要跳转到的目标地址的内存物理地址,而属性的添加则让这次跳转有了更多的特定功能。 当代码需要经过调用门跳转到目标代码时,首先需要检测当前 CPL、RPL 必须小于等于门描述符特权级 DPL。 接着,需要检查当前 CPL 与跳转的目标代码段的 DPL,通过 call 与 jmp 调用又有所不同:

  • call — 当前 CPL 必须 >= 目标代码段的 DPL
  • jmp — 如果目标代码段为一致代码段,CPL 必须 >= 目标代码段的 DPL,否则,CPL 必须等于目标代码段的 DPL

通过上述的描述可以看到,调用门可以让我们通过 call 指令调用高特权级的非一致代码段,其他情况下,使用调用门来调用与直接调用并没有明显的区别。

7. 栈切换与 TSS

7.1. 栈切换

通过系统为某段代码预先设置的调用门,低特权级的应用程序得以能够调用高特权级的代码段,但这仍然存在着另一个问题,那就是对栈空间的保护问题。 当低特权级的程序通过调用门跳转到高特权级的代码段中去,如果他也随之使用高特权级的堆栈,那么低特权级的程序就可以通过栈顶与栈指针的地址轻松实现对栈内内存的访问和修改,这是极不安全的,因此,通过调用门调用目标代码段的一个重要步骤就是栈切换。 为了安全,每个任务可以最多定义四个栈,分别对应 Ring0 到 Ring3,任务只能使用当前特权级对应的堆栈,一旦进行了特权级切换,栈也会被系统进行自动切换。 要实现自动切换的功能,系统必须预先知道每个栈的基地址与栈顶偏移,这就是任务状态段(Task State Segment)TSS 所描述的。 在栈空间自动切换时,会根据门描述符的 Param Count 参数决定将原栈空间多少个参数复制到新的栈空间。

7.2. TSS

TSS 是位于内存中任意位置的一个内存段,并由一个 GDT 描述符来描述,由任务寄存器 TR 保存 TSS 段的段选择子,通过 LTR 指令可以实现 TR 寄存器内容的加载。 TSS 存储了:

  • 处理器寄存器状态
  • IO 端口权限
  • 内部堆栈指针
  • 先前任务对应 TSS 的段选择子

其结构如下:

TSS 中定义了跳转前各个寄存器的值,主要用于硬件中断等硬件任务切换场景的现场保护,windows、linux 等现代操作系统中只通过软件实现任务切换,则不需要使用这些字段。 同时,TSS 还定义了三对 SS、ESP 的组合,分别对应从 Ring0 到 Ring2 特权级所使用的堆栈,因为 CPU 不允许从 0、1、2 特权级转移到特权级 3,因此,无需为特权级 3 保存 SS、ESP 寄存器的值,在进行长跳转前,特权级 3 使用的 ss 与 esp 寄存器的组合会被压入目标栈中,通过 retf 返回时,会自动还原到原特权级 3 的堆栈。 所谓的长跳转,就是此前我们在代码中已经编写过的,指定选择子的跳转,如 call selector:offset,他实现了两个代码段之间的跳转,与此相对,只指定段偏移的段内跳转则被称为短跳转,长跳转与短跳转最大的不同在于,除了压栈堆栈段指针寄存器 esp 的值外,还会压栈堆栈段基址寄存器 ss,最终,长跳转通过 retf 返回,短跳转则通过 ret 返回。

8. 利用长跳转从 Ring0 进入 Ring3

接下来要解决一个问题,那就是如何从程序开始时的 Ring0 跳转到 Ring3 特权级的程序,这样我们才能通过上述介绍的调用门、TSS 尝试低特权级的 Ring3 跳转到 Ring0。 通过上述介绍,从 Ring3 跳转到 Ring0,需要经过一次长跳转,相应的,只要从这一次长跳转中返回就可以实现从 Ring0 跳转到 Ring3。 而返回的过程,就是从当前栈中弹出长跳转时压入的 ss、esp、cs、eip 四个寄存器值更新到相应的寄存器,并跳转到 cs、eip 指向的代码位置。 利用这个原理,我们先通过将 ss、esp、cs、eip 先后进行压栈,然后再通过 retf 长返回就可以跳转到 cs 所存储的 Ring3 选择子指向的代码了。

9. 后记

本文,我们已经将调用门、TSS 等原理介绍的十分清楚了,代码已经呼之欲出,敬请关注下一篇文章,让我们来实战从 Ring3 到 Ring0。

10. 参考资料

https://en.wikipedia.org/wiki/Call\_gate\_(Intel) https://en.wikipedia.org/wiki/Task\_state\_segment。 https://xem.github.io/minix86/manual/intel-x86-and-64-manual-vol3/o\_fe12b1e2a880e0ce-246.html。 https://www.cnblogs.com/chenwb89/p/operating\_system\_004.html

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-02-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小脑斧科技博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 引言
  • 2. 特权级
    • 2.1. 当前特权级 CPL(Current Privilege Level)
      • 2.2. 描述符特权级 DPL(Descriptor Privilege Level)
        • 2.3. 访问特权级 RPL(Requested Privilege Level)
        • 3. 特权级在不同段中的作用
          • 3.1. 数据段与堆栈段
          • 4. 一致代码段与非一致代码段
          • 5. 门描述符
            • 5.1. 分类
              • 5.2. 包含的信息
                • 5.3. 结构
                • 6. 调用门的使用
                • 7. 栈切换与 TSS
                  • 7.1. 栈切换
                    • 7.2. TSS
                    • 8. 利用长跳转从 Ring0 进入 Ring3
                    • 9. 后记
                    • 10. 参考资料
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档