1 puntos por GN⁺ 2023-12-06 | 1 comentarios | Compartir por WhatsApp
  • En Magic: The Gathering Arena era posible forzar la rendición del oponente en el momento deseado, lo que permitía quedar en un estado donde no se podía perder en partidas con matchmaking
  • Aunque los juegos de cartas suelen encajar bien con una arquitectura autoritativa del servidor, donde este administra todo el estado, el rival bot de MTGA, Sparky, estaba implementado con lógica local del cliente
  • El cliente de MTGA basado en C# permitía acceder mediante reflexión a objetos en tiempo de ejecución y a campos privados, y en el código descompilado aparecían nombres como JoinMatch, ConnectAndJoinMatch y HeadlessClient
  • En las partidas contra bots se usaba una estructura donde ambos asientos se conectaban con el PersonaID y el JWT de la misma cuenta; al aplicar eso a partidas normales, se podía conectar un cliente headless al asiento del oponente y luego llamar a ConcedeGame()
  • Después, el servidor fue parchado para impedir que ambos asientos usaran la misma cuenta y el mismo JWT en partidas con matchmaking, convirtiéndose en un caso donde el límite entre la implementación de bots del lado del cliente y la validación de autenticación de asientos afectó directamente la seguridad real

Por qué es difícil hackear juegos de cartas

  • Los juegos de cartas son por turnos y no intercambian demasiada información entre cliente y servidor, así que encajan bien con una arquitectura autoritativa del servidor
  • El servidor puede administrar todo el estado del juego y enviar al cliente solo la información necesaria
    • La información no pública, como la mano o el mazo del oponente, no existe localmente
    • Intentos como reordenar el mazo o manipular qué carta se roba son difíciles porque el servidor ejecuta la acción y solo devuelve el resultado
  • A diferencia de un shooter en primera persona, en un juego de cartas las acciones del jugador son limitadas y ocurren en momentos definidos
    • En un FPS, información como la posición de los modelos enemigos puede quedar en caché en el cliente, lo que permite cosas como wallhacks
    • El blog sobre anti-wallhack de Riot trata un enfoque para reducir los datos de posición de jugadores en el cliente
  • También es relativamente más fácil detectar comportamientos inválidos
    • Jugar una carta que no está en la mano
    • Actuar cuando no es tu turno

Punto de partida del análisis: la red y el cliente en C#

  • Como punto de partida para hackear el juego, se eligió analizar la comunicación de red
  • La charla de Manfred en DEF CON sobre casos de hacking de MMOs muestra que la ingeniería inversa del protocolo de red es clave para analizar varios bugs
  • Como MTGA está escrito en C#, era posible manipular objetos en tiempo de ejecución en vez de hookear funciones de envío y recepción de tráfico
  • En ensamblados .NET no ofuscados, gracias a los metadata token, los nombres de funciones, variables y clases pueden verse en un formato legible
    • Por eso, el resultado de la descompilación se parece casi a una revisión de código fuente

La estructura de Sparky encontrada en JoinMatch

  • Al buscar nombres de funciones relacionados para encontrar la inicialización de entrada a una partida, apareció la función JoinMatch
  • JoinMatch era una función larga de más de 200 líneas, y al final se confirmó una llamada a ConnectAndJoinMatch
    • Esa función parecía ser el flujo que recibe la configuración de la partida y se conecta al servidor del juego
  • En ese mismo flujo de código había ramas para MatchType.NPE y MatchType.Familiar
    • NPE probablemente corresponde a partidas tipo tutorial para la experiencia de nuevos jugadores
    • Familiar es la partida estándar contra bots
  • La lógica llamada en esa rama estaba conectada con Sparky, el rival bot de MTGA
    • Sparky es el oponente tipo mascota que MTGA usa en tutoriales y partidas contra bots
    • En las partidas contra bots, la lógica del bot se ejecuta dentro del cliente del juego en la máquina local

