Mudanças no antichita popular do BattlEye e maneiras de contorná-los


Principais atualizações do código do shell do BattlEye


O tempo passa, os anti-cheats mudam e, para aumentar a eficácia do produto, as funções aparecem e desaparecem neles. Há um ano, preparei uma descrição detalhada do código de shell BattlEye no meu blog [ tradução em Habré], e esta parte do artigo será um reflexo simples das alterações feitas no código de shell.

Timestamps na lista negra


Em uma análise recente do BattlEye, havia apenas dois carimbos de data e hora de compilação na lista de proibições de sombra, e parece que os desenvolvedores decidiram adicionar muito mais:

0x5B12C900 (action_x64.dll)
0x5A180C35 (TerSafe.dll, Epic Games)
0xFC9B9325 (?)
0x456CED13 (d3dx9_32.dll)
0x46495AD9 (d3dx9_34.dll)
0x47CDEE2B (d3dx9_32.dll)
0x469FF22E (d3dx9_35.dll)
0x48EC3AD7 (D3DCompiler_40.dll)
0x5A8E6020 (?)
0x55C85371 (d3dx9_32.dll)
0x456CED13 (?)
0x46495AD9 (D3DCompiler_40.dll)
0x47CDEE2B (D3DX9_37.dll)
0x469FF22E (?)
0x48EC3AD7 (?)
0xFC9B9325 (?)
0x5A8E6020 (?)
0x55C85371 (?)


Não consegui identificar os carimbos de data / hora restantes e os dois 0xF ******* são os hashes criados pelos assemblies determinísticos do Visual Studio. Agradecemos a @mottikraus e T0B1 por identificar alguns registros de data e hora.

Verificações do módulo


Como a análise principal mostrou, o principal recurso do BattlEye é a enumeração de módulos e, a partir do momento da última análise, outro módulo foi adicionado à lista:

 void battleye::misc::module_unknown1() { if (!GetProcAddress(current_module, "NSPStartup")) return; if (optional_header.data_directory[4].size == 0x1B20 || optional_header.data_directory[4].size == 0xE70 || optional_header.data_directory[4].size == 0x1A38 || timestamp >= 0x5C600000 && timestamp < 0x5C700000) { report_module_unknown report = {}; report.unknown = 0; report.report_id = 0x35; report.val1 = 0x5C0; report.timestamp = timestamp; report.image_size = optional_header.size_of_image; report.entrypoint = optional_header.address_of_entry_point; report.directory_size = optional_header.data_directory[4].size; battleye::report(&report, sizeof(report), false); } } 

Provavelmente, essa é a detecção de determinadas DLLs do proxy, pois o tamanho da tabela de redirecionamento é verificado aqui.

Títulos das janelas


Na análise anterior, vários provedores de truques foram marcados com nomes de janelas, mas desde então o código de shell parou de verificar esses cabeçalhos de janela. A lista de títulos de janelas foi completamente substituída por:

Chod's
Satan5


Nomes das imagens


O BattlEye é famoso por usar métodos de detecção muito primitivos, e um deles é uma lista negra de nomes de imagens. A cada ano, a lista de nomes proibidos de imagens está aumentando e, nos últimos 11 meses, cinco novas foram adicionadas:

frAQBc8W.dll
C:\\Windows\\mscorlib.ni.dll
DxtoryMM_x64.dll
Project1.dll
OWClient.dll

Vale ressaltar que a presença de um módulo com um nome correspondente a qualquer um dos itens da lista não significa que você será banido imediatamente. O mecanismo de relatório também transmite informações básicas do módulo, que provavelmente são usadas para distinguir truques de colisões no servidor BattlEye.

7 zip


