Windwos内核
通过IDT Hook键盘
IRETD(Interrupt Return Double-word)是 x86 架构中的一条汇编指令,用于从中断或异常处理程序中返回到调用程序。
- 当一个中断或异常发生时,CPU 会将当前的 EFLAGS 寄存器、CS(代码段)寄存器和 EIP(指令指针)压入堆栈,并跳转到中断处理程序的入口地址。
- 当中断处理程序完成时,IRETD指令会从堆栈中弹出这些值并恢复它们,返回到中断发生前的状态。
当你 Hook 一个中断函数时,通常会使用“裸函数”(Naked Function),这是因为中断处理程序具有特殊的要求,不允许编译器在函数的入口和退出时自动插入额外的代码。这些额外的代码可能包括保存和恢复寄存器、设置堆栈帧等常规操作。为了确保中断处理程序的行为符合预期,裸函数就显得非常重要。 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | __declspec(naked) void KeyboardProc(){
 __asm{
 pushad
 pushf
 }
 
 
 KdPrint(("KeyboardProc\n"));
 
 __asm{
 popf
 popad
 iretd
 }
 }
 
 | 
Hook键盘中断的过程:
- 保存旧的中断函数
- 写上自己的函数Proc
- 在Unload函数上面替换回原来的KeyProc
以下是Hook键盘和UnHook的代码:
| 12
 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
 
 | VOID HookInterruptOnAllCPUs() {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 IDT_Base = MAKELONG(idtr.LowIDTBase, idtr.HiIDTBase);
 
 m_InterruptGateDescriptor_ptr = (InterruptGateDescriptor**)(IDT_Base);
 
 Old_KeyProc[i] = (m_InterruptGateDescriptor_ptr[0x93]->offset_high) << 16;
 Old_KeyProc[i] += m_InterruptGateDescriptor_ptr[0x93]->offset_low;
 
 __asm
 {
 
 mov eax, cr0
 and eax, not 0x10000
 mov cr0, eax
 }
 m_InterruptGateDescriptor_ptr[0x31]->offset_low = (unsigned int)KeyboardProc & 0xffff;
 m_InterruptGateDescriptor_ptr[0x31]->offset_high = ((unsigned int)KeyboardProc & 0xffff0000) >> 16;
 __asm
 {
 
 mov eax, cr0
 or eax, 0x10000
 mov cr0, eax
 }
 }
 
 
 KeRevertToUserAffinityThread();
 }
 
 VOID UnHookInterruptOnAllCPUs() {
 KAFFINITY affinityMask;
 ULONG numCPUs = KeQueryActiveProcessorCount(NULL);
 KIRQL oldIrql;
 
 for (ULONG i = 0; i < numCPUs; i++) {
 affinityMask = (KAFFINITY)(1ULL << i);
 KeSetSystemAffinityThread(affinityMask);
 
 
 KeRaiseIrql(HIGH_LEVEL, &oldIrql);
 
 IDTR idtr;
 __asm {
 sidt idtr;
 }
 
 unsigned int IDT_Base = MAKELONG(idtr.LowIDTBase, idtr.HiIDTBase);
 
 m_InterruptGateDescriptor_ptr = (InterruptGateDescriptor**)(IDT_Base);
 
 __asm
 {
 
 mov eax, cr0
 and eax, not 0x10000
 mov cr0, eax
 }
 
 m_InterruptGateDescriptor_ptr[0x31]->offset_low = Old_KeyProc[i] & 0xffff;
 m_InterruptGateDescriptor_ptr[0x31]->offset_high = (Old_KeyProc[i] >> 16) & 0xffff;
 
 __asm
 {
 
 mov eax, cr0
 or eax, 0x10000
 mov cr0, eax
 }
 
 KeLowerIrql(oldIrql);
 }
 
 
 KeRevertToUserAffinityThread();
 }
 
 | 
如果我们要知道键盘按下了什么,这就需要去键盘的特定端口拿扫描码:
 in 指令:从指定的 I/O 端口读取数据到 CPU 的寄存器中。
 out 指令:将 CPU 寄存器中的数据写入到指定的 I/O 端口。 
