Easy Anti-Cheat - как работает, обход: внедрение кода в защищенный процесс

Введение

Прежде чем начать, пожалуйста, ознакомьтесь с несколькими статьями, написанными на эту тему ранее. В последней подробно рассказано о том, что это за античит, как он работает и что может.

Kernel Anticheat (EAC, Vanguard, BattlEye). Мануал для разработчиков: как работает, способы обхода, разработка драйверов режима ядра.

Как обойти EAC - Easy Anti Cheat.

Прежде чем мы начнем, на изображении ниже показаны некоторые модули, отвечающие за инициализацию EasyAntiCheat в стандартной процедуре, с кратким описанием того, как он работает.

Примечание: это не единственные модули, которые использует EasyAntiCheat, однако, они единственные, необходимые для понимания того, что будет дальше.

x86 Модуль

Как показано на изображении выше, античит внедряет модуль, помеченный как EasyAntiCheat.dll. Этот модуль служит одним из основных модулей службы при отправке данных на серверы для анализа. Но как эта DLL инжектится? Рассмотрим этот набор функций внутри x86 модуля:

using LauncherCallback = VOID( __stdcall* )( INT, ULONG*, UINT );


enum EasyAntiCheatStatus 
{
        Successful = 0,
        FailedDriverHandle = 1,
        IncompatibleEasyAntiCheatVersion = 2,
        LauncherAlreadyOpen = 3,
        DebuggerDetected = 4,
        WindowsSafeMode = 5,
        WindowsSignatureEnforcement = 6,
        InsufficientMemory = 7,
        DisallowedTool = 8,
        PatchGuardDisabled = 11,
        KernelDebugging = 12,
        UnexpectedError = 13,
        PatchedBootloader = 15,
        GameRunning = 16,
};


const EasyAntiCheatStatus SetupEasyAntiCheatModule( PVOID InternalModule, SIZE_T InternalModuleSize )
{
     // The current value is 0x3C but is subject to change....
        if ( GetDriverVersion( this->DriverHandle ) != CurrentVersion )
                return EasyAntiCheatStatus::FailedDriverHandle;


    // sizeof( MapModuleStructure ) == 0x140
        SIZE_T BufferSize = InternalModuleSize + sizeof( MapModuleStructure );
        MODULE_MAP_STRUCTURE* Buffer = static_cast< MODULE_MAP_STRUCTURE* >( new UINT8[ BufferSize ] );


        // Copy the image into the heap allocation....
        // Currently Heap+0x140
        memcpy( Buffer->Image, InternalModule, InternalModuleSize );


        // Game initialization data such as the name are then copied over...
        // Do note that although this buffer is encrypted with XTEA, the module is also encrypted with its own algo...
        // The following DeviceIoControl tells the driver where to map the DLL (the game).


        XTEA_ENCRYPT( Buffer, InternalModuleSize + sizeof( MapModuleStructure ), -1 );


        SIZE_T ReturnedSize = 0;
        const BOOL Result = DeviceIoControl( this->DriverHandle, MAP_INTERNAL_MODULE, Buffer, BufferSize, &Buffer, BufferSize, &ReturnedSize, nullptr );
        if ( Result && ReturnedSize == BufferSize )
        {
                // Some processing comes here....
                return EasyAntiCheatStatus::Successful;
        }


        // Other data processing occurs and error handling....


        return EasyAntiCheatStatus::UnexpectedError;
}


// The exported name of this function is called "a" inside the x86 package but I have chosen a more fit name for reference.
__declspec( dllexport ) UINT InitEasyAntiCheat( LauncherCallback CallOnStatus , PVOID SharedMemoryBuffer, UINT Num )
{
        // 
        // Sends EasyAntiCheat.sys through an open shared memory buffer "Global\EasyAntiCheatBin"
        // This code is chopped off due to its irrelevance
        // ...
        //


        const EasyAntiCheatStatus Status = SetupEasyAntiCheatModule( InternalModule, sizeof InternalModule /* Some arguments are redacted as they are irrelevant */ );
        switch ( Status )
        {
                case EasyAntiCheatStatus::Successful:
                {
                        SetEventStatus("Easy Anti-Cheat successfully loaded in-game");
                        LoadEvent("launcher_error.success_loaded");
                        break;
                }


                // Handles error codes and generates an error log...
        }


        // ...
}

