1 pontos por GN⁺ 2023-12-06 | 1 comentários | Compartilhar no WhatsApp
  • Em Magic: The Gathering Arena, era possível forçar o pedido de rendição do oponente no momento desejado, tornando possível não perder em partidas de matchmaking
  • Jogos de cartas normalmente se encaixam bem em uma arquitetura autoritativa de servidor, na qual o servidor gerencia todo o estado, mas o oponente bot Sparky no MTGA era implementado com lógica local no cliente
  • O cliente MTGA, baseado em C#, permitia acessar objetos em tempo de execução e campos private por reflexão, e no código decompilado apareciam nomes como JoinMatch, ConnectAndJoinMatch e HeadlessClient
  • Em partidas contra bots, era usada uma estrutura que conectava aos dois assentos com o mesmo PersonaID da conta e o mesmo JWT; aplicando isso a partidas normais, foi possível anexar um cliente headless ao assento do oponente e chamar ConcedeGame()
  • O servidor depois foi corrigido para impedir que os dois assentos usassem a mesma conta e o mesmo JWT em partidas de matchmaking, tornando este um caso em que o limite entre a implementação de bots no cliente e a validação de autenticação dos assentos afetou diretamente a segurança em produção

Por que hackear jogos de cartas é difícil

  • Jogos de cartas são baseados em turnos e não trocam muita informação entre cliente e servidor, então se encaixam bem em uma arquitetura autoritativa de servidor
  • O servidor pode gerenciar todo o estado do jogo e enviar ao cliente apenas as informações necessárias
    • Informações não públicas, como a mão ou o deck do oponente, não existem localmente
    • Tentativas como mudar a ordem do deck ou manipular a próxima compra são difíceis, porque o servidor executa a ação e só informa o resultado
  • Ao contrário de jogos de tiro em primeira pessoa, jogos de cartas têm ações de jogador limitadas e em momentos definidos
    • Em FPS, informações como a posição dos modelos inimigos podem ficar em cache no cliente, possibilitando coisas como wallhack
    • O blog anti-wallhack da Riot aborda uma estratégia de reduzir os dados de posição de jogadores no cliente
  • Também é relativamente mais fácil detectar ações inválidas
    • Jogar uma carta que não está na mão
    • Agir fora do próprio turno

Ponto de partida da análise: rede e cliente C#

  • Foi escolhida como ponto de partida para o hacking de jogo a abordagem de examinar a comunicação de rede
  • A palestra de Manfred na DEF CON, sobre casos de hacking de MMOs, mostra como a engenharia reversa de protocolos de rede é central para analisar vários bugs
  • Como o MTGA é escrito em C#, foi possível manipular objetos em tempo de execução em vez de fazer hook nas funções de envio e recebimento de tráfego
  • Assemblies .NET não ofuscados permitem verificar nomes de funções, variáveis e classes em formato legível graças ao metadata token
    • Por isso, o resultado da decompilação fica praticamente próximo de uma revisão de código-fonte

A estrutura do Sparky encontrada em JoinMatch

  • Ao procurar nomes de funções relacionados para encontrar a inicialização de entrada em partidas, foi encontrada a função JoinMatch
  • JoinMatch era uma função longa, com mais de 200 linhas, e perto do fim foi identificada a chamada a ConnectAndJoinMatch
    • Essa função parecia ser o fluxo que recebia as configurações da partida e se conectava ao servidor do jogo
  • No mesmo fluxo de código havia ramificações para MatchType.NPE e MatchType.Familiar
    • NPE parece significar a experiência de novo jogador, isto é, partidas do tipo tutorial
    • Familiar era a partida padrão contra bot
  • A lógica chamada nesse ramo estava ligada ao Sparky, o oponente bot do MTGA
    • Sparky é o oponente mascote do MTGA usado em tutoriais e partidas contra bot
    • Em partidas contra bot, a lógica do bot roda dentro do cliente do jogo na máquina local

