Rétro-ingénierie du populaire BattlEye anti-triche


BattlEye est principalement un anti-triche allemand tiers, principalement développé par Bastian Heiko Suter, 32 ans. Il fournit (ou tente de fournir) aux éditeurs de jeux un système anti-triche facile à utiliser qui utilise des mécanismes de protection généraux, ainsi que la détection des tricheurs pour des jeux spécifiques afin d'optimiser la sécurité. Comme indiqué sur le site Web du produit, il reste toujours à la pointe de la technologie moderne et utilise des méthodes innovantes de protection et de détection; évidemment, c'est une conséquence de la nationalité du développeur: QUALITY MADE IN GERMANY . BattlEye se compose de nombreux éléments qui travaillent ensemble pour trouver des tricheurs dans les jeux qui ont payé pour l'utilisation du produit. Les quatre éléments principaux sont:

  • BEService
    • Un service système Windows qui communique avec le serveur BattlEye BEServer , qui fournit une communication client-serveur avec BEDaisy et BEClient .
  • BEDaisy
    • Un pilote de noyau Windows qui enregistre les mécanismes de traitement préventif des événements et les mini-filtres pour empêcher les tricheurs de modifier illégalement le jeu
  • Becient
    • Une bibliothèque Windows connectée dynamiquement qui est responsable de la plupart des vecteurs de détection, y compris ceux décrits dans cet article. Après l'initialisation, il s'attache au processus de jeu.
  • Beserver
    • Serveur backend propriétaire, chargé de collecter des informations et de prendre des mesures spécifiques contre les tricheurs.

Shellcode


Récemment, un vidage du code shell BattlEye a fait surface sur Internet, et nous avons décidé d'écrire sur ce que la version actuelle de BattlEye recherche exactement. Nous n'avons pas analysé BattlEye pendant six mois, donc notre dernier vidage du shellcode est très probablement obsolète. Diverses parties du code ont été récupérées uniquement de la mémoire de ce dernier vidage, en supposant que BattlEye n'a terminé que le code shell et n'a pas supprimé les procédures de détection précédentes.

Comment?


BattlEye aurait diffusé le shellcode de son serveur vers un service Windows appelé BEService. Ce service communique avec le module BEClient situé à l'intérieur du jeu. L'échange de données est effectué via le \.namedpipeBattleye et jusqu'en 2018 n'était pas chiffré. Maintenant, toutes les données transmises sont cryptées avec un xor-encryptor avec de très petites clés, ce qui rend extrêmement facile la réalisation d'attaques en clair bien connues. Lorsque le code shell est transmis au client, il est localisé et exécuté en dehors de tous les modules connus, ce qui le rend facile à déterminer. Pour créer un vidage shellcode, vous pouvez soit traiter les fonctions standard de l'API Windows telles que CreateFile, ReadFile, etc., et vider la zone mémoire correspondante de tous les modules appelants (demandant des informations sur la mémoire pour l'adresse renvoyée) qui se trouvent en dehors de tous les modules connus, ou analyser périodiquement l'espace de mémoire virtuelle du jeu à la recherche de mémoire exécutable en dehors de tous les modules connus et la vider sur le disque. Dans le même temps, vous devez garder une trace des zones qui ont déjà été vidées, de sorte que vous n'obtiendrez pas de nombreux vidages identiques.

Explication


Les fragments du pseudocode présentés dans l'article sont fortement modifiés par souci de beauté. Vous ne pourrez pas vider le code shell BattlEye et reconnaître immédiatement ces parties; le shellcode ne contient pas d'appels de fonction et de nombreux algorithmes de l'article sont déployés. Mais en fait, ce n'est pas important, car lorsque vous aurez fini de lire sur cette terrible antiquité, vous aurez l'occasion de la contourner (:

Tri de la mémoire


Le mécanisme de détection le plus courant dans les anti-tricheurs est l'énumération de la mémoire et l'analyse de la mémoire pour rechercher des images de triche connues . Il est facile à mettre en œuvre et, comme le montre le passé, avec la bonne approche, est assez efficace si vous n'avez pas oublié les bases de l'assembleur et mis sur liste noire le prologue d'une fonction commune.

Battleye parcourt tout l'espace d'adressage du processus de jeu (le processus actuel dans ce contexte) et effectue diverses vérifications sur les performances de la page et recherche le code shell en dehors de l'espace mémoire correspondant.

Voici comment il est implémenté dans 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 } 

Anomalies de mémoire


