Windows

参考文章:

Linux 学习笔记(九):基于 TSS 的进程切换-CSDN博客

任务状态段TSS及TSS描述符、局部描述符表LDT及LDT描述符_tss段描述符-CSDN博客

保护模式第六讲-IDT表-中断门 陷阱门 任务门 - iBinary - 博客园 (cnblogs.com)

上一节的通过Cr3任意读写如何防?

如何在自己运行的时候cr3是正确的,但是别的进程拿的时候是错误的?

  1. 中断/系统调用钩子

在操作系统级别拦截系统调用或中断处理程序,并在适当的时候修改 CR3。通过这种方式,可以在当前进程执行时使用正确的 CR3,而在其他进程尝试获取 CR3 时返回伪造的值。

实现步骤

  • 钩住所有对 CR3 的访问,包括直接读取寄存器、查询系统状态等。
  • 当检测到当前进程访问时,返回正确的 CR3;否则,返回伪造的 CR3。
  1. 特权指令拦截

在具有硬件虚拟化技术(如 Intel VT-x、AMD-V)的环境中,可以通过设置控制寄存器的访问权限,使用虚拟化技术拦截对 CR3 的访问。

实现步骤

  • 使用虚拟化技术将自己运行在虚拟机内,虚拟机管理程序(hypervisor)可以拦截并监控所有对 CR3 的访问。
  • 通过检测访问者的上下文决定返回正确的还是伪造的 CR3。
  1. 内核级别的钩子或补丁

修改操作系统的内核代码,使得不同的进程对 CR3 的访问返回不同的结果。这需要深入理解操作系统的内核结构和运行机制。

实现步骤

  • 找到操作系统中处理 CR3 访问的代码路径,通常涉及上下文切换、进程管理等模块。
  • 在这些位置添加代码,判断访问者身份,根据需要返回不同的 CR3。
  1. 使用隐藏技术和自我保护

这种方法涉及在当前进程运行时使用正确的 CR3,但隐藏自己的页表,使得外部的工具或进程无法正确访问或检测。

实现步骤

  • 使用内存隐藏技术,如设置页表不可访问、动态切换页表等。
  • 在需要的时候切换回伪造的 CR3,使其他进程看到的页表不同。
  1. 多态性/自修改代码

通过在代码执行过程中动态修改代码或数据,避免外部检测工具获取真实信息。

实现步骤

  • 定期或随机地修改伪造的页表和数据结构。
  • 使用不同的机制自我保护,避免外部分析。

GDT和IDT

GDT:主要存储段描述符(代码段、数据段、TSS描述符等)和调用门。可以包含任务门,但任务门更常用于IDT。

1
2
3
4
5
6
7
#pragma pack(push, 1) 
struct GDTR
{
unsigned short limit;
unsigned int base;
};
#pragma pack(pop)

IDT:专门用于存储中断和异常处理的门描述符,包括中断门、陷阱门和任务门。每个核都有一个IDT,也就是如果要Hook,就要所有核心都要HOOK

1
2
3
4
5
6
#pragma pack(push, 1) // 确保结构体按 1 字节对齐
struct IDTR {
unsigned short limit; // IDT 表界限(长度),单位为字节
unsigned int base; // IDT 表基址(物理地址)
};
#pragma pack(pop)

1722687153176

Type(二进制) 描述符类型 描述
0000 保留 未定义,保留
0001 16位TSS(可用) 可用的16位任务状态段
0010 LDT 局部描述符表
0011 16位TSS(忙碌) 忙碌的16位任务状态段
0100 16位调用门 16位调用门(Call Gate)
0101 任务门 任务门(Task Gate)
0110 16位中断门 16位中断门(Interrupt Gate)
0111 16位陷阱门 16位陷阱门(Trap Gate)
1000 保留 保留,未定义
1001 32位TSS(可用) 可用的32位任务状态段
1010 保留 保留,未定义
1011 32位TSS(忙碌) 忙碌的32位任务状态段
1100 32位调用门 32位调用门
1101 保留 未定义,保留
1110 32位中断门 32位中断门
1111 32位陷阱门 32位陷阱门

