0


Windows保护模式(一)段寄存器&GDT表

保护模式

x86 CPU的3个模式:实模式、保护模式和虚拟8086模式。

段寄存器

段式内存管理

段式内存管理是将内存划分成若干段,处理器在访问一个内存单元时通过“段基址+偏移”的方式计算出实际的物理地址。
在Intel x86处理器中,有专门的段寄存器,指定每条指令在访问内存时指定在哪个段上进行,以及该段的长度,读写属性,特权级别等。段式内存管理与页式内存管理关系如下图。
在这里插入图片描述
Windows采用了页式内存管理方案,在Intel x86处理器上,Windows不使用段来管理虚拟内存,但是,Intel x86处理器在访问内存时必须要通过段描述符,这意味着Windows将所有的段描述符都构造成了从基地址0开始,且段的大小设置为0x80000000、0xc0000000或0xffffffff,具体取决于段的用途和系统设置。所以,Windows系统中的代码,包括操作系统本身的代码和应用程序代码,所面对的地址空间都是线性地址空间。这种做法相当于屏蔽了处理器中的逻辑地址概念,段只被用于访问控制和内存保护。

什么是段寄存器?

当我们用汇编读写某一个地址时:

mov dword ptr ds:[0x123456], eax

我们真正读写的虚拟地址是:

ds.base + 0x123456

段寄存器有几个,有哪些?

段寄存器有8个,分别为:

ES CS SS DS FS GS LDTR TR

其中以下段寄存器有特殊的用途:

  • cs:代码段寄存器,指向一个包含指令的段,即代码段。
  • ss:栈段寄存器,指向一个包含当前调用栈的段,即栈段。
  • ds:数据段寄存器,指向一个包含全局和静态数据的段,即数据段。

段寄存器结构

段寄存器长度为 96 bit ,结构如下图所示:
在这里插入图片描述
结构体表示:

structSegMent{
    WORD Selector;// 段选择子 16位 可见
    WORD Atrributes;// 段属性 16位 不可见
    DWORD Base            // 段起始地址 32位 不可见
    DWORD Limit            // 段大小 32位 不可见}

Selecter

Selector 即段选择子,它由 Index,TI,RPL 组成。用以指向定义该段的段描述符。其中 Index 表示段描述符在段描述符表中的中的位置。段描述符表分为全局描述符表(GDT,Global Descriptor Table)和局部描述符表(LDT,Local Descriptor Table)。段选择子的 TI 表示在哪一张表中查找段描述符。关于段描述表和段的段描述符下文有解释。这里只需要知道段相关属性是在段描述符中存储的,而段寄存器中的属性是从段描述符中加载出来,以提高内存访问速度。在逻辑地址到线性地址的转换中 Index 和 TI 的作用如下图所示。
在这里插入图片描述

RPL 表示特权请求级别。特别要注意的是,段寄存器 CS 的后两位比特位称为当前特权级 CPL。

Atrributes

即段属性,描述了段对应内存区域的读、写、执行等权限。

Base

表示段的起始地址。

Limit

表示段的长度。

段寄存器的读写

读:

MOV AX,ES
#include<stdio.h>#include<stdlib.h>intmain(){int var =0;
    __asm {xor eax, eax
        mov ax, es
        mov var, eax
    }printf("%X\n", var);system("pause");return0;}

成功将 ES 寄存器中的段选择子部分读入
在这里插入图片描述
写:

MOV DS,AX

同理,可以修改 16 bit 的段选择子,但在修改段选择子的同时,会自动将段描述符表中对应的段描述符中的相关属性写入段寄存器,因此相当于修改了段寄存器中的 96 bit。在接下来的段寄存器属性探测会用到这个性质。
除了MOV指令,还可以使用LES、LSS、LDS、LFS、LGS指令修改段寄存器。
注意:CS不能通过上述的指令进行修改,CS为代码段,CS的改变会导致EIP的改变,要改CS,必须要保证CS与EIP一起改

段寄存器属性探测

对段寄存器中不可见部分进行探测。

段寄存器的属性

段寄存器中的各属性如下图所示:
在这里插入图片描述

注意图中红色字体部分在不同环境中可能不同。