Как видно из кода, EasyAntiCheat отправляет EasyAntiCheat.dll через зашифрованный буфер XTEA драйверу вместе с другой необходимой информацией, такой как GameID, имя процесса и т. д.

С первого взгляда вы заметите, что они также зашифровали модуль с помощью собственного алгоритма, поскольку первые несколько байтов - это A7 ED 96 0C 0F ..., вместо ожидаемого формата заголовка Windows PE. Учитывая, что модуль драйвера, похоже, соответствует тому же формату, реверсирование EasyAntiCheat.exe позволит нам найти ответ. В настоящее время это выглядит следующим образом:

Шифрование содержимого

VOID DecryptModule( PVOID ModuleBase, ULONG ModuleSize )
{
        if ( !ModuleSize ) 
                return;


        UINT8* Module = static_cast< UINT8* >( ModuleBase );
        ULONG DecryptionSize = ModuleSize - 2;


        while ( DecryptionSize )
        {
                Module[ DecryptionSize ] += -3 * DecryptionSize - Module[ DecryptionSize + 1];
                --DecryptionSize;
        }


        Module[ 0 ] -= Module[ 1 ];
        return;
}

Таким образом:

VOID EncryptModule( PVOID ModuleBase, ULONG ModuleSize )
{
        UINT8* Module = static_cast< UINT8* >( ModuleBase );
        ULONG Iteration = 0;


        Module[ ModuleSize - 1 ] += 3 - 3 * ModuleSize;


        while ( Iteration < ModuleSize )
        {
                Module[ Iteration ] -= -3 * Iteration - Module[ Iteration + 1];
                ++Iteration;
        }


        return;
}

Имея этот код, можно легко расшифровать модуль и делать с ним все, что посчитаете нужным. Например, вы можете внедрить старую версию этого модуля, которая потенциально позволяет пользователю избежать добавления любого содержимого в модуль EasyAntiCheat.dll. Поскольку в этом модуле раскрывается не так много информации, мы должны сосредоточить внимание на EasyAntiCheat.sys, чтобы понять, что происходит при доставке модуля.

EasyAntiCheat.sys

Как только EasyAntiCheat.sys получает модуль, он расшифровывает буфер XTEA, а затем расшифровывает зашифрованный PE-образ. После этого он готовится к мануальной загрузке, переключаясь на защищенную игру (используя KeStackAttachProcess) перед запуском следующего кода.

Мануальная загрузка

BOOLEAN MapSections( PVOID ModuleBase, PVOID ImageBuffer, PIMAGE_NT_HEADERS NtHeaders )
{
        if ( !ModuleBase || !ImageBuffer )
            return FALSE;


        UINT8* MappedModule = static_cast< UINT8* >( ModuleBase );
        UINT8* ModuleBuffer = static_cast< UINT8* >( ImageBuffer );
        ULONG SectionCount = NtHeaders->FileHeader.NumberOfSections;


        const PIMAGE_SECTION_HEADER SectionHeaders = IMAGE_FIRST_SECTION( NtHeaders );
        const ULONG PEHeaderSize = SectionHeaders->VirtualAddress;


        // Copy the PE header information.....
        memcpy( ModuleBase, ImageBuffer, PEHeaderSize );


        while( SectionCount )
        {
                const PIMAGE_SECTION_HEADER SectionHeader = &SectionHeaders[ SectionCount ];
                if ( SectionHeader->SizeOfRawData )
                        memcpy( &MappedModule[ SectionHeader->VirtualAddress ], &ModuleBuffer[ SectionHeader->PointerToRawData ], SectionHeader->SizeOfRawData );


                --SectionCount;
        }


        return TRUE;
}


