Engenharia reversa do popular BattlEye anti-fraude


O BattlEye é predominantemente um anti-batota alemão de terceiros, desenvolvido principalmente por Bastian Heiko Suter, 32 anos. Ele fornece (ou tenta fornecer) aos editores de jogos um sistema anti-fraude fácil de usar que usa mecanismos de proteção geral, bem como a detecção de truques para jogos específicos para otimizar a segurança. Conforme indicado no site do produto, ele sempre permanece no topo da tecnologia moderna e utiliza métodos inovadores de proteção e detecção; obviamente, isso é uma consequência da nacionalidade do desenvolvedor: QUALITY MADE IN GERMANY . O BattlEye consiste em muitos elementos que trabalham juntos para encontrar trapaceiros em jogos que pagaram pelo uso do produto. Os quatro elementos principais são:

  • BEService
    • Um serviço de sistema Windows que se comunica com o servidor BattlEye BEServer , que fornece comunicação cliente-servidor com BEDaisy e BEClient .
  • BEDaisy
    • Um driver de kernel do Windows que registra mecanismos preventivos de processamento de eventos e mini filtros para impedir que trapaceiros modifiquem ilegalmente o jogo
  • Becient
    • Uma biblioteca do Windows conectada dinamicamente, responsável pela maioria dos vetores de detecção, incluindo os descritos neste artigo. Após a inicialização, ele é anexado ao processo do jogo.
  • Beserver
    • Servidor de back-end proprietário, responsável por coletar informações e tomar medidas específicas contra trapaceiros.

Shellcode


Recentemente, uma cópia do código do shell BattlEye apareceu na Internet e decidimos escrever sobre o que exatamente a versão atual do BattlEye está procurando. Não analisamos o BattlEye por seis meses, portanto, nosso último despejo do código do shell provavelmente está desatualizado. Várias partes do código foram recuperadas apenas da memória deste último despejo, supondo que o BattlEye apenas completou o código do shell e não excluiu os procedimentos de detecção anteriores.

Como


O BattlEye supostamente transmite o código de shell do seu servidor para um serviço do Windows chamado BEService. Este serviço se comunica com o módulo BEClient localizado dentro do jogo. A troca de dados é realizada através do \.namedpipeBattleye e até 2018 não era criptografada. Agora, todos os dados transmitidos são criptografados com um xor-encryptor com chaves muito pequenas, o que facilita extremamente a execução de ataques de texto simples conhecidos. Quando o código do shell é transmitido ao cliente, ele é localizado e executado fora de todos os módulos conhecidos, o que facilita a determinação. Para criar um despejo de código de shell, você pode processar as funções padrão da API do Windows, como CreateFile, ReadFile etc., e despejar a área de memória correspondente de todos os módulos de chamada (solicitando informações de memória para o endereço retornado) que estão fora de todos os módulos conhecidos ou verifique periodicamente o espaço de memória virtual do jogo em busca de memória executável fora de todos os módulos conhecidos e despeje-o em disco. Ao mesmo tempo, você precisa acompanhar quais áreas já foram despejadas, para que, como resultado, você não obtenha muitos despejos idênticos.

Explicação


Os fragmentos do pseudocódigo apresentados no artigo são fortemente modificados em prol da beleza. Você não poderá despejar o código do shell BattlEye e reconhecer imediatamente essas partes; o shellcode não contém chamadas de função e muitos algoritmos no artigo são implantados. Mas, na verdade, isso não é importante, porque quando você terminar de ler sobre essa terrível antiguidade, terá a oportunidade de contorná-la (:

Classificação de memória


O mecanismo de detecção mais comum em anti-fraudes é a enumeração de memória e a varredura de memória para procurar imagens de fraude conhecidas . É fácil de implementar e, como o passado mostrou, com a abordagem correta, é bastante eficaz se você não esqueceu os conceitos básicos do assembler e colocou na lista negra o prólogo de uma função comum.

Battleye repete todo o espaço de endereço do processo do jogo (o processo atual neste contexto) e realiza várias verificações no desempenho da página e encontra o código do shell fora do espaço de memória correspondente.

Veja como isso é implementado em 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 } 

