相比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,否则会出问题。测试效果如下:
给大家看一下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有关的函数算是“废弃的内存空间”了。