二进制入门学习笔记-16.保护模式-段机制

1.保护模式简介

1.1 保护模式简介

1.1.1 x86的cpu主要有三种运行模式

  • 实模式:具有20 bit分段内存地址空间,对所有可寻址内存、I/O地址和外设硬件可以无限制直接软件访问。
  • 保护模式:和实模式相比最大的区别就是有了内存保护多任务处理、或代码权限级别,并且寻址空间达到了64TB。保护模式具有两种非常重要的机制,段机制页机制
  • 虚拟8086模式:一种为了解决实模式下cpu工作不饱和的模式

2.段机制

2.1 段寄存器

2.1.1 段寄存器简介

在之前学汇编语言内容的时候其实已经接触到了段寄存器,比如如下的代码:

mov dword ptr ds:[0x401111],eax

这段汇编代码的意思是将eax中的值,赋给ds.base+401111这个地址。这个ds就是一个所谓的段寄存器。段寄存器长度为96位,其中16位可见,80位不可见。神奇的是段寄存器读的时候是读了16位,但是写的时候是写96位!!!例如在如下代码中,虽然看起来像是把一个16位的AX存放到了DS寄存器里。但是其实DS在被写的时候是写了96位。

mov DS,AX

段寄存器的结构可以参考如下结构体:

struct SegMent{
    WORD Selector;  //16位,可见;
    WORD Atrributes; //16位,不可见 ; 用于表示读写执行权限
    DWORD BASE;  //32位,不可见 ; 用于表示当前段的基址
    DWORD Limit;  //32位,不可见 ; 用于表示当前段的长度
}

段寄存器有如下八个:

寄存器名称 Selector Atrributes BASE Limit
ES 0023 可读、可写 0 0xFFFFFFFF
CS 001B 可读、可执行 0 0xFFFFFFFF
SS 0023 可读、可写 0 0xFFFFFFFF
DS 0023 可读、可写 0 0xFFFFFFFF
FS 003B 可读、可写 0x7FFDE000 0xFFF
GS - - - -
LDTR - - - -
TR - - - -

2.1.2 段描述符与段选择

当执行mov DS,AX这样的指令时,有80位来源不明的数据。这80位来源不明的数据,是根据AX的值,来查GDT表或者LDT表从而决定的。windows系统主要查的是GDT表,LDT表windows系统没有使用。

2.1.2.1 GDT(全局描述符表)

GDT表的位置存储在gdtr寄存器中。gdtr中存储了GDT表的开始位置和长度。我们可以使用如下命令查看gdtr表的位置。

查看gdt表起始位置
r gdtr
查看gdt表长度
r gdtl

查看gdt表中的内容可以使用如下指令,其中dd中的d表示dword,dq中的q表示qword:

dd gdt表位置
或者
dq gdt表位置

2.1.2.2 段描述符

GDT表当中所储存的元素就是所谓的段描述符。每一个段描述符大小都是8个字节。为了方便分析可以使用dq指令查看段描述符的时候,高位在前面,低位在后面。

段描述符的通用结构如下图所示。

之前都说段寄存器赋值的时候都是要根据段选择子去GDT表中查对应的描述符然后来填充那个来历不明的80位。上图当中反映了段描述符和段寄存器不可见80位直接的关系。其中:

  • Atrributes:对应从高4个字节的第8位到第23位
  • BASE:对应如图中粉红色的三部分组成
  • Limit:对应图中蓝色20位的limit扩展出来的

其中黄色部分位参数,以下为部分参数解释:

  • P位:为1表示段描述符有效,为0表示段描述符无效。
  • G位:从图中我们可以看出,Limit位一共只有16+4=20位,显然比段寄存器中的limit少了12位。G位决定了limit如何扩充出12位,如果G位为0,在前面填充三个0。如果G位为1,在后填充三个F。
  • S位:为1表示代码段或者数据段描述符,为0表示系统段描述符。
  • Type域

    • 当S位为1时:是代码或数据段描述符。此时到底是代码段还是数据段要根据Type域来进行判断。具体判断方式可以参考下图。E表示拓展位,为0是向上拓展,也就是从base到base+limit是有效区域。为1是向下拓展,表示除了base到base+limit是有效区域。W表示可写。A表示是否访问过。R表示可读位。C为一致位,C=1表示一致代码段,C=0表示非一致代码段。
    • 当S位为0时:是系统段描述符。Type域含义如下图所示。
  • D/B位对以下三个段有影响:

    • 情况一,对CS段的影响: D/B位为1采用32位寻址方式,D/B位为0采用16位寻址方式。
    • 情况二,对SS段的影响: D/B位为1时隐式堆栈访问指令(如pop,push,call)使用32位寄存器ESP,D/B位为0时隐式堆栈访问指令(如pop,push,call)使用16位寄存器SP。
    • 情况三,对向下拓展的影响: D/B位为1上限为4GB,D/B位为0上限为64KB,也就是说最大的地址寻址范围也就只有64K。当然如果D/B位为0的话,也就是实模式了,之前的各种属性也都不适用了。

