文章目錄
TSS
什麼是TSS
TSS是一塊內存,大小爲104字節,結構如圖所示:
結構體如下:
typedef struct TSS {
DWORD link; // 保存前一個 TSS 段選擇子,使用 call 指令切換寄存器的時候由CPU填寫。
// 這 6 個值是固定不變的,用於提權,CPU 切換棧的時候用
DWORD esp0; // 保存 0 環棧指針
DWORD ss0; // 保存 0 環棧段選擇子
DWORD esp1; // 保存 1 環棧指針
DWORD ss1; // 保存 1 環棧段選擇子
DWORD esp2; // 保存 2 環棧指針
DWORD ss2; // 保存 2 環棧段選擇子
// 下面這些都是用來做切換寄存器值用的,切換寄存器的時候由CPU自動填寫。
DWORD cr3;
DWORD eip;
DWORD eflags;
DWORD eax;
DWORD ecx;
DWORD edx;
DWORD ebx;
DWORD esp;
DWORD ebp;
DWORD esi;
DWORD edi;
DWORD es;
DWORD cs;
DWORD ss;
DWORD ds;
DWORD fs;
DWORD gs;
DWORD ldt;
// 這個暫時忽略
DWORD io_map;
} TSS;
- Previous Task Link 前一個TSS的鏈接。通過這個字段可以找到上一個TSS
- ESP0:零環的ESP
- SS0:零環的SS
- ESP1:1環的ESP
- SS1:1環的SS
- ESP2:2環的ESP
- SS2:2環的SS
- LDT Segment Seletor:LDT段選擇子,保存了LDT表的基址和長度。 這個選擇子對應的段描述符必須是系統段描述符。LDT段選擇子的數量有多少個取決於TSS有多少個
- IO Map Base Address:IO權限位圖 與硬件相關 可忽略
TSS的作用
TSS是CPU設計的東西,與操作系統無關。一個CPU只有一個TSS,TSS存在的意義在於讓一個CPU可以同時執行多個任務。但是操作系統並沒有使用TSS來進行任務切換,而是直接將任務切換所需要保存的寄存器直接存到了堆棧裏。
如何找到TSS
那麼CPU如果想執行任務切換的話就必須先找到TSS。通過TR段寄存器。
TR寄存器讀寫
將TSS段描述符加載到TR寄存器
指令:LTR
說明:
- 用LTR指令去裝載的話,僅僅是改變TR寄存器的值,並沒有真正改變TSS
- LTR指令只能在系統層使用
- 加載後TSS段描述符狀態位會發生改變
構造TSS段描述符
Base 31:24:00
G:0 0 0 AVL:0 ->0
Limit19:16:0
Type:9(表示未被加載過 加載過後爲B)
Base 23:16:00
Base Address 15:00:0000
Segment Limit 15:00:0068
XX00E9XX`XXXX0068
# Base Address指的是新的TSS的內存地址
修改TSS
在Ring3我們可以通過call far或者jmp far指令來修改TSS。
JMP 訪問代碼段
JMP 0x48 0x12345678
如果0x48是代碼段,執行後CS=0x48 EIP=0x12345678
JMP 訪問任務段
JMP 0x48 0x12345678
如果0x48是TSS段描述符,先修改TR寄存器,再用TR.Base指向的TSS中的值修改當前寄存器
JMP FAR和CALL FAR訪問任務段的區別
- 當使用JMP FAR來實現任務切換時,TSS結構體中的Previous Task Link的值在任務切換完成之後爲0,CPU不會爲其賦值;如果使用CALL FAR來實現任務切換,Previous Task Link的值在任務切換完成之後會CPU會將其填充爲原來的TSS段選擇子
- 當使用JMP FAR來實現任務切換時,EFLAGS寄存器中的NT位不變;當使用CALL FAR來實現任務切換時,EFLAGS寄存器中的NT位就會被置1(NT位會對iret指令產生影響 NT位如果爲0,iret的值從堆棧中取(中斷返回);如果NT位爲1,會找TSS中的Previous Task Link進行返回)
TSS切換實驗
示例代碼
首先準備好需要切換的104個字節,並且賦上正確的值,示例代碼如下
#include "pch.h"
#include<windows.h>
#include<stdio.h>
typedef struct TSS {
DWORD link; // 保存前一個 TSS 段選擇子,使用 call 指令切換寄存器的時候由CPU填寫。
// 這 6 個值是固定不變的,用於提權,CPU 切換棧的時候用
DWORD esp0; // 保存 0 環棧指針
DWORD ss0; // 保存 0 環棧段選擇子
DWORD esp1; // 保存 1 環棧指針
DWORD ss1; // 保存 1 環棧段選擇子
DWORD esp2; // 保存 2 環棧指針
DWORD ss2; // 保存 2 環棧段選擇子
// 下面這些都是用來做切換寄存器值用的,切換寄存器的時候由CPU自動填寫。
DWORD cr3;
DWORD eip;
DWORD eflags;
DWORD eax;
DWORD ecx;
DWORD edx;
DWORD ebx;
DWORD esp;
DWORD ebp;
DWORD esi;
DWORD edi;
DWORD es;
DWORD cs;
DWORD ss;
DWORD ds;
DWORD fs;
DWORD gs;
DWORD ldt;
// 這個暫時忽略
DWORD io_map;
} TSS;
char st[10] = { 0 };
DWORD g_esp = 0;
DWORD g_cs = 0;
TSS tss = {
0x00000000,//link
(DWORD)st, //esp0
0x00000010,//ss0
0x00000000,//esp1
0x00000000,//ss1
0x00000000,//esp2
0x00000000,//ss2
0x00000000,//cr3
0x00401090,//eip-----填裸函數地址
0x00000000,//eflags
0x00000000,//eax
0x00000000,//ecx
0x00000000,//edx
0x00000000,//ebx
(DWORD)st, //esp
0x00000000,//ebp
0x00000000,//esi
0x00000000,//edi
0x00000023,//es
0x00000008,//cs
0x00000010,//ss
0x00000023,//ds
0x00000030,//fs
0x00000000,//gs
0x00000000,//ldt
0x20ac0000
};
void __declspec(naked) func()
{//00401090
__asm
{
int 3
}
}
int main(int argc, char* argv[])
{
printf("%p\n", func);
printf("%x\n", &tss);
printf("cr3:\n");
scanf_s("%x", &(tss.cr3));
char buffer[6] = { 0, 0, 0, 0, 0x48, 0 };
__asm
{
call fword ptr[buffer]
}
printf("g_cs = %08x\ng_esp = %08x\n", g_cs, g_esp);
getchar();
return 0;
}
需要注意的是,我們需要將eip設置爲指定的地址,讓TSS切換完成之後,跳轉到那個地址。這裏將裸函數的地址打印出來之後替換掉了EIP。通過主函數的這句代碼可以打印出要跳轉的裸函數地址
printf("%p\n", func);
構造段描述符
將我們之前準備好的任務段描述符直接拿過來
XX00E9XX`XXXX0068
將地址設置爲我們準備好的TSS結構體,通過下面的代碼打印結構體地址
printf("%x\n", &tss);
構造好的段描述符如下:
0000E940`30180068
接着修改GDT表的段描述符
kd> eq 80b95048 0000E940`30180068
填充CR3
接着編譯,放到虛擬機中執行,運行以後,在windbg中查看CR3寄存器的值
PROCESS 87065d40 SessionId: 1 Cid: 0cc0 Peb: 7ffd9000 ParentCid: 0534
DirBase: 7f0e7560 ObjectTable: c1dcf198 HandleCount: 7.
Image: KernelTest.exe
將前進程的頁目錄基址填充到CR3,接着運行程序 即可完成TSS切換
編譯,放到虛擬機中執行,運行以後,在windbg中查看CR3寄存器的值
PROCESS 87065d40 SessionId: 1 Cid: 0cc0 Peb: 7ffd9000 ParentCid: 0534
DirBase: 7f0e7560 ObjectTable: c1dcf198 HandleCount: 7.
Image: KernelTest.exe
將前進程的頁目錄基址填充到CR3,接着運行程序 即可完成TSS切換