Bot local e HeadlessClient

  • O manipulador da lógica real do bot estava na classe HeadlessClient
  • HeadlessClient era um cliente headless que não renderiza o tabuleiro do jogo, mas se conecta ao servidor e joga a partida
  • O cliente de bot criado localmente usava as mesmas credenciais de autenticação do cliente do jogo
    • PersonaID, que funciona como ID do usuário
    • JSON web token atribuído ao jogo após o login
  • Em partidas contra bot, a estrutura era tal que o servidor não considerava um problema que o mesmo cliente se conectasse efetivamente aos dois lados da partida
  • Assento (seat) é a forma de distinguir qual jogador você é dentro do jogo
    • Em partidas contra bot, era possível preencher assentos diferentes com as mesmas credenciais

Como partidas normais eram sequestradas

  • A lógica das partidas contra bot foi aplicada a partidas normais para testar se seria possível se conectar aos dois assentos
  • As informações necessárias foram obtidas dos objetos do jogo em tempo de execução
    • Configuração da partida atual
    • Host e porta do servidor da partida
    • controllerFabricUri
    • matchId
    • PersonaID
    • Jwt
    • Banco de dados de cartas
    • Gerenciador de partida
    • Sistema de consulta de assets
  • Com base no próprio assento, foi calculado o assento do oponente
    • No código, man.LocalPlayerSeatId % 2U + 1U era usado para obter o outro assento
  • UnityFamiliar.SpawnFamiliar_DEBUG(...) foi usado para conectar o bot ao assento do oponente
  • Em seguida, foi localizado o objeto UnityFamiliar criado e chamada cheatbot.Client.Gre.ConcedeGame() para forçar rendição imediata

Resultado e correção

  • Esse método também funcionava em partidas normais
    • Funcionava mesmo quando o oponente já estava conectado e a partida em andamento
    • Como eram partidas de matchmaking, era possível receber recompensas como se tivesse vencido um oponente humano
  • O cerne da vulnerabilidade era que o servidor de partidas normais permitia a situação em que os dois assentos se conectavam com a mesma conta e o mesmo JWT
  • Depois, o servidor foi corrigido para impedir que os dois assentos usassem a mesma conta e o mesmo JWT em partidas de matchmaking
  • O código InstaWin, no apêndice, usa um MonoBehaviour do Unity para criar um botão de GUI e, ao clicar nele, coleta as informações da partida atual, conecta o bot ao assento do oponente e então força a rendição