BattlEye marque toutes les anomalies dans l'espace d'adressage mémoire, principalement la mémoire des modules exécutables qui ne correspondent pas à l'image chargée:

 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 } } } 

Recherche de motifs


Comme mentionné ci-dessus, BattlEye analyse également la mémoire des processus locaux pour la présence de divers modèles clairement définis, comme le montre la mise en œuvre illustrée ci-dessous.

Lors de la lecture de ce pseudo-code, vous pouvez deviner que ces contrôles peuvent être contournés en réécrivant la zone de code de chaque module chargé, car ils ne numériseront pas à la recherche de motifs dans les images connues. Afin de ne pas tomber dans les contrôles d'intégrité, vous devez télécharger toutes les zones compressées et sur liste blanche et réécrire les zones de code marquées comme RWX , car nous ne pouvons pas effectuer de contrôles d'intégrité sans émuler le packer. Dans la version actuelle du shellcode BattlEye, ces modèles de mémoire sont codés en dur:

 [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 

Ces modèles de mémoire contiennent également un en-tête à deux octets, à savoir la valeur statique inconnue 05 et un identifiant unique.

Ce que nous ne verrons pas, c'est que BattlEye diffuse également de manière dynamique des modèles depuis BEServer et les envoie à BEClient , mais nous n'en discuterons pas dans l'article.

Ils sont analysés de manière itérative par l'algorithme suivant:

 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 } } } } } 

Validation de modules spécifiques (Microsoft)


La vérification des modules signale la présence de modules spécifiques chargés dans le jeu:

 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 } } 

Vérification de modules spécifiques (inconnu)


Une vérification de modules spécifiques a été ajoutée au système, ce qui signale au serveur que vous avez chargé des modules qui répondent à l' un de ces critères:

 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 } 

Nous ne savons pas quels modules répondent à ces critères, mais nous soupçonnons qu'il s'agit d'une tentative de détection d'un ensemble très limité de modules de triche spécifiques.

Addendum: @ how02 nous a informés que le module action_x64.dll a un 0x5B12C900 et contient une zone de code dans laquelle vous pouvez écrire; comme mentionné précédemment, cela peut être utilisé pour exploiter.

Protection de la mémoire


BattlEye implémente également une procédure de détection très douteuse, qui, à notre avis, recherche de la mémoire avec le drapeau PAGE_GUARD défini , sans réellement vérifier si le drapeau PAGE_GUARD est défini :

 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 } } } } 

Tri des fenêtres


Le code shell BattlEye itère sur chacune des fenêtres actuellement visibles pendant le jeu, en contournant les fenêtres de haut en bas (par valeur z). Les GetWindowThreadProcessId fenêtre à l'intérieur du jeu sont exclus de cette énumération, et cela est déterminé en appelant GetWindowThreadProcessId . Par conséquent, vous pouvez lier la fonction correspondante au faux propriétaire de la fenêtre afin que BattlEye ne vérifie pas votre fenêtre .

 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 } } 

Rechercher une anomalie


Si moins de deux fenêtres sont cochées, une notification est envoyée au serveur. Ceci est probablement fait afin d'empêcher l'application de correctifs aux fonctions correspondantes qui ne permettent pas au code shell BattlEye d'examiner les fenêtres:

 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 } 

Tri des processus


En appelant CreateToolhelp32Snapshot itère sur tous les processus en cours d'exécution, mais ne traite aucune erreur , ce qui facilite le patch et évite les procédures de détection suivantes:

Vérification du chemin


Si l'image se trouve dans au moins deux sous-répertoires (à partir de la racine du disque), le système marquera les processus si le chemin d'accès à l'image correspondante contient au moins une de ces lignes:

 Desktop Temp FileRec Documents Downloads Roaming tmp.ex notepad. ...\. cmd.ex 

Si le chemin d'accès au fichier exécutable correspond à l'une de ces lignes, le serveur reçoit une notification concernant le chemin d'accès au fichier exécutable, ainsi que des informations indiquant si le processus parent est l'un des suivants (contient le bit indicateur correspondant envoyé au serveur):

 steam.exe [0x01] explorer.exe [0x02] lsass.exe [0x08] cmd.exe [0x10] 

Si le client ne peut pas ouvrir le descripteur avec les droits QueryLimitedInformation appropriés, il définira le bit d'indicateur 0x04 , si la cause de l'erreur lorsque l'appel OpenProcess échoue n'est pas ERROR_ACCESS_DENIED , ce qui nous donne le dernier conteneur d'énumération pour la valeur d'indicateur correspondante:

 enum BATTLEYE_PROCESS_FLAG { STEAM = 0x1, EXPLORER = 0x2, ERROR = 0x4, LSASS = 0x8, CMD = 0x10 } 

