Debugger.godMode() – Hackear una Aplicación JVM con el Depurador

Otros idiomas: English 中文 Português

En los viejos tiempos, los videojuegos eran diferentes. No solo han evolucionado los gráficos y la mecánica desde entonces, sino que también hay una característica que no parece muy común en los juegos de hoy en día: casi todos tenían códigos de trucos.

Los códigos de trucos eran secuencias de teclas que te daban algo extraordinario, como munición infinita o la capacidad de atravesar paredes. El más común y poderoso de ellos era el ‘modo dios’ – te hacía invencible.

Captura de pantalla del marine de Doom con 'modo dios' habilitado

Así es como se veía tu personaje cuando ingresabas IDDQD – la combinación de teclas para el ‘modo dios’ en Doom. De hecho, esta secuencia de teclas en particular fue tan popular que se convirtió en un meme y ganó popularidad más allá del propio juego.

Si bien el ‘modo dios’ no es tan prevalente en los juegos como lo era antes, y la era del meme IDDQD parece estar desvaneciéndose, uno podría preguntarse si existe un equivalente contemporáneo. Personalmente, tengo mi propia interpretación moderna de IDDQD. Aunque no está necesariamente relacionado con los juegos, evoca la misma sensación de tener superpoderes.

Space Invaders

Para ilustrar mi punto, me gustaría traer un divertido escenario aquí mismo. Incluso si no estás familiarizado con Doom, lo más probable es que hayas visto este juego más antiguo llamado Space Invaders. Como Doom, su trama gira en torno al tema de luchar contra invasores en el espacio.

Mi amigo y colega, Eugene, ha escrito un plugin de IntelliJ IDEA, que te permite jugar a este juego directamente en el editor – una gran forma de pasar el tiempo mientras se espera para complete la indexación.

Space Invaders en el editor de IntelliJ IDEA Space Invaders en el editor de IntelliJ IDEA

No hay un modo dios en este juego, pero si estamos muy decididos, ¿podemos agregarlo nosotros mismos? ¡Volvamos a la clásica tradición de hackear programas con un debugger y descubramos!

Info icon

¡Sean responsables! Obtuve el consentimiento de Eugene antes de manipular su programa. Si estás usando el debugger en un código que no es tuyo, asegúrate de que sea ético. De lo contrario no lo hagas.

Prepararse con las herramientas

Prepárate para una experiencia meta: vamos a depurar IntelliJ IDEA utilizando su propio depurador.

Aquí hay un pequeño problema que necesitamos resolver: para depurar IntelliJ IDEA, necesitamos suspenderlo. Esto hará que el IDE se inmovilice. Por lo tanto, necesitamos una instancia adicional del IDE que se mantendrá funcional y servirá como nuestra herramienta de depuración.

Para manejar varias instancias de IDE, estaré usando JetBrains Toolbox. Esta es una utilidad que organiza los IDEs instalados de JetBrains. Con él, puedes instalar varias versiones o crear atajos para ejecutarlos con diferentes conjuntos de opciones de VM.

Instalemos dos instancias de IntelliJ IDEA:

JetBrains Toolbox muestra varios IDEs de JetBrains incluyendo dos instancias de IntelliJ IDEA llamadas Space Invaders y Debug JetBrains Toolbox muestra varios IDEs de JetBrains incluyendo dos instancias de IntelliJ IDEA llamadas Space Invaders y Debug

Si está utilizando la misma versión del IDE para ambas instancias, asegúrese de especificar diferentes directorios de sistema, configuración y registros en Tool actions | Settings | Configuration. En esta página, también puedes asignar nombres a las instancias del IDE para mayor comodidad.

Para poder depurar la instancia de ‘Space Invaders’, haz clic en More cerca de ella, luego ve a Settings | Edit JVM options. En el archivo que se abre, pega la siguiente línea:

-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
El archivo con opciones de VM que se pasarán a la instancia del IDE El archivo con opciones de VM que se pasarán a la instancia del IDE

Esto hará que la JVM objetivo se ejecute con el agente de depuración y escuche las conexiones entrantes del depurador en el puerto 5005.

