探索黑客技术攻防,实战研究与安全创新

导航菜单

在WIN6上实现SSDT

相比WIN32,WIN64的SSDT发生了较大的变化,在WIN32下挂钩SSDT的代码已经不能在 WIN64下使用了。在网上,也没有关于挂钩WIN64  SSDT的任何 SRC或者BIN,但是各种“在WIN64上不能挂钩SSDT”的言论却占据主流,包括国内某些所谓的“大牛”也这么说。

不过,我偏偏不信这个邪,经过较长一段时间的研究,终于研究出了挂钩SSDT的方法,在此与大家分享一下。至于如何加载无签名驱动,如何破解 PatchGuard,就请参看我以前的文章,这里不再赘述了。

要挂钩SSDT,必然要先找出SSDT的基址,这个我们已经在前几期的文章实现了。找出了SSDT的基址后,就能得到ServiceTableBase的地址。要知道,和SSDT相关的两个结构体SYSTEM_SERVICE_TABLE以及SERVICE_DESCRIPTOR_TABLE并没有发生太大的变化:


typedef struct _SYSTEM_SERVICE_TABLE{
PVOID
PVOID
ServiceTableBase;
ServiceCounterTableBase;
ULONGLONG   NumberOfServices;
PVOID
ParamTableBase;
} SYSTEM_SERVICE_TABLE, *PSYSTEM_SERVICE_TABLE;
typedef struct _SERVICE_DESCRIPTOR_TABLE{
SYSTEM_SERVICE_TABLE ntoskrnl;  // ntoskrnl.exe (native api)
SYSTEM_SERVICE_TABLE win32k;
SYSTEM_SERVICE_TABLE Table3;
SYSTEM_SERVICE_TABLE Table4;
// win32k.sys   (gdi/user)
// not used
// not used
}SERVICE_DESCRIPTOR_TABLE,*PSERVICE_DESCRIPTOR_TABLE;


得到ServiceTableBase的地址后,就能得到每个服务函数的地址了。但和WIN32不一样,这个表存放的并不是 SSDT函数的完整地址,而是其相对于   ServiceTableBase[Index]4的数据(我称它为偏移地址),每个数据占四个字节,所以计算指定Index函数完整地址的公式是:ServiceTableBase[Index]4 + ServiceTableBase。然而,我后来发现,有时候用这个公式取出来的地址是错误的,但是差别不大,总是第24~31位出错,本来该是0的,却出现了1,所以公式要改成:函数地址 = (ServiceTableBase[Index]4 + ServiceTableBase)  0xFFFFFFFF0FFFFFFF;

代码如下:


ULONGLONG GetSSDTFuncCurAddr(ULONG id)
{
ULONG dwtmp=0;
PULONG ServiceTableBase=NULL;
ServiceTableBase=(PULONG)KeServiceDescriptorTable-ServiceTableBase;
dwtmp=ServiceTableBase[id];
dwtmp=dwtmp4;
return
((ULONGLONG)(dwtmp)
+
(ULONGLONG)ServiceTableBase)0xFFFFFFFF0FFFFFFF;
}


反之,从函数的完整地址获得函数偏移地址的代码也就出来了:


ULONG GetOffsetAddress(ULONGLONG FuncAddr)
{
ULONG dwtmp=0;
PULONG ServiceTableBase=NULL;
ServiceTableBase=(PULONG)KeServiceDescriptorTable-ServiceTableBase;
dwtmp=(ULONG)(FuncAddr-(ULONGLONG)ServiceTableBase);
return dwtmp4;
}


知道了这一套机制,HOOKSSDT就很简单了,首先要知道待HOOK函数的序号Index,然后通过公式把自己的代理函数的地址转化为偏移地址,然后把偏移地址的数据填入ServiceTableBase[Index]。也许有些读者看到这里,已经觉得胜利在望了,我当时也是如此,也就是在这里,我栽了个大跟头,整整郁闷了两天两夜!因为我低估了设计这套算法的工程师的智商,我没有考虑一个问题,为什么WIN64的SSDT表存放地址的形式这么奇怪?

只存放偏移地址,而不存放完整地址?难道是为了节省内存?这肯定是不可能的,要知道现在单根4GBDDR31333的金士顿内存大约20美元,绝对的白菜价。那么不是为了节省内存,唯一的可能性就是要给试图挂钩SSDT的人制造麻烦!

