(15)TSS,TR寄存器,TSS描述符,任务段跳转实验

一、TSS,TR寄存器,TSS门描述符的关系

首先,通过下图,了解 TSS,TR寄存器,TSS描述符的关系:

在这里插入图片描述

TSS(Task-state segment)是一块104字节的内存,用于存储大部分寄存器的值;

在这里插入图片描述
TSS设计出来的目的是任务切换,或者说是一次性替换一大堆寄存器。

TR寄存器存储了TSS的地址,大小,和TSS门描述符选择子;
在这里插入图片描述

TSS描述符是GDT表中的一项,操作系统启动时,从门描述符取值初始化TR寄存器。

在这里插入图片描述

二、LTR STR 指令

LTR 指令是0环指令,格式如下:

mov ax,SelectorTSS
ltr ax

执行该指令,从GDT表取TSS描述符填充TR寄存器,但并不会修改其他寄存器。
执行指令后,TSS描述符TYPE域低2位会置1.

STR 指令只会读取 TR 的16位选择子部分,该指令没有特权要求。指令格式如下:

str ax

三、TSS

在这里插入图片描述

Windows只使用了TSS的SS0和ESP0,用于权限切换。
TSS这个东西是Intel设计出来做任务切换的,windows和linux都没有使用任务,而是自己实现了线程。在windows中,TSS唯一的作用就是权限切换时要用到SS0和ESP0,又或者这样理解,TSS就是用来一次性替换一堆寄存器的。
现在我们也知道了为啥之前的课上老师说过windows没有使用LDT表,因为LDT是任务切换用的,一个任务一个LDT表。

四、课后练习

1、使用CALL去访问一个任务段,并能够正确返回。
2、使用JMP去访问一个任务段,并能够正确返回。

使用CALL FAR 和JMP FAR 都可以访问任务段,有两点区别:
使用 CALL FAR 方式,EFLAGS 的 NT位置1,而JMP FAR 方式 NT位=0;
CPU根据NT位决定返回方式,如果NT=1,CPU使用TSS的 Previous task link 里存储的上一个任务的TSS选择子进行返回;如果NT=0,则使用堆栈中的值返回。


下面是两个练习题的实验步骤。这个作业卡了好久,有不少坑点,先在此说明:
坑点1:INT 3 会修改FS寄存器,所以使用 INT 3 必须先保存FS的值。
坑点2:TSS可以使用数组,也可以VirtualAlloc,建议后者,因为TSS最好是页对齐的。
坑点3:定义局部数组作为堆栈,传给TSS[14]时,应该传数组尾部的指针,因为压栈ESP减小,如果传数组首地址,那一压栈就越界了。
坑点4:JMP FAR 方式切换任务并不能提权,返回时要用先前保存的TR寄存器的值(原TSS选择子)返回。

补充一段Intel白皮书对于JMP 任务切换的描述:

(The JMP instruction cannot be used to perform inter-privilege-level
far jumps.) Executing a task switch with the JMP instruction is
somewhat similar to executing a jump through a call gate. Here the
target operand specifies the segment selector of the task gate for the
task being switched to (and the offset part of the target operand is
ignored). The task gate in turn points to the TSS for the task, which
contains the segment selectors for the task’s code and stack segments.
The TSS also contains the EIP value for the next instruc- tion that
was to be executed before the task was suspended. This instruction
pointer value is loaded into the EIP register so that the task begins
executing again at this next instruction. The JMP instruction can also
specify the segment selector of the TSS directly, which eliminates the
indirection of the task gate. See Chapter 7 in Intel® 64 and IA-32
Architectures Software Developer’s Manual, Volume 3A, for detailed
information on the mechanics of a task switch.

下面是代码和执行结果,我会在代码里输出需要执行的指令。
1、使用CALL去访问一个任务段,并能够正确返回。

// TSS.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <Windows.h>
#include <stdio.h>

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,复制TSS.exe进程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);

	return 0;
}

执行结果如图:

在这里插入图片描述

2、使用JMP去访问一个任务段,并能够正确返回。
和CALL FAR对比,NT位不会置1,TSS previous task link 也不会填充旧的TR,因此想要返回,可以先保存旧的TR,然后JMP FAR回去。

// TSS.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <Windows.h>
#include <stdio.h>

DWORD dwOk;
DWORD dwESP;
DWORD dwCS;

BYTE PrevTr[6]; // 旧TR,供裸函数返回

// 任务切换后的EIP
void __declspec(naked) R3Func()
{
	__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,复制TSS.exe进程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)R3Func; // 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
	{
		str ax
		lea edi,[PrevTr+4]
		mov [edi],ax
		
		jmp fword ptr[buff]
	}
	printf("ok: %d\nESP: %x\nCS: %x\n", dwOk, dwESP, dwCS);

	return 0;
}

在这里插入图片描述


版权声明:本文为Kwansy原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。