Inicia el juego

Ejecuta la instancia de Space Invaders, instala el juego, y comienza ejecutando la acción Space Invaders. Para encontrar la acción, presiona Shift dos veces, y comienza a escribir ‘Space Invaders’:

Ejecutando la acción Space Invaders a través del diálogo que se abre al presionar Shift dos veces Ejecutando la acción Space Invaders a través del diálogo que se abre al presionar Shift dos veces

Jugamos un poco y observamos el comportamiento que queremos corregir: cuando los misiles enemigos golpean la nave espacial, la barra de salud en la esquina superior izquierda de la pantalla baja.

Adjunta y suspende

Nuestro viaje de depuración comienza con abrir la instancia ‘Debug’ del IDE y configurar un nuevo proyecto Kotlin. La razón principal por la que necesitamos este proyecto es que no sería posible lanzar el depurador sin uno.

Además, IntelliJ IDEA incluye la biblioteca estándar Java/Kotlin en la plantilla de nuevo proyecto, que podríamos usar más tarde. Explicaré el uso de la biblioteca estándar en los capítulos siguientes.

Después de crear el proyecto, ve al menú principal y selecciona Run | Attach to Process. Esto mostrará la lista de JVMs locales que escuchan las solicitudes de adjuntar el depurador. Selecciona el otro IDE que se está ejecutando de la lista.

Una ventana emergente con la lista de JVMs locales en ejecución Una ventana emergente con la lista de JVMs locales en ejecución

Deberíamos ver el siguiente mensaje en la consola confirmando que el depurador se ha adjuntado con éxito a la VM objetivo.

Connected to the target VM, address: 'localhost:5005', transport: 'socket'

Vamos a la parte interesante: ¿cómo suspendemos la aplicación?

Normalmente, uno establecería un punto de interrupción en el código de aplicación, pero en este caso, carecemos de las fuentes tanto de IntelliJ IDEA como del plugin Space Invaders. Esto no solo nos impide establecer un punto de interrupción, sino que también complica nuestra comprensión de cómo opera el programa. A primera vista, parece que no hay nada que inspeccionar o a través del cual avanzar paso a paso.

Por fortuna para nosotros, IntelliJ IDEA tiene una función conocida como Pause Program. Te permite suspender el programa en cualquier punto arbitrario en el tiempo, sin necesidad de especificar la línea de código correspondiente. Puedes encontrarlo en la barra de herramientas del depurador o en el menú principal: Run | Debugging Actions | Pause Program.

La ventana de herramientas Debug para la instancia suspendida de Space Invaders La ventana de herramientas Debug para la instancia suspendida de Space Invaders

La aplicación se suspende. Esto nos da un punto de partida para la depuración.

Tip icon

Pause Program es una técnica muy poderosa, que es especialmente útil en varios escenarios avanzados. Para aprender más, consulta los artículos relacionados:

Encuentra los objetos relevantes

Si vemos nuestro objetivo en términos de programación, se reduce a evitar que la salud de la nave espacial descienda. Encontremos el objeto que mantiene el estado correspondiente.

Como no sabemos nada sobre el código del plugin, podemos inspeccionar directamente el heap usando la vista Memory del depurador de IntelliJ IDEA:

Aparece un menú al hacer clic en Configuración de diseño en la esquina superior derecha de la ventana de herramientas Debug Aparece un menú al hacer clic en Configuración de diseño en la esquina superior derecha de la ventana de herramientas Debug

Esta función te proporciona información sobre todos los objetos que están actualmente vivos. Escribamos invaders y veamos si podemos encontrar algo:

Escribir 'invaders' en el campo de búsqueda de la vista Memory muestra objetos de clases que pertenecen al paquete 'spaceinvaders' Escribir 'invaders' en el campo de búsqueda de la vista Memory muestra objetos de clases que pertenecen al paquete 'spaceinvaders'

Aparentemente, las clases del plugin pertenecen al paquete com.github.nizienko.spaceinvaders. Dentro de este paquete, hay una clase GameState con varias instancias activas. Parece lo que necesitamos.