El bot local y HeadlessClient

  • El manejador real de la lógica del bot estaba dentro de la clase HeadlessClient
  • HeadlessClient es un cliente headless que no renderiza el tablero, pero se conecta al servidor y juega la partida
  • El cliente bot creado localmente usaba la misma información de autenticación de usuario que el cliente del juego
    • PersonaID, que funciona como ID de usuario
    • El JSON web token que se le asigna al juego después del inicio de sesión
  • En las partidas contra bots, la estructura permitía que el mismo cliente se conectara en la práctica a ambos lados de la partida sin que el servidor lo considerara un problema
  • El asiento (seat) se usa como forma de distinguir qué jugador es cuál dentro del juego
    • En partidas contra bots era posible llenar asientos distintos con la misma información de autenticación

Método para tomar control de una partida normal

  • Se probó aplicar la lógica de las partidas contra bots a una partida normal para ver si era posible conectarse a ambos asientos
  • La información necesaria se obtuvo de los objetos del juego en tiempo de ejecución
    • Configuración actual de la partida
    • Host y puerto del servidor de la partida
    • controllerFabricUri
    • matchId
    • PersonaID
    • Jwt
    • Base de datos de cartas
    • Administrador de partidas
    • Sistema de consulta de assets
  • Se calculó el asiento del oponente a partir del asiento propio
    • En el código se obtiene el otro asiento con man.LocalPlayerSeatId % 2U + 1U
  • Se conectó un bot al asiento del oponente con UnityFamiliar.SpawnFamiliar_DEBUG(...)
  • Luego se buscó el objeto UnityFamiliar creado y se llamó a cheatbot.Client.Gre.ConcedeGame() para forzar una rendición inmediata

Resultado y parche

  • Este método también funcionaba en partidas normales
    • Funcionaba incluso si el oponente ya estaba conectado y la partida estaba en curso
    • Como eran partidas con matchmaking, se obtenían recompensas como si se hubiera vencido a un rival humano
  • El punto central de la vulnerabilidad era que el servidor de partidas normales permitía que ambos asientos se conectaran con la misma cuenta y el mismo JWT
  • Más tarde, el servidor fue parcheado para impedir que en partidas con matchmaking ambos asientos usaran la misma cuenta y el mismo JWT
  • El código InstaWin del apéndice está implementado como un MonoBehaviour de Unity que crea un botón en la GUI y, al hacer clic, recopila la información de la partida actual, conecta el bot al asiento rival y luego fuerza su rendición

