AndroidAuditoría y CTFExplotaciónPentesting

NN8ed Write-up – UpdateMe

 

Buenos días!

En el día de hoy, traemos a un nuevo colaborador. Su nombre es Cristian Barrientos y trabaja como IT Security Analyst en Wise Security y en esta ocasión nos presentará el write-up de uno de los retos propuestos por los chicos de Ka0labs en el CTF del Congreso de Navaja Negra.

¡Esperamos que os guste su entrada!

CTF: Navaja Negra 8 Edición
Categoría:
Móviles
Reto: UpdateMe

 

Introducción

En primer lugar, gracias a los miembros del blog fwhibbit por darme la oportunidad de publicar esta entrada en su blog. Como ya han comentado, hoy os traigo el write-up de uno de los retos que nos encontramos el pasado fin de semana en el congreso de Navaja Negra.

La verdad que este reto no fue resuelto por muchos participantes, y he leído varios métodos de resolver el reto en este CTF creado por @ka0labs_ a quienes aprovecho para agradecer su esfuerzo en hacer estos CTFs tan interesantes y sobretodo didácticos.

A mi me gustaría aportar un método alternativo (que a priori no estaba contemplado) y que probablemente pueda serviros de algo a más de uno en futuros retos.

En primer lugar, como no, debemos obtener la APK para tener un punto de partida, instalarla en un dispositivo Android y analizar su comportamiento, tanto a nivel gráfico como revisando los logs que se generan al usar la aplicación.

 

Ejecución de la APP y monitorización de Logs generados

Al ejecutar la app nos encontramos con el siguiente mensaje:


No sé a vosotros pero a mi me da la sensación de que la APP detecta que el dispositivo está rooteado y que se está copiando la flag en el dispositivo…

En este punto será interesante analizar los Logs (para ello utilizaré pidcat):

 

Según los Logs parece que tiene controles anti-debug (a priori no me importa), controles anti-frida (tenemos dos opciones, evadir los controles anti-frida o usar otro método), otros controles entre los que entiendo que están los controles anti-root de si el dispositivo está rooteado, tenemos un “nn8ed.anotherthing.dex” en la ruta que indica el log (algo a tener en cuenta de cara a futuro) y por último, que el flag no se ha copiado.

Con esta información llegamos a una primera posible conclusión, la APP detecta que se está ejecutando en un dispositivo rooteado y por lo tanto decide no acabar de copiar el flag.

La solución a priori parece muy sencilla, deberemos evadir la detección de root y analizar su comportamiento.

 

Análisis Estático

Yo personalmente prefiero trabajar con el código en smali en lugar de con el código java, simplemente porque al obtener el código java hay partes de código que se pierden y otras que no se traducen correctamente al pasar de un .dex a un .jar.

Así que como yo lo he hecho sobre el smali y el write-up lo escribo yo, lo explicaré desde el código smali :D.

Decompilamos la aplicación con APKTool y obtenemos el código smali.

Una vez tenemos la carpeta smali, tenemos acceso al código de la APP.

¿Qué hay dentro de la carpeta smali?

Estas son las clases que podemos encontrar (sin desglosar las carpetas relacionadas con “android” ya que no parece que contengan nada relacionado con la detección de root).

 

Así que tras analizar el resto de clases podemos llegar a la conclusión de que las únicas clases que tienen indicios de trabajar con detecciones de root son:

  • Todas las clases que hay dentro de la carpeta “rootbeer”.
  • La clase b.smali de la carpeta “updateme”.


¿En qué debemos fijarnos de estas clases?

com/scottyab/rootbeer/a.smali

Si miramos la clase a.smali de la carpeta “rootbeer” encontramos el constructor que podemos ver a continuación:

# direct methods

