Ingeniería inversa en una aplicación con licencia

Veamos en el siguiente ejercicio cómo realizar la ingeniería inversa en la aplicación desarrollada en el punto anterior y luego la modificaremos para que no verifique la licencia. Vamos a trabajar con código Dalvik en notación smali. En el siguiente ejercicio aprenderemos más sobre este código.

Ejercicio paso a paso: Ingeniería inversa en una aplicación con licencia.

En este ejercicio vamos a tratar de eliminar la comprobación de licencia de la aplicación desarrollada en el ejercicio anterior. Al no estar ofuscada podríamos decompilar el código a Java para estudiar mejor su contenido. Este paso nos lo vamos a saltar, dado que nosotros mismos hemos escrito este código.

1.    Copia el fichero APK generado por el proyecto ObtencionLicencia dentro de la carpeta place-apk-here-for-modding de la aplicación APK Multi-Tool.

2.     Ejecuta APK Multi-Tool (script.bat) y selecciona la opción 24. Selecciona el APK copiado en el punto anterior.

3.     Selecciona la opción 9 para decompilar este APK.

4.     Observa cómo en la carpeta Projects se ha generado una carpeta con el nombre del proyecto. En ella puedes encontrar un fichero smali por cada una de las clases originales. Cada uno de estos ficheros estará en unas carpetas según el paquete al que pertenezcan. La estructura de carpetas creadas y los ficheros del paquete org.tomas.girones.jesus.obtencionlicencia se muestran a continuación:


5.     Abre el fichero MainActivity.smali y compáralo con MainActivity.java del ejercicio anterior:

.class public Lorg/tomas/girones/jesus/obtencionlicencia/MainActivity;

.super Landroid/app/Activity;

.source "MainActivity.java"

# interfaces

.implements Lcom/google/android/vending/licensing/LicenseCheckerCallback;

# static fields

.field private static final CLAVE_PUBLICA_LICENCIA:Ljava/lang/String; = …

