Investigación de un archivo desconocido

Reubicación Nueva ciudad. Búsqueda de empleo. Incluso para un profesional de TI, esto puede llevar mucho tiempo. Una serie de entrevistas, que, en general, son muy similares entre sí. Y como suele suceder cuando ya ha encontrado un trabajo, después de un tiempo, se anuncia una oficina interesante.

Era difícil entender lo que estaba haciendo específicamente, sin embargo, su área de interés era el estudio del software de otras personas. Suena intrigante, aunque cuando te das cuenta de que este no parece ser un proveedor que lanza software para seguridad cibernética, te detienes por un segundo y comienzas a rascarte los nabos.



En resumen: desecharon el archivo y se ofrecieron a examinarlo como una tarea de prueba e intentar calcular una determinada firma en función de los datos de entrada presentados. Vale la pena señalar que tenía muy poca experiencia en tales actividades y, probablemente, es por eso que en la primera iteración de la solución solo tuve un par de horas, luego la motivación para hacer esto fue nula. Y sí, por supuesto, lo primero que intenté ejecutar en el teléfono / emulador: esta aplicación no es válida.

Lo que tenemos: un archivo con la extensión ".apk" . Puse la tarea en sí misma bajo el spoiler para que no sea indexada por los motores de búsqueda: ¿qué pasa si a los chicos no les gusta, que pongo la solución en Habr?

Tarea misma
El APK contiene funcionalidades para generar firmas para una matriz asociativa.
Intente obtener una firma para el siguiente conjunto de datos:

{ "user" : "LeetD3vM4st3R", "password": "__s33cr$$tV4lu3__", "hash": "34765983265937875692356935636464" } 


Enrollar las mangas


Se dice que el archivo contiene la funcionalidad de firmar una matriz asociativa. Por la extensión del archivo, entendemos de inmediato que estamos tratando con una aplicación escrita para Android. Primero desempaquetamos el archivo. De hecho, este es un archivo ZIP normal, y cualquier archivador lo manejará a la ligera. Utilicé la utilidad apktool y, como resultó, accidentalmente evité un par de rastrillos. Sí, sucede (generalmente lo contrario, ¿sí?). El hechizo es bastante simple:

 apktool d task.zip 

Resulta que el código y los recursos en el archivo apk también se almacenan empaquetados en binarios separados, y se necesitará otro software para extraerlos. apktool extrajo implícitamente bytes de clase, recursos y lo descompuso todo en una jerarquía de archivos natural. Puedes proceder.

 ├── AndroidManifest.xml ├── apktool.yml ├── lib │   └── arm64-v8a ├── original │   ├── AndroidManifest.xml │   └── META-INF ├── res │   ├── anim │   ├── color │   ├── drawable │   ├── layout │   ├── layout-watch-v20 │   ├── mipmap-anydpi-v26 │   ├── values │   └── values-af ├── smali │   ├── android │   ├── butterknife │   ├── com │   ├── net │   └── org └── unknown   └── org 

Vemos una jerarquía similar (dejó su versión simplificada) y estamos tratando de averiguar por dónde empezar. Vale la pena señalar que todavía una vez escribí un par de pequeñas aplicaciones para Android, por lo que la esencia de la parte de los directorios y, en general, los principios de las aplicaciones de Android del dispositivo, estoy bastante claro.

Para empezar, decido simplemente "caminar" a través de los archivos. Abro AndroidManifest.xml y empiezo a leer de manera significativa. Mi atención es atraída por un atributo extraño

 android:supportsRtl="true" 

Resulta que él es responsable de admitir idiomas con la letra "de derecha a izquierda" en la aplicación. Comenzamos a esforzarnos. No bueno

Además, mi mirada se aferra a la carpeta desconocida. Debajo hay una jerarquía de la forma: org.apache.commons.codec.language.bm y una gran cantidad de archivos de texto con contenido oscuro. Busca en Google el nombre completo del paquete y resulta lo que está almacenado aquí, algo relacionado con el algoritmo de búsqueda de palabras fonéticamente similares al dado. Francamente, aquí comencé a esforzarme más. Después de hurgar un poco en los directorios, en realidad encontré el código en sí, y luego comenzó la diversión. No me encontré con el código de bytes de Java habitual, con el que una vez pude jugar, sino con otra cosa. Muy similar, pero diferente.

Al final resultó que, Android tiene su propia máquina virtual: Dalvik. Y, como todas las máquinas virtuales respetadas, tiene su propio código de bytes. Parece que en el primer intento de resolver este problema, fue en esta triste nota que anuncié el intermedio, hice una reverencia, bajé el telón y lo tiré todo durante 4 meses hasta que mi curiosidad me terminó por completo.

