当前位置: 首页 > java >正文

Windows SSDT Hook(二)

获取SSDT基址

在32位Windows系统中,SSDT(System Service Descriptor Table)作为系统服务调用的核心数据结构,其基址信息是公开且稳定的。由于ntoskrnl.exe内核模块默认导出KeServiceDescriptorTable符号,开发者只需使用extern声明即可直接访问:

extern PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable;

然而,64位系统架构带来了重大变化。微软出于安全考虑,不再导出SSDT相关符号,并引入了以下保护机制:

  1. SSDT表基址变为动态变化,每次系统启动都会重新随机化
  2. 移除了直接的符号导出
  3. 增加了内核PatchGuard保护
    因此,想要获取64位SSDT基址就需要一些特殊技术手段来实现,常见方法包括:
  • 借助第三方符号(PDB)或调试器
    在这里插入图片描述

  • 通过msr寄存器读取

通过读取msr 0xC0000082可得到 KiSystemCall64的函数地址,从此函数地址向下可以定位到KiSystemServiceRepeat函数,从而间接获取 SSDT。
在这里插入图片描述
从上图中可以看出,4C 8D 15[lea r10] 指令后面的操作数即为SSDT地址的相对偏移,因此在通过特征值定位到此处时,可以通过以下公式计算出实际的SSDT地址。

SSDT地址 = Rip + offset = 0x140070FF9 + 0x237847

这种方法的实现在网络上可以轻松获取,这里就不详细展开了。值得注意的一点是,win10 1903版本之后,由于引入了内核隔离机制,通过msr获取到的地址不再是 KiSystemCall64,而是KiSystemCall64Shadow,这也导致了无法按照以上方法找到SSDT表。
在这里插入图片描述

  • 通过特征码扫描内核映像

由于无法再通过msr来实现查找,因此在此基础上又延伸出了类似的方案。即通过某些确定的系统函数地址,向上或向下寻找SSDT的特征值。通过IDA分析内核模块中Zw* 系统调用接口(这里并非ntdll中的用户态调用接口,而是提供给wdm驱动使用的内核态接口),可以看出内核模块中的系统调用实际上同样是通过SSDT表实现的。以ZwCreateFile 函数为例,内部调用KiServiceInternal ,然后通过jmp指令跳转到KiSystemCall64函数中。
在这里插入图片描述
在这里插入图片描述

PLONG HookHandler::GetSSDTBase() {
#ifdef _WIN64const PVOID zw_close = ZwClose;const ULONG offset = 0x400;static PSSDTEntry ssdt_address = nullptr;__try {if (ssdt_address) return ssdt_address->ServiceTableBase;PUCHAR service_internal_address = nullptr;PUCHAR zw_route_address = (PUCHAR)zw_close;// .text:000000014006A653 50               push    rax// .text:000000014006A654 B8 0C 00 00 00   mov     eax, 0Ch// .text:000000014006A659 E9 E2 67 00 00   jmp     KiServiceInternalconst UCHAR service_internal_code[] = {0x50, 0xB8, 0x00, 0x00, 0x00, 0x00, 0xE9, 0x00};for (ULONG i = 0; i < offset; i++) {if ((*(PULONGLONG)&zw_route_address[i] & *(PULONGLONG)&service_internal_code) ==*(PULONGLONG)&service_internal_code) {ULONG jmp_offset = *(PULONG)&zw_route_address[i + 7];service_internal_address = &zw_route_address[i + 6] + 5 + jmp_offset;break;}}if (!service_internal_address) return nullptr;// .text:0000000140070FF2 4C 8D 15 47 78 23 00   lea     r10, KeServiceDescriptorTable// .text:0000000140070FF9 4C 8D 1D 80 78 23 00   lea     r11, KeServiceDescriptorTableShadowconst UCHAR ssdt_table_code[] = {0x4C, 0x8D, 0x15, 0x00};for (ULONG i = 0; i < offset; i++) {if ((*(PULONG)&service_internal_address[i] & *(PULONG)&ssdt_table_code) ==*(PULONG)&ssdt_table_code) {ULONG jmp_offset = *(PULONG)&service_internal_address[i + 3];ssdt_address = (PSSDTEntry)(&service_internal_address[i] + 7 + jmp_offset);break;}}if (!ssdt_address) return nullptr;return ssdt_address->ServiceTableBase;} __except (EXCEPTION_EXECUTE_HANDLER) {return nullptr;}
#elsereturn KeServiceDescriptorTable.ServiceTableBase;
#endif
}

除了上述方法,在网上还见过一种通过在_strnicmp函数到KdDebuggerNotPresent之间搜索8b f8 c1 ef 07 83 e7 20 25 ff 0f 00 00的定位方式。

SSDT解析

前一篇中提到SSDT中的ServiceTableBase指向的一个ULONG数组,32位系统中的数组元素代表函数地址,而在64位系统中,由于ULONG类型无法存储完整的指针类型,因此实际存储的是函数地址相对于SSDT基址的"偏移"。需要注意的是这里的偏移并不仅仅是简单的偏移,而是需要固定公式计算出的偏移长度。

在这里插入图片描述

