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

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

保护模式

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 结构如下图所示
在这里插入图片描述
结构体表示

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

Selecter

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

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

Atrributes

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

Base

表示段的起始地址。

Limit

表示段的长度。

段寄存器的读写

MOV AX,ES

#include<stdio.h>
#include<stdlib.h>

int main() {
    int var = 0;
    __asm {
	    xor eax, eax
	    mov ax, es
	    mov var, eax
    }
    printf("%X\n", var);
    system("pause");
    return 0;
}

成功将 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;

int main() {
    __asm {
        mov ax, ss
        mov ds, ax
        mov dword ptr ds:[var], eax
    }
    printf("%X\n", var);
    system("pause");
    return 0;
}

将将 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;

int main() {
    __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");
    return 0;
}
/*
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 个重要的寄存器用来定位这两张表

  • gdtrGDT 表基址
  • gdtlGDT 表的大小
  • ldtrLDT 表基址
    由于 Windows 不使用 LDT因此这里不做研究。

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

#include<stdio.h>
#include<stdlib.h>

char buf[6];

int main() {
    __asm {
	   sgdt buf;
    }
    printf("gdtr = %X\n", *((unsigned int*)(&(buf[2]))));
	printf("gdtl = %X\n",*((unsigned short*)(&(buf[0]))));
    system("pause");
    return 0;
 }

在这里插入图片描述

使用 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>

int main() {
    unsigned char buf[6] = {0x78, 0x56, 0x34, 0x12, 0x1B, 0x00};
    __asm {
        les eax, fword ptr ds:[buf]
    }
    system("pause");
    return 0;
}

这个指令将 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 {
    		int 3
    		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 改成 0x20R0 下可以把 RPL 改成 0x23 。

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

代码如下

#include <stdio.h>
#include <stdlib.h>

unsigned char ldtTable[0x3ff] = {0};
unsigned char gdtTable[6] = {0};

__declspec(naked) void test() {
    __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;
    }
}


int main(int argc, char *argv[]) {
    char buf[] = {0, 0, 0, 0, 0x48, 0};
    char cldtr[] = {0};
    int a = 10;
    int b = 0;

    *((unsigned int *) (ldtTable + 8)) = 0x0000ffff;
    *((unsigned int *) (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);

    return 0;
}

代码的执行流程为

  • 在用户空间构造一个 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

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

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: windows

“Windows保护模式(一)段寄存器&GDT表” 的相关文章