Enrollar las mangas [2]


"¿Pero no puede ser para que todo sea más fácil?" - Esta es la pregunta que me hice cuando comencé la tarea por segunda vez. Comencé a buscar en Internet un descompilador desde smali hasta Java. Solo vi que es imposible llevar a cabo este proceso sin ambigüedades. Frunciendo un poco el ceño, fue a Github y metió un par de frases clave en la línea de búsqueda. El primero llegó smali2java .

 git clone gradle build java -jar smali2java.jar .. 

Errores Veo un gran seguimiento de pila y errores en varias páginas del terminal. Después de leer un poco sobre la esencia del contenido (y contener las emociones sobre el tamaño de la traza de la pila), encuentro que esta herramienta funciona sobre la base de una cierta gramática descrita y el código de bytes que conoció claramente no corresponde a él. Abro el pequeño bytecode y veo anotaciones, métodos sintéticos y otras construcciones extrañas en él. ¡No había tal cosa en el código de bytes de Java! Cuanto tiempo Eliminar

Más detalles
Resultó que la máquina virtual Dalvik (así como la JVM) no es consciente de la existencia de conceptos tales como clases internas / externas (leer clases anidadas), y el compilador genera los llamados métodos "sintéticos" para proporcionar acceso desde la clase anidada a campos externos, por ejemplo.

Como un ejemplo:


Si la clase externa (OuterClass) tiene un campo

 public class OuterClass { List a; ... } 

Para que la clase privada pueda acceder al campo de la clase externa, el compilador generará implícitamente el siguiente método:

 static synthetic java.util.List getList(OuterClass p1) { p1 = p1.a; return p1; } 

Además, debido a la cocina del "compartimento del motor", se logra el trabajo de algunos otros mecanismos que proporciona el lenguaje.

Puede comenzar a estudiar esta pregunta con más detalle desde aquí .

No ayuda Incluso jura por un código de bytes aparentemente no sospechoso. Abro el código fuente del descompilador, leo y veo algo muy extraño: incluso los programadores hindúes (con el debido respeto) no habrían escrito esto. Un pensamiento se arrastra: no es realmente el código generado. Rechazo la idea durante unos 30 minutos, tratando de entender cuál es el error. COMPLICADO Abro Github nuevamente, y realmente, un analizador de gramática generado. Y aquí está el generador en sí. Guardando todo y tratando de acercarse desde el otro lado.

Vale la pena señalar que un poco más tarde aún intenté cambiar la gramática y en algunos lugares incluso el código de bytes para que el descompilador aún pudiera digerirlo. Pero incluso cuando el código de bytes se hizo válido en términos de gramática del descompilador, el programa simplemente no me devolvió nada. Código abierto ...

Hojeo el código de bytes y me encuentro con constantes desconocidas para mí. En Google, me encuentro con lo mismo en el libro sobre las aplicaciones de Android inversas. Recuerdo que esta es solo la ID asignada por el preprocesador del compilador, que se asigna a los recursos de la aplicación de Android (la constante de tiempo de escritura del código es R. *). La próxima media hora - hora, examinaré brevemente qué registros son responsables de qué, en qué orden se pasan los argumentos, y generalmente profundizaré en la sintaxis.

¿Cómo se ve?


Encontré el diseño de la ventana principal de la aplicación, y desde allí ya entendí lo que estaba sucediendo en la aplicación: en la pantalla principal (Actividad) hay un RecyclerView (condicionalmente, una Vista que puede reutilizar objetos de la interfaz de usuario que no se muestran actualmente para la utilización de la memoria) con campos de entrada pares clave / valor, un par de botones que son responsables de agregar un nuevo par clave / valor a un contenedor abstracto determinado y un botón que genera una firma (firma) para este contenedor.

Al observar las anotaciones y observar una cierta cantidad de código sospechosamente similar al generado, empiezo a buscar en Google. El proyecto utiliza la biblioteca ButterKnife, que permite usar anotaciones para inflar () -> bind () elementos de la interfaz de usuario automáticamente. Si hay anotaciones en la clase, el procesador de anotaciones ButterKnife crea implícitamente otra clase de carpeta de la forma <original_class> __ViewBinding , que hace todo el trabajo sucio bajo el capó. En realidad, obtuve toda esta información de un solo archivo MainActivity después de recrear manualmente la similitud de la fuente de Java. Después de media hora, me di cuenta de que las anotaciones de esta biblioteca también pueden establecer una devolución de llamada en las acciones de los botones y encontré aquellas funciones clave que en realidad eran responsables de agregar un par clave / valor al contenedor y generar una firma.