BOOLEAN MapImage( PVOID ImageBase, SIZE_T ImageSize, PVOID* MappedBase, SIZE_T* MappedSize, PVOID* MappedEntryPoint, /* x86 only */ OPTIONAL ULONG* ExceptionDirectory, /* x86 only */ OPTIONAL ULONG* ExceptionDirectorySize )
{
        if ( !ImageBase || !ImageSize || !MappedBase || !MappedSize || !MappedEntryPoint )
                return FALSE;


        *MappedBase = nullptr;
        *MappedSize = 0;
        *MappedEntryPoint = nullptr;


        if ( ExceptionDirectory && ExceptionDirectorySize )
        {
                // These parameters are only used to resolve the exception directory if the DllHost module is being mapped into Dllhost.exe....
                *ExceptionDirectory = 0;
                *ExceptionDirectorySize = 0;
        }


        ImageType ModuleType;
        const PIMAGE_NT_HEADERS NtHeaders = RtlImageNtHeader( ImageBase );
        if ( NtHeaders->FileHeader.Machine == IMAGE_FILE_MACHINE_AMD64 )
        {
                ModuleType = ImageType::Image64;
        } 
        else if ( NtHeaders->FileHeader.Machine == IMAGE_FILE_MACHINE_I386 )
        {
                ModuleType = ImageType::Image86;
        }


        PVOID MemBuffer = ExAllocatePool( ImageSize );
        if ( MemBuffer )
        {
                // This will be used to effectively "hide" the module within the process...
                const ULONG RandomSizeStart = RandomSeed( 4, 16 ) << 12UL;
                const ULONG RandomSizeEnd = RandomSeed( 4, 16 ) << 12UL;


                memcpy( MemBuffer, ImageBase, ImageSize );


                ULONG64 SizeOfImage = NtHeaders->OptionalHeader.SizeOfImage + ( RandomSizeEnd + RandomSizeStart );


                BOOLEAN VirtualApiResult = 
                        NT_SUCCESS( NtAllocateVirtualMemory( NtCurrentProcess(), MappedBase, 0, &SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE );


                if ( VirtualApiResult )
                {
                        ULONG OldProtect = 0;
                        VirtualApiResult = NT_SUCCESS( NtProtectVirtualMemory( NtCurrentProcess(), MappedBase, SizeOfImage, PAGE_EXECUTE_READWRITE, &OldProtect ) );
                        if ( VirtualApiResult )
                        {
                                // This region is used to throw people off from the module.
                                RandomizeRegion( *MappedBase, RandomSizeStart );
                                VirtualApiResult = NT_SUCCESS( NtProtectVirtualMemory( NtCurrentProcess(), MappedBase, RandomSizeStart, PAGE_READWRITE, &OldProtect ) );


                                if ( VirtualApiResult )
                                {
                                        PVOID ModuleEnd = static_cast< UINT8* >( *MappedBase ) + ( SizeOfImage - RandomSizeEnd );
                                        RandomizeRegion( ModuleEnd,  RandomSizeEnd );
                                        VirtualApiResult = NT_SUCCESS( NtProtectVirtualMemory( NtCurrentProcess(), &ModuleEnd, RandomSizeEnd, PAGE_READONLY, &OldProtect ) );


                                        if ( VirtualApiResult )
                                        {
                                                PVOID RealModule = static_cast< UINT8* >( *MappedBase ) + RandomSizeStart;
                                                ResolveRelocations( RealModule, MemBuffer, ModuleType, NtHeaders );
                                                NtHeaders->OptionalHeader.ImageBase = RealModule;


                                                if ( MapSections( RealModule, MemBuffer, NtHeaders ))
                                                {
                                                        // Applies the correct memory attributes for each section (.text = RX, .data = RW, .rdata = R, etc)
                                                        CorrectSectionProtection( RealModule, NtHeaders );


                            *MappedBase = RealModule;
                                                        *MappedSize = NtHeaders->OptionalHeader.SizeOfImage;
                                                        *MappedEntryPoint = static_cast< UINT8* >( RealModule ) + NtHeaders->OptionalHeader.AddressOfEntryPoint;


                                                        if ( ExceptionDirectory && ExceptionDirectorySize )
                                                        {
                                                                *ExceptionDirectory = NtHeaders->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXCEPTION ].VirtualAddress;
                                                                *ExceptionDirectorySize = NtHeaders->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXCEPTION ].Size;
                                                        }
                                                }
                                        }
                                }
                        }
                }
        }


        if ( MemBuffer )
        {
                ExFreePool( MemBuffer );
                MemBuffer = nullptr;
        }


        return *MappedEntryPoint != NULL;
}

