El objetivo del presente artículo es mostrar el uso de la herramienta radare2 (en adelante r2), para realizar reversing en aplicaciones Android. Utilizando como ejemplo el conocido cliente de Twitter, Falcon Pro.
Mi intención no es fomentar en ningún momento la piratería de las aplicaciones ni desacreditar el arduo trabajo de los desarrolladores. Únicamente estimo interesante el hecho de mostrar cómo modificar el comportamiento original de una aplicación utilizando nuevas herramientas.
Introducción
La elección de Falcon Pro como objeto de experimento no viene dada por mera coincidencia. El desarrollador, se ha tomado las molestias de introducir técnicas anti-tampering para evitar la posibilidad de realizar modificaciones del classes.dex original, comprobaciones de licencia para verificar que la aplicación ha sido comprada a través del market de Google, y una protección en especial, entre algunas otras, que me molesta mucho; determinar si la herramienta ‘Lucky Patcher‘ se encuentra instalada en el terminal y en caso de ser así, impedir la ejecución conjunta de ambas, al menos hasta que el usuario no decida desinstalarla.
Esto es debido a que mayoritariamente ‘Lucky Patcher‘ es una herramienta utilizada para hacer modificaciones ‘on-fly‘ de las aplicaciones instaladas en un dispositivo, permitiendo evadir comprobaciones de licencias, modificaciones de permisos, o realizar en general cualquier alteración de código que pueda alterar el comportamiento original de la aplicación.
En lo personal, utilizo ambas en varios de mis terminales, y como he adquirido sendas licencias, el hecho de que me priven utilizar dichas utilidades conjuntamente me molesta. Por ello, la solución que mostraré a continuación, consiste en eliminar esta comprobación además de las técnicas anti-tampering implementadas.
Aunque en este ejemplo en concreto estemos utilizando r2, existen muchas otras alternativas, por ejemplo desensamblando el fichero de clases y modificando directamente el código dalvik.
Asentando las bases
Para hacer más llevadera la lectura y el seguimiento de las explicaciones, antes de adentrarnos a utilizar r2, explicaremos como modificar la aplicación desensamblando y modificando directamente el código dalvik de la misma. Más tarde podremos volver sobre nuestros pasos y utilizar las nociones adquiridas para afrontar el reto de conseguir nuestro objetivo abordándolo por otra vía.
Sirviéndonos de la utilidad apktool, extraeremos los fuentes:
$ java -jar apktool.jar d -r com.jv.falcon.pro-1.apk
Esto nos generará una salida repleta de ficheros con la extensión *.smali, podéis observar el resultado final en la siguiente captura:
Si os fijáis, algunos ficheros tienen por nombre palabras sin sentido aparente, analizando cualquiera de ellos, podréis descubrir que ocurre lo mismo con los métodos, o los nombres de las variables. Estamos ante una aplicación ofuscada con Proguard, por lo que podemos discernir que nuestro desarrollador, no quiere ponernos las cosas fáciles.
Aun así, nuestro primer paso será realizar una rápida toma de contacto con el código, buscando posibles cadenas que nos permitan discernir hacia dónde dirigir nuestra siguiente actuación. En lo personal, dirigiría mi búsqueda hacia los siguientes valores:
$ grep -Hrsin "validator" * --color=AUTO … cv.smali:190: const-string v0, "LicenseValidator" cv.smali:246: const-string v0, "LicenseValidator" …
Parece que existe un ‘LicenseValidator‘, probemos más suerte buscando por ‘license‘
$ grep -Hrsin "license" * --color=AUTO … cl.smali:43: const-string v0, "com.android.vending.licensing.ILicenseResultListener" cq.smali:760: const-string v1, "LicenseChecker" tf.smali:81: const-string v0, "de.androidpit.app.services.ILicenseService" ti.smali:78: const-string v1, "LICENSED" ti.smali:87: const-string v1, "NOT_LICENSED" tj.smali:289: const-string v0, "AndroidPitLicenseChecker" …
En esta ocasión podemos denotar que hemos acertado más con la cadena a buscar, y podemos afirmar que la aplicación parece basar su seguridad en la conocida ‘Android License Verification Library‘ que se encarga de comprobar si el usuario ha adquirido legalmente una copia legítima procedente de la Play Store.
No obstante, no estando conforme con ello, nuestro desarrollador también ha incluido una comprobación adicional, basada en ‘AndroidPIT Licensing Library‘, como nuestro objetivo principal no es desproveer a la aplicación de este tipo de protecciones, no me detendré en explicar cómo eliminarlas, así que realizaremos una nueva búsqueda compuesta por “luckypatcher“.
$ grep -Hrsin "luckypatcher" * --color=AUTO … rl.smali:253: #const-string v1, "com.dimonvideo.luckypatcher" rl.smali:326: #const-string v0, "/LuckyPatcher" …
Sin aventurarnos demasiado, podemos conjeturar que tal vez lo que andamos buscando se encuentra en el fichero ‘rl.smali‘. Analizándolo más de cerca, podemos encontrar los siguientes puntos de interés:
En la línea 125, se define el siguiente método: ‘.method public static a(Landroid/content/Context;)Ljava/lang/String;‘, donde se encarga de comprobar que la aplicación Falcon Pro está instalada bajo el packagename ‘com.jv.falcon.pro‘, obteniendo la firma asociada al mismo. En caso de no encontrar coincidencia alguna, se abortará la ejecución.
.method public static a(Landroid/content/Context;)Ljava/lang/String;
.locals 3
.parameter
.prologue
.line 132
invoke-virtual {p0}, Landroid/content/Context;->getPackageManager()Landroid/content/pm/PackageManager;
move-result-object v0
.line 135
:try_start_0
const-string v1, "com.jv.falcon.pro"
const/16 v2, 0x40
invoke-virtual {v0, v1, v2}, Landroid/content/pm/PackageManager;->getPackageInfo(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;
move-result-object v0
.line 136
iget-object v1, v0, Landroid/content/pm/PackageInfo;->signatures:[Landroid/content/pm/Signature;
.line 137
new-instance v0, Ljava/lang/String;
const/4 v2, 0x0
aget-object v1, v1, v2
invoke-virtual {v1}, Landroid/content/pm/Signature;->toChars()[C
move-result-object v1
invoke-direct {v0, v1}, Ljava/lang/String;->;([C)V
:try_end_0
.catch Landroid/content/pm/PackageManager$NameNotFoundException; {:try_start_0 .. :try_end_0} :catch_0< .line 141 :goto_0 return-object v0 .line 139 :catch_0 move-exception v0 .line 140 invoke-virtual {v0}, Landroid/content/pm/PackageManager$NameNotFoundException;->printStackTrace()V
.line 141
const/4 v0, 0x0
goto :goto_0
.end method
Nuevamente en la línea 245, nos encontramos con el siguiente método: '.method public static b(Landroid/content/Context;)Z', donde comprueba si entre los paquetes instalados en nuestro terminal, se encuentra alguna coincidencia con las cadenas: 'com.dimonvideo.luckypatcher' y 'com.chelpus.lackypatch'. Esos nombres hacen alusión al packageName que posee LuckyPatcher. Efectivamente, es aquí donde se está realizando la primera comprobación que nosotros queremos evitar:
.method public static b(Landroid/content/Context;)Z
.locals 2
.parameter
.prologue
const/4 v0, 0x1
.line 367
const-string v1, "com.dimonvideo.luckypatcher"
invoke-static {p0, v1}, Lrl;->a(Landroid/content/Context;Ljava/lang/String;)Z
move-result v1
if-eqz v1, :cond_1
.line 375
:cond_0
:goto_0
return v0
.line 371
:cond_1
const-string v1, "com.chelpus.lackypatch"
invoke-static {p0, v1}, Lrl;->a(Landroid/content/Context;Ljava/lang/String;)Z
move-result v1
if-nez v1, :cond_0
.line 375
const/4 v0, 0x0
goto :goto_0
.end method
En el primer caso, está asignando el string 'com.dimonvideo.luckypatcher' al registro v1 y pasándolo como parámetro al método '.method private static a(Landroid/content/Context;Ljava/lang/String;)Z', el resultado es almacenado en v1 nuevamente, y se comprueba que sea distinto de cero, en caso de ser así, se habrá encontrado alguna coincidencia y el proceso de Falcon Pro se cerrará provocando un stackTrace.
Análogamente sucede lo mismo para el string 'com.chelpus.lackypatch', por lo que obviaremos su explicación. Aunque sí nos centraremos en detallar qué hace el método mencionado anteriormente y del que se apoya esta comprobación:
.method private static a(Landroid/content/Context;Ljava/lang/String;)Z
.locals 3
.parameter
.parameter
.prologue
const/4 v0, 0x0
.line 380
:try_start_0
invoke-virtual {p0}, Landroid/content/Context;->getPackageManager()Landroid/content/pm/PackageManager;
move-result-object v1
const/4 v2, 0x0
invoke-virtual {v1, p1, v2}, Landroid/content/pm/PackageManager;->getApplicationInfo(Ljava/lang/String;I)Landroid/content/pm/ApplicationInfo;
:try_end_0
.catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0
move-result-object v1
.line 382
if-nez v1, :cond_0
.line 394
:goto_0
return v0
.line 389
:cond_0
const/4 v0, 0x1
goto :goto_0
.line 390
:catch_0
move-exception v1
goto :goto_0
.end method
Basicamente se realiza una llamada de comprobación al método 'getPackageManager().getApplicationInfo(paramString, 0)' donde paramString, es el nombre del paquete que se quiere comprobar si está instalado. De no existir coincidencia, el resultado será 0, y en caso contrario 1.
Independientemente del valor, el resultado será devuelto a '.method public static b(Landroid/content/Context;)Z' y se comprobará con la instrucción "if-nez v1, :cond_0" definida en las líneas 259 y 274 respectivamente.
La forma de saltarnos esta restricción es variada, en nuestro caso, vamos a proceder a modificar el nombre del paquete que está buscando por cualquier otra string:
- 253 #const-string v1, "com.dimonvideo.luckypatcher" + 254 const-string v1, "aaaaa"
Repetimos el paso nuevamente:
- 268 #const-string v1, "com.chelpus.lackypatch" + 269 const-string v1, "bbbbb"
Una vez llegados a este punto, si recordamos la búsqueda inicial con grep, había otra coincidencia para "/LuckyPatcher" hospedada en el método ".method public a()V":
.method public a()V
.locals 3
.prologue
.line 67
const/4 v0, 0x0
iput-object v0, p0, Lrl;->h:Lru;
.line 69
iget-object v0, p0, Lrl;->g:Landroid/content/Context;
invoke-static {v0}, Lrl;->b(Landroid/content/Context;)Z
move-result v0
if-eqz v0, :cond_1
.line 74
:try_start_0
const-string v0, "/LuckyPatcher"
invoke-static {v0}, Landroid/os/Environment;->getExternalStoragePublicDirectory(Ljava/lang/String;)Ljava/io/File;
move-result-object v0
.line 76
invoke-virtual {v0}, Ljava/io/File;->exists()Z
move-result v1
if-eqz v1, :cond_0
invoke-virtual {v0}, Ljava/io/File;->isDirectory()Z
move-result v1
if-eqz v1, :cond_0
.line 77
const-wide/16 v1, 0x0
invoke-static {v0, v1, v2}, Lrx;->a(Ljava/io/File;J)Z
:try_end_0
.catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0
.line 83
:cond_0
:goto_0
iget-object v0, p0, Lrl;->d:Ltn;
invoke-interface {v0}, Ltn;->b()V
.line 102
:goto_1
return-void
.line 79
:catch_0
move-exception v0
.line 80
invoke-virtual {v0}, Ljava/lang/Exception;->printStackTrace()V
goto :goto_0
.line 90
:cond_1
iget-object v0, p0, Lrl;->g:Landroid/content/Context;
invoke-static {v0}, Lrl;->a(Landroid/content/Context;)Ljava/lang/String;
move-result-object v0
.line 92
const-string v1, "308202f1308201d9a00302010202044ec4ab45300d06092a864886f70d01010b05003029310e300c060355040713055061726973311730150603550403130e4a6f617175696d20566572676573301e170d3132313133303138313330395a170d3432313132333138313330395a3029310e300c060355040713055061726973311730150603550403130e4a6f617175696d2056657267657330820122300d06092a864886f70d01010105000382010f003082010a02820101009e239ca473ada19b7cea9c06cee1ad972af1abb359660eca394818f79d3253fecb25dd2ae7e8ff50d28ed094599b78bc1eab989c9072c872cb4bdf0d5de4f8a8120187603f83e44c4b1a24cc3faf3db88ee9fa20ea9033d6f9f13ea8a5a393c66c2db1b194c41392d0bad7d4493a1cc64e205e8c674047943f7b59264bd76e77d357e721738129250fbef1ba8ff6e3ff7d35a5e58645573a54214d7f710e6aab5422f18caa85042498503da98bc3ff9f8d238915a5e2a095bd2a851bdfff943dbed534bf1d515cda174063a12025f230843e7b0584870be02db941ade13a2a9fe8b59eae3c4e18b2f616f539176e40be36a575b0ba3b75814e3539547cc88a990203010001a321301f301d0603551d0e04160414e39010011f003633f3f95d31c26c5c4c2b443f18300d06092a864886f70d01010b050003820101008a17436dd486111dbb0da3c5f23257ad845746457e9599c82ef52566afa5613922957d88394da56fd189ecf403a8e3ee6ddba1eb1f455cd9d85e89dd5e642bb73b283f44ef4f0cbffa36b217062ef0fea2f4d1b2ff8030b722135f81a9c995bcbe2ed5498494bc366fd6b8a6caca9d33ea64238c025959817d6314c303ecb5952b44c5f9f29596167a48ebca66ebbc68deb97a52ccf0235df97db80bf8c4a8e5c103a187caa3c70050353a0b58fa6fd3e2f7c8897eafceb5934c13dfbc32f8d5235f7208b8c19f1f52fc75dd8618182e27d33f5b1fd55d2dcaea79172102b0e30a69f68993419e8e5ed434b1355ba25f51a8ffa40915324e63489864877b7e7e"
invoke-virtual {v1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v0
#if-eqz v0, :cond_2
.line 95
iget-object v0, p0, Lrl;->e:Ltj;
iget-object v1, p0, Lrl;->d:Ltn;
invoke-virtual {v0, v1}, Ltj;->a(Ltn;)V
goto :goto_1
.line 99
:cond_2
iget-object v0, p0, Lrl;->d:Ltn;
invoke-interface {v0}, Ltn;->b()V
goto :goto_1
.end method
En este punto se suceden dos comprobaciones, en primer lugar se determina si existe algún fichero bajo ese nombre y si es un directorio. En caso de ser así se produce una llamada al método 'invoke-static {v0, v1, v2}, Lrx;->a(Ljava/io/File;J)Z', el cual está definido en el fichero 'rx.smali' concretamente en la línea 988, y su cometido es eliminar todo los ficheros que hayan podido ser creados por la herramienta LuckyPatcher, una solución poco ortodoxa, intrusiva y muy poco ética.
.method public static a(Ljava/io/File;J)Z
.locals 13
.parameter
.parameter
.prologue
const/4 v1, 0x0
.line 585
invoke-virtual {p0}, Ljava/io/File;->list()[Ljava/lang/String;
move-result-object v3
.line 586
if-eqz v3, :cond_0
move v0, v1
.line 587
:goto_0
array-length v2, v3
if-lt v0, v2, :cond_1
.line 610
:cond_0
const/4 v0, 0x1
return v0
.line 589
:cond_1
new-instance v4, Ljava/io/File;
aget-object v2, v3, v0
invoke-direct {v4, p0, v2}, Ljava/io/File;->(Ljava/io/File;Ljava/lang/String;)V
.line 590
invoke-virtual {v4}, Ljava/io/File;->isDirectory()Z
move-result v2
if-eqz v2, :cond_5
.line 593
invoke-virtual {v4}, Ljava/io/File;->list()[Ljava/lang/String;
move-result-object v5
.line 594
if-nez v5, :cond_3
.line 587
:cond_2
:goto_1
add-int/lit8 v0, v0, 0x1
goto :goto_0
.line 597
:cond_3
array-length v6, v5
move v2, v1
:goto_2
if-ge v2, v6, :cond_2
aget-object v7, v5, v2
.line 598
new-instance v8, Ljava/io/File;
invoke-direct {v8, v4, v7}, Ljava/io/File;->(Ljava/io/File;Ljava/lang/String;)V
.line 599
invoke-static {}, Ljava/lang/System;->currentTimeMillis()J
move-result-wide v9
invoke-virtual {v8}, Ljava/io/File;->lastModified()J
move-result-wide v11
add-long/2addr v11, p1
cmp-long v7, v9, v11
if-lez v7, :cond_4
.line 600
invoke-virtual {v8}, Ljava/io/File;->delete()Z
.line 597
:cond_4
add-int/lit8 v2, v2, 0x1
goto :goto_2
.line 604
:cond_5
invoke-virtual {v4}, Ljava/io/File;->isDirectory()Z
move-result v2
if-nez v2, :cond_2
invoke-static {}, Ljava/lang/System;->currentTimeMillis()J
move-result-wide v5
invoke-virtual {v4}, Ljava/io/File;->lastModified()J
move-result-wide v7
add-long/2addr v7, p1
cmp-long v2, v5, v7
if-lez v2, :cond_2
.line 606
invoke-virtual {v4}, Ljava/io/File;->delete()Z
goto :goto_1
.end method
Volviendo a nuestro foco anterior, comprobamos que posteriormente tras la llamada a 'Lrx;->a(Ljava/io/File;J)Z' se procede a almacenar en el registro v0 el resultado de la llamada al método: 'Lrl;->a(Landroid/content/Context;)Ljava/lang/String;', esta función, comentada más arriba determina si existe el packageName 'com.jv.falcon.pro' en el dispositivo y devuelve el signature del mismo.
Si continuamos analizando el código, las instrucciones venideras son una asignación al registro v1 de lo que parece ser la firma que se espera encontrar, y la posterior llamada a 'invoke-virtual {v1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z' para comprobar si el contenido de los registros v0 y v1 coinciden. El resultado es depositado nuevamente en v0, con la instrucción 'move-result v0' y se comprueba si es igual a cero con 'if-eqz v0, :cond_2'.
Todo esto se corresponde con una técnica de anti-tampering, para evitar que la aplicación pueda ser decompilada y modificada, basándose en la comprobación de la firma que posee el paquete. Las modificaciones que nosotros vamos a introducir son las siguientes:
- 326 #const-string v0, "/LuckyPatcher" + 327 const-string v0, "aaaa"
Para que busque un directorio inexistente, y además comentaremos la comprobación realizada por 'if-eqz v0, :cond_2' :
- 387 if-eqz v0, :cond_2 + 387 #if-eqz v0, :cond_2
Con estas modificaciones conseguimos hacer gran parte del trabajo, pero hay otros puntos donde debemos de repetir el proceso. Para ello realicemos la siguiente búsqueda:
$ grep -Hrsin "30820" * --color=AUTO iv.smali:33: const-string v0, "308202f1308201d9a00302010202044… rl.smali:381: const-string v1, "308202f1308201d9a00302010202044… rl.smali:529: const-string v1, "308202f1308201d9a00302010202044…
Todas estas referencias hacen alusión al problema comentado anteriormente, se tratan de comprobaciones para evitar la modificación de la aplicación. Aplicando los siguientes cambios, habremos finalizado nuestro trabajo:
Fichero: iv.smali
- 49 if-eqz v0, :cond_0 + 49 #if-eqz v0, :cond_0
Fichero: rl.smali (Únicamente modificaremos la última coincidencia, la anterior ya la hemos realizado)
- 535 if-eqz v0, :cond_0 + 535 #if-eqz v0, :cond_0
Si recompilamos y realizamos una instalación nueva de 'Falcon Pro', teniendo 'Lucky Patcher' en el mismo dispositivo, podremos observar como nuestro problema ha desaparecido:
Avanzando con r2
Después de haber explicado la base de nuestra investigación, el objetivo a cumplir, y habiendo expuesto cómo realizarlo utilizando técnicas convencionales, es el momento de avanzar un poco más y reproducir el mismo resultado utilizando r2. Como habréis visto, todo lo que se ha explicado anteriormente es trivial y no hay nada de nuevo en ello.
La mejor forma de comenzar, será adquirir una copia del repositorio de r2 desde Git, si no lo tenéis hecho ya:
$ git clone git://github.com/radare/radare2 $ cd radare2 $ sudo sys/install.sh
Como queremos realizar modificaciones sobre el fichero de clases, será necesario que lo abramos de la siguiente forma:
$ r2 -w classes.dex
O si lo preferís, podéis especificarle el APK, y ya se encargará automáticamente de descomprimirlo y abrir el fichero dex:
$ r2 -w apk://com.jv.falcon.pro-1.apk
Antes de comenzar, recapitulemos qué cadenas necesitamos encontrar para servirnos como punto de apoyo:
- com.dimonvideo.luckypatcher
- com.chelpus.lackypatch
- /LuckyPatcher
- 308202f1308201d9a00302010202044…
Si os fijáis, nada más abrir el fichero, automáticamente nos colocamos en el offset 0x00027284, esto es debido a que se trata del entry-point, de cara a poder inspeccionar el fichero completo será mejor situarnos en el offset 0x00000000:
[0x00027284]> s 0
Si tratamos de buscar referencias a las cadenas en los opcodes que conforman la aplicación, no encontraremos nada, únicamente referencias a las direcciones de memoria donde estas se encuentran. Por lo que deberemos de dirigirnos a la sección string_data (Más información) y buscar allí directamente utilizando para ello el comando ‘/‘ seguido del string en cuestión:
[0x00000000]> / com.dimonvideo.luckypatcher Searching 27 bytes from 0x00000000 to 0x003911b8: 63 6f 6d 2e 64 69 6d 6f 6e 76 69 64 65 6f 2e 6c 75 63 6b 79 70 61 74 63 68 65 72 [# ]hits: 190fc0 < 0x003911b8 hits = 1 0x00171d66 hit0_0 "com.dimonvideo.luckypatcher" [0x00000000]> / com.chelpus.lackypatch Searching 22 bytes from 0x00000000 to 0x003911b8: 63 6f 6d 2e 63 68 65 6c 70 75 73 2e 6c 61 63 6b 79 70 61 74 63 68 [# ]hits: 190fc0 < 0x003911b8 hits = 1 0x00171d4e hit1_0 "com.chelpus.lackypatch" [0x00000000]> / LuckyPatcher Searching 12 bytes from 0x00000000 to 0x003911b8: 4c 75 63 6b 79 50 61 74 63 68 65 72 [# ]hits: 190fc0 < 0x003911b8 hits = 1 0x00143db6 hit2_0 "LuckyPatcher" [0x00000000]> / 308202f1308201d9a00302010202044 Searching 31 bytes from 0x00000000 to 0x003911b8: 33 30 38 32 30 32 66 31 33 30 38 32 30 31 64 39 61 30 30 33 30 32 30 31 30 32 30 32 30 34 34 [# ]hits: 190fc0 < 0x003911b8 hits = 1 0x00143f92 hit3_0 "308202f1308201d9a00302010202044ec4ab45300d06092a864886f70d01010b05003029310e300c060355040713055061726973311730150603550403130e"
El primer acercamiento nos revela que:
- El string “com.dimonvideo.luckypatcher” puede comenzar en 0x171d66
- El string “com.chelpus.lackypatch” puede comenzar en 0x171d4e
- El string “LuckyPatcher” puede comenzar en 0x143db6
- El string “308202f1308201d9a00302010202044” puede comenzar en 0x143f92
Si nos desplazamos hasta los respectivos offsets, utilizando ‘s ‘ y posteriormente desensamblamos los cuatro primeros opcodes para corroborar que estamos en la posición exacta, utilizando para ello ‘pd 4‘, obtenemos:
[0x00000000]> s 0x171d66 [0x00171d66]> pd 1 ; -------- hit0_0: 0x00171d66 .string "com.dimonvideo.luckypatcher" ; len=27 [0x00171d66]> s 0x171d4e [0x00171d4e]> pd 1 ; -------- hit1_0: 0x00171d4e .string "com.chelpus.lackypatch" ; len=22 [0x00171d4e]> s 0x143db6 [0x00143db6]> pd 1 ; -------- hit2_0: 0x00143db6 .string "LuckyPatcher" ; len=13 [0x00000000]> s 0x143f92 [0x00143f92]> pd 1 ; -------- hit16_0: 0x00143f92 33303832 if-ne v0, v3, 12856
Por si os interesa, este proceso se puede realizar de forma más cómoda y sencilla utilizando una única línea:
[0x00000000]> pd 1 @@= 0x171d66 0x171d4e 0x143db6 0x143f92
En este último caso, los valores están siendo representados como opcodes, pero no debemos guiarnos de ello, esto es debido a que esta cadena en concreto no se encuentra dentro de la sección string_data, por lo que no es reconocida por el fichero de clases y por tanto al ser parseada por r2, no es interpretada correctamente.
Como comentaba anteriormente, las direcciones de memoria que tenemos y que usaremos como referencias, únicamente son aproximaciones, no son el valor exacto, esto lo podéis comprobar si utilizáis el comando ‘/c ‘ para buscar referencias internas dado un opcode, utilizando además el carácter ‘~‘ utilizado como grep internamente por r2.
Sabiendo que la instrucción que buscamos es: ‘const-string v0, …‘ o ‘const-string v1, …‘, el comando que he comentado anteriormente nos quedaría tal que:
[0x00000000]> /c const-string v1~0x171d66
Esto no nos devolverá ningún resultado. La forma de obtener la longitud original de una cadena, y con ella la posición de inicio para conseguir encontrar la referencia al opcode en cuestión es utilizando los siguientes comandos en combinación:
[0x00171d65]> s 0 [0x00000000]> / com.dimonvideo.luckypatcher Searching 27 bytes from 0x00000000 to 0x003911b8: 63 6f 6d 2e 64 69 6d 6f 6e 76 69 64 65 6f 2e 6c 75 63 6b 79 70 61 74 63 68 65 72 [# ]hits: 190fc0 < 0x003911b8 hits = 1 0x00171d66 hit20_0 "com.dimonvideo.luckypatcher" [0x00000000]> s 0x171d66 [0x00171d66]> pd 1 ; -------- hit20_0: 0x00171d66 .string "com.dimonvideo.luckypatcher" ; len=27 [0x00171d66]> f-hit20_0 [0x00171d66]> pd 1 0x00171d66 .string "com.dimonvideo.luckypatcher" ; len=27 [0x00171d66]> s `fd~[0]` [0x00171d65]> pd 1 ; -------- str.com.dimonvideo.luckypatcher: 0x00171d65 .string "com.dimonvideo.luckypatcher" ; len=27
La explicación es bastante sencilla, si nos dirigimos al offset 0x00171d66 y ejecutamos el comando ‘px‘ para que nos muestre un volcado hexadecimal de la posición:
[0x00000000]> px@0x00171d66 -- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 0x00171d66 636f 6d2e 6469 6d6f 6e76 6964 656f 2e6c com.dimonvideo.l
Esa posición hace referencia al primer carácter que integra la cadena, por lo que no podemos utilizarlo, si volvemos a ejecutar el mismo comando, restándole uno, observaremos dos nuevos bytes aparecer, ’1b’:
[0x00000000]> px@0x00171d66-1 -- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 0x00171d65 1b63 6f6d 2e64 696d 6f6e 7669 6465 6f2e .com.dimonvideo.
El valor hexadecimal ‘1b‘ corresponde al decimal ‘27‘, que a su vez es la longitud del string con el que estamos trabajando. Su posición a la que hacer referencia indica que es 0x00171d65. Si volvemos a repetir los pasos anteriores para buscar los opcodes:
[0x00000000]> /c const-string v1~0x171d65 f hit_3030 @ 0x000fddfe # 4: const-string v1, 0x171d65 [0x00000000]> s 0xfddfe [0x000fddfe]> pd 4 0x000fddfe 1a01701d const-string v1, str.com.dimonvideo.luckypatcher
Repitiendo todo este proceso para las restantes cadenas, podemos confirmar que los offsets de origen a los que dirigirnos para buscar referencias son:
- Offset 0x171d65 para el string “com.dimonvideo.luckypatcher“.
- Offset 0x171d4d para el string “com.chelpus.lackypatch“.
- Offset 0x143db4 para el string “/LuckyPatcher“.
- Offset 0x143f90 para el string “308202f1308201d9a00302010202044…”.
Una vez terminada la búsqueda inicial, podemos proceder a realizar la modificación de las tres primeras strings, teniendo en consideración que no sólo debemos de modificar estas, sino también su longitud. Para ello podemos hacer uso del comando ‘ws pstring‘ que cómo bien indica la ayuda de r2, se encarga de escribir un byte para el tamaño y luego la cadena especificada:
[0x000fde7a]> s 0 [0x00000000]> ws aaa@str.LuckyPatcher [0x00000000]> ws aaa@str.com.dimonvideo.luckypatcher [0x00000000]> ws aaa@str.com.chelpus.lackypatch
Si reiniciamos r2 para que los cambios surtan efecto, y volvemos a realizar una búsqueda en cualquiera de los offsets anteriores, veremos que los cambios aplicados ahora son visibles:
[0x00027284]> s 0xfde7a
[0x000fde7a]> pd 4
0x000fde7a 1a008304 const-string v0, 0x143db4
0x000fde7e 711008040000 invoke-static {v0}, 0x292c4
0x000fde84 0c00 move-result-object v0
0x000fde86 6e105d280000 invoke-virtual {v0}, 0x3b56c
[0x000fde7a]> s 0x143db4
[0x00143db4]> pd 4
0x00143db4 .string "aaa" ; len=3
El siguiente objetivo para terminar de aplicar el parche, consiste en modificar aquellas instrucciones condicionales encargadas de comprobar que no se ha realizado ningún tipo de modificación sobre la aplicación original. Probablemente este punto sea posible realizarlo de múltiples maneras, pero expondré el que yo he estimado más efectivo y rápido.
Sabemos que cada vez que se va a comprobar si la aplicación ha sufrido modificación alguna, se define la cadena “308202f1308201d9a00302010202044…” y posteriormente aparece un condiciona ‘if-eqz v0‘. Uniendo todo lo que llevamos aprendido desde el comienzo de la investigación, podemos obtener un patrón aplicando el siguiente comando:
[0x00000000]> /c const-string v~0x143f90 f hit_7335 @ 0x000c2fe4 # 4: const-string v0, 0x143f90 f hit_13563 @ 0x000fdeca # 4: const-string v1, 0x143f90 f hit_13573 @ 0x000fdff6 # 4: const-string v1, 0x143f90
Luego, sabemos que debemos de desplazarnos a un offset cercano a 0x00c2fe4, 0x000fdeca, 0x000fdff6, vamos a ello:
[0x00000000]> s 0xc2fe4
[0x000c2fe4]> pd
0x000c2fe4 1a00ae04 const-string v0, 0x143f90
0x000c2fe8 5421300d iget-object v1, v2, 0x1eff4
0x000c2fec 7110d5220100 invoke-static {v1}, 0x3892c
0x000c2ff2 0c01 move-result-object v1
0x000c2ff4 711049350100 invoke-static {v1}, 0x41ccc
0x000c2ffa 0c01 move-result-object v1
0x000c2ffc 6e203e291000 invoke-virtual {v0, v1}, 0x3bc74
0x000c3002 0a00 move-result v0
,=> 0x000c3004 38000e00 if-eqz v0, 14
[0x000c2fe4]> s 0xfdeca
[0x000fdeca]> pd
0x000fdeca 1a01ae04 const-string v1, 0x143f90
0x000fdece 6e203e290100 invoke-virtual {v1, v0}, 0x3bc74
0x000fded4 0a00 move-result v0
,=> 0x000fded6 38000a00 if-eqz v0, 10
[0x000fdeca]> s 0xfdff6
[0x000fdff6]> pd
0x000fdff6 1a01ae04 const-string v1, 0x143f90
0x000fdffa 6e203e290100 invoke-virtual {v1, v0}, 0x3bc74
0x000fe000 0a00 move-result v0
,=> 0x000fe002 38000a00 if-eqz v0, 10
- La comprobación que se realiza en el fichero iv.smali se encuentra en el offset 0xc3004.
- La primera comprobación realizada en el fichero rl.smali se encuentra en el offset 0xfded6.
- La segunda comprobación realizada en el fichero rl.smali se encuentra en el offset 0xfe002
Cada instrucción de branch son un total de 8 bytes, y como estamos trabajando a nivel de binario, comentar la línea utilizando el carácter ‘#‘ no es válido. Deberemos de reemplazar el opcode por 2 instrucciones nop, dado que cada un nop son 4 bytes:
[0x00027284]> s 0xfe002
[0x000fe002]> pd 4
,=> 0x000fe002 38000a00 if-eqz v0, 10
| 0x000fe006 5420d112 iget-object v0, v2, 0x211ba
| 0x000fe00a 5421d012 iget-object v1, v2, 0x211b4
| 0x000fe00e 6e20f3351000 invoke-virtual {v0, v1}, 0x4221c
[0x000fe002]> wx 00000000
[0x000fe002]> pd 5
0x000fe002 0000 nop
0x000fe004 0000 nop
0x000fe006 5420d112 iget-object v0, v2, 0x211ba
0x000fe00a 5421d012 iget-object v1, v2, 0x211b4
0x000fe00e 6e20f3351000 invoke-virtual {v0, v1}, 0x4221c
Este proceso debemos de realizarlo para los otros dos valores:
[0x000fe002]> s 0xc3004 [0x000c3004]> wx 00000000 [0x000c3004]> s 0xfded6 [0x000fded6]> wx 00000000
Con esto, podemos dar por finalizado nuestro objetivo, hemos conseguido repetir el mismo proceso detallado en el apartado anterior, utilizando en esta ocasión r2.
Modificando la cabecera del classes.dex
Cuando tratamos el fichero de clases como un binario sobre el que realizar modificaciones, a diferencia del primer método mostrado, los nuevos valores para las firmas del checksum y el SHA-1 no son calculadas, siendo necesario realizar el proceso por nuestra cuenta.
Lo que nos interesa explicar aquí es que todo fichero .dex, presenta una cabecera con unos determinados campos que aportan información detallada sobre el fichero en cuestión, dentro de esos campos, existen dos que son especialmente importantes:
- El campo SHA-1, que lee todo el fichero a nivel de bytes, descartando los 32 primeros y calculando el valor con los restantes.
- El campo checksum, que lee también todo el fichero, descarta los primeros 12 bytes y calculando el valor con los restantes, teniendo en consideración el nuevo valor de SHA-1.
En el ejemplo que se nos presenta, podéis comparar los valores antes y después de realizar las modificaciones:
Original
[] DEX filename: …/classes_original.dex [] DEX magic: 64 65 78 0A 30 33 35 00 [] DEX version: 035 [] DEX checksum: 0x4121A402 [] SHA1 signature: d9e6563988d602323214d16b228f4f8ee4ea94d8 [] File size: 1870044 …
Modificado
[] DEX filename: …/classes_mod.dex [] DEX magic: 64 65 78 0A 30 33 35 00 [] DEX version: 035 [] DEX checksum:0xF386A338 [] SHA1 signature: 365272fe850d8b07a2b3630c68a9c093d451fcf1 [] File size: 1870044 bytes …
De cara a obtener un ejecutable funcional, el último paso necesario es como podréis imaginados modificar esos valores dentro de la cabecera dex, para ello podéis hacer uso de las siguientes funciones que programé basándome en el trabajo previo de Tim Strazzere:
void computeDexSHA1signature(uint32_t dex_size, char *dexFileBytes) {
int i;
SHA_CTX context;
unsigned char md[SHA_DIGEST_LENGTH];
char buf[SHA_DIGEST_LENGTH*2];
memset(md, 0x0, SHA_DIGEST_LENGTH);
memset(buf, 0x0, SHA_DIGEST_LENGTH*2);
SHA1(dexFileBytes+32, dex_size-32, md);
for(i=0; i<SHA_DIGEST_LENGTH; i++)
sprintf((char*)&(buf[i*2]), "%02x", md[i]);
printf("SHA1 Signature: %s\n", buf);
}
void computeDexChecksum(uint32_t dex_size, char *dexFileBytes) {
uLong asum = 0;
asum = adler32(1L, dexFileBytes+12, dex_size-12);
printf("CRC Checksum: 0x%X\n\n", asum);
}
Si lo preferís, también es posible hacerlo utilizando rahash2:
$ rahash2 -f 0x20 -a sha1 classes.dex classes.dex: 0x00000020-0x001c88dc sha1: 365272fe850d8b07a2b3630c68a9c093d451fcf1 $ rahash2 -f 0xC -a adler32 classes.dex classes.dex: 0x0000000c-0x001c88dc adler32: f386a338
Puede ser que los valores devueltos por estas instrucciones no sean los mismos, no es sinónimo de que esté mal, tal vez las cadenas que habéis reemplazado no sean las mismas, o posean una longitud inferior.
Sabemos que la estructura de la cabecera de un fichero .dex presenta la siguiente estructura:
Offset Size Description 0x0 8 'Magic' value: "dex\n009\0" 0x8 4 Checksum 0xC 20 SHA-1 Signature 0x20 4 Length of file in bytes 0x24 4 Length of header in bytes (currently always 0x5C) 0x28 8 Padding (reserved for future use?) 0x30 4 Number of strings in the string table 0x34 4 Absolute offset of the string table 0x38 4 Not sure. String related 0x3C 4 Number of classes in the class list 0x40 4 Absolute offset of the class list 0x44 4 Number of fields in the field table 0x48 4 Absolute offset of the field table 0x4C 4 Number of methods in the method table 0x50 4 Absolute offset of the method table 0x54 4 Number of class definitions in the class definition table 0x58 4 Absolute offset of the class definition table
Donde podemos extraer que:
- Los offsets de inicio y fin para el SHA-1 son respectivamente 0x00000000C-0×00000020
- Los offsets de inicio y fin para el Checksum son respectivamente 0×000000008-0x0000000C
Si queremos modificar los valores existentes por los nuevos calculamos podemos ejecutar lo siguiente
[0x00000000]> s 0xC [0x0000000C]> wx 365272fe850d8b07a2b3630c68a9c093d451fcf1 [0x00000000]> s 0x8 [0x0000000C]> wx f386a338
Finalizando así el parcheo con r2. Ahora bastará con que reemplacéis el fichero de clases antiguo por el nuevo, generéis el APK nuevamente y lo firméis. Ya tendréis la aplicación lista para ser instalada y utilizada evadiendo las protecciones anti-tampering y el control para Lucky Patcher.

