Anomalias de memória


O BattlEye marca todas as anomalias no espaço de endereço da memória, principalmente a memória dos módulos executáveis ​​que não correspondem à imagem carregada:

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

Digitalizando para padrões


Como mencionado acima, o BattlEye também verifica a memória dos processos locais quanto à presença de vários padrões claramente definidos, como pode ser visto na implementação mostrada abaixo.

Ao ler esse pseudo-código, você pode supor que essas verificações possam ser contornadas reescrevendo a área de código de cada módulo carregado, pois elas não serão varridas em busca de padrões em imagens conhecidas. Para não cair nas verificações de integridade, é necessário fazer o download de todas as áreas compactadas e na lista de permissões e reescrever as áreas de código marcadas como RWX , porque não podemos executar verificações de integridade sem emular o empacotador. Na versão atual do código de shell BattlEye, esses padrões de memória são codificados:

 [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 

Esses padrões de memória também contêm um cabeçalho de dois bytes, ou seja, o valor estático desconhecido 05 e um identificador exclusivo.

O que não veremos é que o BattlEye também transmite dinamicamente padrões do BEServer e os envia para o BEClient , mas não discutiremos isso no artigo.

Eles são verificados iterativamente pelo seguinte algoritmo:

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

Validação de módulos específicos (Microsoft)


As verificações de módulo relatam a presença de módulos específicos carregados no jogo:

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

Verificando módulos específicos (desconhecido)


Uma verificação de módulos específicos foi adicionada ao sistema, que sinaliza ao servidor que você carregou os módulos que atendem a qualquer um destes critérios:

 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 } 

Não sabemos quais módulos atendem a esses critérios, mas suspeitamos que esta seja uma tentativa de detectar um conjunto muito limitado de módulos de truques específicos.

Adendo: @ how02 nos informou que o módulo action_x64.dll possui um 0x5B12C900 e 0x5B12C900 e contém uma área de código na qual você pode escrever; como mencionado anteriormente, isso pode ser usado para exploração.

Proteção de memória


O BattlEye também implementa um procedimento de detecção muito dúbio, que, em nossa opinião, procura memória com o sinalizador PAGE_GUARD definido , sem realmente verificar se o sinalizador PAGE_GUARD está definido :

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

Classificação da janela


O código do shell BattlEye itera sobre cada uma das janelas atualmente visíveis durante o jogo, ignorando as janelas de cima para baixo (por valor z). GetWindowThreadProcessId janela dentro do jogo são excluídos dessa enumeração, e isso é determinado chamando GetWindowThreadProcessId . Portanto, você pode vincular a função correspondente ao falso proprietário da janela para que o BattlEye não verifique sua janela .

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

Pesquisa anomalia


Se menos de duas janelas estiverem marcadas, uma notificação será enviada ao servidor. Provavelmente, isso é feito para impedir o patch das funções correspondentes que não permitem que o código do shell BattlEye examine qualquer janela:

 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 } 

Classificação do processo


Ao chamar CreateToolhelp32Snapshot itera todos os processos em execução, mas não processa nenhum erro , facilitando o patch e evitando os seguintes procedimentos de detecção:

Verificação de caminho


Se a imagem estiver dentro de pelo menos dois subdiretórios (contando a partir da raiz do disco), o sistema sinalizará os processos se o caminho para a imagem correspondente contiver pelo menos uma destas linhas:

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

Se o caminho para o arquivo executável corresponder a uma dessas linhas, o servidor receberá uma notificação sobre o caminho para o arquivo executável, além de informações sobre se o processo pai é um dos seguintes (contém o bit de sinalizador correspondente enviado ao servidor):

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

Se o cliente não puder abrir o descritor com os direitos QueryLimitedInformation apropriados, ele definirá o bit de sinalizador 0x04 , se a causa do erro quando a chamada do OpenProcess falhar não for ERROR_ACCESS_DENIED , que nos fornece o último contêiner de enumeração para o valor do sinalizador correspondente:

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

