Análisis de malware
Invertir cargadores de malware: el caso Matsnu-A, parte 2
10 de agosto de por Kyriakos Economou
En la última entrega, examinamos la estructura de datos del cargador PEB. Retomamos aquí la discusión.
Localice y aísle el ejecutable descifrado integrado
¡Conviértete en un ingeniero inverso certificado!
Obtenga capacitación práctica en vivo sobre análisis de malware desde cualquier lugar y conviértase en un analista certificado de ingeniería inversa. Comienza a aprender
¡Conviértete en un ingeniero inverso certificado!
Obtenga capacitación práctica en vivo sobre análisis de malware desde cualquier lugar y conviértase en un analista certificado de ingeniería inversa. Comienza a aprender
Una vez almacenados los VA de las API necesarias, volvemos a la siguiente instrucción después de la LLAMADA a la dirección 004085CD que mencionamos anteriormente.
El fragmento de código que sigue también es de gran interés:
004085D2 64A1 30000000 MOV EAX, DWORD PTR FS:[30] ß obtener dirección de PEB
004085D8 8B40 08 MOV EAX, DWORD PTR DS:[EAX+8] ß obtener base de autoimagen de PEB (módulo principal)
004085DB 8983 38153A00 MOV DWORD PTR DS:[EBX+3A1538], EAX ß almacena la base de autoimagen
004085E1 8BBB 38153A00 MOV EDI, DWORD PTR DS:[EBX+3A1538] ß mover la base de autoimagen a EDI
004085E7 03BB 60153A00 AGREGAR EDI, DWORD PTR DS:[EBX+3A1560] ß agregar EDI como una constante (AE000)
Las siguientes cinco instrucciones son otro ejemplo de ofuscación. El resultado final siempre es 10000, por lo que podría simplemente hacer MOV ESI, 10000. Sin embargo, esto podría ser un cálculo dinámico del tamaño del área necesaria para asignar en función de las características del archivo empaquetado con este cargador.
004085ED SER 61010000 MOV ESI, 161
004085F2 03B3 5C153A00 AÑADIR ESI, DWORD PTR DS:[EBX+3A155C]
004085F8 03B3 6C153A00 AÑADIR ESI, DWORD PTR DS:[EBX+3A156C]
004085FE 81C6 00000100 AÑADIR ESI, 10000
00408604 81E6 0000FFFF Y ESI, FFFF0000
Luego usa la API VirtualAlloc para asignar algo de memoria adicional con derechos de acceso PAGE_EXECUTE_READWRITE .
0040860A 6A 40 EMPUJE 40
0040860C 68 00300000 EMPUJAR 3000
00408611 56 EMPUJE ESI
00408612 6A 00 EMPUJAR 0
00408614 FF93 25153A00 CALL NEAR DWORD PTR DS:[EBX+3A1525] ß apunta a la tabla de importaciones anterior, en la dirección de la API VirtualAlloc.
Una vez que se asigna la nueva área de memoria, comenzará a escribir código allí. La primera transferencia de código se realiza unas cuantas instrucciones más tarde.
00408631 F3A4 REP MOVS BYTE PTR ES:[EDI], BYTE PTR DS:[ESI] ß ESI apunta a 00408993 , EDI es cualquier dirección devuelta por la API VirtualAlloc y ECX, que es el contador, es 161 .
El siguiente código se transfiere al área de memoria asignada:
0040865C F3A4 REP MOVS BYTE PTR ES:[EDI], BYTE PTR DS:[ESI] ß ESI apunta a 00402488 , EDI es cualquier dirección devuelta por VirtualAlloc API + 4349 , y ECX esta vez es BE .
Siguiente transferencia de código:
0040866A F3A4 REP MOVS BYTE PTR ES:[EDI], BYTE PTR DS:[ESI] ß ESI apunta a 00403140 , EDI es cualquier dirección devuelta por VirtualAlloc API + 4407 , y ECX esta vez es 17B7 .
Siguiente transferencia de código:
00408678 F3A4 REP MOVS BYTE PTR ES:[EDI], BYTE PTR DS:[ESI] ß ESI apunta a 00406028 , EDI es cualquier dirección devuelta por VirtualAlloc API + 5BBE , y ECX esta vez es 255C .
La siguiente parte interesante del código del cargador se encuentra en la dirección 0040868C , donde llama a una función que descifra una parte del código transferido al área de memoria previamente asignada.
0040896E 89CE MOV ESI, ECX
00408970 83E6 03 Y ESI, 3
00408973 75 12 JNZ CORTO 00408987
00408975 8B5D 10 MOV EBX, DWORD PTR SS:[EBP+10]
00408978 6601DA AÑADIR DX, BX
0040897B 6BD2 03 IMUL EDX, EDX, 3
0040897E 66F7D2 NO DX
00408981 C1CA 07 ROR EDX, 7
00408984 8955 10 MOV DWORD PTR SS:[EBP+10], EDX
00408987 3010 XOR BYTE PTR DS:[EAX], DL ß
después de realizar algunos cálculos, realiza una XOR del byte en la ubicación de memoria señalada por EAX con el valor almacenado en DL. La dirección inicial es 002D4349 .
00408989 40 INC EAX
0040898A C1CA 08 ROR EDX, 8
0040898D E2 DF LOOPD CORTO 0040896E ß bucle arriba 3DD1 veces
Una vez salimos de esta función, nos encontramos con otra transferencia de código.
004087CA A4 MOVS BYTE PTR ES:[EDI], BYTE PTR DS:[ESI] ß ESI apunta al área de memoria en la que la función anterior estaba realizando el descifrado y EDI. EDI es cualquier dirección devuelta por VirtualAlloc API + 161 . En este caso, ECX es el contador (que aquí es igual a 3DD1) .
Antes de continuar, echemos un vistazo a cómo se ve el código descifrado que se va a transferir. Solo muestro un pequeño bloque de código desde el principio.
002D4349
M 8 Z €8gÿ@Ñxá.ã€.º|.´.Í!¸`L. Este
prog3amÃc£n §tóbe ×
002D4389 çu¯Ìi D?OS.mode …V$D PE LÅrOXàà9
sSC…*ÊØ™’ð0Vž@‚;¯
002D43C9 ‚»ÁR
h@m!.*h?Äx¼1Î0.tXŸ4Fkð UPX W0íC’‰`™àQ1″‘Èð»8 (¨@ˆ.rrsRc
002D4409 Ý ÌV+:(àÀ3.0P4}!…²7{ïIØÎÙBbh6!è€.È×þÿU‰å ì Ç…ðûßÀ
Bueno, parece que el código descifrado es un módulo ejecutable, pero obviamente aún no está bien reconstruido en la memoria. El código que sigue tiene como objetivo reconstruir el ejecutable descifrado en la memoria, y el siguiente bloque de código muestra el comienzo del ejecutable una vez hecho esto.
002D0161 MZ€……ÿÿ..@……@…………….. …..€…
002D01A1 º.´.Í!¸LÍ!Este programa no se puede ejecutar en modo DOS…$…….
002D01E1 PE..L.ÅrO…….à.
C.@……à..€..ð…0…@…….
002D0221 ……………..@…………………………… ……
002D0261 ¼1….0.¼…………………………… ……….
002D02A1 ………………………………………… …… UPX0 ….
002D02E1 .à……………………€..à UPX1 …..@…ð…8…. ………
002D0321 ….@..à .rsrc …….0…….:…………….@..À 3.04.UPX !..²7 {
´BÛš§‹öò.ïwgˆ@ ªpàÈà–àörÝw.
Es bastante obvio que el cargador acaba de descifrar un módulo ejecutable empaquetado en UPX. Cuando esto sucede, los escenarios más comunes son: volcar esta área de memoria en un nuevo archivo e iniciar un proceso hijo, o transferir la ejecución directamente al punto de entrada del ejecutable descifrado en la memoria.
En cualquier caso, vamos a necesitar este ejecutable para poder analizar la siguiente etapa, por lo que lo voy a aislar de la memoria en dos sencillos pasos. Primero, voy a volcar toda el área de memoria asignada porque sé que el ejecutable está allí, y luego cortaré todo el código prefijado que ya no necesito y guardaré el archivo.
La siguiente demuestra el segundo paso:
2: Selección de bytes antepuestos para cortar
En este punto podemos comenzar a trabajar directamente en el ejecutable empaquetado UPX que acabamos de guardar, ya que el cargador saltará a su punto de entrada en la memoria de todos modos después de escribir su código desde la memoria asignada dentro de su propio espacio de direcciones de imagen PE.
002D006A FFE1 JMP NEAR ECX ß salta al punto de entrada del archivo empaquetado UPX.
Pasando por la tercera etapa del cargador
Ahora podemos comenzar a trabajar en el archivo empaquetado UPX que extrajimos de la memoria del cargador durante la parte anterior del análisis. Descomprimir manualmente un archivo empaquetado en UPX es bastante trivial por lo que no voy a dedicar más líneas hablando de UPX. En lugar de ello continuaré con el análisis del código del cargador del malware.
0040E5D4 55 PULSADOR EBP ß OEP
0040E5D5 89E5 MOVIMIENTO EBP, ESP
0040E5D7 81EC 3400 SUBESP, 234
0040E5DD C745 CC 0000000 MOV DWORD PTR SS:[EBP-34], 0
0040E5E4 C745 D0 0000000 MOV DWORD PTR SS:[EBP-30], 0
0040E5EB C745 D4 0000000 MOV DWORD PTR SS:[EBP-2C], 0
Unas pocas instrucciones más tarde, observamos un intento de detectar si el malware se está ejecutando actualmente dentro de una zona de pruebas. No puedo decir en qué entorno de pruebas el autor probó el siguiente truco, pero así es como se implementa.
En realidad, empuja en la pila la ruta absoluta del directorio en el que se encuentra y luego empuja en la pila la cadena «sand-box». Finalmente, utiliza la función strstr para comprobar si la ruta absoluta contiene esta subcadena.
Comprobación de SandBox:
0040E666 51 EMPUJE ECX
0040E667 50 EMPUJE EAX
0040E668 FF15 AAF84000 LLAMADA ntdll.strstr
Vista de pila:
0006FD4C 0006FD54 s1=»c:usersr.c.edesktopmatsuiupx_packed_decrypted.pe»
0006FD50 0006FF70 s2 = «caja de arena»
Si la verificación descrita anteriormente tiene éxito, el proceso finalizará.
Durante esta etapa, el cargador primero copiará la tabla de importaciones de una ubicación a otra y luego intentará crear un proceso hijo e inyectarle un hilo. Si echas un vistazo, unas cuantas instrucciones más adelante notarás algunas llamadas a la función memcpy a través de la cual se crean copias de la tabla de importaciones.
Una vez copiada la tabla de importaciones, se realiza una LLAMADA a una función en la dirección 0040E6F8 . Esta función está dedicada a la creación del proceso hijo y también llama a otra función dedicada a la inyección del hilo malicioso. En la siguiente parte, demostraré dos formas de mantener el control de la ejecución del código inyectado en el nuevo hilo, que está configurado para ejecutarse inmediatamente después de la creación.
Mantenga el control sobre los hilos inyectados
Al ingresar a la función de CALL mencionada anteriormente, podemos ver que el fragmento de código que inicia el proceso hijo está en modo suspendido.
0040E808 56 EMPUJE ESI
0040E809 57 EMPUJE EDI
0040E80A 6A 00 EMPUJAR 0
0040E80C 6A 00 EMPUJAR 0
0040E80E 6A 04 EMPUJE 4
0040E810 6A 00 EMPUJAR 0
0040E812 6A 00 EMPUJAR 0
0040E814 6A 00 EMPUJAR 0
0040E816 50 EMPUJE EAX
0040E817 6A 00 EMPUJAR 0
0040E819 FF15 E4F14000 LLAMADA kernel32.CreateProcessA
Vista de pila:
0006FC6C 00000000 |NombreArchivoMódulo = NULL
0006FC70 00403DB9 |CommandLine = «svchost.exe»
0006FC74 00000000 |pProcessSecurity = NULL
0006FC78 00000000 |pThreadSecurity = NULO
0006FC7C 00000000 |HeredarManejadores = FALSO
0006FC80 00000004 |CreationFlags = CREATE_SUSPENDED
0006FC84 00000000 |pEntorno = NULL
0006FC88 00000000 |DirActual = NULO
0006FC8C 0006FCA0 |pInformación de inicio = 0006FCA0
0006FC90 0006FCE4 pProcessInfo = 0006FCE4
Como puede ver, el autor elige iniciar svchost.exe como proceso secundario que no haría que un usuario sospeche algo a través de los nombres de los procesos del administrador de tareas (o cualquier otra herramienta de enumeración de procesos).
En este punto necesitamos saber el PID del proceso hijo que se va a crear, el cual podemos recuperar de la estructura PROCESS_INFORMATION una vez creado el proceso hijo. Debido a que Windows tendrá más de un proceso con el mismo nombre, necesitamos saber cuál fue creado por el cargador del malware para poder adjuntarlo más adelante.
Unas líneas más adelante, en la dirección 0040E828 , LLAMARÁ la función dedicada a la inyección del hilo malicioso. Primero asignará algo de memoria adicional al proceso hijo, aún en modo suspendido.
0040E847 C745 F9 0000000 MOV DWORD PTR SS:[EBP-7], 0
0040E84E 6A 40 EMPUJAR 40
0040E850 68 00301000 EMPUJAR 103000
0040E855 68 D4D50000 EMPUJAR 0D5D4
0040E85A 6A 00 EMPUJAR 0
0040E85C FF75 08 EMPUJAR DWORD PTR SS:[EBP+8]
0040E85F FF15 A8F14000 kernel32.VirtualAllocEx
Luego utilizará la API WriteProcessMemory para inyectar el código en el área de memoria asignada dentro del subproceso secundario.
0040E874 51 EMPUJE ECX
0040E875 68 D4D50000 EMPUJAR 0D5D4
0040E87A 56 EMPUJE ESI
0040E87B FF75 E8 EMPUJAR DWORD PTR SS:[EBP-18]
0040E87E FF75 08 EMPUJAR DWORD PTR SS:[EBP+8]
0040E881 FF15 B4F14000 LLAMADA kernel32.WriteProcessMemory
Vista de pila:
0006FC54 00000038 |hProceso = 00000038 (ventana)
0006FC58 7FFA0000 |Dirección = 7FFA0000
0006FC5C 00401000 |Búfer = UPX_pack.00401000
0006FC60 0000D5D4 |Bytes para escribir = D5D4 (54740.)
0006FC64 0006FC68 pBytesEscritos = 0006FC68
Método 1: inyectar un bucle infinito
Como puede ver arriba, la dirección de inicio del búfer que se copiará al proceso hijo es 00401000 . Entonces, en este punto, y como no queremos perdernos la ejecución del hilo, podemos ir al buffer y cambiar (en este caso) los primeros 2 bytes de 5589 a EBFE (que corresponde a una instrucción de salto que vuelve a sí mismo). De esta forma creamos un bucle infinito .
Finally, the loader will start the injected thread, but remember, we had set an infinite loop at the beginning of it.
0040E88A 50 PUSH EAX
0040E88B 6A 00 PUSH 0
0040E88D 6A 00 PUSH 0
0040E88F FF75 E8 PUSH DWORD PTR SS:[EBP-18]
0040E892 6A 00 PUSH 0
0040E894 6A 00 PUSH 0
0040E896 FF75 08 PUSH DWORD PTR SS:[EBP+8]
0040E899 FF15 B8F14000 CALL kernel32.CreateRemoteThread
Once this step is done, we can attach to the child process and analyse the injected thread which keeps looping over the first instruction. Then we cab go there and take control of it in order to restore the two original bytes.
Method 2 – Modify EP Memory Dump
Another trick that we can use in this case is: to wait for the loader to copy the imports table (as seen previously during the explanation of the first method). But instead of letting the loader to copy the code starting from address 00401000 to the child process, we can set the entry point there, dump and fix the imports, as we normally do during manual unpacking practices.
In this case, the technique is safe primarily because the code of the injected thread needs to be stand-alone in the context of the process address space in which it runs. In other words, since this piece of code it is injected inside the address space of another process cannot rely on the memory alignment of the other modules, their image base etc.
So it is safe to set the entry point at address 00401000, once the imports are copied and just dump from there and save it as a new executable file.
Conclusion
The behaviour of the loader examined during this article is very similar to the most common loaders used by various types of malwares in nowadays, such as ransomware, fake AVs etc. Keep in mind that in most of the cases, the loader at some point will make use of at least one of the following three APIs: VirtualAlloc, VirtualAllocEx, or ZwAllocateVirtualMemory. So it is good practice to keep an eye on them and on the memory area(s) allocated through them.
Become a certified reverse engineer!
Get live, hands-on malware analysis training from anywhere, and become a Certified Reverse Engineering Analyst. Start Learning
Become a certified reverse engineer!
Get live, hands-on malware analysis training from anywhere, and become a Certified Reverse Engineering Analyst. Start Learning
Have fun!