驱动-分段机制

一、分段机制

在 32 位保护模式下,段寄存器就不再直接保存段基址了,而是分为了 可见的 16 位段选择子 部分以及不可见的 80 位 高速缓存部分,对于需要访问的信息(基址、限长以及属性)都被保存在了不可见部分。这一部分的信息无法被人为的查看,由 CPU 自动加载。当段寄存器被修改的时候,CPU 会通过可见部分的段选择子从 GDT 或 LDT(windows没启用)中找到对应的段描述符,在进行权限检查后,将相关内容加载到告诉缓存部分。

二、段选择子

从下面的结构体可以知道,段选择子由 INDEX、TI 以及 RPL 组成。其中 RPL 全称为请求特权级别,表示当前使用什么样的权限去发出请求,TI 指定了当前使用过的是 GDT 还是 LDT,其中的的 INDEX 表示的是 GDT 或 LDT 的下标。通过 TI 和 IDNEX 可以获取到相应的段描述符。

typedef struct _SELECTOR
{
    unsigned short index: 13;       // index 存在于 GDT 或 LDT 中的下标     
    unsigned short TI   : 1;        // TI   当前查 GDT(0) 还是 LDT(1)
    unsigned short RPL  : 2;        // RPL  请求权限级别 
} SELECTOR, *PSELECTOR;

三、段描述符

GDT 或 LDT 中保存的就是段描述符,段描述符的结构体如下所示,总大小为 8 字节(64 位)

typedef struct _DESCRIPTOR
{
    unsigned int limit1 :16;        // 段限长 [0-15]
    unsigned int base1  :16;        // 段基址 [0-15]
    unsigned int base2  : 8;        // 段基址 [16-23]
    unsigned int TYPE   : 4;        // 类型
    unsigned int S      : 1:        // 系统段0 \ 用户段1
    unsigned int DPL    : 2;        // 段特权级别
    unsigned int P      : 1;        // 有效位
    unsigned int limit2 : 4;        // 段限长 [16-19]
    unsigned int AVL    : 1;        // 保留
    unsigned int L      : 1;        // 保留
    unsigned int DB     : 1;        // 默认大小
    unsigned int G      : 1;        // 限长的单位
    unsigned int base3  : 8;        // 段基址 [24-31]
} DESCRIPTOR, *PDESCRIPTOR;
  • P: 表示当前的段描述符是否有效,如果有效则保存的是 1 否则是 0。被加载的段描述符必须有效。
  • AVL:所有兼容 i386 的处理器必须提供 AVL 位供系统软件使用,通常我们不做考虑
  • L:如果处理器处于 64 位模式,则提供了 L 位,表示当前使用的的是长模式。
  • DPL:表示想要访问(切换)当前的段描述符,最少需要拥有什么样的权限。
  • Base: 由 3 部分组成,一共描述了一个 32 位的地址,指明段的基址部分。
  • Limit 和 G:Limit 由两个部分组成,共描述了 20 位的数据,表示段限长。当 G 位为 0 的时候,LImit 的单位是 1
    Byte,如果 G 位为1,就表示 Limit 的单位是 4 KB,可能存在的两种限长分别是 000FFFFF 或 0xFFFFFFFF
    若 G 为 1,假设 Limit 为 0xFFFFF,则限长为 0xFFFFF*0x1000 + 0xFFF -> 0xFFFFFFFF
    若 G 为 0,假设 Limit 为 0xFFFFF,单位为 1Byte,则表示的是 0x000FFFFF;
  • D\B:用于设置默认操作数和地址的大小,对于栈来说,这一位叫做 B 位,当 B 位为 1 的时候,默认操作的栈以 4
    字节为单位(push 1 实际 sub esp, 4),对于其它段来讲,着一位叫做 D 位,如果 D 为 1,则操作的数据以及默认的地址是
    4 字节,mov eax, ds:[0x100]。以上两种情况都可以使用 OPCODE 前缀进行切换。
    在这里插入图片描述

四、用户段的两种情况

  • 如果 S 为 1 那么当前就是用户端,用户段可能存在两种情况,分别是代码段和数据段
  • 如果 TYPE 的第一位是 1 那么当前就是代码段,看到的 TYPE 会大于 7
    • C: 标识当前是否是一致代码段,如果是一致代码段,那么我们可以使用低权限访问高权限的内容,如果当前是非一致代码段,则要求访问者的权限和当前的权限必须保持一致。
    • R: 当前对应的段是否是可读
    • A: 表示当前是否被访问了
  • 如果 TYPE 的第一位是 0 那么当前就是数据段,看到的 TYPE 会小于 8
    • E: 表示当前是 extend-up 或者 enxtend-down 的,如果是向上扩展的
    • W: 当前对应的段是否是可写的
    • A: 表示当前是否被访问了
      在这里插入图片描述

