特权级和特权级保护

xiaoeryu Lv5

1. 特权级保护的必要性和特权保护机制

用描述符实施段与段之间的隔离和保护,建立在程序之间分工协作的基础上,首先用户程序需要在内核的支持下运行而不能独立运行。

内核需要加载和重定位用户程序,为用户程序每个段创建描述符,将段选择子回填到用户程序的头部中,因为这个原因用户程序只能访问自己的代码段、数据段和栈段。

当然这样并不能有效地阻止用户程序访问GDT,如下代码:

        mov eax, 0x28		;00101_0_00
        mov ds, eax
        mov dword[0],012345678

用户程序虽然不知道5号描述符指向哪个段,但是仍然可以破坏段中的数据,甚至用户可以重新定义一个新的GDT来替换内核建立的GDT,从而达到破坏的目的。

用户程序只要知道内核中历程的段选择子段内偏移就可以调用例程执行,这样会很容易的破坏内核。

系统的多任务如下:

使用特权级来划分内核和用户程序,任务的共有部分和私有部分之间的隔离特权级从0~3

特权指令:只有0特权级的程序能够执行

02. 当前特权级CPL

特权级是以处理器的工作特点和工作方法来划分的。

处理器不知道当前执行的是哪个程序,但是可以知道是哪个段:因为要用到段寄存器

程序的特权级就是:组成这个程序的所有代码的特权级。

处理器正在执行哪个段,其特权级就是当前特权级CPL(Current Priviledge Level),也即是当前正在执行的程序的特权级。

那么在哪里体现CPL呢?段寄存器如下:

段选择器:低两位保存CPL

也有例外,如下:

         mov cr0,eax                        ;设置PE位
      
         ;以下进入保护模式... ...
         jmp dword 0x0010:flush             ;16位的描述符选择子:32位偏移
                                            ;清流水线并串行化处理器  

当然CR0PE位进入保护模式之后,处理器自动处于0特权级,但是这个特权级无法使用CS的段选择器来指示,因为这时候CS的段选择器中仍然保存着实模式下的逻辑段地址,而不是段选择子。之后在执行了jmp指令之后,CS才会被刷新,用来指示当前特权级。

在引入保护模式和特权级之后,实模式被赋予了新的内涵,实模式下的成刷始终是0特权级的,在进入保护模式之后,处理器是继承了实模式下的0特权级

03. 描述特权级DPL

描述特权级**DPL(Descriptor Privilege Level)**:

描述符特权级用来描述指定的实体的特权级,即描述符描述的是一个段,那么DPL就是这个段的特权级。对于一个正在执行的代码段,其CPL和当前的DPL是一致的。

程序在不同的代码段内执行,那么CPL就要等于目标代码段的DPL。即控制转移只能发生在两个特权级相同的代码段之间,
例如:CS指向代码段ACPL = 0代码段BDPL要等于0
才能从代码段A跳转到代码段B内执行。

当然也可以中一个低特权级的代码段转移到一个高特权级的代码段去执行,需要特殊的方法。但是无论如何都不能从一个高特权级的代码段转移到一个低特权级的代码段内执行。

访问数据时的特权级:

再当前代码段内访问数据段,那么当前代码段的特权级需要大于目标数据段的特权级,即CPL ≤ DPL。代码段特权级特别低的话表示其可靠性和安全性不高不允许其访问高特权级的数据段。

上一章代码中,从引导程序跳转执行内核程序时,两者特权级都是0,可以跳转。从内核程序跳转执行用户称刷时,两者特权级均为0,可以跳转。其中执行ldt、lldt这些只能再0特权级执行的指令也是合法的。

04. 任务公共部分和私有部分的特权级划分

一个任务由内核和用户程序共同组成,当内核为用户程序创建描述符时,将用户程序的特权级设置为3,内核的特权级设置为0。