系统段的简略介绍

  1. 任务状态段TSS(Task-State Segment):
    • TSS 是一种特权结构,保存了任务切换所需的处理器状态。分为 16 位和 32 位两种。
    • 可用的 TSS 描述符(Type 为 0001 或 1001)表示该 TSS 可被加载为当前任务。
    • 忙碌的 TSS 描述符(Type 为 0011 或 1011)表示该 TSS 正在使用。
  2. 局部描述符表(LDT):
    • LDT 是一个描述符表,可以定义属于特定任务或进程的段。LDT 描述符(Type 为 0010)用于在 GDT 中指定一个 LDT 的位置和大小。
  3. 调用门(Call Gate):
    • 调用门用于实现不同特权级别之间的安全调用。16 位和 32 位调用门(Type 分别为 0100 和 1100)指定了目标代码段和入口点,用来实现系统调用。
  4. 任务门(Task Gate):
    • 任务门用于任务切换,直接切换到指定的 TSS。任务门描述符(Type 为 0101)保存了目标 TSS 的选择子。
  5. 中断门(Interrupt Gate):
    • 中断门用于处理中断或异常。16 位和 32 位中断门(Type 分别为 0110 和 1110)指定了中断处理程序的入口地址。中断门将中断控制转移到特定的处理程序,并自动清除中断标志(IF),以防止嵌套中断。
  6. 陷阱门(Trap Gate):
    • 陷阱门类似于中断门,但在转移控制权时不会清除中断标志。16 位和 32 位陷阱门(Type 分别为 0111 和 1111)用于处理需要保留中断能力的情况。

TSS任务状态段

在一个多任务环境中,当发生了任务切换,需保护现场,因此每个任务的应当用一个额外的内存区域保存相关信息,即任务状态段(TSS);TSS格式固定,104个字节,处理器固件能识别TSS中元素,并在任务切换时读取其中信息。 TSS 就是内存中的一个结构体,里面包含了 几乎所有的 CPU 寄存器的映像 。

1722739623369

1722739717275

每创建一个任务,就要创建一个104字节的TSS

过程就是首先生成一个TSS,然后放到GDT里面,就可以得到一个下标(段选择子)

TR寄存器

TR(Task Register)即任务寄存器,指向当前进程对应的 TSS 结构体。

TR 指向当前进程对应的 TSS 结构体,但具体是怎么 “指向” 的呢?—— 通过 GDT 表。具体实现为:TR 存储当前进程的 TSS 结构体的段选择符,通过段选择符在 GDT 表中找到对应 TSS 结构体的内存位置。

在Windbg里面可以用 dg 指令去解析这个段选择子

1722739457717

如何用汇编切换tr寄存器?

1
2
mov ax,28
ldtr ax

TSS 切换

所谓的 TSS 切换 就是将 CPU 中 几乎所有的寄存器 都复制到 TR 指向的那个 TSS 结构体中保存起来,同时还要找到一个目标 TSS(即将要切换到的下一个进程所对应的 TSS),并将其中存放的寄存器映像 “扣” 在CPU 上,就完成了执行现场的切换,如下图所示。

TSS是一个失败的设计

GDT表只能放8192项,而线程的最大数量是65536个,这下GDT放不下了,咋整??

所以TSS需要自己管理

在结构体 _KPCR中(fs:0)有结构体 _KPRCB

在_KPRCB里面有一个 Ptr32 _KTSS结构体

1722741975119

这样查出来的TSS地址和dg tr 查出来的地址是一样的

目前操作系统好像已经不用GDT存了,但是我们仍然可以用Tr寄存器去查询TSS的地址

做法是改掉tr寄存器作为段选择子查出来的地址就可以不需要GDT存储了

没有浮点寄存器?浮点处理器是单独的处理器,叫协助处理器,当需要计算浮点的时候,cpu就会叫这个处理器去处理

中断

什么是中断

CPU 必须支持中断 不管你当前执行了多少流程. 但是只要你操作键盘. 那么CPU就会第一时间响应

比如 win + D 显示桌面 win + L 锁屏,中断时基于硬件的.

可屏蔽中断
比如键盘. 键盘是可屏蔽中断

不可屏蔽中断.
当我们拔掉电源之后,CPU并不是直接熄灭的. 而是有电容的,此时不管你IF是什么.都会执行 int 2中断. 来进行一些收尾的动作.

inter手册说. 中断和异常 是强制性执行流的转移 也就是不管你处于那个环境.只要有中断 和异常.都会强制执行的.换句话来讲就是 从当前流程强制转到另一个流程

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

