Cómo ganar en juegos Android… mediante reverse engineering

Hace unos meses, empecé a jugar a un juego de la plataforma Android. El caso es que como perdía más rondas que ganaba, decidí utilizar métodos menos ortodoxos: utilizar la ingeniería inversa para hacerme con el control de los datos de la aplicación, ganando en todo momento cualquier turno. Así pues, en un primer momento mi compañero David Lladró y yo, tratamos de descifrar las comunicaciones que se establecían entre la aplicación y el servidor.

Para ello, extrajimos el fichero “classex.dex” (el fichero ejecutable de la máquina virtual Dalvik que utiliza Android) del “apk” (el fichero mediante el cual se distribuyen e instalan las aplicaciones de Android) y lo convertimos a “jar” (el tipo de fichero en el que se empaquetan las clases compiladas de Java) mediante la herramienta “dex2jar”. A continuación, con la herramienta “jdgui”, descompilamos el fichero “jar”, y obtuvimos el código Java de la aplicación… o eso creíamos, ya que las funciones que generaban las claves de cifrado y descifrado estaban ofuscadas y fue imposible obtenerlas.

Ya de noche, y tras estar todo el día dándole vueltas, me propuse realizar el segundo asalto a la aplicación (qué paciencia hay que tener para aguantarme). Esta vez, decidí centrar mis esfuerzos en la lógica del programa, estudiar todo los procedimientos que se llevan a cabo en una partida, los tiempos… y esas pequeñas cosas que, de controlarlas, permitan tener una ventaja sobre el adversario y ganarle.

Apoyándome en el pseudo-código descompilado anteriormente (aún quitando los fragmentos ofuscados, el código no era ni mucho menos funcional), encontré que en la clase dónde se gestionan los diálogos de la aplicación había algo muy interesante:

ClaseTurno{
     ...
     diálogo.opciónPulsada(opciónSeleccionada);
     ...
}

De este código se podía entender que se almacena mi jugada en una variable de la clase diálogo, y que después se enviará (presumiblemente, cifrada mediante la clave que no fuimos capaces de conseguir al principio) al servidor para que este facilite la siguiente jugada. También pude comprobar que en la clase existía un atributo llamado “correcta”, que por el nombre, supuse que almacenaría el identificador de la jugada válida.

A estas alturas, seguro que el lector ve por dónde van a ir los tiros… Si en la memoria del teléfono ya tengo almacenado el valor de la jugada que me hace ganar, ¿qué me impide modificar la aplicación para que siempre se envíe el valor de dicho atributo, ignorando el valor que haya facilitado?

Sin embargo, existía un problema: como ya he dicho, el código descompilado no era en absoluto funcional. Por lo que no se me ocurrió otra idea que bajar un nivel y comenzar a jugar con el código smali (el lenguaje ensamblador para la máquina virtual Dalvik). Para ello, volví otra vez al fichero apk, y con la herramienta “apktool” (que más tarde nos permitirá reconstruir la aplicación a partir de los ficheros obtenidos), extraje todos los ficheros smali que componían la aplicación.

Ya con la estructura smali en mano, surgió un nuevo problema: el código anterior sobrescribía un método de una clase padre, que a su vez estaba incluida en otra clase (la propia del fichero Java), por lo que el compilador de Dalvik había separado todas estas clases en ficheros de tipo “NombreClase$$N.smali”, donde $N es un número de fichero. Tras un rato, identifiqué el código que se correspondía con el fragmento mostrado anteriormente:

iget-object v0, p0, ClaseTurno$5;->this$0:ClaseTurno;

iget-object v0, v0, ClaseTurno;->diálogo;

iget v1, p0, ClaseTurno$5;->opciónSeleccionada:I

invoke-interface {v0, v1}, diálogo;->opciónPulsada(I)V

Para entender este código, debemos saber que el registro p0 es la referencia a “this”, que es utilizado como dirección base para cargar una referencia a la clase principal en el registro v0, que a continuación es sobrescrito con una referencia a diálogo. Siguiendo el mismo procedimiento, en el registro v1 se carga la variable opciónSeleccionada, para a continuación ejecutar una llamada a la función opciónPulsada que está almacenada en v0, pasándole como único parámetro el registro v1.

Llegados a este punto, sólo tuve que hacer los siguientes cambios para conseguir que independientemente de la opción pulsada, siempre se enviase al servidor la opción que me hace ganar:

iget-object v0, p0, ClaseTurno$5;->this$0:ClaseTurno;

iget-object v0, v0, ClaseTurno;->diálogo;

iget-object v1, p0, ClaseTurno$5;->this$0:ClaseTurno;

iget v1, v1, ClaseTurno;->correcta:I

invoke-interface {v0, v1}, diálogo;->opciónPulsada(I)V

De manera similar al código smali anterior, ahora en v1 cargamos la referencia a la clase principal, que es donde está almacenado el atributo correcta, y a continuación sobrescribimos el registro con dicho atributo. De este modo, siempre se llamará al método opciónPulsada pasándole como parámetro el atributo “correcta”, lo que hará que se envíe la respuesta correcta independientemente de nuestra elección.

Para poder ejecutar la aplicación, sólo hace falta reensamblar el apk con “apktool”, y firmarlo con una clave que nosotros hayamos generado (el manual del desarrollador de Android da buenos ejemplos sobre cómo hacerlo) para que se pueda instalar en cualquier teléfono Android que esté soportado por la aplicación original.

Cómo hemos podido ver en esta entrada, la aplicación ha sido vulnerada porque el servidor remoto no realizaba ninguna comprobación, ya que se delegaban en el cliente. Aunque puede parecer trivial, este error es común en aplicaciones que utilicen una arquitectura cliente-servidor como son las aplicaciones web. Por tanto, como corolario recordar que las comprobaciones siempre se deben realizar en el lado del servidor, independientemente de si el cliente las realiza o no.

Antes de finalizar el post, me gustaría aclarar que el código mostrado aquí ha sido modificado por respeto a los derechos de los autores de la misma. Y aunque todo lo mostrado ya no funciona, puesto que nos pusimos en contacto con los desarrolladores de la aplicación para darles tiempo a arreglar este error, somos conscientes de que existen otros que permitirían realizar comportamientos similares. Aún así, desde aquí me gustaría agradecer la profesionalidad y deportividad con la que se tomaron todo este asunto.

Comments

  1. Esto huele a triviados y recuerda al spectrum metiendo pokes xD.

  2. Buen trabajo chicos, solo puedo decir 2 cosas:

    – Habéis hecho un trabajo de chinos

    – Buen gesto por vuestra parte el comunicar el error al desarrollador :)

  3. Fernando Seco says

    Lo que hace el vicio…

  4. ¿Algo que ver con esto? http://cl.ly/V6r9