Por supuesto, durante el estudio, tuve que entrar en los "menudillos" de varias bibliotecas y complementos, porque incluso los hermosos landos con cookies no cubren todos los casos de uso y detalles, lo que para cualquier "reversa", creo, es una práctica común.

La pereza es amiga de un programador


Después de pasar más tiempo en la segunda fuente, estaba completamente cansado y me di cuenta de que no era posible cocinar gachas. Estoy subiendo a Github nuevamente, y esta vez estoy mirando más de cerca. Encuentro el proyecto Smali2PsuedoJava , un descompilador en "código pseudo-Java". Incluso si esta utilidad, al menos algo puede conducir a una apariencia humana, entonces para mí el autor es una jarra de su cerveza favorita (bueno, o al menos poner un asterisco en Github, para empezar).

¡Y realmente funciona! Efecto en la cara:



Conoce a Cipher.so


Un poco más tarde, al estudiar el pseudocódigo de Java del proyecto y compararlo incrédulamente con el código de bytes más pequeño, encuentro una biblioteca extraña en el código: Cipher.so. Buscando en Google, descubro que es el cifrado de un conjunto de valores de tiempo de compilación dentro del archivo APK. Esto generalmente es necesario cuando la aplicación usa constantes de la forma: direcciones IP, credenciales para una base de datos externa, tokens para autorización, etc. - lo que se puede obtener con la ayuda de ingeniería inversa de la aplicación. Es cierto, el autor escribe claramente que este proyecto está abandonado, dicen, vete. Esto se está poniendo interesante.

Esta biblioteca proporciona acceso a los valores a través de la biblioteca Java, donde el método específico es la clave que nos interesa. Solo alimenta mi interés, y comienzo a escalar más profundo.

En resumen, qué hace Cipher.so y cómo funciona:

  • en el archivo Gradle de nuestro proyecto se registran las claves y los valores correspondientes
  • Todos los valores clave se empaquetarán automáticamente en una biblioteca dinámica separada (.so), que se generará en tiempo de compilación. Sí, sí, SERÁ generado.
  • entonces estas claves se pueden obtener de los métodos Java generados por Cipher.so
  • después de crear el APK, los nombres de las claves son codificados por MD5 (para mayor seguridad, por supuesto)

Después de encontrar la biblioteca dinámica que necesito en la carpeta de archivo, procedo a elegirla. Para comenzar, como un reverso experimentado (no), trato de comenzar con uno simple: decido mirar la sección con constantes y líneas interesantes en un binario tipo ELF. Desafortunadamente, faltan usuarios de Mac readelf listos para usar, y antes del comienzo decimos lo que apreciamos:

 brew install binuitls 

Y no olvides escribir la ruta a / usr / local en PATH, porque brew te protege de todo de una manera caballerosa ...

 greadelf -p .rodata lib/arm64-v8a/libcipher-lib.so | head -n 15 

Limitamos la salida a las primeras 15 líneas, de lo contrario, esto puede generar un shock para un ingeniero no preparado.



En las direcciones inferiores notamos líneas sospechosas. Como descubrí, al estudiar las fuentes de Cipher.so, las claves y los valores se colocan en el std :: map habitual : esto proporciona poca información, pero sabemos que en el binario en sí, junto con las contraseñas cifradas, también hay claves ofuscadas.

¿Cómo es el cifrado de valores? Al estudiar la fuente, descubrí que el cifrado se produce utilizando AES, el sistema de cifrado simétrico estándar. Entonces, si hay valores cifrados, entonces la clave debería estar cerca ... Después de estudiarla por un tiempo, me encontré con un problema en el mismo proyecto con el título provocativo "Almacenamiento de claves inseguras: los secretos son muy fáciles de recuperar" . En él, de hecho, descubrí que la clave se almacena en forma clara en el binario, y encontré el algoritmo de descifrado. En el ejemplo, la clave estaba en la dirección cero, y aunque entendí que el compilador podría ponerla en otro lugar en la sección .rodata del archivo binario, decidí que esta unidad sospechosa en la dirección cero es la clave.

Intento n. ° 1: procedo a descifrar los valores y creo que la clave de cifrado es la misma. El error OpenSSL sugiere que algo no está bien. Después de leer un poco las fuentes de Cipher.so, entiendo que si el usuario no especifica una clave durante el ensamblaje, se utiliza la clave predeterminada: Cipher.so@DEFAULT .

