有前面对SSDT的了解之后,如果还有不是很了解的同学,请参考SSDT详解及Win32 API到系统服务描述符表调用的完整过程。已经讲的很详细了。
一、SSDT HOOK原理
其实 SSDT Hook 的原理是很简单,从前篇的分析,我们可以知道在 SSDT 这个数组中呢,保存了系统服务的地址,比如对于 Ring0 下的 NtQuerySystemInformation 这个系统服务的地址,就保存在 KeServiceDescriptorTable[105h] 中,既然是 Hook 的话,我们就可以将这个 KeServiceDescriptorTable[105h] 下保存的服务地址替换掉,将我们自己的 Hook 处理函数的地址来替换掉原来的地址,这样当每次调用 KeServiceDescriptorTable[105h]时就会调用我们自己的这个 Hook 处理函数(MyHookNtQuerySystemInformation )了。
这样的话,每次系统调用 NtQuerySystemInformation 这个系统服务时,实质上调用的就是 MyHookNtQuerySystemInformation 了,而我们为了保证系统的稳定性(至少不让其崩溃),一般会在 MyHookNtQuerySystemInformation 中调用系统中原来的服务,也就是NtQuerySystemInformation。
这里借用网上一张图让大家有更加清晰的认识:
其实Hook的原理差不多都是这样的,替换当前函数地址,修改成自定义的函数地址,然后再调用回原来函数的地址。
二、常用的应用层获取进程的方法
①使用ToolHelp遍历获取到所有进程。
hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
②使用 PSAPI 下的 EnumProcesses 获取到所有进程的 PID,然后提升进程权限为 SE_DEBUG 权限,
再调用 OpenProcess 即可打开进程,从而获取到进程的基本信息。
③使用Ntdll.dll 中的未文档化的 API – NtQuerySystemInformation,而Windows 任务管理器就是通过这种方式来获取到所有的进程信息的。
所以隐藏进程最简单的办法就是Hook 掉 NtQuerySystemInformation 函数 。
至于进程保护的话,第一种则是该进程自行终止,第二种情况则是该进程被其他进程给杀掉,第一种情况基本上对于窗口应用程序来说,一般都是用户点击了右上角的 x 按钮,然后产生 WM_CLOSE 消息,最后由窗口过程退出进程,这种情况下,我们应该是需要允许退出的,也就是进程是可以正常退出的。而第二种情况的话,就是进程被别的进程杀掉,比如在任务管理器中就可以杀掉绝大部分的应用程序进程,而这里的进程保护就是要实现进程不能够被任务管理器或者其他的进程管理工具杀掉。在 Ring3 中,由一个进程结束其他进程,先会调用API OpenProcess这个我已经在前篇说的很详细了,然后再去调用Kernel32.dll 中的 TerminateProcess,如果追溯这个 TerminateProcess,可以发现,其调用了 Ntdll.dll 中的 NtTerminateProcess API,然后再追溯下去就可以到 ntoskrnl.exe 中的 ZwTerminateProcess 和系统服务NtTerminateProcess 了。所以这里我们选择Hook掉NtTerminateProcess 函数。
三、代码说明
口说还不如来点实际的代码实际。
首先我们来定义SSDT需要用到的结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | typedef struct _KSYSTEM_SERVICE_TABLE { PULONG ServiceTableBase; // SSDT (System Service Dispatch Table)的基地址 PULONG ServiceCounterTableBase; // 用于 checked builds, 包含 SSDT 中每个服务被调用的次数 ULONG NumberOfService; // 服务函数的个数, NumberOfService * 4 就是整个地址表的大小 ULONG ParamTableBase; // SSPT(System Service Parameter Table)的基地址 } KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE; typedef struct _KSERVICE_TABLE_DESCRIPTOR { KSYSTEM_SERVICE_TABLE ntoskrnl; // ntoskrnl.exe 的服务函数 KSYSTEM_SERVICE_TABLE win32k; // win32k.sys 的服务函数(GDI32.dll/User32.dll 的内核支持) KSYSTEM_SERVICE_TABLE notUsed1; KSYSTEM_SERVICE_TABLE notUsed2; } KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR; //导出由 ntoskrnl.exe 所导出的 SSDT extern PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable; |
在 Hook 任意的系统服务之前,将 SSDT 中的所有系统服务的地址保存或者说是备份到 ULONG 数组中即可。而后在解除 Hook 时,我们就可以从这个 ULONG 数组中取出原有系统服务的地址。
1 2 3 4 5 6 | //定义 SSDT(系统服务描述表) 中服务个数的最大数目 //这里定义为 1024 个,实际上在win7 32是 0x0191个 #define MAX_SYSTEM_SERVICE_NUMBER 1024 //用来保存 SSDT 中所有的旧的服务函数的地址 ULONG oldSysServiceAddr[MAX_SYSTEM_SERVICE_NUMBER]; |
我们还需要备份、安装、解除的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | //=====================================================================================// //Name: VOID BackupSysServicesTable() // // // //Descripion: 用来备份 SSDT 中原有服务的地址,因为我们在解除 Hook 时需要还原 SSDT 中原有地址 // // // //=====================================================================================// VOID BackupSysServicesTable() { ULONG i; for(i = 0; (i < KeServiceDescriptorTable->ntoskrnl.NumberOfService) && (i < MAX_SYSTEM_SERVICE_NUMBER); i++) { oldSysServiceAddr[i] = KeServiceDescriptorTable->ntoskrnl.ServiceTableBase[i]; } } //=====================================================================================// //Name: NTSTATUS InstallSysServiceHook() // // // //Descripion: 实现 Hook 的安装,主要是在 SSDT 中用 newService 来替换掉 oldService // // // //=====================================================================================// NTSTATUS InstallSysServiceHook(ULONG oldService, ULONG newService) { ULONG uOldAttr = 0; EnableWriteProtect(&uOldAttr); SYSCALL_FUNCTION(oldService) = newService; DisableWriteProtect(uOldAttr); return STATUS_SUCCESS; } //=====================================================================================// //Name: NTSTATUS UnInstallSysServiceHook() // // // //Descripion: 实现 Hook 的解除,主要是在 SSDT 中用备份下的服务地址来替换掉 oldService // // // //=====================================================================================// NTSTATUS UnInstallSysServiceHook(ULONG oldService) { ULONG uOldAttr = 0; EnableWriteProtect(&uOldAttr); SYSCALL_FUNCTION(oldService) = oldSysServiceAddr[SYSCALL_INDEX(oldService)]; DisableWriteProtect(uOldAttr); return STATUS_SUCCESS; } |
然后,SSDT 在内存中是具有只读属性保护的,如果你想修改 SSDT 中的内容,你必须先要解除只读属性,也就是要赋予 SSDT 所在的这块内存具有可写属性才行。而这个属性保存在寄存器CR0的第17位,index为16.如果为1表示只读,置为0才可写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | //=====================================================================================// //Name: VOID DisableWriteProtect() // // // //Descripion: 用来去掉内存的可写属性,从而实现内存只读 // // // //=====================================================================================// VOID DisableWriteProtect(ULONG oldAttr) { _asm { mov eax, oldAttr mov cr0, eax sti; } } //=====================================================================================// //Name: VOID EnableWriteProtect() // // // //Descripion: 用来去掉内存的只读保护,从而实现可以写内存 // // // //=====================================================================================// VOID EnableWriteProtect(PULONG pOldAttr) { ULONG uAttr; _asm { cli; mov eax, cr0; mov uAttr, eax; and eax, 0FFFEFFFFh; // CR0 16 BIT = 0 mov cr0, eax; }; //保存原有的 CRO 属性 *pOldAttr = uAttr; } |
注意到上面有两个很重要的宏,即 SYSCALL_FUNCTION 和 SYSCALL_INDEX 宏。
1 2 3 4 5 6 | //根据 Zw_ServiceFunction 获取 Zw_ServiceFunction 在 SSDT 中所对应的服务的索引号 #define SYSCALL_INDEX(ServiceFunction) (*(PULONG)((PUCHAR)ServiceFunction + 1)) //根据 Zw_ServiceFunction 来获得服务在 SSDT 中的索引号, //然后再通过该索引号来获取 Nt_ServiceFunction的地址 #define SYSCALL_FUNCTION(ServiceFunction) KeServiceDescriptorTable->ntoskrnl.ServiceTableBase[SYSCALL_INDEX(ServiceFunction)] |
上面的事情做完后, 实现进程隐藏只需要Hook NtQuerySystemInformation函数,并实现我们自定义的函数即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | NTSTATUS HookNtQuerySystemInformation ( __in SYSTEM_INFORMATION_CLASS SystemInformationClass, __out_bcount_opt(SystemInformationLength) PVOID SystemInformation, __in ULONG SystemInformationLength, __out_opt PULONG ReturnLength ) { NTSTATUS rtStatus; pOldNtQuerySystemInformation = (NTQUERYSYSTEMINFORMATION)oldSysServiceAddr[SYSCALL_INDEX(ZwQuerySystemInformation)]; //执行原来的函数 rtStatus = pOldNtQuerySystemInformation(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength); if(NT_SUCCESS(rtStatus)) { //如果查询的是进程信息 if(SystemProcessInformation == SystemInformationClass) { PSYSTEM_PROCESS_INFORMATION pPrevProcessInfo = NULL; PSYSTEM_PROCESS_INFORMATION pCurrProcessInfo = (PSYSTEM_PROCESS_INFORMATION)SystemInformation; while(pCurrProcessInfo != NULL) { //获取当前遍历的 SYSTEM_PROCESS_INFORMATION 节点的进程名称和进程 ID ULONG uPID = (ULONG)pCurrProcessInfo->UniqueProcessId; UNICODE_STRING strTmpProcessName = pCurrProcessInfo->ImageName; //判断当前遍历的这个进程是否为需要隐藏的进程 if(ValidateProcessNeedHide(uPID) != -1) { if(pPrevProcessInfo) { if(pCurrProcessInfo->NextEntryOffset) { //将当前这个进程(即要隐藏的进程)从 SystemInformation 中摘除(更改链表偏移指针实现) pPrevProcessInfo->NextEntryOffset += pCurrProcessInfo->NextEntryOffset; } else { //说明当前要隐藏的这个进程是进程链表中的最后一个 pPrevProcessInfo->NextEntryOffset = 0; } } else { //第一个遍历到得进程就是需要隐藏的进程 if(pCurrProcessInfo->NextEntryOffset) { (PCHAR)SystemInformation += pCurrProcessInfo->NextEntryOffset; } else { SystemInformation = NULL; } } } //遍历下一个 SYSTEM_PROCESS_INFORMATION 节点 pPrevProcessInfo = pCurrProcessInfo; //遍历结束 if(pCurrProcessInfo->NextEntryOffset) { pCurrProcessInfo = (PSYSTEM_PROCESS_INFORMATION)(((PCHAR)pCurrProcessInfo) + pCurrProcessInfo->NextEntryOffset); } else { pCurrProcessInfo = NULL; } } } } return rtStatus; } |
上面我们看到是先调用原来的NtQuerySystemInformation得到返回结果,然后根据我们需要隐藏的进程id,遍历返回结果的进程链表,从链表中摘除该节点,然后在返回给应用层。
从下面的代码中,我们看到在安装 Hook 和解除 Hook 时参数传递进去的是 ZwQuerySystemInformation,这样很有可能会让很多朋友认为我们在 Ring0 下的 Hook 的是 ZwQuerySystemInformation,如果你这样认为的话,那就大错特错了,我们这里传入 ZwQuerySystemInformation ,是因为我们需要调用 SYS_INDEX(ZwQuerySystemInformation)来获得 NtQuerySystemInformation 在 SSDT 中的地址所在的索引号,然后我们根据这个索引号来 Hook NtQuerySystemInformation,所以我们Hook的是NtQuerySystemInformation!!!
InstallSysServiceHook((ULONG)ZwQuerySystemInformation, (ULONG)HookNtQuerySystemInformation);
进程保护呢其实也是比较简单的,因为从上面一层的调用会传递一个进程句柄下来,而后我们可以根据这个进程句柄来获得进程的 EPROCESS 对象(进程位于执行体层得对象),通过这个 EPROCESS 对象,我们就可以获得这个请求被结束的进程的 PID,我们再判断这个 PID 是否是我们已经保护了的 PID,如果是的话,直接返回一个请求被拒绝即可,而如果这个 PID 未被保护,自然我们就交给原来的 NtTerminateProcess 处理即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | NTSTATUS HookNtTerminateProcess( __in_opt HANDLE ProcessHandle, __in NTSTATUS ExitStatus ) { ULONG uPID; NTSTATUS rtStatus; PCHAR pStrProcName; PEPROCESS pEProcess; ANSI_STRING strProcName; //通过进程句柄来获得该进程所对应的 FileObject 对象,由于这里是进程对象,自然获得的是 EPROCESS 对象 rtStatus = ObReferenceObjectByHandle(ProcessHandle, FILE_READ_DATA, NULL, KernelMode, &pEProcess, NULL); if(!NT_SUCCESS(rtStatus)) { return rtStatus; } //保存 SSDT 中原来的 NtTerminateProcess 地址 pOldNtTerminateProcess = (NTTERMINATEPROCESS)oldSysServiceAddr[SYSCALL_INDEX(ZwTerminateProcess)]; //通过该函数可以获取到进程名称和进程 ID,该函数在内核中实质是导出的(在 WRK 中可以看到) //但是 ntddk.h 中并没有到处,所以需要自己声明才能使用 uPID = (ULONG)PsGetProcessId(pEProcess); pStrProcName = (PCHAR)PsGetProcessImageFileName(pEProcess); //通过进程名来初始化一个 ASCII 字符串 RtlInitAnsiString(&strProcName, pStrProcName); if(ValidateProcessNeedProtect(uPID) != -1) { //确保调用者进程能够结束(这里主要是指 taskmgr.exe) if(uPID != (ULONG)PsGetProcessId(PsGetCurrentProcess())) { //如果该进程是所保护的的进程的话,则返回权限不够的异常即可 return STATUS_ACCESS_DENIED; } } //对于非保护的进程可以直接调用原来 SSDT 中的 NtTerminateProcess 来结束进程 rtStatus = pOldNtTerminateProcess(ProcessHandle, ExitStatus); return rtStatus; } |
四、效果演示
隐藏进程的效果就不演示了,反正隐藏之后在任务管理器就找不到你隐藏的进程了。
如果进程被保护了的话,如果你从任务管理器结束该进程,则会返回: