zoukankan      html  css  js  c++  java
  • x86架构:从实模式进入保护模式

    一、先聊聊保护模式的基本概念:为什么叫保护模式?到底保护了啥?是怎么保护的?

     1、保护模式:要搞定出保护模式的意义,需要理解另一个概念:实模式;实模式下,存在以下重大的安全隐患:

         (1)内存没权限区别:用户程序可以访问任意内存,包括操作系统运行的内存、其他程序运行的内存(个人观点:这是万恶之源。病毒、木马、外挂都是想尽各种办法访问操作系统或其他进程的代码/数据,达到自己的某些特定目的),所有的代码和数据都是能查询和删改的,毫无隐私可言

         (2)用户程序可以随意访问和删除段寄存器,达到和上述同样的效果

         (3)对I/O端口无限制的访问,比如打开文件、发送网络数据包、打印到屏幕或分配内存等;

         (4)可以执行所有指令

      2、保护模式保护了啥? 本质上:

         (1)限制应用程序对内存、寄存器的访问,保护操作系统、其他进程的代码/数据不被恶意访问、甚至删改

         (2)限制用户程序对某些I/O端口的访问。

         (3)限制用户程序执行某些指令

      3、怎么保护的?

           (1)通过GDT/LDT/IDT表、CS/DS/SS等段寄存器,严格限制用户程序对内存的读写:访问内存前,CS中的CPL先和selector选择子中的RPL对比,数值小的再和GDT表的DPL比,如果数值小于等于,说明是有权限的,才能继续访问对应的内存!(这里多说几句: 64位的windows已经平坦了,进程访问的虚拟地址已经拉通,段寄存器名存实亡;进程之间隔离主要通过CR3,把不同进程同样的虚拟地址映射到不同的物理地址;换句话说,内存保护主要靠页表、MMU等隔离不同进程的物理内存

           

    附: 

      CPL:全称current privilege level,存放在代码段寄存器中(cs),代表当前执行程序的特权级;常见的调试器都能查到:一般都是11,即所谓的3环
           
      RPL:  全称request privilege level,请求特权级,存放在段选择子中。注意,不是段寄存器中,不是段寄存器中,不是段寄存器中;它的含义是当前我想以 RPL 这个级别来请求你把这个段选择子置入段寄存器。实际上 RPL 并没有什么用(个人观点,仅供参考),因为请求者任何时刻都可以让 RPL = 0。但是如果请求者是 CPL = 0 的程序,用 RPL = 3 的级别来请求 DPL = 0的数据段,必然会失败;
      DPL:  全称descriptor privilege level,存放在段描述符中,用于表示段的特权级

           (2)要想访问必须通过操作系统内核,比如打开文件、发送网络数据包、打印到屏幕或分配内存

         

    二、这里简化一下说说要点:

    1、 生成并加载GDT表

      实模式下任何进程可以无限制读写任何内存,甚至os的内存,毫无安全性可言;需要对用户进程读写内存的地址做严格限制,衍生出了保护模式;保护模式将内存分成不同的段,段基址、limit、各种属性存放在GDT表;用户程序读写段内存时需要先通过段寄存器的selector在GDT找到段描述符,查看是否有权限、偏移地址是否超过limit等。如果一切ok,可以继续读写段内数据;

      cs、ds、ss段的描述符:

        注意点:(1)用户程序运行在3环,是没有权限更改GDT的,所以这种方式完全可以限制3环程序对内存的读写;windows要想改GDT,要么连接windbg,要么写驱动;

                      (2)lgdt [cs:gdt_size],从操作数指向的地址取6字节,高4字节作为gdt的基址,低2字节作为gdt中描述符的个数,如下:

           

          这6字节早在编译时就确定了:

          

         (3)GDT的机制是CPU定的,操作系统负责运维和使用各段的数据 

    2、打开A20

         8086下,地址线有20位,寻址范围从0x00000~0xfffff,超过0xfffff的地址会被cpu重新从0x00000开始,相当于丢掉进位(或把地址对0xfffff取模)。80286以后,地址总线扩展到24位,为了访问0x100000~0x10FFEF之间的内存,而不是象8086/8088那样回绕到0,需要开启A20地址线,方法如下:

       mov dx,0x92                        ;南桥ICH芯片内的端口0x92
        in al,dx
        or al,0x02
        out dx,al                        ;打开A20

    3、开启保护模式

         CPU提供的控制寄存器CR0~CR3用于控制CPU的运行模式。CR0第一位(0位)是保护模式允许位(Protection Enable,PE),如果把这个位置为1,那么处理器将会进入保护模式,核心代码如下:

        cli                                ;关闭中断,但后面一直没打开
        
        mov eax,cr0
        or eax,0x01
        mov cr0,eax                        ;设置PE位,处理器进入保护模式

        注意:要先调用cli把if置0,关闭中断,避免给CR0赋值时被打断;

    4、清空段寄存器中的缓存和旧流水线指令

     (1)段寄存器实际上有96位,保护模式下的汇编指令只能操作位于低16位的selector,剩余80位中部分作为缓存(可用sreg命令查看段寄存器dh、dl的值,这些都是描述符的缓存),存储了段基址。只要段不改变,缓存就不会更新;但保护模式下不能直接用实模式的地址(权限不够、位数不对等),需要清空;

       (2)cpu和内存的速度差异很大,为了提高效率,cpu会提前预测并执行某些分支指令(幽灵漏洞就是这么来的,细节可参考B站的一个科普视频:https://www.bilibili.com/video/BV1eW411i7ZM?from=search&seid=2138940898008952062,这就是所谓的乱序执行,进入保护模式后这些指令的逻辑结果可能是有问题的,也要马上清除;

         清除各个段寄存器、乱序指令缓存的办法:马上调用jmp指令跳转,让cpu认为当前各种缓存已经失效,核心代码如下:

    ;保护模式
        jmp 0x0008:flush-$$                ;现在是在16位保护模式下,0x0008依然是段的选择子,而flush则是偏移地址
        [bits 32]
    flush:                                ;头部加了vstart=0x7c00,这里变成了0x7c85,所以上面的jmp中的偏移要减去开头的基址,得到偏移    
        mov cx,0x0010                    
        mov ds,cx

    5、完整代码:

    ;---------------------保护模式主引导扇区程序---------------------
    SECTION protectModel vstart=0x7c00 align=16    
        mov ax,0x00 
        mov ss,ax
        mov sp,0x7c00
        
        mov ax,[cs:gdt_base];ax=0x7e00
        mov dx,[cs:gdt_base+0x02];dx=0x0000;
        ;mov ax,[cs:gdt_base+0x7c00];这段已经被加载到0x7c00,所以需要加上;ax=0x7e00
        ;mov dx,[cs:gdt_base+0x7c00+0x02];dx=0x0000;
        mov bx,0x10
        div bx
        
        mov ds,ax                        ;得到base基地址,ds=0x7e0
        mov bx,dx                        ;得到偏移地址,这里是0x0000
    
    ;---------------------安装描述符---------------------
        ;描述符0
        mov dword [ebx+0x00],0x00        ;第一个描述符必须是0;这里默认是ds段,也就是gdt_base的基址
        mov dword [ebx+0x04],0x00        ;ebx是gdt表的偏移,ds是gdt的基址
        
        ;描述符1
        mov dword [ebx+0x08],0x7c0001FF
        mov dword [ebx+0x0c],0x00409800    ;基地址0x00007c00,段界限0x001FF,粒度是字节,
                                        ;长度是512字节,在内存中的32位段,特权级为0,只能执行的代码段
        ;描述符2
        mov dword [ebx+0x10],0x8000FFFF    
        mov dword [ebx+0x14],0x0040920B ;基地址0x000B8000,段界限0x0FFFF,粒度是字节,
                                        ;长度是64KB,在内存中的32位段,特权级为0,可以读写的向上拓展的数据段
        ;描述符3                                
        mov dword [ebx+0x18],0x00007A00    
        mov dword [ebx+0x1c],0x00409600 ;基地址0x00000000,段界限0x07A00,粒度是字节,
                                        ;在内存中的32位段,特权级为0,可以读写的向下拓展的栈段
                                        
        mov word [cs:gdt_size],31;写入GDT段界限,4个描述符是32个字节,所以界限就是31
        lgdt [cs:gdt_size]        ;load gdt
        ;mov word [cs:gdt_size+0x7c00],31;写入GDT段界限,4个描述符是32个字节,所以界限就是31
        ;lgdt [cs:gdt_size+0x7c00]        ;load gdt
        
        mov dx,0x92                        ;南桥ICH芯片内的端口0x92
        in al,dx
        or al,0x02
        out dx,al                        ;打开A20
        
        cli                                ;关闭中断,但后面一直没打开
        
        mov eax,cr0
        or eax,0x01
        mov cr0,eax                        ;设置PE位,处理器进入保护模式
        
        ;保护模式
        jmp 0x0008:flush-$$                ;现在是在16位保护模式下,0x0008依然是段的选择子,而flush则是偏移地址
        [bits 32]
    flush:                                ;头部加了vstart=0x7c00,这里变成了0x7c85,所以上面的jmp中的偏移要减去开头的基址,得到偏移    
        mov cx,0x0010                    
        mov ds,cx
        
        ;以下在屏幕上显示"Protect mode OK." 
        mov byte [0x00],'P'  
        mov byte [0x02],'r'
        mov byte [0x04],'o'
        mov byte [0x06],'t'
        mov byte [0x08],'e'
        mov byte [0x0a],'c'
        mov byte [0x0c],'t'
        mov byte [0x0e],' '
        mov byte [0x10],'m'
        mov byte [0x12],'o'
        mov byte [0x14],'d'
        mov byte [0x16],'e'
        mov byte [0x18],' '
        mov byte [0x1a],'O'
        mov byte [0x1c],'K'
             
        ;以下用简单的示例来帮助阐述32位保护模式下的堆栈操作 
         mov cx,00000000000_11_000B         ;加载堆栈段选择子
         mov ss,cx
         mov esp,0x7c00
    
         mov ebp,esp                        ;保存堆栈指针 
         push byte '.'                      ;压入立即数(字节)
         
         sub ebp,4
         cmp ebp,esp                        ;判断压入立即数时,ESP是否减4 
         jnz ghalt                          
         pop eax
         mov [0x1e],al                      ;显示句点 
          
      ghalt:     
             hlt                                ;已经禁止中断,将不会被唤醒 
    
    ;-------------------------------------------------------------------------------
         
        gdt_size    dw 0
        gdt_base    dd 0x00007e00        ;GDT的物理地址,主引导扇区是512个字节,这个地址刚好在主引导扇区之后 
                                 
        times 510-($-$$) db 0
                         db 0x55,0xaa

    说明:

    (1)整段代码被加载到0x7c00处,所以在开头额外加vstart=0x7c00,让nasm从这个地址开始编译;

        紧接着这4行代码也要更改:去掉0x7c00,因为gdt_base和gdt_size已经相对0x7c00计算偏移

      mov ax,[cs:gdt_base+0x7c00]
       mov dx,[cs:gdt_base+0x7c00+0x02]

      mov word [cs:gdt_size+0x7c00],31;写入GDT段界限,4个描述符是32个字节,所以界限就是31
    lgdt [cs:gdt_size+0x7c00]

    (2)flush也是相对0x7c00开始计算偏移的,具体偏移是0x7c85,远超代码段的limit,导致出错异常,又跳回biso启动处执行

    解决办法也简单,直接改成jmp 0x0008:flush-$$ 即可,让后面的偏移值相对于段开始的地方,这次对了,如下:
    
    

      (3)81行代码:push byte '.'  ,但nasm还是编译成push 0x0000002e, 估计是为了内存对齐

        (4)效果:在频幕上打印一行字

     参考:

    1、https://manybutfinite.com/post/cpu-rings-privilege-and-protection/  cpu运行级别和保护机制

    2、 https://www.cnblogs.com/Philip-Tell-Truth/p/5211248.html   

    3、x86汇编:从实模式到保护模式

    4、 https://blog.csdn.net/q1007729991/article/details/52727332  cpu特权等级

  • 相关阅读:
    Xcode代码块快捷输入
    Git常用命令
    vim
    MACOX中apache配置
    IOS中实现动画的几种方式
    Swift与OC混合编译
    网络图像加载
    我对互联网的理解
    运行时
    自动布局使用
  • 原文地址:https://www.cnblogs.com/theseventhson/p/13042476.html
Copyright © 2011-2022 走看看