五、权限检查

  • 使用指令(mov\lxx)去修改段寄存器的时候,首先需要满足以下条件
    • 对于 CS 段寄存器,不能直接修改,只能依赖于门,只能加载代码段描述符
    • 对于 SS 段寄存器,只能加载可读可写的段描述符
    • 对于 DS 段寄存器,可以加载任何一个具有读写权限的段描述符
    • 对于 FS 段寄存器,操作系统为其设置了固定的值,R3 保存 TEB,R0 保存 KPCR
    • 对于 GS 寄存器,在 x64 使用, x86 保存
  • 三种权限的含义
    • CPL:当前代码的执行权限,特指 CS 段寄存器的最低两位(RPL)
    • RPL:当前实际使用的权限,类似于指定文件的打开方式,应该 >= CPL
    • DPL:段描述符特权级别,想要访问当前的段描述符,应该拥有的最低权限
  • 在切换段选择子的时候,会进行以下权限的检查
    • MAX(CPL, RPL) <= DPL,当前的最低权限数字上需要小于等于 DPL

六、系统描述符

  • S位为0时是系统描述符,代表的是门
    • 调用门:type为12 C
    • 中断们:type为14 E
    • 陷进门:type为15 F
    • 任务门:type为5 5
  • 门的结构
    • 偏移:16位
    • P位:1 一般为1,为有效的
    • DPL:2 当前调用门的权限
    • S位:1 系统段时始终为0
    • type:4 调用门的类型
    • 000:3 就是0
    • 参数个数:4
    • 段选择子:16 指向切换指定的段的权限
    • 偏移:16位
#include <stdio.h>
#include <windows.h>

// 门结构体
typedef struct _CALLGATE {
	unsigned int offset1 : 16;
	unsigned int selector : 16;
	unsigned int paramcount : 5;
	unsigned int : 3;
	unsigned int type : 4;
	unsigned int S : 1;
	unsigned int DPL : 2;
	unsigned int P : 1;
	unsigned int offset2 : 16;
} CALLGATE, *PCALLGATE;

// 在进行权限切换的过程中,栈也会随之改变
short r0_ss = 0;
int r0_esp = 0;

// 裸函数,只生成用户编写的代码,在其中操作高地址空间
_declspec(naked) void r0_function()
{
    // 如果是有参数的调用门堆栈布局如下
    // 如果有pushad 前32位是8个寄存器
    // 如果有pushfd 然后4位是eflag
    // 再后4位返回地址
    // 再后4位cs
    // 再后是参数1-参数2-参数3 等等参数
    // 然后是esp
    // 最后是ss
	__asm
	{
		int 3
		mov r0_ss, ss		; 获取 r0 权限的 ss
		mov r0_esp, esp		; 获取 r0 权限的 esp
		retf				; 调用门必须使用 retf 进行返回
        //中断门的时候使用IRET或IRETD返回,因为中断比调用多push一个eflag

		; 实现通过调用门读取到 gdt 中的第 3 项
		; sgdt 可以获取 gdt 的地址
	}
}

int main()
{
	// 1. 根据当前函数的位置,构建一个调用门,用于进行跳转
	// offset: 0045???? ????6c20	-> 需要跳转的函数偏移-为上方r0_function函数的地址
	// selector: ???????? 0008???? (1 0 00) ->  需要切换的权限 段选择子 GDT表任意一个0环权限的下标索引即可
	// 	P : 1   DPL:11  S:0    
	// DPL: ????E??? ????????	-> 访问当前调用门的权限
	// TYPE: ?????C?? ????????	-> 访问当前调用门的类型

	// 0045EC00`00086c20

	// 查看GDT表,并找到全0的段描述符 使用构建的门进行填充
	// !dq sgdt 		windbg指令查看sgdt表
    // !eq (sgdt+8*9) 	0045EC00`00086c20 改(sgdt+8*9)的内容,回车后,输入新内容
    
	// 2. 构建一个远跳的地址,在远跳中,前面4个是偏移是没有意义的,给什么都可以
	//	后面保存的是段选择子,可以用于找到调用门描述符,要求 RPL <= DPL
	//  构建索引为9的段选择子
	//	- 0x004B -> 01001(INDEX) 0(TI) 11(RPL)
	BYTE dest[] = { 0x00, 0x00, 0x00, 0x00, 0x4B, 0x00 };

    // call far 是6个字节 所以上面需要构建前4个字节 后面选择子的数据
	// 3. 使用 call far 语句,跳转到指定的调用门中,在进行R3到R0 的转化的
	//	时候 CPU 会默认的将用户 SS ESP CS IP 保存到栈中
	__asm push fs
	__asm call fword ptr dest;
	__asm pop fs

	// 4. 输出 R0 下的 esp 和 ss,如果R0代码设置断点,那么 fs 会被改变,调
	//	用函数会崩溃,需要在进入门之前先进行保存
	printf("%04X: %08X\n", r0_ss, r0_esp);
	system("pause");

	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值