探测 Attribute

注意:为了确保结果可靠,前面代码中的变量要设为全局变量,因为在栈中的变量编译器会强制转换为 ss 寄存器访问(如下图所示),因此修改 ds 寄存器的段选择子后产生的异常可能是 printf 读取格式化字符串时触发的
在这里插入图片描述

首先将 ds 寄存器的段选择子修改为 ss 寄存器的段选择子,代码可以正常运行。因为 ss 寄存器的段选择子对应的段描述符的属性是可读写的(实际上是同一个段选择子),修改后 ds 寄存器的 Attribute 字段描述为可读写,因此可以正常读写。

#include<stdio.h>#include<stdlib.h>int var;intmain(){
    __asm {
        mov ax, ss
        mov ds, ax
        mov dword ptr ds:[var], eax
    }printf("%X\n", var);system("pause");return0;}

将将 ds 寄存器的段选择子修改为 cs 寄存器的段选择子后,由于 cs 寄存器的段选择子对应的段描述符的属性是可读,可执行但不可写,因此会触发异常。

    __asm {
        mov ax, cs //修改为 cs
        mov ds, ax
        mov dword ptr ds:[var], eax
    }

在这里插入图片描述
上面的两个例子说明段寄存器的 Attribute 在写入时会被更改!

探测 Base

分别将 fs 和 ds 的段选择子放入 gs 中,结果在不同的偏移出读出了相同的数据,说明段基址不同。即段寄存器的 Base 在写入时会被更改!

#include<stdio.h>#include<stdlib.h>int var1, var2;intmain(){
    __asm {
            mov ax, fs
            mov gs, ax
            mov eax, gs:[0]
            mov dword ptr ds:[var1], eax
            
            mov ax, ds
            mov gs, ax
            mov eax, dword ptr gs:[0x7FFDF000]
            mov dword ptr ds:[var2], eax
    }printf("var1 = %X\n", var1);printf("var2 = %X\n", var2);system("pause");return0;}/*
var1 = 12FFB0
var2 = 12FFB0
请按任意键继续. . .
*/

探测 Limit

    __asm{
        mov ax, fs
        mov gs, ax
        mov eax, gs:[0x1000-0x4+1]}

编译器能成功编译上述代码,但程序运行过程中报错
这是因为 FS 段寄存器的 Limit 为 0xFFF,而上述代码会读到 0x1000 偏移。
改为下面所示的代码后可以正常运行。

    __asm {
        mov ax, fs
        mov gs, ax
        mov eax, gs:[0x1000-0x4]}

GDT 表

在前面解释段选择子是提到过:

  • GDT:全局描述符表
  • LDT :局部描述符表

有 3 个重要的寄存器用来定位这两张表:

  • gdtr:GDT 表基址
  • gdtl:GDT 表的大小
  • ldtr:LDT 表基址 由于 Windows 不使用 LDT,因此这里不做研究。

sgdt 汇编指令可以读取 gdtr 和 gdtl:

#include<stdio.h>#include<stdlib.h>char buf[6];intmain(){
    __asm {
       sgdt buf;}printf("gdtr = %X\n",*((unsignedint*)(&(buf[2]))));printf("gdtl = %X\n",*((unsignedshort*)(&(buf[0]))));system("pause");return0;}

在这里插入图片描述

使用 WinDbg 查看 GDT 表:

kd> dq gdtr
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  80008954`b1000068 80008954`b1680068
8003f060  00009302`2f40ffff 0000920b`80003fff
8003f070  ff0092ff`700003ff 80009a40`0000ffff

段描述符

GDT表中存储的元素称为段描述符,每个段描述符占用空间为8个字节。

