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

这篇具有很好参考价值的文章主要介绍了Windows保护模式(一)段寄存器&GDT表。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

保护模式

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

段寄存器

段式内存管理

段式内存管理是将内存划分成若干段,处理器在访问一个内存单元时通过“段基址+偏移”的方式计算出实际的物理地址。
在Intel x86处理器中,有专门的段寄存器,指定每条指令在访问内存时指定在哪个段上进行,以及该段的长度,读写属性,特权级别等。段式内存管理与页式内存管理关系如下图。
Windows保护模式(一)段寄存器&GDT表
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 ,结构如下图所示:
Windows保护模式(一)段寄存器&GDT表
结构体表示:

struct SegMent
{
	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 的作用如下图所示。
Windows保护模式(一)段寄存器&GDT表

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

段寄存器属性探测

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

段寄存器的属性

段寄存器中的各属性如下图所示:
Windows保护模式(一)段寄存器&GDT表

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

探测 Attribute

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

首先将 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
    }

Windows保护模式(一)段寄存器&GDT表
上面的两个例子说明段寄存器的 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 个重要的寄存器用来定位这两张表:

  • gdtr:GDT 表基址
  • gdtl:GDT 表的大小
  • ldtr:LDT 表基址
    由于 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;
 }

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

使用 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 位):
Windows保护模式(一)段寄存器&GDT表
Windbg 使用 dg + 段选择子可以查看段描述符各属性:
Windows保护模式(一)段寄存器&GDT表
段描述符的各个属性:

  • 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域结构图如下:
    Windows保护模式(一)段寄存器&GDT表

    • 第11位为0:段描述符为数据段描述符
      第11位为1:段描述符为代码段描述符
    • A位:若该代码段/数据段未被访问过,则值为0,否则为1
    • W位:若为1,表示该段可写
    • E位:若为0,则向上拓展,若为1,则向下拓展 Windows保护模式(一)段寄存器&GDT表
      向上拓展:有效范围为fs.Base ~ fs.Base+Limit
      向下拓展:有效范围除了fs.Base ~ fs.Base+Limit
    • R位:若为1,表示该段可读
    • C位:一致位。若为1,则是一致代码段;若为0,则是非一致代码段。

    当S = 0时,即段描述符为系统段描述符时,Type域结构图如下:
    Windows保护模式(一)段寄存器&GDT表

  • 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
      Windows保护模式(一)段寄存器&GDT表
  • 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 寄存器。
Windows保护模式(一)段寄存器&GDT表
注意: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 不同,可以用来验证下面的权限检查规则。
Windows保护模式(一)段寄存器&GDT表

  • 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 则可以正常读写内存。
    Windows保护模式(一)段寄存器&GDT表
  • 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>

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 偏移处构造一个调用门
Windows保护模式(一)段寄存器&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  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保护模式(一)段寄存器&GDT表文章来源地址https://www.toymoban.com/news/detail-415845.html

到了这里,关于Windows保护模式(一)段寄存器&GDT表的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • STM32F103ZET6 GPIO工作模式介绍+使用寄存器点亮第一个LED灯

    目录  GPIO的工作模式介绍 1.输入模式(模拟、上拉、下拉、浮空) 2.输出模式(推挽/开漏) 3.复用功能(推挽/开漏) 4.模拟输入输出(上下拉无影响) 如何使用寄存器点亮第一个LED灯         在输入模式时,施密特触发器打开,输出被禁止。可通过输入数据寄存器 GPIOx_

    2024年02月06日
    浏览(71)
  • 【汇编中的寄存器分类与不同寄存器的用途】

    寄存器分类 在计算机体系结构中,8086CPU,寄存器可以分为以下几类: 1. 通用寄存器: 通用寄存器是用于存储数据和执行算术运算的寄存器。在 x86 架构中,这些通用寄存器通常包括 AX、BX、CX、DX、SI、DI、BP 和 SP。其中,AX、BX、CX 和 DX 寄存器可以分别作为累加器(accumulat

    2024年02月09日
    浏览(53)
  • stm32的BRR寄存器和BSRR寄存器

    1、BRR---   bit   RESET(置0)  register   //高16位无,低16位置1为0,不能写1 2 、BSRR---   bit   SET(设置1或0)       register   //低16位设置1为0 BSRR:用于低16位的作用是让指定的IO口置1;而高16位的作用是让指定的IO口置0。  

    2024年02月11日
    浏览(48)
  • 寄存器内存读写指令(二) —— 多寄存器读写 LDM / STM

    有的时候,CPU可能会遇到 a++; b++; c++,这个时候为了提升效率,CPU可能会一次将多个寄存器里的变量保存到内存中。这个时候之前介绍的 LDR / STR 指令虽然也能实现,但只能操作一个寄存器的读写。 因此,考虑到这点,下面介绍多个寄存器的读写指令 将 多个寄存器 的数据写

    2024年02月07日
    浏览(60)
  • 锁存器、D触发器、寄存器理解

    1、锁存器        锁存器对脉冲的电平敏感,也就是电平触发,在有效的电平下,锁存器处于使能状态,输出随着输入发生变化,此时它不锁存信号,就像一个缓冲器一样;在锁存器没有使能时,则数据被锁住,输入信号不起作用,此时输出一直为锁存的状态信息(锁存最后

    2024年02月09日
    浏览(45)
  • FPGA之 寄存器、触发器、锁存器

    每个slice有8个存储元素,每个存储元素如下图所示:  其中四个为DFF/LATCH,可以配置为边沿触发D型触发器或电平敏感锁存器输入上图。D输入可以通过AFFMUX, BFFMUX, CFFMUX或DFFMUX的LUT输出直接驱动,也可以通过AX, BX, CX或DX输入绕过函数发生器的 BYPASS slice输入直接驱动。当配置为锁存

    2024年01月18日
    浏览(57)
  • CPSR寄存器

    ​ 对于ARMv7架构的CPSR如下: N: 两个表示的有符号整数运算时,n=1表示运算结果为负数,n=0表示结果为正数或零。 Z: z=1表示运算的结果为零;z=0表示运算的结果不为零。对于CMP指令,Z=1表示进行比较的两个数大小相等。 C: 下面分四种情况: 在加法指令中(包括比较指令CMN),

    2023年04月08日
    浏览(49)
  • ARM寄存器组织

     ARM有37个32位长的寄存器: 1个用做PC(Program Counter); 1个用做CPSR(Current Program Status Register); 5个用做SPSR(Saved Program Status Registers); 30个通用寄存器。 ARM处理器共有37个寄存器,被分为若干个组(BANK),这些寄存器均为32位的寄存器。6个状态寄存器,用以标识CPU的工作状

    2024年02月01日
    浏览(79)
  • ARM 寄存器

    Cortex A 系列的 ARM 处理器共有 40 个 32 位寄存器,其中 33 个为通用寄存器,7 个为状态寄存器。用户模式和系统模式共用同一组寄存器。 一、未分组寄存器 R0~R7 有些寄存器是所有运行模式共用的,如 R0~R7,它们被称为未分组寄存器。 在所有运行模式下,未分组寄存器都指向同

    2024年02月02日
    浏览(48)
  • verilog——移位寄存器

    在Verilog中,你可以使用移位寄存器来实现数据的移位操作。移位寄存器是一种常用的数字电路,用于将数据向左或向右移动一个或多个位置。这在数字信号处理、通信系统和其他应用中非常有用。以下是一个使用Verilog实现的简单移位寄存器的示例: module ShiftRegister (   inpu

    2024年02月05日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包