1 comentarios

 
GN⁺ 2023-12-06
Opiniones de Hacker News
  • La primera vez que me metí de lleno en Linux fue al inspeccionar el tráfico de red con ShowEQ para EverQuest.
    En ese entonces el tráfico no estaba cifrado y contenía mucha información útil. Duplicábamos el tráfico hacia una máquina Linux con un hub y dibujábamos un mapa en tiempo real de la zona; mostraba la ubicación de monstruos, NPC y usuarios, e incluso el botín que tenían los monstruos, lo que permitía elegir solo ciertos monstruos para matar. La ventaja era que, al ser un método manual, era imposible de detectar; al final SOE se dio cuenta y empezó a cifrar el tráfico.

    • Al final los datos tienen que descifrarse y leerse, así que terminabas haciendo ingeniería inversa del cliente para encontrar la forma de descifrarlos en tiempo real.
      Entonces la contraparte introducía firmas basadas en claves, uno intentaba otra vez robar la clave desde el cliente y romper el cifrado, y luego, con la llegada de los sistemas antitrampas, empezaba el juego del gato y el ratón.
    • De adolescente hice algo parecido con Dark Age of Camelot, y fue muy útil para aprender sniffing de red, la diferencia entre hubs y switches, y Linux.
      ¿Valió la pena por la ventaja en el juego? No. No jugaba en serio, así que pasé 2 semanas instalándolo y lo usé más o menos 1 semana, pero como experiencia de aprendizaje fue excelente.
    • Recuerdo que ShowEQ se usó para demostrar varias teorías y bugs que Verant/Sony seguía negando.
      Por ejemplo, la existencia de los hell levels, que no los humanos sino los halflings recibían un bono de experiencia y que de hecho había diferencias de experiencia por raza/clase, y que la alquimia temprana de los Shaman realmente estaba rota. Y creo que eso terminó llevando a eqemulator.org.
    • Me pregunto de qué sirve el cifrado. Si el cliente de todos modos tiene que descifrarlo, ¿no basta con engancharse ahí?
    • ShowEQ todavía funciona, y también existe MySEQ, basado en Windows, que lee la memoria.
      Parece que a los dueños actuales de EQ no les importa mucho, así que en general se puede usar. Eso sí, ninguna de las dos apps mostró nunca el botín que tenían los monstruos, salvo el equipo visible. Con los años también cambiaron algunos datos: antes se enviaba la vida exacta de los monstruos, pero ahora solo envían el porcentaje.
  • No termino de entender la parte de que “un bot casi completo capaz de jugar una partida arbitraria de Magic: The Gathering es lo bastante pequeño como para ejecutarse en una máquina local”.
    Si una IA de MTG fuera tan pesada como para ser una carga en las máquinas de los clientes, sospecho que tampoco la ejecutarían en el servidor. Los duelos contra bots en juegos de cartas normalmente no se cobran por partida, así que el costo crecería mucho. Los servidores tampoco son magia: en su mayoría usan las mismas CPU x86 que una máquina local, y a menudo con frecuencias más bajas que una de escritorio. Para reducir el tiempo de turno del bot frente a lo local tendrían que usar muchos más núcleos que el cliente; asignar 8 a 16 núcleos por jugador suena como una pesadilla en los picos de concurrencia. Si el jugador CPU no soporta multinúcleo, ejecutarlo localmente debería ser más rápido de todos modos.

    • Aquí no se hablaba de capacidad de procesamiento, sino de uso de memoria.
      Significa que el motor de reglas del bot es lo bastante pequeño como para caber en la memoria de un iPhone viejo o de un dispositivo Android. En un servidor podrías tener muchas máquinas de estado o motores de reglas cargados en memoria, y quizá usar muy poca capacidad de procesamiento para ejecutar una solicitud específica.
    • En escritorio sería cierto, pero también hay que recordar que MTGA corre en teléfonos.
    • MTG es un juego con un sistema de reglas extremadamente complejo.
  • Daniel, felicidades otra vez por llegar a la portada. Después de que se publicó este artículo, probé hackear MTGA y también hablamos un poco en GitHub [0].
    Para quien le interese: ahora estoy trabajando de forma inactiva en un cliente no oficial de MTGA que por el momento casi no tiene funciones. El objetivo es automatizar partidas clasificatorias y ofrecer oponentes bot más fuertes. Últimamente he estado ocupado con otras cosas, y todavía no tengo claro cómo hacer una buena UI que sea fácil de entender, así que es difícil prever siquiera cuándo llegará a ser mínimamente útil. Además, me sigue interesando escuchar más historias de hacking de juegos de Daniel u otras personas: cómo encuentran bugs como este, cómo no preocuparse por un ban después de hackear, cómo divulgar bugs como mencionó @aethros, o cómo estructurar clientes no oficiales para juegos de cartas.
    [0] https://github.com/MayerDaniel/mayerdaniel.github.io/issues/...

  • Decir que uno pensaba que crear un oponente de IA para un juego complejo como MTG tendría mucha sobrecarga es quedarse bastante corto.
    El juego es casi Turing completo, y la gente juega mucho con eso incluso dejando de lado los bucles infinitos. Aun así, supongo que ya hay bastante investigación sobre la parte de estrategia de IA. Y como el autor publicó el artículo directamente, quizá el título podría llevar “Show HN:”.

    • Show HN es para proyectos que la gente puede probar directamente, no para entradas de blog: https://news.ycombinator.com/showhn.html
    • Decir “se puede codificar una máquina de Turing dentro del juego” y decir “es difícil escribir un programa que pueda realizar acciones legales en cualquier situación” no son en absoluto lo mismo.
      Lo difícil para quien escribe el bot parece ser lo segundo.
    • De hecho, es Turing completo: https://arxiv.org/abs/1904.09828
    • El juego sigue cambiando a medida que rotan los sets nuevos, pero tengo entendido que en ciertas interacciones se pueden crear bucles infinitos, o se pudo en varios momentos.
      Por ejemplo, crear y matar criaturas ficha mediante efectos de entrada/salida/enderezar. Esos bucles no siempre le hacen daño a alguna de las partes.
    • Me pregunto si existe alguna estrategia real que se acerque a esa complejidad.
      Nunca jugué MTG, pero esa afirmación parece decir que es técnicamente posible si uno construye deliberadamente una estructura con estado, no con el objetivo de ganar.
  • Es muy divertido jugar Magic 93/94 old-school con mi hijo usando cartas físicas.
    Cada año vamos a Madrid para participar en el Campeonato Mundial de 7pts Singleton. Este verano mi hijo quedó 9.º y estoy muy orgulloso. 7pts Singleton es un formato excelente: permite armar mazos muy variados y ofrece una jugabilidad equilibrada a un costo relativamente accesible (https://7pts-singleton.com)

    • Es difícil llamar barato a un formato donde Black Lotus, Ancestral Recall y los Moxen son legales.
      Aunque en 7pts Singleton no se puedan usar todos, uno solo ya cuesta de miles a decenas de miles de dólares. Aun así, qué bueno que disfrutes el juego con tu hijo, y felicidades por haber quedado en el ranking.
  • ¿Lógica de juego puramente del lado del cliente? Cuando hice un juego pequeño hace tiempo, a veces tenía que poner lógica de juego en el cliente por capacidad de respuesta, pero nada impedía volver a ejecutar la misma lógica en el servidor, así que eso hice.
    En juegos en tiempo real como FPS o RTS puede ser difícil, pero en un juego de cartas no hay excusa. En un juego de cartas así, lo correcto es no enviar al cliente más información de la que un jugador real podría ver. Por ejemplo, no enviar el contenido de las cartas en la mano del oponente, solo la cantidad. Las acciones que se envían al servidor también deberían ser solo sobre uno mismo, así que no debería ser posible declarar la rendición del oponente. Si digo “¡me rindo!”, el servidor debería interpretarlo como que yo me rendí.

    • Por el artículo, se puede asumir que el juego está escrito justamente de esa manera. Solo recibe la información necesaria en el momento necesario, y solo envía sus propias acciones.
      La vulnerabilidad era que se podía abrir un segundo cliente y conectarse a la partida en curso ocupando el asiento del oponente. Una vez hecho eso, se podían enviar acciones del oponente, incluida la rendición.
    • Como queda claro en el artículo, toda la jugabilidad se procesa del lado del servidor.
      Lo que aprovechó el autor no fue la lógica de juego del lado del cliente, sino un problema en la autorización y en el código de acceso a la partida. Decir “no debería poder declarar la rendición en nombre del oponente” es repetir exactamente cómo funcionaba el exploit que encontró el autor.
    • En el primer tercio del artículo se dice explícitamente que el juego está implementado así.
  • En League of Legends hubo un bug de división por cero con cierta combinación de campeón e ítems que hacía que el servidor expulsara a todos los jugadores y luego crasheara.
    Como el abusador era expulsado al final, su equipo recibía la victoria, mientras que el equipo rival recibía Loss Prevented en vez de un resultado normal.

    • Si el oponente no recibe una derrota sino un resultado no normal, entonces el equipo ganador tampoco debería recibir una victoria.
  • Me gustan los detalles accesibles pero perspicaces de este artículo.
    Pero no entiendo lo de conectar un bot durante una partida real. ¿Por qué el juego permite unirse a mitad de una partida, y por qué cuando el bot se rinde eso se procesa como la rendición del oponente? Si se estuviera creando una partida de 3 jugadores, que el jugador 3 se rinda no debería implicar que también se rindió el jugador 2.

    • No se convierte en una partida de 3 jugadores; sigue siendo una partida de 2 jugadores.
      El código averigua el índice del asiento de mi cuenta y luego hace que el bot entre con el otro índice de asiento. El problema es que no verifica si el usuario que se une es el usuario correcto que debería estar en ese asiento. También se podría pensar que no deberían permitir reconectarse a un asiento que ya está conectado, pero si el juego también soporta móviles, querrías que el tiempo límite por desconexión sea bastante largo. No querrías bloquear a un jugador que se desconectó brevemente y se reconectó antes de que el tiempo límite detectara que la conexión anterior murió. En otros juegos he visto que, al intentar reconectar, aparece por unos 10 segundos “no se puede entrar a una partida en curso” y recién después se reconecta. Como analogía, sería como aparecer en un torneo de MTG de una tienda local, empujar a alguien fuera de su silla, sentarse y gritar “¡me rindo!”, y que el juez acepte que el jugador B concedió porque lo declaró la persona sentada en esa silla.
      1. Probablemente sea la forma en que manejan la reconexión tras una desconexión: cerrando la conexión anterior.
      2. El bot reemplaza al jugador 2, envía la rendición, y el servidor la registra como la rendición del jugador 2.
    • MTG: Arena no permite partidas de 3 jugadores, así que el bot se mete en el asiento del oponente.
      Por eso parece que podía rendirse en nombre del oponente. Puede que los desarrolladores no imaginaran que alguien tendría motivos para intentar algo así y por eso no lo validaron.
  • Me recuerda a la época de Diablo 2, cuando se podía reutilizar el mismo paquete de conexión para hacer que un personaje de servidor abierto (LAN) entrara a los servidores oficiales de internet de bnet.
    Como los datos del servidor abierto se guardaban completamente en local, se podían crear todo tipo de ítems que no deberían existir, y el servidor oficial los aceptaba.

  • Hace poco volví a entrar a MTG con MTGA. Es un juego en Unity y no usa il2cpp, así que lo descompilé rápido; aunque hubiera usado il2cpp, no creo que hubiera sido mucha protección, y encontré cosas bastante interesantes.
    Había cosas como claves para la build de Epic Launcher y APIs no documentadas. No es que quiera usarlas para hacer trampa; simplemente me gustaría que hubiera historial de combates. Algo como ver qué partidas gané y perdí, o volver a ver el estado del campo de batalla de una partida terminada. También voy a revisar este artículo, y espero que lo parcheen pronto.

    • Esta vulnerabilidad ya había sido parcheada antes de escribir el artículo, y se divulgó a MTG.
      Si quieres ver historiales, puedes revisar https://untapped.gg/en. Hablé un poco con ellos y, en la práctica, hacen lo que quieres. La mayor parte de la información la obtienen del log de depuración de MTG que está en el directorio de la aplicación de MTGA, así que, si quieres, también podrías hacer tu propio tracker. El sitio también explica esto: https://help.hearthsim.net/en/articles/3620440-how-do-i-supp...
    • https://www.17lands.com/ recopila estadísticas de victorias y derrotas en juegos Limited, y también guarda historiales de partida turno por turno tanto en Limited como en Constructed. Participé como colaborador.
    • Para recopilar datos de juego, tengo entendido que https://mtgaassistant.net/ es de las opciones más comunes.
    • Tengo entendido que esta función ya existe. Probablemente sean los logs del jugador.
      Recuerdo que había bastantes apps que los usaban para hacer seguimiento.