SSDT Hook—— 本质上和inline hook没有区别,无非是在内核层面而已!注意Windows Vista X64 驱动需要签名或者绕过PG,32位可以随便用,从21年freebuf的文章看,恶意软件还是使用很多
SSDT Hook属于内核层Hook,也是最底层的Hook。由于用户层的API最后实质也是调用内核API(Kernel32->Ntdll->Ntoskrnl),所以该Hook方法最为强大。不过值得注意的是
https://bbs.pediy.com/thread-187613.htm win7以上64位下驱动需要签名,32 随便吧==》x86可以 . x64需要过PG 可以参考紫水晶的一篇帖子,通过已签名的驱动加载未签名驱动,然后任意执行..
查了下:为了确保系统的安全性与稳定性,微软从 Windows Vista X64 开始对系统内核增加了一定的限制,其主要增加了两种保护措施,一是KPP (内核补丁保护),KPP是机制其利用了PG(PatchGuard)技术,PG技术在x64系统下加入了内核哨兵,用于检测系统内核是否被恶意篡改(打补丁),如果发现被打了补丁,则会导致关键结构损毁直接蓝屏,二是DSE (驱动强制签名),DSE技术则是拒绝加载不包含正确签名的驱动。
这里有说明:https://www.cnblogs.com/LyShark/p/11639533.html
6.1SSDT原理
内核通过SSDT(System Service Descriptor Table)调用各种内核函数,SSDT就是一个函数表,只要得到一个索引值,就能根据这个索引值在该表中得到想要的函数地址。
下图0x80563520处就是ntoskrnl对应的服务描述符表结构SSDT。那么第一个32位的0x804e58a0则是SSDT Base,即SSDT的首地址。
通过对这些地址反汇编,就能得到相应的函数,下图中0x80591bfb是SSDT表中的第一个函数NtAcceptConnectPort的地址。
我们接下来试着寻找NtQuerySystemInformation的地址,首先反汇编ZwQuerySystemInformation,得知它要寻找SSDT中索引号为0xAD的地址。
从上面我们可以知道,NtQuerySystemInformation的索引号为0xAD,那么我们就可以算出NtQuerySystemInformation的地址:
0x80591bfb + 0xAD = 0x8056ff1
6.2SSDT Hook
其实内核层Hook并没想象中的那么高大上,Hook的原理相同,只不过Hook的对象不一样罢了。Hook步骤还是那5步:
1.修改内存属性为RWX。
2.拼接汇编码jmp [HookFunc]。
3.保存原代码头5个字节。
4.将头5个字节替换为2的汇编码。
5.恢复前5个字节。
6.恢复内存属性。
搜了下21年freebuf的文章,看ssdt这种hook的rootkit究竟在恶意软件里的现状是如何的?
4.从可行性来看Windows Rootkit
前面的内容提到,Windows引入了两大安全机制来对抗Rootkit,分别是签名验证和PatchGuard,我们将针对这两个点分别展开讨论。
4.1签名验证
关于这部分内容,国外安全研究员Bill Demirkapi在Black Hat 2021的议题《Demystifying Modern Windows Rootkits》中给出了答案,相应的解决方案分别为直接购买、滥用泄露证书和寻找“0day”驱动。
4.1.1 购买证书
这种方式其实没什么好说的,攻击者唯一需要考虑的问题,就是购买渠道是否足够可靠,是否存在身份暴露的风险。
4.1.2 滥用泄露证书
从可行性上来说,Windows根本不关心证书是否已经过期或者已经被吊销,通过泄露的证书,攻击者就可以生成在任意Windows版本下都有效的驱动签名
由于不需要购买证书,在降低成本的同时也避免了因购买渠道不可靠而暴露身份的风险,此外,通过这种方式进行植入所需的前置条件也不算多,与挖掘“0day”驱动的方式相比,技术难度降低很多,当然,掌握了泄露证书的情报后,相关安全厂商可以针对此类Rootkit进行查杀拦截
下图是收集到的一些历史泄露证书,从此图可以看出泄露的情报并不少见
4.1.3 “0day”驱动利用
从可行性来说,一定存在着可被利用的“0day”驱动,而历史上,就曾有知名的APT组织利用具有合法签名驱动程序来进行恶意驱动的加载,该组织是俄罗斯APT黑客组织Turla,它利用的合法驱动为VirtualBox,下文是对该利用过程的描述
4.2 PatchGuard
网上有着包含win7、win10在内的不少开源项目,攻击者可通过集成这些项目绕过PatchGuard,往内核中植入恶意代码,实现Rootkit功能
5.从现状来看Windows Rootkit
当我们尝试在VT上进行Hunting,会发现无效证书的利用非常普遍
其实,就算你遇到一个有着合法签名的Rootkit也不算什么新鲜事了
回过头来单看2021,Windows Rootkit攻击更多地集中在游戏行业(我想,这也是它们相对而言较快暴露的一个原因,传播量变大的同时,也遭受了更多的关注),但当Rootkit对准更高价值的目标时,当它们的目的不再是简单地获利时,当它们的动静更小,隐藏更具针对性时,我们是否做好应对准备了呢?毕竟从技术角度而言,APT组织又有什么理由拒绝Rootkit呢?
值得注意的是,当APT组织拿起Rootkit这个武器时,它们要对准的将会是包括政府、军事在内的各种重要组织机构,它们的目的将不再是简单地获利,而是对目标地长期监控和重要情报的窃取,这一点从历史APT运用Rootkit进行的攻击事件中不难发现。
windows SSDT和驱动保护
对于windwos逆向人员来说,不论是写外挂、写病毒/木马,都需要打开其他内存的空间,改写某些关键数据,达到改变其原有执行流程的目的。那么日常的工作肯定涉及到openprocess、readprocessmemory、writeprocessmemory等函数;这些函数都是怎么被调用的了?
1、windows提供了大量的系统函数供3层的应用调用。这些函数被统一编号,入口地址放在一张表里,编号就是索引,通过编号就能找到函数的入口地址;64位的表结构可以通过windbg查看,如下:
x64的SSDT表和32位比复杂了一些,为了便于读者理解,我用不同颜色(黄、绿、蓝、橙)做了标注;开发人员在3环调用openprocess、readprocessmemory、writeprocessmemory等函数,最终都会通过这个表找到对应的内核入口地址,进而跳转到内核空间执行;具体的函数实现可以通过逆向ntdll.dll、kerner32.dll、ntoskrl.exe等内核文件查看,这里不赘述(SSDT hook已经烂大街了,google一下资料大堆);各大厂商最初的驱动保护就是hook SSDT表的关键函数,一旦发现第三方程序打开自己的进程,直接返回false,达到保护自己进程数据不被篡改的目的;今天演示一下hook terminalprocess函数,让其无法关闭计算器或记事本的进程;
2、通过微软官网查询得知:windwos提供的terminalProcess函数在kerner32.dll中:
用IDA打开kernerl32.dll,切换到import,发现terminalProcess是从ntdll.dll导入的
继续追查ntdll.dll,在export找到目标函数,双击进入函数体,如下:
这个函数有两个重要信息:
(1)mov eax, 2Ch: 2c=44,是系统调用号(同一函数在windwos不同版本的调用号是不一样的,我刚开始做实验时总是蓝屏,调试了好长时间才发现是调用号搞错了),也就是terminalprocess在SSDT中的编号,根据这个编号就能找到函数的入口地址(当然不是直接现成地展示在表内,而要经过一些简单地计算)
(2)通过syscall进入内核
3、核心代码(下面的参考【3】);注意:本人的测试环境是win10.0.16299.125,调用号是0x2c;其他版本的系统可能不一样,建议读者自己用IDA查查ntdll.dll,否则直接蓝屏;
#include "hook.h"
#include "asmUtil.h"
PSYSTEM_SERVICE_TABLE KeServiceDescriptorTable;
NTTERMINATEPROCESS NtTerminateProcess = NULL;
ULONG OldTpVal;
/*
用户点击关闭,系统会调用原NtTerminateProcess,并传递ProcessHandle和ExitStatus两个参数;但SSDT已经被改成了KeBugCheckEx,所以
会先执行KeBugCheckEx。进入后又执行jmp,跳转到我们自己定义的Fake_NtTerminateProcess。这时EIP变了好几次,但是堆栈一直没变,所以
Fake_NtTerminateProcess的参数就是原NtTerminateProcess的参数ProcessHandle和ExitStatus;所以后续也能重新调回NtTerminateProcess
走原来正常的流程;
*/
NTSTATUS __fastcall Fake_NtTerminateProcess(IN HANDLE ProcessHandle, IN NTSTATUS ExitStatus)
{
//Dbg_Break();
PEPROCESS Process;
// 通过进程句柄来获取该进程所对应的FileObject对象,由于这里是进程对象,自然获得的是EPROCESS对象
NTSTATUS st = ObReferenceObjectByHandle(ProcessHandle, 0, *PsProcessType, KernelMode, &Process, NULL);
DbgPrint("\r\n-------Fake_NtTerminateProcess called! NT_SUCCESS(st):% d------------------------\r\n",NT_SUCCESS(st));
DbgPrint("\r\n-------Fake_NtTerminateProcess called! st:% d------------------------\r\n", st);
if (NT_SUCCESS(st)) //#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
{
//if (!_stricmp(PsGetProcessImageFileName(Process), "Calculator.exe"))
//if (strcmp(PsGetProcessImageFileName(Process), "Calculator.exe") == 0)
DbgPrint("\r\n-------PsGetProcessImageFileName(Process):% s------------------------\r\n", PsGetProcessImageFileName(Process));
if ((!_stricmp(PsGetProcessImageFileName(Process), "Calculator.exe"))
|| (!_stricmp(PsGetProcessImageFileName(Process), "notepad.exe")))
{
//ObDeReferenceObject(&Process);
return STATUS_ACCESS_DENIED;
}
else
{
//ObDeReferenceObject(&Process);
/*这个已经被挂钩了,会不会形成死循环????*/
return NtTerminateProcess(ProcessHandle, ExitStatus);
}
}
else
{
return STATUS_ACCESS_DENIED;
}
}
/*关闭内核页面写保护*/
KIRQL WPOFFx64()
{
KIRQL irql = KeRaiseIrqlToDpcLevel();
UINT64 cr0 = __readcr0();
cr0 &= 0xfffffffffffeffff;
__writecr0(cr0);
_disable();
return irql;
}
/*打开内核页面写保护*/
void WPONx64(KIRQL irql)
{
UINT64 cr0 = __readcr0();
cr0 |= 0x10000;
_enable();
__writecr0(cr0);
KeLowerIrql(irql);
}
// win10的变了,用下面的替代
ULONGLONG GetKeServiceDescriptorTable64_win10()
{
PUCHAR StartSearchAddress = (PUCHAR)__readmsr(0xC0000082);
PUCHAR EndSearchAddress = StartSearchAddress + 0x500;
PUCHAR i = NULL;
UCHAR b1 = 0, b2 = 0, b3 = 0;
ULONG templong = 0;
ULONGLONG addr = 0;
for (i = StartSearchAddress; i < EndSearchAddress; i++)
{
if (MmIsAddressValid(i) && MmIsAddressValid(i + 1) && MmIsAddressValid(i + 2))
{
b1 = *i;
b2 = *(i + 1);
b3 = *(i + 2);
if (b1 == 0x4c && b2 == 0x8d && b3 == 0x15) //4c8d15
{
memcpy(&templong, i + 3, 4);
addr = (ULONGLONG)templong + (ULONGLONG)i + 7;
return addr;
}
}
}
return 0;
}
/*
根据调用号找到目标内核函数地址
kd> x nt!KeServiceDescriptorTable
fffff803`4e9a1880 nt!KeServiceDescriptorTable = <no type information>
kd> dq fffff803`4e9a1880
fffff803`4e9a1880 fffff803`4e839c10 00000000`00000000
fffff803`4e9a1890 00000000`000001d0 fffff803`4e83a354
注意事项:
1、这4个都是指针,都是8字节的;
ServiceTableBase:fffff803`4e839c10
ServiceCounterTableBase:00000000`00000000
NumberOfServices:00000000`000001d0
ParamTableBase:fffff803`4e83a354
2、ServiceTableBase存储的是4字节的偏移:
(2.2) 第0x2c=44号函数NtTerminateProcess偏移:
kd> dd fffff803`4e839c10+0x29*4
fffff803`4e839cb4 fd9c8d00 01a27c00 01a99001 02150f00;注意低位在后面
0x29函数偏移:fffff803`4e839c10 + 02150f00>>4 =fffff803`4e839c10 + 2150F0 = FFFF F803 4EA4 ED00,和下面NtTerminateProcess的起始地址是吻合的:
kd> u nt!NtTerminateProcess
nt!NtTerminateProcess:
fffff803`4ea4ed00 4c8bdc mov r11,rsp
fffff803`4ea4ed03 49895b10 mov qword ptr [r11+10h],rbx
fffff803`4ea4ed07 49897320 mov qword ptr [r11+20h],rsi
*/
ULONGLONG GetSSDTFuncCurAddr(ULONG id)
{
LONG dwtmp = 0;
PULONG ServiceTableBase = NULL;
ServiceTableBase = (PULONG)KeServiceDescriptorTable->ServiceTableBase;
dwtmp = ServiceTableBase[id];
dwtmp = dwtmp >> 4;
return (LONGLONG)dwtmp + (ULONGLONG)ServiceTableBase;
}
/*
(2.3)反过来求偏移
(2.3.1)kd> u nt!NtTerminateProcess
nt!NtTerminateProcess:
fffff803`4ea4ed00 4c8bdc mov r11,rsp
(2.3.2)差距:
nt!NtTerminateProcess:fffff803`4ea4ed00 - ServiceTableBase:fffff803`4e839c10 = 21 50F0
(2.3.3)偏移:
21 50F0 << 4 = 215 0F00
*/
ULONG GetOffsetAddress(ULONGLONG FuncAddr)
{
ULONG dwtmp = 0;
PULONG ServiceTableBase = NULL;
ServiceTableBase = (PULONG)KeServiceDescriptorTable->ServiceTableBase;
dwtmp = (ULONG)(FuncAddr - (ULONGLONG)ServiceTableBase);
return dwtmp << 4;
}
/*
SSDT在ntoskrnl中;内核函数和用户自己的驱动不在一个4GB空间,32位的偏移是直接跳不过去的;
修改这个偏移地址的值,使之跳转到 KeBugCheckEx ,然后在 x KeBugCheckEx
的头部写一个 2 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";
/*通过jmp跳转,而不是call,可以让Fake_NtTerminateProcess直接利用原NtTerminateProcess
留下的参数*/
myfun = (ULONGLONG)Fake_NtTerminateProcess;
memcpy(jmp_code + 2, &myfun, 8);
irql = WPOFFx64();
memset(KeBugCheckEx, 0x90, 15);
memcpy(KeBugCheckEx, jmp_code, 12);
WPONx64(irql);
}
/*
填写KeBugCheckEx的地址
在KeBugCheckEx填写jmp,跳到Fake_NtTerminateProcess
不能直接填写Fake_NtTerminateProcess的地址,因为它们不再同一个4GB
*/
VOID HookSSDT(PSYSTEM_SERVICE_TABLE received)
{
KIRQL irql;
ULONGLONG dwtmp = 0;
PULONG ServiceTableBase = NULL;
KeServiceDescriptorTable = received;
//get old address
//Dbg_Break();
NtTerminateProcess = (NTTERMINATEPROCESS)GetSSDTFuncCurAddr(44);
DbgPrint("\r\n------------------------Old_NtTerminateProcess: %llx-----------------------\r\n", (ULONGLONG)NtTerminateProcess);
//set kebugcheckex
//Dbg_Break();
FuckKeBugCheckEx();
//show new address
ServiceTableBase = (PULONG)KeServiceDescriptorTable->ServiceTableBase;
//OldTpVal = ServiceTableBase[41]; //win7编号是0x29 = 41
OldTpVal = ServiceTableBase[44]; //win10逆向ntdll的时候发现编号是0x2c = 44
irql = WPOFFx64();
/*
我们挂钩的函数是KeBugCheckEx,所以把该函数的偏移算出来(只有32位,在4GB内)
把SSDT原本terminalProcess的地方替换掉(都在SSDT,在同一个4GB范围内)
这样一旦调用terminalProcess,实际会调用KeBugCheckEx,然后再到我们自己的代码;
*/
ServiceTableBase[44] = GetOffsetAddress((ULONGLONG)KeBugCheckEx);
WPONx64(irql);
DbgPrint("\r\n------------------------KeBugCheckEx: %llx-----------------------\r\n", (ULONGLONG)KeBugCheckEx);
DbgPrint("\r\n------------------------New_NtTerminateProcess: %llx-----------------------\r\n", GetSSDTFuncCurAddr(44));
}
VOID UnhookSSDT()
{
KIRQL irql;
PULONG ServiceTableBase = NULL;
ServiceTableBase = (PULONG)KeServiceDescriptorTable->ServiceTableBase;
//set value
irql = WPOFFx64();
ServiceTableBase[44] = GetOffsetAddress((ULONGLONG)NtTerminateProcess); //OldTpVal;//直接填写这个旧值也行
WPONx64(irql);
//没必要恢复KeBugCheckEx的内容了,反正执行到KeBugCheckEx时已经完蛋了。
DbgPrint("\r\n------------------------NtTerminateProcess: %llx-----------------------\r\n", GetSSDTFuncCurAddr(44));
}
ULONGLONG SearchforKeServiceDescriptorTable64(ULONGLONG StartSearchAddress, ULONGLONG EndSearchAddress)
{
UCHAR b1 = 0, b2 =