Windwos内核
通过IDT Hook键盘
IRETD
(Interrupt Return Double-word)是 x86 架构中的一条汇编指令,用于从中断或异常处理程序中返回到调用程序。
- 当一个中断或异常发生时,CPU 会将当前的 EFLAGS 寄存器、CS(代码段)寄存器和 EIP(指令指针)压入堆栈,并跳转到中断处理程序的入口地址。
- 当中断处理程序完成时,
IRETD
指令会从堆栈中弹出这些值并恢复它们,返回到中断发生前的状态。
当你 Hook 一个中断函数时,通常会使用“裸函数”(Naked Function),这是因为中断处理程序具有特殊的要求,不允许编译器在函数的入口和退出时自动插入额外的代码。这些额外的代码可能包括保存和恢复寄存器、设置堆栈帧等常规操作。为了确保中断处理程序的行为符合预期,裸函数就显得非常重要。
1 2 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的代码:
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
| 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 端口。
1 2 3 4 5
| __asm { in al,60h #从端口 0x60 读取一个字节的数据,并存储在寄存器 al 中。 mov Scancode,al }
|
当然这会出现一个问题,扫描码只能被拿一次,如果我们再jmp到原来的keyproc,那么原来的KeyProc将会拿不到扫描码,所以我们要人工塞入扫描码
1 2 3 4
| mov al,0d2h out 64h,al#当你向端口 0x64 发送 0xD2 时,控制器就知道你要传递一个字节的数据给连接的 PS/2 设备。 mov al,ScanCode out 60,al
|
但是IDT Hook并不流行,因为想写兼容性好比较难。。。
还有一种方法,是去从原来的KeyProc拿,后续会说
调用门
首先调用门不会引起进程切换
IDT 是专门用于处理中断和异常的,适用于中断门和陷阱门,而调用门设计用于在不同特权级别间执行函数调用,存储在 GDT 或 LDT 中。
调用门用于在不同特权级之间实现受控的程序控制转移 本质上,它只是一个描述符,一个不同于代码段和数据段的描述符,可以安装在GDT或者LGT中,但是不能安装在IDT(中断描述符表)中。
调用门描述符占用 8 字节(64 位)
字段说明:
- 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 位):
1 2 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应该是相对固定的
调用流程:
左边是三环的,右边是零环的,retf就回去三环了
模拟系统API的实现:
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
| __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
应该是call 48h:0,但是段选择子的后2位指定0环还是三环,这里要填三环,也就是11(3),所以应该是call 4bh:0
但是编译器不让过,所以我们选择用机器码实现
比如用x32dbg去做补丁
如果提供了参数,如何给0环传参呢?
这是三环代码:
1 2 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环是可以找到对应参数的
但是我们知道GDT表是有限的,那么多API,难道每一个API都需要在GDT表注册一个调用门吗?
实际上可以是这样的:
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
| 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 } }
|