EQU8-内核组件分析

admin 2021年11月19日15:04:59EQU8-内核组件分析已关闭评论620 views字数 22720阅读75分44秒阅读模式

备注

原文地址:https://back.engineering/12/08/2021/

原文标题:EQU8 - Kernel Component Analysis

原文信息:2021年8月12日, 作者rec_power


免责声明

请注意,从这里开始发布的所有代码都只是经过大量美化的伪代码。对于你可能在其中发现的风格上的不一致,我想补充的是,它绝不代表程序的原始源代码。我不赞成使用从这篇博文中收集到的任何信息,为任何受EQU8保护的游戏或其模拟游戏制作/编写/开发作弊器。

这篇博文的撰写和发表仅出于教育目的。

介绍

这篇博文将是对EQU8反作弊内核驱动程序的完整分析。内核驱动程序仅由24个函数组成,其主要目标似乎是通过传统的处理复制/打开方法访问游戏内存,从而避免人们进行外部欺骗。

EQU8似乎正在成为一个传播相当广泛的反作弊软件。在这篇文章中,我们将看到它是如何工作的,以及可以做些什么来改进它。

EQU8的反作弊系统是由什么组成的?

反作弊由几个用户模式组件、服务代理和内核驱动程序组成,本文将重点介绍这些模块。如上所述,内核驱动程序负责防止外部欺骗。

内核驱动程序的位置

内核驱动程序位于(至少在我的系统上)C:WindowsSystem32driversEQU8_HELPER_36.sys中。它似乎是在您第一次运行EQU8保护的游戏时下载并安装的。

开始分析

内核驱动没有被打包,也没有被虚拟化,简单地看一下文件大小,再看看它的熵,就能马上知道。

