Guía sobre la aplicación inversa cliente-servidor en el ejemplo del trabajo NeoQUEST-2020


Hoy tenemos un programa rico (¡habría tantas áreas de ciberseguridad a la vez!): Considere descompilar una aplicación de Android, interceptar el tráfico para obtener URL, reconstruir apk sin código fuente, trabajar con criptoanalistas y mucho más :)

Según la leyenda NeoQUEST-2020 , el héroe encontró viejas piezas de robot que deben usarse para obtener la llave. ¡Vamos a ponerlo en marcha!

1. Reversim apk


Entonces, ante nosotros hay un poco que logramos extraer de un robot semi-desmontado, una aplicación apk que de alguna manera debería ayudarnos a obtener la clave. Hagamos lo más obvio: ejecute apk y observe su funcionalidad. La interfaz de aplicación más que minimalista no deja dudas: este es un cliente de archivos FileDroid personalizado que le permite descargar un archivo desde un servidor remoto. De acuerdo, parece fácil. Conectamos el teléfono a Internet, hacemos un intento de descarga (inmediatamente key.txt - bueno, ¿y si?) - sin éxito, falta el archivo en el servidor.



Pasamos al siguiente evento en términos de complejidad: descompilamos apk usando JADXy analizar el código fuente de la aplicación, que, afortunadamente, no está en absoluto ofuscado. Nuestra tarea actual es comprender qué archivos ofrece el servidor remoto para descargar y seleccionar el que tenga la clave.

Comenzamos con la clase com.ctf.filedroid.MainActivity, que contiene el método onClick (), que es el más interesante para nosotros, en el que se procesa el clic en el botón "Descargar". Dentro de este método, la clase ConnectionHandler se llama dos veces: primero, se llama al método ConnectionHandler.getToken (), y solo luego, ConnectionHandler.getEncryptedFile (), que pasa el nombre del archivo solicitado por el usuario.



Sí, es decir, ¡primero necesitamos una ficha! Examinaremos un poco más el proceso de obtención.
El método ConnectionHandler.getToken () toma dos líneas de entrada y luego envía una solicitud GET, pasando estas líneas como los parámetros "crc" y "sign". En respuesta, el servidor envía los datos en formato JSON, desde el cual nuestra aplicación extrae el token de acceso y lo utiliza para descargar el archivo. Esto es todo, por supuesto, bueno, pero ¿qué son "crc" y "sign"?



