Windows XP 段保护(三)

ʕ •ᴥ•ʔ ɔ:

1 中断门

回顾:

保护模式保护的是内存、特权指令(保护寄存器)

  1. 调用门3环堆栈切换到0环堆栈:ESP0、SS0来自TSS,而TSS由Windows的线程提供。
  2. 实际上3环进0环是在一个线程上执行的,该线程从3环进到0环,维护两个堆栈(一个是3环的,一个是0环的),线程从0环返回时并不会破环0环的堆栈,下次该线程再进0环时将从ESP0处继续使用堆栈。
  3. 一个核只有一个TSS(内存块),该CPU共享。但是不同的线程一般TSS值不同(TSS可修改,线程切换时将值填到TSS中)。

Windows没有使用调用门,但是使用了中断门:

  1. 系统调用(老的CPU,从3环到0环。新的CPU直接通过快速调用)
  2. 调试(int 3)

执行调用门的指令:CALL CS:EIP,CS是段选择子,包含了查找GDT表的是一个索引。
但当CPU执行如下指令:INT N,查询的却是另外一张表,这张表叫IDT

IDT即中断描述符表,同GDT一样,IDT也是由一系列描述符组成的,每个描述符占8个字节。但要注意的是,IDT表中的第一个元素不是NULL。

IDT 表包含三种门描述符(具体参看Intel开发手册卷3的6.10 IDT):

  1. 中断门描述符
  2. 任务门描述符
  3. 陷阱门描述符

使用windbg查看IDT表的基地址和长度:

52.png

关于GDTR、IDTR、LDTR、TR寄存器可查看Intel开发手册卷3的2.4内存管理寄存器。

53.png

  1. 中断门描述符:可以看到,中断门是没有参数的。

    D=1,32位的中断门描述符,Type=0x1110=0xE。

    D=0,16位的中断门描述符,Type=0x0110=0x6。

    54.png

    55.png

  2. 本文描述的是软件中断(软中断)

  • 使用INT N来实现的中断是软中断,N=0~255,N称为中断向量,也是IDT表的索引。

  • N=32~255为用户自定义的中断,当前P=0。

各类中断号对应的含义如下图:

59.png

  1. 中断门的执行流程:

    56.png

    35.png

    指明中断门的段选择子指向的GDT段描述符需要是一个代码段描述符(和调用门中的段选择子一样都是指向代码段描述符)。

中断门执行流程:

  1. 当执行int n时,以n为索引(下标)去IDT表找对应的描述符,这个n是几就找到IDT表对应的第n+1个(从0开始)。

  2. 获取到中断门段描述符后检查权限,进行段权限检查(没有RPL,只检查CPL,CPL>=DPL)。

  3. 权限检查通过后,获取新的段选择子(中断门描述符16-31位)与之对应的GDT表中的段描述符的Base,再加上IDT表中的Offset作为EIP去跳转,即$EIP = IDT-Interrupt-Segment-Description.Offset ⊕ GDT-Descriptor.Base$

  1. 中断门的堆栈切换(中断门没有参数):

57.png

  1. 中断门的返回:

    INT N指令:
    1、在没有权限切换时,会向堆栈PUSH3个值,分别是:
    CS EFLAG EIP(返回地址)
    2、在有权限切换时,会向堆栈PUSH5个值,分别是
    SS ESP EFLAG CS EIP
    在中断门中,不能通过RETF返回,而应该通过IRET/IRETD指令返回。

中断门的返回:

  • 16位:iret
  • 32位:iretd
  • 64位:iretq

调用门的返回:retf

iretd比retf多返回一个EFLAG寄存器,该寄存器在中断门执行时,将IF位清零(EFLAG寄存器下标位9的位),如果IF位为0,那CPU将不再接收可屏蔽中断。(具体可见Intel手册卷3第六章中断和异常)

从硬件层面来看,中断分为可屏蔽(受IF位影响,IF=0,可屏蔽中断,中断产生时CPU不管)和不可屏蔽。

58.png