| 12
 3
 4
 5
 
 | __asm{
 in al,60h #从端口 0x60 读取一个字节的数据,并存储在寄存器 al 中。
 mov Scancode,al
 }
 
 | 
当然这会出现一个问题,扫描码只能被拿一次,如果我们再jmp到原来的keyproc,那么原来的KeyProc将会拿不到扫描码,所以我们要人工塞入扫描码
| 12
 3
 4
 
 | mov al,0d2hout 64h,al#当你向端口 0x64 发送 0xD2 时,控制器就知道你要传递一个字节的数据给连接的 PS/2 设备。
 mov al,ScanCode
 out 60,al
 
 | 
但是IDT Hook并不流行,因为想写兼容性好比较难。。。
还有一种方法,是去从原来的KeyProc拿,后续会说
调用门
首先调用门不会引起进程切换
IDT 是专门用于处理中断和异常的,适用于中断门和陷阱门,而调用门设计用于在不同特权级别间执行函数调用,存储在 GDT 或 LDT 中。 
调用门用于在不同特权级之间实现受控的程序控制转移  本质上,它只是一个描述符,一个不同于代码段和数据段的描述符,可以安装在GDT或者LGT中,但是不能安装在IDT(中断描述符表)中。 
 调用门描述符占用 8 字节(64 位) 
/1724140317571.png)
字段说明:
- Offset 15:0(16 位):
- Selector(16 位):
- 目标代码段选择子(Segment Selector),指向 GDT 或 LDT 中的一个代码段描述符。
 
- Param Count(5 位):
- 参数数量(Parameter Count),指定在调用门使用时,传递给目标代码段的参数数量。这些参数从调用门调用者的堆栈复制到目标代码段的堆栈中。参数是2^5=32个参数。
 
- Reserved(3 位):
- Type:这是一个 4 位的字段,用于表示调用门的类型。对于调用门,它的值应该是 1100(对应 16 位调用门)或1110(对应 32 位调用门)。该字段在描述符的低字节中。
- s:区别系统段还是数据段,0是系统段
- DPL(2 位):
- 描述符特权级别(Descriptor Privilege Level),定义了调用门的特权级别。调用门的 DPL 值表示可以调用此门的最低权限级别。
 
- P(1 位):
- 存在位(Present bit),如果为 1,表示描述符有效;如果为 0,表示描述符无效。
 
- Offset 31:16(16 位):
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | #pragma pack(push, 1)  struct CallGateDescriptor {
 unsigned short offset_low;
 unsigned short selector;
 unsigned char  param_count : 5;
 unsigned char  reserved : 3;
 unsigned char  Type : 4;
 unsigned char  s : 1;
 unsigned char  dpl : 2;
 unsigned char  present : 1;
 unsigned short offset_high;
 };
 #pragma pack(pop)
 
 | 