2.1.2.2 段选择子

段选择子是一个十六位的数,这个数指向了定义该段的段描述符。其实前面段寄存器里面可见的16位就是这个段选择子。段选择子的结构如下图所示: 除了mov指令以外,能够用来修改段寄存器的指令还有如下几个:

  • LES
  • LSS
  • LDS
  • LFS
  • LGS

以LES为例,使用方法如下:

char buffer[6];
__asm{
    les ecx,fword ptr ds:[buffer] //高两个字节给es,低四个字节给ecx。
    //注意保证所给选择子的RPL要小于等于其index所指定的段描述符的DPL
}

2.2 段权限检查

2.2.1 CPL(Current Privilege Level)

CPL(Current Privilege Level)是当前进程的特权级别。位于CS和SS段选择子的后两位(二者永远保持一致)。如下图中,CS段选择子为001B。二进制表示为0000 0000 0001 1011。可以看到后两位为3。则当前进程的特权级别为3,是一个Ring3程序。 在windbg中对系统下断,查看cs段寄存器,可以发现最后两位为0。显然当前程序是一个Ring0级别的程序。

2.2.3 DPL(Descriptor Privilege Level)

DPL(Descriptor Privilege Level)是描述符特权级别,存储在段描述符中,规定了访问该段所需要的特权级别。例如,在如下代码中

mov DS,AX

如果在当前程序的CPL=3,AX段选择子所指向的段描述符的DPL=0那么就无法访问。

2.2.4 RPL(Request Privilege Level)

RPL在段选择子的最后两位。这个RPL是我们自己可以随便指定的。可以参考上文段选择子结构图。

2.2.5 数据段的段权限检查

数据段中,段权限的检查和代码段以及系统段中是不一样的。数据段权限检查需要权限满足以下要求方可访问所需数据段。

RPL<=DPL && CPL<=DPL

2.3 代码跨段跳转

2.3.1 代码跨段跳转流程

前面学习的时候讲过LESLSSLDSLFSLGS可以帮助我们修改段寄存器。但是这里面却没有LCS这样的指令。主要是因为CS代码段CS的改变也意味着EIP的改变。同时修改CS和EIP的指令又如下几项:

  • JMP FAR
  • CALL FAR
  • RETF
  • INT
  • IRETED

本节以JMP FAR为例,分析CPU段间跳转的流程:

JMP FAR 0x20:0x004183D7
  • 1.首先拆分段选择子0x200x20的二进制形式为0000 0000 0010 0000。分析可得,RPL=0TI=0Index = 4

  • 2.查表得到段描述符

    • TI=0所以查GDT表
    • Index=4所以查对应段描述符
    • 只有下面四种情况可以跳转:代码段调用门(系统段描述符),TSS任务段(系统段描述符),任务门(系统段描述符)
  • 3.权限检查

    • 如果是非一致代码段,要求CPL==DPL,并且RPL<=DPL。
    • 如果是一致代码段,要求:CPL>=DPL。
  • 4.加载段描述符:CPU将段描述符加载到CS段寄存器中
  • 5.代码执行:CPU将新的段寄存器的Base+offset的值写到EIP。然后执行CS:EIP处的代码。段间跳转结束。

2.3.2 长调用(CALL FAR)

CALL FAR的指令格式为:

CALL CS:EIP

//EIP是废弃的可以随便填
//CS必须指向一个调用门

