
BattlEye — это преимущетвенно немецкий сторонний античит, в основном разрабатываемый 32-летним Бастианом Хейко Сутером. Он предоставляет (или пытается предоставить) издателям игр простую в применении систему античита, использующую механизмы общей защиты, а также обнаружение читов для конкретных игр для оптимизации безопасности. Как сказано на веб-сайте продукта, он всегда остаётся на вершине современных технологий и использует инновационные методики защиты и обнаружения; очевидно, это следствие национальности разработчика: QUALITY MADE IN GERMANY. BattlEye состоит из множества элементов, совместно работающих над поиском читеров в играх, оплативших использование продукта. Четырьмя основными элементами являются:
- BEService
- Системная служба Windows, обменивающаяся данными с сервером BattlEye BEServer, который обеспечивает возможности клиент-серверной связи с BEDaisy и BEClient.
- BEDaisy
- Драйвер ядра Windows, регистрирующий превентивные механизмы обработки событий и мини-фильтры, чтобы препятствовать читерам в незаконном изменении игры
- BEClient
- Динамически подключаемая библиотека Windows, отвечающая за большинство векторов обнаружения, в том числе за описанные в данной статье. После инициализации она привязывается к процессу игры.
- BEServer
- Проприетарный бэкенд-сервер, отвечающий за сбор информации и принятие конкретных мер против читеров.
Шелл-код
Недавно в Интернете всплыл дамп шелл-кода BattlEye, и мы решили написать о том, что же конкретно ищет текущая версия BattlEye. Мы не анализировали BattlEye целых шесть месяцев, поэтому наш последний дамп шелл-кода скорее всего уже устарел. Различные части кода восстанавливали только по памяти из этого последнего дампа, предположив, что в BattlEye дополняется только шелл-код и не удаляются предыдущие процедуры обнаружения.
Как?
BattlEye предположительно выполняет потоковую передачу шелл-кода со своего сервера в службу Windows под названием BEService. Эта служба обменивается данными с модулем BEClient, расположенным внутри процесса игры. Обмен данными выполняется через конвейер \.namedpipeBattleye и до 2018 года был незашифрованным. Теперь все передаваемые данные шифруются xor-шифровальщиком с очень маленькими ключами, из-за чего чрезвычайно просто выполнять известные атаки на основе открытых текстов (plaintext attacks). Когда шелл-код передаётся клиенту, он располагается и выполняется за пределами всех известных модулей, из-за чего его легко определить. Для создания дампа шелл-кода можно или обрабатывать стандартные функции Windows API типа CreateFile, ReadFile и т.п., и выполнять дамп соответствующей области памяти всех вызывающих модулей (запрашивая информацию о памяти по возвращаемому адресу), находящихся за пределами всех известных модулей, или периодически сканировать пространство виртуальной памяти игры в поисках исполняемой памяти за пределами всех известных модулей, и дампить её на диск. При этом нужно отслеживать, дамп каких областей уже был выполнен, чтобы в результате не получилось множество одинаковых дампов.
Объяснение
Представленные в статье фрагменты псевдокода сильно модифицированы ради красоты. Вы не сможете сдампить шелл-код BattlEye и сразу же узнать эти части; шелл-код не содержит вызовов функций, а многие алгоритмы в статье развёрнуты. Но на самом деле это не важно, потому что когда вы закончите читать об этом ужасном античите, вам представится возможность обойти его (:
Перебирание памяти
Самый распространённый механизм обнаружения в античитах — это перебирание (memory enumeration) и сканирование памяти для поиска известных образов читов. Он легко реализуем и, как показало прошлое, при правильном подходе достаточно эффективен, если вы не забыли основы ассемблера и занесли в чёрный список пролог общей функции.
Battleye перебирает всё адресное пространство процесса игры (текущего процесса в данном контексте) и выполняет различные проверки на предмет исполняемости страницы и нахождения за пределами соответствующего пространства памяти шелл-кода.
Вот как это реализовано в Battleye:
// MEMORY ENUMERATION for (current_address = 0 // QUERY MEMORY_BASIC_INFORMATION NtQueryVirtualMemory(GetCurrentProcess(), current_address, 0, &memory_information, 0x30, &return_length) >= 0 current_address = memory_information.base_address + memory_information.region_size) { const auto outside_of_shellcode = memory_information.base_address > shellcode_entry || memory_information.base_address + memory_information.region_size <= shellcode_entry const auto executable_memory = memory_information.state == MEM_COMMIT && (memory_information.protect == PAGE_EXECUTE || memory_information.protect == PAGE_EXECUTE_READ || memory_information.protect == PAGE_EXECUTE_READWRITE const auto unknown_whitelist = memory_information.protect != PAGE_EXECUTE_READWRITE || memory_information.region_size != 100000000 if (!executable_memory || !outside_of_shellcode || !unknown_whitelist) continue // RUN CHECKS memory::anomaly_check(memory_information memory::pattern_check(current_address, memory_information memory::module_specific_check_microsoft(memory_information memory::guard_check(current_address, memory_information memory::module_specific_check_unknown(memory_information }
Аномалии памяти
BattlEye помечает все аномалии в адресном пространстве памяти, в основном памяти исполняемых модулей, не соответствующей загруженному образу:
void memory::anomaly_check(MEMORY_BASIC_INFORMATION memory_information) { // REPORT ANY EXECUTABLE PAGE OUTSIDE OF KNOWN MODULES if (memory_information.type == MEM_PRIVATE || memory_information.type == MEM_MAPPED) { if ((memory_information.base_address & 0xFF0000000000) != 0x7F0000000000 && // UPPER EQUALS 0x7F (memory_information.base_address & 0xFFF000000000) != 0x7F000000000 && // UPPER EQUALS 0x7F0 (memory_information.base_address & 0xFFFFF0000000) != 0x70000000 && // UPPER EQUALS 0x70000 memory_information.base_address != 0x3E0000)) { memory_report.unknown = 0 memory_report.report_id = 0x2F memory_report.base_address = memory_information.base_address memory_report.region_size = memory_information.region_size memory_report.memory_info = memory_information.type | memory_information.protect | memory_information.state battleye::report(&memory_report, sizeof(memory_report), 0 } } }
Сканирование в поисках паттернов
Как говорилось выше, BattlEye также сканирует память локальных процессов на наличие различных чётко прописанных паттернов, как это видно из показанной ниже реализации.
При чтении этого псевдокода вы можете догадаться, что эти проверки можно обойти переписыванием области кода каждого загруженного модуля, так как они не будут выполнять сканирования в поисках паттернов в известных образах. Чтобы не попасть на проверки целостности, нужно загружать все упакованные и помещённые в белый список области и переписывать области кода, помеченные как RWX, потому что мы не можем выполнять проверки целостности без эмуляции упаковщика. В текущей версии шелл-кода BattlEye эти паттерны памяти жёстко прописаны:
[05 18] ojectsPUBGChinese [05 17] BattleGroundsPrivate_CheatESP [05 17] [%.0fm] %s [05 3E] 0000Neck0000Chest0000000Mouse 10 [05 3F] PlayerESPColor [05 40] Aimbot: %d02D3E2041 [05 36] HackMachine [05 4A] VisualHacks.net [05 50] 3E232F653E31314E4E563D4276282A3A2E463F757523286752552E6F30584748 [05 4F] DLLInjection-master\x64\Release\ [05 52] NameESP [05 48] Skullhack [05 55] .rdata$zzzdbg [05 39] AimBot [05 39] EB4941803C123F755C623FEB388D41D0FBEC93C977583E930EB683E1DF [05 5F] 55E9 [05 5F] 57E9 [05 5F] 60E9 [05 68] D3D11Present initialised [05 6E] [ %.0fM ] [05 74] [hp:%d]%dm [05 36] 48836424380488D4C2458488B5424504C8BC848894C24304C8BC7488D4C2460 [05 36] 741FBA80000FF15607E0085C07510F2F1087801008B8788100EB [05 36] 40F2AA156F8D2894E9AB4489535D34F9CPOSITION0000COL [05 7A] FFE090 [05 79] %s00%d00POSITION0000COLOR0000000 [05 36] 8E85765DCDDA452E75BA12B4C7B94872116DB948A1DAA6B948A7676BB948902C [05 8A] n<assembly xmlsn='urn:schemas-mi
Эти паттерны памяти также содержат двухбайтный заголовок, а именно неизвестное статическое значение 05 и уникальный идентификатор.
Чего мы не увидим, так это того, что BattlEye также динамически выполняет потоковую передачу паттернов из BEServer и отправляет их в BEClient, но в статье мы рассматривать это не будем.
Они итеративно сканируются следующим алгоритмом:
void memory::pattern_check(void* current_address, MEMORY_BASIC_INFORMATION memory_information) { const auto is_user32 = memory_information.allocation_base == GetModuleHandleA("user32.dll" // ONLY SCAN PRIVATE MEMORY AND USER32 CODE SECTION if (memory_information.type != MEM_PRIVATE && !is_user32) continue for (address = current_address address != memory_information.base_address + memory_information.region_size address += PAGE_SIZE) // PAGE_SIZE { // READ ENTIRE PAGE FROM LOCAL PROCESS INTO BUFFER if (NtReadVirtualMemory(GetCurrentProcess(), address, buffer, PAGE_SIZE, 0) < 0) continue for (pattern_index = 0 pattern_index < 0x1C/*PATTERN COUNT*/ ++pattern_index) { if (pattern[pattern_index].header == 0x57A && !is_user32) // ONLY DO FFE090 SEARCHES WHEN IN USER32 continue for (offset = 0 pattern[pattern_index].length + offset <= PAGE_SIZE ++offset) { const auto pattern_matches = memory::pattern_match(&address[offset], pattern[pattern_index // BASIC PATTERN MATCH if (pattern_matches) { // PATTERN FOUND IN MEMORY pattern_report.unknown = 0 pattern_report.report_id = 0x35 pattern_report.type = pattern[index].header pattern_report.data = &address[offset pattern_report.base_address = memory_information.base_address pattern_report.region_size = memory_information.region_size pattern_report.memory_info = memory_information.type | memory_information.protect | memory_information.state battleye::report(&pattern_report, sizeof(pattern_report), 0 } } } } }
Проверка конкретных модулей (Microsoft)
Проверки модулей сообщают о наличии конкретных модулей, загруженных в процесс игры:
void memory::module_specific_check_microsoft(MEMORY_BASIC_INFORMATION memory_information) { auto executable = memory_information.protect == PAGE_EXECUTE || memory_information.protect == PAGE_EXECUTE_READ || memory_information.protect == PAGE_EXECUTE_READWRITE auto allocated = memory_information.state == MEM_COMMIT if (!allocated || !executable) continue auto mmres_handle = GetModuleHandleA("mmres.dll" auto mshtml_handle = GetModuleHandleA("mshtml.dll" if (mmres_handle && mmres_handle == memory_information.allocation_base) { battleye_module_anomaly_report module_anomaly_report module_anomaly_report.unknown = 0 module_anomaly_report.report_id = 0x5B module_anomaly_report.identifier = 0x3480 module_anomaly_report.region_size = memory_information.region_size battleye::report(&module_anomaly_report, sizeof(module_anomaly_report), 0 } else if (mshtml_handle && mshtml_handle == memory_information.allocation_base) { battleye_module_anomaly_report module_anomaly_report module_anomaly_report.unknown = 0 module_anomaly_report.report_id = 0x5B module_anomaly_report.identifier = 0xB480 module_anomaly_report.region_size = memory_information.region_size battleye::report(&module_anomaly_report, sizeof(module_anomaly_report), 0 } }
Проверка конкретных модулей (неизвестных)
В систему добавлена проверка конкретных модулей, сигнализирующая серверу о том, что у вас загружены модули, удовлетворяющие любому из этих критериев:
void memory::module_specific_check_unknown(MEMORY_BASIC_INFORMATION memory_information) { const auto dos_header = (DOS_HEADER*)module_handle const auto pe_header = (PE_HEADER*)(module_handle + dos_header->e_lfanew const auto is_image = memory_information.state == MEM_COMMIT && memory_information.type == MEM_IMAGE if (!is_image) return const auto is_base = memory_information.base_address == memory_information.allocation_base if (!is_base) return const auto match_1 = time_date_stamp == 0x5B12C900 && *(__int8*)(memory_information.base_address + 0x1000) == 0x00 && *(__int32*)(memory_information.base_address + 0x501000) != 0x353E900 const auto match_2 = time_date_stamp == 0x5A180C35 && *(__int8*)(memory_information.base_address + 0x1000) != 0x00 const auto match_2 = time_date_stamp == 0xFC9B9325 && *(__int8*)(memory_information.base_address + 0x6D3000) != 0x00 if (!match_1 && !match_2 && !match_3) return const auto buffer_offset = 0x00 // OFFSET DEPENDS ON WHICH MODULE MATCHES, RESPECTIVELY 0x501000, 0x1000 AND 0x6D3000 unknown_module_report.unknown1 = 0 unknown_module_report.report_id = 0x46 unknown_module_report.unknown2 = 1 unknown_module_report.data = *(__int128*)(memory_information.base_address + buffer_offset battleye::report(&unknown_module_report, sizeof(unknown_module_report), 0 }
Мы не знаем, какие модули удовлетворяют этим критериям, но подозреваем, что это попытка обнаружения очень ограниченного набора конкретных чит-модулей.
Дополнение: @how02 сообщил нам, что модуль action_x64.dll имеет метку времени 0x5B12C900 и содержит область кода, в которую можно выполнять запись; как и говорилось ранее, это можно использовать для эксплойта.
Защита памяти
В BattlEye также внедрена очень сомнительная процедура обнаружения, которая, по нашему мнению, ищет память с установленным флагом PAGE_GUARD, не проверяя на самом деле, установлен ли флаг PAGE_GUARD:
void memory::guard_check(void* current_address, MEMORY_BASIC_INFORMATION memory_information) { if (memory_information.protect != PAGE_NOACCESS) { auto bad_ptr = IsBadReadPtr(current_address, sizeof(temporary_buffer auto read = NtReadVirtualMemory( GetCurrentProcess(), current_address, temporary_buffer, sizeof(temporary_buffer), 0 if (read < 0 || bad_ptr) { auto query = NtQueryVirtualMemory( GetCurrentProcess(), current_address, 0, &new_memory_information, sizeof(new_memory_information), &return_length memory_guard_report.guard = query < 0 || new_memory_information.state != memory_information.state || new_memory_information.protect != memory_information.protect if (memory_guard_report.guard) { memory_guard_report.unknown = 0 memory_guard_report.report_id = 0x21 memory_guard_report.base_address = memory_information.base_address memory_guard_report.region_size = (int)memory_information.region_size memory_guard_report.memory_info = memory_information.type | memory_information.protect | memory_information.state battleye::report(&memory_guard_report, sizeof(memory_guard_report), 0 } } } }
Перебирание окон
Шелл-код BattlEye перебирает каждое из текущих видимых во время работы игры окон, обходя окна сверху вниз (по z-значению). Дескрипторы окон, находящиеся внутри процесса игры, исключаются из этого перебора, и это определяется вызовом GetWindowThreadProcessId. Следовательно, можно привязать соответствующую функцию к ложному владельцу окна, чтобы BattlEye не проверял ваше окно.
void window_handler::enumerate() { for (auto window_handle = GetTopWindow window_handle window_handle = GetWindow(window_handle, GW_HWNDNEXT), // GET WINDOW BELOW ++window_handler::windows_enumerated) // INCREMENT GLOBAL COUNT FOR LATER USAGE { auto window_process_pid = 0 GetWindowThreadProcessId(window_handle, &window_process_pid if (window_process_pid == GetCurrentProcessId()) continue // APPEND INFORMATION TO THE MISC. REPORT, THIS IS EXPLAINED LATER IN THE ARTICLE window_handler::handle_summary(window_handle constexpr auto max_character_count = 0x80 const auto length = GetWindowTextA(window_handle, window_title_report.window_title, max_character_count // DOES WINDOW TITLE MATCH ANY OF THE BLACKLISTED TITLES? if (!contains(window_title_report.window_title, "CheatAut") && !contains(window_title_report.window_title, "pubg_kh") && !contains(window_title_report.window_title, "conl -") && !contains(window_title_report.window_title, "PerfectA") && !contains(window_title_report.window_title, "AIMWA") && !contains(window_title_report.window_title, "PUBG AIM") && !contains(window_title_report.window_title, "HyperChe")) continue // REPORT WINDOW window_title_report.unknown_1 = 0 window_title_report.report_id = 0x33 battleye::report(&window_title_report, sizeof(window_title_report) + length, 0 } }
Аномалия при перебирании
Если проверено меньше, чем два окна, серверу отправляется уведомление. Вероятно, это сделано для того, чтобы предотвратить патчинг соответствующих функций, не позволяющих шелл-коду BattlEye исследовать ни одно окно:
void window_handler::check_count() { if (window_handler::windows_enumerated > 1) return // WINDOW ENUMERATION FAILED, MOST LIKELY DUE TO HOOK window_anomaly_report.unknown_1 = 0 window_anomaly_report.report_id = 0x44 window_anomaly_report.enumerated_windows = windows_enumerated battleye::report(&window_anomaly_report, sizeof(window_anomaly_report), 0 }
Перебирание процессов
При помощи вызова CreateToolhelp32Snapshot BattlEye перебирает все запущенные процессы, но не обрабатывает никакие ошибки, благодаря чему очень легко пропатчить и избежать выполнения следующих процедур обнаружения:
Проверка пути
Если образ находится внутри как минимум двух подкаталогов (считая от корня диска), то система пометит процессы флагом, если путь к соответствующему образу содержит хотя бы одну из этих строк:
Desktop Temp FileRec Documents Downloads Roaming tmp.ex notepad. ...\. cmd.ex
Если путь к исполняемому файлу соответствует одной из этих строк, то сервер получает уведомление о пути к исполняемому файлу, а также информацию о том, является ли родительский процесс одним из следующих (содержит соответствующий бит флага, отправляемый серверу):
steam.exe [0x01] explorer.exe [0x02] lsass.exe [0x08] cmd.exe [0x10]
Если клиент не может открыть дескриптор с соответствующими правами QueryLimitedInformation, то он установит бит флага 0x04, если причина ошибки при сбое вызова OpenProcess не равна ERROR_ACCESS_DENIED, что даёт нам последний контейнер перечисления для соответствующего значения флага:
enum BATTLEYE_PROCESS_FLAG { STEAM = 0x1, EXPLORER = 0x2, ERROR = 0x4, LSASS = 0x8, CMD = 0x10 }
Если родительским процессом является steam, то пользователю мгновенно устанавливается флаг и об этом сообщается серверу с id уведомления 0x40
Имя образа
Если процесс соответствует любому из множества представленных ниже критериев, то вам мгновенно устанавливается флаг и об этом сообщается серверу с id уведомления 0x38
Имя образа содержит "Loadlibr" Имя образа содержит "Rng " Имя образа содержит "A0E7FFFFFF81" Имя образа содержит "RNG " Имя образа содержит "90E54355" Имя образа содержит "2.6.ex" Имя образа содержит "TempFile.exe"
Оверлей игр Steam
BattlEye отслеживает процесс оверлея игр Steam, отвечающий за внутриигровой оверлей, известный большинству пользователей Steam. Полное имя хоста оверлея игр Steam — gameoverlayui.exe; известно, что его часто используют для эксплойтов рендеринга, потому что довольно легко взломать и выполнять незаконную отрисовку в окне игры. Проверка имеет следующее условие:
file size != 0 && image name contains (case insensitive) gameoverlayu
Дальнейшие проверки, специфичные для оверлея игр Steam, практически аналогичны процедурам, выполняемым для самого процесса игры, поэтому в псевдокоде пропущены.
Сканирование памяти оверлея игр Steam
Процесс оверлея игр Steam сканируется на наличие паттернов и аномалий. Нам не удалось пробраться глубже в кроличью нору, и узнать, для чего эти паттерны, потому что они очень обобщённые и скорее всего связаны с модулями читов.
void gameoverlay::pattern_scan(MEMORY_BASIC_INFORMATION memory_information) { // PATTERNS: // Home // F1 // FFFF83C48C30000000000 // \.pipe%s // C760000C64730 // 60C01810033D2 // ... // PATTERN SCAN, ALMOST IDENTICAL CODE TO THE AFOREMENTIONED PATTERN SCANNING ROUTINE gameoverlay_memory_report.unknown_1 = 0 gameoverlay_memory_report.report_id = 0x35 gameoverlay_memory_report.identifier = 0x56C gameoverlay_memory_report.data = &buffer[offset gameoverlay_memory_report.base_address = memory_information.base_address gameoverlay_memory_report.region_size = (int)memory_information.region_size gameoverlay_memory_report.memory_info = memory_information.type | memory_information.protect | memory_information.state battleye::report(&gameoverlay_memory_report, sizeof(gameoverlay_memory_report), 0 }
Процедура сканирования также ищет любые аномалии в виде исполняемого кода за пределами загруженных изображений, предполагая, что взломщики инъектировали код в процесс оверлея:
void gameoverlay::memory_anomaly_scan(MEMORY_BASIC_INFORMATION memory_information) { // ... // ALMOST IDENTICAL ANOMALY SCAN COMPARED TO MEMORY ENUMERATION ROUTINE OF GAME PROCESS gameoverlay_report.unknown = 0 gameoverlay_report.report_id = 0x3B gameoverlay_report.base_address = memory_information.base_address gameoverlay_report.region_size = memory_information.region_size gameoverlay_report.memory_info = memory_information.type | memory_information.protect | memory_information.state battleye::report(&gameoverlay_report, sizeof(gameoverlay_report), 0 }
Защита процесса оверлея игр Steam
Если процесс оверлея игр Steam защищён какой-нибудь защитой процессов Windows наподобие Light (WinTcb), то сервер получит об этом уведомление.
void gameoverlay::protection_check(HANDLE process_handle) { auto process_protection = 0 NtQueryInformationProcess( process_handle, ProcessProtectionInformation, &process_protection, sizeof(process_protection), nullptr if (process_protection == 0) // NO PROTECTION return gameoverlay_protected_report.unknown = 0 gameoverlay_protected_report.report_id = 0x35 gameoverlay_protected_report.identifier = 0x5B1 gameoverlay_protected_report.data = process_protection battleye::report(&gameoverlay_protected_report, sizeof(gameoverlay_protected_report), 0 }
Кроме того, если соответствующий вызов OpenProcess к процессу оверлея возвращает ERROR_ACCESS_DENIED, то о пользователе высылается уведомление с id 3B.
Перебирание модулей
Модули процесса оверлея игр Steam тоже перебираются, в частности, ищутся vgui2_s.dll и gameoverlayui.dll. Для этих модулей выполняется несколько проверок, начиная с gameoverlayui.dll.
Если выполняется это условие: [gameoverlayui.dll+6C779] == 08BE55DC3CCCCB8????????C3CCCCCC, то шелл-код сканирует vtable по адресу, хранящемуся в байтах ????????. Если любой из этих элементов vtable находится за пределами исходного модуля gameoverlayui.dll или указывает на инструкцию int 3, то о пользователе сообщается на сервер с id уведомления 3B.
void gameoverlay::scan_vtable(HANDLE process_handle, char* buffer, MODULEENTRY32 module_entry) { char function_buffer[16 for (vtable_index = 0 vtable_index < 20 vtable_index += 4) { NtReadVirtualMemory( process_handle, *(int*)&buffer[vtable_index], &function_buffer, sizeof(function_buffer), 0 if (*(int*)&buffer[vtable_index] < module_entry.modBaseAddr || *(int*)&buffer[vtable_index] >= module_entry.modBaseAddr + module_entry.modBaseSize || function_buffer[0] == 0xCC ) // FUNCTION PADDING { gameoverlay_vtable_report.report_id = 0x3B gameoverlay_vtable_report.vtable_index = vtable_index gameoverlay_vtable_report.address = buffer[vtable_index battleye::report(&gameoverlay_vtable_report, sizeof(gameoverlay_vtable_report), 0 } } }
Для модуля vgui2_s.dll тоже выполняется специфическая процедура проверки:
void vgui::scan() { if (!equals(vgui_buffer, "6A08B31FF561C8BD??????????FF96????????8BD????????8B1FF90")) { auto could_read = NtReadVirtualMemory( process_handle, module_entry.modBaseAddr + 0x48338, vgui_buffer, 8, 0) >= 0 constexpr auto pattern_offset = 0x48378 // IF READ DID NOT FAIL AND PATTERN IS FOUND if (could_read && equals(vgui_buffer, "6A46A06A26A")) { vgui_report.unknown_1 = 0 vgui_report.report_id = 0x3B vgui_report.unknown_2 = 0 vgui_report.address = LODWORD(module_entry.modBaseAddr) + pattern_offset // READ TARGET BUFFER INTO REPORT NtReadVirtualMemory( process_handle, module_entry.modBaseAddr + pattern_offset, vgui_report.buffer, sizeof(vgui_report.buffer), 0 battleye::report(&vgui_report, sizeof(vgui_report), 0 } } else if ( // READ ADDRESS FROM CODE NtReadVirtualMemory(process_handle, *(int*)&vgui_buffer[9], vgui_buffer, 4, 0) >= 0 && // READ POINTER TO CLASS NtReadVirtualMemory(process_handle, *(int*)vgui_buffer, vgui_buffer, 4, 0) >= 0 && // READ POINTER TO VIRTUAL TABLE NtReadVirtualMemory(process_handle, *(int*)vgui_buffer, vgui_buffer, sizeof(vgui_buffer), 0) >= 0) { for (vtable_index = 0 vtable_index < 984 vtable_index += 4 ) // 984/4 VTABLE ENTRY COUNT { NtReadVirtualMemory(process_handle, *(int*)&vgui_buffer[vtable_index], &vtable_entry, sizeof(vtable_entry), 0 if (*(int*)&vgui_buffer[vtable_index] < module_entry.modBaseAddr || *(int*)&vgui_buffer[vtable_index] >= module_entry.modBaseAddr + module_entry.modBaseSize || vtable_entry == 0xCC ) { vgui_vtable_report.unknown = 0 vgui_vtable_report.report_id = 0x3B vgui_vtable_report.vtable_index = vtable_index vgui_vtable_report.address = *(int*)&vgui_buffer[vtable_index battleye::report(&vgui_vtable_report, sizeof(vgui_vtable_report), 0 } } }
Данная процедура проверяет наличие изменений по смещению 48378, которое является расположением области кода:
push 04 push offset aCBuildslaveSte_4 ; "c:\buildslave\steam_rel_client_win32"... push offset aAssertionFaile_7 ; "Assertion Failed: IsValidIndex(elem)"
Затем процедура проверяет наличие очень специфического и кажущегося мусорным изменения:
push 04 push 00 push 02 push ??
Нам не удалось найти копию vgui2_s.dll, не соответствующую первой из двух вышеприведённых проверок, поэтому не можем узнать, какую vtable она проверяет.
Потоки оверлея игр Steam
Потоки в процессе оверлея игр Steam тоже перебираются:
void gameoverlay::check_thread(THREADENTRY32 thread_entry) { const auto tread_handle = OpenThread(THREAD_SUSPEND_RESUME|THREAD_GET_CONTEXT, 0, thread_entry.th32ThreadID if (thread_handle) { suspend_count = ResumeThread(thread_handle if (suspend_count > 0) { SuspendThread(thread_handle gameoverlay_thread_report.unknown = 0 gameoverlay_thread_report.report_id = 0x3B gameoverlay_thread_report.suspend_count = suspend_count battleye::report(&gameoverlay_thread_report, sizeof(gameoverlay_thread_report), 0 } if (GetThreadContext(thread_handle, &context) && context.Dr7) { gameoverlay_debug_report.unknown = 0 gameoverlay_debug_report.report_id = 0x3B gameoverlay_debug_report.debug_register = context.Dr0 battleye::report(&gameoverlay_debug_report, sizeof(gameoverlay_debug_report), 0 } } }
LSASS
Адресное пространство памяти процесса Windows lsass.exe, также известного как процесс Local Security Authority, тоже перебирается и о всех аномалиях сообщается серверу, как и в случае двух предыдущих проверок:
if (equals(process_entry.executable_path, "lsass.exe")) { auto lsass_handle = OpenProcess(QueryInformation, 0, (unsigned int)process_entry.th32ProcessID if (lsass_handle) { for (address = 0 NtQueryVirtualMemory(lsass_handle, address, 0, &lsass_memory_info, 0x30, &bytes_needed) >= 0 address = lsass_memory_info.base_address + lsass_memory_info.region_size) { if (lsass_memory_info.state == MEM_COMMIT && lsass_memory_info.type == MEM_PRIVATE && (lsass_memory_info.protect == PAGE_EXECUTE || lsass_memory_info.protect == PAGE_EXECUTE_READ || lsass_memory_info.protect == PAGE_EXECUTE_READWRITE)) { // FOUND EXECUTABLE MEMORY OUTSIDE OF MODULES lsass_report.unknown = 0 lsass_report.report_id = 0x42 lsass_report.base_address = lsass_memory_info.base_address lsass_report.region_size = lsass_memory_info.region_size lsass_report.memory_info = lsass_memory_info.type | lsass_memory_info.protect | lsass_memory_info.state battleye::report(&lsass_report, sizeof(lsass_report), 0 } } CloseHandle(lsass_handle } }
LSASS ранее использовался в эксплойтах для выполнения операций с памятью, так как любой процесс, которому нужно Интернет-подключение, должен предоставить доступ к нему LSASS. В настоящее время BattlEye справляется с этой проблемой, вручную очищая дескриптор процесса от операций чтения/записи, а затем подключая ReadProcessMemory/WriteProcessMemory, перенаправляя вызовы на свой драйвер BEDaisy. Далее BEDaisy решает, является ли операция с памятью законной. Если он считает, что операция законна, то он продолжает её, а в противном случае намеренно включает машине синий экран.
Различные уведомления
BattlEye собирает различную информацию и отправляет её серверу с id уведомления 3C. Эта информация состоит из следующих элементов:
- Любое окно с флагом WS_EX_TOPMOST или его аналогами:
- Текст окна (Unicode)
- Имя класса окна (Unicode)
- Window style
- Window extended style
- Прямоугольник окна
- Путь к образу процесса-владельца
- Размер образа процесса-владельца
- Любой процесс с открытым игре дескриптором процесса (VM_WRITE|VM_READ)
- Имя образа
- Путь к образу
- Размер образа
- Доступ к дескриптору
- Размер отдельных файлов игры:
- ….ContentPaksTslGame-WindowsNoEditor_assets_world.pak
- ….ContentPaksTslGame-WindowsNoEditor_ui.pak
- ….ContentPaksTslGame-WindowsNoEditor_sound.pak
- Содержимое отдельных файлов игры:
- ….BLGameCookedContentScriptBLGame.u
- Информация об обходе NtGetContextThread
- Выполняются все инструкции перехода (E9), и адрес назначения записывается в лог
NoEye
В BattlEye реализована довольно ленивая проверка обнаружения публично доступного руткита для обхода этого античита под названием NoEye: система при помощи GetFileAttributesExA проверяет размер файла BE_DLL.dll, если эта библиотека найдена на диске.
void noeye::detect() { WIN32_FILE_ATTRIBUTE_DATA file_information if (GetFileAttributesExA("BE_DLL.dll", 0, &file_information)) { noeye_report.unknown = 0 noeye_report.report_id = 0x3D noeye_report.file_size = file_information.nFileSizeLow battleye::report(&noeye_report, sizeof(noeye_report), 0 } }
Наличие драйверов
Проверяется наличие устройств Beep и Null; в случае их наличия создаётся уведомление. В обычном состоянии эти два устройства не доступны в системе, и это может говорить о том, что кто-то вручную включил устройство. Такая техника называется driver device hijacking. Это делается для того, чтобы обеспечить обмен данными IOCTL с зловредным драйвером без необходимости отдельного объекта драйвера для этого драйвера.
void driver::check_beep() { auto handle = CreateFileA("\\.\Beep", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0 if (handle != INVALID_HANDLE_VALUE) { beep_report.unknown = 0 beep_report.report_id = 0x3E battleye::report(&beep_report, sizeof(beep_report), 0 CloseHandle(handle } }
void driver::check_null() { auto handle = CreateFileA("\\.\Null", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0 if (handle != INVALID_HANDLE_VALUE) { null_report.unknown = 0 null_report.report_id = 0x3E battleye::report(&null_report, sizeof(null_report), 0 CloseHandle(handle } }
Sleep delta
Кроме того, BattlEye может запрашивать у текущего потока одну секунду бездействия и измеряет разность количества циклов до и после бездействия (sleep):
void sleep::check_delta() { const auto tick_count = GetTickCount Sleep(1000 const auto tick_delta = GetTickCount() - tick_count if (tick_delta >= 1200) { sleep_report.unknown = 0 sleep_report.report_id = 0x45 sleep_report.delta = tick_delta battleye::report(&sleep_report, sizeof(sleep_report), 0 } }
7zip
В BattlEye добавлена очень ленивая проверка целостности, чтобы пользователи не могли загружать библиотеку 7zip в процессы игры и перезаписывать области. Это делалось пользователями, чтобы снизить серьёзность описанных ранее сканирований паттернов и обнаружения аномалий. В BattlEye просто решили добавить проверки целостности для этой конкретной библиотеки 7zip.
void module::check_7zip() { constexpr auto sz_7zipdll = "..\..\Plugins\ZipUtility\ThirdParty\7zpp\dll\Win64\7z.dll" const auto module_handle = GetModuleHandleA(sz_7zipdll if (module_handle && *(int*)(module_handle + 0x1000) != 0xFF1441C7) { sevenzip_report.unknown_1 = 0 sevenzip_report.report_id = 0x46 sevenzip_report.unknown_2 = 0 sevenzip_report.data1 = *(__int64*)(module_handle + 0x1000 sevenzip_report.data2 = *(__int64*)(module_handle + 0x1008 battleye::report(&sevenzip_report, sizeof(sevenzip_report), 0 } }
Уровень аппаратных абстракций
BattlEye проверяет наличие динамически подключаемой библиотеки уровня аппаратных абстракций Windows (hal.dll), и сообщает серверу, загружена ли она внутри процесса игры.
void module::check_hal() { const auto module_handle = GetModuleHandleA("hal.dll" if (module_handle) { hal_report.unknown_1 = 0 hal_report.report_id = 0x46 hal_report.unknown_2 = 2 hal_report.data1 = *(__int64*)(module_handle + 0x1000 hal_report.data2 = *(__int64*)(module_handle + 0x1008 battleye::report(&hal_report, sizeof(hal_report), 0 } }
Проверки образов
Также BattlEye проверяет различные образы, загруженные в процесс игры. Эти модули предположительно являются подписанными образами, которыми каким-то образом манипулируют, изменяя их поведение на зловредное, но подробнее мы ничего сказать о них не можем, только об их обнаружении:
nvToolsExt64_1
void module::check_nvtoolsext64_1 { const auto module_handle = GetModuleHandleA("nvToolsExt64_1.dll" if (module_handle) { nvtools_report.unknown = 0 nvtools_report.report_id = 0x48 nvtools_report.module_id = 0x5A8 nvtools_report.size_of_image = (PE_HEADER*)(module_handle + (DOS_HEADER*)(module_handle)->e_lfanew))->SizeOfImage battleye::report(&nvtools_report, sizeof(nvtools_report), 0 } }
ws2detour_x96
void module::check_ws2detour_x96 { const auto module_handle = GetModuleHandleA("ws2detour_x96.dll" if (module_handle) { ws2detour_report.unknown = 0 ws2detour_report.report_id = 0x48 ws2detour_report.module_id = 0x5B5 ws2detour_report.size_of_image = (PE_HEADER*)(module_handle + (DOS_HEADER*)(module_handle)->e_lfanew))->SizeOfImage battleye::report(&ws2detour_report, sizeof(ws2detour_report), 0 } }
networkdllx64
void module::check_networkdllx64 { const auto module_handle = GetModuleHandleA("networkdllx64.dll" if (module_handle) { const auto dos_header = (DOS_HEADER*)module_handle const auto pe_header = (PE_HEADER*)(module_handle + dos_header->e_lfanew const auto size_of_image = pe_header->SizeOfImage if (size_of_image < 0x200000 || size_of_image >= 0x400000) { if (pe_header->sections[DEBUG_DIRECTORY].size == 0x1B20) { networkdll64_report.unknown = 0 networkdll64_report.report_id = 0x48 networkdll64_report.module_id = 0x5B7 networkdll64_report.data = pe_header->TimeDatestamp battleye::report(&networkdll64_report, sizeof(networkdll64_report), 0 } } else { networkdll64_report.unknown = 0 networkdll64_report.report_id = 0x48 networkdll64_report.module_id = 0x5B7 networkdll64_report.data = pe_header->sections[DEBUG_DIRECTORY].size battleye::report(&networkdll64_report, sizeof(networkdll64_report), 0 } } }
nxdetours_64
void module::check_nxdetours_64 { const auto module_handle = GetModuleHandleA("nxdetours_64.dll" if (module_handle) { nxdetours64_report.unknown = 0 nxdetours64_report.report_id = 0x48 nxdetours64_report.module_id = 0x5B8 nxdetours64_report.size_of_image = (PE_HEADER*)(module_handle + (DOS_HEADER*)(module_handle)->e_lfanew))->SizeOfImage battleye::report(&nxdetours64_report, sizeof(nxdetours64_report), 0 } }
nvcompiler
void module::check_nvcompiler { const auto module_handle = GetModuleHandleA("nvcompiler.dll" if (module_handle) { nvcompiler_report.unknown = 0 nvcompiler_report.report_id = 0x48 nvcompiler_report.module_id = 0x5BC nvcompiler_report.data = *(int*)(module_handle + 0x1000 battleye::report(&nvcompiler_report, sizeof(nvcompiler_report), 0 } }
wmp
void module::check_wmp { const auto module_handle = GetModuleHandleA("wmp.dll" if (module_handle) { wmp_report.unknown = 0 wmp_report.report_id = 0x48 wmp_report.module_id = 0x5BE wmp_report.data = *(int*)(module_handle + 0x1000 battleye::report(&wmp_report, sizeof(wmp_report), 0 } }
Идентификаторы перечисления модулей
Приведём для справки id перечисления для модулей:
enum module_id { nvtoolsext64 = 0x5A8, ws2detour_x96 = 0x5B5, networkdll64 = 0x5B7, nxdetours_64 = 0x5B8, nvcompiler = 0x5BC, wmp = 0x5BE
Сканирование таблиц TCP
Шелл-код BattlEye выполняет поиск списка TCP-соединений всей системы (известного под названием «таблица TCP», TCP table) и сообщает, если пользователь подключен хотя бы к одному из IP-адресов Cloudflare-шлюза, принадлежащих немецкому веб-сайту pay-to-cheat под названием xera.ph. Этот механизм был добавлен в шелл-код для обнаружения пользователей, у которых при работе игры запущен launcher, благодаря чему они легко распознаются. Единственная проблема этого механизма заключается в том, что IP-адреса Cloudflare-шлюза позже могут сменить владельцев. и если их новый владелец будет распространять ПО, подключаемое к его серверам через определённый порт, то без всяких сомнений будут происходить ложноположительные срабатывания античита.
Пользователи провайдера услуг pay-to-cheat xera.ph уже долгое время сообщают о том, что их ловят, и разработчики никак не могут с этим справиться. Мы связались с разработчиками xera.ph, чтобы сообщить им об их глупом поведении, но они неправильно нас поняли и отправили нам бесплатную копию, не подумав, что мы сможем взломать и зарелизить её. Мы так не поступим, но, наверно, не стоит бесплатно отправлять проприетарные лицензионные двоичные файлы людям, занимающимся реверс-инжинирингом, и ожидать, что их не спиратят.
void network::scan_tcp_table { memset(local_port_buffer, 0, sizeof(local_port_buffer for (iteration_index = 0 iteration_index < 500 ++iteration_index) { // GET NECESSARY SIZE OF TCP TABLE auto table_size = 0 GetExtendedTcpTable(0, &table_size, false, AF_INET, TCP_TABLE_OWNER_MODULE_ALL, 0 // ALLOCATE BUFFER OF PROPER SIZE FOR TCP TABLE auto allocated_ip_table = (MIB_TCPTABLE_OWNER_MODULE*)malloc(table_size if (GetExtendedTcpTable(allocated_ip_table, &table_size, false, AF_INET, TCP_TABLE_OWNER_MODULE_ALL, 0) != NO_ERROR) goto cleanup for (entry_index = 0 entry_index < allocated_ip_table->dwNumEntries ++entry_index) { const auto ip_address_match_1 = allocated_ip_table->table[entry_index].dwRemoteAddr == 0x656B1468 // 104.20.107.101 const auto ip_address_match_2 = allocated_ip_table->table[entry_index].dwRemoteAddr == 0x656C1468 // 104.20.108.101 const auto port_match = allocated_ip_table->table[entry_index].dwRemotePort == 20480 if ( (!ip_address_match_1 && !ip_address_match_2) || !port_match) continue for (port_index = 0 port_index < 10 && allocated_ip_table->table[entry_index].dwLocalPort != local_port_buffer[port_index ++port_index) { if (local_port_buffer[port_index]) continue tcp_table_report.unknown = 0 tcp_table_report.report_id = 0x48 tcp_table_report.module_id = 0x5B9 tcp_table_report.data = BYTE1(allocated_ip_table->table[entry_index].dwLocalPort) | (LOBYTE(allocated_ip_table->table[entry_index.dwLocalPort) << 8 battleye::report(&tcp_table_report, sizeof(tcp_table_report), 0 local_port_buffer[port_index] = allocated_ip_table->table[entry_index].dwLocalPort break } } cleanup: // FREE TABLE AND SLEEP free(allocated_ip_table Sleep(10 } }
Типы уведомлений
Приведём для справки все известные типы уведомлений из шелл-кода:
enum BATTLEYE_REPORT_ID { MEMORY_GUARD = 0x21, MEMORY_SUSPICIOUS = 0x2F, WINDOW_TITLE = 0x33, MEMORY = 0x35, PROCESS_ANOMALY = 0x38, DRIVER_BEEP_PRESENCE = 0x3E, DRIVER_NULL_PRESENCE = 0x3F, MISCELLANEOUS_ANOMALY = 0x3B, PROCESS_SUSPICIOUS = 0x40, LSASS_MEMORY = 0x42, SLEEP_ANOMALY = 0x45, MEMORY_MODULE_SPECIFIC = 0x46, GENERIC_ANOMALY = 0x48, MEMORY_MODULE_SPECIFIC2 = 0x5B, }
ссылка на оригинал статьи https://habr.com/ru/post/483068/
Добавить комментарий