保护模式
什么是保护模式
x86 CPU的3个模式:实模式、保护模式、虚拟8086模式。
AMD64与Intel64
AMD在1999年的时候拓展了这套指令集,成为x86-64后改名叫AMD64,AMD是首先开发了64拓展,但是AMD的
64位拓展并不支持32位,后来Intel也开发了64位拓展成为Intel64并首先做到了向下兼容32位。
保护的特点
段的机制、页的机制,段页机制主要是为了保护操作系统的数据结构,比如保护系统的GDT表和IDT表,关键寄存器比如CR0寄存器
段寄存器结构
什么是段寄存器
mov dword ptr ds:[0x123456],eax(我们真正读写的地址是ds.base+0x123456)
段寄存器共有8个:ES(扩展段寄存器)、CS(代码段寄存器)、SS(堆栈段寄存器)、DS(数据段寄存器)、FS(标志段寄存器)、GS(全局段寄存器)、LDTR(局部描述符表寄存器)、TR(任务寄存器)
段寄存器结构
段寄存器有96位,但是可见部分只有16位其余80位为不可见部分
其中32位的Base、32位的Limit、16位Attribute这三部分不可见、16位的Selecter可见
段寄存器结构体定义
struct SegWent
{
WORD Selector;//可见
WORD Attribute;//不可见(该16位决定了段的可读可写可执行属性)
DWORD Base;//不可见(该段从哪里开始执行)
DWORD Limit;//不可见 //(应该是该段的大小范围)
}
段寄存器的读写
读取段寄存器
MOV AX,ES(只能读16位可见部分)
读写LDTR指令:SLDT/LLDT
读写TR的指令: STR/LTR
写段寄存器
比如:MOV DS,AX 写时写96位
段寄存器属性探测
段寄存器成员简介
类型 段寄存器 Selector(可变) Attribute Base Limit
拓展段 ES 0023 可读、可写 0 0xFFFFFFFF
代码段 CS 001B 可读、可执行 0 0xFFFFFFFF
堆栈段 SS 0023 可读、可写 0 0xFFFFFFFF
数据段 DS 0023 可读、可写 0 0xFFFFFFFF
标志段 FS 003B 可读、可写 0x7??(可变) 0xFFF
全局段 GS - - - -
段寄存器探测读写属性
(可以运行说明ds数据段寄存器是可读可写属性)
__asm{
mov ax,ds
mov ss,ds//堆栈段=数据段(ss=ds)
mov dword ptr ss:[xxxx],ecx
}
(无法运行,说明cs代码段寄存器可读但不可写)
__asm{
mov ax,cs
mov ds,ax//数据段=代码段DS=CS
mov dword ptr ds:[xxx],ecx
}
段寄存探测base属性
__asm{
mov ax,fs
mov gs,ax
mov eax,gs:[0] //现在的GS段寄存器实际上就是变成了FS段寄存器=fs.base+0
mov dword ptr ds:[xxx],eax//讲fs.base+0的内容给数据段的某个地址
}
段寄存器探测Limit属性
(会崩溃因为fs的Limit长度位0xFFF,越界导致崩溃)
__asm{
mov ax,fs
mov gs,ax//局部描述符表寄存器=标志段寄存器
mov eax,gs:[0x1000]//FS[0x1000]导致越界崩溃
}
段描述符与段选择子
GDT(全局描述符表)LDT(局部描述符表)
当执行MOV DS,AX类似的指令时,CPU会查表,根据AX的值来决定时查找GDT还是LDT,查表的什么位置和多少数据。
LDT局部描述符表目前没有在Windows操作系统中使用。
gdtr寄存器
这个寄存器存储了两个值,一个是GDT表的位置,另一个值时GDT表有多大。
GDT是全局描述符表,gdtr是寄存器,gdtr寄存器大小为48位。
gdtl
在windbg下使用r gdtl 命令可以查看GDT大小
windbg dd、dq命令
dd 内存地址,将按DWORD大小数据分为4行8列的内存数据,每个数据4个字节32位。
例子:
00000000 00000000 FFFFFFFF 12345687
dq 内存地址,将按QWORD大小数据分为2行8列的内存数据,每个数据8个字节共64位。
例子:
000000000`00000000 00cf9b00`0000fffff
L40 参数代表列出多少组数据,比如dq那么列出DWORD 40组数据,dq那么列出QWORD 40组数据。
段描述符
段寄存器有96位可见的Select只有16位,段描述符只有64位。
GDT表里面的每一个元素称为段描述符,每一个段描述符长度为8个字节。
段选择子
段选择子是一个16位的段描述符,该描述符指向了定义该段的段描述符。
MOV DS,AX其中AX就是段选择子。
段选择子数据结构:
| Index(15-3)| TI(2) | RPL(0-1)|
RPL:请求特权级别
TI:0=查找GDT表 1=查找LDT表
Index:处理器将索引乘以8在加上GDT或者LDT基地址,就是要加载的,要加载GDT中那个段标识符有Index决定。
解析段选择子
假设段选择子是1B那么拆成2进制数据为:11011 其中0-1是RPL特权级值为11,TI属性为0说明查询GDT表。
Index为11十进制为3说明查找GDT表3*8的位置,因为每个段描述符长度为8字节,下标从0开始也就是3*8,位置的段描述符。
加载段表述符至段寄存器
除了MOV指令,还可以使用LES,LSS,LDS,LFS,LGS指令修改寄存器
CS不能通过上述的指令进行修改,CS为代码段,CS的改变会导致EIP的改变,要修改CS必须保证CS与EIP一起修改。
char buffer[6];
__asm{
les,ecx,fword ptr ds:[buffer]//高2个字节给es,低四个字节给ecx。
}
注意:RPL<=DPL(在数值上)
GDT全局描述符表
段描述符属性P位和G位
GDT表中段描述符P位含义
P = 1 段描述符有效
P = 0 段描述符无效
说明段描述符是否有效
段寄存器Attribute结构对应段描述符位置
段寄存器Attribute结构对应GDT段描述符的12-23位的位置。
其中包含了S、DPL、P、SeqLimit、AVL、0、D/B、G等位统称为段寄存器的Attribute结构
段寄存器Base结构
段寄存器中Base值由GDT全局描述符表中3个部分的段描述符组成。
第一部分:高位高四字节24~31位
第二部分:高四字节的第0位和第7位
第三部分:低四字节的16位到31位
这三部分组成了Base结构的32位的值
段寄存器Limit结构
Limit结构由GDT段描述符中2部分的段描述符组成
第一部分:高四字节16~19位
第二部分:低0位到低15位
这两部分加起来总共是20位,表示最大范围FFFFF,段寄存器中Limit为32位。
段描述符G位作用
G位位于GDT表段描述符的高位的23位。
如果G位为0则表示段寄存器中Limit结构为字节表示最大范围为000FFFFF表述为20位。
如果G位为1则表示为4KB,那么段寄存器中Limit结构最大范围为FFFFFFFF表述为32位。
注意项
FS对应的段描述符比较特殊,查分后的值与段寄存器中的值不符合,其中牵扯到操作系统线程。
段描述符属性_S位_TYPE域
S位与Type域在段描述符中位置
S位位于高12位,Type域位于8-11位。
段描述符S位概述
快速判断:9或者F一定是数据段或代码段。
快速判断8或者E则是系统段。
S位为1则是代码段描述符或者是数据段描述符,S位为0则是系统段描述符。
GDT段描述符分类:
第一类:数据段或者代码段描述符
第二类:系统段描述符
S位一般与P位、DPL位、3位4字节组合,DPL一般都是成对出现。
如果S位为1,DPL位为11,P位为1,则dword显示数据为F。
如果S为为1,DPL位为00,P位为1,则dword显示数据为9。
可以通过上述方式快速定位S位具体是否是代码段或是数据段。
段描述符Type域概述
Type域的性质由S位决定,如果S位是1则可能是代码段或数据段。
决定是代码还是数据则由Type域决定。
使用windbg如果S位后面的Type域第11位如果为1则一定是代码段,为0则是数据段。
可以用简单方式来判断是代码段还是数据段,如果S位后面的是值小于8则是数据段否则为代码段。
段描述符Type域详解(代码段或数据段S=1)
Type域数据段位解析:
A:是否被访问过,操作系统启动的时候段描述符没有被加载过则A位为0,如果被访问过则为1。
W: 是否可写,如果为0则数据段不可写,如果为1则数据段可写。
E:拓展位,当拓展位为0则向上拓展,为1则向下拓展。
向上拓展:以fs段描述符为例,如果是向上拓展则fs.base的起始地址+limit范围地址是有效的,其余地址无效。
向下拓展:以fs段描述符为例,如果是向下拓展则fs.base的起始地址+limit范围地址是无效的,其余地址有效。
11位: 10位(E): 9位(W): 8位(A):
0 1 1 1
<8 向下拓展 可写 被访问过
Tyep域数据段解析:
A:(访问位)是否被访问过,与数据段描述一致。
R:(可读位)当前段是否可读,0不可读,1可读。
C:(一致位)C=1一致代码段,C=0非一致代码段。
段描述符Type域概述(系统段S=0)
Type域系统段含义描述:
11位 10位 9位 8位 描述:
0 0 0 0 Reserved
0 0 0 1 16-Bit TSS(Available)
0 0 1 0 LDT
0 0 1 1 16-Bit TSS(Busy)
0 1 0 0 16-Bit Call Gate
0 1 0 1 Task Gate
0 1 1 0 16-Bit Interrupt Gate
0 1 1 1 16-Bit Trap Gate
1 0 0 0 Reserved
1 0 0 1 32-Bit TSS(Available)
1 0 1 0 Reserved
1 0 1 1 32-Bit TSS(Busy)
1 1 0 0 32-Bit Call Gate
1 1 0 1 Reserved
1 1 1 0 32-Bit Interrupt Gate
1 1 1 1 32-Bit Trap Gate
段描述符属性_DB位
段描述符属性DB位概述
D/B位于GDT表中高位的第22位,长度为1字节。
D/B位对CS段、SS段、向下拓展的数据段有效。
DB位对CS段的影响
D=1采用32位寻址方式。
D=0采用16位寻址方式。
指令前缀67可改变寻址方式。
DB位对SS段的影响
D=1 隐式堆栈访问指令(如:PUSH POP CALL)使用32位堆栈指针寄存器ESP。
D=0 隐式堆栈访问指令(如:PUSH POP CALL)使用16位堆栈指针寄存器SP。
DB位对向下拓展的数据段
D=1段上线为4GB(32位最大范围)。
D=0段上线为64KB(16位最大范围)。
段权限检查
CPU分级
CPU分为4个级别Ring0、Ring1、Ring2、Ring3。
CPU之所以分级是比如有些特权指令只能在Ring0下运行。
如何查看程序处于几环
CPL(Current Privilege Level)当前特权级。
CPL位于CS段选择子中的RPL请求特权级别中,比如段选择子请求特权级别为11则16进制为3说明运行在3环。
SS堆栈段和CS代码段请求特权级别永远是一致的。
DPL(描述符特权级别)概述
DPL存储在段描述符中,规定了访问该段所需要的特权级被是什么。
比如在实际使用中你想访问一个GDT表那么你需要具备一个什么特权。
例子:mov DS,AX
如果AX指向的段DPL=0但是当前CS中的RPL=3那么这行指令是不会成功的。
段权限检查(数据段)
CPL<=DPL 并且RPL<=RPL(在数值上的比较)
总结:
CPL:CPU当前权限级别
DPL:如果你想访问这个GDT你应该具备什么样的权限
RPL:用什么权限去访问一个段
RPL作用:
我们本可以用读写打权限去打开一个文件,为了避免出租哦有时候我们
只使用只读权限去打开。
代码跨段跳转流程
代码间的跳转(段间跳转非调用门之类的)
段间跳转,有两种情况,即要跳转一直代码段还是非一致代码段。
同时修改CS与EIP的指令:
JMP FAR / CALL FAR / RETF / INT / IRETED
之所以用到同时修改CS和EIP的指令是因为,指令集并没有只修改CS段的指令。
如果修改了CS段那么EIP就不准确就没有意义了。
JMP FAR 概述
JMP FAR例子: JMP 0X20:0X004183D7
0x20实际上就是段选择子拆分后:RPL=00 TI=0 Index=4。
TI=0查GDT表,Index=4找到对应的段描述符
四种情况可以跳转:代码段、调用门、TSS任务段、任务门则执行成功。
比如是数据段那么将执行失败。
权限检查:
如果是非一致代码段要求CPL==DPL并且RPL<=DPL。
如果是一致代码段,要求CPL>=DPL。
加载段描述符:
通过上述检查后cpu会将段描述符加载到CS段寄存器中。
代码执行:
CPU将CS.Base+Offset的值写入EIP然后执行CS:EIP处的代码,段跳转结束。
JMP 0X20(BASE):0X004183D7(Offset)
0x004183D7是EIP地址。
一致代码段和非一致代码段
一致代码段(共享段):操作系统的某些代码可以被R3层直接调用且不会对操作系统造成破坏。
非一致代码段:不希望R3层直接访问的数据。
跨段跳转五步骤
1.段描述符拆分
2.查表得到段描述符
3.权限检查
4.加载段描述符
5.代码段执行
总结:
对于一致代码段:也就是共享的段
特权级别高的程序不允许访问特权级别低的数据:核心态不允许访问用户态数据。
特权级低的程序可以访问到特权级别高的数据,但特权级别不会改变:用户态还是用户态。
对于普通代码段:也就是非一致代码段:
只允许同级访问。
绝对禁止不同级别访问:核心态不是用户态,用户态不是核心态。
长调用与短调用
CALL FAR(长调用)
CALL FAR比JMP FAR要复杂,JMP并不影响堆栈,但CALL指令会影响。
JMP FAR可以实现段间的跳转,如果要实现跨段的调用就必须要学习CALL FAR,也就是长调用。
短调用CALL
指令格式: CALL 立即数/寄存器/内存
发生改变的寄存器:ESP EIP
堆栈变化和经典CALL一样
长调用(跨段不提权)
长调用跨段不提权指的是之前CPL=3他调用的那个段选择子也是CPL=3那么就是跨段不提权。
指令格式: CALL CS:EIP(EIP是废弃的)
CALL FAR之后栈里会额外加入调用者CS,传统CALL是压入返回地址。
长调用相比短调用先入栈了调用CS在入栈了返回地址。
RETF指令:是将返回地址和调用者CS段弹栈恢复,如果用RET则不行栈将不平衡。
需要注意的是CS是一个段选择子,查询GDT表中的一个段描述符,
这个段描述符必须要是一个调用门,最终指向的代码并不是由EIP决定的。
发生改变的寄存器:EIP ESP CS
长调用(跨段并提权)
CALL FAR(跨段提权)可以通过调用门提权,提升CPL权限。
跨段并提权首先将调用者SS、调用者ESP、调用者CS、返回地址依次入栈。
发生改变的寄存器:ESP EIP CS SS
一旦涉及到权限的变化,那么堆栈也将发生切换,发生切换的堆栈,
比如从3环堆栈切换到了0环堆栈。
总结:
1.跨段调用时,一旦有权限切换,就会切换堆栈。
2.CS的权限一旦改变,SS权限也要随之改变,CS与SS等级必须一样。
3.JMP FAR只能跳转到同级非一致代码段,但CALL FAR可以通过调用门提权,提升CPL权限。
门描述符
调用门执行流程
指令格式:CALL CS:EIP(EIP是废弃的)
执行步骤:
1.根据CS的值,查GDT表找到对应的段描述符,这个段描述符是一个调用门。
2.在调用门描述符中存储另一个代码段段的选择子。
3.选择子指向的段 段.Base+偏移地址就是真正要执行的地址。
门描述符
S位=0(系统段)Type域=1100 这两个条件满足就是调用门这个时候他就是门描述符。
门描述符低32位里面的16到第31位是你要真正指向的段选择子。
过程:通过调用门找到段选择子。
门描述符两段偏移:
第一段偏移位于低0位到16位
第二段偏移位于高16位到31位
这两段偏移组成了一段32位的地址。
段选择子中段的Base加上两个偏移组成的32为地址就是真正执行的位置。
windows并没有使用调用门,但是可以自己构造一个调用门。
构造一个调用门(无参 提权)
高32位:0000(偏移2)EC(P为=1 DPL=11(因为门的使用在3环)S=0 Type=1100)00(不传参)
低32位:你要提权过去段选择子比如0x0008不提权直接使用这个段选择子将失败,偏移不清楚0000。
调用门代码:
int main(){
char buff[6];
*(DWORD*)&buff[0]=0x000000000;//EIP废弃
*(WORD*)&buff[4]=0x48;//段选择子
__asm{
call fword ptr[buff]//长调用并提权
}
getchar();
return 0;
}
sgdt 指令
sgdt 指令可以在3环也可以在0环运行,他会将gdtr寄存器能容返回,获得GDT表位置。
例子:sgdt fword ptr ds:[0x0117337C]
调用门(有参)
有参数调用门,可以通过push压栈参数。
调用门总结:
1.当通过门,权限不变的时候,只会PUSH两个值:CS、返回地址新的CS的值由调用门决定。
2.当通过门,权限改变的时候,会PUSH四个值:SS ESP CS 返回地址,新的CS值由调用门决定,新的SS和ESP由TSS提供。
3.通过调用门时,要执行那个代码有调用门决定,但使用RETF返回时,由堆栈中压入的值决定,就这就是所,进门时只能按照指定路线走,出门时可以翻墙(只要改变堆栈里面的值就可以想去哪去哪)。
4.可以通过新建门的方式出去,也就是Call。
|