Intento n. ° 2: error nuevamente. Hmm ... ¿Está realmente redefinido por esta constante? Es bastante simple cometer un error: código confuso escrito en Gradle, con formato "desaparecido". Lo reviso de nuevo. Todo parece ser así.

En lugar de las claves están sus hashes MD5, y luego trato de probar suerte y abrir un servicio con tablas de arcoiris. Voila: una de las claves es la palabra "contraseña". No hay segundo Nos da, por supuesto, no mucho. Ambas claves están en las direcciones 240 y 2a2, respectivamente. En principio, reconocerlos de inmediato es fácil: 32 caracteres (MD5).

Lo revisé todo nuevamente e intenté hacer el descifrado con todas las otras líneas (que están en las direcciones más bajas) como clave para el descifrado: todo es en vano.
Entonces, hay otra clave secreta, el algoritmo de acciones parece ser correcto. Dejo de lado esta tarea e intento no enterrarme.

Habiendo hurgado un poco en el algoritmo de firma del contenedor, todavía veo llamadas a la biblioteca Cipher.so y al código que también usa las funciones criptográficas de la biblioteca Java.

Un acertijo (que nunca resolví)


En la función responsable del cifrado, al principio hay una comprobación de claves en el contenedor.

 public byte[] a(java/util/Map p1) { v0 = p1.size() v1 = 0x0; if (v0 != 0) goto :cond_0 p1 = new byte[v1]; return p1; :cond_0 v0 = "user"; v0 = p1.containsKey(v0) if (v0 == 0) goto :cond_1 p1 = new byte[v1]; return p1; ... 

Literalmente: si hay una clave de "usuario", este contenedor no está firmado (se devuelve una firma cero). Un sentimiento extraño: parece que el problema está resuelto, pero de alguna manera parece sospechosamente simple. Entonces, ¿por qué inventar todo lo demás? Para llevar por mal camino? Entonces, ¿por qué no he estudiado este código con fluidez antes? Hmm ...

No, no es verdad Especifiqué la respuesta de un determinado usuario en un mensajero azul, cuyos contactos se me proporcionaron al asignar la tarea. Excavando más. ¿Quizás el valor / clave de entrada establecido de alguna manera cambia a medida que se agrega al contenedor? Leí el código cuidadosamente.

Tenga en cuenta que el descompilador eliminó las anotaciones del código smali. ¿Y si se quita algo importante? Compruebo los archivos principales, parece que nada significativo. Todo lo importante está en su lugar, pero el significado no se pierde. Compruebo las funciones de devolución de llamada que son responsables de escribir un par clave / valor desde TextBox condicional a contenedores internos. No encontré nada criminal.

Me volví lo más escéptico posible sobre cada línea de código: ya no puedo confiar en nadie.

Solución simple # 2: Noté que el procedimiento de firma comienza al verificar la presencia de algún valor (subcadena en la cadena) en la firma del certificado con el que se firmó la solicitud.

 @OnClick //   protected void huvot324yo873yvo837yvo() { String signature = "no data"; boolean result = some_packages.isKeyInSignature(this); if result { Map map = new HashMap(); ... 

El significado en sí, por supuesto, se encuentra encriptado en ese binario desafortunado. Y, de hecho, si este valor no está en la firma, entonces el algoritmo no firmará nada, sino que simplemente devolverá la cadena "sin datos", como la firma ... Nuevamente, somos tomados para Cipher ...

Pelea final de descifrado clave


Para entender la magnitud de la tragedia, me confundí así:

Hice un volcado hexadecimal de esta sección y escudriñé las dos primeras líneas, cuyas sospechas no disminuyeron desde el principio.



Si presta atención, el carácter que separa las líneas aquí es '0x00'. También es comúnmente utilizado por la biblioteca estándar de C, en funciones de cadena. A partir de eso, no es menos interesante, ¿qué tipo de personaje espacial está en el medio de la primera línea? A continuación, comienzan los intentos locos, donde la clave es:

  • toda la primera fila
  • primera línea antes del espacio
  • primera línea desde el espacio hasta el final
  • ...

El grado de paranoia ya se puede estimar. Cuando no entiendes lo difícil y astuta que debería ser la tarea, entonces comienzas a conducir. Y sin embargo, eso no. Entonces se me ocurrió la idea: "¿Funciona correctamente el algoritmo debido a un problema en mi máquina?". En general, la secuencia de acciones allí es lógica y no planteó preguntas, pero la pregunta es: ¿los comandos en mi máquina hacen lo que se requiere de ellos? Entonces que piensas?

Habiendo verificado todos los pasos manualmente, resultó que

 echo "some_base64_input" | openssl base64 -d 

en algunos argumentos de entrada, de repente devuelve una cadena vacía. Hmm

Reemplazándolo con el primer decodificador base64 en la máquina y clasificando los principales candidatos, se encontró inmediatamente una clave adecuada y las claves se descifraron en consecuencia.

Obtener una firma de un certificado


 class a { public static boolean isKeyInSignature(android.content.Context p1) { v0 = 0x0; try TRY_0{ v1 = p1.getPackageManager() p0 = p1.getPackageName() v2 = 0x40; // GET_SIGNATURES PackageInfo p0 = v1.getPackageInfo(p0, v2) android.content.pm.Signature[] p0 = p0.signatures; // Order are not guaranteed v1 = p0.length; v2 = 0x0; :goto_0 if (v2 >= v1) goto :cond_1 v3 = p0[v2]; String v3 = v3.toCharsString() String v4 = net.idik.lib.cipher.so.CipherClient.a() v3 = v3.contains(v4) }TRY_0 catch TRY_0 (android/content/pm/PackageManager$NameNotFoundException) goto :catch_0; if (v3 == 0) goto :cond_0 p1 = 0x1; return p1; :cond_0 v2 = v2 + 0x1; goto :goto_0 :catch_0 p0 = Thrown Exception p1.printStackTrace() :cond_1 return v0; } 

Así es como se ve el pseudocódigo generado después de mis ediciones menores. Confunde un par de cosas:

  • poco conocimiento de la criptografía y la "cocina" de los certificados del dispositivo
  • De acuerdo con la documentación, este método no garantiza el orden de los certificados en la colección devuelta y, en consecuencia, no sería posible realizar un ciclo en el mismo orden: ¿qué sucede si la solicitud se firmó con más de un certificado?
  • falta de conocimiento sobre cómo extraer el certificado del APK, dado que no está claro qué hace Android Runtime en este caso

Tuve que profundizar en todos estos problemas y el resultado fue el siguiente:

  • el certificado en sí está en el directorio original / META-INF / CERT.RSA

    en este directorio solo hay un archivo con esta extensión, lo que significa que la aplicación está firmada con un solo certificado
  • En el sitio sobre ingeniería de investigación de aplicaciones de Android, se encontró una lista que puede extraer la firma que necesitamos como lo hace Android. Según el autor, al menos.

Al ejecutar este código, puedo descubrir la firma y, en realidad, la clave que necesitamos es una subcadena. Adelante La solución simple # 2 está siendo barrida.

De hecho, la clave está en el certificado, solo queda entender lo que sigue, porque si tenemos la clave de "usuario", todos también obtendremos una firma cero, y como aprendimos anteriormente, esta es la respuesta incorrecta.

¡Escriba la documentación con cuidado!


La investigación adicional sobre el hecho de que los datos ingresados ​​desde los campos de texto se modifican se descarta por falta de evidencia. La paranoia rueda con renovado vigor: ¿tal vez el código que extrajo la firma del certificado es incorrecto o es una implementación de código para versiones antiguas de Android? Abro la documentación nuevamente y veo lo siguiente: ( https://developer.android.com/reference/android/content/pm/Signature.html#toChars () ):



Nota: la función codifica la firma como texto ASCII. El resultado que recibí arriba fue una representación hexadecimal de los datos. Esta API me pareció extraña, pero si crees en la documentación, resulta que volví a detenerme y la clave cifrada no es una subcadena de la firma. Después de sentarme pensativamente en el código por un tiempo, no pude soportarlo y abrí el código fuente de esta clase. https://android.googlesource.com/platform/frameworks/base/+/e639da7/core/java/android/content/pm/Signature.java

La respuesta no se hizo esperar. Y en realidad, en el código mismo, una pintura al óleo: el formato de salida es una cadena hexadecimal ordinaria. Y ahora piense: o no entiendo algo, o la documentación está escrita "ligeramente" incorrectamente. No habiendo regañado de ninguna manera, me puse a trabajar de nuevo.

Resumen


Han pasado las siguientes n horas:

  • RecyclerView .. , StackOverflow
  • , , Java. , - («user») . .

, ( ).

No , . , , , , . . — , , , .

, , . . , . , - , , « », , .

- c – arturbrsg .

Stay tuned.

Source: https://habr.com/ru/post/450394/


All Articles