段描述符结构如下图所示(第一行为高 32 位,第二行为低 32 位):
在这里插入图片描述
Windbg 使用 dg + 段选择子可以查看段描述符各属性:
在这里插入图片描述
段描述符的各个属性:

  • P位 P = 1:段描述符有效 P = 0:段描述符无效段描述符加载时,首先看P位是否为1
  • G位 G=0:段寄存器的Limit元素单位为字节,最大值为0x000FFFFF G=1:段寄存器的Limit元素单位为4KB,最大值为0xFFFFFFFF
  • S位 S = 1:段描述符为代码段或数据段描述符 S = 0:段描述符为系统段描述符
  • Type域 当S = 1时,即段描述符为代码段或数组段描述符时,Type域结构图如下:在这里插入图片描述- 第11位为0:段描述符为数据段描述符 第11位为1:段描述符为代码段描述符- A位:若该代码段/数据段未被访问过,则值为0,否则为1- W位:若为1,表示该段可写- E位:若为0,则向上拓展,若为1,则向下拓展 在这里插入图片描述 向上拓展:有效范围为fs.Base ~ fs.Base+Limit 向下拓展:有效范围除了fs.Base ~ fs.Base+Limit- R位:若为1,表示该段可读- C位:一致位。若为1,则是一致代码段;若为0,则是非一致代码段。当S = 0时,即段描述符为系统段描述符时,Type域结构图如下:在这里插入图片描述
  • D\B位- 情况1:对CS段的影响 D=1:采用32位寻址方式 D=0:采用16位寻址方式- 情况2:对SS段的影响 D=1:隐式堆栈访问指令(如:PUSH POP CALL)使用32位堆栈指针寄存器ESP D=0:隐式堆栈访问指令(如:PUSH POP CALL)使用16位堆栈指针寄存器SP- 情况3:向下拓展的数据段 D=1:段上限为4GB D=0:段上限为64KB在这里插入图片描述
  • DPL 描述: DPL存储在段描述符中,规定了访问所在段描述符所需要的特权级别是多少 DPL数值越大,访问所在段描述符所需要的权限越低注意:在Windows中,DPL只会出现两种情况,要么全为0,要么全为1例:若AX指向的段描述符的DPL=0,但当前程序的CPL=3,那么这条指令是不会成功的!

段描述符与段寄存器结构的对应关系

  • Attribute - 位于段描述符高四字节的第8-23位
  • Base - 第一部分:位于段描述符高四字节的第24-31位- 第二部分:位于段描述符高四字节的第0-7位- 第三部分:位于段描述符低四字节的第16-31位
  • Limit - 第一部分:位于段描述符高四字节的第16-19位- 第二部分:位于段描述符低四字节的第0-15位

加载段描述符到段寄存器

前面提到过段寄存器的读写,通过修改段寄存器可以使段描述符加载到段寄存器中。

这里演示一下修改 es 段寄存器的 les 指令:

#include<stdlib.h>intmain(){unsignedchar buf[6]={0x78,0x56,0x34,0x12,0x1B,0x00};
    __asm {
        les eax, fword ptr ds:[buf]}system("pause");return0;}

这个指令将 buf 前 4 字节赋值给 eax ,后 2 字节赋值给 es 寄存器。
在这里插入图片描述
注意:RPL<=DPL(在数值上)

段权限检查

段权限描述

  • 当前特权等级(CPL) 段寄存器 CS 的后两位比特位称为当前特权级注意:段选择子SS和CS的后两位比特位相同 如: → CS = 0x001B → 0x001B = 二进制:0000 0000 0001 1011 → 二进制:11 = 十进制:3 → 因此:当前进程处于3环
  • 请求特权等级(RPL) 段选择子的后两比特位(除CS外)。 RPL是针对段选择子而言的,每个段的选择子都有自己的RPL RPL表示用什么权限去访问一个段
  • DPL 段描述符中的一个属性,规定了访问所在段描述符所需要的特权级别是多少。

段权限检查规则

