保护模式
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 即段选择子,它由 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>
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 个重要的寄存器用来定位这两张表:
- 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;
}
使用 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域结构图如下:
- 第11位为0:段描述符为数据段描述符
-
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
- 情况1:对CS段的影响
-
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(数值上)
验证:
使用这一驱动代码验证,即便在 CPL = 0 的情况下,如果 RPL 在数值上大于数据段 DPL 也会触发蓝屏,相反,如果满足 RPL ≤ \le ≤ CPL 则可以正常读写内存。#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
≤
\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 偏移处构造一个调用门
文章来源:https://www.toymoban.com/news/detail-415845.html
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
继续运行代码,成功执行并退出
文章来源地址https://www.toymoban.com/news/detail-415845.html
到了这里,关于Windows保护模式(一)段寄存器&GDT表的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!