TSS结构

TSS段描述符

实验流程
- 构建TSS段描述符,写入到GDT表的空白区
c
0000e912`9e6c20ac
base = 0x00129e6c
eq 80b99048 0000e912`9e6c20ac

- 把cr3填入我们的程序
c
!process 0 0

3 . 最终成功断在了我们的test函数里:

完整代码:
c
#include <iostream>
#include <Windows.h>
using namespace std;
struct _KiIoAccessMap
{
UCHAR DirectionMap[32]; //0x0
UCHAR IoMap[8196]; //0x20
};
typedef struct _KTSS
{
USHORT Backlink; //0x0
USHORT Reserved0; //0x2
ULONG Esp0; //0x4
USHORT Ss0; //0x8
USHORT Reserved1; //0xa
ULONG NotUsed1[4]; //0xc
ULONG CR3; //0x1c
ULONG Eip; //0x20
ULONG EFlags; //0x24
ULONG Eax; //0x28
ULONG Ecx; //0x2c
ULONG Edx; //0x30
ULONG Ebx; //0x34
ULONG Esp; //0x38
ULONG Ebp; //0x3c
ULONG Esi; //0x40
ULONG Edi; //0x44
USHORT Es; //0x48
USHORT Reserved2; //0x4a
USHORT Cs; //0x4c
USHORT Reserved3; //0x4e
USHORT Ss; //0x50
USHORT Reserved4; //0x52
USHORT Ds; //0x54
USHORT Reserved5; //0x56
USHORT Fs; //0x58
USHORT Reserved6; //0x5a
USHORT Gs; //0x5c
USHORT Reserved7; //0x5e
USHORT LDT; //0x60
USHORT Reserved8; //0x62
USHORT Flags; //0x64
USHORT IoMapBase; //0x66
struct _KiIoAccessMap IoMaps[1]; //0x68
UCHAR IntDirectionMap[32]; //0x208c
}KTSS;
void __declspec(naked) test()
{
__asm
{
int 3;
//win7 32位 29912分页模式有个bug:
// 上个任务段保存的cr0=0,导致返回3环出错
// 此段代码就是为了修复cr3
sub esp, 8; 借栈 8 字节给 sgdt 用
// ------------------------------
// Step 1: 取 TR 和 GDT Base
// ------------------------------
str bx; BX = 当前 TSS 段选择子
sgdt fword ptr[esp]; [esp + 0..1] = limit, [esp + 2..5] = GDT base
mov edx, [esp + 2]; EDX = GDT Base
// ------------------------------
// Step 2: 通过 TR 找到"当前 TSS 描述符"
// selector->index->描述符地址
// ------------------------------
movzx ecx, bx; ECX = selector
and ecx, 0FFF8h; 清 RPL / TI
shr ecx, 3; index = selector >> 3
lea esi, [edx + ecx * 8]; ESI = 当前 TSS 段描述符地址(每个 8 字节)
// ------------------------------
// Step 3: 从描述符拼当前 TSS 基址
// 描述符布局:
// +2 WORD Base 0..15
// +4 BYTE Base 16..23
// +7 BYTE Base 24..31
// ------------------------------
movzx eax, word ptr[esi + 2]; EAX = BaseLow
movzx ecx, byte ptr[esi + 4]; ECX = BaseMid
shl ecx, 16
or eax, ecx
movzx ecx, byte ptr[esi + 7]; ECX = BaseHigh
shl ecx, 24
or eax, ecx; EAX = 当前 TSS 线性基址
mov edi, eax; EDI = CurrentTssBase
// ------------------------------
// Step 4: 从当前 TSS 里取 Backlink(上一个 TSS 的 selector)
// TSS.Backlink 偏移 = 0x00
// ------------------------------
movzx cx, word ptr[edi + 0]; CX = Backlink selector
// 如果 Backlink 为 0 或 index 为 0,认为不存在上一个任务,直接退出
test cx, 0FFF8h
jz NoPrevTask
// ------------------------------
// Step 5: 备份当前 TSS 中的 CR3
// TSS.CR3 偏移 = 0x1C
// ------------------------------
mov eax, [edi + 1Ch]; EAX = CurrentTss.CR3 (备份)
// ------------------------------
// Step 6: 通过 Backlink selector 找到"上一个 TSS 描述符"
// ------------------------------
movzx ebx, cx; EBX = prev selector
and ebx, 0FFF8h
shr ebx, 3; index = selector >> 3
lea esi, [edx + ebx * 8]; ESI = 上一个 TSS 的段描述符
// ------------------------------
// Step 7: 拼出"上一个 TSS"基址
// ------------------------------
movzx edx, word ptr[esi + 2]; EDX = BaseLow
movzx ecx, byte ptr[esi + 4]; ECX = BaseMid
shl ecx, 16
or edx, ecx
movzx ecx, byte ptr[esi + 7]; ECX = BaseHigh
shl ecx, 24
or edx, ecx; EDX = PrevTssBase
// ------------------------------
// Step 8: 把当前 TSS 的 CR3 写入上一个 TSS
// PrevTss.CR3 偏移同样是 0x1C
// ------------------------------
mov[edx + 1Ch], eax; PrevTss.CR3 = CurrentTss.CR3
NoPrevTask :
add esp, 8; 还原栈
}
__asm
{
pushfd;
pop eax;
or eax, 0x4000; //int 3会把efl的NT位置0,所有这里需要手动把Eflags的NT位置1
push eax;
popfd;
iret
}
}
int main(void)
{
//申请栈空间
char bufEsp0[0x2000] = { 0 };
char bufEsp3[0x2000] = { 0 };
memset(bufEsp0, 0, 0x2000);
memset(bufEsp3, 0, 0x2000); //测试发现不这样操作,进0环后物理页没映射
KTSS tss = { 0 };
tss.Esp0 = (ULONG)bufEsp0 + 0x2000;
tss.Esp = (ULONG)bufEsp3 + 0x2000;
tss.Ss0 = 0x10;
tss.Ss = 0x10;
tss.Cs = 0x8;
tss.Ds = 0x23;
tss.Es = 0x23;
tss.Fs = 0x30;
tss.EFlags = 2;
tss.Eip = (ULONG)test;
tss.IoMapBase = 0x20ac;
cout << "test:" << test << endl;
cout << "TSS:" << &tss << endl;
cout << "请输入CR3:";
cin >> hex >> tss.CR3;
char bufcode[] = { 0,0,0,0,0x48,0 }; //执行构建的任务段,执行完需要手动修复cr3
__asm
{
call fword ptr bufcode;
}
system("pause");
system("pause");
return 0;
}
实验中遇到的一些问题
一、call 任务段选择子 会发生什么?
- 权限检查等操作
- CPU自动保存以下寄存器到当前 TSS:
c
• EIP
• EFLAGS
• ESP + SS
• CS
• DS/ES/FS/GS
• CR3(地址空间基础)
• LDT selector
• I/O位图位置等
- CPU将目标TSS设置为 Busy 状态
- 加载目标TSS内容到寄存器(执行上下文切换)
c
CR3 → 切换页目录(
EIP → 新任务入口地址
ESP/SS → 新任务栈
EFLAGS
CS/DS/ES/FS/GS
- 设置 NT =1
- 执行 TSS 指向的任务代码,从恢复的 EIP 继续执行
二、任务段是如何返回的?
任务段返回使用 IRET,CPU 会检测当前任务 TSS 中的 NT标志位,如果 NT = 1 且 IRET 被执行,则:
CPU 不是简单地弹栈,而是执行:
- 从当前 TSS 中读取 BackLink 字段,找到上一个任务的 TSS
- 自动切换任务到 BackLink 指向的 TSS,恢复任务寄存器、段寄存器、CR3、EFLAGS 等
- 清理 Busy 标志并恢复先前任务状态,返回到前一个任务
三、为什么要在test函数里把NT位置1?
int 3中断会发生:
c
IF = 0
TF = 0
NT = 0
如果NT位=0,那么任务段在iret时,会执行普通中断返回(从栈恢复 CS,EIP,EFLAGS,SS,ESP),导致程序返回失败蓝屏!