Si le processus parent est à la vapeur, le drapeau est instantanément défini pour l'utilisateur et le serveur en est informé avec l'ID de notification 0x40

Nom de l'image


Si le processus répond à l'un des nombreux critères présentés ci-dessous, vous définissez instantanément un indicateur et cela est signalé au serveur avec l'ID de notification 0x38

    "Loadlibr"    "Rng "    "A0E7FFFFFF81"    "RNG "    "90E54355"    "2.6.ex"    "TempFile.exe" 

Superposition de jeu Steam


BattlEye surveille le processus de superposition du jeu Steam, qui est responsable de la superposition dans le jeu, connue de la plupart des utilisateurs de Steam. gameoverlayui.exe est le nom d'hôte complet de la superposition Steam Games; il est connu qu'il est souvent utilisé pour le rendu d'exploits, car il est assez facile de pirater et d'effectuer un rendu illégal dans la fenêtre de jeu. La vérification a la condition suivante:

 file size != 0 && image name contains (case insensitive) gameoverlayu 

Les vérifications supplémentaires spécifiques à la superposition des jeux Steam sont presque identiques aux procédures effectuées pour le processus de jeu lui-même, par conséquent, sont omises dans le pseudo-code.

Analyse de la mémoire de superposition de vapeur


Le processus de superposition des jeux Steam est analysé à la recherche de modèles et d'anomalies. Nous n'avons pas pu pénétrer plus profondément dans le terrier du lapin et découvrir à quoi servent ces modèles, car ils sont très généralisés et très probablement associés aux modules de triche.

 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 } 

La procédure de scan recherche également toute anomalie sous forme de code exécutable en dehors des images téléchargées, suggérant que des crackers ont injecté le code dans le processus de superposition:

 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 } 

Protection de superposition de jeux Steam


Si le processus de superposition de jeux Steam est protégé par une protection des processus Windows comme Light (WinTcb) , le serveur recevra une notification à ce sujet.

 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 } 

De plus, si l'appel OpenProcess correspondant renvoie ERROR_ACCESS_DENIED au processus de superposition, une notification est envoyée concernant l'utilisateur avec l'ID 3B .

Modules de tri


Les modules de processus de superposition de jeux Steam sont également recherchés, en particulier, vgui2_s.dll et vgui2_s.dll gameoverlayui.dll . Plusieurs vérifications sont effectuées pour ces modules, en commençant par gameoverlayui.dll .

Si cette condition est remplie: [gameoverlayui.dll+6C779] == 08BE55DC3CCCCB8????????C3CCCCCC , le [gameoverlayui.dll+6C779] == 08BE55DC3CCCCB8????????C3CCCCCC scanne la vtable à l'adresse stockée en octets ???????? . Si l'un de ces éléments vtable se trouve en dehors du module source gameoverlayui.dll ou pointe vers une instruction int 3 , l'utilisateur est signalé au serveur avec un ID de notification de 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 } } } 

Une procédure de vérification spécifique est également effectuée pour le module 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 } } } 

Cette procédure vérifie les modifications du décalage 48378, qui correspond à l'emplacement de la zone de code:

 push 04 push offset aCBuildslaveSte_4 ; "c:\buildslave\steam_rel_client_win32"... push offset aAssertionFaile_7 ; "Assertion Failed: IsValidIndex(elem)" 

La procédure vérifie ensuite une modification très spécifique et apparemment ordonnée:

 push 04 push 00 push 02 push ?? 

Nous n'avons pas pu trouver une copie de vgui2_s.dll qui ne correspond pas à la première des deux vérifications ci-dessus, nous ne pouvons donc pas savoir quelle table virtuelle il vérifie.

Flux de superposition de vapeur


Les flux en cours de superposition de jeux Steam sont également déplacés:

 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


L'espace d'adressage mémoire du processus Windows lsass.exe , également appelé processus Local Security Authority, est également analysé et toutes les anomalies sont signalées au serveur, comme dans le cas de deux vérifications précédentes:

 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 était auparavant utilisé dans des exploits pour effectuer des opérations avec de la mémoire, car tout processus nécessitant une connexion Internet doit lui fournir un accès LSASS. BattlEye traite actuellement ce problème en effaçant manuellement le descripteur de processus des opérations de lecture / écriture, puis en attachant ReadProcessMemory/ WriteProcessMemory, en redirigeant les appels vers son pilote BEDaisy. Ensuite, BEDaisy décide si l'opération de mémoire est légale. S'il pense que l'opération est légale, il la poursuit et allume délibérément la machine sur un écran bleu.