Это стандартный мануальный загрузчик. Он пытается скрыться, выделяя дополнительную память вокруг своей в надежде, что реверсер не увидит, что это, по сути, динамический код. Вы также должны отметить, что если раздел содержит необработанные данные, мы можем загрузить его содержимое в игре. Это означает, что злоумышленник может намеренно добавить дополнительный раздел (или, возможно, захватить существующий раздел), и EasyAntiCheat.sys небрежно загрузит этот код без какой-либо проверки.

Выполнение кода

Выполнить код довольно просто. EAC использует доставку APC для выполнения шелл-кода в пользовательском режиме, который отображается следующей функцией:

PVOID MapShellcode(ModuleMapInstance* Instance)
{
        SIZE_T ShellcodeSize = PAGE_SIZE; // 0x1000
        PVOID ShellcodeBase = nullptr;


        BOOLEAN VirtualApiResult = 
                NT_SUCCESS( NtAllocateVirtualMemory( NtCurrentProcess(), &ShellcodeBase, 0, &ShellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE );


        if ( !VirtualApiResult || !ShellcodeBase )
                return nullptr;


        if ( Instance->ImageType == ImageType::Image64 )
        {
                UINT8 ShellcodeBuffer[] =
                {
                        0x48, 0x83, 0xEC, 0x28,        // SUB RSP, 0x28
                        0x4D, 0x31, 0xC0, // XOR R8, R8
                        0x48, 0x31, 0xD2, // XOR RDX, RDX
                        0x48, 0xFF, 0xC2, // INC RDX
                        0x48, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // MOV RAX, 0
                        0xFF, 0xD0,        // CALL RAX
                        0x48, 0x83, 0xC4, 0x28, // ADD RSP, 0x28
                        0xC3 // RETN
                };


                memcpy( &ShellcodeBuffer[15], Instance->DllEntryPoint, sizeof( Instance->DllEntryPoint ) );
                memcpy( ShellcodeBase, ShellcodeBuffer, sizeof( ShellcodeBuffer ) );
        }
        else
        {
                UINT8 ShellcodeBuffer[] =
                {
                        0x6A, 0x00,        // PUSH 0
                        0x6A, 0x01,        // PUSH 1
                        0xFF, 0x74, 0xE4, 0x0C, // PUSH [RSP+0xC]
                        0xB8, 0x00, 0x00, 0x00, 0x00, // MOV EAX, 0
                        0xFF, 0xD0,        // CALL EAX
                        0xC2, 0x04, 0x00 // RETN 4
                };


                memcpy( &ShellcodeBuffer[9], Instance->DllEntryPoint, sizeof( Instance->DllEntryPoint ) / 2 );
                memcpy( ShellcodeBase, ShellcodeBuffer, sizeof( ShellcodeBuffer ) );
        }


        return ShellcodeBase;
}

После того, как EP этого модуля был выполнен, его заголовок впоследствии стирается, гарантируя, что реверс-инженеры не имеют к нему доступа. EasyAntiCheat.dll кодирует определенные данные, такие как HANDLE, в драйвер EasyAntiCheat.sys внутри этого неиспользуемого пространства. В этом ручном загрузчике есть много других функций, таких как определение IAT модуля.

EasyAntiCheat.dll

Прежде чем мы перейдем к уязвимости, давайте взглянем на сам модуль EasyAntiCheat.dll, чтобы увидеть, какие последствия может иметь захват этой полезной нагрузки. Как мы все знаем, (manual mapping) - это популярный механизм внедрения кода, которым пользуются разработчики читов. Чтобы гарантировать, что EasyAntiCheat не собирает данные из допустимой области памяти, создана внутренняя система белых списков системных модулей, а также диапазон загружаемых вручную образов. Мы можем увидеть пример того, как это используется в функции ниже:

BOOLEAN IsInValidMemory( EACGlobal* GlobalContext, ULONG64 VirtualAddress )
{
        if ( !VirtualAddress )
                return FALSE;


        ModuleListEntry* ModuleList = &GlobalContext->ModuleList;
        RtlEnterCriticalSection( ModuleList ); // Wait until the list is available....
        ModuleListEntry* CurrentEntry = ModuleList->Flink;
        for ( i = ModuleList->Flink; CurrentEntry != i; CurrentEntry = CurrentEntry->Flink; )
        {
                if ( CurrentEntry->Unk0 && CurrentEntry->Unk1 && 
                        VirtualAddress >= CurrentEntry->ImageBase && VirtualAddress < CurrentEntry->ImageBase + CurrentEntry->SizeOfImage )
                {
                        break;
                }
        }


        RtlLeaveCriticalSection(ModuleList);
        InternalModuleBase = GlobalContext->MappedImageBase;


        // If it landed inside a legit module or within EasyAntiCheat.dll, return TRUE.
        if ( i != ModuleList || VirtualAddress >= StartAddress && VirtualAddress < GlobalContext->MappedImageSize + StartAddress )
                return TRUE;


        // Other regions like dynamically allocated shellcode below....
        return FALSE;
}

Эта функция регулярно выполняется внутри EasyAntiCheat.dll, чтобы определить, находится ли адрес в допустимой памяти. Как вы могли заметить, если адрес находится внутри внутреннего модуля, он возвращает значение TRUE. Многие вещи, от которых EAC защищает игру (незаконное создание потоков, встроенные перехватчики и т. д.), обходятся с помощью загрузки вашего кода в EasyAntiCheat.dll.

Примечание: EAC не всегда использует эту функцию и довольно часто имеет встроенные проверки, чтобы определить, существует ли адрес в его памяти.

Эксплуатация

Теперь, когда мы это понимаем, мы можем разработать собственные полезные данные, чтобы перехватить выполнение в пользовательском режиме и добавить наш код к существующему коду EAC. Схема этого эксплойта выглядит примерно так:

image

Более подробно, вам нужно будет внедрить DLL в eac_launcher.exe, который будет выполнять следующие действия:

  1. Рекурсивное сканирование паттернов для функции SetupEasyAntiCheatModule.
  2. Как только мы найдем совпадение, подключите функцию и извлеките существующий образ.
  3. Расшифруйте образ с помощью DecryptModule, затем измените существующий раздел, чтобы внедрить новый код.
  4. Измените атрибуты раздела, чтобы они содержали PAGE_EXECUTE_READWRITE.
  5. Обновите параметр ImageSize (и SizeOfImage в структуре IMAGE_OPTIONAL_HEADER) и вызовите EncryptModule, чтобы пересобрать модуль.
  6. Пропатчите DllEntryPoint, чтобы выполнить REL32 JMP для вашего DllEntryPoint.
  7. После запуска EP восстановите патчи и вызовите точку входа EasyAntiCheat.dll.
  8. Готово!

Чтобы не иметь дела с вызовом x86, я решил, что лучше всего разместить инструкцию int3, которая вызовет остановку после выполнения функции. Затем я обработал это с помощью VEH (Vectored Exception Handler), чтобы выполнить подключаемую процедуру и, наконец, восстановить исходный код операции с измененными параметрами.

Следует также отметить, что вы должны добавить информацию о заголовке PE в заголовок EasyAntiCheat.dll. Это связано с тем, что такая информация, как перемещения и данные импорта, не будет разрешена, и, следовательно, потребуется другой способ обхода для правильной загрузки вашего модуля или ожидания краша.

Вы также должны знать, что EasyAntiCheat.dll имеет проверки целостности, выполняемые внутри EasyAntiCheat.sys; поэтому не пытайтесь исправлять недоступные для записи разделы без обхода! Это дополнительно подразумевает, что вы можете намеренно создать несколько разделов в двоичном файле и принудительно заставить драйвер защищать определенные разделы кода для вас.

Демонстрация

Следующее видео является демонстрацией этого метода в действии, отображая лог внутри DbgView.exe путем вызова OutputDebugStringA внутри игры.

Вывод

EasyAntiCheat.sys непреднамеренно создал идеальные условия для выполнения кода в игре, которые позволяют динамически запускать код внутри процесса из пользовательского режима и позволяют подключать и выполнять любой код без конфликта с античитом.

Примечание

Приветствую. Статья переведена и дополнена мной с англоязычного форума и будет обновляться/дополняться в процессе появления новой информации. Если вы найдете здесь ошибки или захотите дополнить статью, пожалуйста, воспользуйтесь личными сообщениями на форуме и проинформируйте меня.

3 Likes