Jump to content
  • Segurança em jogos

       (3 reviews)

    esoj

    Neste artigo analisaremos alguns tópicos sobre o tema tanto sob a ótica do desenvolvedor, quanto do hacker, comparando os desafios e técnicas referentes ao desenvolvimento de cheats para jogos online e offline.

    Jogos single player

    Do ponto de vista do desenvolvedor a integridade de um jogo single player raramente apresenta um problema para o produto e a comunidade. Devido à ausência de interação entre os jogadores, hacks pouco podem fazer além de obter feitos incríveis como concluir o jogo em tempos recordes ou desbloquear todas as conquistas para que o jogador possa exibir-se para seus amigos. Às vezes é até mesmo desejável pelo desenvolvedor que os jogadores comprometam a integridade do jogo através da criação de Mods,  por exemplo, pois eles expandem e renovam o jogo, criando formas completamente diferentes de jogá-lo.

    Do ponto de vista do profissional de segurança, jogos são uma maneira incrível de testar o seu potencial de engenharia reversa e até mesmo programação, quando você reescreve alguma seção crítica do game, por exemplo.

    Scanners de memória

    Sem dúvida jogos são programas complexos. Além de toda a lógica de gameplay o programa deve conter as instruções necessárias para renderizar itens na tela, gerenciar dispositivos de som, receber input do usuário, além de diversas outras funções comuns presentes nas engines como pathfinding, sistema de partículas, gerenciamento de animações, etc. Fazer a engenharia reversa de um jogo tentando compreender cada etapa do programa até encontrar as seções relacionadas à lógica de jogo seria como procurar uma agulha no palheiro.

    Por este motivo desenvolveram-se ferramentas para inverter esse processo. Com a ajuda de Scanners de memória, a partir de elementos visuais do jogo como vida ou dinheiro, é possível descobrir o valor da variável desejada. Isso permite ao hacker alterar aquele valor ou usar aquela variável (quem acessa esse endereço?) para identificar seções de código relacionadas, criando exploits ainda mais complexos.

    O processo de scan por valor funciona em três etapas:

    1. Escaneia-se um valor conhecido, como vida, dinheiro, pontuação, etc;
    2. Altera-se essa variável dentro do jogo para que ela mude de valor;
    3. Repete-se as etapas 1 e 2 até obter um número pequeno de possíveis endereços.

    Ao fim do processo, é possível trocar o valor da variável dentro do jogo, obtendo vantagens como grandes quantias de vida ou dinheiro.

    O scan de memória funciona buscando todos os endereços de memória do processo que contém aquele exato valor. Quando o segundo scan é feito, ele utiliza a primeira lista de resultado e remove os endereços que não se adequam ao valor atual. Ao fim do processo, sobram bons palpites para o real endereço da variável:

    Exemplo de Scan de memória usando Cheat engine

    Scan por proximidade

    Esta técnica se baseia a partir da relação de proximidade espacial entre as variáveis. Por exemplo, se em um jogo o personagem possuir uma barra de vida cujo valor exato é incerto, uma estratégia possível é buscar por algum valor conhecido associado e após encontrar aquele valor olhar em suas proximidades para checar se a variável vida está lá. Isso ocorre porque na maioria das vezes os programadores armazenam os dados de cada elemento do jogo em objetos ou structs, que são contínuos na memória:

    Struct player{
      Char nome[50];
      Int dinheiro;
      float vida ;
      float vidaMax ;
      Imagem *sprite;
    }

     

    Exemplo de Scan por proximidade usando Cheat engine

    Fuzzing

    As vezes vale a pena alterar uma região de memória próxima a variáveis importantes e observar se algo mudou dentro do jogo. Apesar de diversas vezes resultar em crashes, às vezes pode render resultados interessantes,descobrindo listas ou IDs de itens:

    Exemplo de Fuzzing obtendo os endereços da lista de parceiros e desbloqueando personagens não jogáveis

    Quem acessa esse endereço? e Injeção de código

    Na maioria dos debuggers é possível adicionar um breakpoint de dado, que trava a execução do programa quando uma instrução faz uma leitura ou uma escrita naquele endereço. Isso pode ser muito útil para encontrar funções relacionadas à aquela variável.

    Em alguns casos além de apenas alterar o valor de variáveis é interessante alterar também trechos de código para que o programa se comporte diferente. Para isso, uma técnica usada para alterar dinamicamente o código em execução é o code hooking. Esta técnica consiste em substituir uma instrução por um jump para uma região de memória não utilizada ou alocada pelo debugger. Nessa região, além do código removido dar espaço ao jump, podemos adicionar novas instruções. Ao fim do hook ele retorna para o endereço subsequente ao pulo:

    image.thumb.png.91e5230e298cdc055b35e0e55e21887e.png

    Esquema de mapa de memória e execução durante um hook

    Em um dos casos que encontrei tratava-se de um jogo onde a pontuação mudava dinamicamente a cada nível, com isto, apesar do valor da variável da pontuação ser conhecida, trocar manualmente seu valor era desconfortável.

    Portanto, adicionando um breakpoint de dado no valor da pontuação encontrei a função que checa se a pontuação foi atingida a cada quadro:

     1.mov eax,[ecx+000000A0]          ;Pontuação atual lida
     2.mov ecx,[ecx+000000A4]          ;Pontuação meta lida
     3.cmp eax,ecx                        ; compara os pontos atuais com a meta
     4.<--je 05AE94C6                    
     5.mov eax,[ebp+08]
     6.mov eax,[eax+000000A8]
     7.test eax,eax
     8.jne 05AE9C7F                      ;Todo este trecho é pulado se não atingirmos a meta
     9.mov eax,[ebp+08]
    10.movzx eax,byte ptr [eax+7C]
    11.test eax,eax
    12.jne 05AE9C7F
    13.-->mov eax,[ebp+08]
    14.mov eax,[eax+28]                  ;resto da função
    15.mov ecx,eax
    ...    

    Apesar de ser muito difícil interpretar o que toda a função faz, apenas as 4 primeiras instruções são importantes.

    Se a pontuação atual for diferente do objetivo, o trecho do meio responsável por passar de fase não roda. Eu tentei remover apenas o jump e forçar sua execução mas o jogo trava, provavelmente devido a uma comparação futura com a pontuação. Portanto, a ideia é roubar o fluxo de execução logo antes da comparação da instrução 3 ser executada.

    Para descobrir o endereço de onde deve ser injetado o pulo é possível fazer um scan por Array of bytes. No caso buscamos o padrão das instruções acima. Essa é uma maneira consistente de manter o cheat funcionando mesmo após atualizações que modifiquem o binário, pois mesmo que o endereço da função mude, dificilmente suas instruções serão alteradas

    O Cheat Engine em especial permite escrever um script que realiza essa busca e aplica o hook. Quando o script está ativo ele aplica os patches necessários, enquanto está desativado ele reverte as alterações para o código original. Dessa forma foi simples criar um botão de auto win para qualquer estágio do jogo:

    aobscan(INJECT,8B 89 A4 00 00 00 3B C1 74) ;sequência de instruções a ser buscada
    alloc(newmem,$1000)
    
    [ENABLE]
    
    code:
      mov eax,[ecx+000000A4]	;Instrução original de leitura - lê a meta
      mov [ecx+000000A0],eax	;copia a meta para a pontuação atual
      mov ecx,eax				;e também para o registrador onde está a pontuação atual
      jmp return				;volta para a execução normal de código

     

    Exemplo de injeção de código criando um botão de auto win

    Exemplo de injeção de código alterando o comportamento dos inimigos para curar o jogador

    Online multiplayer

    Enquanto nos jogos single player o uso de cheats traz pouco ou nenhum problema para os demais jogadores, quando se trata de multiplayer online, sobretudo dos jogos mais competitivos, cheats podem se tornar um pesadelo quando jogadores passam a obter vantagens absurdas sobre os demais, criando um ambiente injusto e frustrante dentro da partida.

    Felizmente as engines que suportam multiplayer já são pensadas com a segurança em mente, mitigando a maioria das técnicas discutidas na seção sobre single player.

    Modelo de cliente servidor

    O modelo de cliente servidor assume que existe uma máquina na rede que possui autoridade sobre as demais máquinas, máquina esta chamada de servidor. O servidor também contém um único estado do jogo que é replicado para os demais clientes. Toda a interação entre jogadores deve passar pelo servidor, já que os clientes não possuem nenhuma conexão entre si. Com isto, é tarefa do servidor validar todos os inputs recebidos antes de replicá-los. Existem duas formas de interação servidor-cliente, são elas Remote Procedure Calls (RPCs) e Replicação de Variáveis.

    RPCs são um pedido feito para outra máquina rodar um código específico e estes pedidos podem ser feitos pelo servidor ou pelo cliente. Por exemplo, se o jogador 1 de um jogo tiro deseja atirar, os outros clientes devem ser capazes de observar que o disparo foi realizado. Para isso ele deve solicitar ao servidor que realize um disparo usando uma RPC. Em seguida, o servidor deve solicitar aos demais jogadores que repliquem esse disparo, partindo da cópia que cada um deles tem do jogador 1:

    image.thumb.png.b1115b7a01667b172238354a59b3fa3b.png

    Exemplo de código multiplayer para FPS implementado na Unreal Engine 4

    A segunda forma de comunicação é a replicação de variáveis. Diferente de RPCs, a replicação ocorre apenas do servidor para o cliente. No exemplo acima a função dar dano altera (server side) a variável "vidaDoPersonagem". Quando isso acontece, o servidor envia um pacote atualizando o seu valor para todos os clientes. Portanto, nenhum cliente possui de fato a variável vida mas uma cópia dela. Por isso as técnicas de alteração de variáveis discutidas na seção de jogos offline raramente funcionam em multiplayers. Embora seja possível alterar essa cópia, o estado do jogo no servidor permanece intacto, então mesmo que você tenha 1000 de vida em sua cópia, quando você morrer no servidor, ainda ocorrerá uma RPC para a sua máquina e para os outros jogadores avisando que você morreu.

    Tradeoff entre jogabilidade e segurança - O problema do input

    Sempre que o servidor recebe uma atualização do cliente ele deve checar se a ação feita pelo jogador é válida, como por exemplo se o jogador ainda possui munição antes de atirar, se a arma não está em recarga, etc. Todavia, algumas ações são praticamente impossíveis de serem verificadas. Um servidor poderia verificar por exemplo todo o input feito pelo jogador, enviando cada botão pressionado para que o servidor calcule coisas como posição ou ângulo da câmera, porém devido à latência de rede e o tempo de processamento isso significaria uma jogabilidade travada e lenta. A menos que o jogo seja lento como uma partida de xadrez, o servidor precisa confiar parcialmente no input dado pelo usuário. Por isso, cheats como speedhacks e aimbots são tão comuns. Posição e orientação de onde o jogador está olhando são basicamente controladas pelo cliente.

    Algumas engines como a Unreal conseguem mitigar os efeitos de possíveis speed hacks calculando em server side a posição que o personagem deveria estar e aplicando as devidas correções no cliente. Essas correções também servem para sincronizar o cliente com o servidor em situações de dessincronia como durante perdas de pacotes:

    Exemplo de correção de posicionamento pelo servidor na Unreal Engine 4

    Padrões inseguros de programação para multiplayer

    • Não validar corretamente a entrada de RPCs;
    • No caso acima um cliente não íntegro poderia passar um valor elevado como parâmetro, dando danos absurdos aos adversários;
    • Não validar o tempo entre RPCs;
    • O jogador não deve atirar milhares de vezes por segundo, mas não tem nada no servidor que impeça isso;
    • Não replicar as variáveis necessárias;
    • O desenvolvedor não deve confiar ao cliente variáveis importantes como vida ou munição.

    image.thumb.png.e21861527fa20aeabd42326036331630.png

    Exemplo de código multiplayer vulnerável

    RCEs e ataques a outros jogadores

    Quando um desenvolvedor publica um jogo ele está distribuindo software que vai rodar no computador de diversos usuários. Como qualquer outro programa, ele é suscetível à ataques clássicos de corrupção de memória como buffer overflow, Use After Free, etc, que podem permitir execução remota de código para outros atacantes que possam interagir com o programa. Esse foi o caso do CVE-2019-15943, onde no Counter-Strike: Global Offensive (CSGO) um jogador poderia convidar outro player para uma partida utilizando um mapa customizado. O mapa customizado era criado especificamente para fazer o jogo crashar, corrompendo a cadeia SHE e potencialmente permitindo a execução de código na máquina do usuário que entrou na partida.

    Acessibilidade ao código, Unity e Among Us

    Há alguns meses atrás eu estava curioso para ver como o game Among Us implementa algumas coisas. Uma breve pesquisa no google revela que o jogo foi desenvolvido usando a Unity Engine. Saber em qual engine o programa é produzido é útil na hora de olhar pelos arquivos do game durante uma análise estática. A Unity utiliza C# como linguagem de programação, gerando arquivos .NET (quando no Windows) que são facilmente decompilados e editáveis por ferramentas como dnSpy, tornando o processo de modificação e engenharia reversa bem simples. No entando, atualmente a Unity suporta também o uso do IL2cpp, um conversor de linguagem intermediária (IL) para C++ antes de gerar o pacote do projeto. Isso gera benefícios como performance para o jogo e também dificulta o acesso ao código.

    Entretanto, o processo de engenharia reversa não é impossível. Utilizando a ferramenta IL2cppDumper é possível extrair a partir dos arquivos de metadados os offsets de cada função do GameAssembly.dll, bem como seus respectivos nomes. Vasculhando pelas funções foi possível encontrar a rotina que gerencia o cooldown do botão de matar quando o jogador é o impostor:

    image.thumb.png.df6b97d102bdb188fa00b0593a47a72c.png

    Método responsável pelo cooldown do botão, decompilado pelo ghidra

    image.png.9247326a8073dba6ae10616b9cf7ffc1.png

    Código assembly referente ao método SetTimeKiller

    Aqui vemos que a função recebe o tempo como parâmetro e seu primeiro uso é escrevê-lo em um dos atributos do objeto. Um hack interessante e bem crítico seria transformar qualquer tempo de cooldown em 0.

    image.png.2fcfcb3d58b9823e463524512cad3f41.png

    Código assembly após o patch

    Troquei a instrução xmm0,[ebp +0xc] que lia o argumento da pilha por um simples xor eax,eax, zerando o registrador.
    No lugar de movss [esi+0x44],xmm0 basta mover o valor de eax, como se todo o argumento fosse 0. Com apenas duas instruções alteradas, o exploit funcionou bem mesmo em sessões online.

    O servidor não checava o tempo entre as RPCs para matar o jogador, permitindo assim abusos por clientes modificados e eventualmente frustrando os demais jogadores depois de morrerem em partidas de 10 segundos ou menos.


    image.png.94e60cf0fa19d46354d1050305297ab3.png
     
    Método após o patch, decompilado pelo Ghidra

    Referências


    Revisão: Leandro Fróes
    • Agradecer 1
    • Curtir 2

    User Feedback

    Join the conversation

    You can post now and register later. If you have an account, sign in now to post with your account.
    Note: Your post will require moderator approval before it will be visible.

    Guest

    • This will not be shown to other users.
    • Add a review...

      ×   Pasted as rich text.   Paste as plain text instead

        Only 75 emoji are allowed.

      ×   Your link has been automatically embedded.   Display as a link instead

      ×   Your previous content has been restored.   Clear editor

      ×   You cannot paste images directly. Upload or insert images from URL.


    Guest Professor Utônio

       3 of 3 members found this review helpful 3 / 3 members

    Não entendo nada do assunto, mas ta muito bom! Bem formatado e com vídeos e imagens bem explicativas

    Link to review
    Share on other sites

    Guest xaolin matador de porco

       2 of 2 members found this review helpful 2 / 2 members

    o autor esta solteiro?

    Link to review
    Share on other sites

    Gabriel Sidnei

       1 of 1 member found this review helpful 1 / 1 member

    Se tratando de jogos online no nosso atual cenário, os desenvolvedores tem uma grande arma em suas mãos que é o uso de AntiCheaters com drivers. Os mais famosos utilizados atualmente são: Easy Anti-Cheat (Famoso EAC) ou o Battleye.

    Claro que nada é seguro de tudo, mas, boa parte dos desenvolvedores que atuavam em uma era recente estragando a jogabilidade de outros apenas por diversão (E pela facilidade de desenvolver) acabaram parando de criar hacks.

    Sua análise está ótima e passa uma boa visão para os leitores de como é difícil proteger as coisas online devido a lentidão na comunicação Cliente/Servidor.

    Link to review
    Share on other sites

    Guest Vitor Machado

       1 of 1 member found this review helpful 1 / 1 member

    Muito bom!

    Link to review
    Share on other sites


  • Similar Content

×
×
  • Create New...