1 comentários

 
GN⁺ 2023-12-06
Opiniões no Hacker News
  • O que me levou a mergulhar de verdade no Linux pela primeira vez foi examinar o tráfego de rede com o ShowEQ para EverQuest
    Na época, o tráfego não era criptografado e continha muita informação útil. Com um hub, eu duplicava o tráfego para uma máquina Linux e desenhava um mapa em tempo real da zona, mostrando a localização de monstros, NPCs e usuários, além até do loot que os monstros carregavam, o que permitia escolher só monstros específicos para matar. A vantagem era que, por ser um método passivo, era impossível de detectar; no fim, a SOE percebeu e começou a criptografar o tráfego

    • No fim das contas, os dados precisam ser descriptografados para serem lidos, então acabaram fazendo engenharia reversa do cliente para descobrir como descriptografar em tempo real
      Aí o outro lado introduz assinaturas baseadas em chaves, você tenta roubar a chave do cliente de novo, quebra a criptografia, e depois entram sistemas anti-cheat, dando início ao jogo de gato e rato
    • Quando eu era adolescente, fiz algo parecido também em Dark Age of Camelot, e foi muito útil para aprender sniffing de rede, a diferença entre hubs e switches, e Linux
      Valeu a pena a ponto de ganhar vantagem no jogo? Não. Eu nem jogava tão a sério, então passei 2 semanas instalando e usei por mais ou menos 1 semana, mas como experiência de aprendizado foi excelente
    • Lembro que o ShowEQ foi usado para provar várias teorias e bugs que a Verant/Sony continuava negando
      Por exemplo, a existência dos hell levels, o fato de que não eram humanos, mas halflings que recebiam bônus de experiência, e que havia mesmo diferenças de experiência por raça/classe, além de que a alquimia inicial dos Shamans estava de fato quebrada. E acho que isso acabou levando ao eqemulator.org
    • Fico curioso sobre como a criptografia ajuda. O cliente vai ter que descriptografar de qualquer jeito, então não é só aproveitar isso?
    • O ShowEQ ainda funciona, e também existe o MySEQ, baseado em Windows, que lê memória
      O atual dono de EQ não parece ligar muito, então é relativamente tranquilo usar. Porém, tirando o equipamento visível, nenhum dos dois apps jamais mostrou o loot que um monstro carregava. Com o passar dos anos, alguns dados também mudaram: antes, a vida exata dos monstros era transmitida, mas hoje só enviam a porcentagem
  • Não entendi bem a parte de que “um bot quase completo capaz de jogar uma partida arbitrária de Magic: The Gathering é pequeno o bastante para rodar em uma máquina local”
    Se uma IA de MTG for pesada demais para rodar na máquina do cliente, acho que também não a rodariam no servidor. Partidas contra bots em jogos de cartas normalmente não são cobradas por partida, então o custo cresce bastante. Servidores também não são mágicos: em geral usam as mesmas CPUs x86 de máquinas locais, e muitas vezes com clocks menores que desktops. Para reduzir o tempo dos turnos do bot em relação ao local, seria preciso usar muito mais núcleos que o cliente; se forem 8 a 16 núcleos alocados por jogador, isso parece um pesadelo nos picos de usuários simultâneos. Se o jogador controlado pela CPU não tiver suporte a múltiplos núcleos, rodar localmente deveria ser mais rápido de qualquer forma

    • O ponto aqui não era desempenho de processamento, mas uso de memória
      Quer dizer que o motor de regras do bot é pequeno o bastante para caber na memória de um iPhone antigo ou de um dispositivo Android. Num servidor, daria para manter muitas máquinas de estado ou motores de regras na memória, e executar uma solicitação específica talvez quase não use capacidade de processamento
    • Em desktops isso faz sentido, mas é preciso lembrar que MTGA também roda em celulares
    • MTG é um jogo com um sistema de regras extremamente complexo
  • Daniel, parabéns mais uma vez por chegar ao topo. Depois que este texto foi publicado, tentei hackear o MTGA e também conversei um pouco no GitHub [0]
    Para quem tiver interesse: atualmente estou trabalhando, de forma inativa, em um cliente não oficial de MTGA que quase não tem funcionalidades. O objetivo é automatizar partidas ranqueadas e oferecer adversários bots mais fortes. Ultimamente outras coisas tomaram a frente, e ainda estou meio perdido sobre como criar uma boa UI que seja clara de visualizar, então é difícil até estimar quando isso ficará minimamente útil. Além disso, continuo interessado em ouvir histórias do Daniel ou de outras pessoas sobre hacking de jogos: como encontram bugs assim, como não se preocupar em ser banido depois de hackear, formas de divulgar bugs como a @aethros mencionou, estruturação de clientes não oficiais para jogos de cartas etc.
    [0] https://github.com/MayerDaniel/mayerdaniel.github.io/issues/...

  • Dizer que imaginava que criar um oponente de IA para um jogo complexo como MTG teria uma grande sobrecarga é um baita eufemismo
    O jogo é praticamente Turing-completo, e mesmo excluindo loops infinitos, as pessoas brincam bastante com isso. Ainda assim, imagino que já exista muita pesquisa sobre a parte de estratégia de IA. E, como o próprio autor publicou o texto, acho que o título poderia receber um “Show HN:”

    • Show HN é para projetos que as pessoas podem experimentar diretamente, não para posts de blog: https://news.ycombinator.com/showhn.html
    • “É possível codificar uma máquina de Turing dentro do jogo” e “é difícil escrever um programa capaz de fazer jogadas legais em todas as situações” não são nem um pouco a mesma coisa
      A dificuldade para quem escreve bots parece ser a segunda
    • É realmente Turing-completo: https://arxiv.org/abs/1904.09828
    • À medida que novos sets entram e saem da rotação, o jogo continua mudando, mas pelo que sei em certas interações é possível criar loops infinitos, ou já foi possível em vários momentos
      Por exemplo, criando e matando criaturas-token com efeitos de entrada/saída/desvirar. Esses loops nem sempre causam dano a algum dos lados
    • Fico curioso se existe alguma estratégia real que chegue perto dessa complexidade
      Nunca joguei MTG, mas essa afirmação parece significar que, se você criar de propósito uma estrutura com estado, em vez de tentar vencer, isso se torna tecnicamente possível
  • Jogar Old School Magic 93/94 com meu filho usando cards físicos é muito divertido
    Todo ano vamos a Madrid para participar do Campeonato Mundial de 7pts Singleton. Fiquei muito orgulhoso porque neste verão meu filho ficou em 9º lugar. 7pts Singleton é um formato excelente, que permite uma grande variedade de construções de decks e oferece uma jogabilidade equilibrada a um custo relativamente acessível (https://7pts-singleton.com)

    • É difícil chamar de barato um formato em que Black Lotus, Ancestral Recall e Moxen são legais
      Mesmo que em 7pts Singleton não dê para usar todos eles, só um já custa de milhares a dezenas de milhares de dólares. Ainda assim, é ótimo que você aproveite o jogo com seu filho, e parabéns pela colocação
  • Lógica de jogo puramente do lado do cliente? Quando fiz um joguinho no passado, houve momentos em que precisei colocar a lógica de jogo no cliente por responsividade, mas nada impedia de rodar a mesma lógica de novo no servidor, então foi isso que fiz
    Em jogos em tempo real como FPS ou RTS isso pode ser difícil, mas em um jogo de cartas não há desculpa. Num jogo de cartas assim, o certo é não enviar ao cliente mais informações do que um jogador real poderia ver. Por exemplo, não enviar o conteúdo das cartas na mão do oponente, apenas a quantidade de cartas. As ações enviadas ao servidor também deveriam dizer respeito apenas a si mesmo; portanto, não deveria ser possível declarar a rendição do oponente. Se eu digo “rendo-me!”, o servidor deveria interpretar isso como a minha rendição

    • Pelo artigo, dá para considerar que o jogo foi escrito exatamente desse jeito. Ele só recebe as informações necessárias no momento necessário e só envia as próprias ações
      A vulnerabilidade era que dava para abrir um segundo cliente e entrar na partida em andamento no assento do oponente. Uma vez feito isso, era possível enviar ações do oponente, incluindo rendição
    • Como fica claro no artigo, toda a jogabilidade é processada do lado do servidor
      O que o autor explorou não foi lógica de jogo no cliente, mas um problema no código de autorização e de entrada na partida. A frase “não deveria ser possível declarar a rendição no lugar do oponente” é basicamente uma repetição de como o exploit que o autor encontrou funcionava
    • No primeiro terço do artigo, ele diz explicitamente que o jogo foi implementado dessa forma
  • Em League of Legends, havia um bug de divisão por zero com uma combinação específica de campeão e itens que fazia o servidor expulsar todos os jogadores e depois travar
    Como o abusador era o último a ser expulso, a equipe dele recebia a vitória, enquanto a equipe adversária recebia Loss Prevented em vez de um resultado normal

    • Se o adversário não recebe uma derrota como resultado normal, a equipe vencedora também não deveria receber uma vitória
  • O artigo é acessível e, ao mesmo tempo, tem detalhes perspicazes
    Mas não entendo o caso de conectar um bot durante uma partida real. Por que o jogo permite entrar no meio da partida, e por que, quando o bot se rende, isso é tratado como a rendição do oponente? Se fosse para criar uma partida de 3 jogadores, a rendição do jogador 3 não deveria fazer o jogador 2 se render também

    • Não vira uma partida de 3 jogadores; continua sendo uma partida de 2 jogadores
      O código descobre o índice do assento da minha conta e então faz o bot entrar com outro índice de assento. O problema é que ele não verifica se o usuário que está entrando é o usuário correto que deveria estar naquele assento. Também dá para pensar que não deveriam permitir uma nova conexão a um assento já conectado, mas, se o jogo também dá suporte a mobile, provavelmente você vai querer um timeout de desconexão relativamente longo. Isso porque você não quer bloquear um jogador que caiu por um instante e reconectou antes de o timeout perceber que a conexão anterior morreu. Já vi outros jogos mostrarem algo como “não é possível entrar em uma partida em andamento” por uns 10 segundos quando se tenta reconectar, e só depois a reconexão acontece. Como analogia, seria como aparecer em um torneio de MTG numa loja de cartas local, empurrar alguém para fora da cadeira, sentar e gritar “Eu concedo!”, e o juiz aceitar que o jogador B desistiu porque a declaração veio da pessoa sentada naquela cadeira
      1. É muito provável que seja a forma como lidam com reconexão após desconexão. Encerrando a conexão antiga
      2. O bot substitui o jogador 2, envia a rendição, e o servidor registra isso como a rendição do jogador 2
    • MTG: Arena não permite partidas de 3 jogadores, então o bot se intromete no assento do oponente
      Por isso parece que era possível se render no lugar do oponente. Talvez os desenvolvedores não tenham imaginado que alguém teria motivo para tentar isso e, por isso, não fizeram a verificação
  • Isso me lembra a época de Diablo 2, quando era possível reutilizar o mesmo pacote de conexão para colocar um personagem de servidor aberto (LAN) nos servidores oficiais de internet da bnet
    Como os dados do servidor aberto ficavam todos salvos localmente, dava para criar todo tipo de item que originalmente não deveria existir, e o servidor oficial aceitava

  • Recentemente voltei a entrar em MTG pelo MTGA. É um jogo em Unity e não usa il2cpp, então fiz uma descompilação rápida; mesmo se usasse il2cpp, acho que não seria grande proteção, e encontrei algumas coisas bem interessantes
    Havia coisas como chaves para a build do Epic Launcher e APIs não documentadas. Não quero usar isso para trapacear; eu só queria que houvesse histórico de partidas. Algo como ver quais partidas ganhei ou perdi e rever o estado do campo de batalha de uma partida encerrada. Também vou conferir este artigo e espero que seja corrigido logo

    • Esta vulnerabilidade já tinha sido corrigida antes de o artigo ser escrito, e foi divulgada à MTG
      Se você quer ver histórico, dê uma olhada em https://untapped.gg/en. Conversei um pouco com eles, e basicamente fazem o que você quer. A maior parte das informações vem dos logs de depuração do MTG no diretório da aplicação MTGA, então, se quiser, você também pode criar seu próprio tracker. O site também explica isso: https://help.hearthsim.net/en/articles/3620440-how-do-i-supp...
    • https://www.17lands.com/ coleta estatísticas de vitórias e derrotas de jogos Limited e também salva o histórico de jogo turno a turno tanto em Limited quanto em Constructed. Participei como colaborador
    • Para coleta de dados de jogo, https://mtgaassistant.net/ é, pelo que sei, uma das opções mais comuns
    • Pelo que sei, essa funcionalidade já existe. Provavelmente é o log do jogador
      Lembro que havia vários aplicativos que faziam rastreamento usando esse log