注意:下面都是通常情况下的权限检查
在 GDT 表中,下标为 2 和 4 的段描述符仅 DPL 不同,可以用来验证下面的权限检查规则。
在这里插入图片描述

  • RPL ≤ \le ≤ 数据段 DPL(数值上) 验证:#include<ntddk.h>VOID Unload(IN PDRIVER_OBJECT DriverObject){KdPrint(("Goodbye driver!\n"));}int g_value =0;NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING ReistryPath){ DriverObject->DriverUnload = Unload;KdPrint(("Hello driver!\n")); __asm {int3 mov ax,0x11 mov ds, ax mov ebx,0x64 mov dword ptr ds :[g_value], ebx mov ax,0x20 mov ds, ax }KdPrint(("g_value: %X\n", g_value));return STATUS_SUCCESS;} 使用这一驱动代码验证,即便在 CPL = 0 的情况下,如果 RPL 在数值上大于数据段 DPL 也会触发蓝屏,相反,如果满足 RPL ≤ \le ≤ CPL 则可以正常读写内存。在这里插入图片描述
  • CPL ≤ \le ≤ 代码段 DPL(数值上) 验证: 实际观察发现无论内核态还是用户态 CPL 始终等于 DPL 。
  • CPL ≤ \le ≤ 数据段 DPL(数值上) 验证: R3 下不能把 DS 改为 0x10 ,但是可以把 DS 改为 0x20 。
  • CPL 和 RPL 大小上无直接关系 验证: R3 下可以把 RPL 改成 0x20,R0 下可以把 RPL 改成 0x23 。

拓展:利用调用门提权构造 LDT

代码如下:

#include<stdio.h>#include<stdlib.h>unsignedchar ldtTable[0x3ff]={0};unsignedchar gdtTable[6]={0};__declspec(naked)voidtest(){
    __asm {
        pushad;
        pushfd;
        lea eax,[gdtTable+2];
        mov eax,[eax];
        lea eax,[eax+0x90];
        lea ecx,ldtTable;
        mov bx,cx;
        shl ebx,0x10;
        mov bx,0x03ff;
        mov dword ptr ds:[eax],ebx;
        lea eax,[eax+4];
        shr ecx,0x10;
        mov byte ptr ds:[eax],cl;
        mov byte ptr ds:[eax+1],0xe2;
        mov byte ptr ds:[eax+4],ch;
        mov ax,0x93;
        lldt ax;
        popfd;
        popad;
        retf;}}intmain(int argc,char*argv[]){char buf[]={0,0,0,0,0x48,0};char cldtr[]={0};int a =10;int b =0;*((unsignedint*)(ldtTable +8))=0x0000ffff;*((unsignedint*)(ldtTable +0xc))=0x00cfe300;printf("test: %X, ldtTable: %X\n", test, ldtTable);system("pause");

    __asm {
        sgdt gdtTable;
        push fs;
        call fword ptr buf;
        sldt cldtr;
        pop fs;
        mov ax,0x0f;
        mov ds,ax;
        mov eax,a;
        mov b,eax;}printf("a = %d\nb = %d\n", a, b);return0;}

代码的执行流程为:

  • 在用户空间构造一个 LDT 表,并且在 LDT 的第二项构造一个 DPL 为 3 的数据段
  • 利用调用门提权到 0 环然后在 GDT 表的 0x90 偏移处构造一个 LDT 段描述符,然后利用 lldt 指令将该段描述符加载到 ldtr 寄存器中
  • 返回 3 环后将 ds 寄存器指向构造的 LDT 表中实现构造好的第二项

执行代码,获得 test 函数地址,并根据函数地址在 GDT 表 0x48 偏移处构造一个调用门
在这里插入图片描述

kd> dq gdtr
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`23800068 80008955`23e80068
8003f060  00009302`2f40ffff 0000920b`80003fff
8003f070  ff0092ff`700003ff 80009a40`0000ffff
kd> eq 8003f048 0040ec00`0008100a
kd> dq gdtr
8003f000  00000000`00000000 00cf9b00`0000ffff
8003f010  00cf9300`0000ffff 00cffb00`0000ffff
8003f020  00cff300`0000ffff 80008b04`200020ab
8003f030  ffc093df`f0000001 0040f300`00000fff
8003f040  0000f200`0400ffff 0040ec00`0008100a
8003f050  80008955`23800068 80008955`23e80068
8003f060  00009302`2f40ffff 0000920b`80003fff
8003f070  ff0092ff`700003ff 80009a40`0000ffff

继续运行代码,成功执行并退出
在这里插入图片描述

标签: windows 系统安全

本文转载自: https://blog.csdn.net/qq_45323960/article/details/127181691
版权归原作者 _sky123_ 所有, 如有侵权,请联系我们删除。

“Windows保护模式(一)段寄存器&GDT表”的评论:

还没有评论