以win7 x86为例(x64就不是我等菜鸟玩的了)
预备资料:
本地系统服务的地址在内核结构中称为系统服务调度表(System Service Dispatch Table, SSDT)中列出。该表可以基于系统调用编号进行索引,以便定位函数的内存地址。还有一个系统服务参数表(System Service Parameter Table,SSPT)指定了每个系统服务的函数参数的字节数(每个函数对应一个字节)。
KeServiceDescriptorTable是由内核导出的表。给出如下结构
1 typedef struct _SystemServiceDescriptorTable 2 { 3 PVOID ServiceTableBase; 4 PULONG ServiceCounterTableBase; 5 ULONG NumberOfService; 6 ULONG ParamTableBase; 7 }SystemServiceTable,*PSystemServiceTable;
typedef struct _SERVICE_DESCRIPTOR_TABLE { SYSTEM_SERVICE_TABLE ntoskrnel; //ntoskrnl.exe的服务函数 SYSTEM_SERVICE_TABLE win32k; //win32k.sys的服务函数,(gdi.dll/user.dll的内核支持) SYSTEM_SERVICE_TABLE NotUsed1; SYSTEM_SERVICE_TABLE NotUsed2; }SYSTEM_DESCRIPTOR_TABLE,*PSYSTEM_DESCRIPTOR_TABLE;
参数说明:
ServiceTableBase :SSDT的基地址。
ServiceCounterTable 此域用于操作系统的 checked builds,包含着 SSDT 中每个服务被调用次数的计数器。这个计数器由 INT 2Eh 处理程序 (KiSystemService)更新。
NumberOfServices 由 ServiceTableBase 描述的服务的数目。
ParamTableBase 包含每个系统服务参数字节数表的基地址。
引用pediy的大牛堕落天才的话来说:
内核中有两个系统服务描述符表,一个是KeServiceDescriptorTable(由ntoskrnl.exe导出),一个是KeServieDescriptorTableShadow(没有导出)。两者的区别是,KeServiceDescriptorTable仅有ntoskrnel一项,KeServieDescriptorTableShadow包含了ntoskrnel以及win32k。一般的Native API的服务地址由KeServiceDescriptorTable分派,gdi.dll/user.dll的内核API调用服务地址由KeServieDescriptorTableShadow分派。还有要清楚一点的是win32k.sys只有在GUI线程中才加载,一般情况下是不加载的,所以要Hook KeServieDescriptorTableShadow的话,一般是用一个GUI程序通过IoControlCode来触发。
需要注意的是,一般情况下R0的驱动内存中是没有win32k.sys的,只有在GUI线程加载的时候才会存在,准确的说只有在PsConvertToGuiThread调用的时候把KeServiceDescriptorTable 切换为KeServieDescriptorTableShadow的时候才会在内存中存在win32k.sys。注意:并不是一定要有GUI存在。
准备资料:
Ring3层上的一个程序调用了API OpenProcess函数,那么这个函数具体是怎么跑的呢?
在应用层上:OpenProcess函数定位于Kernel32.dll中 运行OpenProcess函数后该函数会调用位于ntdll.dll中的NtOpenProcess函数
NtOpenProcess这个函数可以用Pe分析工具在ntdll.dll的导出表中找到。在调用前,ntdll.dll会完成函数参数的检查
Ps::应用层上的Zw系列函数和Nt系列函数是一样的
从应用层过渡到内核:调用一个中断 并且要将所要调用的服务号(SSDT数组中的索引值)存放到寄存器eax中
并且将参数地址放到指定的寄存器edx中,再将参数复制到内核地址空间中
最后根据eax中的索引值来调用SSDT数组中的指定服务
在内核层上:系统服务分发函数KiSystemService根据eax寄存器中的索引值找到指定的SSDT项,最后根据该SSDT项中所存放的
系统服务的地址来调用这个系统服务。
那么,KeServiceDescriptorTable到底是怎么样的呢?
windbg中输入指令
kd> dd KeServiceDescriptorTable
83f759c0 83e7c6f0 00000000 00000191 83e7cd38
83f759d0 00000000 00000000 00000000 00000000
83f759e0 83ee8493 00000000 00000000 00000bb8
83f759f0 00000011 00000100 5385d2ba d717548f
83f75a00 83e7c6f0 00000000 00000191 83e7cd38
83f75a10 00000000 00000000 00000000 00000000
83f75a20 00000000 00000000 83f75a24 00000240
83f75a30 00000240 00000000 00000003 00000000
可知道,服务函数个数是0x191
83e7c6f0就是KeServiceDescriptorTable.ntoskrnel.ServiceTableBase
跟进83e7c6f0
kd> dd 83e7c6f0
83e7c6f0 8406d0cb 83ec622b 84018e4e 83e316e1
83e7c700 8408de6e 83f0a48a 840f6b6d 840f6bb6
83e7c710 840042d7 84110426 8411167b 840126f7
83e7c720 8401a875 840e9979 84097718 8401b19c
83e7c730 83faec97 840dbb1c 84005c7d 84058e0f
83e7c740 8408bcf6 83fe7d73 84060821 83ffb3be
83e7c750 84017a35 83ffa093 84017773 8402044b
83e7c760 84025123 840d6ad3 84080906 8401fb6f
这些就是系统服务函数的地址了。
kd> dd 8406d0cb
8406d0cb 8b55ff8b 24a164ec 66000001 008488ff
8406d0db 57560000 75ff016a fff6331c 75ff1875
8406d0eb 75ff5614 0c75ff10 56565656 e80875ff
8406d0fb ffff9289 240d8b64 8b000001 84818df8
8406d10b 66000000 b70f00ff c63b6600 418d1575
8406d11b 74003940 b139660e 00000086 bce80575
8406d12b 81ffdc97 000703ff bf0575c0 c000000b
8406d13b 5e5fc78b 0018c25d 90909090 68146a90
试着跟进8406d0cb
kd> u 8406d0cb
nt!NtAcceptConnectPort:
8406d0cb 8bff mov edi,edi
8406d0cd 55 push ebp
8406d0ce 8bec mov ebp,esp
8406d0d0 64a124010000 mov eax,dword ptr fs:[00000124h]
8406d0d6 66ff8884000000 dec word ptr [eax+84h]
8406d0dd 56 push esi
8406d0de 57 push edi
8406d0df 6a01 push 1
看,8406d0cb就是NtAcceptConnectPort系统服务函数的地址
那么,给出公式某个系统服务函数的地址 = KeServiceDescriptorTable.ntoskrnel.ServiceTableBase + eax * 4 指向的值
不信的朋友们可以动手试一下,会发现,实际上,SSDT就是一个用来保存Windows服务地址的数组。
正文:
以SSDT HOOK ZwCreateUserProcess为例 这里引用Windows内核安全编程从入门到实践的示例代码
DriverEntry中的几行代码来完成SSDT HOOK
if (!NT_SUCCESS(Init_HCode())) { return STATUS_FAILED_DRIVER_ENTRY; } RealAddr.NtCreateUserProcess = Hook_SSDT(NtCreateUserProcessID, myNtCreateUserProcess);
给出相关函数源码
NTSTATUS Init_HCode() { UNICODE_STRING uDllName;
KdPrint(("Find Code...\n")); RtlInitUnicodeString(&uDllName, L"\\Device\\HarddiskVolume1\\Windows\\System32\\ntdll.dll"); NtCreateUserProcessID = GetSysServiceId(&uDllName, "ZwCreateUserProcess"); if (!NtCreateUserProcessID) { return STATUS_UNSUCCESSFUL; } return STATUS_SUCCESS; }
ULONG GetSysServiceId(IN PUNICODE_STRING uDllName, IN char* cSearchFnName) { ULONG uFnAddr = 0; ULONG uID = 0; int i = 0; NTSTATUS ntstatus; HANDLE hFile; OBJECT_ATTRIBUTES oattr; IO_STATUS_BLOCK iosb; HANDLE hSection; PVOID pBaseAddr = NULL; SIZE_T viewSize = 0; ULONG uModBase = 0; IMAGE_DOS_HEADER *doshdr; IMAGE_OPTIONAL_HEADER *opthdr; IMAGE_EXPORT_DIRECTORY *pExportTable; ULONG *dwAddrFns, *dwAddrNames; USHORT *dwAddrNameOrdinals; char *cFunName; ULONG dwFnOrdinal; InitializeObjectAttributes(&oattr, uDllName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL); ntstatus = ZwOpenFile(&hFile, GENERIC_READ, &oattr, &iosb, FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_SYNCHRONOUS_IO_ALERT); if (!NT_SUCCESS(ntstatus)) { //KdPrint(("[GetSysServiceId] ZwOpenFile Failure!\n")); return 0; } ntstatus = ZwCreateSection(&hSection, SECTION_MAP_READ | SECTION_MAP_WRITE, NULL, 0, PAGE_READWRITE, 0x1000000, hFile); if (!NT_SUCCESS(ntstatus)) { //KdPrint(("[GetSysServiceId] ZwSectionFailure!\n")); ZwClose(hFile); return 0; } ntstatus = ZwMapViewOfSection(hSection, NtCurrentProcess(), &pBaseAddr, 0, 1024, 0, &viewSize, ViewShare, MEM_TOP_DOWN, PAGE_READWRITE); if (!NT_SUCCESS(ntstatus)) { //KdPrint(("[GetSysServiceId ZwMapViewOfSectionFailure!\n")); ZwClose(hFile); ZwClose(hSection); return 0; } uModBase = (ULONG)pBaseAddr; do { doshdr = (IMAGE_DOS_HEADER *)uModBase; if (NULL == doshdr) { break; } opthdr = (IMAGE_OPTIONAL_HEADER *)(uModBase + doshdr->e_lfanew + 24); if (NULL == opthdr) { break; } pExportTable = (IMAGE_EXPORT_DIRECTORY *)(uModBase + opthdr->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); if (NULL == pExportTable) { break; } dwAddrFns = (ULONG *)(uModBase + pExportTable->AddressOfFunctions); dwAddrNames = (ULONG *)(uModBase + pExportTable->AddressOfNames); dwAddrNameOrdinals = (USHORT *)(uModBase + pExportTable->AddressOfNameOrdinals); for (i = 0; i < pExportTable->NumberOfNames; ++i) { cFunName = (char *)(uModBase + dwAddrNames[i]); if (!_strnicmp(cSearchFnName, cFunName, strlen(cSearchFnName))) { dwFnOrdinal = pExportTable->Base + dwAddrNameOrdinals[i] - 1; uFnAddr = uModBase + dwAddrFns[dwFnOrdinal]; uID = *(ULONG*)((ULONG)uFnAddr + 1);//取得ID break; } } } while (FALSE); ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddr); ZwClose(hFile); ZwClose(hSection); return uID; }
ULONG Hook_SSDT(ULONG fnID, PVOID fnName) { ULONG Address; ULONG RealAddress; unsigned int NumberOfServices; Address = (ULONG)KeServiceDescriptorTable->ServiceTableBase + fnID * 4; RealAddress = *(ULONG*)Address; __asm //解除内存保护 { cli push eax mov eax, cr0 and eax, not 10000h mov cr0, eax pop eax } InterlockedExchange((LONG*)Address, (LONG)fnName);//注:原子操作 __asm { push eax mov eax, cr0 or eax, 10000h mov cr0, eax pop eax sti } return RealAddress; }
最后给个UnHook
VOID Unhook(ULONG fnID,ULONG RealAddr) { ULONG Address; KIRQL oldIrql; Address = (ULONG)KeServiceDescriptorTable->ServiceTableBase + fnID * 4; __asm { cli mov eax, cr0 and eax, not 10000h mov cr0, eax } oldIrql = KeRaiseIrqlToDpcLevel(); InterlockedExchange((LONG *)Address,RealAddr); KeLowerIrql(oldIrql); __asm { mov eax, cr0 or eax, 10000h mov cr0, eax sti } }
至此,SSDT HOOK的简单介绍就到这里了。