调用门的使用
使用调用门时,系统会从当前堆栈复制参数到目标堆栈,并跳转到目标代码段指定的入口地址,完成跨权限级别的调用。调用门主要用于特权级别之间的受控调用,比如从用户模式调用到内核模式。
特权级检查规则
在通过调用门进行特权级别转换时,CPU 会依次检查以下内容:
- CPL(当前特权级),由当前代码段的 CS 寄存器的低 2 位决定。
- RPL(请求特权级),由调用门选择子的低 2 位决定。
- 调用门描述符的 DPL,确保调用者的有效特权级(CPL 和 RPL)不高于调用门的 DPL。
- 目标代码段描述符的 DPL,确保调用者的 CPL 不高于目标段的 DPL。
- 目标代码段描述符的 C 位,决定是否允许一致性代码段在更低特权级别下被执行。
需要说明的是:如果通过调用门把控制转移到了更高特权级的非一致代码段中,那么CPL就会被设置为目标代码段的DPL值,并且会引起堆栈切换。 
人工造一个调用门
1.在GDT表找一个没用的项
2.三环就用 call   段选择子(末两位置为1,表示三环可以用):xxxx  (段选择子后面瞎写都行,主要就是段选择子,就是gdt表的某项)或者jmp 段选择子:0,这样就不会把三环地址返回值压入栈,也就不回来了。call的话压入的是0环的栈,会导致段寄存器要切换
要切的是cs0 eip0     ss0 esp0
在0环, retf返回三环,从零环的栈弹出cs3 eip3    ss3 esp3,这样就又回到了三环
3.实现门描述符 (syscall dpl = 3 ,type = 386gate,seletor=8)
选择子填8应该是相对固定的
/1724143131999.png)
调用流程:
/1724140856589.png)
左边是三环的,右边是零环的,retf就回去三环了
模拟系统API的实现:
| 12
 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
 
 | __declspec(naked) void Syscall() {
 __asm
 {
 pushad
 
 push fs
 mov ax,30h
 mov fs,ax
 
 }
 KdPrint(("Syscall\n"));
 __asm {
 
 pop fs
 popad
 retf
 }
 }
 
 VOID Register_CallGate(CallGateDescriptor* CallGateDescriptor_ptr)
 {
 
 GDTR gdtr;
 __asm
 {
 sgdt gdtr
 }
 unsigned int Base = gdtr.base;
 GDTR* gdtr_ptr = (GDTR*)Base;
 CallGateDescriptor* CallGateDescriptor_ptr = (CallGateDescriptor*)&gdtr_ptr[9];
 Register_CallGate(CallGateDescriptor_ptr);
 
 CallGateDescriptor_ptr->present = 1;
 CallGateDescriptor_ptr->dpl = 3;
 CallGateDescriptor_ptr->offset_high = (unsigned int)Syscall >> 16;
 CallGateDescriptor_ptr->offset_high = (unsigned int)Syscall & 0xffff;
 CallGateDescriptor_ptr->param_count = 0;
 CallGateDescriptor_ptr->reserved = 0;
 CallGateDescriptor_ptr->s = 0;
 CallGateDescriptor_ptr->selector = 8;
 CallGateDescriptor_ptr->Type = 0xc;
 }
 
 | 
注意注意:
从三环进入零环,或者从零环进入三环,fs都得用户自己手动来切换,如果不切换,可能就会崩溃
另外Windbg只要产生断点异常,都会将fs改为30,方便拿参数,所以如果有时候会崩溃,要注意看fs有没有改回三环的0x3b
/1724153322949.png)
应该是call 48h:0,但是段选择子的后2位指定0环还是三环,这里要填三环,也就是11(3),所以应该是call 4bh:0
/1724153481527.png)
但是编译器不让过,所以我们选择用机器码实现
比如用x32dbg去做补丁
/1724154158299.png)
如果提供了参数,如何给0环传参呢?
这是三环代码:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 
 | #include <iostream>using namespace std;
 int main()
 {
 cout << "syscall begin" << endl;
 __asm
 {
 push 0x11223344
 _emit 0x9a
 _emit 0x00
 _emit 0x00
 _emit 0x00
 _emit 0x00
 _emit 0x4b
 _emit 0x00
 }
 
 cout << "syscall end" << endl;
 system("pause");
 return 0;
 }
 
 | 
在0环是可以找到对应参数的
/1724163410254.png)
但是我们知道GDT表是有限的,那么多API,难道每一个API都需要在GDT表注册一个调用门吗?
实际上可以是这样的:
| 12
 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
 
 | SYSCALL G_Sysservice[]={
 &Syscall1,
 &Syscall2,
 &Syscall3
 }
 
 UCHAR G_SysParams[]
 {
 0,
 4,
 5
 }
 
 __declspec(naked) void Syscall()
 {
 __asm{
 pushad
 push fs
 mov ax,30h
 mov fs,ax
 
 
 movzx ecx,byte ptr[G_SysParams+eax*1]
 mov ebx,ecx
 shr ecx,2
 sub esp,ecx
 mov esi,edx
 mov edi,esp
 rep movsd
 
 
 call dword ptr[G_Sysservice+eax*4];
 
 add esp,ebx
 pop fs
 popad
 retf
 }
 }
 
 |