Al hacer doble clic en GameState se muestran todas las instancias de esta clase:

Se abre un cuadro de diálogo que muestra las instancias vivas de GameState Se abre un cuadro de diálogo que muestra las instancias vivas de GameState

Resulta que es un enum – que no es exactamente lo que estábamos buscando. Continuando nuestra búsqueda, encontramos una sola instancia de Game.

Expandir el nodo nos permite inspeccionar los campos de la instancia:

Vista de memoria con un nodo de objeto expandido que muestra los campos del objeto Vista de memoria con un nodo de objeto expandido que muestra los campos del objeto

La propiedad health parece ser el de interés aquí. Entre sus campos, podemos encontrar _value. En mi caso, era 100, lo cual coincide con la barra de salud está llena cuando suspendí el juego. Entonces, es probable que este sea el campo correcto a considerar y que su valor oscile entre 0 y 100.

Ponemos esta hipótesis a prueba. Haz clic derecho en _value, luego selecciona Set Value. Elige un valor que sea diferente al que tienes actualmente. Por ejemplo, yo elegí 50.

Vista de memoria con un campo de texto contra el campo 'health' conteniendo el valor 50 ingresado por el usuario Vista de memoria con un campo de texto contra el campo 'health' conteniendo el valor 50 ingresado por el usuario

En este paso, nos encontramos con un error que dice Cannot evaluate methods after Pause action:

Un mensaje de error que dice 'Cannot evaluate methods after Pause action' Un mensaje de error que dice 'Cannot evaluate methods after Pause action'

El problema surge porque usamos Pause Program en lugar de puntos de interrupción, y esta característica tiene algunas limitaciones. Sin embargo, podemos recurrir a un pequeño truco para solucionar esto.

Lo describí en una de las publicaciones anteriores dedicada a Pause Program. En caso de que te lo hayas perdido allí, aquí está lo que se necesita hacer: una vez que la aplicación se ha pausado, realiza una acción de avance paso a paso, como Step Into o Step Over. Al hacerlo se habilitará el uso de características avanzadas como Set Value y Evaluate Expression.

Después de aplicar el truco descrito anteriormente, deberíamos poder establecer el valor para health. Intenta modificar el valor, luego reanuda la aplicación para ver si la barra de salud muestra algún cambio. ¡De hecho, lo hace!

Por lo tanto, hemos localizado el objeto que mantiene el estado relevante. Al menos, podemos llenar manualmente la barra de salud de vez en cuando. Aunque todavía no es un éxito completo, ya es un paso significativo hacia adelante.

Etiquetas y expresiones

Ahora que hemos identificado el objeto en el que debemos concentrarnos, sería útil marcarlo. Para aclarar, esto es lo que se ve un objeto marcado:

Pestaña Variables mostrando un array de objetos User, con uno de ellos marcado con una etiqueta de depuración que dice User_Charlie Pestaña Variables mostrando un array de objetos User, con uno de ellos marcado con una etiqueta de depuración que dice User_Charlie

Las etiquetas pueden ser beneficiosas de muchas maneras. Dentro del contexto de este artículo, marcar el objeto relevante asegura que podamos usarlo directamente en características como Evaluate Expression sin depender del contexto de ejecución actual.

Desafortunadamente, no es posible marcar directamente _value, pero podemos marcar el objeto que lo contiene. Para hacer esto, haz clic derecho en health, selecciona Mark Object, y luego dale un nombre.

Seleccione la etiqueta del objeto de diálogo que pide a tu usuario que ingrese un nombre para el objeto Seleccione la etiqueta del objeto de diálogo que pide a tu usuario que ingrese un nombre para el objeto

Ahora podemos probar cómo funciona la etiqueta en otros lugares. Abre el diálogo Evaluate Expression e ingresa health_object_DebugLabel como la expresión. Como puedes ver, el objeto es accesible desde cualquier lugar del programa a través del diálogo Evaluate:

Diálogo Evaluate con etiqueta de depuración ingresada como la expresión Diálogo Evaluate con etiqueta de depuración ingresada como la expresión