.field private static final SALT:[B

# instance fields

.field comprobarLicencia:Lcom/google/android/vending/licensing/                                                                   LicenseChecker;

.field dialogo:Landroid/app/ProgressDialog;

.field permitir:Z

# direct methods

.method static constructor <clinit>()V

    .locals 1

    .prologue

    .line 18

    const/16 v0, 0x14

    new-array v0, v0, [B

    fill-array-data v0, :array_0

    sput-object v0, Lorg/…/obtencionlicencia/MainActivity;->SALT:[B

    .line 19

    return-void

    ...

Nota: Las líneas subrayadas desaparecerán en la versión ofuscada que veremos más adelante.

6.     Busca el método allow()y compáralo con su equivalente en Java: 

@Override public void allow(int reason) {

   permitir = true;

   …

# virtual methods

.method public allow(I)V     

    .locals 3

    .parameter "reason"

    .prologue

    const/4 v2, 0x1

    .line 54

    iput-boolean v2, p0, Lorg/tomas/girones/jesus/obtencionlicencia/
                                                                                       MainActivity;->permitir:Z

    … 

Aunque no conozcamos en profundidad la notación smali, resulta fácil de interpretar. Las líneas que empiecen por son comentarios. Luego se define el método público allow() que toma como parámetro un int (I) y devuelve void (V). La siguiente línea indica que va a utilizar tres variables locales. Se identificarán como los registros v0, v1 y v2. Estos registros son siempre de 32 bits[1] y pueden almacenar un tipo simple o un puntero. Si el método recibe parámetros, estos se almacenan en los últimos registros. Todo método que no sea estático recibe como primer parámetro un puntero al objeto llamado (this). Este parámetro está implícito, no se indica en la lista de parámetros. En el método estudiado tendremos dos parámetros: this, que se almacenará en v1, y un entero, que se encuentra en v2. Para facilitar la lectura a estos registros se les asigna un nombre alternativo: v1 = p0 (primer parámetro) y v2 = p1 (segundo parámetro)[2][3].

Luego se indica que el primer parámetro en Java se llamaba reason. Esta es información de debug no relevante para ejecutar el código. Por fin llegamos a las instrucciones Dalvik[4]. La primera almacena la constante de 4 bits 0x1en el registro v2. Los valores constantes se suelen expresar en hexadecimal, en este caso equivale a 1. La siguiente instrucción almacena el valor booleano que hay en v2 en el campo permitir del objeto indicado en p0.

7.     Analicemos la siguiente línea de Java:

  Toast.makeText(this,"Licencia correcta: "+reason, Toast.LENGTH_LONG)
                                                                   .show();

Que ha sido compilada a:

    …

    .line 55

    new-instance v0, Ljava/lang/StringBuilder;

    const-string v1, "Licencia correcta: "

    invoke-direct {v0, v1}, Ljava/lang/StringBuilder;-><init>
                                                                                              (Ljava/lang/String;)V

    invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->
                                                                   append(I)Ljava/lang/StringBuilder;

    move-result-object v0

    invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()
                                                                                                     Ljava/lang/String;

    move-result-object v0

    invoke-static {p0, v0, v2}, Landroid/widget/Toast;->  
                    makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)
                                                                                               Landroid/widget/Toast;

    move-result-object v0

    .line 56

    invoke-virtual {v0}, Landroid/widget/Toast;->show()V

    … 

Creamos una nueva instancia de la clase StringBuilder que será apuntada por v0. Luego hacemos que v1 apunte a la constante de cadena indicada. Invocamos al constructor (<init>) de StringBuilder pasándole dos parámetros: en v0 el puntero al objeto a crear y en v1el objeto String para inicializarlo. Esta llamada no devuelve ningún resultado (V). Luego invocamos al método StringBuilder.append(int) pasándole dos parámetros: en v0 el puntero al objeto y en p1el entero a concatenar, en este caso el parámetro con el que nos han llamado (reason). El resultado es un nuevo StringBuilde con la concatenación de este entero. En la siguiente instrucción movemos el resultado de la última invocación al registro v0. A continuación llamamos al método estático[1] Toast.makeText(Context, CharSequence, int), pasándole tres parámetros[2]: p0 con nuestra referencia (this), v0el StringBuilderantes creado (esta clase es descendiente de CharSequence) y v2 con el valor 1 (LENGTH_LONG) . Este método devuelve un objeto Toast que es almacenado en la siguiente instrucción en v0. La última instrucción mostrada corresponde a la línea 56 de Java. En ella se llama al método virtual Toast.show() del objeto apuntado por v0.

8.     Analicemos el final del método:

    …

    dialogo.dismiss();

}

Que ha sido compilada a:

    …

    .line 57

    iget-object v0, p0, Lorg/tomas/girones/jesus/obtencionlicencia/
                                        MainActivity;-> dialogo:Landroid/app/ProgressDialog;

    invoke-virtual {v0}, Landroid/app/ProgressDialog;->dismiss()V

    .line 58

    return-void

.end method

La primera instrucción almacena en v0 la referencia al objeto almacenado en el campo dialog del objeto apuntado por p0. También se indican las clases de los dos objetos apuntados. En la siguiente instrucción se llama al método ProgressDialog.dismiss()del objeto apuntado por v0. La última instrucción retorna del método sin devolver ningún parámetro.

9.     Piensa las modificaciones que tendríamos que introducir en el código para que pudiéramos ejecutar la aplicación sin disponer de licencia. Una de las opciones más sencillas sería modificar el método dontAllow() para que ejecutara permitir = true; en lugar de permitir = false;. Veamos cómo se modificaría esta instrucción en los siguientes puntos.

10.     Busca el método dontAllow() y compáralo con su equivalente en Java: 

@Override public void dontAllow(int reason) {

   permitir = false;

   …

 

# virtual methods

.method public dontAllow(I)V     

    .locals 2

    .parameter "reason"

    .prologue

    const/4 v0, 0x0

    .line 62

    iput-boolean v0, p0, Lorg/tomas/girones/jesus/obtencionlicencia/
                                                                                       MainActivity;->permitir:Z

    …   

11.     El valor subrayado corresponde a false. Para cambiarlo a true reemplázalo por el valor 0x1. Guarda el fichero MainActivity.smali.

12.     En APK-Multi-Tool selecciona la opción 12 Compile Non-System APK Files.

13.     Selecciona la opción 2 Create an unsigned apk. Dentro de la carpeta place-apk-here-for-modding se creará el fichero unsignedApalabrados.apk.

14.     Selecciona la opción 13 Sign apk. Dentro de la carpeta anterior se creará el fichero signedObtencionLicencia.apk.

15.     Instala este fichero en un dispositivo real y ejecútalo.

16.     Entra dentro de Google Play Developer Console y en la izquierda selecciona la pestaña de Configuración.

17.     Modifica el valor de Respuesta de licencia de prueba a NOT_LICENSEC.

18.     En la aplicación pulsa el botón «Comprobar Licencia». Verifica que no se obtiene licencia.

19.     Pulsa el botón «Entrar». Verifica que se permite la entrada.

 Práctica: Modificar otros aspectos de la aplicación de licencia. 

 

Trata de realizar otros cambios en la aplicación. Por ejemplo, que te permita entrar en la aplicación sin comprobar la licencia. También puedes eliminar el Toast que muestra el resultado negativo de la verificación de licencia.


[1]Observa como en este ejemplo aparecen tres tipos de invocaciones:

invoke-virtual: se utiliza para llamar a métodos de forma dinámica. En este tipo de llamada se indica al objeto el método y es él quien decide el método al que realmente se llama. Este tipo de llamada permite el polimorfismo de Java. Es decir, si en la llamada a StringBuilder.append()el objeto usado fuera un descendiente de esta clase y hubiera sobrescrito este método, se llamaría a un método diferente a StringBuilder.append().

invoke-direct: se llama directamente al método, sin realizar una resolución dinámica. Se utiliza con constructores o cuando se indica private o final en el método.

invoke-static:se llama cuando se indica static en el método. Se hace una llamada directa, pero a diferencia de los casos anteriores no se indica el objeto involucrado.

[2]Recuerda que en llamada a un método estático no se añade de forma implícita una referencia al objeto.


[1]Un boublede 64 bits se almacena en dos registros.

[2]Si en este método se hubieran pedido cuatro registros (.locals 4), el primer parámetro estaría en v2=p0y el segundo en v3=p1.

[3]Información sobre registros y parámetros: https://code.google.com/p/smali/wiki/Registers.

[4]Puedes encontrar una tabla con el significado de todas las instrucciones Dalvick en http://pallergabor.uw.hu/androidblog/dalvik_opcodes.html.