O 7-Zip foi amplamente utilizado e continua sendo usado pelos participantes na cena de trapaça como um preenchedor de memória para espaços vazios (code-caves). O BattlEye tenta lidar com isso executando uma verificação de integridade muito ruim, que mudou desde o meu artigo anterior:

 void module::check_7zip() { const auto module_handle = GetModuleHandleA("..\\..\\Plugins\\ZipUtility\\ThirdParty\\7zpp\\dll\\Win64\\7z.dll"); // --- REMOVED --- // if (module_handle && *(int*)(module_handle + 0x1000) != 0xFF1441C7) // --- ADDED --- if (module_handle && *(int*)(module_handle + 0x1008) != 0x83485348) { 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), false); } } 

Parece que os desenvolvedores do BattlEye imaginaram que meu artigo anterior levou muitos usuários a ignorar essa verificação simplesmente copiando os bytes desejados para o local verificado pelo BattlEye. Como eles resolveram a situação? Mudamos a verificação em oito bytes e continuamos a usar o mesmo método ruim de verificar a integridade. A partição executável somente leitura, e tudo o que você precisa fazer é baixar o 7-Zip do disco e comparar as partições movidas entre si; se houver discrepâncias, algo está errado. Sério, pessoal, realizar verificações de integridade não é tão difícil.

Verificação de rede


Enumerar a tabela TCP ainda funciona, mas depois que lancei uma análise anterior criticando os desenvolvedores por sinalizarem os endereços IP do Cloudflare, eles ainda removeram essa verificação. O Anti-cheat ainda relata a porta que o xera.ph usa para a conexão, mas os desenvolvedores adicionaram uma nova verificação para determinar se o processo com a conexão tem proteção ativa (presumivelmente isso é feito usando o manipulador).

 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) { // --- REMOVED --- // 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 // +++ ADDED +++ const auto target_process = OpenProcess(QueryLimitedInformation, 0, ip_table->table[entry_index].dwOwningPid); const auto protected = target_process == INVALID_HANDLE && GetLastError() == 0x57; if (!protected) { CloseHandle(target_process); return; } const auto port_match = allocated_ip_table->table[entry_index].dwRemotePort == 20480; 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), false); local_port_buffer[port_index] = allocated_ip_table->table[entry_index].dwLocalPort; break } } cleanup: // FREE TABLE AND SLEEP free(allocated_ip_table); Sleep(10 } } 

Obrigado IChooseYou e resumo

BattlEye Stack Bypass


Jogos de hackers são um jogo constante de gato e rato, então rumores de novos truques estão se espalhando como fogo. Nesta parte, veremos novas técnicas heurísticas que foram recentemente adicionadas ao nosso arsenal por um grande fornecedor de BattlEye anti-cheats. Na maioria das vezes, essas técnicas são chamadas de stack walking. Geralmente eles são implementados processando uma função e percorrendo a pilha para descobrir quem chamou especificamente essa função. Por que você precisa fazer isso? Como qualquer outro programa, os hacks de videogame têm um conjunto de funções conhecidas que eles usam para obter informações do teclado, enviar para o console ou calcular certas expressões matemáticas. Além disso, os hackers de videogame adoram esconder sua existência, seja na memória ou no disco, para que o software anti-fraude não os encontre. Mas o que os programas de truques esquecem é que eles chamam funções regularmente de outras bibliotecas, e isso pode ser usado para detectar heuristicamente truques desconhecidos. Ao implementar o mecanismo de deslocamento de pilha para funções como std::print , podemos encontrar esses truques, mesmo que estejam mascarados.

O BattlEye implementou um “desvio de pilha”, apesar de isso não ter sido anunciado publicamente e no momento da publicação do artigo havia apenas rumores. Preste atenção às aspas - o que você verá aqui não é realmente um tour de pilha real, mas apenas uma combinação de verificar o endereço de retorno e o dump do programa de chamada. Uma implementação de passagem de pilha verdadeira passaria pela pilha e geraria uma pilha de chamadas real.

Como expliquei em um artigo anterior sobre o BattlEye, o sistema anti-fraude transmite dinamicamente o código do shell para o jogo quando está em execução. Esses códigos de shell têm tamanhos e tarefas diferentes e não são transmitidos simultaneamente. Uma propriedade notável desse sistema é que os pesquisadores precisam analisar dinamicamente o anti-cheat durante a partida multiplayer, o que complica a determinação das características desse anti-cheat. Ele também permite que o anti-fraude aplique várias medidas a diferentes usuários, por exemplo, para transferir um módulo mais profundamente invasivo apenas para uma pessoa que tenha uma taxa incomumente alta de assassinatos e mortes, e similares.

Um desses códigos de shell, BattlEye, é responsável por executar essa análise de pilha; o chamaremos shellcode8kb porque é um pouco menor em comparação com o shellcodemain , que eu documentei aqui . Esse pequeno código de shell usando a função AddVectoredExceptionHandler prepara um manipulador de exceções vetorizado e define traps de interrupção nas seguintes funções:

GetAsyncKeyState
GetCursorPos
IsBadReadPtr
NtUserGetAsyncKeyState
GetForegroundWindow
CallWindowProcW
NtUserPeekMessage
NtSetEvent
sqrtf
__stdio_common_vsprintf_s
CDXGIFactory::TakeLock
TppTimerpExecuteCallback


Para fazer isso, ele simplesmente percorre a lista de funções usadas de maneira padrão, configurando a primeira instrução da função correspondente como int3 , que é usada como um ponto de interrupção. Após definir um ponto de interrupção, todas as chamadas para a função correspondente passam pelo manipulador de exceções, que tem acesso total aos registradores e à pilha. Com esse acesso, o manipulador de exceções despeja o endereço do programa de chamada da parte superior da pilha e, se uma das condições heurísticas for atendida, 32 bytes da função de chamada serão despejados e enviados ao servidor BattlEye com o identificador de relatório 0x31 :

 __int64 battleye::exception_handler(_EXCEPTION_POINTERS *exception) { if (exception->ExceptionRecord->ExceptionCode != STATUS_BREAKPOINT) return 0; const auto caller_function = *(__int64 **)exception->ContextRecord->Rsp; MEMORY_BASIC_INFORMATION caller_memory_information = {}; auto desired_size = 0; // QUERY THE MEMORY PAGE OF THE CALLER const auto call_failed = NtQueryVirtualMemory( GetCurrentProcess(), caller_function, MemoryBasicInformation, &caller_memory_information, sizeof(caller_memory_information), &desired_size) < 0; // IS THE MEMORY SOMEHOW NOT COMMITTED? (WOULD SUGGEST VAD MANIPULATIUON) const auto non_commit = caller_memory_information.State != MEM_COMMIT; // IS THE PAGE EXECUTABLE BUT DOES NOT BELONG TO A PROPERLY LOADED MODULE? const auto foreign_image = caller_memory_information.Type != MEM_IMAGE && caller_memory_information.RegionSize > 0x2000; // IS THE CALL BEING SPOOFED BY NAMAZSO? const auto spoof = *(_WORD *)caller_function == 0x23FF; // jmp qword ptr [rbx] // FLAG ALL ANBORMALITIES if (call_failed || non_commit || foreign_image || spoof) { report_stack.unknown = 0; report_stack.report_id = 0x31; report_stack.hook_id = hook_id; report_stack.caller = (__int64)caller_function; report_stack.function_dump[0] = *caller_function; report_stack.function_dump[1] = caller_function[1]; report_stack.function_dump[2] = caller_function[2]; report_stack.function_dump[3] = caller_function[3]; if (!call_failed) { report_stack.allocation_base = caller_memory_information.AllocationBase; report_stack.base_address = caller_memory_information.BaseAddress; report_stack.region_size = caller_memory_information.RegionSize; report_stack.type_protect_state = caller_memory_information.Type | caller_memory_information.Protect | caller_memory_information.State; } battleye::report(&report_stack, sizeof(report_stack), false); return -1; } } 

Como podemos ver, o manipulador de exceções descarta todas as funções de chamada no caso de uma alteração sem cerimônia na página de memória ou quando a função não pertence a um módulo de processo conhecido (o tipo de página de memória MEM_IMAGE não foi definido pelo manualmappers). Ele também despeja funções de chamada quando falha em chamar NtQueryVirtualMemory, para que os cheats não se vinculem a essa chamada do sistema e ocultam seu módulo do despejo de pilha. A última condição é realmente bastante interessante, marca todas as funções de chamada que usam o dispositivo jmp qword ptr [rbx] - o método usado para "falsificar o endereço de retorno". Foi lançado pelo apelido de meu co-secretário namazso. Parece que os desenvolvedores do BattlEye viram que as pessoas usam esse método de falsificação em seus jogos e decidiram apontar diretamente para ele. Vale ressaltar aqui que o método descrito por namazsos funciona bem, basta usar um dispositivo diferente, ou completamente diferente, ou apenas um registro diferente - isso não importa.

Dica do desenvolvedor do BattlEye: O CDXGIFactory::TakeLock na memória está incorreto porque você (acidentalmente ou intencionalmente) ativou o preenchimento CC, que é muito diferente a cada compilação. Para máxima compatibilidade, você precisa remover o preenchimento (o primeiro byte da assinatura) e, portanto, provavelmente pegará mais trapaceiros :)

A estrutura completa enviada ao servidor BattlEye é assim:

 struct __unaligned battleye_stack_report { __int8 unknown; __int8 report_id; __int8 val0; __int64 caller; __int64 function_dump[4]; __int64 allocation_base; __int64 base_address; __int32 region_size; __int32 type_protect_state; }; 

Reconhecimento de hipervisor no BattlEye


O jogo de gato e rato no campo dos jogos de hackers continua a ser uma fonte de inovação em façanhas e luta contra trapaças. O uso da tecnologia de virtualização em jogos de hackers começou a se desenvolver ativamente após o advento de hipervisores fáceis de usar como DdiMon Satoshi Tanda e o hvpp Peter Benes. Esses dois projetos são usados ​​pelos truques mais pagos da cena dos hackers clandestinos devido ao baixo limite de entrada e à documentação detalhada. É provável que esses lançamentos acelerem a corrida armamentista no campo dos hipervisores, que agora está começando a se manifestar na comunidade de hackers de jogos. Aqui está o que o administrador de uma das maiores comunidades de hackers de jogos com o apelido wlan diz sobre esta situação:

Com o advento de sistemas de hipervisor prontos para uso para jogos de hackers, tornou-se inevitável que anti-cheats como o BattlEye se concentrassem no reconhecimento generalizado da virtualização.

O uso generalizado de hipervisores se deve a melhorias recentes no anti-cheat, que deixaram aos hackers muito poucas oportunidades de modificar os jogos de maneira tradicional. A popularidade dos hipervisores pode ser explicada pela simplicidade de evitar anti-fraude, porque a virtualização simplifica a ocultação de informações usando mecanismos como syscall hooks e MMU virtualization .

Recentemente, o BattlEye implementou o reconhecimento de hipervisores comuns, como as plataformas mencionadas acima (DdiMon, hvpp), usando detecção baseada em tempo. Este reconhecimento tenta detectar valores de tempo de instrução CPUID não padrão. O CPUID é uma instrução de custo relativamente baixo em equipamentos reais, geralmente exigindo apenas duzentos ciclos, e em um ambiente virtual, sua execução pode demorar dez vezes mais devido a operações desnecessárias causadas pelo mecanismo de introspecção. O mecanismo de introspecção é diferente do equipamento real, que simplesmente executa a operação da maneira esperada, porque, com base em um critério arbitrário, rastreia e altera condicionalmente os dados retornados ao hóspede.

Curiosidade: o CPUID é usado ativamente nesses procedimentos de reconhecimento temporário porque é uma instrução com uma saída incondicional, bem como uma instrução com serialização sem privilégios. Isso significa que o CPUID é usado como uma barreira e garante que as instruções antes e depois sejam seguidas; ao mesmo tempo, os horários se tornam independentes da reordenação usual das instruções. Você também pode usar instruções como XSETBV , que também executam uma saída incondicional, mas para garantir o tempo independente, isso exigirá algum tipo de instrução de barreira, para que nenhuma reordenação ocorra antes ou depois dela, afetando a confiabilidade dos tempos.

Reconhecimento


A seguir, é apresentado o procedimento de reconhecimento do módulo BattlEye "BEClient2"; Executei sua engenharia reversa e recriei o código em pseudo-C, e depois o publiquei no twitter . No dia seguinte ao meu tweet, os desenvolvedores do BattlEye inesperadamente mudaram a ofuscação do BEClient2, aparentemente esperando que isso me impedisse de analisar o módulo. A ofuscação anterior não mudou por mais de um ano, mas mudou no dia seguinte ao meu tweet sobre isso - uma velocidade impressionante.

 void battleye::take_time() { // SET THREAD PRIORITY TO THE HIGHEST const auto old_priority = SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL); // CALCULATE CYCLES FOR 1000MS const auto timestamp_calibrator = __rdtsc(); Sleep(1000); const auto timestamp_calibration = __rdtsc() - timestamp_calibrator; // TIME CPUID auto total_time = 0; for (std::size_t count = 0; count < 0x6694; count++) { // SAVE PRE CPUID TIME const auto timestamp_pre = __rdtsc(); std::uint32_t cpuid_data[4] = {}; __cpuid(cpuid_data, 0); // SAVE THE DELTA total_time += __rdtsc() - timestamp_pre; } // SAVE THE RESULT IN THE GLOBAL REPORT TABLE battleye::report_table[0x1A8] = 10000000 * total_time / timestamp_calibration / 0x65; // RESTORE THREAD PRIORITY SetThreadPriority(GetCurrentThread(), old_priority); } 

Como eu disse acima, esta é a técnica de reconhecimento mais comum usando instruções interceptadas incondicionalmente. No entanto, é vulnerável ao tempo falso, e falaremos sobre isso em detalhes na próxima seção.

Bypass de reconhecimento


Este método de reconhecimento tem problemas. Em primeiro lugar, é propenso ao tempo falso, o que geralmente é feito de duas maneiras: alterando o TSC no VMCS ou diminuindo o TSC toda vez que o CPUID é executado. Existem muitas outras maneiras de lidar com ataques baseados em tempo, mas o último é muito mais fácil de implementar, porque você pode garantir que o tempo de execução da instrução esteja dentro de um ou dois ciclos de sincronização de execução em equipamentos reais. A dificuldade de descobrir essa técnica de falsificação de tempo depende da experiência do desenvolvedor. Na próxima seção, veremos o tempo de falsificação e melhoria da implementação criada no BattlEye. A segunda razão para essa falha no método de reconhecimento é que o atraso do CPUID (tempo de execução) em diferentes processadores é muito diferente, dependendo do valor da planilha. Pode levar até 70-300 ciclos de relógio para concluir. O terceiro problema com este procedimento de reconhecimento é usar SetThreadPriority. Essa função do Windows é usada para definir o valor da prioridade de um determinado descritor de fluxo, mas o sistema operacional nem sempre atende à solicitação. Essa função é simplesmente uma sugestão para aumentar a prioridade do encadeamento e não há garantia de que isso aconteça. Assim, é possível que esse método seja afetado por interrupções ou outros processos.

Nesse caso, é fácil ignorar o reconhecimento, e a técnica descrita de contrafação derrota efetivamente esse método de reconhecimento. Se os desenvolvedores do BattlEye quiserem melhorar esse método, a seção a seguir fornece algumas recomendações.

Melhoria


Esse recurso pode ser aprimorado de várias maneiras. Primeiro, você pode desativar intencionalmente as interrupções e forçar a prioridade de um encadeamento alterando CR8 para o IRQL mais alto. Também seria ideal isolar essa verificação em um núcleo da CPU. Outra melhoria: você deve usar cronômetros diferentes, mas muitos deles não são tão precisos quanto o TSC, mas existe um cronômetro chamado APERF, ou Actual Performance Clock. Eu recomendo esse timer porque é mais difícil trapacear com ele e ele só acumula um contador quando o processador lógico está no estado de energia C0. Essa é uma ótima alternativa ao uso do TSC. Você também pode usar o timer ACPI, HPET, PIT, GPU, NTP ou PPERF, que é semelhante ao APERF, mas conta as medidas que são percebidas como instruções de execução. A desvantagem disso é que você precisa habilitar o HWP, que pode ser desativado pelo operador intermediário e, portanto, é inútil.

Abaixo está uma versão aprimorada do procedimento de reconhecimento que deve ser executado no kernel:

 void battleye::take_time() { std::uint32_t cpuid_regs[4] = {}; _disable(); const auto aperf_pre = __readmsr(IA32_APERF_MSR) << 32; __cpuid(&cpuid_regs, 1); const auto aperf_post = __readmsr(IA32_APERF_MSR) << 32; const auto aperf_diff = aperf_post - aperf_pre; // CPUID IET ARRAY STORE // BATTLEYE REPORT TABLE STORE _enable(); } 

Nota: IET significa Tempo de Execução da Instrução.

No entanto, o procedimento ainda pode ser muito confiável na detecção de hipervisores comuns, pois os tempos de execução da CPUID podem variar bastante.Seria melhor comparar o IET das duas instruções. Um deles deve ter um atraso de execução maior que o CPUID. Por exemplo, pode ser FYL2XP1 - uma instrução aritmética que demora um pouco mais para ser concluída do que o IET médio da instrução CPUID. Além disso, ele não causa nenhuma interceptação no hipervisor e seu tempo pode ser medido com segurança. Usando essas duas funções, a função de criação de perfil pode criar uma matriz para armazenar as instruções IET CPUID e FYL2XP1. Usando o temporizador APERF, seria possível obter o relógio inicial de uma instrução aritmética, executar a instrução e calcular o delta do relógio para ela. Os resultados podem ser armazenados na matriz IET por N ciclos de criação de perfil, obtendo o valor médio e repetindo o processo para o CPUID. Se o tempo de execução da instrução CPUID for maior que a instrução aritmética,esse é um sinal confiável de que o sistema é virtual, porque uma instrução aritmética sob nenhuma circunstância poderia gastar mais tempo do que executar o CPUID para obter informações sobre o fabricante ou a versão. Esse procedimento de reconhecimento também poderá detectar aqueles que usam o deslocamento / escala do TSC.

Repito, os desenvolvedores precisariam forçar a ligação ao núcleo computacional para executar essa verificação em um único núcleo, desativar as interrupções e forçar o IRQL a definir o valor máximo para garantir dados consistentes e confiáveis. Seria surpreendente se os desenvolvedores do BattlEye decidissem implementar isso, porque requer muito mais esforço. Existem outras duas rotinas de reconhecimento de máquina virtual no driver do kernel BattlEye, mas este é um tópico para outro artigo.

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


All Articles