Windows XP 段保护(三)
ʕ •ᴥ•ʔ ɔ:
1 中断门
回顾:
保护模式保护的是内存、特权指令(保护寄存器)。
- 调用门3环堆栈切换到0环堆栈:ESP0、SS0来自TSS,而TSS由Windows的线程提供。
- 实际上3环进0环是在一个线程上执行的,该线程从3环进到0环,维护两个堆栈(一个是3环的,一个是0环的),线程从0环返回时并不会破环0环的堆栈,下次该线程再进0环时将从ESP0处继续使用堆栈。
- 一个核只有一个TSS(内存块),该CPU共享。但是不同的线程一般TSS值不同(TSS可修改,线程切换时将值填到TSS中)。
Windows没有使用调用门,但是使用了中断门:
- 系统调用(老的CPU,从3环到0环。新的CPU直接通过快速调用)
- 调试(int 3)
执行调用门的指令:CALL CS:EIP
,CS是段选择子,包含了查找GDT表的是一个索引。
但当CPU执行如下指令:INT N,查询的却是另外一张表,这张表叫IDT。
IDT即中断描述符表,同GDT一样,IDT也是由一系列描述符组成的,每个描述符占8个字节。但要注意的是,IDT表中的第一个元素不是NULL。
IDT 表包含三种门描述符(具体参看Intel开发手册卷3的6.10 IDT):
- 中断门描述符
- 任务门描述符
- 陷阱门描述符
使用windbg查看IDT表的基地址和长度:
关于GDTR、IDTR、LDTR、TR寄存器可查看Intel开发手册卷3的2.4内存管理寄存器。
中断门描述符:可以看到,中断门是没有参数的。
D=1,32位的中断门描述符,Type=0x1110=0xE。
D=0,16位的中断门描述符,Type=0x0110=0x6。
本文描述的是软件中断(软中断)。
使用INT N来实现的中断是软中断,N=0~255,N称为中断向量,也是IDT表的索引。
N=32~255为用户自定义的中断,当前P=0。
各类中断号对应的含义如下图:
中断门的执行流程:
指明中断门的段选择子指向的GDT段描述符需要是一个代码段描述符(和调用门中的段选择子一样都是指向代码段描述符)。
中断门执行流程:
当执行
int n
时,以n为索引(下标)去IDT表找对应的描述符,这个n是几就找到IDT表对应的第n+1个(从0开始)。获取到中断门段描述符后检查权限,进行段权限检查(没有RPL,只检查CPL,CPL>=DPL)。
权限检查通过后,获取新的段选择子(中断门描述符16-31位)与之对应的GDT表中的段描述符的Base,再加上IDT表中的Offset作为EIP去跳转,即$EIP = IDT-Interrupt-Segment-Description.Offset ⊕ GDT-Descriptor.Base$
- 中断门的堆栈切换(中断门没有参数):
中断门的返回:
INT N
指令:
1、在没有权限切换时,会向堆栈PUSH3个值,分别是:
CS EFLAG EIP(返回地址)
2、在有权限切换时,会向堆栈PUSH5个值,分别是
SS ESP EFLAG CS EIP
在中断门中,不能通过RETF返回,而应该通过IRET/IRETD指令返回。
中断门的返回:
- 16位:
iret
- 32位:
iretd
- 64位:
iretq
调用门的返回:retf
。
iretd比retf多返回一个EFLAG寄存器,该寄存器在中断门执行时,将IF位清零(EFLAG寄存器下标位9的位),如果IF位为0,那CPU将不再接收可屏蔽中断。(具体可见Intel手册卷3第六章中断和异常)
从硬件层面来看,中断分为可屏蔽(受IF位影响,IF=0,可屏蔽中断,中断产生时CPU不管)和不可屏蔽。
1.1 中断门段权限检查
- 中断门描述符DPL > CS段描述符的DPL,即中断门描述符DPL=CPL=3>CS.DPL=0。
- 中断门描述符RPL=0/3。
1.2 调用门和中断门的区别
- 调用门通过call far指令执行,但中断门通过int指令执行。
- 调用门查GDT表,中断门查IDT表后再查GDT表。
- call cs:eip中的CS是段选择子,由三部分组成。但int [index]指令中的index只是索引,中断门不检查RPL,只检查CPL。
- 调用门可以有参数,但中断门没有。
- 调用门提权时push了四个寄存器:EIP(返回地址) CS ESP SS,返回时用RETF指令返回。中断门提权时push了五个寄存器:EIP(返回地址) CS EFLAGS ESP SS,返回时用IRETD指令返回。
1.3 练习:使用中断门
题目:构造中断门读高2G的内存,并观察在3环、0环的EFALG寄存器的变化。
定义一个裸函数,在里面读取一下IDT表的一个值,以证明自己的CPL是0。
观察IDT表,找一个P=0的来构造中断门描述符。
该项在IDT表的下标为32,则中断可构造:int 32
根据中断门描述符结构,构造一个中断门描述符去查找GDT表。$EIP = Interrupt-Segment-Selector.Offset ⊕ GDT-Descriptor.Base$
初步构造:
- 偏移:0000 0000
- P=1
- DPL=11(DPL=CPL=3>CS.DPL=0)
- S=0
- Type=1110=0xE
- 中断门无参数
- 段选择子:
- RPL=00/11,随意(比较CPL、DPL)
- TI=0
- Index=1
- 00001 0 00=0x08,00001 0 11=0x0B
- 0000EE00`00080000或者0000EE00·000B0000
根据代码构造中断门描述符。
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
BYTE IDTItem0[8];
// R0函数,读取了IDT表第一项
// 00401020
void __declspec(naked) R0Function()
{
__asm
{
//int 3 调试用的
pushad
pushfd
mov eax,0x8003f400
mov ebx,[eax]
mov ecx,[eax+0x4]
mov dword ptr ds:[IDTItem0],ebx
mov dword ptr ds:[IDTItem0+0x4],ecx
popfd
popad
iretd // iret 会蓝屏,因为 iret的硬编码是66CF,32位下应该使用iretd,硬编码是CF
}
}
int main(int argc, char* argv[])
{
__asm
{
int 32
}
printf("%08x %08x\n", *(PDWORD)IDTItem0, *(PDWORD)((PBYTE)IDTItem0+0x4));
getchar();
return 0;
}则中断门描述符为:0040EE00`00081020或者0040EE00·000B1020。
观察到此时:EFLAG寄存器ELF=0x216。
将中断门描述符写入到IDT表。
回到XP的VC6中在裸函数代码第一行加个
int 3
方便查看此时0环的堆栈情况。(需要重新编译查看裸函数的入口地址,如果改变需要修改IDT表的第32项),F5执行如下图。此时,堆栈中比提权调用门多一个3环的EFLAG寄存器。且EFLAG0=0x16,EFLAG3=0x216,对比看得出结论:中断门在0环将IF位置0了。
1.4 练习:RETF返回中断门
中断门提权进0环的堆栈如下右图:
相比于调用门多压入一个EFLAG3寄存器,在中断门中用RETF返回,只需将[ESP0+0x8]写到EFLAG3,然后让ESP3和SS3向低地址移动4字节即可。
构造代码。
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
BYTE IDTItem0[8];
// R0 函数,读取了IDT表第一项
// 004113A0
void __declspec(naked) R0Function()
{
__asm
{
//int 3 // 调试用的
pushad //esp - 0x20
mov eax,0x8003f400
mov ebx,[eax]
mov ecx,[eax+0x4]
mov dword ptr ds:[IDTItem0],ebx
mov dword ptr ds:[IDTItem0+0x4],ecx
// 要求用 retf 返回
add esp,0x28 // esp0指向eflags3
popfd // esp0+4,esp0指向3环esp3
mov eax,[esp] // 将原ESP3和SS3向低地址移动4字节
mov [esp-0x4],eax
mov eax,[esp+0x4]
mov [esp],eax
sub esp,0x2C
popad
retf
}
}
int main(int argc, char* argv[])
{
__asm
{
INT 32
}
printf("%08x %08x\n", *(PDWORD)IDTItem0, *(PDWORD)((PBYTE)IDTItem0+0x4));
getchar();
return 0;
}构造中断门描述符,0040EE00`00081020。
修改IDT表的中断向量为32的那项。
回到XP中取消断点后F5执行结果如下。
1.5 练习:IRETD返回调用门
iretd返回会弹出5个值(EIP、CS3、EFLAG3、ESP3、SS3),但是retf返回仅4个值(EIP、CS3、ESP3、SS3)
故使用IRETD返回调用门时只需要:将ESP和SS向高地址移动4字节,将EFLAG写到[ESP+0x8]即可。
这样能成功的原因:调用门提权时并不会将EFLAG的IF清零,所以在0环执行代码的时候将EFLAG压栈不会产生错误。
调用门(无参数):
构造代码。
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
DWORD dwHigh2GValue;
// 该函数通过 CALL FAR 调用,使用调用门提权,拥有0环权限
void __declspec(naked) FunctionHas0CPL()
{
__asm
{
pushad //esp - 0x20
pushfd //esp - 0x24
// 读取了GDT表第二项的低4字节
mov eax,0x8003f008
mov eax,[eax]
mov dwHigh2GValue,eax
// 要求用 iretd 返回
add esp,0x30 // esp指向ss
mov eax,[esp] // 将原ESP和SS向高地址移动4字节
mov [esp+0x4],eax
mov eax,[esp-0x4]
mov [esp],eax
pushfd
sub esp,0x2c // 还原esp
popfd
popad
iretd
}
}
int main(int argc, char* argv[])
{
char buff[6] = {0,0,0,0,0x48,0};
__asm
{
call fword ptr [buff] // 长调用,使用调用门提权
}
printf("%08x\n",dwHigh2GValue);
getchar();
return 0;
}构造调用门描述符,0040EC00`00081020。
修改GDT表对应的项,eq 8003f048 0040EC00`00081020。
回到XP中取消断点后F5执行,如下。
2 陷阱门
陷阱门和中断门除了以下区别,在使用上没什么区别。
- TYPE域(32位)
- 中断门:Type=0x1110=0xE。
- 陷阱门:Type=0x1111=0xF。
- IF位是否清零(进入0环后)
- 中断门:IF清零
- 陷阱门:IF不清零
使用陷阱门同样会压栈5个参数,应该说通过INT N进入0环都会压栈5个参数。Windows不使用陷阱门(但是我们可以构造,因为CPU支持)。
陷阱门结构如下:
- D=0,16位陷阱门
- D=1,32位陷阱门
中断门:高四字节的的第8位 = 0;
陷阱门:高四字节的的第8位 = 1。
CPU 必须支持中断,中断分为可屏蔽中断和不可屏蔽中断。
中断是基于硬件的,鼠标,键盘是可屏蔽中断,电源属于不可屏蔽中断。当我们拔掉电源之后,CPU并不是直接熄灭的,而是有电容的,此时不管你eflags的IF位是什么,都会执行int 2
中断,来进行一些收尾的动作。
中断是可以进行软件模拟的,称为软中断。 也就是通过 int n 来进行模拟。 我们构造的中断门,并且进行int n
模拟就是模拟了一次软中断。
- GDT表中没有调用门描述符,IDT表中没有陷阱门描述符。
- Windows不使用调用门和陷阱门。
3 任务段
在调用门、中断门与陷阱门中,一旦出现权限切换,那么就会有堆栈的切换。而且,由于CS的CPL发生改变,也导致了SS也必须要切换。切换时,会有新的ESP0和SS0(CS是由中断门或者调用门指定)这2个值是从TSS来的。
执行流程:3环CALL任务段选择子 --> 根据段选择子到GDT找到对应任务段描述符 --> 将任务段描述符加载到TR寄存器,同时根据任务段中的Base找到TSS内存块的起始地址 --> 根据TSS中的EIP去执行代码 --> IRETD返回。
3.1 TSS结构及作用
TSS(Task-state segment ),任务状态段。是一块内存,大小为104字节,其中存的是一堆寄存器的值。一个任务对应一块TSS。
32位的TSS结构图如下:
用结构体可以表示为:
1 | typedef struct TSS { |
低地址中的ESP0,SS0用于从3环堆栈到0环堆栈。切换CR3等于切换进程。
Intel的设计TSS初衷是:切换任务(站在CPU的角度来说,操作系统中的线程可以称为任务)。CPU考虑到操作系统的线程在执行的时候会不停的切换,所以设计了TSS,让任务可以来回的切换。
但是操作系统并没有采用该方法切换线程(Windows、Linux都没有这样做)。Windows仅使用了TSS中的ESP0、SS0。
对TSS作用的理解应该仅限于存储寄存器即可,跟任务(线程)切换没有关系。TSS的意义就在于可以同时换掉”一堆”寄存器。
- TSS是一块104字节内存,通过TR寄存器找到这块内存,TR寄存器的Base指向这块内存,Limit为这块内存的大小,TR的值来自GDT表的TSS段描述符。
- CPU中的一个任务对应一块TSS内存,任务切换时TSS也会跟着切换,TSS是跟随任务的一个链表,如32位TSS的低4字节就指向前一个TSS段描述符的段选择子。
- TSS替换寄存器的过程:3环代码CALL/JMPTR使用段选择子触发TSS段描述符,然后将段描述符加载到TR寄存器,根据TR的Base找到TSS内存块,将内存块的值加载到其结构包涵的所有寄存器中,然后执行TSS中的EIP,该EIP是从TSS段内存块中来的。
关于TSS结构中的LDT Segment Selector成员:
- 该成员是LDTR段寄存器的可见16位部分(段选择子)。
- LDT段选择子去查LDT表,根据LDT表中对应的段描述符装载此时的LDTR寄存器。
- LDTR段寄存器的Base指向LDT表,Limit为LDT表大小。
- 一个LDTR寄存器的值对应于当前任务,一个任务一张LDT表。
- Windows没有使用LDT表和LDTR寄存器。
3.2 TSS、TR读写
TSS替换寄存器的过程:3环代码CALL/JMP使用TR段选择子触发TSS段描述符,然后将段描述符加载到TR寄存器,根据TR的Base找到TSS内存块,将内存块的值加载到其结构包涵的所有寄存器中。过程如下:
TSS段描述符结构如下:
TSS段描述符中:
- Type域
- 高四字节的第9位是一个判断位,如果此时该TSS段描述符已经被加载到TR寄存器中,那么该位为1,16进制下为B。
- 如果该TSS段描述符没有被加载到TR寄存器中,那么该位为0,16进制下为9。
- G位
- G=0,说明寻址按字节来(TSS用)
- G=1,说明寻址按4kb来(页)
TR寄存器读写
(1)写TR寄存器:将TSS段描述符加载到TR寄存器,使用指令:LTR
有几点需要注意:
用LTR指令去装载的话 仅仅是改变TR寄存器的值(96位) ,并没有真正改变TSS。
LTR指令只能在系统层使用。(当前CPU权限必须是0环的)
加载后TSS段描述符会状态位会发生改变。(高四字节的第9位发生变化)
1
2mov ax,SelectorTSS
ltr ax执行该指令,从GDT表取TSS描述符填充TR寄存器,但并不会修改其他寄存器。
执行指令后,TSS描述符TYPE域低2位会置1。
(2)读TR寄存器:使用指令:STR
如果用STR去读的话,只读了TR的16位也就是段选择子。这跟读取CS段寄存器一样,读取16位。(读16位写96位)
1
str ax
注意:
使用LTR
仅能在0环权限下修改TR段寄存器,但是并不会改变TSS的值,要想同时改变TR寄存器和TSS的值,可以在Ring3下使用CALL FAR、JMP FAR指令来修改。(在Intel手册第二卷有关于Call的三种用法介绍:段内调用、远调用、任务段调用)
3.3 CALL、JMP访问任务段的区别
- 调用时的区别
CALL FAR:EFLAGS 的 NT位置1,会修改TSS previous task link,属于任务嵌套。
JMP FAR: NT位=0,不会修改TSS previous task link,不属于任务嵌套。
- 使用
iretd
返回时的区别:CPU根据NT位决定返回方式,NT位影响iretd
- 如果NT=1,CPU使用TSS的 Previous task link 里存储的上一个任务的TSS选择子进行返回(是上一个TSS段选择子,里面保存的EIP即为返回地址)任务返回
- 如果NT=0,则使用堆栈中的值返回(中断返回)
关于在我们自己的代码中使用int 3
,函数在执行到该指令无法正常返回时蓝屏的解释(int 3在0环做的两件事)?(目前还不清楚具体原因)
- 修改FS段寄存器
- 将NT位置0
但是在使用int 3后,恢复上述两种任何一个值都可以正常返回。
3.4 练习:使用CALL调用任务段修改所有寄存器
使用CALL FAR实现用TSS替换寄存器。
CALL FAR:EFLAGS 的 NT位置1,会修改TSS previous task link
大体流程为:
- 准备一个104字节的TSS,并附上正确的值。
- 准备一个自己写的TSS段描述符,写入到GDT表的一个空白的位置。
- 修改TR寄存器(CALL FAR,JMP FAR)。
- 注意TSS中的EIP,该EIP是从TSS内存块中来的。
TSS可以使用数组,也可以VirtualAlloc
,建议后者,因为TSS最好是页对齐的。
准备TSS。
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// 此数组的地址就是TSS描述符中的Base
DWORD *TSS = (DWORD*)VirtualAlloc(NULL,104,MEM_COMMIT,PAGE_READWRITE);
TSS[0] = 0x00000000; // Previous Task Link 由CPU自己填充,表示上一个任务TR的选择子
TSS[1] = 0x00000000; // ESP0
TSS[2] = 0x00000000; // SS0
TSS[3] = 0x00000000; // ESP1
TSS[4] = 0x00000000; // SS1
TSS[5] = 0x00000000; // ESP2
TSS[6] = 0x00000000; // SS2
TSS[7] = dwCr3; // CR3 学到页就知道是啥了
TSS[8] = (DWORD)R0Func; // EIP
TSS[9] = 0x00000000; // EFLAGS
TSS[10] = 0x00000000; // EAX
TSS[11] = 0x00000000; // ECX
TSS[12] = 0x00000000; // EDX
TSS[13] = 0x00000000; // EBX
TSS[14] = (DWORD)esp+0x900; // ESP,解释:esp是一个0x1000的字节数组,作为裸函数的栈,这里传进去的应该是高地址,压栈才不会越界
TSS[15] = 0x00000000; // EBP
TSS[16] = 0x00000000; // ESI
TSS[17] = 0x00000000; // EDI
TSS[18] = 0x00000023; // ES
TSS[19] = 0x00000008; // CS 0x0000001B
TSS[20] = 0x00000010; // SS 0x00000023
TSS[21] = 0x00000023; // DS
TSS[22] = 0x00000030; // FS 0x0000003B
TSS[23] = 0x00000000; // GS
TSS[24] = 0x00000000; // LDT Segment Selector
TSS[25] = 0x20ac0000; // I/O Map Base Address准备TSS段描述符并写入GDT表P=0的位置,XX00E9XX `XXXX0068,X为申请的104字节内存的首地址。
- G位为0,单位是字节
- TSS一开始的类型是9(可用),当加载到TR中就会变成B( 正被占用)
完整代码如下。需要注意:
- 程序运行时需要修改CR3的值,使用
!process 0 0
- TSS中的EIP为即将要执行的函数的入口地址
- TSS中的ESP为即将要执行的函数的堆栈地址,但是堆栈是往低地址使用,所以需要给ESP赋值一个已经申请预留的堆栈中的一个相对较高的地址
- IO位图、GS、LDT Windows已经没有使用了(win7没用IO位图)
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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
DWORD dwOk;
DWORD dwESP;
DWORD dwCS;
// 任务切换后的EIP
void __declspec(naked) R0Func()
{
__asm
{
pushad
pushfd
push fs
int 3 // int 3 会修改FS
pop fs
mov eax,1
mov dword ptr ds:[dwOk],eax
mov eax,esp
mov dword ptr ds:[dwESP],eax
mov ax,cs
mov word ptr ds:[dwCS],ax
popfd
popad
iretd
}
}
int _tmain(int argc, _TCHAR* argv[])
{
DWORD dwCr3; // windbg获取
char esp[0x1000]; // 任务切换后的栈,数组名就是ESP
// 此数组的地址就是TSS描述符中的Base
DWORD *TSS = (DWORD*)VirtualAlloc(NULL,104,MEM_COMMIT,PAGE_READWRITE);
if (TSS == NULL)
{
printf("VirtualAlloc 失败,%d\n", GetLastError());
getchar();
return -1;
}
printf("请在windbg执行: eq 8003f048 %02x00e9%02x`%04x0068\n", ((DWORD)TSS>>24) & 0x000000FF,((DWORD)TSS>>16) & 0x000000FF, (WORD)TSS);
printf("请在windbg中执行!process 0 0,复制当前进程DirBase的值,并输入.\nCR3: "); // 在windbg中执行 !process 0 0 获取,DirBase: 13600420 这个数要启动程序后现查
scanf("%x", &dwCr3); // 注意是%x
TSS[0] = 0x00000000; // Previous Task Link CPU填充,表示上一个任务的选择子
TSS[1] = 0x00000000; // ESP0
TSS[2] = 0x00000000; // SS0
TSS[3] = 0x00000000; // ESP1
TSS[4] = 0x00000000; // SS1
TSS[5] = 0x00000000; // ESP2
TSS[6] = 0x00000000; // SS2
TSS[7] = dwCr3; // CR3 学到页就知道是啥了
TSS[8] = (DWORD)R0Func; // EIP
TSS[9] = 0x00000000; // EFLAGS
TSS[10] = 0x00000000; // EAX
TSS[11] = 0x00000000; // ECX
TSS[12] = 0x00000000; // EDX
TSS[13] = 0x00000000; // EBX
TSS[14] = (DWORD)esp+0x900; // ESP,解释:esp是一个0x1000的字节数组,作为裸函数的栈,这里传进去的应该是高地址,压栈才不会越界
TSS[15] = 0x00000000; // EBP
TSS[16] = 0x00000000; // ESI
TSS[17] = 0x00000000; // EDI
TSS[18] = 0x00000023; // ES
TSS[19] = 0x00000008; // CS 0x0000001B
TSS[20] = 0x00000010; // SS 0x00000023
TSS[21] = 0x00000023; // DS
TSS[22] = 0x00000030; // FS 0x0000003B
TSS[23] = 0x00000000; // GS
TSS[24] = 0x00000000; // LDT Segment Selector
TSS[25] = 0x20ac0000; // I/O Map Base Address
char buff[6] = {0,0,0,0,0x48,0};
__asm
{
call fword ptr[buff]
}
printf("ok: %d\nESP: %x\nCS: %x\n", dwOk, dwESP, dwCS);
system("Pause");
getchar();
return 0;
}- 程序运行时需要修改CR3的值,使用
在XP中执行上述代码。
此时根据提示在Windbg中修改GDT表的段描述符:
eq 8003f048 0000e93a·00000068
!process 0 0;g
获取CR3的值根据CR3的值回到XP中输入并回车,此时将会在Windbg中断在int 3,输入g执行返回,如果使用p单步将无法正常返回。
使用int 3断下来后单步调试时观察堆栈和寄存器。
在XP中执行CALL FAR前下断点观察寄存器。
观察此时的TSS情况。
在XP中取消断点后F5执行,此时Windbg中查看0堆栈情况。堆栈中即为函数压入栈的8个通用寄存器、EFLAG寄存器、FS段寄存器。观察寄存器值也都成功替换。
此时观察TSS中的数值。
根据段选择子0x0028找到对应的段描述符80008b04`200020ab为任务切换的TSS段描述符,纪录的TSS段起始地址为:80042000,如下图即为上一个TSS段内存的数据。
3.5 练习:使用JMP调用任务段修改所有寄存器
JMP FAR: NT位置0,不会修改TSS previous task link(PTL)。
分别使用两种方式返回:
JMP:既然是JMP触发执行的任务段,那也可以在裸函数中使用JMP跳到之前的TR保存的任务段进行返回。
iretd:由于JMP FAR任务段不会保存PTL,且NT位置0,则需要在裸函数中将前一个TR的选择子填充到TSS的前2字节和NT置1。(但是在真正测试时即使在iretd前修改NT位为1,当执行到EIP=iretd时CPU还是会将NT位置为0,无法正确返回。)
准备TSS。
准备TSS段描述符并写入GDT表P=0的位置,XX00E9XX `XXXX0068,X为申请的104字节内存的首地址。
完整代码如下。需要获取任务切换前的TR寄存器的段选择子,方便裸函数跳转返回。
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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
DWORD dwOk;
DWORD dwESP;
DWORD dwCS;
BYTE PrevTR[6] = {0};//保存任务切换前的TR段选择子
// 任务切换后的EIP
void __declspec(naked) R0Func()
{
__asm
{
pushad
pushfd
push fs
int 3 // int 3 会修改FS
pop fs
mov eax,1
mov dword ptr ds:[dwOk],eax
mov eax,esp
mov dword ptr ds:[dwESP],eax
mov ax,cs
mov word ptr ds:[dwCS],ax
popfd
popad
jmp fword ptr ds:[PrevTr]
}
}
int _tmain(int argc, _TCHAR* argv[])
{
DWORD dwCr3; // windbg获取
char esp[0x1000]; // 任务切换后的栈,数组名就是ESP
// 此数组的地址就是TSS描述符中的Base
DWORD *TSS = (DWORD*)VirtualAlloc(NULL,104,MEM_COMMIT,PAGE_READWRITE);
if (TSS == NULL)
{
printf("VirtualAlloc 失败,%d\n", GetLastError());
getchar();
return -1;
}
printf("请在windbg执行: eq 8003f048 %02x00e9%02x`%04x0068\n", ((DWORD)TSS>>24) & 0x000000FF,((DWORD)TSS>>16) & 0x000000FF, (WORD)TSS);
printf("请在windbg中执行!process 0 0,复制当前进程DirBase的值,并输入.\nCR3: "); // 在windbg中执行 !process 0 0 获取,DirBase: 13600420 这个数要启动程序后现查
scanf("%x", &dwCr3); // 注意是%x
TSS[0] = 0x00000000; // Previous Task Link CPU填充,表示上一个任务的选择子
TSS[1] = 0x00000000; // ESP0
TSS[2] = 0x00000000; // SS0
TSS[3] = 0x00000000; // ESP1
TSS[4] = 0x00000000; // SS1
TSS[5] = 0x00000000; // ESP2
TSS[6] = 0x00000000; // SS2
TSS[7] = dwCr3; // CR3 学到页就知道是啥了
TSS[8] = (DWORD)R0Func; // EIP
TSS[9] = 0x00000000; // EFLAGS
TSS[10] = 0x00000000; // EAX
TSS[11] = 0x00000000; // ECX
TSS[12] = 0x00000000; // EDX
TSS[13] = 0x00000000; // EBX
TSS[14] = (DWORD)esp+0x900; // ESP,解释:esp是一个0x1000的字节数组,作为裸函数的栈,这里传进去的应该是高地址,压栈才不会越界
TSS[15] = 0x00000000; // EBP
TSS[16] = 0x00000000; // ESI
TSS[17] = 0x00000000; // EDI
TSS[18] = 0x00000023; // ES
TSS[19] = 0x00000008; // CS 0x0000001B
TSS[20] = 0x00000010; // SS 0x00000023
TSS[21] = 0x00000023; // DS
TSS[22] = 0x00000030; // FS 0x0000003B
TSS[23] = 0x00000000; // GS
TSS[24] = 0x00000000; // LDT Segment Selector
TSS[25] = 0x20ac0000; // I/O Map Base Address
char buff[6] = {0,0,0,0,0x48,0};
__asm
{
pushad
str ax
lea edi,[PrevTR+4]//JMP后面的6个字节,低4字节为EIP,高2字节为段选择子
mov [edi],ax
popad
jmp fword ptr[buff]
}
printf("ok: %d\nESP: %x\nCS: %x\n", dwOk, dwESP, dwCS);
system("Pause");
getchar();
return 0;
}VC6中运行后到Windbg中写对应的GDT表TSS段描述符,并获取CR3的值。
- eq 8003f048 0000e93a`00000068
!process 0 0;g
回到XP的VC6中输入CR3值后回车,此时断在裸函数的int 3,观察寄存器和堆栈。
观察TSS段。
- 此时的PTL=0x00,说明JMP AFR任务段确实没有修改TSS中的PTL。
- 对应的段寄存器的值已经修改成功。
此时Windbg中输入
g
运行后成功返回,如下图。
在另外的测试中,使用iretd返回时,即使NT置1,并修改TSS的PTL,当执行到eip=iretd
指令时,EFLAG又回自动将NT位置0,所以无法使用iretd进行返回,或者可以尝试使用中断返回试试(使用堆栈)。
结论:3环CALL任务段选择子 --> GDT找到对应任务段描述符 --> 将任务段描述符加载到TR寄存器,同时根据任务段中的Base找到TSS内存块的起始地址 --> 根据TSS中的EIP去执行代码 --> IRETD返回。
- CALL任务段不会将返回地址压栈,因为使用TR的选择子跳过去。返回也不会使用到返回地址,而是使用PTL(前一个TR的段选择子)。
- 即使在iretd前修改NT位为1,当执行到EIP=iretd时CPU还是会将NT位置为0,无法正确返回(有可能是因为要切回去的那个任务的TSS段描述符的B位为1,正在忙,处理器是通过TSS的B位来检测重入的。因中断,iret,call,jmp指令发起的任务切换时,处理器固件会检测新任务的TSS的B位,如果该位为1,则处理器不允许这样的任务切换)。
- 无论何时,只要处理器碰到iret指令,它都会检查NT位。
具体详细的可以参考它的笔记二十六、二十七。
4 任务门
Windows、Linux都没有使用CPU提供的通过TSS来切换线程,而是使用堆栈。那操作系统用TSS来做什么?
既然已经有了任务段为什么还要有任务门?
任务门存在的意义其一 int 8双重错误举例:
假设一个除以0错误,首先会触发int 0中断去查IDT表中对应的地址去执行,如果在执行过程中如果再次出错时就会触发int 8。
int 8是如何接管处理的呢?一旦进入8号中断,将会替换一堆寄存器,保证CPU 能跳到一个正确的地方去执行(除非那个地方也被破坏了),此时什么错误都无所谓了,收集信息后,蓝屏。
任务门int 8查的IDT表即为任务门:
IDT–00008500`00501198–0050–0101 0 0 00–0xA–GDT第十一个
GDT–80008955`27000068–32位TSS任务段Base:80552700–dd 80552700–80544509–uf 80544509
调用任务段的三种方法:CALL/JMP FAR、任务门。
IDT表可以包含3种门描述符:
- 任务门描述符
- 中断门描述符
- 陷阱门描述符
任务门描述符:
TSS段描述符结构如下:
任务门的执行过程:
- INT N指令来去IDT表中找对应的任务门描述符
- 查询IDT表找到任务门描述符
- 通过任务门描述符表的TSS段选择子查询GDT表,找到任务段描述符
- 根据TSS段描述符的Base找到TSS任务段内存块
- 使用TSS段中的值修改寄存器
- IRETD返回
执行流程图如下:
4.1 练习:使用任务门修改所有的寄存器的值
构造任务门段描述符:0000E500`00480000
- P=1
- DPL=0x11=3(能让3环的代码访问到这个门)
- S=0
- 0x1110=0xE
- 任务门:0x0101=0x5
- TSS段选择子:0x0048(查GDT的第10项)0100 1 0 00
修改IDT表。(找个P=0的,如第33项)eq 8003f500 0000E500`00480000
代码如下。
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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
DWORD dwOk;
DWORD dwESP;
DWORD dwCS;
DWORD *TSS = NULL; //TSS内存块
// 任务切换后的EIP
void __declspec(naked) R0Func()
{
__asm
{
pushad
pushfd
mov eax,1
mov dword ptr ds:[dwOk],eax
mov eax,esp
mov dword ptr ds:[dwESP],eax
mov ax,cs
mov word ptr ds:[dwCS],ax
popfd
popad
iretd
}
}
int _tmain(int argc, _TCHAR* argv[])
{
DWORD dwCr3; // windbg获取
char esp[0x1000]; // 任务切换后的栈,数组名就是ESP
// 此数组的地址就是TSS描述符中的Base
TSS = (DWORD*)VirtualAlloc(NULL,104,MEM_COMMIT,PAGE_READWRITE);
if (TSS == NULL)
{
printf("VirtualAlloc 失败,%d\n", GetLastError());
getchar();
return -1;
}
printf("请在windbg执行: eq 8003f048 %02x00e9%02x`%04x0068\n", ((DWORD)TSS>>24) & 0x000000FF,((DWORD)TSS>>16) & 0x000000FF, (WORD)TSS);
printf("请在windbg中执行!process 0 0,复制当前进程DirBase的值,并输入.\nCR3: "); // 在windbg中执行 !process 0 0 获取,DirBase: 13600420 这个数要启动程序后现查
scanf("%x", &dwCr3); // 注意是%x
TSS[0] = 0x00000000; // Previous Task Link CPU填充,表示上一个任务的选择子
TSS[1] = 0x00000000; // ESP0
TSS[2] = 0x00000000; // SS0
TSS[3] = 0x00000000; // ESP1
TSS[4] = 0x00000000; // SS1
TSS[5] = 0x00000000; // ESP2
TSS[6] = 0x00000000; // SS2
TSS[7] = dwCr3; // CR3 学到页就知道是啥了
TSS[8] = (DWORD)R0Func; // EIP
TSS[9] = 0x00000000; // EFLAGS
TSS[10] = 0x00000000; // EAX
TSS[11] = 0x00000000; // ECX
TSS[12] = 0x00000000; // EDX
TSS[13] = 0x00000000; // EBX
TSS[14] = (DWORD)esp+0x900; // ESP,解释:esp是一个0x1000的字节数组,作为裸函数的栈,这里传进去的应该是高地址,压栈才不会越界
TSS[15] = 0x00000000; // EBP
TSS[16] = 0x00000000; // ESI
TSS[17] = 0x00000000; // EDI
TSS[18] = 0x00000023; // ES
TSS[19] = 0x00000008; // CS 0x0000001B
TSS[20] = 0x00000010; // SS 0x00000023
TSS[21] = 0x00000023; // DS
TSS[22] = 0x00000030; // FS 0x0000003B
TSS[23] = 0x00000000; // GS
TSS[24] = 0x00000000; // LDT Segment Selector
TSS[25] = 0x20ac0000; // I/O Map Base Address
char buff[6] = {0,0,0,0,0x48,0};
__asm
{
//jmp fword ptr[buff]
int 32
}
printf("ok: %d\nESP: %x\nCS: %x\n", dwOk, dwESP, dwCS);
system("Pause");
getchar();
return 0;
}在VC6中执行代码。
根据TSS任务段的地址在GDT表中构造TSS任务段描述符,然后获取CR3的值。(
!process 0 0
)回到XP的VC6中输入CR3的值06e40300后F5继续执行。