Windows XP 段保护(二)
ʕ •ᴥ•ʔ ɔ:
1 段权限检查
- 根据段选择子找到段描述符之后,将段描述符加载到段寄存器之前会进行权限检查(是否将该段描述符加载到段寄存器中)。
- Windows没有使用R1、R2
- CS、SS段寄存器的低2位存储的就是CPL(RPL是段选择子的低2位,CPL特指CS、SS低2位),当前程序的特权级别。Windows中只能为0x00或0x11
G位:决定limit的大小
- 1:limit = 0xFFFFFFFF
- 0:limit =0xFFFFF
D/B位:地址空间的大小
- 1:4GB(FFFFFFFF)
- 0:64K(FFFF)
RPL:段选择子的权限(我以什么样的特权去访问你)
DPL:段的权限(你要访问我,你应该具有什么特权)
CPL:CPU当前的权限级别(CPL特指CS、SS低2位)
数据段(DS)的权限检查和CS、SS段不同。
1.1 段权限检查
段权限检查步骤:
1、段选择子拆分
2、查表得到段描述符
3、权限检查
4、加载段描述符到段寄存器
5、代码执行EIP
数据段权限检查(满足条件,才能成功访问数据段。):
CPL <= DPL 并且 RPL <= DPL (数值上的比较)
代码段权限检查:
- 非一致代码段:CPL = DPL 并且 RPL <= DPL
- 一致代码段:CPL >= DPL
1.2 练习
<1> 在3环能加载的数据段有哪些?
3环CPL=3,只能加载DPL=3的数据段。
<2> 在0环能加载的数据段有哪些?
0环CPL=0,满足RPL<=DPL的数据段都可以加载。
<3> 详细描述这下面代码的执行过程:
mov ax,0x23
mov ds,ax
段选择子是0x0023,RPL=11b=3,属于最低权限,只能访问DPL=3的数据段。而CPL则无影响,不管是0环还是3环,都满足CPL<=RPL,只要RPL满足,CPL也一定满足。
当执行 mov ds,ax 时,CPU先解析段选择子0023,然后去GDT表找段描述符,检查段描述符P位是否有效,然后检查S位,确认是数据段或代码段,然后检查TYPE域确认是数据段,然后看DPL是否等于3.只要上述条件都满足,则mov指令执行成功,只要有一条不满足,mov失败。
2 断间跳转JMP FAR
3环执行JMP FAR并不能提权(CPL不改变)。
可参看:《(8)JMP FAR段间跳转》、《深耕保护模式(二)》
要点回顾:
段寄存器:ES、CS、SS、DS、FS、GS、LDTR、TR
段寄存器读写:除CS外,其他的段寄存器都可以通过MOV,LES,LSS,LDS,LFS,LGS,LTR(仅在0环之行)指令进行修改。
CS为什么不可以直接修改呢?
CS为代码段,CS的改变意味着EIP的改变,改变CS的同时必须修改EIP和SS,SS改变就必须修改堆栈,所以我们无法使用上面的指令来进行修改。
段间跳转,有两种情况,即要跳转的段是一致代码段还是非一致代码段(参见代码段type域)。
同时修改CS与EIP的指令:
JMP FAR / CALL FAR / RETF / INT /IRETED
只改变EIP的指令:
JMP / CALL / JCC / RET
一致代码段:也就是共享的段,要求:CPL >= DPL。
- 特权级高的程序不允许访问特权级低的数据:核心态不允许访问用户态的数据
- 特权级低的程序可以访问到特权级高的数据,但特权级不会改变:用户态还是用户态
非一致代码段:普通代码段(Windows使用),要求:CPL = DPL 并且 RPL <= DPL。
- 只允许同级访问
- 绝对禁止不同级别的访问:核心态不是用户态,用户态也不是核心态
代码间的跳转(段间跳转 非调用门之类的) 执行流程
JMP 0x20:0x004183D7 CPU如何执行这行代码?
(1)段选择子拆分
0x20 对应二进制形式 0000 0000 0010 0000
- RPL = 00
- TI = 0
- Index = 4
(2) 查表得到段描述符
TI = 0 所以查GDT表
Index = 4 找到对应的段描述符,并不是所有的段描述符都可以跳转。
四种情况可以跳转:代码段、调用门、TSS任务段、任务门。
(3)权限检查
如果是非一致代码段,要求:CPL = DPL 并且 RPL <= DPL。
如果是一致代码段,要求:CPL >= DPL。
简要说明什么是一致代码段什么是非一致代码段。
一致代码段又称共享代码段。假设操作系统有一段代码是提供了某些通用功能,这段代码并不会对内核产生影响,并希望这些功能能够被应用层(三环程序)直接使用,即可让一直代码段去修饰这块代码。这也是为什么一致代码段要求:CPL >= DPL(当前权限比描述权限低)就可以了。这段代码就是给低权限的应用使用的。
非一致代码段相反,严格控制权限。
(4)加载段描述符
通过上面的权限检查后,CPU会将段描述符加载到CS段寄存器中.
(5)代码执行
CPU将 CS.Base + Offset 的值写入EIP 然后执行CS:EIP处的代码,段间跳转结束.
注意:直接对代码段进行JMP 或者 CALL的操作,无论目标是一致代码段还是非一致代码段,CPL都不会发生改变.如果要提升CPL的权限,只能通过调用门。
2.1 练习:非一致性代码段跳转
条件:非一致性代码段跳转,只允许同级访问:CPL=3的CPU只允许访问DPL=3的代码
要求分析:
(1)CPL=DPL=0x11,RPL<=DPL,则RPL可为0x00或0x11,TI=0,则选择子:0x–000/0x–011;
(2)因为要修改CS,所以段描述符的Type域应该是Code —- Type最高位为1,则Type为:0x1xxx;
(3)非一致性代码—-Type次高位为0,则Type为:0x10xx —- >=0x8;
(4)P、DPL、S(因为要修改CS,S=1)位分别为:0x1111 —- F。
段选择子分别为以下两种情况:
本练习以段选择子中RPL = 0x11为例
该描述符即指向一个非代码段,我们需要构造一个段选择子来找到该描述符,然后将该段描述符加载到CS寄存器,最后执行跳转。
步骤一:
根据段描述符构造段选择子:
(1)该段描述符为第四个,Index=3
(2)TI=0(查GDT)
(3)RPL=0x11,如果此例RPL=0x00,则修改CS时会发生错误。因为非一致性代码跳转不改变CPU权限CPL。
(4)段选择子=0x0011 0 11=0x1B
(5)Base=0,G=1则Limit=0xFFFFFFFF,则Offset可随意构造
(6)JMP FAR格式:CS:EIP=1B:0xxxxxxxx,跳转地址为:CS.Base+Offset
(7)如果执行成功,应该会修改CS和EIP
步骤二:
打开OD观察此时的CS=0x1B、EIP=0x00441EC如下图
步骤三:
为了能看到CS变化,则当前的段描述符的位置需要修改,可以将该段描述符复制到0x8003F048处,该处目前为0(P=0),如果复制到一个原先P=1的段描述符可能会卡死或者蓝屏。
此时段选择子Index=9 —- 0x1001 0 11=0x4B,在XP中,修改此时的EIP地址处:JMP FAR 004B:0x0044420C,单步F8看CS、EIP变化。
结论:
非一致性代码段JMP FAR跳转时,满足段权限检查:CPL=DPL=0x11,RPL<=DPL即能成功。
如果此例RPL=0x00,则修改CS时会发生错误。因为JMP FAR不改变CPU权限CPL。
所以3环的非一致性代码跳转成功条件:CPL=DPL=0x11,RPL<=DPL,RPL=0x11。
2.2 练习:一致性代码段跳转
条件:只允许低权限的程序访问高权限的代码:CPL=3的CPU只允许访问DPL=0的代码。
要求:
(1)目前CPL=3,则P=1,DPL=00,S=1(因为要修改CS。DATA/CODE),1001=9
(2)Type:Decimal=1(代码段),C=1(一致性代码),11xx>=0xC,由于此时的的CPL=0x11,因为不能修改特权级别,所以RPL必须RPL=0x11
(3)构造段选择子0x004B
直接构造一个:00cf9F00`0000ffff
重启一下OD,让CS复位如下图。
OD中EIP指向的地址构造:JMP FAR 004B:0x0044420D,F8单步看CS、EIP变化。
结论:
一致性代码段JMP FAR跳转时,满足段权限检查:CPL>DPL即能成功。
如果此例RPL=0x00,则修改CS时会发生错误。因为JMP FAR不改变CPU权限CPL。
所以3环的一致性代码跳转成功条件:CPL>DPL,RPL=0x11。
2.3 总结
1、为了对数据进行保护,普通代码段是禁止不同级别进行访问的。用户态的代码不能访问内核的数据,同样,内核态的代码也不能访问用户态的数据.
2、如果想提供一些通用的功能,而且这些功能并不会破坏内核数据,那么可以选择一致代码段,这样低级别的程序可以在不提升CPL权限等级的情况下即可以访问.
3、如果想访问普通代码段,只有通过“调用门”等提示CPL权限,才能访问。
3 长调用与短调用
通过JMP FAR可以实现段间的跳转,如果要实现跨段的调用就必须要学习CALL FAR,也就是长调用。也叫代码跨段跳转CALL FAR。
CALL FAR 比JMP FAR要复杂,JMP并不影响堆栈,但CALL指令会影响。
- JMP FAR:段间跳转,不能提权。
- CALL FAR:跨段跳转,可以提权。
Windows只使用非一致性代码段,没有使用一致性代码段。
CPU提供了任务切换,但是Windows没有使用CPU提供的机制,而是自己用操作系统实现了线程切换。
3.1 短调用
指令格式:
1 | CALL 立即数/寄存器/内存 |
堆栈变化:
3.2 长调用(不提权)
指令格式:
1 | CALL CS:EIP(EIP是废弃的) |
CALL FAR 和 RETF 一般是成对的,RETF 的执行流程(权限控制)和 CALL FAR 是一样的,所以下面的堆栈图我就不画 RETF 了。
长调用跨段不提权时:3环跳转到另一个3环代码段,不会切换堆栈。
发生改变的寄存器:ESP EIP CS
无参数的堆栈变化:
3.3 长调用(提权)
提权,提的是CS寄存器的低两位CPL,而不是段选择子的低两位RPL。
指令格式:
1 | CALL CS:EIP(EIP是废弃的) |
提权无参数:
提权有参数:
- CALL CS:EIP EIP废弃不用,真正要跳转的地方是由段描述符装载进CS后的CS决定。
- 堆栈的变化:同一个线程从3环的堆栈切换到0环的堆栈。
- 在段描述符加载进CS之前,会将CS保存到堆栈,方便RETF回来时恢复原来的CS。
- 跨段的意思就是修改CS,不提权:CS段描述符的DPL=CPL时,提权:CS段描述符的DPL < CPL时。
- RETF回去时也要进行权限检查。
- 不管提不提权,调用门描述符的DPL=0x11,如果不为3那么我们将无法访问到门描述符,敲门的资格都没有。
长调用的EIP是废弃的,所有信息都根据CS获取,这个CS是段选择子,指向GDT表中的一个特殊的段描述符,这个段描述符叫调用门。
Windows中没有使用调用门,需要自己去构造调用门。
跨段并提权的调用:新的CS、EIP来自段描述符,但是新的R0下的SS、ESP来自TSS(可理解为一块内存)。
总结:
- 跨段调用时,一旦有权限切换,就会切换堆栈(从R3的堆栈切换到R0的堆栈)。
- CS的权限一旦改变,SS的权限也要随着改变,CS与SS的等级必须一样。
- JMP FAR 只能跳转到同级非一致代码段,但CALL FAR可以通过调用门提权,提升CPL的权限。
SS与ESP从哪里来?参见TSS段。
4 调用门(无参)
指令格式:
1 | CALL CS:EIP(EIP是废弃的) |
执行步骤:
- 根据CS的值查GDT表,找到对应的段描述符,这个描述符是一个调用门。(S=0,Type=1100)
- 在调用门描述符中存储另一个代码段段的选择子。(具体看下图低四字节16到31位)
- 该选择子指向的段段.Base + 调用门偏移地址 = 就是真正要执行的地址。
为了能够访问不同特权级的代码段,处理器提供了一个特殊的描述符集合,叫做门描述符。以下是四种门描述符:
- 调用门
- 陷阱门
- 中断门
- 任务门
调用门段描述符结构:
需要注意的几点:
- GDT表中四种情况可以跳转:代码段、调用门、TSS任务段、任务门。
- Windows中没有使用调用门,需要自己去构造调用门。
- 如果从3环使用调用门,则DPL要为3,如果不为3那么我们将无法访问到门描述符,敲门的资格都没有。
- ParamCount是传参用的,为参数个数。
- Segenment Selector是段选择子,指向要加载到CS的的段描述符,也就是要执行的段描述符,RPL=0x00/0x11。
4.1 调用门段权限检查
4.2 练习:使用调用门进行调试
本题要求:写代码实现一个调用门进行提权,并观察在3环和0环下的ESP、CS、SS段寄存器变化,观察3环进到0环时的堆栈。
先使用自己代码的段选择子找到调用门,然后根据调用门去找要调用的段描述符。
调用门描述符的DPL必须=0x11,因为使用VC6做实验,低4字节高16位(段选择子)要指向一个0环的代码段描述符才能完成本实验。
要执行的程序入口地址 = Call_Gate.Offset ⊕ CS.Base
步骤:
构造调用门段描述符,0x00000000`00000000;(是DPL要为3,否则在3环无法访问到门描述符)
- P=1,DPL=11,S=0,0x1110=0xE,0x0000E000`00000000;
- 调用门Type=0x1100=0xC,0x0000EC00`00000000;
- 构造选择子:调用门来执行提权,需要根据自身结构中隐含的段选择子去找一个DPL=00的CS段描述符,可构造0x0008/0x000B,0x0000EC00`00080000。剩下还需要构造一个偏移地址让CS去执行。
将段描述符写入GDT表中P=0的上面去,否则可能会卡死或蓝屏。
构造偏移地址:
1 |
|
如图:
要执行的程序入口地址:0x00401020,则调用门段描述符:0x0040EC00`00081020。
观察3环下:ESP=0x0012FF28,CS=0x001B,SS=0x0023。
注意:在XP中继续执行时如果不取消此时设置的断点的话,从0环返回时将会卡死(目前应该在0环调试状态下)。
此时,取消断点,F5执行,观察Windbg。
观察0环下:ESP=0xAA15ADD0,CS=0x0008,SS=0x0010。
注意:
- 函数
GetGdtRegister()
虽然是我们在3环写的,但是通过调用门去执行的时候,该函数的执行权限为0环。 - 提权后,通用寄存器的值不会发生改变。
- 我们的代码虽然写的是三环程序的int3,但是由于这里权限已经提升,断点异常已经不再是三环程序处理(内核相比应用层具有优先处理权),应有内核层处理。这里直观的感受就是,0环调试器(windbg)断点了,vc6无法断点。
4.3 练习:在0环读取GDT表数据
既然已经提权到0环权限,那么我们就可以写只能在驱动开发中才能写的代码。
本题题目:读取gdt表打印、读取高2G内存中的值并打印。(三环权限是不能够读取高2G内存的,属于内核管理)
1 |
|
F9下断点,获取FunctionHas0CPL()函数的入口地址。
修改调用门段描述符。0040EC00`00081030
将代码中的
int 3
注释后执行,否则无法正常从0环返回到3环。在XP的VC6中F5执行如下图。
5 调用门(有参数)
有参数的调用门相比无参的调用门主要有两点变化:
- 调用门段描述符带有参数的个数(如下图1)
- 从3环堆栈切换到0环的堆栈时参数在栈中的位置(如下图2)。
1 |
|
构造调用门有参数的段描述符:0040EC03`00081020。
回到XP中取消断点,F5执行,此时将会在断点在0环的int 3。观察堆栈和预期一样。
需要注释代码中的
int 3
才可以正常返回,执行结果如下图。
6 调用门阶段性测试
关于0环的翻墙:3环的代码通过调用门执行0环权限下的代码后,RETF回来时通过修改返回地址跳到其他地方,即称为翻墙。
题目:
1、构造一个调用门,实现3环读取高2G内存。
2、在第一题的基础上进行修改,实现通过翻墙的方式返回到其他地址。
3、在第一题的基础上进行修改,在门中再建一个门跳转到其他地址。
要求:
代码正常执行不蓝屏。
6.1 CALL FAR翻墙
翻墙即不走原来的RETF,而是修改堆栈中保存的返回地址,跳转到其他地址。
本题以无参函数示例。
步骤:
- 构造2个函数,提权后将第一个函数的返回地址改为第二个函数的入口地址。
1 |
|
如下图,第二个函数的入口地址是
0x00401050
,代码中修改如下。根据第一个函数的入口地址构造调用门段描述符,0040EC00`00081030
执行结果如下:
6.2 双调用门
题目:通过调用门提权后,在第一个函数中再使用一个调用门去执行第二个函数。
注意:这里使用了2个调用门段描述符,第1个调用门段描述符的DPL=0x11,第2个调用门段描述符的DPL=0x00。因为第一个函数去调用第二个函数是在0环权限下,所以第二个调用门的DPL=0x00。
步骤:
构造2个函数,提权后进入第一个函数,在第一个函数中再使用调用门去执行第二个函数。
第一个函数执行成功时,设置bFlag_1=1。
第二个函数执行成功时,设置bFlag_2=1。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
DWORD dwHigh2GValue;
BYTE bFlag_1 = 0;
BYTE bFlag_2 = 0;
char Gate_1[6] = {0,0,0,0,0x48,0}; //CS_1:8003f048 0040EC00`00081030
char Gate_2[6] = {0,0,0,0,0x90,0}; //CS_2:P=1,DPL=0x00,S=0,0x1000=8,调用门Type=0x1100=C
//另一个门的CS的位置8003f090
//CS_2:8003f090 00408C00`00081060
// 该函数通过 CALL FAR 调用,使用调用门提权,拥有0环权限
void __declspec(naked) FunctionHas0CPL_1()
{
__asm
{
//int 3 注意这里的断点在调试时可以用,但是在真正运行时不能
//写到函数里,不然会导致程序崩溃
pushad
pushfd
// 读取了GDT表第二项的低4字节
mov eax,0x8003f008
mov eax,[eax]
mov dwHigh2GValue,eax
// 修改返回地址,跳转到Exit函数执行
//mov eax,0x401060
//mov [esp+0x24],eax
test eax,eax
mov al,0x1
mov bFlag_1,al //mov byte ptr ds:[bFlag_1],al
call fword ptr ds:[Gate_2]
popfd
popad
retf // 注意堆栈平衡,写错蓝屏
}
}
void __declspec(naked) FunctionHas0CPL_2()
{
__asm
{
//int 3 注意这里的断点在调试时可以用,但是在真正运行时不能
//写到函数里,不然会导致程序崩溃
pushad
pushfd
test eax,eax
mov al,0x1
mov bFlag_2,al //mov byte ptr ds:[bFlag_2],al
popfd
popad
retf // 注意堆栈平衡,写错蓝屏
}
}
int main(int argc, char* argv[])
{
__asm
{
call fword ptr ds:[Gate_1] // 长调用,使用调用门提权
}
printf("%08x\nGate_1:%x,Gate_2:%x\n",dwHigh2GValue,bFlag_1,bFlag_2);
getchar();
return 0;
}获取两个函数的入口地址准备构造两个调用门的段描述符。
构造2个调用门的段描述符,第一个放在一个P=0的0x8003F048。第二个放在P=0的0x8003F090,如下图。
eq 8003F048 0040EC00`00081030
eq 8003F090 00408C00`00081060(DPL=0x00)
回到XP的VC6中F9取消断点后F5执行,结果如下图。