.method static constructor <clinit>()V
    .locals 15
    const-string v0, "com.noshufou.android.su"
    const-string v1, "com.noshufou.android.su.elite"
    const-string v2, "eu.chainfire.supersu"
    const-string v3, "com.koushikdutta.superuser"
    const-string v4, "com.thirdparty.superuser"
    const-string v5, "com.yellowes.su"
    const-string v6, "com.topjohnwu.magisk"
    filled-new-array/range {v0 .. v6}, [Ljava/lang/String;
    move-result-object v0
    sput-object v0, Lcom/scottyab/rootbeer/a;->a:[Ljava/lang/String;
    const-string v1, "com.koushikdutta.rommanager"
    const-string v2, "com.koushikdutta.rommanager.license"
    const-string v3, "com.dimonvideo.luckypatcher"
    const-string v4, "com.chelpus.lackypatch"
    const-string v5, "com.ramdroid.appquarantine"
    const-string v6, "com.ramdroid.appquarantinepro"
    const-string v7, "com.android.vending.billing.InAppBillingService.COIN"
    const-string v8, "com.chelpus.luckypatcher"
    filled-new-array/range {v1 .. v8}, [Ljava/lang/String;
    move-result-object v0
    sput-object v0, Lcom/scottyab/rootbeer/a;->b:[Ljava/lang/String;
    const-string v1, "com.devadvance.rootcloak"
    const-string v2, "com.devadvance.rootcloakplus"
    const-string v3, "de.robv.android.xposed.installer"
    const-string v4, "com.saurik.substrate"
    const-string v5, "com.zachspong.temprootremovejb"
    const-string v6, "com.amphoras.hidemyroot"
    const-string v7, "com.amphoras.hidemyrootadfree"
    const-string v8, "com.formyhm.hiderootPremium"
    const-string v9, "com.formyhm.hideroot"
    filled-new-array/range {v1 .. v9}, [Ljava/lang/String;
    move-result-object v0
    sput-object v0, Lcom/scottyab/rootbeer/a;->c:[Ljava/lang/String;
    const-string v1, "/data/local/"
    const-string v2, "/data/local/bin/"
    const-string v3, "/data/local/xbin/"
    const-string v4, "/sbin/"
    const-string v5, "/su/bin/"
    const-string v6, "/system/bin/"
    const-string v7, "/system/bin/.ext/"
    const-string v8, "/system/bin/failsafe/"
    const-string v9, "/system/sd/xbin/"
    const-string v10, "/system/usr/we-need-root/"
    const-string v11, "/system/xbin/"
    const-string v12, "/cache"
    const-string v13, "/data"
    const-string v14, "/dev"
    filled-new-array/range {v1 .. v14}, [Ljava/lang/String;
    move-result-object v0
    sput-object v0, Lcom/scottyab/rootbeer/a;->d:[Ljava/lang/String;
    const-string v1, "/system"
    const-string v2, "/system/bin"
    const-string v3, "/system/sbin"
    const-string v4, "/system/xbin"
    const-string v5, "/vendor/bin"
    const-string v6, "/sbin"
    const-string v7, "/etc"
    filled-new-array/range {v1 .. v7}, [Ljava/lang/String;
    move-result-object v0
    sput-object v0, Lcom/scottyab/rootbeer/a;->e:[Ljava/lang/String;
  return-void
.end method

Lo único que hace este constructor es guardar en varios Strings lo que vemos entrecomillas en cada línea de código, con la intención de comprobar posibles packages de aplicaciones/módulos relacionados con dispositivos rooted por un lado, y por otro rutas del sistema que se suelen crear al “rootear” el dispositivo o que no suelen ser accesibles desde dispositivos “no rooted”.

Por lo tanto todos estos String se utilizarán en alguna otra clase para realizar las comprobaciones pertinentes, así que está clase es importante pero no hay ninguna función que debamos “hookear”.

 

com/scottyab/rootbeer/b.smali

En esta clase encontramos varias funciones, muchas de ellas son diferentes controles que comprueban si el dispositivo está rooted o no.

No voy a entrar en el detalle de todas estas funciones, pero a modo de resumen se encargan de comprobar lo que explicaba en la clase anterior “a.smali”, comprueban que existan binarios tipo “su”, intentan escribir en zonas del sistema que solo un dispositivo rooted te permite escribir, comprueba si existen aplicaciones típicas de dispositivos rooted, etc.

Y entre todas estas funciones encontramos una función “a()Z” que se encarga de llamar a todas las otras, y si alguna de estas devuelve true, la función “a()Z” devolverá true indicando que alguno de los checks ha detectado que el dispositivo está rooted, cosa que nos facilita mucho el trabajo ya que en lugar de tener que hookear todos y cada uno de los checks únicamente hay que hookear el que se encarga de llamarlos a todos.

A continuación se puede ver la función “a()Z” a la que me refiero:

.method public a()Z
    .locals 1
    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->c()Z
    move-result v0
    if-nez v0, :cond_1
    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->d()Z
    move-result v0
    if-nez v0, :cond_1
    const-string v0, "su"
    invoke-virtual {p0, v0}, Lcom/scottyab/rootbeer/b;->a(Ljava/lang/String;)Z
    move-result v0
    if-nez v0, :cond_1
    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->f()Z
    move-result v0
    if-nez v0, :cond_1
    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->g()Z
    move-result v0
    if-nez v0, :cond_1
    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->b()Z
    move-result v0
    if-nez v0, :cond_1
    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->h()Z
    move-result v0
    if-nez v0, :cond_1
    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->j()Z
    move-result v0
    if-nez v0, :cond_1
    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->e()Z
    move-result v0
    if-eqz v0, :cond_0
    goto :goto_0
    :cond_0
    const/4 v0, 0x0
    return v0
    :cond_1
    :goto_0
    const/4 v0, 0x1
    return v0
.end method

Pero claro, antes de acabar la parte de análisis estático, es necesario mirar a ver si hubiera alguna otra clase con funciones que estén comprobando root.

Y eso nos lleva a una última clase que es necesario estudiar en detalle.

nn8ed/updateme/b.smali

De la misma manera que hemos hecho anteriormente en “com/scottyab/rootbeer/b.smali”, vemos que hay varios métodos que una vez más realizan varios checks que permiten a la APP detectar un dispositivo rooted, sin embargo, hay un método “d()Z” que recoge todos los checks y se encarga de devolver true o false en función de lo que devuelven todos los checks que invoca.

Por lo tanto ya tenemos 2 funciones candidatas a estar comprobando detección de root y a priori no vemos ninguna otra.

 

Hooking de funciones con XPosed Framework

Dado que estamos ante un writeup de la resolución de un CTF no me voy a poner a explicar cómo funciona XPosed, pero sí os voy a decir que es muy sencillo empezar a hookear con XPosed (buscas un módulo de XPosed que ya esté creado y lo adaptas a la aplicación que quieras hookear).

Si ya tienes un módulo de XPosed personalizado para hookear cualquier aplicación ya podemos generar los métodos adecuados para que las dos funciones que hemos encontrado devuelvan siempre false independientemente de lo que los checks indiquen.

¿Cuáles serían estos métodos?

Como hemos ido viendo a lo largo del análisis estático únicamente es necesario modificar el valor de retorno de las funciones “a()Z” de la clase “com/scottyab/rootbeer/b.smali” y “d()Z” de la clase “nn8ed/updateme/b.smali”, para que siempre devuelvan false independientemente de lo que fuera a devolver, de manera que si alguno de los checks ha hecho que la función se resuelva como “true” el hook modifica el valor de retorno por false, haciendo creer a la aplicación que los checks no detectan indicios de estar ejecutándose en un dispositivo rooteado.

 

Ejecución y Monitorización de la APP tras habilitar los Hooks

Habilitamos el módulo de XPosed creado y reiniciamos el dispositivo (principal inconveniente de XPosed).

En mi caso el módulo se llamaba: KaoBypasser

Después de reiniciar el dispositivo y abrir la aplicación nos encontramos que ya no aparece la alerta de “Root detected”.

Dado que ya no aparece el mensaje de “Root detected” la aplicación se puede usar con total normalidad, por lo tanto pulsamos en DOIT y vemos que aparece una notificación que dice “The thing is done”.

 

Llegados a este punto toca mirar los logs a ver si vemos alguna diferencia respecto a los logs encontrados cuando la aplicación detectaba root, donde se indicaba que la flag no se había podido copiar.

En primer lugar vemos los mismos logs de antes más los que hemos añadido con XPosed simplemente para ver que realmente se están ejecutando los hooks que hemos creado:

Si continuamos mirando el log, después de generarse algunos errores provocados por el nn8ed.anotherthing.dex, encontramos lo siguiente:

En los logs anteriores se puede ver que, como mínimo, aparecen logs indicando que la flag se ha copiado en la ruta “/data/user/0/nn8ed.updateme/files/flag.txt”.

Por lo tanto vamos a esa ruta a ver qué encontramos:

Finalmente encontramos la ansiada flag, completando así el reto:

nn8ed{y0um45t4lw4ysUpd4t3!}

 

Espero que os haya gustado, y nuevamente agradecer a Ka0Labs por este magnífico reto, y a Follow The White Rabbit por permitirme publicar la resolución en su blog.

Un saludo!

Cristian Barrientos

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Los datos introducidos se guardarán en nuestra base de datos como parte del comentario publicado, como se indica en la política de privacidad.