任务段
什么是任务段
在说任务门之前,我们需要先了解任务段。
在前文说到,每当出现权限切换时,就会有堆栈的切换。而随着权限的切换,CS的CPL就会跟着发生切换,那么SS也会跟着切换。那么问题来了,SS与ESP都是从哪里来的?
答案就是:从TSS中来。
那么什么是TSS呢?
TSS是一种任务状态段,在这里,我们需要先明白 一个概念,我们平时所说的线程是对于操作系统而言的,而对于CPU来说,线程就是任务。
TSS
TSS是一块内存结构,他的大小有104个字节,其内部结构具体如下:
我们可以看到,TSS内部存放了0-3环的所有寄存器,还有各种段寄存器等。因此,当进行任务切换时,寄存器的值就会从该结构中来。
相关结构介绍:
-
Previous Task Link :存储的是上一个TSS。
其中CR3需要我们手动获取。
因此,我们可以理解成:每个任务,都有一个TSS结构。
那么我们该如何找到TSS呢?
在说到如何找到TSS之前,我们还需要补充几个概念,一个是TR寄存器,一个是TSS段描述符
TSS段描述符
TSS段描述符,位于GDT表中,是系统段描述符的一种,具体结构如下:
由图可知:
-
其高32位的第4和第3个字节为:e9或eb,其中Type = 9时,表明TSS段描述符未被加载到TR段寄存器中;当Type = B时,则表明TSS段描述符已经被加载到TR段寄存器中。
-
Limit = 0x68
-
因此,我们可以得出简单的构造出,TSS段描述符= XX00e9XX`XXXX0068
TR寄存器
在前面介绍中,我们提到了TR寄存器,我们可以通过TR寄存器找到TSS,如下图:
TR寄存器是寄存器中的一种,他的值是当操作系统启动时,从TSS段描述符加载的。
而TR.Base = TSS结构的地址。
TR.Limit = TSS大小
TR寄存器的读写
将TSS段描述符加载到TR寄存器
指令:LTR
这里需要注意以下几点:
-
用LTR指令,仅仅改变的是TR寄存器,并没有真正的改变TSS
-
LTR只能在0环使用
-
加载后TSS段描述符的状态位会发生改变
介绍完TR寄存器、TSS段描述符、TSS,在回到这张图中
我们可以从图中分析,TSS如何寻找?
首先CPU会通过TR寄存器找到TSS,结构,并根据TR寄存器的Limit确定TSS的大小。而TR寄存器的值从哪来呢?TR寄存器从TSS段描述符中来。
那么完整的过程就是,先从TSS段描述符加载TR寄存器,在从TR寄存器中的BaseAddr与Limit确定TSS的位置和大小。
读TR寄存器
指令:STR
如果用STR去读的话,只读了TR的16位,也就是选择子。
如何Ring3中利用任务段中提权?
我们可以使用CALL指令,或者JMP指令。
JMP指令
用JMP访问一个代码段时,改变的是CS和EIP。
如:JMP 0x48:0x123456
如果0x48是代码段,执行后:CS->0x48 EIP->0x123456。
用JMP 去访问一个任务段的时候,如果0x48是TSS描述符,先修改TR寄存器,在用TR.Base指向的TSS中的值修改当前的寄存器。
但是,因为JMP过去之后,PTL并不会记录前一个TSS的位置,所以,我们需要再JMP之前,先记录一下当前TSS的位置。
那么如何实现呢?
代码如下:
// RenWuMenJMP.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <windows.h>
DWORD iTSS[26];
DWORD ESP0[0x1000];
DWORD ESP3[0x1000];
DWORD dwESP;
DWORD dwCS;
DWORD dwCR3;
BYTE dwBack[6];
WORD dwTSS;
_declspec(naked) void Call(){
_asm{
mov dwESP,ESP
mov ax,cs
mov dwCS,eax
jmp fword ptr[dwBack]
}
}
int main(int argc, char* argv[])
{
memset(iTSS,0,sizeof(iTSS));
memset(ESP0,0,sizeof(ESP0));
memset(ESP3,0,sizeof(ESP3));
dwESP = 0;
dwCS = 0;
dwCR3 = 0;
dwTSS = 0;
_asm
{
str dwTSS
}
dwBack[4] = dwTSS;
printf("%xn",dwBack[4]);
printf("%xn",dwTSS);
iTSS[1] = (DWORD)(ESP0+0x900); // ESP
iTSS[2] = 0x10; // SS0
iTSS[8] = (DWORD)Call; // EIP
iTSS[14] = (DWORD)(ESP3+0x900); // ESP3
iTSS[18] = 0x23; // ES
iTSS[19] = 0x08; // CS
iTSS[20] = 0x10; // SS
iTSS[21] = 0x23; // DS
iTSS[22] = 0x30; // FS
printf("iTSS:%x ESP3:%x ESP0:%xn",iTSS,(ESP3+0x900),(ESP0+0x900));
printf("input cr3:");
scanf("%x",&dwCR3);
iTSS[7] = dwCR3; // cr3
char buf[6] = {0,0,0,0,0x48,0};
_asm{
jmp fword ptr [buf];
}
printf("ESP:%x CS:%xn",dwESP,dwCS);
getchar();
return 0;
}
CALL指令
// RenWuDuanCall.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <windows.h>
DWORD iTSS[26];
DWORD ESP0[0x1000];
DWORD ESP3[0x1000];
DWORD dwESP;
DWORD dwCS;
DWORD dwCR3;
_declspec(naked) void Call(){
_asm{
mov dwESP,ESP
mov ax,cs
mov dwCS,eax
iretd
}
}
int main(int argc, char* argv[])
{
memset(iTSS,0,sizeof(iTSS));
memset(ESP0,0,sizeof(ESP0));
memset(ESP3,0,sizeof(ESP3));
dwESP = 0;
dwCS = 0;
dwCR3 = 0;
iTSS[1] = (DWORD)(ESP0+0x900); // ESP
iTSS[2] = 0x10; // SS0
iTSS[8] = (DWORD)Call; // EIP
iTSS[14] = (DWORD)(ESP3+0x900); // ESP3
iTSS[18] = 0x23; // ES
iTSS[19] = 0x08; // CS
iTSS[20] = 0x10; // SS
iTSS[21] = 0x23; // DS
iTSS[22] = 0x30; // FS
printf("iTSS:%x ESP3:%x ESP0:%xn",iTSS,(ESP3+0x900),(ESP0+0x900));
printf("input cr3:");
scanf("%x",&dwCR3);
iTSS[7] = dwCR3; // cr3
char buf[6] = {0,0,0,0,0x48,0};
_asm{
call fword ptr buf;
}
printf("ESP:%x CS:%xn",dwESP,dwCS);
getchar();
return 0;
}
使用JMP 与 使用CALL的区别
-
JMP指令TSS中的Previous Task Link为0,系统不会帮忙自动添加上一个TSS的位置。如果是CALL的话,该位置为原来TSS的段选择子。
-
JMP 指令 NT位不变;CALL 指令 NT位被置1。而NT位 会对iret指令有影响。
-
NT = 0 IRET返回值会从堆栈中取,相当于中断返回
-
NT = 1 IRET不是中断返回,cpu会找TSS的PTL,也就是第一个值,通过他来返回。
任务门
任务门灰色部分为保留部分,而任务门的构造如下:
-
0000 e500 0048 0000
-
其中0048为TSS段选择子(可修改)
-
TSS段中存放的是:XX00e9XX`XXXX0068
而任务段了解之后,那么任务门就比较简单了。我们直接说任务门执行过程。
-
执行指令INT N
-
通过INT指令进入IDT表,查询第N个中断门描述符
-
通过中断门描述符,查GDT表,找到任务段描述符
-
通过任务段描述符加载TR寄存器
-
通过TR寄存器找到TSS
-
通过TSS段中的值,修改寄存器并进行提权
-
IRETD返回。
这里就不放代码了,都差不多。
原文始发于微信公众号(loochSec):任务门&&任务段(提权)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论