执行的步骤有如下几步:

  • 1.根据CS的值查GDT表,找到对应的段描述符,并且这个段描述符必须是一个门描述符
  • 2.找到调用门描述符中存储的另一个代码段的段选择子。
  • 3.选择子指向的段.base+偏移地址就是真正执行的地址。

其中门描述符的结构如下图所示:

2.3.2.1 长调用(跨段不提权)

CALL指令进行长调用时格式为CALL FAR CS:EIP(EIP是废弃的)。调用流程如下:

  • 1.先将调用者CS压入栈中
  • 2.压入返回地址
  • 3.执行完了ret的时候先把调用者CS的段选择子赋值给CS,然后再返回

2.3.2.2 长调用(跨段提权)

当进行长调用并且提权的时候,比如从R3切换到R0,堆栈空间会先切换到R0的堆栈空间,然后依次压入调用者的SS调用者的ESP调用者的CS返回地址

2.3.2.3 调用门实验

  • 1.主程序代码如下:
#include "stdafx.h"
#include <windows.h>

void __declspec(naked) GetRegister(){
    _asm
    {
        int 3
        retf
    }
}
int main(int argc, char* argv[])
{
    char buff[6];
    *(DWORD*)&buff[0] = 0x12345678;
    *(WORD*)&buff[4] = 0x48;
    _asm
    {
      call fword ptr[buff]
    }
    getchar();
    return 0;

}
  • 2.查看所需调用的代码地址

  • 3.在gdt表中自己新建一个段描述符

新建一个系统段描述符为0040EC00 00081020

  • 4.此时运行程序,就会发现,出现了一个系统断点在windbg里面

2.3.2.3 长调用实验(带参数)

代码如下:

#include "stdafx.h"
int x, y, z;
__declspec(naked) void getParam(int a, int b, int c) {
    __asm {
        //int 3
        pushad //寄存器压栈,其入栈顺序是:EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI。长度为0x20
        pushfd //将32位标志寄存器EFLAGS压入堆栈。长度为0x4
        mov eax, [esp+0x24+0x08+0x08] 
        mov x, eax
        mov eax, [esp+0x24+0x08+0x04] 
        mov y, eax
        mov eax, [esp+0x24+0x08+0x00] 
        mov z, eax
        popfd
        popad
        retf 0x0c
    }
}
int main(int argc, char* argv[])
{
    // 构造cs:eip
    char cs_eip[6] = {0, 0, 0, 0, 0x48, 0};
    __asm {
        push 1
        push 2
        push 3
        call fword ptr [cs_eip];
    }
    printf("%x %y %z \n", x, y, z);
    getchar();
    return 0;
}

带参数时主要注意入栈顺序,入栈顺序依次为SSESP参数1参数2...参数nCSEIP

2.3.3 IDT表

IDT表全称为Interrupt Descriptor Table,意为中断描述符表是CPU用来处理中断和程序异常的。IDT表当中主要描述三种门描述符,分别是任务门描述符中断门描述符陷阱门描述符。查看IDT表及其长度方法如下:

2.3.4 中断门

2.3.4.1 中断门简介

windows系统没有使用调用门,但是在从3环到0环时,用的中断门(老的CPU),新的没有使用。新的使用快速调用。中断门和调用门类似,但是中断门查的是IDT表。中断门的门描述符结构参考白皮书P2431页,其结构如下图所示,其中这个D表示Size of gate: 1 = 32 bits; 0 = 16 bits 从图中可以看出,中断门描述符S位为0,TYPE域为1110

2.3.4.2 中断门实验

  • 1.查看当前idt表,从表中可以发现0x20这个位置的描述符是无效的,因此替换这里的描述符。

  • 2.根据该地址,拼接出完整的中断门描述符。 此处的中断门描述符应该为 0040 ee00 0008 1030

  • 3.编写代码如下,调用中断门。运行代码,可以发现读取成功。

#include "stdafx.h"
#include <windows.h>
#include <stdio.h>

DWORD dwH2GValue;

void _declspec(naked)GetH2GValue()
{
    _asm{
        pushad  //寄存器压栈其入栈顺序是:EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI .
        pushfd  //将32位标志寄存器EFLAGS压入堆栈
        mov eax,[0x8003F00C]//读高两G内存的值
        mov ebx,[eax]
        mov dwH2GValue,ebx
        popfd
        popad
        iretd
    }
}

void PrintH2GValue()
{
    printf("%x\n", dwH2GValue);
}