inter手册所说. 任何可屏蔽中断 可以通过使用 interl 架构定义的中断向量(0`255) 也就是说有一个中断表. 大小是255项. 通过 E Flags 中的 if 位 可以进行屏蔽 (对于不可屏蔽中断无效)

软件中断模拟的注意事项. inter手册所属. 0-255 我们都是可以通过 int n进行模拟的. 但是有一个注意的问题就是. 比如不可屏蔽中断. int 2. 如果我们用int 2来进行模拟 是不会产生真正的效果的 意思就是说.你执行int 2确实是执行了. 但是硬件上并没有激活. 也就是说你是假的

inter手册所说 0-31中断向量是给 NMI中断使用的.也就是给CPU使用的. 剩下的32 - 255 才是给用户使用的.

1722848982075

中断门:

门的本质一样的,就是去IDT表找,根据IDT表找到对应的段选择子然后加载

中断门可以将处理器从较低特权级(如用户模式)切换到较高特权级(如内核模式)。

中断门用于处理硬件中断和软件异常。当中断发生时,处理器使用IDT查找相应的中断门,然后跳转到由该门描述的中断处理程序。

当中断发生时,处理器会自动保存当前的指令指针(EIP或RIP)和标志寄存器(EFLAGS)的状态,并跳转到中断处理程序。这使得中断处理程序可以在执行完后恢复到中断前的状态。

中断门 是由int xxx汇编指令来进行触发的. 进而去IDT表中中寻找中断门段描述符

int xxx; xxx是索引. 是IDT表中的索引. IDT.base + xxx * 8 = 实际的中断门所在的位置.

也可以理解为 IDT[xxx -1]

例如 int 3 查找的就是 IDT[2] 索引 数组从下标从0开始 0 1 2 正好查找的是第三项

相比于16位,32位系统因为多了保护模式,因此中断门不仅描述了函数地址,还有DPL(描述特权级),参数等,

IDT表基地址存在于IDTR寄存器,用来存陷阱门,中断门

解析中断门:

1722846566673

在32位模式下,中断门描述符占8字节(64位),结构如下:

  • Offset 15:0(0-15位):中断处理程序入口地址的低16位。

  • Selector(16-31位):段选择器,指向中断处理程序所在的代码段。

  • Reserved(32-39位):保留位,通常置为0。

  • Type and Attributes

    (40-47位):描述符的类型和一些控制属性。

    • Type(40-43位):固定为1110(0xE),表示这是一个32位中断门。
    • DPL(Descriptor Privilege Level)(45-46位):描述符特权级别,0为最高优先级,3为最低优先级。
    • P(Present)(47位):指示描述符是否有效,1表示有效。
  • Offset 31:16(48-63位):中断处理程序入口地址的高16位。

1
2
3
4
5
6
7
8
9
10
11
#pragma pack(push, 1) // 确保结构体按 1 字节对齐
struct InterruptGateDescriptor {
uint16_t offset_low; // 中断处理程序入口地址的低 16 位
uint16_t selector; // 目标代码段选择器
uint8_t reserved; // 保留位(通常为 0)
uint8_t type : 5; // 门类型,11110 表示 32 位中断门
uint8_t dpl : 2; // 描述符特权级别(DPL),通常为 0
uint8_t present : 1; // 存在位(P),1 表示有效
uint16_t offset_high; // 中断处理程序入口地址的高 16 位
};
#pragma pack(pop)

中断门的Call调用流程流程图

1722843507962

1.首先通过中断号寻址到中断门 ,然后从中断门中分别取出记录的 offset(函数偏移) + 代码段段选择子

2.然后根据代码段段选择子. 去GDT表或者LDT表中查询 代码段描述符

3.从代码段描述符中取出记录的 Base(基地址),还要查个权限,没有问题就进入第四步

4.取出的基地址与中断门中记录额函数偏移(offset)相加得到一个真正的函数地址.

5.进行调用

任务门和任务状态段的关系

任务状态段(TSS)

TSS是一个数据结构,用于保存任务的CPU状态。它包含了任务切换时所需的寄存器值、堆栈指针等信息。每个任务都有一个TSS,描述了任务在切换时需要保存和恢复的状态。

任务门(Task Gate)

任务门是IDT或GDT中的一个特殊描述符,用于触发任务切换。当处理器遇到任务门时,会通过任务门指向的TSS描述符切换到另一个任务。

关系和工作机制

  1. 任务门指向TSS描述符
    • 任务门不直接包含TSS的信息,而是指向一个TSS描述符。
    • TSS描述符存储在GDT或LDT中,描述了具体TSS的位置和大小。
  2. 任务切换
    • 当处理器遇到任务门(例如在IDT中的任务门描述符),处理器会通过任务门找到对应的TSS描述符。
    • 处理器根据TSS描述符找到具体的TSS,并加载TSS中的状态,从而切换到新的任务。
    • 在任务切换过程中,处理器会保存当前任务的状态到当前任务的TSS,并从新的TSS加载新的任务的状态。

总结

  • TSS:保存和恢复任务状态的具体数据结构。
  • 任务门:指向TSS描述符的描述符,用于触发任务切换。

三种Hook技术

先看看,后续有机会学到上演示代码

1. HOOK IDT

IDT Hooking 是一种修改中断描述符表(IDT)的技术。通过修改IDT中的中断门或陷阱门的指向,可以重定向中断或异常处理程序。以下是IDT Hooking的主要应用:

  • 拦截和监控中断和异常:可以用来捕获特定的中断或异常,例如捕获系统调用来监控或修改其行为。
  • 实现调试功能:可以通过Hook系统中断来捕获异常,帮助开发者调试程序。
  • 恶意软件:某些恶意软件可能使用这种技术来隐藏自己或劫持合法程序的控制流。

注意:IDT Hooking 是一个非常敏感的操作,因为它直接修改系统关键的中断处理机制。误用可能导致系统不稳定或崩溃。

2. APIC 重定位 Hook

APIC(Advanced Programmable Interrupt Controller)重定位 Hook 是一种修改和控制APIC的技术。APIC用于管理和分配中断请求(IRQ)。通过这种技术,可以实现以下功能:

  • 中断管理和优化:改变中断的分发方式,提高系统性能或减少中断延迟。
  • 安全和监控:监控和控制硬件中断,以检测异常活动或保护系统安全。

重定位APIC通常涉及修改 APIC基址寄存器(APIC Base Address Register, APICBAR)中断重映射表(Interrupt Remapping Table),这些操作通常需要特权级别较高的访问权限。

3. Hook 驱动

Hook 驱动 是指拦截和修改驱动程序的行为。驱动程序是操作系统与硬件之间的接口,它们处理硬件请求并将其转换为操作系统可以理解的格式。通过Hook驱动程序,可以实现:

  • 功能扩展:为现有驱动程序添加额外的功能或修改其行为。
  • 安全监控:拦截和分析驱动程序的输入和输出数据,以检测和防止恶意活动。
  • 调试:捕获和记录驱动程序的调用,以帮助开发和调试硬件设备。

Hook驱动的技术通常涉及修改内核空间的代码或数据结构,这通常需要较高的系统权限,类似于内核模式的DLL注入技术。

屏蔽中断

屏蔽INT 3中断

首先解析一下INT 3中断在IDT表的中断门表项

1722863435543

按照之前说的解析一下中断门

在32位模式下,中断门描述符占8字节(64位),结构如下:

1722846566673

  • Offset 15:0(0-15位):中断处理程序入口地址的偏移低16位。

  • Selector(16-31位):段选择器,指向中断处理程序所在的代码段。

  • Reserved(32-39位):保留位,通常置为0。

  • Type and Attributes

    (40-47位):描述符的类型和一些控制属性。

    • Type(40-43位):固定为1110(0xE),表示这是一个32位中断门。
    • DPL(Descriptor Privilege Level)(45-46位):描述符特权级别,0为最高优先级,3为最低优先级。
    • P(Present)(47位):指示描述符是否有效,1表示有效。
  • Offset 31:16(48-63位):中断处理程序入口地址的偏移高16位。

1
2
3
4
5
6
7
8
9
10
11
#pragma pack(push, 1) // 确保结构体按 1 字节对齐
struct InterruptGateDescriptor {
uint16_t offset_low; // 中断处理程序入口地址的低 16 位
uint16_t selector; // 目标代码段选择器
uint8_t reserved : // 保留位(通常为 0)
uint8_t type : 5; // 门类型,11110 表示 32 位中断门
uint8_t dpl : 2; // 描述符特权级别(DPL),通常为 0
uint8_t present : 1; // 存在位(P),1 表示有效
uint16_t offset_high; // 中断处理程序入口地址的高 16 位
};
#pragma pack(pop)

所以我们可以得到以下信息:
中断处理程序入口地址:0x83c4bfb0
段选择子(因为是代码段,所以是cs):0x8(可以查出是0环)
Type位:01110
DPL:3(最低优先级)
P:1(有效)

c++

当然也有自动解析IDT的命令:

1
!idt -a

这会列出全部的解析过的IDT表项

然后会发现有0x20 ~ 0x29是空的,这是留给用户自己的

1722864645714

根据段选择子,查出来段基址是0

1722863886021

因此函数的入口地址为:Base + offset = 0 + 0x83c4bfb0 = 0x83c4bfb0

也就是说产生INT3异常之后,代码就会强制转移至0x83c4bfb0

那么如何屏蔽呢?

可以直接把中断门表项抹除吗?当然不行了,因为这样Cpu会崩溃,直接寄

我们可以直接Return,但是不能用RET指令,而是要用IRETD

修改以后,我们回到调试器,发现把软件拉到调试器,根本断不下来,就直接运行了,甚至于在Windbg也断不下来

1722865073398

当然这样会屏蔽所有程序的INT 3 中断,我们可以在HOOK中断的时候加一个判断即可,判断是不是需要保护的程序,如果是,那么将屏蔽中断

解决方法:调试器也上驱动,这样对抗,才可以调试

注册中断代码:

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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#include "header.h"

#pragma pack(push, 1) // 强制1字节对齐


struct InterruptGateDescriptor {
unsigned short offset_low; // 中断处理程序入口地址的低 16 位
unsigned short selector; // 目标代码段选择器
unsigned char reserved; // 保留位(通常为 0)
unsigned char type : 5; // 门类型,11110 表示 32 位中断门
unsigned char dpl : 2; // 描述符特权级别(DPL),通常为 0
unsigned char present : 1; // 存在位(P),1 表示有效
unsigned short offset_high; // 中断处理程序入口地址的高 16 位
};

struct IDTR {
unsigned short limit; // IDT 表界限(长度),单位为字节
unsigned int base; // IDT 表基址(物理地址)
};
#pragma pack(pop)

__declspec(naked) VOID Interrupt()
{
__asm
{

pushad; 保存通用寄存器
mov ax, 0x30; 修改 fs 寄存器,确保与当前特权级别和上下文兼容
mov fs, ax
}
KdPrint(("中断调用成功!\n"));

__asm
{
popad; 恢复通用寄存器
iretd; 从中断返回
}
}





VOID Register_InterruptGate()
{

KAFFINITY affinityMask;
ULONG numCPUs = KeQueryActiveProcessorCount(NULL);

for (ULONG i = 0; i < numCPUs; i++) {
affinityMask = (KAFFINITY)(1ULL << i);
KeSetSystemAffinityThread(affinityMask);
//注册门
IDTR idtr;
__asm
{
sidt idtr
}
unsigned int Base = idtr.base;



InterruptGateDescriptor* InterruptGateDescriptor_ptr = (InterruptGateDescriptor*)Base;

InterruptGateDescriptor_ptr[0x21].present = 1;//表示描述符有效
InterruptGateDescriptor_ptr[0x21].dpl = 3; //三环可以使用
InterruptGateDescriptor_ptr[0x21].offset_high = (unsigned int)Interrupt >> 16;
InterruptGateDescriptor_ptr[0x21].offset_low = (unsigned int)Interrupt & 0xffff;
InterruptGateDescriptor_ptr[0x21].reserved = 0;
InterruptGateDescriptor_ptr[0x21].selector = 8;
InterruptGateDescriptor_ptr[0x21].type = 0xE;

}

// 恢复线程的原始 CPU 亲和性
KeRevertToUserAffinityThread();

}




NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
KdPrint(("Create!"));
KdBreakPoint();
// 驱动程序卸载例程&注册例程
DriverObject->DriverUnload = DriverUnloadRoutine;
//创建设备
NTSTATUS status;
PDEVICE_OBJECT DeviceObject = NULL;
UNICODE_STRING DeviceName;
RtlInitUnicodeString(&DeviceName, L"\\Device\\MyDevice");
status = IoCreateDevice(
DriverObject, // 驱动程序对象
0, // 设备扩展大小
&DeviceName, // 设备名称
FILE_DEVICE_UNKNOWN, // 设备类型
0, // 设备特征
FALSE, // 非独占设备
&DeviceObject // 返回的设备对象指针
);

if (!NT_SUCCESS(status))
{
KdPrint(("Failed to create device: %X\n", status));
return status;
}
KdPrint(("Device created successfully\n"));

UNICODE_STRING symbolicLink = RTL_CONSTANT_STRING(L"\\??\\MyDevice_Link");
status = IoCreateSymbolicLink(&symbolicLink, &DeviceName);
if (!NT_SUCCESS(status))
{
KdPrint(("Failed to create device: %X\n", status));
return status;
}
KdPrint(("Device created successfully\n"));
Register_InterruptGate();


return STATUS_SUCCESS;
}




VOID DriverUnloadRoutine(IN PDRIVER_OBJECT DriverObject)
{
if (DriverObject->DeviceObject != NULL)
{
UNICODE_STRING symbolicLink = RTL_CONSTANT_STRING(L"\\??\\MyDevice_Link");
IoDeleteSymbolicLink(&symbolicLink);
IoDeleteDevice(DriverObject->DeviceObject);
}
//UnHookInterruptOnAllCPUs();
DbgPrint("Driver unloaded\n");
}

三环代码:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;
int main()
{
__asm
{
int 0x21
}
system("pause");
return 0;
}