// 计算函数地址
PVOID func_addr = (PVOID)(((LONG)ssdt_base[info.func_index] >> 4) + (ULONGLONG)func_addr);
// 计算偏移
ssdt_base[info.func_index] = (LONG)((ULONGLONG)target_addr - (ULONGLONG)ssdt_base) << 4

跳转指令

由于ServiceTableBase中存储的实际上是相对于SSDT基址的"偏移",因此hook函数的地址必须在以SSDT基址为中心的4GB内存范围内。而通常情况下我们自己的驱动的加载地址必然在4GB范围外,因此我们不能简单的将Hook函数的地址写入SSDT表,而应该在范围内选择一块地址作为函数跳转的中转。

第一步,选择合适的跳转指令

这里我们选择长度最小的12字节跳转法,实现从中转地址跳转到Hook函数。

#pragma pack(push, 1)
struct HookShellCode {USHORT mov;ULONG_PTR addr;UCHAR push;UCHAR ret;
};
#pragma pack(pop)
HookShellCode shell_code;
shell_code.mov = 0xB848;
shell_code.addr = (ULONG_PTR)func_addr;
shell_code.push = 0x50;
shell_code.ret = 0xC3;

第二步,遍历所有的代码段,从中找出合适的中转地址

PULONG kernel_base = GetKernelBase();// 获取内核模块基址
if (!kernel_base) return nullptr;// Dos Header
PIMAGE_DOS_HEADER dos_header = (PIMAGE_DOS_HEADER)kernel_base;
if (dos_header->e_magic != IMAGE_DOS_SIGNATURE) return nullptr;// NT Header
PIMAGE_NT_HEADERS nt_header = (PIMAGE_NT_HEADERS)((PUCHAR)dos_header + dos_header->e_lfanew);
if (nt_header->Signature != IMAGE_NT_SIGNATURE) return nullptr;// Section Header
IMAGE_SECTION_HEADER* section_header = IMAGE_FIRST_SECTION(nt_header);
ULONG rva = (ULONG)((PUCHAR)base_addr - (PUCHAR)kernel_base);
// 遍历代码段
for (USHORT i = 0; i < nt_header->FileHeader.NumberOfSections; i++) {// find the section which function existsif (section_header[i].VirtualAddress <= rva &&section_header[i].VirtualAddress + section_header[i].Misc.VirtualSize > rva) {PUCHAR section_addr = (PUCHAR)kernel_base + section_header[i].VirtualAddress;for (ULONG offset = 0, size = 0; offset < section_header[i].SizeOfRawData; offset++) {if (section_addr[i] == 0x90 || section_addr[i] == 0xCC)  // NOP or INT3size++;elsesize = 0;// 找到大小匹配的空闲内存if (size == HOOK_CODE_SIZE)return (PULONG)(section_addr + offset - HOOK_CODE_SIZE + 1);}}
}

PatchGuard

基于以上内容,就可以实现在64位系统上的SSDT Hook了。然而,当实际运行Hook驱动程序时,会发现正常运行一段时间后,系统就会触发BSOD(错误代码:0x109,内核检测到关键内核代码或数据损坏),而这就是PatchGuard的作用了。

到目前为止,通过PatchGuard的方法未作深入研究,仅通过网络得知可能存在以下两种方法:

  • 通过某种方式伪装骗过PG检测,例如EPT Hook等。
  • 直接与PG对抗
http://www.xdnf.cn/news/10000.html

相关文章:

  • 【软件设计】通过软件设计提高 Flash 的擦写次数
  • 每日Prompt:指尖做画
  • kuboard自带ETCD存储满了处理方案
  • (21)量子计算对密码学的影响
  • EasyExcel复杂Excel导出
  • 测试用例篇章
  • C语言创意编程:用趣味实例玩转基础语法(4)
  • CIO大会, AI课笔记手稿分享
  • VScode ios 模拟器安装cocoapods
  • Java Spring Boot 自定义注解详解与实践
  • `docker commit` 和 `docker save`区别
  • 每日c/c++题 备战蓝桥杯(P1011 [NOIP 1998 提高组] 车站)
  • 论文速读《UAV-Flow Colosseo: 自然语言控制无人机系统》
  • If possible, you should set the Secure flag for these cookies 修复方案
  • 操作系统原理第8章:文件管理 重点内容
  • 2025.05.30【转录组】|Ribo-seq数据流程详解(一 质量控制)
  • split_conversion将json转成yolo训练用的txt,在直接按照8:1:1的比例分成训练集,测试集,验证集
  • RuoYi前后端分离框架集成手机短信验证码(二)之前端篇
  • 学习vue3阶段性复习(插槽,Pinia,生命周期)
  • VSCode+Cline 安装配置及使用说明
  • vue+threeJs 绘制3D圆形
  • Linux 的主要时钟类型
  • 菜鸟之路Day36一一Web开发综合案例(部门管理)
  • ARXML解析与可视化工具
  • 硬件学习笔记--64 MCU的ARM核架构发展及特点
  • CentOS 7 环境中部署 LNMP(Linux + Nginx + MySQL 5.7 + PHP)
  • AI科技前沿动态:5.26 - 5.30 一周速览
  • Jetson Orin Nano - SONY imx415 camera驱动开发
  • 2025年5月24号高项综合知识真题以及答案解析(第1批次)
  • redis未授权(CVE-2022-0543)