Se o processo pai for steam, o sinalizador será definido instantaneamente para o usuário e o servidor será informado sobre isso com o ID de notificação 0x40

Nome da imagem


Se o processo atender a um dos muitos critérios apresentados abaixo, você definirá instantaneamente um sinalizador e isso será relatado ao servidor com o ID de notificação 0x38

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

Sobreposição de jogos do Steam


O BattlEye monitora o processo de sobreposição de jogos do Steam, responsável pela sobreposição no jogo, conhecida pela maioria dos usuários do Steam. O nome completo do host da sobreposição dos Jogos Steam é gameoverlayui.exe ; sabe-se que é frequentemente usado para renderizações de exploração, porque é muito fácil hackear e executar renderizações ilegais na janela do jogo. A verificação tem a seguinte condição:

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

Outras verificações específicas para a sobreposição de jogos Steam são quase idênticas aos procedimentos executados para o próprio processo do jogo, portanto, são omitidas no pseudo-código.

Varredura de memória de sobreposição de vapor


O processo de sobreposição de jogos Steam é verificado quanto a padrões e anomalias. Não fomos capazes de nos aprofundar na toca do coelho e descobrir para que servem esses padrões, porque eles são muito generalizados e provavelmente associados a módulos de truques.

 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 } 

O procedimento de digitalização também procura anomalias na forma de código executável fora das imagens baixadas, sugerindo que os crackers injetaram o código no processo de sobreposição:

 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 } 

Proteção de sobreposição de jogos do Steam


Se o processo de sobreposição de jogos do Steam estiver protegido por alguma proteção dos processos do Windows, como o Light (WinTcb) , o servidor receberá uma notificação sobre isso.

 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 } 

Além disso, se a chamada OpenProcess correspondente retornar ERROR_ACCESS_DENIED ao processo de sobreposição, uma notificação será enviada sobre o usuário com o ID 3B .

Módulos de classificação


Os módulos do processo de sobreposição de jogos do Steam também são pesquisados, em particular, vgui2_s.dll e gameoverlayui.dll . Várias verificações são realizadas para esses módulos, começando com gameoverlayui.dll .

Se essa condição for atendida: [gameoverlayui.dll+6C779] == 08BE55DC3CCCCB8????????C3CCCCCC , o [gameoverlayui.dll+6C779] == 08BE55DC3CCCCB8????????C3CCCCCC do [gameoverlayui.dll+6C779] == 08BE55DC3CCCCB8????????C3CCCCCC endereços no endereço armazenado em bytes ???????? . Se algum desses elementos da vtable estiver fora do módulo de origem gameoverlayui.dll ou apontar para uma instrução int 3 , o usuário será reportado ao servidor com um ID de notificação 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 } } } 

Um procedimento de verificação específico também é executado para o módulo 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 } } } 

Este procedimento verifica alterações no deslocamento 48378, que é o local da área de código:

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

O procedimento verifica uma alteração muito específica e aparentemente óbvia:

 push 04 push 00 push 02 push ?? 

Não foi possível encontrar uma cópia do vgui2_s.dll que não correspondia à primeira das duas verificações acima; portanto, não podemos descobrir qual vtable ela verifica.

Fluxos de sobreposição de vapor


Os fluxos no processo de sobreposição de jogos Steam também são movidos:

 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


O espaço de endereço de memória do processo Windows lsass.exe , também conhecido como processo da Autoridade de Segurança Local, também é verificado e todas as anomalias são relatadas ao servidor, como no caso das duas verificações anteriores:

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

O LSASS foi usado anteriormente em explorações para executar operações com memória, pois qualquer processo que precise de uma conexão com a Internet deve fornecer acesso a LSASS. No momento, o BattlEye está lidando com esse problema, limpando manualmente o identificador do processo das operações de leitura / gravação e, em seguida, anexando ReadProcessMemory/ WriteProcessMemory, redirecionando as chamadas para o driver BEDaisy. Em seguida, o BEDaisy decide se a operação de memória é legal. Se ele acredita que a operação é legal, ele continua e, caso contrário, deliberadamente liga a máquina em uma tela azul.