下面是驱动的熵值([从这里生成的图表](https://servertest.online/entropy):

因此,考虑到它没有被打包也没有被虚拟化,我在IDA中加载了它,并开始了我对驱动的静态分析。传递给ExAllocatePoolWithTag的所有驱动程序分配的pooltag是8UQE。

驱动程序入口

执行从这里开始,在DriverEntry中,所有内核驱动程序的入口点。

下面是它的外观:

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
_security_init_cookie();
return RealDriverEntry(DriverObject, RegistryPath);
}

很明显,这只是编译器在这里放置的一个项目,用于初始化堆栈上的安全cookie,因此让我们看看RealDriverEntry函数中的内容:

```
NTSTATUS RealDriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
PDEVICE_OBJECT DeviceObject = nullptr;

NTSTATUS StatusCodeBuffer = BuildDosAndDeviceNameFromSessionId(RegistryPath);

if ( StatusCodeBuffer >= STATUS_SUCCESS )
{
StatusCodeBuffer = IoCreateDevice(DriverObject, 0, &DeviceName, 0x22u, 0, 1u, &DeviceObject);

if ( StatusCodeBuffer >= STATUS_SUCCESS )
{
  StatusCodeBuffer = IoCreateSymbolicLink(&SymbolicLinkName, &DeviceName);

  if ( StatusCodeBuffer >= STATUS_SUCCESS )
  {
    DriverObject->MajorFunction[IRP_MJ_DEVICE_IO_CONTROL] = MajorFunctionControl;
    DriverObject->MajorFunction[IRP_MJ_CREATE] = MajorFunctionCreate;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = MajorFunctionClose;
    DriverObject->DriverUnload = DriverUnload;
    StatusCodeBuffer = SetupHandleCallbacks();

    if ( StatusCodeBuffer >= STATUS_SUCCESS ) 
    {
      return StatusCodeBuffer;
    }

    IoDeleteSymbolicLink(&SymbolicLinkName);
  }
}

}

FreeStringBuffers();

if ( DeviceObject )
{
IoDeleteDevice(DeviceObject);
}

return StatusCodeBuffer;
}
```

驱动程序似乎只是在BuildDosAndDeviceNameFromSessionId函数中分配DeviceName和SymbolicLinkName,然后继续设置几个主要函数指针,以便能够与用户模式通信。

正如函数名称所示,BuildDosAndDeviceNameFromSessionId只是基于运行软件的计算机的会话id构建一个SymbolicLinkName和一个DeviceName。

以下是BuildDosAndDeviceNameFromSessionId函数的伪代码:

```
NTSTATUS GetDosDeviceName(PUNICODE_STRING RegistryPath)
{
_KEY_VALUE_FULL_INFORMATION KeyValueInformationStructure;
NTSTATUS StatusCode;
NTSTATUS StatusCodeBuffer;

ULONG v4;
char
v5;

ULONG v6;
_WORD v7;

unsigned int v8;
_UNICODE_STRING DestinationString;

struct _OBJECT_ATTRIBUTES ObjectAttributes;

ULONG ResultLength;

void KeyHandle;

// This is bad, it should've been initialized with INVALID_HANDLE_VALUE, not 0.
KeyHandle = 0i64;
KeyValueInformationStructure = 0i64;

if ( !RegistryPath || !RegistryPath->Buffer || !RegistryPath->Length )
{
return STATUS_INVALID_PARAMETER;
}

ObjectAttributes.ObjectName = RegistryPath;
ObjectAttributes.Length = 48;
ObjectAttributes.RootDirectory = 0i64;
ObjectAttributes.Attributes = 576;
*&ObjectAttributes.SecurityDescriptor = 0i64;
StatusCode = ZwOpenKey(&KeyHandle, 0xF003Fu, &ObjectAttributes);

if ( StatusCode < 0 )
{
FreeStringBuffers();
goto END;
}

RtlInitUnicodeString(&DestinationString, L"SessionId");
StatusCodeBuffer = ZwQueryValueKey(KeyHandle, &DestinationString, KeyValueFullInformation, 0i64, 0, &ResultLength);
StatusCode = StatusCodeBuffer;
if ( StatusCodeBuffer == STATUS_BUFFER_TOO_SMALL || StatusCodeBuffer == STATUS_BUFFER_OVERFLOW)
{
KeyValueInformationStructure = ExAllocatePoolWithTag(PagedPool, ResultLength, 0x38555145u);
if ( KeyValueInformationStructure )
{
StatusCode = ZwQueryValueKey(
KeyHandle,
&DestinationString,
KeyValueFullInformation,
KeyValueInformationStructure,
ResultLength,
&ResultLength);
if ( StatusCode < 0 )
{
FreeStringBuffers();
goto END;
}
if ( KeyValueInformationStructure->Type == 1 )
{
v4 = 0;
v5 = KeyValueInformationStructure + KeyValueInformationStructure->DataOffset;
v6 = KeyValueInformationStructure->DataLength >> 1;
if ( v6 )
{
v7 = (KeyValueInformationStructure + KeyValueInformationStructure->DataOffset);
do
{
if ( !*v7 )
break;
++v4;
++v7;
}
while ( v4 < v6 );
}
v8 = 2 * v4;
StatusCode = ConcatenateUnicodeStrings(
&DeviceName,
L"Device",
KeyValueInformationStructure + KeyValueInformationStructure->DataOffset,
2 * v4);
if ( StatusCode < 0 )
{
FreeStringBuffers();
goto END;
}

    StatusCode = ConcatenateUnicodeStrings(&SymbolicLinkName, L"\DosDevices\", v5, v8);
  }
}

}

if ( StatusCode < 0 )
{
FreeStringBuffers();
goto END;
}

END:

if ( KeyValueInformationStructure )
{
ExFreePoolWithTag(KeyValueInformationStructure, 0x38555145u);
}

// I'm fairly certain this is not how you check whether a handle has been returned or not.
// You should check if (KeyHandle != INVALID_HANDLE_VALUE).
// Furthermore, KeyHandle should have been initialized with INVALID_HANDLE_VALUE.
if ( KeyHandle )
{
ZwClose(KeyHandle);
}

return StatusCode;
}
```

FreeStringBuffers函数只是释放DeviceName和ClinkName全局变量分配的缓冲区。

以下是FreeStringBuffers的伪代码:

```
NTSTATUS FreeStringBuffers()
{
NTSTATUS result;

if ( DeviceName.Buffer )
{
ExFreePoolWithTag(DeviceName.Buffer, 0x38555145u);
result = 0;
*&DeviceName.Length = 0i64;
DeviceName.Buffer = 0i64;
}

if ( SymbolicLinkName.Buffer )
{
ExFreePoolWithTag(SymbolicLinkName.Buffer, 0x38555145u);
result = 0;
*&SymbolicLinkName.Length = 0i64;
SymbolicLinkName.Buffer = 0i64;
}

return result;
}
```

使用的另一个实用程序函数是concatenateUnicode字符串,它只是连接两个unicode字符串。

下面是ConcatenateUnicodeStrings(连接unicode字符串)的伪代码:

```
int64_t __fastcall ConcatenateUnicodeStrings(
PUNICODE_STRING BaseString,
wchar_t AdditionString,
const void
DataOffset,
unsigned int Size)
{
__int64 AdditionStringSize;
size_t SizeBuffer;
size_t RealAdditionStringByteSize;
WCHAR *PoolWithQuotaTag;

AdditionStringSize = -1i64;
SizeBuffer = Size;
do
++AdditionStringSize;
while ( AdditionString[AdditionStringSize] );
RealAdditionStringByteSize = (2 * AdditionStringSize);

if ( RealAdditionStringByteSize + Size >= 0x10000 )
{
return STATUS_BUFFER_OVERFLOW;
}

PoolWithQuotaTag = ExAllocatePoolWithQuotaTag(PagedPool, RealAdditionStringByteSize + Size, 0x38555145u);
BaseString->Buffer = PoolWithQuotaTag;

if ( !PoolWithQuotaTag )
{
return STATUS_NO_MEMORY
}

BaseString->Length = RealAdditionStringByteSize + SizeBuffer;
BaseString->MaximumLength = RealAdditionStringByteSize + SizeBuffer;
memmove(PoolWithQuotaTag, AdditionString, RealAdditionStringByteSize);
memmove(BaseString->Buffer + RealAdditionStringByteSize, DataOffset, SizeBuffer);

return 0i64;
}
```

没什么特别的,真的。但现在我们已经看到了反作弊器是如何设置的,我们可以开始更深入地了解其内部结构。

主要函数

驱动程序设置的主要函数是相当有趣的。IRP_MJ_CREATE/IRP_MJ_CLOSE处理创建和关闭设备的句柄,但仍然调节驱动程序内部的一些重要功能,我们很快就会看到,而IRP_MJ_DEVICE_CONTROL则处理发送到驱动程序的与启用/禁用功能有关的IRP,并收集存储在驱动程序中的信息。

IRP_MJ_CREATE

下面是MajorFunctionCreate创建函数的伪代码:

```
NTSTATUS MajorFunctionCreate(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
NTSTATUS StatusCode = 0;

// SeTcbPrivilege is usually possessed by processes with the power of doing things
// such as creating login tokens, running programs under the SYSTEM "account", impersonating
// other users freely, etc.
if ( !SeSinglePrivilegeCheck(RtlConvertLongToLuid(SE_TCB_PRIVILEGE), 1) )
{
StatusCode = STATUS_PRIVILEGE_NOT_HELD;
}

if ( !Irp )
{
return STATUS_UNSUCCESSFUL;
}

if ( StatusCode >= STATUS_SUCCESS )
{
// Protects the current process the request comes from if the process holds enough
// privilege to be able to open it.
StatusCode = AddCurrentProcessToProtectedProcessesList();

// Global variable
GlobalEqu8Process = IoGetCurrentProcess();
// Global variable
GlobalEqu8ProcessId = PsGetProcessId(GlobalEqu8Process);

}

Irp->IoStatus.Information = 0i64;
Irp->IoStatus.Status = StatusCode;
IofCompleteRequest(Irp, 0);

return 0;
}
```

正如我们所看到的,该函数只是对调用的进程进行了一些权限检查,然后如果有足够的权限,就将该进程添加到内核驱动内部的受保护进程列表中。保护运作的机制将在后面的文章中进行分析,现在让我们看看MajorFunctionClose函数。

特权检查似乎只是检查SE_TCB_PRIVILEGE,这是一个允许进程具有的能力,如冒充任何用户账户、创建新的登录令牌、生成在SYSTEM "账户 "下运行的进程等。

IRP_MJ_CLOSE

下面是MajorFunctionClose函数的伪代码:

```
NTSTATUS MajorFunctionClose(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
// Global variable indicating whether or not the process protections enforced via ObRegisterCallbacks callbacks have been set up.
ObRegisterCallbacksEnabled = 0;
// Global variable used in many functions as a buffer for the currently calling process.
FreeLinkedList(&CallingEprocess);
// Global variable holding the protected processes list.
FreeLinkedList(&ProtectedProcessesList);

// Global variable holding a pointer to the KPROCESS structure of the Equ8 process (a PEPROCESS).
GlobalEqu8Process = 0i64;

if ( !Irp )
{
return STATUS_UNSUCCESSFUL;
}

Irp->IoStatus.Status = 0;
Irp->IoStatus.Information = 0i64;
IofCompleteRequest(Irp, 0);
return 0;
}
```

正如我们所看到的,关闭函数只是取消了几个全局变量的初始化,然后返回。没什么特别的。它可能是在usermode服务通过IRP_MJ_DEVICE_CONTROL主要函数禁用所有其他保护措施并准备卸载驱动程序后被调用的。

IRP_MJ_DEVICE_CONTROL

这是一些操作发生的地方,它处理:

  • 正在启动ETW跟踪。
  • 保护特定进程。
  • 将记录的ObRegisterCallbacks数据从预操作功能发送到用户模式。

以下是MajorFunctionControl函数的伪代码:

```
NTSTATUS MajorFunctionControl(PDEVICE_OBJECT DeviceObject, PIRP irp)
{
_IO_STACK_LOCATION CurrentStackLocation;
uint32_t v3;
_IRP
SystemBuffer;
NTSTATUS StatusCode;
ULONG Options;
struct _KPROCESS *ProtectedProcessBuffer;
NTSTATUS StatusCodeBuffer;
uint32_t v10;

CurrentStackLocation = irp->Tail.Overlay.CurrentStackLocation;
v3 = 0;
SystemBuffer = irp->AssociatedIrp.MasterIrp;
StatusCode = 0xC0000001;
Options = CurrentStackLocation->Parameters.Create.Options;

// Enable tracing.
if ( CurrentStackLocation->Parameters.Read.ByteOffset.LowPart == 0x22E040 )
{
if ( !SystemBuffer || Options != 32 )
goto CompleteRequest;
StatusCodeBuffer = EtwEnableTrace_call(
*&SystemBuffer->Type,
SystemBuffer->MdlAddress,
HIDWORD(SystemBuffer->MdlAddress),
&SystemBuffer->Flags);
StatusCode = StatusCodeBuffer;
goto CompleteRequest;
}
if ( CurrentStackLocation->Parameters.Read.ByteOffset.LowPart != 0x22E044 )
{
// Protects a process by PID.
if ( CurrentStackLocation->Parameters.Read.ByteOffset.LowPart == 0x22E048 )
{
if ( !SystemBuffer || Options != 4 )
{
goto CompleteRequest;
}

  if ( CallbacksRegistrationHandle )
  {
    ProtectedProcessBuffer = &ProtectedProcessesList;
    StatusCodeBuffer = ProtectProcessByPID(ProtectedProcessBuffer, *&SystemBuffer->Type);
    StatusCode = STATUS_ILLEGAL_FUNCTION;
    goto CompleteRequest;
  }
}
else
{
  if ( CurrentStackLocation->Parameters.Read.ByteOffset.LowPart != 0x22E04C )
  {
    if ( CurrentStackLocation->Parameters.Read.ByteOffset.LowPart == 0x22E050 && !ObRegisterCallbacksEnabled )
    {
      ObRegisterCallbacksEnabled = 1;
      StatusCode = 0;
    }

    goto CompleteRequest;
  }

  if ( !SystemBuffer || Options != 4 ) 
  {
    goto CompleteRequest;
  }

  if ( CallbacksRegistrationHandle )
  {
    ProtectedProcessBuffer = &CallingEprocess;
    StatusCodeBuffer = AddProcessToProtectedProcessesList(ProtectedProcessBuffer, *&SystemBuffer->Type);
    StatusCode = StatusCodeBuffer;
    goto CompleteRequest;
  }

}
StatusCode = STATUS_ILLEGAL_FUNCTION;
goto CompleteRequest;

}

// Send the ob register callbacks data logged to usermode.
if ( SystemBuffer && CurrentStackLocation->Parameters.Read.Length == 420 )
{
ExAcquireFastMutex(&FastMutex);
v10 = 13 * dword_1400030F0;
*&SystemBuffer->Type = dword_1400030F0;
memmove(&SystemBuffer->Size + 1, &unk_140003100, v10);
dword_1400030F0 = 0;
v3 = v10 + 4;
ExReleaseFastMutex(&FastMutex);
StatusCode = STATUS_SUCCESS;
}

CompleteRequest:
irp->IoStatus.Information = v3;
irp->IoStatus.Status = StatusCode;
IofCompleteRequest(irp, 0);
return StatusCode;
}
```

这里发生了很多事情,我们几乎可以立即识别一些重要的IOCTL代码:

  • 0x22E050: 启用ObRegisterCallbacks进程保护(如果未启用)(实际上只翻转一个变量,真正的初始化是在SetupHandleCallbacks中的RealDriverEntry函数中完成的,我们将在后面查看。)
  • 0x22E048: 保护一个进程
  • 0x22E040: 启用ETW跟踪(可用于调试目的或欺骗检测)。

如果条件正确,每个IOCTL请求似乎都会单方面发送handle回调收集的数据(如果用户模式缓冲区足够大。)。由于我不打算启用或帮助任何人伪造信息发送到usermode模块或完全模拟内核驱动程序,因此我不会发布与将信息发送到usermode相关的美化伪代码。回调收集的信息存储在一个只有4个成员的结构中,最大大小为32以存储最后32个事件。

但是,我确实认为,提供受保护进程在内存中的布局在分析的范围内:

```

pragma pack(1)

struct ProcessEntry
{
LIST_ENTRY ListEntry;
PEPROCESS Process;
int32_t ProcessId;
};

using LPProcessEntry = ProcessEntry*;
```

设置句柄回调

现在我们已经了解了内核驱动程序如何与usermode通信以及它提供的信息,我们可以开始了解handle操作回调是如何设置的。句柄回调是在进程尝试与系统内的进程交互后直接调用的函数。由回调函数按进程进行过滤。回调可以注册为仅由系统在某些操作上调用。

以下是设置主函数指针后立即在DriverEntry中调用的SetupHandleCallbacks函数的伪代码:

```
int64_t SetupHandleCallbacks()
{
int64_t ObUnRegisterCallbacks_buffer;
int64_t result;
_OB_OPERATION_REGISTRATION OperationRegistration;
POBJECT_TYPE OperationType;
int64_t Unused2;
int64_t (__fastcall
PreHandleOperationPointerBuffer)(int64_t, POB_PRE_OPERATION_INFORMATION);
int64_t Unused;
_OB_CALLBACK_REGISTRATION CallbackRegistration;
_UNICODE_STRING DestinationString;
_UNICODE_STRING SystemRoutineName;
_UNICODE_STRING v10;
_UNICODE_STRING string_363705;

SpinLock = 0i64;
FastMutex.Owner = 0i64;
FastMutex.Contention = 0;
ProtectedProcessesList.ListEntry.Blink = &ProtectedProcessesList;
ProtectedProcessesList.ListEntry.Flink = &ProtectedProcessesList;
ProtectedProcessesList.ProcessId = &ProtectedProcessesList.Process;
ProtectedProcessesList.Process = &ProtectedProcessesList.Process;
FastMutex.Count = 1;
KeInitializeEvent(&FastMutex.Event, SynchronizationEvent, 0);

// Decrypts a seemingly normal string into another string.
if ( !DecryptFunctionName(aResolvingSyms, &XorDecryptionKey, 0xFui64) )
{
return STATUS_INFO_LENGTH_MISMATCH;
}

RtlInitUnicodeString(&DestinationString, EtwEnableTraceEncryptedString);
EtwEnableTrace_ptr = MmGetSystemRoutineAddress(&DestinationString);
RtlInitUnicodeString(&SystemRoutineName, L"ObRegisterCallbacks");
RtlInitUnicodeString(&ObUnRegisterCallbacksString, L"ObUnRegisterCallbacks");
ObRegisterCallbacks_ptr = MmGetSystemRoutineAddress(&SystemRoutineName);
ObUnRegisterCallbacks_ptr = MmGetSystemRoutineAddress(&ObUnRegisterCallbacksString);
ObUnRegisterCallbacks_buffer = ObUnRegisterCallbacks_ptr;

// Re-encrypts the decrypted string into another seemingly normal string.
if ( !DecryptFunctionName(aResolvingSyms, &XorDecryptionKey, 0xFui64) )
{
return STATUS_INFO_LENGTH_MISMATCH;
}

if ( !ObRegisterCallbacks_ptr )
{
// If the ObUnRegisterCallbacks pointer is null it sets up the CreateProcessNotifyRoutine anyway? Weird.
CreateProcessNotifyRoutineResult = PsSetCreateProcessNotifyRoutine(NotifyRoutine, 0) >= 0;
return STATUS_SUCCESS;
}

// If the ObUnRegisterCallbacks pointer is null it sets up the CreateProcessNotifyRoutine anyway? Weird.

if ( !ObUnRegisterCallbacks_buffer )
{
CreateProcessNotifyRoutineResult = PsSetCreateProcessNotifyRoutine(NotifyRoutine, 0) >= 0;
return STATUS_SUCCESS;
}

// Zero out the structures
memset(&CallbackRegistration, 0, sizeof(CallbackRegistration));
memset(&OperationRegistration, 0, sizeof(OperationRegistration));

OperationType = 0i64;
Unused2 = 0i64;
PreHandleOperationPointerBuffer = 0i64;
Unused = 0i64;
// Filter altitude.
RtlInitUnicodeString(&string_363705, L"363705");
OperationRegistration.PostOperation = 0i64;
Unused = 0i64;
CallbackRegistration.RegistrationContext = 0i64;
OperationRegistration.ObjectType = PsProcessType;
OperationType = PsThreadType;
CallbackRegistration.OperationRegistration = &OperationRegistration;
// The driver only checks for the creation and duplication of handles.
OperationRegistration.Operations = 3;
// This is the callback function that's called by the system.
OperationRegistration.PreOperation = PreHandleOperation;
LODWORD(Unused2) = 3;
// This is the callback function that's called by the system.
PreHandleOperationPointerBuffer = PreHandleOperation;
*&CallbackRegistration.Version = 0x20100;
CallbackRegistration.Altitude = string_363705;
// The function pointer is then properly called with the filled out information.
result = ObRegisterCallbacks_ptr(&CallbackRegistration, &CallbacksRegistrationHandle);

if ( result >= STATUS_SUCCESS )
{
CreateProcessNotifyRoutineResult = PsSetCreateProcessNotifyRoutine(NotifyRoutine, 0) >= STATUS_SUCCESS;
return 0i64;
}

return result;
}
```

此函数不仅设置回调,还负责获取整个程序所需的一些指针,例如EtwTraceCall指针。EtwTraceCall字符串是在使用值“Resolving Syms”解密看似无害的字符串后计算出来的。为线程和进程操作设置回调后,函数将继续设置进程创建通知例程。

函数名加密

内核驱动程序似乎包含一些字符串,这些字符串使用不同的旋转密钥加密以生成不同的字符串,从而隐藏驱动程序实际上正在调用EtwTraceCall的事实。它应该让逆向工程师思考解决问题的方法。字符串只不过是调试过程中的遗留问题,而实际上,它首先转换为RtlFormatMessage,然后转换为EtwTraceCall。

以下是调用加密/解密例程的相关伪代码:

```
// The XorDecryptionKey global variable equals 0x17.
// The aResolvingSyms string's precise value is "Resolving Syms." (in utf-16)
// Decrypts a seemingly normal string into another string.
if ( !DecryptFunctionName(aResolvingSyms, &XorDecryptionKey, 0xFui64) )
{
return STATUS_INFO_LENGTH_MISMATCH;
}

RtlInitUnicodeString(&DestinationString, EtwEnableTraceEncryptedString);
EtwEnableTrace_ptr = MmGetSystemRoutineAddress(&DestinationString);
RtlInitUnicodeString(&SystemRoutineName, L"ObRegisterCallbacks");
RtlInitUnicodeString(&ObUnRegisterCallbacksString, L"ObUnRegisterCallbacks");
ObRegisterCallbacks_ptr = MmGetSystemRoutineAddress(&SystemRoutineName);
ObUnRegisterCallbacks_ptr = MmGetSystemRoutineAddress(&ObUnRegisterCallbacksString);
ObUnRegisterCallbacks_buffer = ObUnRegisterCallbacks_ptr;

// Re-encrypts the decrypted string into another seemingly normal string.
if ( !DecryptFunctionName(aResolvingSyms, &XorDecryptionKey, 0xFui64) )
{
return STATUS_INFO_LENGTH_MISMATCH;
}
```

这是加密/解密例程的伪代码,它只是一个简单的XOR方法:

```
bool DecryptFunctionName(wchar_t message, int64_t key, uint32_t64_t size)
{
int64_t v6;
wchar_t
v7;
wchar_t *v8;
int64_t v9;
signed int v10;
int64_t v11;

if ( !message || !key )
{
return 0;
}

v6 = 0i64;
v7 = message + 16;
if ( v7 )
{
v8 = v7;
v9 = 128i64;
do
{
if ( !v8 )
break;
++v8;
--v9;
}
while ( v9 );
v10 = v9 == 0 ? 0xC000000D : 0;
v6 = v9 ? 128 - v9 : 0i64;
}
else
{
v10 = 0xC000000D;
}
if ( v10 < 0 || size > v6 + 8 )
return 0;
if ( size )
{
v11 = key - v7;
do
{
v7 ^= *(v7 + v11);
++v7;
--size;
}
while ( size );
}
return 1;
}```

Process creation routine

The notification routine for process creation simply checks whether the process was created by EQU8's launcher, and then proceeds to protect it if so, adding it to the linked list of protected processes. Otherwise, the process seems to get removed from the linked list.

Here's the pseudo-code for the NotifyRoutine:
```cpp
void NotifyRoutine(HANDLE ParentId, HANDLE ProcessId, BOOLEAN Create)
{
int ProcessIdBuffer;
KIRQL Irql;
_LIST_ENTRY ListHead;
KIRQL IrqlBuffer;
ProcessEntry
v7;
_LIST_ENTRY *Blink;

ProcessIdBuffer = ProcessId;
// Check if it's a process creation
if ( Create )
{
// Check if the parent process is Equ8's process
if ( GlobalEqu8ProcessId && GlobalEqu8ProcessId == ParentId )
{
// Protect the process then
ProtectProcessByPID(&ProtectedProcessesList, ProcessId);
}
}
// If it's not a process creation
else
{
Irql = KeAcquireSpinLockRaiseToDpc(&SpinLock);
ListHead = ProtectedProcessesList.ListEntry.Flink;
IrqlBuffer = Irql;
if ( ProtectedProcessesList.ListEntry.Flink == &ProtectedProcessesList )
{
LABEL_5:
KeReleaseSpinLock(&SpinLock, Irql);
}
else
{
// Begin iteration of the linked list
while ( 1 )
{
v7 = ListHead->Flink;
if ( ProcessIdBuffer == LODWORD(ListHead[1].Blink) )
break;
ListHead = ListHead->Flink;
if ( v7 == &ProtectedProcessesList )
goto LABEL_5;
}
if ( v7->ListEntry.Blink != ListHead || (Blink = ListHead->Blink, Blink->Flink != ListHead) )
__fastfail(3u);
// Remove the entry
Blink->Flink = &v7->ListEntry;
v7->ListEntry.Blink = Blink;
KeReleaseSpinLock(&SpinLock, IrqlBuffer);
ObfDereferenceObject(ListHead[1].Flink);
ExFreePoolWithTag(ListHead, 0x38555145u);
}
}
}
```

操作前的回调函数

最后,预处理操作函数是所有魔法发生的函数。它负责存储来自尝试打开游戏句柄的应用程序的信息,并排除句柄访问。它是EQU8保护的应用程序免受外部内存读/写欺骗的主要防线。

以下是预处理操作函数的伪代码:

```
int64_t PreHandleOperation(int64_t RegistrationContext, POB_PRE_OPERATION_INFORMATION OperationInformation)
{
uint32_t v2;
PEPROCESS CurrentProcess;
POBJECT_TYPE ObjectType;
struct _KPROCESS
Object;
char ObjectTypeData;
int AccessMaskToClear;
POB_PRE_OPERATION_PARAMETERS Parameters;
ACCESS_MASK OriginalDesiredAccess;
uint32_t ProcessId;
uint32_t CurrentProcessId;
char v13;
_DWORD v14;
char
v15;

v2 = 0;
if ( ObRegisterCallbacksEnabled )
{
if ( OperationInformation )
{
if ( OperationInformation->Object )
{
if ( OperationInformation->Parameters )
{
CurrentProcess = IoGetCurrentProcess();
if ( !IsProcessProtected(&ProtectedProcessesList.Process, CurrentProcess) )
{
ObjectType = OperationInformation->ObjectType;
if ( ObjectType == PsProcessType )
{
Object = OperationInformation->Object;
// Object type: Process
ObjectTypeData = 2;
// Flags: SYNCHRONIZE | UNKNOWN
AccessMaskToClear = 0x103601;
}
else
{
if ( ObjectType != PsThreadType )
{
return 0i64;
}
// Object type: Thread
ObjectTypeData = 4;
// Flags: SYNCHRONIZE | UNKNOWN
AccessMaskToClear = 0x100C00;
Object = PsGetThreadProcess(OperationInformation->Object);
}
if ( OperationInformation->Operation == OB_OPERATION_HANDLE_CREATE || OperationInformation->Operation == OB_OPERATION_HANDLE_DUPLICATE )
{
// For the handle to be stripped a few conditions need to be satisfied:
// the target process needs to be a protected process
// the calling process mustn't be the Equ8 process
// the calling process msutn't equal IoGetCurrentProcess()
// the desired access must equal PROCESS_VM_OPERATION
Parameters = OperationInformation->Parameters;
OriginalDesiredAccess = Parameters->CreateHandleInformation.OriginalDesiredAccess;
if ( (GlobalEqu8Process != IoGetCurrentProcess() || Parameters->CreateHandleInformation.DesiredAccess == PROCESS_VM_OPERATION)
&& Object != IoGetCurrentProcess() )
{
if ( IsProcessProtected(&ProtectedProcessesList, Object) )
{
// Strip handle accesses
Parameters->CreateHandleInformation.DesiredAccess &= AccessMaskToClear;

              // If the original access is different from the stripped one
              if ( OriginalDesiredAccess != Parameters->CreateHandleInformation.DesiredAccess )
              {
                // Prepare filling out the (unreleased) information structure you could reverse from this pseudo-code here.
                ProcessId = PsGetProcessId(Object);
                CurrentProcessId = PsGetCurrentProcessId();
                v13 = ObjectTypeData | OperationInformation->Flags & 1;

                // Acquire the mutex to start writing to the information array.
                ExAcquireFastMutex(&FastMutex);

                // Checks if the index is less than 32 (Maximum size of the information array)
                if ( NextStoredInformationIndex < 0x20 )
                {
                  // If it's more than zero, and the information is not a duplicate, store it.
                  if ( NextStoredInformationIndex )
                  {
                    v14 = &unk_140003108;
                    while ( *(v14 - 1) != CurrentProcessId || *v14 != ProcessId || *(v14 + 4) != v13 )
                    {
                      ++v2;
                      v14 = (v14 + 13);
                      if ( v2 >= NextStoredInformationIndex )
                        goto LABEL_24;
                    }
                  }
                  else
                  {

LABEL_24:
// Store the information.
v15 = &unk_140003100 + 13 * NextStoredInformationIndex++;
(v15 + 1) = CurrentProcessId;
(v15 + 2) = ProcessId;
*v15 = OriginalDesiredAccess;
v15[12] = v13;
}
}

                // Release the mutex to access the data.
                ExReleaseFastMutex(&FastMutex);
              }
            }
          }
        }
      }
    }
  }
}

}
return 0i64;
}
```

伪代码本身包含许多注释来解释正在发生的事情,但是对于那些不真正进入C++的人来说,这里是如何处理句柄的例程如何工作的...

当发生句柄创建或复制事件时,操作系统将调用该例程,然后该例程将:

  • 检查它要访问的所有指针是否有效。
  • 检查当前进程是否为受保护进程。
  • 如果是,它将存储句柄打开操作的一些基本信息,例如该操作是在线程还是进程上进行的。
  • 如果操作没有发生在线程或进程上,则回调例程将直接返回。
  • 否则,将执行句柄排除例程的核心:句柄排除逻辑。

一个句柄将被剥夺访问权限,那么:

  • 目标进程是受保护的进程。
  • 调用进程不是Equ8进程。
  • 调用进程不等于IoGetCurrentProcess()
  • 所需的访问等于PROCESS_VM_OPERATION(程虚拟机操作)。

驱动程序卸载

最后,这里是驱动程序如何卸载自己,以及它调用的所有过程,以在系统卸载之前完全关闭其功能。一切都由存储在驱动程序DriverObject中的DriverRunLoad函数指针处理。

下面是它的伪代码:

```
void DriverUnload(PDRIVER_OBJECT DriverObject)
{
_DEVICE_OBJECT *DeviceObject;

// If the notify routine is registered.
if ( CreateProcessNotifyRoutineResult )
{
// Unregister it.
PsSetCreateProcessNotifyRoutine(NotifyRoutine, 1u);
CreateProcessNotifyRoutineResult = 0;
}

// Disable the ObRegisterCallbacks-dependent features of the driver.
ObRegisterCallbacksEnabled = 0;

// Free the protected processes list.
FreeLinkedList(&ProtectedProcessesList.Process);
FreeLinkedList(&ProtectedProcessesList);

// Remove the handle stripping callbacks.
if ( CallbacksRegistrationHandle )
{
ObUnRegisterCallbacks_ptr();
}

// Delete the symbolic link.
if ( SymbolicLinkName.Buffer )
{
IoDeleteSymbolicLink(&SymbolicLinkName);
}

// Free SymbolicLinkName's and DeviceName's buffers.
FreeStringBuffers();

// Delete the device object.
if ( DriverObject )
{
DeviceObject = DriverObject->DeviceObject;
if ( DeviceObject )
{
IoDeleteDevice(DeviceObject);
}
}

// The driver gets unloaded by the system after this function returns.
}
```

同样没有什么特别的,只是释放缓冲区,删除回调,删除设备,符号链接,然后卸载自己。

总结

我发现EQU8的内核驱动程序缺乏任何形式的保护,无法抵御基于内核的攻击者。它不包含任何完整性检查,也不包含任何针对基于内核的攻击者的检测。

以下是一些可以改进其运行的措施:

  • 添加.text节完整性检查,不仅仅是其中一个,而是使用不同的方法和操作模式的几个。在usermode中映射驱动程序,让usermode模块散列.text部分,并根据最初计算的部分进行检查,这将是一个开始,同时进行简单的CRC检查,计算驱动程序内部运行的不同函数的CRC。
  • 使某些人更难篡改全局变量,实现了某种加密、卷影副本和变量的用户模式验证。
  • 检查是否有人完全替换了驱动程序应该与用户模式通信的主要函数。

关于检测内核异常:

  • 让人更难完全模拟内核驱动程序及其功能。
  • 即使使用基本的软件保护器也能保护二进制文件,强烈建议使用代码虚拟化来阻碍逆向工程。
  • 在内核中添加对易受攻击的驱动程序跟踪的某种检测,在“ntoskrnl.exe”中查询“MmUnloadedDrivers”和“PiDDBCacheTable”将是一个开始,只是为了转到“CI.dll”中的其他未记录列表,如“g_CiEaCacheLookasideList”。可能有更多的列表和更多未记录的方法来找出哪些驱动程序已经卸载到系统中,例如事件日志,但我的工作不是现在就在这里列出所有驱动程序。
  • 添加启发式方法来捕捉在常规驱动程序地址空间之外运行的系统线程,简单地迭代加载模块的列表,并检查每一个线程的起始地址是否位于有效的内存中,这将是检测与脆弱驱动程序映射的驱动程序的一个开始。
  • 等等

EQU8是一个相对较新的反作弊工具,在过去的几年里,它并没有像BattlEye和EasyAntiCheat那样被广泛采用,反作弊技术本身并没有受到太多的攻击和干扰,所以,在一定程度上,这些都是可以预期的。随着时间的推移,它提供的保护肯定会变得更好。

相关推荐: ThinkPHP3.2 二三事

前言 之前看到【漏洞通报】ThinkPHP3.2.x RCE漏洞通报只是粗略的看了下漏洞原理,看了就等于自己会了吗? 本着要对这个漏洞负责的态度,当然要实际操作一下。 漏洞概述 在受影响的 ThinkPHP 版本中,如果业务代码中存在模板赋值方法 assign…

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年11月19日15:04:59
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   EQU8-内核组件分析https://cn-sec.com/archives/632601.html