Notifications diverses


BattlEye collecte diverses informations et les envoie au serveur avec l'ID de notification 3C. Ces informations se composent des éléments suivants:

  • Toute fenêtre avec l'indicateur WS_EX_TOPMOST ou ses analogues:
    • (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 implémente une vérification plutôt paresseuse pour détecter un rootkit accessible au public pour contourner cet anti-triche appelé NoEye: le système utilise GetFileAttributesExA pour vérifier la taille du fichier BE_DLL.dllsi cette bibliothèque se trouve sur le disque.

 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 } } 

Disponibilité des pilotes


Vérifie les appareils Beep et Null; le cas échéant, une notification est générée. Dans l'état normal, ces deux périphériques ne sont pas disponibles dans le système, ce qui peut indiquer que quelqu'un a allumé manuellement le périphérique. Cette technique est appelée détournement de périphérique du pilote. Cela permet de garantir que les données IOCTL sont échangées avec le pilote malveillant sans avoir besoin d'un objet pilote distinct pour ce pilote.

 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 } } 

Delta du sommeil


De plus, BattlEye peut demander une seconde d'inactivité au thread actuel et mesure la différence dans le nombre de cycles avant et après l'inactivité (sommeil):

 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


Un contrôle d'intégrité très paresseux a été ajouté à BattlEye afin que les utilisateurs ne puissent pas charger la bibliothèque 7zip dans les processus de jeu et écraser les zones. Cela a été fait par les utilisateurs pour réduire la gravité des analyses de modèle décrites précédemment et la détection des anomalies. BattlEye a simplement décidé d'ajouter des contrôles d'intégrité pour cette bibliothèque 7zip particulière.

 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 } } 

Couche d'abstraction matérielle


BattlEye vérifie la présence d'une bibliothèque de couche d'abstraction matérielle Windows liée dynamiquement (hal.dll) et indique au serveur si elle est chargée à l'intérieur du jeu.

 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 } } 

Vérifications d'image


BattlEye vérifie également diverses images chargées dans le jeu. Ces modules sont censés être des images signées qui sont en quelque sorte manipulées, changeant leur comportement en malveillants, mais nous ne pouvons rien dire de plus à leur sujet, seulement sur leur détection:

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 } } 

Identificateurs d'énumération de module


Pour référence, nous donnons l'ID d'énumération pour les modules:

 enum module_id { nvtoolsext64 = 0x5A8, ws2detour_x96 = 0x5B5, networkdll64 = 0x5B7, nxdetours_64 = 0x5B8, nvcompiler = 0x5BC, wmp = 0x5BE 

Analyser les tables TCP


Le shellcode BattlEye recherche une liste de connexions TCP pour l'ensemble du système (connue sous le nom de table TCP) et signale si l'utilisateur est connecté à au moins une des adresses IP de passerelle Cloudflare appartenant au site Web payant allemand -cheat appelé xera.ph . Ce mécanisme a été ajouté au code shell pour détecter les utilisateurs dont le lanceur est en cours d'exécution lorsque le jeu est en cours d'exécution, ce qui les rend faciles à reconnaître. Le seul problème avec ce mécanisme est que les adresses IP de la passerelle Cloudflare peuvent changer de propriétaire ultérieurement. et si leur nouveau propriétaire distribue un logiciel qui se connecte à ses serveurs via un port spécifique, alors sans aucun doute de faux déclencheurs anti-triche positifs se produiront. Utilisateurs de

fournisseurs de services xera.ph payantsPendant longtemps, ils ont signalé qu'ils étaient pris et les développeurs ne peuvent en aucun cas y faire face. Nous avons contacté les développeurs de xera.ph pour les informer de leur comportement stupide, mais ils nous ont mal compris et nous ont envoyé une copie gratuite, sans penser que nous pourrions pirater et publier. Nous ne le ferons pas, mais vous ne devriez probablement pas envoyer gratuitement des fichiers binaires propriétaires sous licence aux personnes impliquées dans le reverse engineering et vous attendre à ce qu'elles ne soient pas piratées.

 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 } } 

Types de notifications


Voici pour référence tous les types de notifications connus du code shell:

 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, } 

Source: https://habr.com/ru/post/fr483068/


All Articles