效果如图所示:
无法结束进程:
CE无图标:
内存无法读取
可以看到被保护的进程无法被结束,同时在CE内无法看到图标,并且无法被读取内存。驱动的进程保护,游戏的无图标保护内存和杀软的自我保护,实际上都是一个原理。真是一招鲜吃遍天啊。
所有的实现其实都来自于一个API->ObRegisterCallbacks
。这个函数可以让你设置一个回调函数,当我们对进程或者线程的句柄进程操作时,可以在操作前和操作后拿到进程相关的信息,从而实现句柄降权,这样就可以阻止掉需要使用句柄的相关操作,例如读写内存和结束进程。
函数原型如下:
NTSTATUS ObRegisterCallbacks(
[in] POB_CALLBACK_REGISTRATION CallbackRegistration,
[out] PVOID *RegistrationHandle
);
我们来看下这个关键的结构体
typedef struct _OB_CALLBACK_REGISTRATION {
USHORT Version;
USHORT OperationRegistrationCount;
UNICODE_STRING Altitude;
PVOID RegistrationContext;
OB_OPERATION_REGISTRATION *OperationRegistration;
} OB_CALLBACK_REGISTRATION, *POB_CALLBACK_REGISTRATION;
ObGetFilterVersion()
函数获得OperationRegistration
的数量来看一下这个结构体的成员
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;
PsProcessType
,要么是PsThreadType
接下来按照这个步骤来写代码
OB_OPERATION_REGISTRATION
结构体OB_CALLBACK_REGISTRATION
结构体ObRegisterCallbacks
注册回调函数首先初始化OB_OPERATION_REGISTRATION
结构体
OB_OPERATION_REGISTRATION obOperationRegistrations;
obOperationRegistrations.ObjectType = PsProcessType;
obOperationRegistrations.Operations |= OB_OPERATION_HANDLE_CREATE;
obOperationRegistrations.Operations |= OB_OPERATION_HANDLE_DUPLICATE;
obOperationRegistrations.PreOperation = PreOperationCallback;
obOperationRegistrations.PostOperation = NULL;
这个结构可以注册多个,我们只注册一个,处理进程句柄就够了。操作后的回调我们也不需要,直接填NULL
然后再来初始化第二个结构体OB_CALLBACK_REGISTRATION
OB_CALLBACK_REGISTRATION obCallbackRegistration = { 0 };
UNICODE_STRING altitude = { 0 };
RtlInitUnicodeString(&altitude, L"1000");
obCallbackRegistration.Version = ObGetFilterVersion();
obCallbackRegistration.OperationRegistrationCount = 1;
obCallbackRegistration.RegistrationContext = NULL;
obCallbackRegistration.Altitude = altitude;
obCallbackRegistration.OperationRegistration = &obOperationRegistrations;
最后再来调用API注册回调
ObRegisterCallbacks(&obCallbackRegistration, &g_RegistrationHandle);
这里延伸一下:ObRegisterCallbacks
这个函数是有校验的
里面会调用MmVerifyCallbackFunction
这个函数进行校验
校验的方法是判断 DriverSection 里面的一个字段是否包含了0x20,所以我们只要把这个字段跟0x20按位与就行了。
另外这个句柄需要放到全局变量里面,方便卸载的时候用,如果卸载驱动的时候没取消回调的注册,会蓝屏!
PVOID g_RegistrationHandle = NULL;
然后再来编写我们的回调函数
OB_PREOP_CALLBACK_STATUS
PreOperationCallback(_In_ PVOID RegistrationContext,
_Inout_ POB_PRE_OPERATION_INFORMATION PreInfo)
{
//获取操作的进程对象
PEPROCESS process = (PEPROCESS)PreInfo->Object;
//获取进程名
PUCHAR processName = PsGetProcessImageFileName(process);
if (_stricmp((char*)processName, "Notepad.exe") != 0)
{
//不是我们关心的进程,直接return
return OB_PREOP_SUCCESS;
}
if (PreInfo->Operation == OB_OPERATION_HANDLE_CREATE)
{
PreInfo->Parameters->CreateHandleInformation.DesiredAccess=0;
}
if (PreInfo->Operation == OB_OPERATION_HANDLE_DUPLICATE)
{
PreInfo->Parameters->DuplicateHandleInformation.DesiredAccess =0;
}
return OB_PREOP_SUCCESS;
}
在回调函数里我们判断是否是我们关心的进程,如果不是,直接把所有权限抹掉。如果要实现游戏的效果,抹掉单个权限,比如禁用掉读写,就可以借助下面这些宏
#define PROCESS_TERMINATE (0x0001)
#define PROCESS_CREATE_THREAD (0x0002)
#define PROCESS_SET_SESSIONID (0x0004)
#define PROCESS_VM_OPERATION (0x0008)
#define PROCESS_VM_READ (0x0010)
#define PROCESS_VM_WRITE (0x0020)
#define PROCESS_DUP_HANDLE (0x0040)
#define PROCESS_CREATE_PROCESS (0x0080)
#define PROCESS_SET_QUOTA (0x0100)
#define PROCESS_SET_INFORMATION (0x0200)
#define PROCESS_QUERY_INFORMATION (0x0400)
#define PROCESS_SUSPEND_RESUME (0x0800)
#define PROCESS_QUERY_LIMITED_INFORMATION (0x1000)
#define PROCESS_SET_LIMITED_INFORMATION (0x2000)
注册完成之后,在PCHunter的Object钩子这一栏,显示挂钩函数->PreOperation,PCHunter直接标红是可以查到的,还能看到挂钩的驱动名
既然知道了原理,那想要摘除就很简单了,能防自然也能攻。
OB_PREOP_CALLBACK_STATUS preCallback(
_In_ PVOID RegistrationContext,
_Inout_ POB_PRE_OPERATION_INFORMATION PreInfo
)
{
if (PreInfo->Operation == OB_OPERATION_HANDLE_CREATE)
{
PreInfo->Parameters->CreateHandleInformation.DesiredAccess = PROCESS_ALL_ACCESS;
PreInfo->Parameters->CreateHandleInformation.OriginalDesiredAccess = PROCESS_ALL_ACCESS;
}
else
{
PreInfo->Parameters->DuplicateHandleInformation.DesiredAccess = PROCESS_ALL_ACCESS;
PreInfo->Parameters->DuplicateHandleInformation.OriginalDesiredAccess = PROCESS_ALL_ACCESS;
}
return OB_PREOP_SUCCESS;
}
还是一样的代码,改一下回调,不管什么进程,都把句柄权限开到最大即可。
关闭杀软:
游戏破图标:
既然破图标的操作这么简单粗暴,那么游戏自然不会坐视不管。在下了Object钩子之后,还会增加一层保险,就是剥夺所有线程和进程的游戏操作权限
VOID StripHandlePermission()
{
__try
{
CheckDebugPort(g_FlagProcessPid);
CheckDebugPort((HANDLE)g_StarterPid);
PSYSTEM_HANDLE_INFORMATION_EX HandleInfo = QueryHandleTable();
if (HandleInfo) {
for (int i = 0; i < HandleInfo->NumberOfHandles; i++)
{
//7 是 process 属性
if (HandleInfo->Information[i].ObjectTypeNumber == 7 || HandleInfo->Information[i].ObjectTypeNumber == OB_TYPE_INDEX_PROCESS || HandleInfo->Information[i].ObjectTypeNumber == OB_TYPE_INDEX_THREAD)
{
if (g_FlagProcessPid == (HANDLE)-1)
break;
if (HandleInfo->Information[i].ProcessId == (ULONG)g_FlagProcessPid || HandleInfo->Information[i].ProcessId == 4)
continue;
bool bCheck = ((HandleInfo->Information[i].GrantedAccess & PROCESS_VM_READ) == PROCESS_VM_READ ||
(HandleInfo->Information[i].GrantedAccess & PROCESS_VM_OPERATION) == PROCESS_VM_OPERATION ||
(HandleInfo->Information[i].GrantedAccess & PROCESS_VM_WRITE) == PROCESS_VM_WRITE);
PEPROCESS pEprocess = (PEPROCESS)HandleInfo->Information[i].Object;
if (pEprocess) {
HANDLE handle_pid = *(PHANDLE)((PUCHAR)pEprocess + g_OsData.UniqueProcessId);
HANDLE handle_pid2 = *(PHANDLE)((PUCHAR)pEprocess + g_OsData.InheritedFromUniqueProcessId);
if (bCheck && (handle_pid == g_FlagProcessPid || handle_pid2 == g_FlagProcessPid)) {
pEprocess = NULL;
NTSTATUS status = PsLookupProcessByProcessId((HANDLE)HandleInfo->Information[i].ProcessId, &pEprocess);
if (NT_SUCCESS(status)) {
//DebugPrint("Full Acess Handle! pid: %d \n", HandleInfo->Information[i].ProcessId);
PHANDLE_TABLE HandleTable = *(PHANDLE_TABLE*)((PUCHAR)pEprocess + g_OsData.ObjTable);
if (MmIsAddressValid((void*)HandleTable)) {
ExEnumHandleTable(HandleTable, g_isWin7 ? (DWORD64*)&StripHandleCallback_win7 : (DWORD64*)&StripHandleCallback_win10, (PVOID)HandleInfo->Information[i].Handle, NULL);
}
ObDereferenceObject(pEprocess);
}
}
}
}
}
ExFreePoolWithTag(HandleInfo, POOL_TAG);
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return;
}
}
这段代码实现的功能就是枚举进程列表,然后遍历所有进程的私有句柄表,查看句柄类型为7的进程对象,如果这个进程对象的ID是游戏的ID,那么说明游戏句柄被我正在遍历的这个进程打开了,就把这个句柄的读写权限抹除。这样可以起到保护游戏进程的作用
游戏完全可以创建一个线程,定时去执行这个操作。这样的话即使我们的回调恢复了句柄完整的属性,也无济无事,还需要把这个部分给处理掉。
如果把恢复权限的回调代码也定时执行的话就会出现争抢句柄权限的局面,导致游戏的内存一闪一闪,这样做并不能完全解决问题。
那么如果我们想要防止游戏定时把我们的句柄降权,可以采取的方法如下:
1.构建EPROCESS结构。将目标进程的EPROCESS结构复制到我们构建的那块内存中。
2.将新构建的EPROCESS结构中的PID、进程名清空或修改。
3.将目标进程的第一级页表的内容复制到一块新内存中,并将新构建的EPROCESS结构的DirectoryTableBase改为新内存的物理地址,从而实现CR3的替换。
4.修改我们自身进程的私有句柄表中的目标进程的句柄结构的Object为新构建的EPROCESS结构。
5.此时我们再拿着新的句柄操作的进程与目标进程是同一个进程空间,但是指向的进程结构却是不同的。
这个步骤可能不太好理解,这里我画了个图:
以上两步实现了一个EPROCESS结构的替换,因为CR3变了所以不管你怎么操作原来的EPROCESS结构体,那么修改的也不是真正的游戏进程了。
由于我们的CE还需要去操作游戏内存,所以需要把这一份新的游戏进程的EPROCESS的地址放到CE进程的句柄表内,也就是第4步的操作。
这样的话就实现了句柄防止降权,也就实现了提权操作。还有一种方法是把私有句柄表的链表给摘掉,这样的话也可以防止降权。
杀软的自保也是通过ObRegisterCallbacks
实现的,这个操作基本可以防住大部分的R3的病毒。
但是在一些系统进程中会有完整的读写权限的句柄,比如lsass.exe
,csrss.exe
,只要把这个拥有完整权限的句柄复制一份,就可以直接在三环关闭杀软了。相关的代码各位可以百度,网上肯定有现成的。
既然我们下的Object回调PCHunter直接标红,那么别人查起来也不难。有这么一个操作可以隐藏自己设置的回调,就是把进程保护回调直接注册到ntoskerl.exe,让操作系统的内核模块帮我们保护进程。这样的话在PCHunter里面就是没有任何异常的,而且别人想查起来也很难。这个操作是之前看某个驱动大佬的课程学习到的,当时直呼牛逼。具体细节就不展开了。
https://download.csdn.net/download/qq_38474570/87085676?spm=1001.2014.3001.5501