int main()
{
    _asm{
        INT 0x20
    }
    PrintH2GValue();
    getchar();
    return 0;
}

2.3.4.2 中断门注意事项

IDT表中各种类型的门,都可以通过 int [index] 汇编指令进入。使用 int 指令进入中断门,会对栈产生一定的影响。如果从 3 环进入 0 环,首先会切换成 0 环栈在 0 环栈压入 ss3, esp3, eflags3, cs3, eip3

2.3.5 陷阱门

陷阱门和中断门几乎一模一样,唯一的区别是中断门在执行的时候会对EFLAGS寄存器产生影响,进入中断门会使得EFLAGS寄存器的第九位IF(Interrupt Flag)位置0。 IF(Interrupt Flag)位的作用是当接收到可屏蔽中断信号时,如果该位为0,则cpu将对该中断信号不理睬。陷阱门描述符结构如下图: 测试时只需要把中断门描述符中TYPE从e(1110)改成f(1111)即可。

2.3.6 任务段(TSS)

在调用中断门和陷阱门的时候,一旦出现权限切换,当前进程的CS和SS也一定会发生改变,并且会有新的ESP。其中CS是我们指定的,ESPSS其实是从TSS(Task-state segment)也就是任务状态段中得到的。TSS是一块内存,大小为104字节TSS最大的价值就是可以快速保存和恢复所有的寄存器,也就解释了前面从R3切换到R0新的ESP,SS是从哪里来的(直接从TSS里面查ESP0,SS0即可)。TSS结构如下图所示(手册P2489):

2.3.6.1 CPU如何找到TSS?

CPU找TSS时,首先查看TR寄存器,该寄存器中的值是在操作系统启动的时候从GDT表中加载出来的。查找过程如下图所示(手册P2494):

2.3.6.2 TSS段描述符

TSS段描述符是系统段中的一种。TSS段描述符特征为S位为0,TYPE域为1001(9)或1011(B)。其中TYPE的第三位的B表示是否已经加载到TR寄存器中。TSS段描述符的结构如下图所示(手册P2491)。 TSS段描述符指向的TSS结构如下:

typedef struct TSS {
    DWORD link; // 保存前一个 TSS 段选择子,使用 call 指令切换寄存器的时候由CPU填写。
    DWORD esp0;  //0 环栈指针
    DWORD ss0;  //0 环数据段选择子
    DWORD esp1; // 1 环栈指针
    DWORD ss1;  // 1 环数据段选择子
    DWORD esp2; // 2 环栈指针
    DWORD ss2;  // 2 环数据段选择子
    DWORD cr3;//这个地址需要实时查看来填
    DWORD eip;//跳转后的代码函数地址
    DWORD eflags;
    DWORD eax;
    DWORD ecx;
    DWORD edx;
    DWORD ebx;
    DWORD esp;
    DWORD ebp;
    DWORD esi;
    DWORD edi;
    DWORD es;
    DWORD cs;
    DWORD ss;
    DWORD ds;
    DWORD fs;
    DWORD gs;
    DWORD ldt;
    DWORD io_map;
} TSS;

2.3.6.3 TR段寄存器的读写

  • 1.将TSS段描述符加载到TR寄存器
    • 指令LTR
    • 注意:
      • a.) LTR仅仅是改变了TR寄存器的值,没有真正的改变TSS
      • b.) LTR执行只能在Ring0
      • c.) 加载后TSS段描述符的Busy flag会变为1
  • 2.读TR寄存器

    • 指令:STR
    • 注意:
      • STR只能读到TR的段选择子

2.3.7 任务门

任务门描述符存储在IDT表中,任务门描述符结构如下图所示(手册P2495): 其中Reserved区域为保留区域,构造时填0即可。TSS Segment Selector指向GDT表当中的一个任务段描述符。任务门的执行过程如下:

  • 1.INT N,其中N为任务门在IDT中的序号
  • 2.查IDT表找到任务门描述符
  • 3.通过任务门描述符,查GDT表,找到任务段描述符
  • 4.使用TSS段中的值修改寄存器
  • 5.IRETD返回

2.3.7.1 任务门实验

  • 1.编写一个测试用的入口函数,编译并获取该函数地址
