内容简介:在 WIN64 上 HOOK SSDT 和 UNHOOK SSDT 在原理上跟 WIN32 没什么不同, 甚至说 HOOK 和 UNHOOK 在本质上也没有不同, 都是在指定的地址上填写一串数字而已(填写代理函数的地址时叫做 HOOK,填写原始函数的地址时叫做 UNHOOK)。 不过实现起来还是很大不同的。 废话不多说,开始分点讲解 HOOK 和 UNHOOK。要挂钩 SSDT,必然要先得到 ServiceTableBase 的地址。和 SSDT 相关的两个结构体 SYSTEM_SERVICE_TABLE
在 WIN64 上 HOOK SSDT 和 UNHOOK SSDT 在原理上跟 WIN32 没什么不同, 甚至说 HOOK 和 UNHOOK 在本质上也没有不同, 都是在指定的地址上填写一串数字而已(填写代理函数的地址时叫做 HOOK,填写原始函数的地址时叫做 UNHOOK)。 不过实现起来还是很大不同的。 废话不多说,开始分点讲解 HOOK 和 UNHOOK。
一、 HOOK SSDT
要挂钩 SSDT,必然要先得到 ServiceTableBase 的地址。和 SSDT 相关的两个结构体 SYSTEM_SERVICE_TABLE 以及 SERVICE_DESCRIPTOR_TABLE 并没有发生什么的变化(除了整个结构体的长度胖了一倍):
typedef struct _SYSTEM_SERVICE_TABLE{ PVOID ServiceTableBase; PVOID ServiceCounterTableBase; SIZE_T NumberOfServices; PVOID ParamTableBase; } SYSTEM_SERVICE_TABLE, *PSYSTEM_SERVICE_TABLE; typedef struct _SERVICE_DESCRIPTOR_TABLE{ SYSTEM_SERVICE_TABLE ntoskrnl; // ntoskrnl.exe (native api) SYSTEM_SERVICE_TABLE win32k; // win32k.sys (gdi/user) SYSTEM_SERVICE_TABLE Table3; // not used SYSTEM_SERVICE_TABLE Table4; // not used }SERVICE_DESCRIPTOR_TABLE,*PSERVICE_DESCRIPTOR_TABLE;
得到 ServiceTableBase 的地址后,就能得到每个服务函数的地址了。但和WIN32 不一样,这个表存放的并不是 SSDT 函数的完整地址,而是其相对于ServiceTableBase[Index]>>4 的数据(我称它为偏移地址),每个数据占四个字节,所以计算指定 Index 函数完整地址的公式是: ServiceTableBase[Index]>>4+ ServiceTableBase。代码如下:
ULONGLONG GetSSDTFuncCurAddr(ULONG id) { LONG dwtmp=0; PULONG ServiceTableBase=NULL; ServiceTableBase=(PULONG)KeServiceDescriptorTable->ServiceTableBase; dwtmp=ServiceTableBase[id]; dwtmp=dwtmp>>4; return dwtmp + (ULONGLONG)ServiceTableBase; }
反之,从函数的完整地址获得函数偏移地址的代码也就出来了:
ULONG GetOffsetAddress(ULONGLONG FuncAddr) { LONG dwtmp=0; PULONG ServiceTableBase=NULL; ServiceTableBase=(PULONG)KeServiceDescriptorTable->ServiceTableBase; dwtmp=(LONG)(FuncAddr-(ULONGLONG)ServiceTableBase); return dwtmp<<4; }
知道了这一套机制, HOOK SSDT 就很简单了,首先获得待 HOOK 函数的序号Index,然后通过公式把自己的代理函数的地址转化为偏移地址,然后把偏移地址的数据填入 ServiceTableBase[Index]。也许有些读者看到这里,已经觉得胜利在望了,我当时也是如此。但实际上我在这里栽了个大跟头,整整郁闷了很长时间!因为我低估了设计这套算法的工程师的智商,我没有考虑一个问题,为什么 WIN64 的 SSDT 表存放地址的形式这么奇怪?只存放偏移地址,而不存放完整地址?难道是为了节省内存?这肯定是不可能的,要知道现在内存白菜价。那么不是为了节省内存,唯一的可能性就是要给试图挂钩 SSDT 的人制造麻烦!要知道, WIN64 内核里每个驱动都不在同一个 4GB 里,而 4 字节的整数只能表示 4GB的范围!所以无论你怎么修改这个值,都跳不出 ntoskrnl 的手掌心。如果你想通过修改这个值来跳转到你的代理函数,那是绝对不可能的。 因为你的驱动的地址不可能跟 ntoskrnl 在同一个 4GB 里。 然而,这位工程师也低估了我们中国人的智商,在中国有两句成语,这位工程师一定没听过,叫“明修栈道,暗渡陈仓”以及“上有政策,下有对策”。虽然不能直接用 4 字节来表示自己的代理函数所在的地址,但是还是可以修改这个值的。要知道, ntoskrnl 虽然有很多地方的代码通常是不会被执行的,比如 KeBugCheckEx。所以我的办法是: 修改这个偏移地址的值,使之跳转到 KeBugCheckEx,然后在 KeBugCheckEx 的头部写一个 12 字节的 mov – jmp,这是一个可以跨越 4GB 的跳转,跳到我们的函数里!代码如下:
VOID FuckKeBugCheckEx() { KIRQL irql; ULONGLONG myfun; UCHAR jmp_code[]="\x48\xB8\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\xFF\xE0"; myfun=(ULONGLONG)Fake_NtTerminateProcess; memcpy(jmp_code+2,&myfun,8); irql=WPOFFx64(); memset(KeBugCheckEx,0x90,15); memcpy(KeBugCheckEx,jmp_code,12); WPONx64(irql); } VOID HookSSDT() { KIRQL irql; ULONGLONG dwtmp=0; PULONG ServiceTableBase=NULL; //get old address NtTerminateProcess=(NTTERMINATEPROCESS)GetSSDTFuncCurAddr(41); dprintf("Old_NtTerminateProcess: %llx",(ULONGLONG)NtTerminateProcess); //set kebugcheckex FuckKeBugCheckEx(); //show new address ServiceTableBase=(PULONG)KeServiceDescriptorTable->ServiceTableBase; OldTpVal=ServiceTableBase[41]; //record old offset value irql=WPOFFx64(); ServiceTableBase[41]=GetOffsetAddress((ULONGLONG)KeBugCheckEx); WPONx64(irql); dprintf("KeBugCheckEx: %llx",(ULONGLONG)KeBugCheckEx); dprintf("New_NtTerminateProcess: %llx",GetSSDTFuncCurAddr(41)); }
在代理函数里这么写,保护名为 calc.exe和 loaddrv.exe的程序不被结束:
NTSTATUS __fastcall Fake_NtTerminateProcess(IN HANDLE ProcessHandle, IN NTSTATUSExitStatus) { PEPROCESS Process; NTSTATUS st = ObReferenceObjectByHandle (ProcessHandle, 0, *PsProcessType, KernelMode,&Process, NULL); DbgPrint("Fake_NtTerminateProcess called!"); if(NT_SUCCESS(st)) { if(!_stricmp(PsGetProcessImageFileName(Process),"loaddrv.exe")||!_stricmp(PsGetProcessImageFileName(Process),"calc.exe")) return STATUS_ACCESS_DENIED; else return NtTerminateProcess(ProcessHandle,ExitStatus); } else return STATUS_ACCESS_DENIED; }
注意在代理函数一定要注明是__fastcall,否则会出问题。测试效果如下:
给大家看一下 WINDBG 里的反汇编结果(挂钩前和挂钩后):
用 WIN64AST 查看的效果如下(挂钩前和挂钩后):
接下来给出取消 SSDT HOOK 的代码,在这个代码里我没有复原 KeBugCheckEx的原始内容,因为执行到 KeBugCheckEx 就意味着蓝屏,所以是否恢复KeBugCheckEx 的原始机器码都无所谓了:
VOID UnhookSSDT() { KIRQL irql; PULONG ServiceTableBase=NULL; ServiceTableBase=(PULONG)KeServiceDescriptorTable->ServiceTableBase; //set value irql=WPOFFx64(); ServiceTableBase[41]=GetOffsetAddress((ULONGLONG)NtTerminateProcess); WPONx64(irql); //没必要恢复 KeBugCheckEx 的内容了,反正执行到 KeBugCheckEx 时已经完蛋了。 dprintf("NtTerminateProcess: %llx",GetSSDTFuncCurAddr(41)); }
网上 SSDT HOOK 的代码,动辄几百行的代码,感觉简直是在吓唬人。现在,我用一行代码凸显出 SSDT HOOK 的本质:
WIN32 内核:
KeServiceDescriptorTable->ServiceTableBase[Index] = 代理函数绝对地址
WIN64 内核:
KeServiceDescriptorTable->ServiceTableBase[Index] = 代理函数偏移地址
也就是说,在 WIN32 下只需要四行代码即可实现 SSDT HOOK,分别是:关闭内存写保护、保存旧地址、设置新地址、打卡内存写保护。而 WIN64 系统显得复杂些,还需要计算偏移地址、找出一块在位于 NTOSKRNL 空间里的废弃内存,并在这块废弃内存里进行二次跳转才能转到自己的处理函数。
二、 UNHOOK SSDT
要恢复 SSDT,首先要获得 SSDT 各个函数的原始地址,而 SSDT 各个函数的原始地址,自然是存储在内核文件里的。于是,有了以下思路:
1.获得内核里 KiServiceTable 的地址(变量名称: KiServiceTable)
2.获得内核文件在内核里的加载地址(变量名称: NtosBase)
3.获得内核文件在 PE32+结构体里的映像基址(变量名称: NtosImageBase)
4.在自身进程里加载内核文件并取得映射地址(变量名称: NtosInProcess)
5.计算出 KiServiceTable 和 NtosBase 之间的“距离”(变量名称: RVA)
6.获得指定 INDEX 函数的地址(计算公式: *(PULONGLONG)(NtosInProcess + RVA+ 8 * index) – NtosImageBase + NtosBase)
思路和 WIN32 下获得 SSDT 函数原始地址差异不大,接下来解释一下第六步的计算公式是怎么得来的。首先看一张 IDA 的截图:
可见,从文件中的 KiServiceTable 地址开始,每 8 个字节,存储一个函数的“理想地址”(之所以说是理想地址,是因为这个地址是基于『内核文件的映像基址 NtosImageBase』的,而不是基于『内核文件的加载基址 NtosBase』的)。因此,得到 8 * index。由于已经获得了 KiServiceTable 和 NtosBase 之间的“距离”(RVA = KiServiceTable – NtosBase),也已知内核文件在自身进程里的映射地址(NtosInProcess),所以就能算出文件中的 KiServiceTable 的地址(NtosInProcess + RVA)。所以, 存储各个函数原始地址的文件地址就是:NtosInProcess + RVA + 8 * index。把这个地址的值取出来(长度为 8),就是: *(PULONGLONG)(NtosInProcess + RVA + 8 * index)。前面说了,由于得到的这个函数地址是理想地址,因为它假设的加载基址是 PE32+结构体里的成员ImageBase(映像基址)的值。而实际上,内核文件的加载基址肯定不可能是这个值,所以还要减去内核文件的映像基址(NtosImageBase)再加上内核文件的实际加载基址(NtosBase)。接下来,给出每一步的具体实现过程的代码。
1.获得 KiServiceTable 的地址其实就是获得 KeServiceDescriptorTable->ServiceTableBase 的地址而已,具体知识之前已经讲过,这里就不赘述了, 直接给出代码:
ULONGLONG GetKeServiceDescriptorTable64() { char KiSystemServiceStart_pattern[13] = "\x8B\xF8\xC1\xEF\x07\x83\xE7\x20\x25\xFF\x0F\x00\x00"; ULONGLONG CodeScanStart = (ULONGLONG)&_strnicmp; ULONGLONG CodeScanEnd = (ULONGLONG)&KdDebuggerNotPresent; UNICODE_STRING Symbol; ULONGLONG i, tbl_address, b; for (i = 0; i < CodeScanEnd - CodeScanStart; i++) { if (!memcmp((char*)(ULONGLONG)CodeScanStart +i, (char*)KiSystemServiceStart_pattern,13)) { for (b = 0; b < 50; b++) { tbl_address = ((ULONGLONG)CodeScanStart+i+b); if (*(USHORT*) ((ULONGLONG)tbl_address ) == (USHORT)0x8d4c) return ((LONGLONG)tbl_address +7) + *(LONG*)(tbl_address +3); } } } return 0; } ULONG64 ssdt_base_aadress=GetKeServiceDescriptorTable64(); KiServiceTable=*(PULONGLONG)ssdt_base_aadress;
2.获得内核文件在内核里的加载地址
这个本质上属于枚举内核模块,使用 ZwQuerySystemInformation 的SystemModuleInformation 功能号实现。 由于第一个加载的总是内核文件,所以直接获得 0 号模块的基址即可。另外,还要获得内核文件的名称,因为根据 CPU核心数目等硬件条件的不同,内核文件的名称也是不尽相同的。
ULONGLONG GetNtosBaseAndPath(char *ModuleName) { ULONG NeedSize, i, ModuleCount, BufferSize = 0x5000; PVOID pBuffer = NULL; ULONGLONG qwBase = 0; NTSTATUS Result; PSYSTEM_MODULE_INFORMATION pSystemModuleInformation; do { pBuffer = malloc( BufferSize ); if( pBuffer == NULL ) { return FALSE; } Result = ZwQuerySystemInformation( SystemModuleInformation, pBuffer, BufferSize, &NeedSize ); if( Result == STATUS_INFO_LENGTH_MISMATCH ) { free( pBuffer ); BufferSize *= 2; } else if( !NT_SUCCESS(Result) ) { free( pBuffer ); return FALSE; } } while( Result == STATUS_INFO_LENGTH_MISMATCH ); pSystemModuleInformation = (PSYSTEM_MODULE_INFORMATION)pBuffer; if(ModuleName!=NULL) strcpy(ModuleName,pSystemModuleInformation->Module[0].ImageName+pSystemModuleInformat ion->Module[0].ModuleNameOffset); qwBase=(ULONGLONG)pSystemModuleInformation->Module[0].Base; free(pBuffer); return qwBase; }
3.获得内核文件的映像基址这个直接解析 PE32+文件的结构即可。
DWORD FileLen(char *filename) { WIN32_FIND_DATAA fileInfo={0}; DWORD fileSize=0; HANDLE hFind; hFind = FindFirstFileA(filename ,&fileInfo); if(hFind != INVALID_HANDLE_VALUE) { fileSize = fileInfo.nFileSizeLow; FindClose(hFind); } return fileSize; } CHAR *LoadDllContext(char *filename) { DWORD dwReadWrite, LenOfFile=FileLen(filename); HANDLE hFile = CreateFileA(filename, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0); if (hFile != INVALID_HANDLE_VALUE) { PCHAR buffer=(PCHAR)malloc(LenOfFile); SetFilePointer(hFile, 0, 0, FILE_BEGIN); ReadFile(hFile, buffer, LenOfFile, &dwReadWrite, 0); CloseHandle(hFile); return buffer; } return NULL; } VOID GetNtosImageBase() { PIMAGE_NT_HEADERS64 pinths64; PIMAGE_DOS_HEADER pdih; char *NtosFileData=NULL; NtosFileData=LoadDllContext(NtosName); pdih=(PIMAGE_DOS_HEADER)NtosFileData; pinths64=(PIMAGE_NT_HEADERS64)(NtosFileData+pdih->e_lfanew); NtosImageBase=pinths64->OptionalHeader.ImageBase; printf("ImageBase: %llx\n",NtosImageBase); }
4/5/6.获得 SSDT 函数的原始地址原理已经在前面解释过,这里直接给出代码。
ULONGLONG GetFunctionOriginalAddress(DWORD index) { if ( NtosInProcess==0 ) NtosInProcess = (ULONGLONG)LoadLibraryExA(NtosName,0,DONT_RESOLVE_DLL_REFERENCES); ULONGLONG RVA=KiServiceTable-NtosBase; ULONGLONG temp=*(PULONGLONG)(NtosInProcess+RVA+8*(ULONGLONG)index); ULONGLONG RVA_index=temp-NtosImageBase; return RVA_index+NtosBase; }
接下来测试一下效果,在测试前,运行 SSDT HOOK NtTerminateProcess 的DEMO(检测出了 SSDT 的异常项)。
检测出了异常的项目就需要恢复。其实恢复 SSDT 本质上和挂钩 SSDT 本质上没有不同,都是在 KiServiceTable 的指定偏移处写入一个 INT32 值。代码如下:
LONG GetOffsetAddress(ULONGLONG FuncAddr) { LONG dwtmp=0; PULONG ServiceTableBase=NULL; if(KeServiceDescriptorTable==NULL) KeServiceDescriptorTable=(PSYSTEM_SERVICE_TABLE)GetKeServiceDescriptorTable64(); ServiceTableBase=(PULONG)KeServiceDescriptorTable->ServiceTableBase; dwtmp=(LONG)(FuncAddr-(ULONGLONG)ServiceTableBase); return dwtmp<<4; } VOID UnHookSSDT(ULONG id, ULONGLONG FuncAddr) //传入正确的地址 { KIRQL irql; LONG dwtmp; PULONG ServiceTableBase=NULL; dwtmp=GetOffsetAddress(FuncAddr); ServiceTableBase=(PULONG)KeServiceDescriptorTable->ServiceTableBase; irql=WPOFFx64(); ServiceTableBase[id]=dwtmp; //核心就这一句 WPONx64(irql); }
接下来测试效果(输入要恢复的函数的 Index):
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。