1.保护模式简介
1.1 保护模式简介
1.1.1 x86的cpu主要有三种运行模式
- 实模式:具有20 bit分段内存地址空间,对所有可寻址内存、I/O地址和外设硬件可以无限制直接软件访问。
- 保护模式:和实模式相比最大的区别就是有了
内存保护
、多任务处理
、或代码权限级别
,并且寻址空间达到了64TB。保护模式具有两种非常重要的机制,段机制
和页机制
。 - 虚拟8086模式:一种为了解决实模式下cpu工作不饱和的模式
2.段机制
2.1 段寄存器
2.1.1 段寄存器简介
在之前学汇编语言内容的时候其实已经接触到了段寄存器,比如如下的代码:
mov dword ptr ds:[0x401111],eax
这段汇编代码的意思是将eax中的值,赋给ds.base+401111
这个地址。这个ds就是一个所谓的段寄存器。段寄存器长度为96位,其中16位可见,80位不可见。神奇的是段寄存器读的时候是读了16位,但是写的时候是写96位!!!例如在如下代码中,虽然看起来像是把一个16位的AX
存放到了DS
寄存器里。但是其实DS
在被写的时候是写了96位。
mov DS,AX
段寄存器的结构可以参考如下结构体:
struct SegMent{ WORD Selector; //16位,可见; WORD Atrributes; //16位,不可见 ; 用于表示读写执行权限 DWORD BASE; //32位,不可见 ; 用于表示当前段的基址 DWORD Limit; //32位,不可见 ; 用于表示当前段的长度 }
段寄存器有如下八个:
寄存器名称 |
Selector |
Atrributes |
BASE |
Limit |
---|---|---|---|---|
ES | 0023 | 可读、可写 | 0 | 0xFFFFFFFF |
CS | 001B | 可读、可执行 | 0 | 0xFFFFFFFF |
SS | 0023 | 可读、可写 | 0 | 0xFFFFFFFF |
DS | 0023 | 可读、可写 | 0 | 0xFFFFFFFF |
FS | 003B | 可读、可写 | 0x7FFDE000 |
0xFFF |
GS | - | - | - | - |
LDTR | - | - | - | - |
TR | - | - | - | - |
2.1.2 段描述符与段选择
当执行mov DS,AX
这样的指令时,有80位来源不明的数据。这80位来源不明的数据,是根据AX
的值,来查GDT表
或者LDT表
从而决定的。windows系统主要查的是GDT表,LDT表windows系统没有使用。
2.1.2.1 GDT(全局描述符表)
GDT表的位置存储在gdtr寄存器
中。gdtr中存储了GDT表的开始位置和长度。我们可以使用如下命令查看gdtr表的位置。
查看gdt表起始位置 r gdtr 查看gdt表长度 r gdtl
查看gdt表中的内容可以使用如下指令,其中dd中的d表示dword,dq中的q表示qword:
dd gdt表位置 或者 dq gdt表位置
2.1.2.2 段描述符
GDT表当中所储存的元素就是所谓的段描述符
。每一个段描述符大小都是8个字节。为了方便分析可以使用dq指令查看段描述符的时候,高位在前面,低位在后面。
段描述符的通用结构如下图所示。
之前都说段寄存器赋值的时候都是要根据段选择子去GDT表中查对应的描述符然后来填充那个来历不明的80位。上图当中反映了段描述符和段寄存器不可见80位直接的关系。其中:
Atrributes
:对应从高4个字节的第8位到第23位BASE
:对应如图中粉红色的三部分组成Limit
:对应图中蓝色20位的limit扩展出来的
其中黄色部分位参数,以下为部分参数解释:
- P位:为1表示段描述符有效,为0表示段描述符无效。
- G位:从图中我们可以看出,Limit位一共只有16+4=20位,显然比段寄存器中的limit少了12位。G位决定了limit如何扩充出12位,如果G位为0,在前面填充三个0。如果G位为1,在后填充三个F。
- S位:为1表示代码段或者数据段描述符,为0表示系统段描述符。
-
Type域:
-
- 当S位为1时:是代码或数据段描述符。此时到底是代码段还是数据段要根据Type域来进行判断。具体判断方式可以参考下图。E表示拓展位,为0是向上拓展,也就是从base到base+limit是有效区域。为1是向下拓展,表示除了base到base+limit是有效区域。W表示可写。A表示是否访问过。R表示可读位。C为一致位,C=1表示一致代码段,C=0表示非一致代码段。
-
- 当S位为0时:是系统段描述符。Type域含义如下图所示。
-
D/B位对以下三个段有影响:
-
- 情况一,对CS段的影响: D/B位为1采用32位寻址方式,D/B位为0采用16位寻址方式。
-
- 情况二,对SS段的影响: D/B位为1时隐式堆栈访问指令(如pop,push,call)使用32位寄存器ESP,D/B位为0时隐式堆栈访问指令(如pop,push,call)使用16位寄存器SP。
-
- 情况三,对向下拓展的影响: D/B位为1上限为4GB,D/B位为0上限为64KB,也就是说最大的地址寻址范围也就只有64K。当然如果D/B位为0的话,也就是实模式了,之前的各种属性也都不适用了。
2.1.2.2 段选择子
段选择子是一个十六位的数,这个数指向了定义该段的段描述符。其实前面段寄存器里面可见的16位就是这个段选择子。段选择子的结构如下图所示: 除了mov指令以外,能够用来修改段寄存器的指令还有如下几个:
LES
LSS
LDS
LFS
LGS
以LES为例,使用方法如下:
char buffer[6]; __asm{ les ecx,fword ptr ds:[buffer] //高两个字节给es,低四个字节给ecx。 //注意保证所给选择子的RPL要小于等于其index所指定的段描述符的DPL }
2.2 段权限检查
2.2.1 CPL(Current Privilege Level)
CPL(Current Privilege Level)是当前进程的特权级别。位于CS和SS段选择子的后两位(二者永远保持一致)。如下图中,CS段选择子为001B
。二进制表示为0000 0000 0001 1011
。可以看到后两位为3。则当前进程的特权级别为3,是一个Ring3程序。
在windbg中对系统下断,查看cs段寄存器,可以发现最后两位为0。显然当前程序是一个Ring0级别的程序。
2.2.3 DPL(Descriptor Privilege Level)
DPL(Descriptor Privilege Level)是描述符特权级别,存储在段描述符中,规定了访问该段所需要的特权级别。例如,在如下代码中
mov DS,AX
如果在当前程序的CPL=3,AX段选择子所指向的段描述符的DPL=0那么就无法访问。
2.2.4 RPL(Request Privilege Level)
RPL在段选择子的最后两位。这个RPL是我们自己可以随便指定的。可以参考上文段选择子结构图。
2.2.5 数据段的段权限检查
数据段中,段权限的检查和代码段以及系统段中是不一样的。数据段权限检查需要权限满足以下要求方可访问所需数据段。
RPL<=DPL && CPL<=DPL
2.3 代码跨段跳转
2.3.1 代码跨段跳转流程
前面学习的时候讲过LES
、LSS
、LDS
、LFS
、LGS
可以帮助我们修改段寄存器。但是这里面却没有LCS
这样的指令。主要是因为CS
是代码段
,CS的改变也意味着EIP的改变。同时修改CS和EIP的指令又如下几项:
- JMP FAR
- CALL FAR
- RETF
- INT
- IRETED
本节以JMP FAR
为例,分析CPU段间跳转的流程:
JMP FAR 0x20:0x004183D7
-
1.首先拆分段选择子
0x20
,0x20
的二进制形式为0000 0000 0010 0000
。分析可得,RPL=0
,TI=0
,Index = 4
。 -
2.查表得到段描述符
-
- TI=0所以查GDT表
-
- Index=4所以查对应段描述符
-
- 只有下面四种情况可以跳转:
代码段
,调用门
(系统段描述符),TSS任务段
(系统段描述符),任务门
(系统段描述符)
- 只有下面四种情况可以跳转:
-
3.权限检查
-
- 如果是非一致代码段,要求CPL==DPL,并且RPL<=DPL。
-
- 如果是一致代码段,要求:CPL>=DPL。
- 4.加载段描述符:CPU将段描述符加载到CS段寄存器中
- 5.代码执行:CPU将新的段寄存器的Base+offset的值写到EIP。然后执行
CS:EIP
处的代码。段间跳转结束。
2.3.2 长调用(CALL FAR)
CALL FAR的指令格式为:
CALL CS:EIP //EIP是废弃的可以随便填 //CS必须指向一个调用门
执行的步骤有如下几步:
- 1.根据CS的值查GDT表,找到对应的段描述符,并且这个段描述符必须是一个
门描述符
。 - 2.找到调用门描述符中存储的另一个代码段的段选择子。
- 3.选择子指向的段.base+偏移地址就是真正执行的地址。
其中门描述符
的结构如下图所示:
2.3.2.1 长调用(跨段不提权)
CALL指令进行长调用时格式为CALL FAR CS:EIP(EIP是废弃的)
。调用流程如下:
- 1.先将
调用者CS
压入栈中 - 2.压入返回地址
- 3.执行完了ret的时候先把
调用者CS
的段选择子赋值给CS
,然后再返回
2.3.2.2 长调用(跨段提权)
当进行长调用并且提权的时候,比如从R3切换到R0,堆栈空间会先切换到R0的堆栈空间,然后依次压入调用者的SS
,调用者的ESP
,调用者的CS
,返回地址
。
2.3.2.3 调用门实验
- 1.主程序代码如下:
#include "stdafx.h" #include <windows.h> void __declspec(naked) GetRegister(){ _asm { int 3 retf } } int main(int argc, char* argv[]) { char buff[6]; *(DWORD*)&buff[0] = 0x12345678; *(WORD*)&buff[4] = 0x48; _asm { call fword ptr[buff] } getchar(); return 0; }
-
2.查看所需调用的代码地址
-
3.在gdt表中自己新建一个段描述符
新建一个系统段描述符为0040EC00 00081020
- 4.此时运行程序,就会发现,出现了一个系统断点在windbg里面
2.3.2.3 长调用实验(带参数)
代码如下:
#include "stdafx.h" int x, y, z; __declspec(naked) void getParam(int a, int b, int c) { __asm { //int 3 pushad //寄存器压栈,其入栈顺序是:EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI。长度为0x20 pushfd //将32位标志寄存器EFLAGS压入堆栈。长度为0x4 mov eax, [esp+0x24+0x08+0x08] mov x, eax mov eax, [esp+0x24+0x08+0x04] mov y, eax mov eax, [esp+0x24+0x08+0x00] mov z, eax popfd popad retf 0x0c } } int main(int argc, char* argv[]) { // 构造cs:eip char cs_eip[6] = {0, 0, 0, 0, 0x48, 0}; __asm { push 1 push 2 push 3 call fword ptr [cs_eip]; } printf("%x %y %z \n", x, y, z); getchar(); return 0; }
带参数时主要注意入栈顺序,入栈顺序依次为SS
,ESP
,参数1
,参数2
,...
,参数n
,CS
,EIP
。
2.3.3 IDT表
IDT
表全称为Interrupt Descriptor Table
,意为中断描述符表是CPU用来处理中断和程序异常的。IDT表当中主要描述三种门描述符,分别是任务门描述符
,中断门描述符
和陷阱门描述符
。查看IDT表及其长度方法如下:
2.3.4 中断门
2.3.4.1 中断门简介
windows系统没有使用调用门,但是在从3环到0环时,用的中断门(老的CPU),新的没有使用。新的使用快速调用。中断门和调用门类似,但是中断门查的是IDT
表。中断门的门描述符结构参考白皮书P2431页,其结构如下图所示,其中这个D
表示Size of gate: 1 = 32 bits; 0 = 16 bits
。
从图中可以看出,中断门描述符S位为0,TYPE域为1110。
2.3.4.2 中断门实验
-
1.查看当前idt表,从表中可以发现0x20这个位置的描述符是无效的,因此替换这里的描述符。
-
2.根据该地址,拼接出完整的中断门描述符。 此处的中断门描述符应该为
0040 ee00 0008 1030
-
3.编写代码如下,调用中断门。运行代码,可以发现读取成功。
#include "stdafx.h" #include <windows.h> #include <stdio.h> DWORD dwH2GValue; void _declspec(naked)GetH2GValue() { _asm{ pushad //寄存器压栈,其入栈顺序是:EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI . pushfd //将32位标志寄存器EFLAGS压入堆栈 mov eax,[0x8003F00C]//读高两G内存的值 mov ebx,[eax] mov dwH2GValue,ebx popfd popad iretd } } void PrintH2GValue() { printf("%x\n", dwH2GValue); } int main() { _asm{ INT 0x20 } PrintH2GValue(); getchar(); return 0; }
2.3.4.2 中断门注意事项
IDT表中各种类型的门,都可以通过 int [index] 汇编指令进入。使用 int 指令进入中断门,会对栈产生一定的影响。如果从 3 环进入 0 环,首先会切换成 0 环栈
,在 0 环栈压入 ss3, esp3, eflags3, cs3, eip3
。
2.3.5 陷阱门
陷阱门和中断门几乎一模一样,唯一的区别是中断门在执行的时候会对EFLAGS寄存器
产生影响,进入中断门会使得EFLAGS寄存器
的第九位IF(Interrupt Flag)位
置0。 IF(Interrupt Flag)位的作用是当接收到可屏蔽中断信号
时,如果该位为0,则cpu将对该中断信号不理睬。陷阱门描述符结构如下图:
测试时只需要把中断门描述符中TYPE从e(1110)
改成f(1111)
即可。
2.3.6 任务段(TSS)
在调用中断门和陷阱门的时候,一旦出现权限切换,当前进程的CS和SS也一定会发生改变,并且会有新的ESP。其中CS
是我们指定的,ESP
和SS
其实是从TSS(Task-state segment)
也就是任务状态段
中得到的。TSS
是一块内存,大小为104字节。TSS最大的价值就是可以快速保存和恢复所有的寄存器,也就解释了前面从R3切换到R0新的ESP,SS是从哪里来的(直接从TSS里面查ESP0,SS0即可)。TSS结构如下图所示(手册P2489):
2.3.6.1 CPU如何找到TSS?
CPU找TSS时,首先查看TR寄存器
,该寄存器中的值是在操作系统启动的时候从GDT
表中加载出来的。查找过程如下图所示(手册P2494):
2.3.6.2 TSS段描述符
TSS段描述符
是系统段中的一种。TSS段描述符特征为S位为0,TYPE域为1001(9)或1011(B)。其中TYPE的第三位的B表示是否已经加载到TR寄存器中。TSS段描述符
的结构如下图所示(手册P2491)。
TSS段描述符指向的TSS结构如下:
typedef struct TSS { DWORD link; // 保存前一个 TSS 段选择子,使用 call 指令切换寄存器的时候由CPU填写。 DWORD esp0; //0 环栈指针 DWORD ss0; //0 环数据段选择子 DWORD esp1; // 1 环栈指针 DWORD ss1; // 1 环数据段选择子 DWORD esp2; // 2 环栈指针 DWORD ss2; // 2 环数据段选择子 DWORD cr3;//这个地址需要实时查看来填 DWORD eip;//跳转后的代码函数地址 DWORD eflags; DWORD eax; DWORD ecx; DWORD edx; DWORD ebx; DWORD esp; DWORD ebp; DWORD esi; DWORD edi; DWORD es; DWORD cs; DWORD ss; DWORD ds; DWORD fs; DWORD gs; DWORD ldt; DWORD io_map; } TSS;
2.3.6.3 TR段寄存器的读写
- 1.将TSS段描述符加载到TR寄存器
-
- 指令:
LTR
- 指令:
-
- 注意:
-
-
- a.) LTR仅仅是改变了TR寄存器的值,没有真正的改变TSS
-
-
-
- b.) LTR执行只能在Ring0
-
-
-
- c.) 加载后TSS段描述符的Busy flag会变为1
-
-
2.读TR寄存器
-
- 指令:
STR
- 指令:
-
- 注意:
-
-
- STR只能读到TR的段选择子
-
2.3.7 任务门
任务门描述符
存储在IDT
表中,任务门描述符结构如下图所示(手册P2495):
其中Reserved
区域为保留区域,构造时填0
即可。TSS Segment Selector
指向GDT
表当中的一个任务段描述符。任务门的执行过程如下:
- 1.
INT N
,其中N为任务门在IDT中的序号 - 2.查IDT表找到任务门描述符
- 3.通过任务门描述符,查GDT表,找到任务段描述符
- 4.使用TSS段中的值修改寄存器
- 5.IRETD返回
2.3.7.1 任务门实验
- 1.编写一个测试用的入口函数,编译并获取该函数地址
DWORD test_esp; __declspec(naked) TSSTest(){//00401020 __asm{ mov test_esp,esp iretd } }
- 2.构造TSS段
TSS tss = {//0x00427a30 0x00000000,//link 0x12345678,//esp0,自定义的ESP0地址 0x00000010,//ss0 0x00000000,//esp1 0x00000000,//ss1 0x00000000,//esp2 0x00000000,//ss2 0x00000000,//cr3 0x00401020,//eip,测试入口函数的地址。 0x00000000,//eflags 0x00000000,//eax 0x00000000,//ecx 0x00000000,//edx 0x00000000,//ebx 0x12345678,//esp,自定义的ESP0 0x00000000,//ebp 0x00000000,//esi 0x00000000,//edi 0x00000023,//es 0x00000008,//cs 0x00000010,//ss 0x00000023,//ds 0x00000030,//fs 0x00000000,//gs 0x00000000,//ldt 0x20ac0000 // 这个位置暂时忽略,填这个值就行了 };
- 3.设计TSS段描述符,并将TSS段描述符安装到GDT表 根据TSS段描述符结构图,设计TSS段描述符:
eq 8003f048 0000e942`7a300068
- 4.设计任务门,安装到IDT表
刚刚TSS段描述符安排在了
8003f048
这个位置,因此设计任务门描述符如下为0000e500 00480000
。然后在IDT表里面找个空位安装一下。
eq 8003f500 0000e500`00480000
- 5.读取cr3,并使用int指令进入任务门
int main(int argc, char* argv[]) { printf("input cr3:\n"); &tss; scanf("%x", &(tss.cr3)); // 使用 !process 0 0查看 DirBase __asm { int 0x20 } printf("test_esp = %x\n", test_esp); return 0; }
完整代码如下:
// testTSS.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include "windows.h" DWORD test_esp; __declspec(naked) TSSTest(){//00401020 __asm{ mov test_esp,esp iretd } } typedef struct TSS { DWORD link; // 保存前一个 TSS 段选择子,使用 call 指令切换寄存器的时候由CPU填写。 DWORD esp0; //0 环栈指针 DWORD ss0; //0 环数据段选择子 DWORD esp1; // 1 环栈指针 DWORD ss1; // 1 环数据段选择子 DWORD esp2; // 2 环栈指针 DWORD ss2; // 2 环数据段选择子 DWORD cr3;//这个地址需要实时查看来填 DWORD eip;//跳转后的代码函数地址 DWORD eflags; DWORD eax; DWORD ecx; DWORD edx; DWORD ebx; DWORD esp; DWORD ebp; DWORD esi; DWORD edi; DWORD es; DWORD cs; DWORD ss; DWORD ds; DWORD fs; DWORD gs; DWORD ldt; DWORD io_map; } TSS; TSS tss = {//0x00427a30 0x00000000,//link 0x12345678,//esp0,自定义的ESP0地址 0x00000010,//ss0 0x00000000,//esp1 0x00000000,//ss1 0x00000000,//esp2 0x00000000,//ss2 0x00000000,//cr3 0x00401020,//eip,测试入口函数的地址。 0x00000000,//eflags 0x00000000,//eax 0x00000000,//ecx 0x00000000,//edx 0x00000000,//ebx 0x12345678,//esp,自定义的ESP0 0x00000000,//ebp 0x00000000,//esi 0x00000000,//edi 0x00000023,//es 0x00000008,//cs 0x00000010,//ss 0x00000023,//ds 0x00000030,//fs 0x00000000,//gs 0x00000000,//ldt 0x20ac0000 // 这个位置暂时忽略,填这个值就行了 }; int main(int argc, char* argv[]) { printf("input cr3:\n"); &tss; scanf("%x", &(tss.cr3)); // 使用 !process 0 0查看 DirBase __asm { int 0x20 } printf("test_esp = %x\n", test_esp); return 0; }