Várias notificações


O BattlEye coleta várias informações e as envia ao servidor com o ID da notificação 3C. Esta informação consiste nos seguintes elementos:

  • Qualquer janela com o sinalizador WS_EX_TOPMOST ou seus análogos:
    • (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


O BattlEye implementa uma verificação bastante preguiçosa para detectar um rootkit acessível ao público para contornar esse anti-fraude chamado NoEye: o sistema usa GetFileAttributesExA para verificar o tamanho do arquivo BE_DLL.dllse essa biblioteca for encontrada no disco.

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

Disponibilidade do driver


Verifica bip e dispositivos nulos; se houver, uma notificação é gerada. No estado normal, esses dois dispositivos não estão disponíveis no sistema e isso pode indicar que alguém ligou o dispositivo manualmente. Essa técnica é chamada seqüestro de dispositivo de driver. Isso é para garantir que os dados IOCTL sejam trocados com o driver mal-intencionado sem a necessidade de um objeto de driver separado para esse driver.

 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 do sono


Além disso, o BattlEye pode solicitar um segundo de inatividade do encadeamento atual e mede a diferença no número de ciclos antes e depois da inatividade (suspensão):

 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


Uma verificação de integridade muito lenta foi adicionada ao BattlEye para que os usuários não possam carregar a biblioteca 7zip nos processos de jogo e sobrescrever áreas. Isso foi feito pelos usuários para reduzir a gravidade das varreduras de padrões descritas anteriormente e a detecção de anomalias. O BattlEye simplesmente decidiu adicionar verificações de integridade para essa biblioteca 7zip específica.

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

Camada de abstração de hardware


O BattlEye verifica a presença de uma biblioteca de camadas de abstração de hardware do Windows vinculada dinamicamente (hal.dll) e informa ao servidor se está carregada dentro do jogo.

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

Verificações de imagem


O BattlEye também verifica várias imagens carregadas no jogo. Esses módulos são imagens supostamente assinadas que são de alguma forma manipuladas, alterando seu comportamento para malicioso, mas não podemos dizer mais nada sobre eles, apenas sobre sua detecção:

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

Identificadores de enumeração de módulo


Para referência, fornecemos o ID da enumeração para os módulos:

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

Digitalizar tabelas TCP


O código de shell BattlEye procura uma lista de conexões TCP para todo o sistema (conhecido como tabela TCP) e informa se o usuário está conectado a pelo menos um dos endereços IP do gateway Cloudflare pertencentes ao site de pagamento alemão -cheat chamado xera.ph . Esse mecanismo foi adicionado ao código do shell para detectar usuários com o iniciador em execução quando o jogo está em execução, tornando-os facilmente reconhecíveis. O único problema com esse mecanismo é que os endereços IP do gateway Cloudflare podem mudar de proprietário posteriormente. e se o novo proprietário distribuir software que se conecta aos servidores por meio de uma porta específica, sem dúvida, ocorrerão gatilhos anti-fraude positivos falsos. Usuários do

provedor de serviços xera.ph que pagam para enganarHá muito tempo, eles relatam que estão sendo capturados e os desenvolvedores não conseguem lidar com isso de nenhuma maneira. Entramos em contato com os desenvolvedores do xera.ph para informá-los sobre seu comportamento estúpido, mas eles nos entenderam mal e nos enviaram uma cópia gratuita, sem pensar que poderíamos hackear e liberá-lo. Nós não faremos isso, mas você provavelmente não deve enviar arquivos binários licenciados proprietários gratuitamente para as pessoas envolvidas na engenharia reversa e espera que eles não sejam pirateados.

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

Tipos de Notificações


Aqui estão para referência todos os tipos conhecidos de notificações do código do 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/pt483068/


All Articles