DWORD test_esp;
__declspec(naked) TSSTest(){//00401020
    __asm{
        mov test_esp,esp
        iretd
    }
}
  • 2.构造TSS段
TSS tss = {//0x00427a30
        0x00000000,//link
        0x12345678,//esp0,自定义的ESP0地址
        0x00000010,//ss0
        0x00000000,//esp1
        0x00000000,//ss1
        0x00000000,//esp2
        0x00000000,//ss2
        0x00000000,//cr3
        0x00401020,//eip,测试入口函数的地址。
        0x00000000,//eflags
        0x00000000,//eax
        0x00000000,//ecx
        0x00000000,//edx
        0x00000000,//ebx
        0x12345678,//esp,自定义的ESP0
        0x00000000,//ebp
        0x00000000,//esi
        0x00000000,//edi
        0x00000023,//es  
        0x00000008,//cs  
        0x00000010,//ss
        0x00000023,//ds
        0x00000030,//fs
        0x00000000,//gs
        0x00000000,//ldt
        0x20ac0000 // 这个位置暂时忽略,填这个值就行了
};

  • 3.设计TSS段描述符,并将TSS段描述符安装到GDT表 根据TSS段描述符结构图,设计TSS段描述符:
eq 8003f048 0000e942`7a300068
  • 4.设计任务门,安装到IDT表 刚刚TSS段描述符安排在了8003f048这个位置,因此设计任务门描述符如下为0000e500 00480000。然后在IDT表里面找个空位安装一下。
eq 8003f500 0000e500`00480000
  • 5.读取cr3,并使用int指令进入任务门
int main(int argc, char* argv[])
{
    printf("input cr3:\n");
    &tss;
    scanf("%x", &(tss.cr3)); // 使用 !process 0 0查看 DirBase

    __asm {
        int 0x20
    }

    printf("test_esp = %x\n", test_esp);
    return 0;
}

完整代码如下:

// testTSS.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include "windows.h"
DWORD test_esp;
__declspec(naked) TSSTest(){//00401020
    __asm{
        mov test_esp,esp
        iretd
    }
}
typedef struct TSS {
    DWORD link; // 保存前一个 TSS 段选择子,使用 call 指令切换寄存器的时候由CPU填写。
    DWORD esp0;  //0 环栈指针
    DWORD ss0;  //0 环数据段选择子
    DWORD esp1; // 1 环栈指针
    DWORD ss1;  // 1 环数据段选择子
    DWORD esp2; // 2 环栈指针
    DWORD ss2;  // 2 环数据段选择子
    DWORD cr3;//这个地址需要实时查看来填
    DWORD eip;//跳转后的代码函数地址
    DWORD eflags;
    DWORD eax;
    DWORD ecx;
    DWORD edx;
    DWORD ebx;
    DWORD esp;
    DWORD ebp;
    DWORD esi;
    DWORD edi;
    DWORD es;
    DWORD cs;
    DWORD ss;
    DWORD ds;
    DWORD fs;
    DWORD gs;
    DWORD ldt;
    DWORD io_map;
} TSS;
TSS tss = {//0x00427a30
        0x00000000,//link
        0x12345678,//esp0,自定义的ESP0地址
        0x00000010,//ss0
        0x00000000,//esp1
        0x00000000,//ss1
        0x00000000,//esp2
        0x00000000,//ss2
        0x00000000,//cr3
        0x00401020,//eip,测试入口函数的地址。
        0x00000000,//eflags
        0x00000000,//eax
        0x00000000,//ecx
        0x00000000,//edx
        0x00000000,//ebx
        0x12345678,//esp,自定义的ESP0
        0x00000000,//ebp
        0x00000000,//esi
        0x00000000,//edi
        0x00000023,//es  
        0x00000008,//cs  
        0x00000010,//ss
        0x00000023,//ds
        0x00000030,//fs
        0x00000000,//gs
        0x00000000,//ldt
        0x20ac0000 // 这个位置暂时忽略,填这个值就行了
};
int main(int argc, char* argv[])
{
    printf("input cr3:\n");
    &tss;
    scanf("%x", &(tss.cr3)); // 使用 !process 0 0查看 DirBase

    __asm {
        int 0x20
    }

    printf("test_esp = %x\n", test_esp);
    return 0;
}

附件

Intel+64及IA-32+架构软件开发者手册