在代码中:将我们之前建立程序头部段时设置的0特权级改为3

    ;建立程序头部段描述符
    mov eax,edi                        ;程序头部起始线性地址
    mov ebx,[edi+0x04]                 ;段长度
    dec ebx                            ;段界限
    ;mov ecx,0x00409200                 ;字节粒度的数据段描述符,特权级0
    mov ecx,0x0040F200                 ;字节粒度的数据段描述符,特权级3
    call sys_routine_seg_sel:make_seg_descriptor

同上,将代码段、数据段、栈段的特权级均改为3

但是只修改特权级的话程序将不能运行,因为当前内核的CPl = 0,用户程序DPL = 3,在任何情况下都是不允许jmp、call指令从高特权级向低特权级转移执行的。就算能进入用户程序,则当前的CPL = 3,在用户程序中去调用内核的例程也是不被允许的。

调试程序,在内核跳转执行用户程序时中断:

显示特权级检查没有通过。

05. 依从的代码段

处理器原则上不允许两个不同特权级的代码段转移执行,但是符合一定条件是可以的。

方法一:将目标代码段设置为依从的代码段,依从的代码段即使其特权级较高也可以从地特权代码段进入。

  • S位:为1表示储存器的段描述符
  • X位:为1表示代码段
  • C位:为0表示普通的代码段,为1表示依从的代码段,一个代码段是依从的表示可以种低特权级进入、但是不能从高特权级进入(当前代码段的CPL 依从代码段DPL

上图中右边CPL = 3可以转移到左边DPL = 2的代码段执行。转移之后程序是在CPL = 3的特权级上执行,而不是在依从代码段的DPL = 2特权级上去执行。

06. 门描述符和调用门

接上一节。

方法二:通过门实施转移

门描述符描述的是一些系统管理单元,比如描述一个任务、描述一个例程或子程序。

如果门描述符描述的是一个例程,就称之为调用门。

如果高特权级是依从的,可以从低特权级高特权级代码段转移执行。
若不是依从的,通过调用门也可以从低特权级高特权级代码段转移执行。

调用门格式:

  • S:为0表示系统描述符
  • TYPE:为1100表示调用门、描述的是一个例程
  • P:为0表示调用门无效、为1表示有效;
  • DPL:表示调用门本身的特权级
  • 高32位的0~4位:保存用栈传递的参数的个数最大能表示2^5 - 1= 31个。

调用门涉及三个部分:

  1. 当前代码段特权级CPL
  2. 调用门描述符的DPL
  3. 目标代码段描述符的DPL
    需要符合:数值上有目标代码段描述符的DPL ≤ 当前代码段特权级CPL ≤ 调用门描述符的DPL。
    比如这个需要CPL为1或者2的时候才能通过调用门
  • CALL指令通过调用门之后可以返回,通过之后处理器在目标代码段的特权级上执行,即CPL由低到高。
  • JMP指令通过调用门之后不可以返回,通过之后特权级不变还是在原来的特权级上执行。

调用一个例程时,可以使用寄存器和栈传递参数,调用者通过调用门之后将参数压栈,返回时从栈中返回参数。

但是通过调用门之后特权级可能改变当前特权级指令CPL,从低特权级变为高特权级。此时处理器要求栈也必须切换,从低特权级的栈切换到高特权级的栈,还要复制参数,为了防止栈出错调用门描述符中需要保存要传递参数的数量,保存在高32位的0~4中最多表示2^5 - 1= 31个。

07. 本章程序说明及特权级检查的时机

本章程序有:
主引导程序:c13_mbr0.asm
内核程序:c14_core2.asm
用户程序:c13_app1.asm

进入保护模式之后就要进行特权级指令检查,jmp far指令进入内核执行,需要进行特权级检查。

  1. 上图中下面两行不需要进行特权级检查,虽然需要访问段中数据,但是在访问前需要指定段的位置。
  2. 就如前两行代码,将一个段选择子传入段寄存器DS时,要检查当前特权级CPL是否高于等于目标数据段描述符的DPL,即数值上当前CPL ≤ 目标数据段DPL
  3. 若通过检查表示DS会被加载,那么后续的内存访问指令都是合法的,如果不通过检查,后续的指令就没有机会成功执行。
  4. 特权级检查的典型时机如下:其中特权指令只能在0特权级下执行。 本章程序可以从0特权级的内核进入3特权级的用户程序执行,也可以在3特权级的用户程序中使用0特权级的接口例程。

08. 请求特权级RPL

本章程序:

  1. 用户程序中CPL = 3,调用门中的DPL = 3,内核代码段的DPL = 0,满足条件可以执行转移。转移之后处理器以CPL = 0特权级执行
  2. 在内核的硬盘读写例程中将数据段的选择子传送给DS,需要进行特权级检查,由高特权级高特权级的代码段可以访问低特权级或同级的数据段,在本程序中硬盘读写例程中CPL = 0 ≤ 用户程序数据段的DPL = 3,即满足条件通过检查。 若用户程序通过调用门执行内核的硬盘的读写例程时传递的数据段选择子是内核数据段选择子,那么在将数据段选择子传递给DS时也可以通过检查,那么用户程序就可以通过调用门破坏内核数据段了。

说明只是依靠当前代码段特权级CPL目标数据段描述符特权级DPL进行特权级检查时不够充分的。

在这里要访问数据段的是用户程序,用户程序自己不能访问外部设备,它需要通过请求内核硬盘读写例程去访问一个数据段,在内核硬盘读写例程中当前CPL = 0,之前是3,说明请求者的信息被隐藏了。

如果能够恢复请求者身份,知道它是3级特权的用户程序,就能够知道不允许它去访问0特权级的内核数据段,将内核数据段选择子传递到DS的请求也就不可能执行,这个问题不能只靠处理器来解决。

在访问一个数据段之前,需要将段选择子传送到DS的段选择器,在进行这个操作时进行特权级检查
段选择子:

其中RPL(Request Privilege Level)表示请求者的特权级,那么检查如下:

对于RPL的检查已经内置到处理器中,由处理器固件完成的是一个例行的操作。因此当程序员意识到请求着不是当前程序或当前代码段,而是一个低特权级的程序,那么在硬盘读写例程中:

此时就可以检查出请求者的特权级RPL低于内核数据段特权级DPL,在数值上有RPL = 3 ≥ DPL = 0,不满足条件,指令终止处理器产生一个异常中断。

09. 请求特权级调整指令ARPL

在程序中:

;-------------------------------------------------------------------------------
    ;此例程用于说明如何通过请求特权级RPL解决因请求者身份与CPL不同而带来的安全问题
    read_hard_disk_with_gate:		;从硬盘读取一个逻辑扇区
                                    ;输入:PUSH 逻辑扇区号
                                    ;      PUSH 目标缓冲区所在段的选择子
                                    ;      PUSH 目标缓冲区在段内的偏移量
                                    ;返回:无
    push eax
    push ebx
    push ecx
    
    mov ax,[esp+0x10]				;获取调用者的CS
    arpl [esp+0x18],ax              ;将数据段选择子调整到真实的请求特权级别
    mov ds,[esp+0x18]               ;用真实的段选择子加载段寄存器DS
    
    mov eax,[esp+0x1c]              ;从栈中取得逻辑扇区号
    mov ebx,[esp+0x14]              ;从栈中取得缓冲区在段内的偏移量
    
    ;此部分的功能是读硬盘,并传送到缓冲区,予以省略。
    
    retf 12

假定已经为这个例程创建调用门,调用门的特权级是3,这个例程可以从特权级3的用户程序调用。

在CS的低2位就是进入当前例程前请求者的特权级,将其取出传送给数据段选择子的RPL字段,即可修改请求者的特权级。使用ARPL指令修改。

ARPL指令比较两个操作数的低2位,若目的操作数RPL < 源操作数RPL,则修改目的操作数的RPL,使其与源操作数RPL保持一致,同时标志寄存器的0标志位ZF = 1。否则不改变目的操作数的RPL,标志寄存器的0标志位ZF清零

代码最后,指令retf 12表示由被调用者来保持堆栈平衡,传入三个参数,一个参数4个字节,共12个字节。

10. 一般情况下的请求特权级设置

绝大多数时候,请求者就是当前代码段或者当前程序,此时只需要将段选择子的RPL设置成当前当前特权级CPL就可以了。
c13_mbr0.asm程序中:

         ;以下进入保护模式... ...
         jmp dword 0x0010:flush             ;16位的描述符选择子:32位偏移
                                            ;清流水线并串行化处理器  
  1. 此时当前特权级CPL为0,是从实模式继承来的;
  2. 请求特权级RPL位于选择子0x0010 = 0000000000010_0_00中,其中RPL = 0
  3. 转移的目标位置是初始代码段,其中DPL为00。 因此在此条指令执行时,CPL = RPL = DPL,可以通过特权级检查。

进入保护模式设置数据段:

    mov eax, 0x0008		;0x0008 = 0000000000001_0_00 
    mov ds, eax
  1. 此时CPL = 0
  2. 请求特权级在选择子0x0008,即请求特权级RPL = 0
  3. 目标数据段是4G字节数据段,其中描述符DPL = 0;
    能够通过特权级检查

接下来设置栈段:

    mov eax,0x0018                      ;加载堆栈段选择子 11:00011_0_00
    mov ss,eax
    xor esp,esp                         ;堆栈指针 <- 0 
  1. 此时CPL = 0;
  2. 请求特权级在选择子0x0018,即请求特权级RPL = 0;
  3. 目标数据段是栈段,其描述符中DPL = 0;
  4. 能够通过特权级检查

11. 为内核接口例程创建调用门

进入内核之后使用指令call sys_routing_seg_sel:put_string在内核中转移是允许的,因为内核公共例程段的特权级内核代码段的特权级是相同的,都是0特权级可以直接调用。但是用户程序的特权级是3,所以要为那些提供给用户程序使用的例程创建调用门。

在内核的核心数据段中定义了符号地址检索表,其中有例程的名字,例程所在段的段内偏移,例程所在段的段内选择子。现在分别为这些例程创建调用门,并且把它们例程所在段选择子改为调用门选择子

创建调用门的代码如下:

         ;以下开始安装为整个系统服务的调用门。特权级之间的控制转移必须使用门
         mov edi,salt                       ;C-SALT表的起始位置
         mov ecx,salt_items                 ;C-SALT表的条目数量
  .b3:
         push ecx
         mov eax,[edi+256]                  ;该条目入口点的32位偏移地址
         mov bx,[edi+260]                   ;该条目入口点的段选择子
         mov cx,1_11_0_1100_000_00000B      ;特权级3的调用门(3以上的特权级才
                                            ;允许访问),0个参数(因为用寄存器
                                            ;传递参数,而没有用栈)
         call sys_routine_seg_sel:make_gate_descriptor
         call sys_routine_seg_sel:set_up_gdt_descriptor
         mov [edi+260],cx                   ;将返回的门描述符选择子回填,此时默认RPL=0
         add edi,salt_item_len              ;指向下一个C-SALT条目
         pop ecx
         loop .b3

其中调用门描述符:

安装调用门之后的GDT布局:

12. 调用门测试和调用门转移过程

接上一节,本节对代码段进行测试:

    ;对门进行测试
    mov ebx,message_2
    call far [salt_1+256]				;通过门显示信息(偏移量将被忽略)
                                        ;此时DS指向内核数据段
    
    mov ebx,message_3
    call sys_routine_seg_sel:put_string ;在内核中调用例程不需要通过门
  1. DS是指向内核数据段,偏移salt_1+256指向一个地址,地址处存放的是一个偏移和一个选择子;
  2. 处理器使用段选择子到GDT中取出描述符,发现是一个调用门描述符,包含一个代码段选择子和段内偏移;
  3. 处理器将代码段选择子传送到CS的段选择器部分,将段内偏移传送到EIP;
  4. 处理器使用CS段选择器中的选择子访问GDT,将取出的描述符存放到CS描述符高速缓存器;
  5. 处理器使用CS描述符高速缓存器加上EIP中的偏移转移到目标例程开始执行

在这里salt_1+256地址处指定的偏移量和选择子中,只是使用了选择子部分。即再通过调用门实施控制转移时,在指令中提供的偏移量会被忽略。但是在指令中偏移量还是要加上。如下:

若0x0030指定的选择子是一个调用门,那么指令中指定的偏移量0x0000C000不会被使用,但是在书写时不能不写,但是可以写一个任意值。

通过调用门实施控制转移时的特权级检查

13. 通过调用门实施由低特权级到高特权级的转移

接上一节,本节具体看代码c14_core2.asm

其中用户程序调用内核例程和过程:

特权级检查:

14. 通过调用门转移控制时的栈切换过程

CALL指令通过调用门实施控制转移可以改变程序的当前特权级CPL。比如从用户程序的3特权级用call指令通过调用门进入内核程序后,当前的CPL也会变为0特权级。

问题在于通过调用门转移的时候,栈的切换时处理器自动进行的,那么处理器是如何知道应该切换到哪一个栈,又是如何知道栈在哪里的呢。

TSS中没有保存3特权级栈段选择子和栈指针时因为通过调用门转移时,是从低特权级转移到高特权级中,特权级3是最低的,不可能从更低的特权级转移到3特权级中了,所以TSS中也就不需要存储特权级3的栈段选择子和栈指针。

在用户程序中:

         mov eax,100                         ;逻辑扇区号100
         mov ebx,buffer                      ;缓冲区偏移地址
         call far [fs:ReadDiskData]          ;段间调用 

call far指令执行时,当前CPL = 3,目标例程位于内核的公共例程段,内核的公共例程段的特权级DPL = 0,因此进入目标例程时需要对栈进行切换。

切换之前的栈时用户程序自己的栈,若转移之前需要通过栈传递参数则存在用户程序自己的栈中,此处并未使用栈传递参数,而是通过寄存器传递参数。

  1. 一旦处理器发现特权级更改就要对栈进行切换,而目标代码段的特权级是0;
  2. 然后立即到当前任务的TSS中,取出特权级0的栈段选择子SS0、和栈指针EIP0
  3. 分别传送到栈段寄存器SS和栈指针寄存器ESP
  4. SS的段选择器部分发生改变后,处理器立即根据段选择子到GDT/LDT中取出描述符
  5. 将取出的描述符传送到SS描述符高速缓存器中,这样新的栈段就可以使用了。 切换到新栈之后,处理器立即压入旧栈的SS和ESP,这样做是为了当从调用门返回时,可以返回到用户程序原来自己的栈中。其中段选择子是16位的,压入时使用movsz指令0扩展至32位。接着处理器将旧栈中的参数传递到新栈中(参数个数记录在调用门描述符高32位的0~4)。因为在这个调用中使用的是寄存器传参,所以这里参数部分是没有的。

复制完参数之后,处理器再将控制转移前的CS和EIP压栈,这样做是为了能够返回到原来的程序,也就是调用者那里。

栈切换的过程是由处理器自动进行的,现在处理器就可以执行目标例程了。

当例程返回时:

从被调用程序的栈中返回SS、ESP、CS、EIP,如此一来就可以返回到原来的调用者那里去执行原来的程序,并切换回原来的旧栈。

15. 通过调用门转移控制并返回的完整描述

通过调用门转移控制并返回的全过程:
使用call far指令通过调用门转移控制时,如果改变了当前的特权级别则必须切换栈,即从当前任务的固有栈切换到切换到与目标代码段相同特权级别的栈上。

栈的切换是由处理器固件自动进行的,当前栈是由SS和ESP的当前内容指示的,要切换到的新栈位于当前任务的TSS中,处理器知道如何找到它。在栈切换前处理器要检查新栈是否有足够的空间完成本次控制转移。

控制转移和栈切换的过程如下:

如果调用门的控制转移是由jmp far指令发起的,则转移后不再返回,而且没有特权级的变化就不需要切换栈;
相反,如果调用门的控制转移是由call far指令发起的,那么可以使用远转移指令retf返回到调用者。

返回时,处理器从栈中弹出调用者的代码段选择子指令指针,不管是从相同的特权级还是从不同的特权级返回,为了安全处理器都会进行特权级检查,控制返回的全过程如下:

特权级检查不是在实际访问时进行的,而是在将段选择子带入寄存器时进行的,因此当控制低特权级的段通过调用门进入高特权级的段之后,假如高特权级的段使用指令mov ds, … 将一个高特权级的数据段选择子带入DS时,如果能够通过特权级检查时没有问题的。

在返回第特权级的段后,低特权级的程序依然能够使用指令mov [xx], xx来访问高特权级的数据段,而不会进行任何检查,这是很危险的。为了解决这个问题,在执行retf时处理器要检查数据段寄存器,根据他们找到相应的段描述符,要是有任意一个段描述符的DPL高于调用者的特权级也就是返回后的新CPL,那么处理器将会把数值0传送到该段寄存器的段选择器中(0是一个特殊的段选择子,处理器允许传入而且不会引发任何异常),但是后续使用这样的段选择器访问内存时一定会引发处理器异常中断。

TSS中的SS0、ESP0等是静态的,除非软件修改它们,处理是不会修改它们的。当处理器通过调用门进入特权级0的代码段时,处理器会使用SS0和ESP0切换到特权级0的栈段,在此之后栈指针可能会因为压栈出栈而改变,但是返回时并不会更新到TSS中的ESP0,下次通过调用门进入特权级0的代码段时,用的依然是静态的ESP0的值。

16. 创建0、1、2特权级的栈并登记在TSS中

上一节介绍了通过调用门转移控制和返回的全过程,据此知道必须要创建特权级为0、1、2的栈段,并登记在当前任务的任务状态段TSS中,对于每一个栈包括栈的线性基地址、栈段选择子、初始栈指针。先把这些信息保存到TCB最后再来填写TSS。

load_relocate_program程序中,处理完SALT之后,从栈中取得TCB的线性基地址,之后就可以创建0、1、2特权级的栈段了:

    mov esi,[ebp+11*4]                  ;从堆栈中取得TCB的基地址
    
    ;创建0特权级栈
    mov ecx,0                           ;以4KB为单位的栈段界限值
    mov [es:esi+0x1a],ecx               ;登记0特权级栈界限到TCB
    inc ecx
    shl ecx,12                          ;乘以4096,得到段大小
    push ecx
    call sys_routine_seg_sel:allocate_memory
    mov [es:esi+0x1e],ecx               ;登记0特权级栈基地址到TCB
    mov eax,ecx
    mov ebx,[es:esi+0x1a]               ;段长度(界限)
    mov ecx,0x00c09200                  ;4KB粒度,读写,特权级0
    call sys_routine_seg_sel:make_seg_descriptor
    mov ebx,esi                         ;TCB的基地址
    call fill_descriptor_in_ldt
    ;or cx,0000_0000_0000_0000          ;设置选择子的特权级为0
    mov [es:esi+0x22],cx                ;登记0特权级堆栈选择子到TCB
    pop dword [es:esi+0x24]             ;登记0特权级堆栈初始ESP到TCB
                                        ;对于一个向上扩展的栈;来说,初始栈指针
                                        ;应该设置成栈的总大小

创建0、1、2特权级栈之后任务控制块TCB的结构如下

创建完3个特权级栈之后,先创建LDT描述符,并将其安装在GDT中,之后创建任务状态段TSS

    ;在GDT中登记LDT描述符
    mov eax,[es:esi+0x0c]              ;LDT的起始线性地址
    movzx ebx,word [es:esi+0x0a]       ;LDT段界限
    mov ecx,0x00008200                 ;LDT描述符,特权级0
    call sys_routine_seg_sel:make_seg_descriptor
    call sys_routine_seg_sel:set_up_gdt_descriptor
    mov [es:esi+0x10],cx               ;登记LDT选择子到TCB中
    
    ;创建用户程序的TSS
    mov ecx,104                        ;tss的基本尺寸
    mov [es:esi+0x12],cx              
    dec word [es:esi+0x12]             ;登记TSS界限值到TCB 
    call sys_routine_seg_sel:allocate_memory
    mov [es:esi+0x14],ecx              ;登记TSS基地址到TCB

之后从TCB中取出0、1、2三个特权级的栈段选择子和栈指针,并填写到TSS中:

    ;登记基本的TSS表格内容
    mov edx,[es:esi+0x24]              ;登记0特权级栈初始ESP
    mov [es:ecx+4],edx                 ;到TSS中
    
    mov dx,[es:esi+0x22]               ;登记0特权级栈段选择子
    mov [es:ecx+8],dx                  ;到TSS中
    
    mov edx,[es:esi+0x32]              ;登记1特权级栈初始ESP
    mov [es:ecx+12],edx                ;到TSS中
    
    mov dx,[es:esi+0x30]               ;登记1特权级栈段选择子
    mov [es:ecx+16],dx                 ;到TSS中
    
    mov edx,[es:esi+0x40]              ;登记2特权级栈初始ESP
    mov [es:ecx+20],edx                ;到TSS中
    
    mov dx,[es:esi+0x3e]               ;登记2特权级栈段选择子
    mov [es:ecx+24],dx                 ;到TSS中
    
    mov dx,[es:esi+0x10]               ;登记任务的LDT选择子
    mov [es:ecx+96],dx                 ;到TSS中
    
    mov word [es:ecx+100],0            ;T=0

之后就是创建TSS描述符,并在GDT中登记TSS描述符:

    ;在GDT中登记TSS描述符
    mov eax,[es:esi+0x14]              ;TSS的起始线性地址
    movzx ebx,word [es:esi+0x12]       ;段长度(界限)
    mov ecx,0x00008900                 ;TSS描述符,特权级0
    call sys_routine_seg_sel:make_seg_descriptor
    call sys_routine_seg_sel:set_up_gdt_descriptor
    mov [es:esi+0x18],cx               ;登记TSS选择子到TCB

之后就是ret 8指令返回到调用者。

17. 通过模拟调用门返回进入用户程序执行

接上一节,返回到内核start调用者之后,加载任务寄存器TR以及局部描述符表寄存器LDTR

    ltr [ecx+0x18]		;加载任务状态段
    lldt [ecx+0x10]		;加载LDT

这两条指令执行之后表明当前正在一个任务中执行,现在是在任务的全局部分执行,并且应该转移到任务的私有部分(用户程序)执行。

在用户程序中每个段的描述符特权级DPL = 3,以前使用jmp指令完成转移,但是当前程序特权级CPL = 0,从高特权级使用jmpcall指令转移到低特权级时不被允许的。

通过模拟从调用门返回进入低特权级的用户程序中执行,在栈中压入以下部分:

代码如下:

    mov ds,[ecx+0x44]		;切换到用户程序头部段
                            ;使用DS取得数据,之后再回头修改DS
    
    ;以下假装是从调用门返回。摹仿处理器压入返回参数
    push dword [0x1c]		;调用前的堆栈段选择子
    push dword 0			;调用前的esp
    
    push dword [0x0c]		;调用前的代码段选择子
    push dword [0x08]		;调用前的eip
    
    retf

之后进入用户程序执行,执行完成之后返回:

        jmp far [fs:TerminatePorgram]			;将控制权返回到系统

但是jmp执行时,CPL = 3,DPL = 0,条件不成立,不能使jmp指令返回,这里改成call far即可,不过call指令会在栈中压入返回地址。不过在用户程序终止后,其占用的资源都会被回收,包括栈段,这种情况下压入数据和不压入数据是一样的。

Virtual Box运行:

  • 标题: 特权级和特权级保护
  • 作者: xiaoeryu
  • 创建于 : 2022-12-11 22:46:00
  • 更新于 : 2023-10-03 13:00:06
  • 链接: https://github.com/xiaoeryu/2022/12/11/28-特权级和特权级保护/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论