要点回顾
- 前文提到,TSS、TSS段描述符以及TR段寄存器。
- TSS是一块内存,大小104字节。通过TSS可以同时替换"一堆"寄存器,包括通用寄存器和段寄存器等。
- CPU通过TR段寄存器来找到TSS。如果想用自己的TSS段描述符来替换原来的寄存器,就需要修改TR段寄存器,TR段寄存器的值又是来自TSS段描述符。
- TR段寄存器的BASE指向了TSS(也就是这104个字节)在哪里,TR.LIMIT指定了当前的TSS的大小。
- TR段寄存器的值又是从GDT表加载的,GDT表中存储了某个所谓的TSS段描述符。在系统启动的时候把这个段描述符的数据加载到TR段寄存器中,通过TR段寄存器就可以找到TSS了。
- TSS是Intel在硬件的角度提供的任务切换的方式,但是操作系统在真正使用的时候可能并没有使用这种方式来切换任务或者切换线程,它们是一个概念。
TSS段描述符
注意:高4字节的第23位,也就是G位。
G位=0:代表limit界限单位是字节。G位=1:代表limit界限单位是4KB。
前文中的实验通常都是1,本文中为0。
为什么是0?因为LIMIT指向的TSS是以字节为单位的。
构造TSS段描述符
1.准备104个字节
希望切换以后的寄存器变成什么样就填什么。
2.准备好段描述符并写入GDT表中
3.修改TR段寄存器
- 在R0可以通过LTR指令修改TR段寄存器。
- 在R3可以通过 CALL FAR 或者 JMP FAR 指令修改。
用JMP去访问一个代码段的时候,改变的是CS和EIP:
JMP 0x48:0x123456,如果0x48是代码段,执行后:CS-->0x48、EIP-->0x123456。
用JMP去访问一个任务段的时候:
如果0x48是TSS段描述符,先修改TR段寄存器,再用TR.BASE指向的TSS中的值修改当前的寄存器。
代码
#include "stdafx.h"
#include <windows.h>
DWORD dwOK;
DWORD dwESP;
DWORD dwCS;
__declspec(naked)func() //函数地址:0x00401020
{
dwOK = 1;
__asm
{
int 3 //程序一旦执行到此处会中断到windbg中
mov eax,esp
mov dwESP,eax
mov ax,cs
mov word ptr [dwCS],ax
//切换回去的代码没写
//因为使用CALL和JMP切换进来以后再回来的方式不一样
//内部实现细节也完全不一样
}
}
//eq 8003f0c0 0000e912`fdcc0068
int main(int argc, char* argv[])
{
char bu[0x10]; //0x12ff70
int iCr3;
printf("input CR3:\n");
scanf("%x",&iCr3); //通过windbg工具指令:!process 0 0 获取
DWORD iTss[0x68] =
{
0x00000000, //link 当一次性切换一堆寄存器的时候,该地方存储的就是原104个字节的段选择子,该值不需要填充,当发生切换时由CPU自动完成
//因为并不是中断门或调用门,故esp0、ss0,esp1、ss1,esp2、ss2可以填0
0x00000000, //esp0 (DWORD)bu 想进0环取esp0和ss0
0x00000000, //ss0
0x00000000, //esp1 想进1环取esp1和ss1 Windows并未使用1环,自己使用时可以相进就近
0x00000000, //ss1
0x00000000, //esp2 想进2环取esp2和ss2 Windows并未使用2环,自己使用时可以相进就近
0x00000000, //ss2
(DWORD)iCr3, //cr3 目前只需要知道它是个寄存器即可, 必须填写
0x00401020, //eip 下一次执行的代码位置, 必须填写
0x00000000, //eflags
0x00000000, //eax
0x00000000, //ecx
0x00000000, //edx
0x00000000, //ebx
(DWORD)bu, //esp 代码执行切换的时候, 一旦发生切换所有寄存器都会发生变化, EIP变了那么ESP也会发生变化, 上面定义的数组当成堆栈来用
0x00000000, //ebp
0x00000000, //esi
0x00000000, //edi
0x00000023, //es 无论3环还是0环都是23
0x00000008, //cs 0x0000001B 3环1B, 0环08
0x00000010, //ss 0x00000023 3环23, 0环10
0x00000023, //ds 无论3环还是0环都是23
0x00000030, //fs 0x0000003B 3环3B, 0环30
0x00000000, //gs 永远是0, window并未使用
0x00000000, //ldt
0x20ac0000 //IO权限位图, Windows2000以后并未使用, 但结构中仍然存在
};
char buff[6];
*(DWORD*)&buff[0] = 0x12345678;
*(WORD*)&buff[4] = 0xC0;
__asm
{
call fword ptr[buff]
}
printf("ok = %d, ESP = %x, CS = %x\n", dwOK, dwESP, dwCS);
return 0;
}
实验
构建描述符
段描述符不见得是这个位置,按照自己机器中的去写。
找到CR3的值并写入
因为CR3的值是必须提供的。
1.运行
运行后要求输入CR3的值。
2.查看所有进程
3.找到CR3的值
11680280就是当前要用的程序的CR3的值。
4.输入CR3的值并按下回车
按下回车后回到windbg中发现程序已经断下。
断下以后单步执行一次,或几次。
可以看到断到了int3断点,且下面的代码也是程序中内联汇编的代码。
然后观察地址可以发现是0040102A,刚好是函数所在的地址。
证明通过切换的时候,代码就已经执行了,下面来看一下寄存器。
CR3改变了。
EIP:内联汇编中写的是401020,目前是401032。因为刚才断下后单步执行了几步,所以此处的EIP是没有问题的。
CS:8,SS:10,ESP:12ff70,还有FS、ES、DS等都指定了具体的值。
已经正确的切换成了指定的值。
由于后面切换回去的内联汇编代码没有写,所以继续执行的话会蓝屏。可以动手尝试一下使用CALL或JMP切换回去。