Para comprender esto, nos movemos más hacia la clase Checks, proporcionando amablemente los métodos badHash () y badSign (). El primero calcula la suma de comprobación de classes.dex y resources.arsc, concatena estos dos valores y los envuelve en Base64 (preste atención a la bandera 10 = NO_WRAP | URL_SAFE, será útil). ¿Y qué hay del segundo método? Y hace lo mismo con la huella digital SHA-256 de la firma de la aplicación. Eh, parece que FileDroid no está realmente ansioso por ser reconstruido :(



Ok, digamos que recibimos el token. ¿Que sigue? Lo pasamos a la entrada del método ConnectionHandler.getEncryptedFile (), que agrega el nombre del archivo solicitado al token y genera otra solicitud GET, esta vez con los parámetros "token" y "archivo". El servidor en respuesta (a juzgar por el nombre del método) envía un archivo cifrado, que se almacena en / sdcard /.

Entonces, para resumir un pequeño subtotal: tenemos dos noticias, y ... ambas son malas. En primer lugar, FileDroid no es compatible con nuestro entusiasmo por modificar apk (se comprueban la suma de comprobación y la firma), y en segundo lugar, el archivo recibido del servidor promete cifrarse.

Bien, resolveremos los problemas a medida que estén disponibles, y ahora nuestro principal problema es que todavía no sabemos qué archivo necesitamos descargar. Sin embargo, al estudiar la clase ConnectionHandler, no pudimos evitar notar que justo entre los métodos getToken () y getEncryptedFile (), los desarrolladores de FileDroid olvidaron otro método muy seductor llamado nombre getListing (). Entonces, el servidor admite dicha funcionalidad ... ¡Parece que esto es lo que necesita!



Para obtener el listado, necesitaremos los conocidos "crc" y "sign"; no hay problema, ya sabemos de dónde vienen. Leemos los valores, enviamos una solicitud GET y ... Entonces, deténgase. ¿A dónde vamos a enviar la solicitud GET? Sería bueno obtener primero la URL del servidor remoto. Eh, volvemos a MainActivity.onClick () y vemos cómo se generan los argumentos netPath para llamar a los métodos getToken () y getEncryptedFile ():

Method getSecureMethod = 
wat.class.getDeclaredMethod("getSecure", new Class[]{String.class});

// . . .

// netPath --> ConnectionHandler.getToken()
(String) getSecureMethod.invoke((Object) null, new Object[]{"fnks"})

// netPath --> ConnectionHandler. getEncryptedFile()
(String) getSecureMethod.invoke((Object) null, new Object[]{"qdkm"})

Las combinaciones de letras extrañas “fnks” y “qdmk” nos obligan a recurrir al resultado de la descompilación del método wat.getSecure (). Spoiler: JADX tiene este resultado más o menos.



Tras una inspección más cercana, queda claro que todo este contenido no muy agradable del método se puede reemplazar con el caso de interruptor habitual de este tipo:

// . . .
switch(CODE)
{
    case «qdkm»: 
        r.2 = com.ctf.filedroid.x37AtsW8g.rlieh786d(2);
        break;
    case «tkog»: 
        r2 = com.ctf.filedroid.x37AtsW8g.rlieh786d(1);
        break;
    case «fnks»: 
        String r2 = com.ctf.filedroid.x37AtsW8g.rlieh786d(0);
	break;
}
java.lang.StringBuilder r1 = new java.lang.StringBuilder 
r1.<init>(r2) 
java.lang.String r0 = r1.toString() 
java.lang.String r1 = radon(r0)
return r1 

Dado que "fnks" y "qdmk" ya se utilizan para obtener un token y descargar un archivo, "tkog" debe proporcionar la URL necesaria para solicitar una lista de archivos disponibles en el servidor. Parece que existe la esperanza de obtener la ruta requerida a bajo costo ... En primer lugar, veamos cómo se almacenan las URL en la aplicación. Abrimos la función com.ctf.filedroid.x37AtsW8g.rlieh786d () y vemos que cada URL se guarda como una matriz de bytes codificada, y la función misma forma una cadena a partir de estos bytes y la devuelve.



Bueno. Pero luego la línea se pasa a la función com.ctf.filedroid.wat.radon (), cuya implementación se envía a la biblioteca nativa libae3d8oe1.so. Reverse arm64? Buen intento, FileDroid, pero vamos en otro momento?

2. Obtenga las URL del servidor


Intentemos acercarnos desde el otro lado: para interceptar el tráfico, obtener URL en texto claro (y como beneficio adicional, ¡también valores de suma de verificación y firma!), Hacer coincidirlos para formar conjuntos de bytes de com.ctf.filedroid.x37AtsW8g.rlieh786d () - puede ¿El cifrado resulta ser el cifrado habitual de César o XOR? .. Entonces no será difícil restaurar la tercera URL y realizar el listado.

Para redirigir el tráfico, puede usar cualquier proxy conveniente ( Charles , fiddler , BURPetc.) Configuramos el reenvío en un dispositivo móvil, instalamos el certificado apropiado, verificamos que la intercepción sea exitosa y ejecutamos FileFroid. Estamos tratando de descargar un archivo arbitrario y ... ver "NetworkError". Este error fue causado por la presencia de fijación de certificados (consulte el método com.ctf.filedroid.ConnectionHandler.sendRequest): el cliente del archivo verifica que el certificado "conectado" en la aplicación corresponde al servidor con el que interactúa. ¡Ahora está claro por qué se controla la integridad de los recursos de la aplicación!



Sin embargo, en el tráfico interceptado, podemos ver al menos el nombre de dominio del servidor al que accede el cliente de archivos, lo que significa que la esperanza de descifrar las URL permanece.



Volvamos a la función com.ctf.filedroid.x37AtsW8g.rlieh786d () y tenga en cuenta que las primeras decenas de bytes coinciden en todas las matrices:

cArr[0] = new char[]{'K', 'S', 'Y', '5', 'E', 'R', 'Q', 'J', 'S', '0', 't', 'W', 'B', '2', 'w', 'k', 'N', 'j', '8', 'O', 'D', 'l', 'd', 'K', 'C', 'l', 'U', 'B', 'c', 'T', 'Q', '3', 'P', 'h', 'V', 'J', 'Q', 'R', 'F', 'L', 'U', 'R', '5', 'p', 'b', 'i', . . .};

cArr[1] = new char[]{'K', 'S', 'Y', '5', 'E', 'R', 'Q', 'J', 'S', '0', 't', 'W', 'B', '2', 'w', 'k', 'N', 'j', '8', 'O', 'D', 'l', 'd', 'K', 'C', 'l', 'U', 'B', 'c', 'T', 'Q', '3', 'P', 'h', 'V', 'J', 'Q', 'R', 'F', 'L', 'U', 'R', '5', 'p', 'b', 'j', . . .};

cArr[2] = new char[]{'K', 'S', 'Y', '5', 'E', 'R', 'Q', 'J', 'S', '0', 't', 'W', 'B', '2', 'w', 'k', 'N', 'j', '8', 'O', 'D', 'l', 'd', 'K', 'C', 'l', 'U', 'B', 'c', 'T', 'Q', '3', 'P', 'h', 'V', 'J', 'Q', 'R', 'F', 'L', 'U', 'R', '5', 'p', 'b', 'j', . . ., '='};

Además, el último byte de la tercera matriz sugiere que no era sin base64. Intentemos decodificar y meter los bytes resultantes con una parte conocida de la URL:



¡Parece que nadie ha estado tan feliz con ARMag3dd0n! El punto es pequeño: decodifica secuencialmente URL base64 y xorim con la clave encontrada. Pero ... ¿y si no fuera XOR, sino un cifrado de permutación hecho a sí mismo, que no puedes detectar incluso con cien intentos?

3. Reconstruir apk con Frida


Como parte de este artículo, consideraremos un método de solución más sencillo (y, en nuestra opinión, más bello): usar el marco Frida , que nos permitirá ejecutar métodos de aplicación apk arbitrarios con los argumentos que necesitamos en tiempo de ejecución. Para hacer esto, necesita un teléfono con derechos de root o un emulador. Asumimos el siguiente plan de acción:

  1. Instalación de componentes Frida en una PC y un teléfono de prueba.
  2. Recupere URL que coincidan con el token o las solicitudes de listado y descargue el archivo (usando Frida).
  3. Recuperando los valores de suma de comprobación y firma de la aplicación original.
  4. Obtener una lista de archivos almacenados en el servidor e identificar el archivo deseado.
  5. Descargue y descifre el archivo.

Primero, aclararemos la relación entre el teléfono rooteado y el apk. Instalamos la aplicación, la ejecutamos, pero el cliente de archivos no quiere arrancar completamente, solo parpadea y se cierra. Verificamos los mensajes a través de logcat: sí, lo es, FileDroid ya siente que algo anda mal y se resiste como puede.



Volvemos a la clase MainActivity y encontramos que el método doChecks () se llama en onCreate (), y muestra los siguientes errores en el registro:



Además, onResume () también verifica si el puerto típico de Frida está abierto:



nuestro cliente de archivos es un poco intolerante a depuración, root y Frida misma. Tal oposición no está incluida en nuestros planes, por lo que obtenemos el código pequeño de la aplicación utilizando la utilidad apktool, abra el archivo MainActivity.smali en cualquier editor de texto, encuentre el método onCreate () y convierta la llamada doChecks () en un comentario inofensivo:



Luego privamos al método suicide () de la oportunidad de cerrar realmente la aplicación:



Luego, construyamos nuestra aplicación ligeramente mejorada nuevamente usando apktool y firme ejecutando los siguientes comandos (puede que necesite derechos de administrador):

cd "C:\Program Files\Java\jdk-14\bin"
.\keytool -genkey -v -keystore filedroid.keystore -alias filedroid_alias -keyalg RSA -keysize 2048 -validity 10000
.\jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore  filedroid.keystore filedroid_patched.apk filedroid_alias
.\jarsigner -verify -verbose -certs filedroid_patched.apk

Reinstalamos la aplicación en el teléfono, la ejecutamos: ¡hurra, la descarga se realiza sin incidentes, el registro está limpio!



Procedemos a instalar el framework Frida en una PC y dispositivo móvil:

$ sudo pip3 install frida-tools
$ wget https://github.com/frida/frida/releases/download/$(frida --version)/frida-server-$(frida --version)-android-arm.xz
$ unxz frida-server-$(frida --version)-android-arm.xz
$ adb push frida-server-$(frida --version)-android-arm /data/local/tmp/frida-server

Inicie el servidor de framework Frida en un dispositivo móvil:

$ adb shell su - "chmod 755 /data/local/tmp/frida-server"
$ adb shell su - "/data/local/tmp/frida-server &"  

Estamos preparando un script get-urls.js simple que llamará a wat.getSecure () para todos los servidores de solicitudes compatibles:

Java.perform(function () 
{
     const wat = Java.use('com.ctf.filedroid.wat');
	console.log(wat.getSecure("fnks"));
	console.log(wat.getSecure("qdmk"));
	console.log(wat.getSecure("tkog"));
});

Lanzamos FileDroid en el dispositivo móvil y nos "aferramos" con nuestro script al proceso correspondiente:



4. Obtenga la lista de archivos en el servidor


¡Finalmente, el servidor remoto se ha acercado un poco más a nosotros! Ahora sabemos que el servidor admite solicitudes de las siguientes maneras:

  1. archiveroid.neoquest.ru/api/verifyme?crc= {crc} & sign = {sign}
  2. archiveroid.neoquest.ru/api/list_post_apocalyptic_collection?crc= {crc} & sign = {sign}
  3. archiveroid.neoquest.ru/api/file?file= {file} & token = {token}

Para obtener una lista de los archivos disponibles, queda calcular los valores de suma de comprobación y firma de la aplicación original, y luego codificarlos en base64.

Tal script en python3 le permitirá hacer esto:

Revelación
import hashlib
import binascii
import base64
from asn1crypto import cms, x509
from zipfile import ZipFile


def get_info(apk):
    with ZipFile(apk, 'r') as zipObj:
        classes = zipObj.read("classes.dex")
        resources = zipObj.read("resources.arsc")
        cert = zipObj.read("META-INF/CERT.RSA")
        crc = "%s%s" % (get_crc(classes), get_crc(resources))
        return get_full_crc(classes, resources).decode("utf-8"), get_sign(cert).decode("utf-8")


def get_crc(file):
    crc = binascii.crc32(file) & 0xffffffff
    return crc


def get_full_crc(classes, resources):
    crc = "%s%s" % (get_crc(classes), get_crc(resources))
    return base64.urlsafe_b64encode(bytes(crc, "utf-8"))


def get_sign(file):
    pkcs7 = cms.ContentInfo.load(file)
    data = pkcs7['content']['certificates'][0].chosen.dump()
    sha256 = hashlib.sha256()
    sha256.update(data)
    return base64.urlsafe_b64encode(sha256.digest())  

get_info('filedroid.apk')


También puedes hacerlo manualmente. Consideramos que CRC32 de classes.dex y resources.arsc es una herramienta conveniente (por ejemplo, para Linux, la utilidad crc32 estándar), obtenemos los valores 1276945813 y 2814166583 respectivamente, los concatenamos (saldrán 12769458132814166583) y codificamos en base64, por ejemplo, aquí :



Para realizar un procedimiento similar para firmar la aplicación, en la ventana JADX, vaya a la sección "Firma APK", copie el valor "Huella digital SHA-256" y codifíquelo en base64 como una matriz de bytes:



Importante:en el apk original, la codificación base64 se realiza con el indicador URL_SAFE, es decir en lugar de los caracteres "+" y "/", "-" y "_" se utilizan, respectivamente. Es necesario asegurarse de que esto también se observará con la autocodificación. Para hacer esto, al codificar en línea, puede reemplazar el alfabeto usado con "ABCDEFGHIJKLMNOPQRSTUVWXYZabcde fghijklmnopqrstuvwxyz0123456789 + /" con "ABCDEFGHIJKLMNOPQRSTUVWXYZabc script64 usando 64 script.jpc.jpc.jpc.

Finalmente, tenemos todos los ingredientes para listar archivos con éxito:

  1. archiveroid.neoquest.ru/api/list_post_apocalyptic_collection?crc= {crc} & sign = {sign}
  2. crc: MTI3Njk0NTgxMzI4MTQxNjY1ODM =
  3. signo: HeiTSPWdCuhpbmVxqLxW-uhrozfG_QWpTv9ygn45eHY =

Ejecutamos la solicitud GET - y ¡salud, nuestra lista! Además, el nombre de uno de los archivos habla por sí mismo: "abrir si quieres escapar", parece que lo necesitamos.



A continuación, solicitamos un token de acceso único y descargamos el archivo:

import requests

response = requests.get('https://filedroid.neoquest.ru/api/verifyme', 
    		params={    'crc': 'MTI3Njk0NTgxMzI4MTQxNjY1ODM=', 
'sign': HeiTSPWdCuhpbmVxqLxW-uhrozfG_QWpTv9ygn45eHY=},
verify=False)
    
token = response.json()['token']
print(token)
response = requests.get('https://filedroid.neoquest.ru/api/file', 
params={'token': token, 'file': '0p3n1fuw4nt2esk4p3.jpg'}, verify=False)

with open("0p3n1fuw4nt2esk4p3.jpg", 'wb') as fd:
        fd.write(response.content)

Abrimos el archivo descargado y recordamos una pequeña circunstancia que dejamos para más adelante:



5. Agregue una pizca de criptografía ...


Eh, demasiado temprano posponemos FileDroid. Volvamos a JADX y veamos si los desarrolladores de clientes de archivos nos han dejado algo útil. Sí, este es el caso cuando la limpieza del código claramente no es popular: el método decryptFile () no utilizado está esperando silenciosamente nuestra atención en la clase ConnectionHandler. ¿Que tenemos?

Encriptación en modo AES el CBC , sinhroposylka ocupa los primeros 16 bytes ... Pereza - el motor del progreso, es mejor usar Frida nuevamente y descifrar nuestro 0p3n1fuw4nt2esk4p3.jpg sin esfuerzo. Pero solo pasa como clave de cifrado? No hay muchas opciones, pero dada la presencia de otro método "olvidado" savePlainFile (String file, String token), la elección es obvia.

Prepare el siguiente script decrypt.js (como token indique el valor real, por ejemplo, 'HoHknc572mVpZESSQN1Xa7S9zOidxX1PMbykdoM1EXI ='):

Java.perform(function () {
    const JavaString = Java.use('java.lang.String');
    const file_name = JavaString.$new('0p3n1fuw4nt2esk4p3.jpg');
    const ConnectionHandler = Java.use('com.ctf.filedroid.ConnectionHandler');
    const result = ConnectionHandler.savePlainFile(file_name, <token>);
    console.log(result);
});

Ponemos el archivo cifrado 0p3n1fuw4nt2esk4p3.jpg en / sdcard /, ejecutamos FileDroid e inyectamos el script decrypt.js usando Frida. Después de que se ejecute el script, el archivo plainfile.jpg aparecerá en / sdcard /. Lo abrimos y ... ¡solo resuelto!



Esta difícil tarea requería que los participantes tuvieran conocimientos y habilidades en varias áreas de seguridad de la información a la vez, ¡y nos complace que la mayoría de los competidores lo hayan logrado!

Esperamos que aquellos que no tuvieron suficiente tiempo o conocimiento antes de recibir la clave ahora pasen con éxito tareas similares en cualquier CTF :)

All Articles