¿Qué tal cambiar la salud de la nave espacial desde Evaluate? health_object_DebugLabel._value = 100 no funciona.

Al mismo tiempo, _value parece ser un campo respaldado de una propiedad de Kotlin. Si esto es cierto, Kotlin debe haber generado un getter correspondiente:

health_object_DebugLabel.getValue()

El diálogo Evaluate no cree que esto sea código válido, pero lo intentaremos de todas formas:

Referenciar una propiedad a través de una etiqueta de depuración en el diálogo Evaluate Referenciar una propiedad a través de una etiqueta de depuración en el diálogo Evaluate

La expresión devuelve la salud actual de la nave espacial, ¡así que este enfoque funciona! Razonablemente, el setter debería funcionar también:

health_object_DebugLabel.setValue(100)

Después de evaluar el setter, reanudemos la aplicación y verifiquemos que los cambios hayan surtido efecto. ¡En efecto, la barra de salud está llena!

Engancha la expresión

El único paso restante para alcanzar nuestro objetivo es automatizar la modificación del estado, de modo que la ‘recarga de salud’ ocurra tras bambalinas, permitiéndonos disfrutar del juego sin interrupciones.

Esto se puede hacer usando puntos de interrupción no suspendidos. Este tipo de puntos de interrupción se suele utilizar para registrar; sin embargo, la expresión de registro no necesita ser necesariamente pura. Por lo tanto, podemos introducir el efecto secundario deseado dentro de la expresión de registro. Aún así, sigue siendo incierto dónde establecer este punto de interrupción, ya que no tenemos el código fuente de la aplicación.

¿Recuerdas que dije que podríamos usar las fuentes de la biblioteca estándar de Java? Aquí está la idea. IntelliJ IDEA y sus plugins están escritos en Java/Kotlin, y utilizan Swing como el framework de UI. Por lo tanto, Space Invaders seguramente llama a código de estas dependencias. Esto significa que podemos usar sus fuentes para establecer puntos de interrupción.

Info icon

Para mantener la simplicidad, no especificamos una versión de JDK en particular e inicializamos el proyecto con la que sugería IntelliJ IDEA. Sin embargo, para obtener los mejores resultados, se recomienda utilizar fuentes que coincidan estrechamente con la versión utilizada en el tiempo de ejecución.

Hay numerosas ubicaciones adecuadas para establecer un punto de interrupción. Decidí establecer un punto de interrupción de método en java.awt.event.KeyListener::keyPressed. Esto desencadenará el efecto secundario cada vez que presionemos una tecla:

Diálogo de puntos de interrupción que muestra un punto de interrupción de registro para java.awt.event.KeyListener::keyPressed Diálogo de puntos de interrupción que muestra un punto de interrupción de registro para java.awt.event.KeyListener::keyPressed
Info icon

Establecer un punto de interrupción con una expresión en un código caliente podría ralentizar significativamente la aplicación objetivo.

Regresemos a Space Invaders y veamos si nuestro modo IDDQD casero funciona. ¡Sí funciona!

Jugando a Space Invaders en IntelliJ IDEA: cada vez que la nave espacial recibe un golpe, su barra de salud se llena automáticamente

Conclusión

En este artículo, utilizamos el depurador para averiguar cómo funciona una aplicación por debajo. Después de ordenar esto, pudimos navegar en su memoria y modificar su funcionalidad, ¡todo sin acceder a las fuentes de la aplicación! Espero que mi comparación del depurador con IDDQD no haya sido demasiado audaz, y que hayas aprendido algunas técnicas que te darán poder en tus retos de depuración.

Me gustaría extender mis felicitaciones a Eugene Nizienko por hacer el plugin de Space Invaders y Egor Ushakov por compartir constantemente conmigo una gran cantidad de creatividad en la depuración y programación. Los ordenadores son el doble de divertidos con gente así.

Si tienes desafíos de depuración en mente que te gustaría que abordara en las próximas publicaciones, hazmelo saber!

¡Feliz hacking!

all posts ->