Loading... # 内核反调试对抗 ## TitanHide源码笔记 源码:https://github.com/mrexodia/TitanHide/tree/master/TitanHide ### TitanHide.cpp #### DriverEntry 1. 初始化了字符串`L"\\Device\\"`和`L"\\DosDevices\\"`,后面可知是位创建设备和符号链接做准备 2. 提取了DriverName,设定上我们可以修改TitanHide的名称防止被检测 3. 初始化InitLog,查看源码可知初始化了LogFilename值,保存到`L"\\DosDevices\\C:\\DriverName\\DriverName.log"` 4. 注册卸载函数和IRP调度函数,IRP类型包括打开设备、关闭设备、写入设备 5. 初始化ntdll.dll和没有文档说明的API 6. **FindCrossThreadFlagsOffset**,看名字可以知道是找线程标志位偏移用的 7. 创建TitanHide驱动设备以及符号链接,为之后用户态访问做铺垫 8. Hooks初始化 copilot总结了下 | 层次 | 名称 | 访问方式 | 代码示例 | | :------: | :-------------------: | :----------: | :---------------------------------: | | 用户态 | \\.\TitanHide | Win32 API | CreateFile("\\\\.\\TitanHide", ...) | | 符号链接 | \DosDevices\TitanHide | 内核转换 | IoCreateSymbolicLink() 创建 | | 内核设备 | \Device\TitanHide | 驱动直接访问 | IoCreateDevice() 创建 | #### DriverUnload 1. 清理设备和符号链接 2. 清理API和Hook #### DriverCreateClose和DriverDefaultHandler DriverCreateClose相比DriverDefaultHandler只是返回了STATUS_SUCCESS,告诉用户态允许CreateFile和CloseHandle #### DriverWrite 1. 读取IRP栈位置 2. 提取缓冲区数据`Irp->AssociatedIrp.SystemBuffer` 3. 传递给Hider::ProcessData ### log.cpp #### InitLog 前面已分析过,用来初始化LogFilename #### Log 1. 使用C语言的**可变参数机制**,使得Log类似printf那样接受不定数量的参数,`va_start(vl, format); `初始化,指向第一个可变参数,format 是最后一个固定参数 2. _vsnprintf格式化字符串,然后清理参数列表 3. ZwCreateFile、ZwWriteFile和ZwClose创建、写入、关闭LogFilename日志文件 ### hider.cpp hider.h设置了一个Hider类包括ProcessData和IsHidden两个返回bool的静态函数 #### ProcessData 前面已经知道了传递给ProcessData是Buffer和大小 1. 首先判断大小是HIDE_INFO结构体大小的整数倍,不是直接返回false;HIDE_INFO结构体如下 ~~~c++ struct HIDE_INFO { HIDE_COMMAND Command; ULONG Type; ULONG Pid; }; ~~~ 2. 接着计算了Buffer里包含多少个HIDE_INFO结构体,并定义了HIDE_INFO指针 3. for循环遍历HIDE_INFO结构体 4. 根据每个HIDE_INFO里的Command成员做不同操作 5. 遍历完成返回true 下面分析每个Command成员做的操作 ~~~c++ enum HIDE_COMMAND { HidePid, //Hide a process UnhidePid, //Unhide a process UnhideAll //Unhide everything }; ~~~ ##### HidePid(0):隐藏进程 1. 查找进程是否已经在隐藏列表中 * 调用EntryFind,传入Pid,获取入口点 * 如果返回值-1,就调用EntryAdd添加到列表 * 如果存在就更新类型EntrySet 2. 处理特殊情况 - HideThreadHideFromDebugger * 调用UndoHideFromDebuggerInRunningThreads ##### UnhidePid(1): 取消部分隐藏 1. 查找条目EntryFind 2. 找到后清除指定的隐藏类型标志EntryUnset 3. 如果所有标志都清空,删除整个条目EntryDel ##### UnhideAll(2):清空所有隐藏 直接调用EntryClear #### 各种Entry* * EntryFind:遍历HideEntries结构体数组,找到Pid相等的返回下标;没找到返回-1 ~~~c++ struct HIDE_ENTRY { ULONG Type; ULONG Pid; }; ~~~ * EntryAdd:向HideEntries结构体里添加新的进程条目 * 先检查TotalHideEntries没有超过MAX_HIDE_ENTRIES(65536) * RtlCopyMemory拷贝HIDE_ENTRY结构体 * TotalHideEntries++,这里还用到了之前windows核心编程里学的InterlockedExchange,保证线程安全(防止多线程读取旧值) * EntryDel:很明显和EntryAdd相反 * TotalHideEntries--,同样调用了InterlockedExchange * 如果TotalHideEntries为0,调用EntryClear * 反之调用RtlCopyMemory用后面的结构体前移把HideEntries[EntryIndex]覆盖掉 * EntrySet:使用`|= Type`来设置HideEntries[EntryIndex].Type的某一位 * EntryUnset:使用`&= ~Type`来取消HideEntries[EntryIndex].Type的某一位 * EntryGet:获取HideEntries[EntryIndex]的Type * EntryClear:把TotalHideEntries置0 #### IsHidden 1. 获取进程在HideEntries里的下标,-1则返回false,表示没有隐藏 2. 获取进程的Type和目标uType与运算,看是否满足目标隐藏类型,满足返回true,否则返回false ### ntdll.cpp ntdll.h设置了一个NTDLL类包括Initialize、Deinitialize和GetExportSsdtIndex,还有两个私有变量FileData和FileSize #### Initialize 1. 初始化ntdll.dll相关信息 2. 调用KeGetCurrentIrql检查当前代码的执行级别,确保在正确的中断请求级别下执行文件操作。 --- 补充下IRQL知识 **IRQL** (Interrupt Request Level) 是 Windows 内核中的执行优先级系统,类似于 CPU 的特权级别,但用于中断处理。 | IRQL级别 | 值 | 说明 | 可用操作 | | :------------: | :--: | :------------------------: | :-------------------------------------: | | PASSIVE_LEVEL | 0 | 用户模式线程,普通内核代码 | 几乎所有操作(文件I/O、分页内存、等待) | | APC_LEVEL | 1 | 异步过程调用 | 大部分操作,不能访问分页内存 | | DISPATCH_LEVEL | 2 | DPC(延迟过程调用) | 不能等待、不能访问分页内存、不能文件I/O | | DIRQL | 3-31 | 设备中断 | 只能访问非分页内存,极少操作 | Q:这里为什么检查IRQL? A:后面要执行文件读取操作 --- 3. ZwCreateFile打开ntdll.dll 4. ZwQueryInformationFile获取StandardInformation信息 ~~~c++ typedef struct _FILE_STANDARD_INFORMATION { LARGE_INTEGER AllocationSize; // 磁盘分配大小 LARGE_INTEGER EndOfFile; // 文件实际大小 ULONG NumberOfLinks; // 硬链接数 BOOLEAN DeletePending; // 是否待删除 BOOLEAN Directory; // 是否是目录 } FILE_STANDARD_INFORMATION; ~~~ 5. 提取文件大小,分配内存,RtlAllocateMemory是_global.cpp里自定义函数,我们直接来看看做的什么 ~~~c++ void* RtlAllocateMemory(bool InZeroMemory, SIZE_T InSize) { void* Result = ExAllocatePoolWithTag(NonPagedPool, InSize, GetPoolTag()); if(InZeroMemory && (Result != NULL)) RtlZeroMemory(Result, InSize); return Result; } ~~~ ExAllocatePoolWithTag分配了不可分页的内存(始终驻留在物理内存),然后RtlZeroMemory会清0 6. ZwReadFile读取文件内容到FileData #### Deinitialize 调用RtlFreeMemory清理了FileData内存 ~~~c++ void RtlFreeMemory(void* InPointer) { ExFreePool(InPointer); } ~~~ #### GetExportSsdtIndex 提取SSDT索引 1. 利用PE::GetExportOffset查找到处函数的RVA(相对虚拟地址)/偏移 2. 利用上面的偏移获取函数代码指针 3. 扫描机器码寻找SSDT索引 * 最多扫描32字节 * 防止越界,不超过FileSize * ret n(0xC3)或ret(0xC2)终止扫描 * 出现0xB8(mov eax, imm32),提取imm32 4. 找不到返回-1 补充下SSDT相关知识 --- 所有 ntdll.dll 中的 Nt* 系统调用函数都遵循相同的模式: ~~~ NtXXX: mov eax, <syscall_number> ; ← 总在函数开头的前几个字节 mov edx, 0x7FFE0300 ; SharedUserData->SystemCall call edx ; 进入内核 ret <stack_cleanup> ~~~ 32 字节足够找到 mov eax 指令,因为它通常在函数的前 5 个字节。 x64下同样有 ~~~ ; x64 系统调用约定: NtQueryInformationProcess: mov r10, rcx ; 备份第一个参数 mov eax, 0x19 ; 系统调用号 syscall ; 直接进入内核 ret ; 字节码: 4C 8B D1 mov r10, rcx B8 19 00 00 00 mov eax, 0x19 ← 仍然是 0xB8! 0F 05 syscall C3 ret ~~~ --- ### pe.cpp #### GetExportOffset 前面ntdll.cpp我们知道GetExportOffset传入了ntdll的数据FileData、数据大小FileSize以及想要导出的函数名字ExportName 1. 将FileData强制转为PIMAGE_DOS_HEADER类型 2. 验证DOS头:检查e_magic值是否为IMAGE_DOS_SIGNATURE(0x5A4D) 3. 验证PE头:FileData + pdh->e_lfanew强制转为PIMAGE_NT_HEADERS类型,检查Signature成员是否为IMAGE_NT_SIGNATURE(0x00004550) 4. 验证导出目录:首先需要区分x86和x64,检查PE头里可选文件头的魔术是否等于0x20b,等于则为64位,反之32位;取DataDirectory 这里补充下PE结构 ~~~c++ // 32 位 PE 文件 typedef struct _IMAGE_NT_HEADERS { ULONG Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32; // 64 位 PE 文件 typedef struct _IMAGE_NT_HEADERS64 { ULONG Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER64 OptionalHeader; } IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64; typedef struct _IMAGE_OPTIONAL_HEADER { // // Standard fields. // USHORT Magic; UCHAR MajorLinkerVersion; UCHAR MinorLinkerVersion; ULONG SizeOfCode; ULONG SizeOfInitializedData; ULONG SizeOfUninitializedData; ULONG AddressOfEntryPoint; ULONG BaseOfCode; ULONG BaseOfData; // // NT additional fields. // ULONG ImageBase; ULONG SectionAlignment; ULONG FileAlignment; USHORT MajorOperatingSystemVersion; USHORT MinorOperatingSystemVersion; USHORT MajorImageVersion; USHORT MinorImageVersion; USHORT MajorSubsystemVersion; USHORT MinorSubsystemVersion; ULONG Win32VersionValue; ULONG SizeOfImage; ULONG SizeOfHeaders; ULONG CheckSum; USHORT Subsystem; USHORT DllCharacteristics; ULONG SizeOfStackReserve; ULONG SizeOfStackCommit; ULONG SizeOfHeapReserve; ULONG SizeOfHeapCommit; ULONG LoaderFlags; ULONG NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32; typedef struct _IMAGE_OPTIONAL_HEADER64 { USHORT Magic; UCHAR MajorLinkerVersion; UCHAR MinorLinkerVersion; ULONG SizeOfCode; ULONG SizeOfInitializedData; ULONG SizeOfUninitializedData; ULONG AddressOfEntryPoint; ULONG BaseOfCode; ULONGLONG ImageBase; ULONG SectionAlignment; ULONG FileAlignment; USHORT MajorOperatingSystemVersion; USHORT MinorOperatingSystemVersion; USHORT MajorImageVersion; USHORT MinorImageVersion; USHORT MajorSubsystemVersion; USHORT MinorSubsystemVersion; ULONG Win32VersionValue; ULONG SizeOfImage; ULONG SizeOfHeaders; ULONG CheckSum; USHORT Subsystem; USHORT DllCharacteristics; ULONGLONG SizeOfStackReserve; ULONGLONG SizeOfStackCommit; ULONGLONG SizeOfHeapReserve; ULONGLONG SizeOfHeapCommit; ULONG LoaderFlags; ULONG NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64; ~~~ 5. 获取导出目录(IMAGE_DIRECTORY_ENTRY_EXPORT)的RVA、大小;RvaToOffset计算导出目录在文件中的偏移 ~~~c++ typedef struct _IMAGE_DATA_DIRECTORY { ULONG VirtualAddress; ULONG Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY; ~~~ 补充下常用的数据目录索引、对应的常量以及描述 | 索引/值 | 常量名 | 描述 | | :-----: | :----------------------------------: | :----------: | | 0 | IMAGE_DIRECTORY_ENTRY_EXPORT | 导出表 | | 1 | IMAGE_DIRECTORY_ENTRY_IMPORT | 导入表 | | 2 | IMAGE_DIRECTORY_ENTRY_RESOURCE | 资源表 | | 3 | IMAGE_DIRECTORY_ENTRY_EXCEPTION | 异常表 | | 4 | IMAGE_DIRECTORY_ENTRY_SECURITY | 安全目录 | | 5 | IMAGE_DIRECTORY_ENTRY_BASERELOC | 重定位表 | | 6 | IMAGE_DIRECTORY_ENTRY_DEBUG | 调试目录 | | 7 | IMAGE_DIRECTORY_ENTRY_ARCHITECTURE | 架构特定数据 | | 8 | IMAGE_DIRECTORY_ENTRY_GLOBALPTR | 全局指针 | | 9 | IMAGE_DIRECTORY_ENTRY_TLS | TLS 表 | | 10 | IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG | 加载配置表 | | 11 | IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT | 绑定导入表 | | 12 | IMAGE_DIRECTORY_ENTRY_IAT | 导入地址表 | | 13 | IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT | 延迟导入表 | | 14 | IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR | COM 描述符 | 6. 验证转换出来的ExportDirOffset不等于PE_ERROR_VALUE(-1) 7. 根据偏移获取导出表,提取导出函数数目,然后利用RvaToOffset提取三个数组偏移:函数地址数组、名称序号数组、函数名称数组 8. 都不等于-1,就根据偏移提取对应数组指针 9. 遍历导出函数,每次循环: * 获取当前函数名称 * 通过序号索引获取函数RVA * 忽略转发导出(函数RVA在导出表范围的) * 比较函数名称,相等就返回此时函数的文件偏移 #### RvaToOffset 1. 获取节数 2. 遍历所有节,检查RVA是否在当前节的范围内 3. 计算文件偏移 #### GetPageBase 根据内存中的指针,找到其所在 PE 节(Section)的基址和大小 1. 验证指针不再模块基址前 2. 计算RVA 3. 同GetExportOffset验证了DOS头、PE头 4. 获取节表 5. 查找RVA所在的节 6. 返回节的基址 #### RvaToSection 类似RvaToOffset,只不过返回的是RVA所在节区的索引 ### undocumented.cpp 这个很明显就是微软一些结构体和函数没有文档说明,需要我们手动定义 #### UndocumentedInit 这个函数干的是把内核函数导出 补充下windows内核函数类型 --- | 类型 | 导出方式 | 可用性 | 示例 | | ---------- | ------------------------- | -------------- | --------------------------------- | | 公开导出 | ntoskrnl.exe 导出表 | 直接链接 | KeWaitForsingleObject | | 部分公开 | ntoskrnl.exe 导出但无文档 | 需动态获取 | NtQueryInformationProcess | | 完全未公开 | 仅存在于SSDT表中 | 必须从SSDT获取 | NtQueryObject、NtGetContextThread | WDK (Windows Driver Kit) 不为某些函数提供导入库或头文件声明,直接调用会导致链接错误。 所以有些函数需要靠运行时动态查找或者从SSDT获取 --- 根据上面补充,我们来看TitanHide怎么实现的两种获取函数地址方法 ##### MmGetSystemRoutineAddress(运行时动态查找) 重复模板,我们只看第一个 ~~~c++ if(!ZwQIP) { UNICODE_STRING routineName; RtlInitUnicodeString(&routineName, L"ZwQueryInformationProcess"); ZwQIP = (ZWQUERYINFORMATIONPROCESS)MmGetSystemRoutineAddress(&routineName); if(!ZwQIP) return false; } ~~~ 1. 检查该函数地址是否存在 2. 不存在,定义函数名字符串 3. 调用MmGetSystemRoutineAddress去ntoskrnl.exe导出表查找并返回函数地址,注意需要强制把返回地址转为对应函数结构 ~~~c++ typedef NTSTATUS(NTAPI* ZWQUERYINFORMATIONPROCESS)( IN HANDLE ProcessHandle, IN PROCESSINFOCLASS ProcessInformationClass, OUT PVOID ProcessInformation, IN ULONG ProcessInformationLength, OUT PULONG ReturnLength OPTIONAL ); ~~~ ##### SSDT::GetFunctionAddress (从 SSDT 表获取) 重复模板,我们只看第一个 ~~~c++ if(!NtQO) { NtQO = (NTQUERYOBJECT)SSDT::GetFunctionAddress("NtQueryObject"); if(!NtQO) return false; } ~~~ 1. 检查该函数地址是否存在 2. 不存在直接SSDT::GetFunctionAddress传入函数名返回函数地址,同样需要强制把返回地址转为对应函数结构 ~~~c++ typedef NTSTATUS(NTAPI* NTQUERYOBJECT)( IN HANDLE Handle OPTIONAL, IN OBJECT_INFORMATION_CLASS ObjectInformationClass, OUT PVOID ObjectInformation OPTIONAL, IN ULONG ObjectInformationLength, OUT PULONG ReturnLength OPTIONAL ); ~~~ 整理所有导出函数如下,这些是我们反调试要用到的 ~~~ //==================================================== // 第一组: 通过 MmGetSystemRoutineAddress 获取 //==================================================== // 这些函数在 ntoskrnl.exe 的导出表中 // 进程信息查询 ZwQueryInformationProcess NtQueryInformationProcess NtSetInformationProcess // 线程信息查询/设置 NtQueryInformationThread ZwSetInformationThread NtSetInformationThread // 系统信息查询 ZwQuerySystemInformation NtQuerySystemInformation // 对象操作 NtClose NtDuplicateObject // 异常处理 KeRaiseUserException //==================================================== // 第二组: 通过 SSDT::GetFunctionAddress 获取 //==================================================== // 这些函数只存在于 SSDT 表中,不在导出表中 // 对象查询 NtQueryObject // 线程上下文 NtGetContextThread NtSetContextThread NtContinue // 调试控制 NtSystemDebugControl // 线程创建/终止 (Vista+) ZwCreateThreadEx NtCreateThreadEx ZwTerminateThread NtTerminateThread ~~~ #### GetKernelBase 用于动态获取 Windows 内核映像 (ntoskrnl.exe) 在内存中的基址,是 TitanHide 访问 SSDT 表的关键步骤 1. 定义数据结构:单个模块信息、模块列表 ~~~c++ typedef struct _SYSTEM_MODULE_ENTRY { HANDLE Section; // 节对象句柄 PVOID MappedBase; // 映射基址(用户态) PVOID ImageBase; // ← 内核基址(我们需要这个) ULONG ImageSize; // ← 映像大小 ULONG Flags; // 标志 USHORT LoadOrderIndex; // 加载顺序索引 USHORT InitOrderIndex; // 初始化顺序索引 USHORT LoadCount; // 加载计数 USHORT OffsetToFileName; // 文件名偏移 UCHAR FullPathName[256]; // 完整路径(如 "\SystemRoot\system32\ntoskrnl.exe") } SYSTEM_MODULE_ENTRY, *PSYSTEM_MODULE_ENTRY; #pragma warning(disable:4200) // 禁用"零长度数组"警告 typedef struct _SYSTEM_MODULE_INFORMATION { ULONG Count; // 模块数量 SYSTEM_MODULE_ENTRY Module[0]; // 可变长度数组(C99 柔性数组) } SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION; ~~~ 2. 第一次调用ZwQuerySystemInformation,获取所需缓冲区大小 ~~~c++ ULONG SystemInfoBufferSize = 0; NTSTATUS status = Undocumented::ZwQuerySystemInformation( SystemModuleInformation, // 查询类型: 系统模块信息 &SystemInfoBufferSize, // 输出缓冲区(实际未使用) 0, // 缓冲区大小为 0 &SystemInfoBufferSize // [OUT] 返回实际所需大小 ); ~~~ 3. 分配内存,这里有个细节是分配了双倍大小,copilot分析是为了防止新模块加载和竞态条件;之后内存置0 4. 第二次调用ZwQuerySystemInformation,获取了实际数据pSystemInfoBuffer 5. 提取内核地址 6. 释放pSystemInfoBuffer 7. 返回内核地址 ### threadhidefromdbg.cpp #### FindCrossThreadFlagsOffset 动态查找ETHREAD 结构体中CrossThreadFlags字段的偏移量,因为ETHREAD结构体未公开,不同Windows版本中偏移不同 1. 初始化了多个变量 2. 处理旧版本Windows(XP/2003) | Windows版本 | BuildNumber | 偏移(x64) | 偏移(x86) | | ----------- | ----------- | ----------- | ----------- | | XP | 2600 | N/A | 0x248 | | 2003 | 3790 | 0x3FC | 0x240 | | Vista+ | $\ge$6000 | 动态查找 | 动态查找 | 3. 分配4KB\*4内存并初始化为-1 4. 查找(ReferenceProcessByName)并附加(KeStackAttachProcess)到svchost.exe进程 5. 创建测试线程(挂起)并获取ETHREAD指针,此时的CrossThreadFlags处于干净状态,大部分标志未设置 6. 第一次扫描记录初始状态,从0x400开始查找(跳过KTHREAD公开部分),每次移动4字节,需要满足: * Terminated标志未设置 * HideFromDebugger标志未设置 ALIGN_UP_BY是将地址向上对齐到页边界,比如 ~~~ Thread = 0xFFFF8000`12345678 PAGE_SIZE = 0x1000 (4KB) ALIGN_UP_BY(Thread, PAGE_SIZE) = 0xFFFF8000`12346000 // 向上对齐到下一个页 End = 0xFFFF8000`12346000 - 0xFFFF8000`12345678 = 0x988 (2440 字节) ~~~ 找到满足的设置CandidateOffsets 7. ZwSetInformationThread设置ThreadHideFromDebugger标志 8. 第二次扫描CandidateOffsets,-1的直接跳过,不是-1的就去Thread重新读取,看是否发生只有HideFromDebugger位变化;满足的记录下来LastMatchFound以及找到的次数MatchesFound 9. 如果MatchesFound不等于1,说明没有找到合适的偏移;找到了就赋值给Offset 10. 清理资源 #### ReferenceProcessByName 顾名思义,根据进程名找到对应进程 1. 两次ZwQuerySystemInformation获取SystemProcessInfo,类似前面undocumented.cpp的GetKernelBase 2. 遍历进程列表(SystemProcessInfo)并比较进程名(ImageName.Buffer),找到后返回进程 #### UndoHideFromDebuggerInRunningThreads 使用 DKOM (Direct Kernel Object Manipulation) 技术,直接修改内核内存,清除指定进程中所有线程的 ThreadHideFromDebugger 标志。 1. 验证CrossThreadFlagsOffset是否设置好 2. 根据Pid获取进程对象,PsLookupProcessByProcessId 3. 两次ZwQuerySystemInformation获取SystemProcessInfo 4. 遍历进程列表(SystemProcessInfo)查找目标进程 进程结构如下 ~~~c++ typedef struct _SYSTEM_PROCESS_INFORMATION { ULONG NextEntryOffset; // 指向下一个条目的偏移 ULONG NumberOfThreads; // 线程数目 LARGE_INTEGER WorkingSetPrivateSize; ULONG HardFaultCount; ULONG NumberOfThreadsHighWatermark; ULONGLONG CycleTime; LARGE_INTEGER CreateTime; LARGE_INTEGER UserTime; LARGE_INTEGER KernelTime; UNICODE_STRING ImageName; // 进程名称 KPRIORITY BasePriority; HANDLE UniqueProcessId; // 进程PID HANDLE InheritedFromUniqueProcessId; ULONG HandleCount; ULONG SessionId; ULONG_PTR UniqueProcessKey; SIZE_T PeakVirtualSize; SIZE_T VirtualSize; ULONG PageFaultCount; SIZE_T PeakWorkingSetSize; SIZE_T WorkingSetSize; SIZE_T QuotaPeakPagedPoolUsage; SIZE_T QuotaPagedPoolUsage; SIZE_T QuotaPeakNonPagedPoolUsage; SIZE_T QuotaNonPagedPoolUsage; SIZE_T PagefileUsage; SIZE_T PeakPagefileUsage; SIZE_T PrivatePageCount; LARGE_INTEGER ReadOperationCount; LARGE_INTEGER WriteOperationCount; LARGE_INTEGER OtherOperationCount; LARGE_INTEGER ReadTransferCount; LARGE_INTEGER WriteTransferCount; LARGE_INTEGER OtherTransferCount; SYSTEM_THREAD_INFORMATION Threads[1]; // SystemProcessInformation // SYSTEM_EXTENDED_THREAD_INFORMATION Threads[1]; // SystemExtendedProcessinformation // SYSTEM_EXTENDED_THREAD_INFORMATION + SYSTEM_PROCESS_INFORMATION_EXTENSION // SystemFullProcessInformation } SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION; ~~~ 链表遍历如下 ~~~ SystemProcessInfo: ┌────────────────────────────────────┐ │ Entry[0]: System (PID 4) │ │ NextEntryOffset = 0x200 │ ─┐ └────────────────────────────────────┘ │ │ ┌────────────────────────────────────┐ │ │ Entry[1]: smss.exe (PID 256) │ <┘ │ NextEntryOffset = 0x180 │ ─┐ └────────────────────────────────────┘ │ │ ┌────────────────────────────────────┐ │ │ Entry[2]: target.exe (PID 1234) │ <┘ ← 找到目标! │ NextEntryOffset = 0x1A0 │ │ NumberOfThreads = 5 │ │ Threads[0].UniqueThread = 5678 │ │ Threads[1].UniqueThread = 5680 │ │ ... │ └────────────────────────────────────┘ ~~~ 5. 找到进程后再去遍历线程,用PsLookupThreadByThreadId获取线程对象 6. 计算线程对象里CrossThreadFlags的位置 7. 原子清除线程里的HideFromDebugger标志,并统计有多少个线程清除了这个标志 8. 打印信息后清理资源 有些软件利用NtSetInformationThread设置了ThreadHideFromDebugger,会导致调试器无法跟踪这个线程,而TitanHide启动后会使用UndoHideFromDebuggerInRunningThreads来把所有ThreadHideFromDebugger标志位清0,这样就可以调试线程了 ### hooks.cpp 非常重要,里面hook的API和反调试关系很大,要重点学习 #### Initialize 1. KeInitializeMutex初始化内核互斥体gDebugPortMutex 2. SSDT::Hook了NtQueryInformationProcess、NtQueryInformationThread、NtQueryObject、NtQuerySystemInformation、NtSetInformationThread、NtClose、NtDuplicateObject、NtGetContextThread、NtSetContextThread、NtSystemDebugControl、NtCreateThreadEx(winXP),并统计和返回hook成功的次数 #### Deinitialize 把初始化hook过的API全部解除hook ### ssdt.cpp 最核心的hook #### SSDTfind 获取SSDT表地址 只看64位 1. 获取Windows内核映像 (ntoskrnl.exe) 在内存中的基址 2. 获取.text节 3. 定位KiSystemServiceStart和KeServiceDescriptorTable(直接找字节pattern),如下图xmmword_1412018C0  #### GetFunctionAddress 1. SSDTfind获取SSDT表位置 2. 获取SSDT的ServiceTable 3. 根据apiname提取在Table里的索引 4. 计算api所在地址 #### FindCaveAddress 从给定CodeStart开始扫描出现连续0x90或0xCC的代码块,出现区域大小为CaveSize就返回这段区域起始地址 #### HOOK SSDT::Hook 1. 定位SSDT(SSDTfind) 2. 获取SSDT的ServiceTable 3. 提取SSDT索引 4. 检查索引是否超出SSDT的服务数目 5. 接下来分64位和32位 ##### 64位 x64 SSDT Hook; 1) find API addr,获取API地址 2) get code page+size,获取代码页和大小 3) find cave address,找到代码洞地址(nop或int 3) 4) hook cave address (using hooklib),调用Hooklib::Hook来生成hook函数 5) change SSDT value,更新SSDT值 ##### 32位 x86 SSDT Hook: 1) change SSDT value 说明: 32位直接使用hook函数的绝对地址,而64位 SSDT表项存储的是相对偏移量,需要额外寻找一个新地址来存储跳转hook函数的代码 #### void SSDT::Hook 1. 查找 SSDT 表 2. 直接使用 hHook->SSDTnew 3. 更新 SSDT 表 上一个的重载版,主要作用是恢复/重新激活之前创建的 hook,但很神奇?没有看到调用的地方 #### SSDT::Unhook 1. 定位SSDT(SSDTfind) 2. 获取SSDT的ServiceTable 3. 根据传入的hHook恢复ServiceTable 4. 如果free为true,调用Hooklib::Unhook ### hooklib.cpp #### hook_internal 1. 分配一个HOOKSTRUCT结构 2. 初始化HOOK指针的地址,hook的地址、操作码(mov、push、ret) 3. 把原来地址拷贝进hook的orig 4. 返回hook #### HOOK Hooklib::Hook 调用hook_internal #### bool Hooklib::Hook 没调用 #### bool Hooklib::Unhook 恢复原始 最后修改:2026 年 03 月 10 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