1.1 中断门段权限检查

  1. 中断门描述符DPL > CS段描述符的DPL,即中断门描述符DPL=CPL=3>CS.DPL=0
  2. 中断门描述符RPL=0/3

1.2 调用门和中断门的区别

  1. 调用门通过call far指令执行,但中断门通过int指令执行。
  2. 调用门查GDT表,中断门查IDT表后再查GDT表。
  3. call cs:eip中的CS是段选择子,由三部分组成。但int [index]指令中的index只是索引,中断门不检查RPL,只检查CPL。
  4. 调用门可以有参数,但中断门没有。
  5. 调用门提权时push了四个寄存器:EIP(返回地址) CS ESP SS,返回时用RETF指令返回。中断门提权时push了五个寄存器:EIP(返回地址) CS EFLAGS ESP SS,返回时用IRETD指令返回。

1.3 练习:使用中断门

题目:构造中断门读高2G的内存,并观察在3环、0环的EFALG寄存器的变化。

定义一个裸函数,在里面读取一下IDT表的一个值,以证明自己的CPL是0。

  1. 观察IDT表,找一个P=0的来构造中断门描述符。

    60.png

    该项在IDT表的下标为32,则中断可构造:int 32

  2. 根据中断门描述符结构,构造一个中断门描述符去查找GDT表。$EIP = Interrupt-Segment-Selector.Offset ⊕ GDT-Descriptor.Base$

    初步构造:

    • 偏移:0000 0000
    • P=1
    • DPL=11(DPL=CPL=3>CS.DPL=0
    • S=0
    • Type=1110=0xE
    • 中断门无参数
    • 段选择子:
      • RPL=00/11,随意(比较CPL、DPL)
      • TI=0
      • Index=1
      • 00001 0 00=0x08,00001 0 11=0x0B
    • 0000EE00`00080000或者0000EE00·000B0000
  3. 根据代码构造中断门描述符。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    #include "stdafx.h"
    #include <windows.h>
    #include <stdio.h>

    BYTE IDTItem0[8];

    // R0函数,读取了IDT表第一项
    // 00401020
    void __declspec(naked) R0Function()
    {
    __asm
    {
    //int 3 调试用的
    pushad
    pushfd

    mov eax,0x8003f400
    mov ebx,[eax]
    mov ecx,[eax+0x4]
    mov dword ptr ds:[IDTItem0],ebx
    mov dword ptr ds:[IDTItem0+0x4],ecx

    popfd
    popad
    iretd // iret 会蓝屏,因为 iret的硬编码是66CF,32位下应该使用iretd,硬编码是CF
    }
    }

    int main(int argc, char* argv[])
    {
    __asm
    {
    int 32
    }
    printf("%08x %08x\n", *(PDWORD)IDTItem0, *(PDWORD)((PBYTE)IDTItem0+0x4));
    getchar();
    return 0;
    }

    62.png

    63.png

    则中断门描述符为:0040EE00`00081020或者0040EE00·000B1020。

    观察到此时:EFLAG寄存器ELF=0x216

  4. 将中断门描述符写入到IDT表。

    64.png

  5. 回到XP的VC6中在裸函数代码第一行加个int 3方便查看此时0环的堆栈情况。(需要重新编译查看裸函数的入口地址,如果改变需要修改IDT表的第32项),F5执行如下图。

    65.png

    此时,堆栈中比提权调用门多一个3环的EFLAG寄存器。且EFLAG0=0x16,EFLAG3=0x216,对比看得出结论:中断门在0环将IF位置0了

    66.png

1.4 练习:RETF返回中断门

中断门提权进0环的堆栈如下右图:

96.png

相比于调用门多压入一个EFLAG3寄存器,在中断门中用RETF返回,只需将[ESP0+0x8]写到EFLAG3,然后让ESP3和SS3向低地址移动4字节即可。

  1. 构造代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    #include "stdafx.h"
    #include <windows.h>
    #include <stdio.h>

    BYTE IDTItem0[8];

    // R0 函数,读取了IDT表第一项
    // 004113A0
    void __declspec(naked) R0Function()
    {
    __asm
    {
    //int 3 // 调试用的
    pushad //esp - 0x20
    mov eax,0x8003f400
    mov ebx,[eax]
    mov ecx,[eax+0x4]
    mov dword ptr ds:[IDTItem0],ebx
    mov dword ptr ds:[IDTItem0+0x4],ecx

    // 要求用 retf 返回
    add esp,0x28 // esp0指向eflags3
    popfd // esp0+4,esp0指向3环esp3
    mov eax,[esp] // 将原ESP3和SS3向低地址移动4字节
    mov [esp-0x4],eax
    mov eax,[esp+0x4]
    mov [esp],eax

    sub esp,0x2C
    popad

    retf
    }
    }

    int main(int argc, char* argv[])
    {
    __asm
    {
    INT 32
    }
    printf("%08x %08x\n", *(PDWORD)IDTItem0, *(PDWORD)((PBYTE)IDTItem0+0x4));
    getchar();
    return 0;
    }
  2. 构造中断门描述符,0040EE00`00081020。

    68.png

    69.png

  3. 修改IDT表的中断向量为32的那项。

    70.png

  4. 回到XP中取消断点后F5执行结果如下。

    67.png

1.5 练习:IRETD返回调用门

iretd返回会弹出5个值(EIP、CS3、EFLAG3、ESP3、SS3),但是retf返回仅4个值(EIP、CS3、ESP3、SS3)

故使用IRETD返回调用门时只需要:将ESP和SS向高地址移动4字节,将EFLAG写到[ESP+0x8]即可

这样能成功的原因:调用门提权时并不会将EFLAG的IF清零,所以在0环执行代码的时候将EFLAG压栈不会产生错误。

调用门(无参数):

95.png

  1. 构造代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    #include "stdafx.h"
    #include <windows.h>
    #include <stdio.h>
    #include <tchar.h>

    DWORD dwHigh2GValue;

    // 该函数通过 CALL FAR 调用,使用调用门提权,拥有0环权限
    void __declspec(naked) FunctionHas0CPL()
    {
    __asm
    {
    pushad //esp - 0x20
    pushfd //esp - 0x24

    // 读取了GDT表第二项的低4字节
    mov eax,0x8003f008
    mov eax,[eax]
    mov dwHigh2GValue,eax

    // 要求用 iretd 返回
    add esp,0x30 // esp指向ss

    mov eax,[esp] // 将原ESP和SS向高地址移动4字节
    mov [esp+0x4],eax
    mov eax,[esp-0x4]
    mov [esp],eax
    pushfd
    sub esp,0x2c // 还原esp

    popfd
    popad

    iretd
    }
    }

    int main(int argc, char* argv[])
    {
    char buff[6] = {0,0,0,0,0x48,0};
    __asm
    {
    call fword ptr [buff] // 长调用,使用调用门提权
    }
    printf("%08x\n",dwHigh2GValue);
    getchar();
    return 0;
    }
  2. 构造调用门描述符,0040EC00`00081020。

    71.png

    72.png

  3. 修改GDT表对应的项,eq 8003f048 0040EC00`00081020。

    73.png

  4. 回到XP中取消断点后F5执行,如下。

    74.png

2 陷阱门

陷阱门和中断门除了以下区别,在使用上没什么区别。

  • TYPE域(32位)
    • 中断门:Type=0x1110=0xE。
    • 陷阱门:Type=0x1111=0xF。
  • IF位是否清零(进入0环后)
    • 中断门:IF清零
    • 陷阱门:IF不清零

使用陷阱门同样会压栈5个参数,应该说通过INT N进入0环都会压栈5个参数。Windows不使用陷阱门(但是我们可以构造,因为CPU支持)。

陷阱门结构如下:

75.png

  • D=0,16位陷阱门
  • D=1,32位陷阱门

中断门:高四字节的的第8位 = 0

陷阱门:高四字节的的第8位 = 1

CPU 必须支持中断,中断分为可屏蔽中断和不可屏蔽中断。

中断是基于硬件的,鼠标,键盘是可屏蔽中断,电源属于不可屏蔽中断。当我们拔掉电源之后,CPU并不是直接熄灭的,而是有电容的,此时不管你eflags的IF位是什么,都会执行int 2中断,来进行一些收尾的动作。

中断是可以进行软件模拟的,称为软中断。 也就是通过 int n 来进行模拟。 我们构造的中断门,并且进行int n模拟就是模拟了一次软中断。

  1. GDT表中没有调用门描述符,IDT表中没有陷阱门描述符。
  2. Windows不使用调用门陷阱门

3 任务段

在调用门、中断门与陷阱门中,一旦出现权限切换,那么就会有堆栈的切换。而且,由于CS的CPL发生改变,也导致了SS也必须要切换。切换时,会有新的ESP0和SS0(CS是由中断门或者调用门指定)这2个值是从TSS来的。

执行流程:3环CALL任务段选择子 --> 根据段选择子到GDT找到对应任务段描述符 --> 将任务段描述符加载到TR寄存器,同时根据任务段中的Base找到TSS内存块的起始地址 --> 根据TSS中的EIP去执行代码 --> IRETD返回。

3.1 TSS结构及作用

TSS(Task-state segment ),任务状态段。是一块内存,大小为104字节,其中存的是一堆寄存器的值。一个任务对应一块TSS。

32位的TSS结构图如下:

76.png

用结构体可以表示为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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_selector;//ldt段选择子,用于换ldtr寄存器,一个TSS对应一个LDT表,就算你有100个任务,那么ldtr寄存器里
//面存储的也是当前ldt表,也就是任务切换时,LDT表会切换,但GDT表不会切换
// 这个暂时忽略
DWORD io_map;
} TSS;

低地址中的ESP0,SS0用于从3环堆栈到0环堆栈。切换CR3等于切换进程。

Intel的设计TSS初衷是:切换任务(站在CPU的角度来说,操作系统中的线程可以称为任务)。CPU考虑到操作系统的线程在执行的时候会不停的切换,所以设计了TSS,让任务可以来回的切换。

但是操作系统并没有采用该方法切换线程(Windows、Linux都没有这样做)。Windows仅使用了TSS中的ESP0、SS0。

对TSS作用的理解应该仅限于存储寄存器即可,跟任务(线程)切换没有关系。TSS的意义就在于可以同时换掉”一堆”寄存器

  1. TSS是一块104字节内存,通过TR寄存器找到这块内存,TR寄存器的Base指向这块内存,Limit为这块内存的大小,TR的值来自GDT表的TSS段描述符。
  2. CPU中的一个任务对应一块TSS内存,任务切换时TSS也会跟着切换,TSS是跟随任务的一个链表,如32位TSS的低4字节就指向前一个TSS段描述符的段选择子
  3. TSS替换寄存器的过程:3环代码CALL/JMPTR使用段选择子触发TSS段描述符,然后将段描述符加载到TR寄存器,根据TR的Base找到TSS内存块,将内存块的值加载到其结构包涵的所有寄存器中,然后执行TSS中的EIP,该EIP是从TSS段内存块中来的

关于TSS结构中的LDT Segment Selector成员:

  1. 该成员是LDTR段寄存器的可见16位部分(段选择子)。
  2. LDT段选择子去查LDT表,根据LDT表中对应的段描述符装载此时的LDTR寄存器。
  3. LDTR段寄存器的Base指向LDT表,Limit为LDT表大小。
  4. 一个LDTR寄存器的值对应于当前任务,一个任务一张LDT表。
  5. Windows没有使用LDT表和LDTR寄存器。

3.2 TSS、TR读写

TSS替换寄存器的过程:3环代码CALL/JMP使用TR段选择子触发TSS段描述符,然后将段描述符加载到TR寄存器,根据TR的Base找到TSS内存块,将内存块的值加载到其结构包涵的所有寄存器中。过程如下:

78.png

TSS段描述符结构如下:

79.png

TSS段描述符中:

  • Type域
    • 高四字节的第9位是一个判断位,如果此时该TSS段描述符已经被加载到TR寄存器中,那么该位为1,16进制下为B。
    • 如果该TSS段描述符没有被加载到TR寄存器中,那么该位为0,16进制下为9。
  • G位
    • G=0,说明寻址按字节来(TSS用)
    • G=1,说明寻址按4kb来(页)

TR寄存器读写

(1)写TR寄存器:将TSS段描述符加载到TR寄存器,使用指令:LTR

有几点需要注意:

  • 用LTR指令去装载的话 仅仅是改变TR寄存器的值(96位) ,并没有真正改变TSS。

  • LTR指令只能在系统层使用。(当前CPU权限必须是0环的)

  • 加载后TSS段描述符会状态位会发生改变。(高四字节的第9位发生变化)

    1
    2
    mov ax,SelectorTSS
    ltr ax

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

(2)读TR寄存器:使用指令:STR

  • 如果用STR去读的话,只读了TR的16位也就是段选择子。这跟读取CS段寄存器一样,读取16位。(读16位写96位)

    1
    str ax

注意:

使用LTR仅能在0环权限下修改TR段寄存器,但是并不会改变TSS的值,要想同时改变TR寄存器和TSS的值,可以在Ring3下使用CALL FAR、JMP FAR指令来修改。(在Intel手册第二卷有关于Call的三种用法介绍:段内调用、远调用、任务段调用)

3.3 CALL、JMP访问任务段的区别

  1. 调用时的区别
  • CALL FAR:EFLAGS 的 NT位置1,会修改TSS previous task link,属于任务嵌套。

  • JMP FAR: NT位=0,不会修改TSS previous task link,不属于任务嵌套。

  1. 使用iretd返回时的区别:CPU根据NT位决定返回方式,NT位影响iretd
  • 如果NT=1,CPU使用TSS的 Previous task link 里存储的上一个任务的TSS选择子进行返回(是上一个TSS段选择子,里面保存的EIP即为返回地址任务返回
  • 如果NT=0,则使用堆栈中的值返回(中断返回

58.png

关于在我们自己的代码中使用int 3,函数在执行到该指令无法正常返回时蓝屏的解释(int 3在0环做的两件事)?(目前还不清楚具体原因

  1. 修改FS段寄存器
  2. 将NT位置0

但是在使用int 3后,恢复上述两种任何一个值都可以正常返回

3.4 练习:使用CALL调用任务段修改所有寄存器

使用CALL FAR实现用TSS替换寄存器。

CALL FAR:EFLAGS 的 NT位置1,会修改TSS previous task link

大体流程为:

  1. 准备一个104字节的TSS,并附上正确的值。
  2. 准备一个自己写的TSS段描述符,写入到GDT表的一个空白的位置。
  3. 修改TR寄存器(CALL FAR,JMP FAR)。
  4. 注意TSS中的EIP,该EIP是从TSS内存块中来的。

TSS可以使用数组,也可以VirtualAlloc,建议后者,因为TSS最好是页对齐的

  1. 准备TSS。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // 此数组的地址就是TSS描述符中的Base
    DWORD *TSS = (DWORD*)VirtualAlloc(NULL,104,MEM_COMMIT,PAGE_READWRITE);
    TSS[0] = 0x00000000; // Previous Task Link 由CPU自己填充,表示上一个任务TR的选择子
    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
  2. 准备TSS段描述符并写入GDT表P=0的位置,XX00E9XX `XXXX0068,X为申请的104字节内存的首地址。

    • G位为0,单位是字节
    • TSS一开始的类型是9(可用),当加载到TR中就会变成B( 正被占用)
  3. 完整代码如下。需要注意:

    • 程序运行时需要修改CR3的值,使用!process 0 0
    • TSS中的EIP为即将要执行的函数的入口地址
    • TSS中的ESP为即将要执行的函数的堆栈地址,但是堆栈是往低地址使用,所以需要给ESP赋值一个已经申请预留的堆栈中的一个相对较高的地址
    • IO位图、GS、LDT Windows已经没有使用了(win7没用IO位图)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    #include "stdafx.h"
    #include <Windows.h>
    #include <stdio.h>
    #include <tchar.h>
    #include <stdlib.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,复制当前进程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);
    system("Pause");
    getchar();
    return 0;
    }
  4. 在XP中执行上述代码。

  5. 此时根据提示在Windbg中修改GDT表的段描述符:

    eq 8003f048 0000e93a·00000068

    !process 0 0;g获取CR3的值

  6. 根据CR3的值回到XP中输入并回车,此时将会在Windbg中断在int 3,输入g执行返回,如果使用p单步将无法正常返回。

    80.png

  7. 使用int 3断下来后单步调试时观察堆栈和寄存器。

    1. 在XP中执行CALL FAR前下断点观察寄存器。

      81.png

    2. 观察此时的TSS情况。

      82.png

    3. 在XP中取消断点后F5执行,此时Windbg中查看0堆栈情况。堆栈中即为函数压入栈的8个通用寄存器、EFLAG寄存器、FS段寄存器。观察寄存器值也都成功替换。

      83.png

    4. 此时观察TSS中的数值。

      84.png

    5. 根据段选择子0x0028找到对应的段描述符80008b04`200020ab为任务切换的TSS段描述符,纪录的TSS段起始地址为:80042000,如下图即为上一个TSS段内存的数据。

      85.png

3.5 练习:使用JMP调用任务段修改所有寄存器

JMP FAR: NT位置0,不会修改TSS previous task link(PTL)。

分别使用两种方式返回:

  • JMP:既然是JMP触发执行的任务段,那也可以在裸函数中使用JMP跳到之前的TR保存的任务段进行返回。

  • iretd:由于JMP FAR任务段不会保存PTL,且NT位置0,则需要在裸函数中将前一个TR的选择子填充到TSS的前2字节和NT置1。(但是在真正测试时即使在iretd前修改NT位为1,当执行到EIP=iretd时CPU还是会将NT位置为0,无法正确返回。

  1. 准备TSS。

  2. 准备TSS段描述符并写入GDT表P=0的位置,XX00E9XX `XXXX0068,X为申请的104字节内存的首地址。

  3. 完整代码如下。需要获取任务切换前的TR寄存器的段选择子,方便裸函数跳转返回。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    #include "stdafx.h"
    #include <Windows.h>
    #include <stdio.h>
    #include <tchar.h>
    #include <stdlib.h>

    DWORD dwOk;
    DWORD dwESP;
    DWORD dwCS;
    BYTE PrevTR[6] = {0};//保存任务切换前的TR段选择子
    // 任务切换后的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

    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,复制当前进程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
    {
    pushad
    str ax
    lea edi,[PrevTR+4]//JMP后面的6个字节,低4字节为EIP,高2字节为段选择子
    mov [edi],ax
    popad

    jmp fword ptr[buff]
    }
    printf("ok: %d\nESP: %x\nCS: %x\n", dwOk, dwESP, dwCS);
    system("Pause");
    getchar();
    return 0;
    }
  4. VC6中运行后到Windbg中写对应的GDT表TSS段描述符,并获取CR3的值。

    • eq 8003f048 0000e93a`00000068
    • !process 0 0;g
  5. 回到XP的VC6中输入CR3值后回车,此时断在裸函数的int 3,观察寄存器和堆栈。

    86.png

  6. 观察TSS段。

    87.png

    • 此时的PTL=0x00,说明JMP AFR任务段确实没有修改TSS中的PTL。
    • 对应的段寄存器的值已经修改成功。
  7. 此时Windbg中输入g运行后成功返回,如下图。

    88.png

在另外的测试中,使用iretd返回时,即使NT置1,并修改TSS的PTL,当执行到eip=iretd指令时,EFLAG又回自动将NT位置0,所以无法使用iretd进行返回,或者可以尝试使用中断返回试试(使用堆栈)。

结论:3环CALL任务段选择子 --> GDT找到对应任务段描述符 --> 将任务段描述符加载到TR寄存器,同时根据任务段中的Base找到TSS内存块的起始地址 --> 根据TSS中的EIP去执行代码 --> IRETD返回。

  1. CALL任务段不会将返回地址压栈,因为使用TR的选择子跳过去。返回也不会使用到返回地址,而是使用PTL(前一个TR的段选择子)
  2. 即使在iretd前修改NT位为1,当执行到EIP=iretd时CPU还是会将NT位置为0,无法正确返回(有可能是因为要切回去的那个任务的TSS段描述符的B位为1,正在忙,处理器是通过TSS的B位来检测重入的。因中断,iret,call,jmp指令发起的任务切换时,处理器固件会检测新任务的TSS的B位,如果该位为1,则处理器不允许这样的任务切换)。
  3. 无论何时,只要处理器碰到iret指令,它都会检查NT位。

具体详细的可以参考它的笔记二十六、二十七

4 任务门

Windows、Linux都没有使用CPU提供的通过TSS来切换线程,而是使用堆栈。那操作系统用TSS来做什么?

既然已经有了任务段为什么还要有任务门?

任务门存在的意义其一 int 8双重错误举例:

假设一个除以0错误,首先会触发int 0中断去查IDT表中对应的地址去执行,如果在执行过程中如果再次出错时就会触发int 8。

int 8是如何接管处理的呢?一旦进入8号中断,将会替换一堆寄存器,保证CPU 能跳到一个正确的地方去执行(除非那个地方也被破坏了),此时什么错误都无所谓了,收集信息后,蓝屏。

任务门int 8查的IDT表即为任务门:

IDT–00008500`00501198–0050–0101 0 0 00–0xA–GDT第十一个

GDT–80008955`27000068–32位TSS任务段Base:80552700–dd 80552700–80544509–uf 80544509

调用任务段的三种方法CALL/JMP FAR、任务门

IDT表可以包含3种门描述符:

  • 任务门描述符
  • 中断门描述符
  • 陷阱门描述符

任务门描述符:

89.png

TSS段描述符结构如下:

79.png

任务门的执行过程:

  1. INT N指令来去IDT表中找对应的任务门描述符
  2. 查询IDT表找到任务门描述符
  3. 通过任务门描述符表的TSS段选择子查询GDT表,找到任务段描述符
  4. 根据TSS段描述符的Base找到TSS任务段内存块
  5. 使用TSS段中的值修改寄存器
  6. IRETD返回

执行流程图如下:

94.png

4.1 练习:使用任务门修改所有的寄存器的值

  1. 构造任务门段描述符:0000E500`00480000

    • P=1
    • DPL=0x11=3(能让3环的代码访问到这个门)
    • S=0
    • 0x1110=0xE
    • 任务门:0x0101=0x5
    • TSS段选择子:0x0048(查GDT的第10项)0100 1 0 00
  2. 修改IDT表。(找个P=0的,如第33项)eq 8003f500 0000E500`00480000

    90.png

  3. 代码如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    #include "stdafx.h"
    #include <Windows.h>
    #include <stdio.h>
    #include <tchar.h>
    #include <stdlib.h>

    DWORD dwOk;
    DWORD dwESP;
    DWORD dwCS;
    DWORD *TSS = NULL; //TSS内存块
    // 任务切换后的EIP
    void __declspec(naked) R0Func()
    {
    __asm
    {
    pushad
    pushfd

    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
    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,复制当前进程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
    {
    //jmp fword ptr[buff]
    int 32
    }
    printf("ok: %d\nESP: %x\nCS: %x\n", dwOk, dwESP, dwCS);
    system("Pause");
    getchar();
    return 0;
    }
    1. 在VC6中执行代码。

    2. 根据TSS任务段的地址在GDT表中构造TSS任务段描述符,然后获取CR3的值。(!process 0 0

      91.png

      92.png

    3. 回到XP的VC6中输入CR3的值06e40300后F5继续执行。

      93.png

Windows XP 段保护(一)

Windows XP 段保护(二)

Windows XP 段保护(三)