没错,就是这个目的。要知道,WIN64内核里每个驱动都不在同一个4GB里,而4字节的整数只能表示4GB的范围!所以无论你怎么修改这个值,都跳不出ntoskrnl的手掌心。如果你想通过修改这个值来跳转到你的代理函数,那是绝对不可能的。因为你的驱动的地址不可能跟ntoskrnl在同一个4GB里。然而,这位工程师也低估了我们中国人的智商,在中国有两句成语,这位工程师一定没听过,叫“明修栈道,暗渡陈仓”以及“上有政策,下有对策”。虽然不能直接用4字节来表示自己的代理函数所在的地址,但是还是可以修改这个值的。要知道,ntoskrnl虽然有很多地方的代码是不会被执行的,比如 KeBugCheckEx。所以我的办法是:修改这个偏移地址的值,使之跳转到KeBugCheckEx,然后在KeBugCheckEx的头部写一个 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;
myfun=(ULONGLONG)Fake_NtTerminateProcess;
memcpy(jmp_code+2,myfun,8);
irql=WPOFFx64();
memset(KeBugCheckEx,0x90,15);
memcpy(KeBugCheckEx,jmp_code,12);
WPONx64(irql);
}
VOID HookSSDT()
{
KIRQL irql;
ULONGLONG dwtmp=0;
PULONG ServiceTableBase=NULL;
//get old address
NtTerminateProcess=(NTTERMINATEPROCESS)GetSSDTFuncCurAddr(41);
dprintf(Old_NtTerminateProcess: %llx,(ULONGLONG)NtTerminateProcess);
//set kebugcheckex
FuckKeBugCheckEx();
//show new address
ServiceTableBase=(PULONG)KeServiceDescriptorTable-ServiceTableBase;
OldTpVal=ServiceTableBase[41];  //record old offset value
irql=WPOFFx64();
ServiceTableBase[41]=GetOffsetAddress((ULONGLONG)KeBugCheckEx);
WPONx64(irql);
dprintf(KeBugCheckEx: %llx,(ULONGLONG)KeBugCheckEx);
dprintf(New_NtTerminateProcess: %llx,GetSSDTFuncCurAddr(41));
}


在代理函数里这么写,保护名为calc.exe和loaddrv.exe的程序不被结束:


NTSTATUS  __fastcall Fake_NtTerminateProcess(IN  HANDLE  ProcessHandle,  IN
NTSTATUS ExitStatus)
{
PEPROCESS Process;
NTSTATUS   st    =    ObReferenceObjectByHandle   (ProcessHandle,    0,
*PsProcessType, KernelMode, Process, NULL);
DbgPrint(Fake_NtTerminateProcess called!);
if(NT_SUCCESS(st))
{
if(!_stricmp(PsGetProcessImageFileName(Process),loaddrv.exe)||!_stri
cmp(PsGetProcessImageFileName(Process),calc.exe))
return STATUS_ACCESS_DENIED;
else
return NtTerminateProcess(ProcessHandle,ExitStatus);
}
else
return STATUS_ACCESS_DENIED;
}


注意在代理函数一定要注明是__fastcall,否则会出问题。测试效果如下:

图片1.png

给大家看一下WINDBG里的反汇编结果(挂钩前和挂钩后):

接下来给出取消SSDT HOOK的代码,在这个代码里我没有复原KeBugCheckEx的原始内容,因为执行到KeBugCheckEx就意味着蓝屏,所以是否恢复KeBugCheckEx的原始机器码都无所谓了:


VOID UnhookSSDT()
{
KIRQL irql;
PULONG ServiceTableBase=NULL;
ServiceTableBase=(PULONG)KeServiceDescriptorTable-ServiceTableBase;
//set value
irql=WPOFFx64();
ServiceTableBase[41]=GetOffsetAddress((ULONGLONG)NtTerminateProcess);
WPONx64(irql);
//没必要恢复KeBugCheckEx的内容了,反正执行到KeBugCheckEx时已经完蛋了。
dprintf(NtTerminateProcess: %llx,GetSSDTFuncCurAddr(41));
}


关于WIN64 SSDT HOOK的内容讲完了,但我还有一句话不吐不快:SSDT  HOOK是很简单的,不知道是什么人出于什么居心把它显得很复杂。看起网上SSDTHOOK的代码,动辄几百行的代码,感觉简直是在吓唬人。现在,我用一行代码凸显出SSDT HOOK的本质:

WIN32内核:


KeServiceDescriptorTable-ServiceTableBase[Index] =代理函数绝对地址


WIN64内核:


KeServiceDescriptorTable-ServiceTableBase[Index] =代理函数偏移地址


也就是说,在WIN32下只需要四行代码即可实现SSDT  HOOK,分别是:关闭内存写保护、保存旧地址、设置新地址、打卡内存写保护。而WIN64系统显得复杂些,还需要计算偏移地址、找出一块在位于NTOSKRNL空间里的废弃内存,并在这块废弃内存里进行二次跳转才能转到自己的处理函数。

本文到此结束,如果有读者有寻找NTOSKRNL空间里的废弃内存空间的好办法,请不吝赐教。在我感觉,除了KeBugCheckEx之外,也就是KeBugCheck2、KeBugCheck3等等几个和BugCheck有关的函数算是“废弃的内存空间”了。