x64内核编程小窥-SSDT HOOK笔记
声明:本文不包含Anti-PatchGuard的技术,只是单纯的记录一下本人在x64下做SSDT HOOK和x86下的一些区别,以及自己记录的x86下和x64下的内核编程的一些区别,所以想看过PatchGuard的看官们要失望了。本文脱胎于Tesla.Angla的教程,纯属笔记和扫盲贴,为的是加深自己对x64内核编程的印象,也为同样像我这样的菜鸟提供一点点资料,算不上原创。为了论坛文章的质量,对于那些被讨论过无数次的、在网上一搜一堆的基础知识,如什么是SSDT表,什么是KeServiceDescriptorTable本文不做介绍,只在最后做点推荐阅读。本文也只是学习的产物,难免有理解错误的地方,如有错误还请大家谅解。
此外,由于是笔记,所以本文的写作方法是对32bit和64bit进行对比,阅读本文最好具备一些win32 hook的基本知识,做过win32 SSDT Hook最好不过。
本人测试环境 win7sp1 x64(虚拟机+主机),VS2013+WDK8.1
一.获取64位下的SSDT表
在x64下做SSDT HOOK和x86下做SSDT HOOK虽然在原理上都是一样的,但是在实现上还是有很大不同的,最起码的,32位下,KeServiceDescriptorTable表是导出的,而在64位下则没有导出。也就是说,在32位下,我们只需要对KeServiceDescriptorTable进行一下声明就可以使用了,而在64位下,则需要另想办法——比如,通过特征码对内存区域进行搜索,这听上去和32位下的获取Shadow SSDT方法貌似一样,实际上,确实是一样的。所以,在64位下做SSDT HOOK明显比在32位下要复杂,但好处在于,可以用同一段代码实现对Shadow SSDT表的查找。直接上代码:
UINT64 getKeServiceDescirptorTable()
{
UINT64 KeServiceDescirptorTable = 0;// 接收KeServiceDescirptorTable地址
PUCHAR addrStartSearch = (PUCHAR)__readmsr(ULONG(0xC0000082)); // 读取KiSystemCall64地址
PUCHAR addrEndSearch = addrStartSearch + 0x500; // 搜索的结束地址
ULONG tmpAddress = 0;// 用于保存临时地址
int j = 0;// 用于进行索引
// 开始搜索,从KiSystemCall64开始搜索其函数体内关于KeServiceDescriptorTable结构的信息
for (PUCHAR i = addrStartSearch; i < addrEndSearch; i++, j++)
{
if (MmIsAddressValid(i) && MmIsAddressValid(i + 1) && MmIsAddressValid(i + 2))
{
//特征码 0x4c 0x8d 0x15
if (addrStartSearch[j] == 0x4c &&
addrStartSearch[j + 1] == 0x8d &&
addrStartSearch[j + 2] == 0x15)
{
RtlCopyMemory(&tmpAddress, i + 3, 4); // 保存后4个机器码
// 得到KeServiceDescirptorTable表真实地址
KeServiceDescirptorTable = tmpAddress + (INT64)i + 7;
}
}
}
return KeServiceDescirptorTable;
}
解释一下:在64位下,要得到KeServiceDescriptorTable表,需要从KiSystemCall64中得其偏移(tmpAddress),而要得到KiSystemCall64需要从C0000082寄存器(msr,特别模块寄存器)中读取其地址。从KiSystemCall64往后搜索0x500字节左右,就可以搜到关于KeServiceDescriptorTable信息.....简单来说就是这样:
__readmsr(0xC00000082)->KiSystemCall64->KeServiceDescriptorTable
MmIsAddressValid是为了检验内存地址是否可读,也可以不加,这样就可以坐等BSOD了
。
得到KeServiceDescriptorTable后将其封装成函数,这样,要得到SSDT表就简单了,SSDT表基址是KeServiceDescriptorTable的第一个结构体成员。所以只需写上如下代码就是:
PSERVICES_DESCRIPTOR_TABLE pServiceDescriptorTable = (PSERVICES_DESCRIPTOR_TABLE)getKeServiceDescirptorTable();
PULONG ssdt = (PULONG)pServiceDescriptorTable->ServiceTableBase;
这样,就可以通过ssdt指针变量来操作SSDT表了。
二.HOOK前的准备
2.1 64位下SSDT表与32位下的区别
需要了解的是,在32位下和64位下变量类型的区别!int、long、char、wchar_t等在64位下长度依旧没变,而指针变量统一是8字节,比如前面定义的PULONG,在64位下用sizeof(PULONG)发现,是8字节的,只不过它指向地址是ULONG类型长度的。64位下要使用8字节长度的变量应使用__int64、INT64、UINT64、ULONGLONG、PULONGLONG等。(均指VS编译器)
前面已经得到SSDT表的起始地址了。要是在32位下,只需这样一行代码,就可以进行HOOK了:
ssdt[nIndex] = hookXXX; // hookXXX,自己的代理函数地址。
在64位下又是怎么样一番景象呢?直接上代码:
ssdt[nIndex] = hookXXX; // hookXXX,自己的代理函数地址。
我擦,看上去不TM一样的么....
注意,这里有个重要的知识点:在64位下,ssdt[nIndex]保存的是一个4字节长度的地址而不是8字节地址(nIndex为ssdt函数索引号)。实际上,这个地址不是ssdt的实际地址,而只是一个ssdt函数相对于ssdt的一个偏移。真实地址计算公式如下:
真实SSDT地址 = ssdt(基址) + ssdt[nIndex]>>4
至于为什么是ssdt[nIndex]>>4而不是ssdt[nIndex],只能说通过逆向发现微软的做法就是这样的.....ssdt[nIndex]>>4才是实际偏移地址......(经查证,这个四节的偏移最后四位是例程的参数个数,所以需要右移四位后取得真正的偏移)
下面贴上具体的计算实际SSDT地址的方法:
UINT64 getSsdtFunctionAddress(UINT index)
{
INT64 address = 0;
PSERVICES_DESCRIPTOR_TABLE pServiceDescriptorTable = (PSERVICES_DESCRIPTOR_TABLE)getKeServiceDescirptorTable();
PULONG ssdt = (PULONG)pServiceDescriptorTable->ServiceTableBase;
ULONG dwOffset = ssdt[index];
dwOffset >>= 4; // get real offset
address = (UINT64)ssdt + dwOffset; // get real address of function in ssdt
KdPrint(("0x%llX\n", address));
return address;
}
2.2 HOOK的手法
知道这些以后,还不能好好的进行SSDT HOOK,具体原因,直接引用Tesla.Angla的话:
“要知道,WIN64内核里每个驱动都不在同一个4GB里,而4字节的整数只能表示 4GB 的范围!所以无论你怎么修改这个值,都跳不出 ntoskrnl 的手掌心。如果你想通过修改这个值来跳转到你的代理函数,那是绝对不可能。 因为你的驱动地址不可能跟 ntoskrnl在同一个4GB里。虽然不能直接用4字节来表示自己的代理函数所在的地址, 但是还可以修改这个值。要知道在ntoskrnl有很多地方的代码通常是不会被执行的,比如 KeBugCheckEx 。所以我的办法是: 修改这个偏移地址的值,使之跳转到KeBugCheckEx ,然后在 KeBugCheckEx的头部写一个12字节的mov - jmp ,这是一个可以跨越 4GB的跳转,跳到我们函数里!”
贴上代码:
VOID initKeBugCheckEx()
{
/*
向KeBugCheckEx头部中写入的数据
48 B8 xxxx mov rax,XXXh;
FF E0 jmp rax
*/
UCHAR jmpCode[13] = "\x48\xB8\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\xFF\xE0"; //12 data
UINT64 proxyFunction;
proxyFunction = (UINT64)proxyNtOpenProcess; //自己的NtOpenProess
RtlCopyMemory(jmpCode + 2, &proxyFunction, 8);
wpOff(); // 关保护
memset(KeBugCheckEx, 0x90, 15); // 初始化15个字节为 nop
RtlCopyMemory(KeBugCheckEx, jmpCode, 12); // mov rax,XXXh;jmp rax; nop; nop;
wpOn(); // 写保护
return;
}
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课