Ahora obtienes el archivo de compilación original de Andrómeda. Cargue la muestra desempaquetada en OllyDBG. Como antes, después del marco de pila en el EP, verá que el malware busca cargar la dirección de la API usando la estructura PEB_LDR_DATA , pero esta vez en lugar de kernel32.dll; el malware intenta encontrar la dirección base ntdll.dll, luego analizará el EAT, realizará un hash de cada API y luego hará una comparación para encontrar las API necesarias:
¡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
Luego de ingresar al CALL, calculará el hash de un buffer ubicado en 0040:
def calc_hash(cadena):
devolver binascii.crc32 (cadena) y 0xffffffff
Luego, lo comparó con 0xBD274BDB, si no coincide, llama a RtlExitUserThread, pronto descubriremos qué tipo de datos se han codificado. Luego, se llamará a ZwAllocateVirtualMemory y me devolverá 7FFA0000, luego la LLAMADA al 00401343 copiará todo el búfer en el espacio asignado. A continuación, vemos la CALL en VA 00401351 que toma 4 argumentos y uno de ellos es un puntero a nuestro buffer desconocido. Esta rutina en realidad realiza un cifrado RC4, se puede reconocer que al observar las constantes, así es como básicamente se detecta algún algoritmo criptográfico; RC4 tiene bucles que van hasta 256 , que es 0x100 en hexadecimal.
Usando ZwAllocateVirtualMemory nuevamente, asignamos un nuevo espacio de memoria a la carga útil parcialmente descifrada para que finalmente se descomprima usando la biblioteca aPLib. El código que sigue es responsable de procesar reubicaciones y arreglar importaciones. Por ejemplo, desde NTDLL Andromeda está importando estas API:
LdrLoadDll, RtlDosPathNameToNtPathName_U, RtlFreeUnicodeString, LdrProcessRelocationBlock, RtlComputeCrc32, RtlWalkHeap, RtlImageNtHeader, RtlImageDirectoryEntryToData, RtlExitUserThread, ZwSetInformationProcess, ZwUnmapViewOfSection, ZwAllocateVirtualMemo ry, ZwMapViewOfSection, ZwFreeVirtualMemory, ZwOpenFile, ZwQueryDirectoryFile, ZwClose, ZwQueryInformationProcess.
Puedes encontrar un script aquí de una versión antigua de Andrómeda gracias a
0xEBFE . Aún necesita realizar algunos cambios menores para que funcione correctamente, en particular las API y las importaciones, que cambiaron un poco:
[descargar]
de importación idaapi *
de importación idautils *
desde aplib importar descomprimir
importar binascii
estructura de importación
# codificar apesta 🙂
IMPORTACIONES = { ‘ntdll.dll’ : (‘ZwResumeThread’, ‘ZwQueryInformationProcess’, ‘ZwMapViewOfSection’, ‘ZwCreateSection’, ‘ZwClose’, ‘ZwUnmapViewOfSection’, ‘NtQueryInformationProcess’, ‘RtlAllocateHeap’, ‘RtlExitUserThread’, ‘RtlFreeHeap’, ‘RtlRandom’,’RtlReAllocateHeap’, ‘RtlSizeHeap’, ‘ZwQuerySection’, ‘RtlWalkHeap’, ‘NtDelayExecution’),
‘kernel32.dll’ : (‘GetModuleFileNameW’, ‘GetThreadContext’, ‘GetWindowsDirectoryW’, ‘GetModuleFileNameA’, ‘CopyFileA’, ‘CreateProcessA’, ‘ExpandEnvironmentStringsA’, ‘CreateProcessW’, ‘CreateThread’, ‘CreateToolhelp32Snapshot’, ‘DeleteFileW’ ,’DisconnectNamedPipe’, ‘ExitProcess’, ‘ExitThread’, ‘ExpandEnvironmentStringsW’, ‘FindCloseChangeNotification’, ‘FindFirstChangeNotificationW,FlushInstructionCache’, ‘FreeLibrary’, ‘GetCurrentProcessId’, ‘GetEnvironmentVariableA’, ‘GetEnvironmentVariableW’, ‘GetExitCodeProcess’, ‘Obtener tamaño de archivo’ , ‘GetFileTime’, ‘GetModuleHandleA’, ‘GetModuleHandleW’, ‘GetProcAddress’, ‘GetProcessHeap’,’CreateNamedPipeA’, ‘GetSystemDirectoryW’, ‘GetTickCount’, ‘GetVersionExA’, ‘GetVolumeInformationA’, ‘GlobalLock’, ‘GlobalSize’, ‘GlobalUnlock’, ‘LoadLibraryA’, ‘LoadLibraryW’, ‘LocalFree’, ‘MultiByteToWideChar’, ‘OpenProcess ‘, ‘OpenThread’, ‘QueueUserAPC’, ‘ReadFile’, ‘ResumeThread’, ‘SetCurrentDirectoryW’, ‘SetEnvironmentVariableA’, ‘SetEnvironmentVariableW’, ‘SetErrorMode’, ‘SetFileAttributesW’, ‘SetFileTime’, ‘SuspendThread’, ‘TerminateProcess’, ‘Thread32First’, ‘Thread32Next’, ‘VirtualAlloc’, ‘VirtualFree’, ‘VirtualProtect’, ‘VirtualQuery’, ‘WaitForSingleObject’, ‘WriteFile’,’lstrcatA’, ‘lstrcatW’, ‘lstrcmpiW’, ‘lstrcpyA’, ‘lstrcpyW’, ‘lstrlenA’, ‘lstrlenW’, ‘CreateFileW’, ‘CreateFileA’, ‘ConnectNamedPipe’, ‘CloseHandle’, ‘GetShortPathNameW’),
‘advapi32.dll’ : (‘CheckTokenMembership’, ‘RegCloseKey’, ‘ConvertStringSidToSidA’, ‘ConvertStringSecurityDescriptorToSecurityDescriptorA’, ‘RegOpenKeyExA’, ‘RegSetValueExW’, ‘RegSetValueExA’, ‘RegSetKeySecurity’, ‘RegQueryValueExW’, ‘RegQueryValueExA ‘, ‘RegOpenKeyExW’ , ‘RegNotifyChangeKeyValue’, ‘RegFlushKey’, ‘RegEnumValueW’, ‘RegEnumValueA’, ‘RegDeleteValueW’, ‘RegDeleteValueA’, ‘RegCreateKeyExW’, ‘RegCreateKeyExA’),
‘ws2_32.dll’: (‘conectar’, ‘apagar’, ‘WSACreateEvent’, ‘closesocket’, ‘WSAStartup’, ‘WSAEventSelect’, ‘socket’, ‘sendto’, ‘recvfrom’, ‘getsockname’, ‘gethostbyname’ , ‘escuchar’, ‘aceptar’, ‘WSASocketA’, ‘enlazar’, ‘htons’),
‘user32.dll’: (‘wsprintfW’, ‘wsprintfA’),
‘ole32.dll’: (‘CoInitialize’),
‘dnsapi.dll’: (‘DnsWriteQuestionToBuffer_W’, ‘DnsRecordListFree’, ‘DnsExtractRecordsFromMessage_W’)}
def calc_hash(cadena):
devolver binascii.crc32 (cadena) y 0xffffffff
def rc4crypt(datos, clave):
x = 0
cuadro = bytearray(rango(256))
para i en el rango (256):
x = (x + cuadro[i] + clave[i % len(clave)]) % 256
cuadro[i], cuadro[x] = cuadro[x], cuadro[i]
x,y = 0, 0
salida = bytearray()
para byte en datos:
x = (x + 1) % 256
y = (y + cuadro[x]) % 256
cuadro[x], cuadro[y] = cuadro[y], cuadro[x]
salida += bytearray([byte ^ caja[(caja[x] + caja[y]) % 256]])
regresar
def fix_payload_relocs_and_import(segmento, relocs_offset):
compensación_actual = 0
# procesando reubicaciones
mientras que Verdadero:
base = Dword(segmento + relocs_offset + current_offset)
tamaño = Dword(segmento + relocs_offset + current_offset + 4)
si (base == 0 y current_offset! = 0) o tamaño == 0:
compensación_actual += 4
romper
compensación_actual += 8
tamaño = (tamaño – 8) // 2
para i en rango (tamaño):
reloc = Palabra (segmento + relocs_offset + current_offset)
si reubicación y 0x3000:
reubicar = reubicar 0xFFF
PatchDword(segmento + base + reubicación, Dword(segmento + base + reubicación) + segmento)
SetFixup(segmento + base + reubicación, idaapi.FIXUP_OFF32 o idaapi.FIXUP_CREATED, 0, Dword(segmento + base + reubicación) + segmento, 0)
compensación_actual += 2
# procesamiento de importaciones
mientras que Verdadero:
module_hash = Dword(segmento + relocs_offset + current_offset)
import_offset = Dword(segmento + relocs_offset + current_offset + 4)
compensación_actual += 8
si module_hash == 0 o import_offset == 0:
romper
módulo = Ninguno
para biblioteca en iter (IMPORTACIONES):
si module_hash == calc_hash(library.lower()):
módulo = biblioteca
mientras que Verdadero:
func_hash = Dword(segmento + relocs_offset + current_offset)
compensación_actual += 4
si func_hash == 0:
romper
si el módulo no es Ninguno:
para función en iter(IMPORTACIONES[módulo]):
si func_hash == calc_hash(función):
MakeDword(segmento + import_offset)
MakeName(segmento + import_offset, SegName(segmento) + ‘_’ + module.split(‘.’)[0] + ‘_’ + función)
demás:
print(‘Importación no encontrada: módulo = 0x{0:08X}, función = 0x{1:08X}’.format(module_hash, func_hash))
compensación_importación += 4
devolver
def decrypt_payload(dirección_cifrada, rc4key, tamaño_cifrado, tamaño_descomprimido, punto_de_entrada, relocs, tamaño_relocs):
buffer = bytearray (tamaño_cifrado)
para i en el rango (len (búfer)):
buffer[i] = Byte(encrypted_addr + i)
descifrado = rc4crypt (búfer, rc4key)
desempaquetado = descomprimir(str(descifrado)).do()
# comprobando la dirección del segmento gratuito
inicio_seg = 0x10000000
mientras que SegName(seg_start) != »:
inicio_seg += 0x10000000
AddSeg(seg_start, seg_start + tamaño_descomprimido, 0, 1, idaapi.saRelPara, idaapi.scPub)
# copiar datos a un nuevo segmento
datos = desempaquetado[0]
para i en rango(len(datos)):
PatchByte(seg_start + i, ord(datos[i]))
fix_payload_relocs_and_import(seg_start, relocs)
MakeFunction(seg_start + punto_entrada)
devolver
definición principal():
payload_addr = AskAddr(ScreenEA(), «Ingrese la dirección de la carga útil de Andrómeda»)
si payload_addr != idaapi.BADADDR y payload_addr no es Ninguno:
carga útil = bytearray(0x28)
para i en rango (len (carga útil)):
carga útil [i] = Byte (dirección_carga útil + i)
dwords = struct.unpack_from(‘LLLLLL’, bytes(carga útil), 0x10)
decrypt_payload(payload_addr + 0x28, carga útil[:16], dwords[0], dwords[2], dwords[3], dwords[4], dwords[5])
si __nombre__ == ‘__principal__’:
principal()
Al final, verá la llamada a: 00401532 |. FFD0 LLAMADA EAX
Esto transferirá el control a la carga útil. Aquí hay una captura de pantalla sobre la carga útil descifrada.
El siguiente paso muestra los trucos antianálisis que se emplean. La llamada en VA 7FF91408 itera a través de los nombres de los procesos y calcula sus valores hash CRC32: si un valor hash coincide con cualquiera de los de una lista de valores hash de procesos VM y herramientas de monitoreo como wireshark.exe, etc., esto indica que la depuración El proceso está dentro de un entorno sandbox o está siendo monitoreado.
Además, este truco no cambia. Al igual que en las versiones 2.07 y 2.08, la versión 2.09 continúa calculando el hash CRC32 del nombre del volumen de la unidad C:, que luego se compara con el valor codificado 0xDD84. Si te atrapan, ejecutarás un bucle infinito que llamará a ZwDelayExecution. simplemente parchee el JNZ después de la llamada o coloque RET en ZwDelayExecution.
Después de eso, creo que la LLAMADA en VA 7FF914stá intentando configurar un enlace KiFastSystemCall, esta API es la API de nivel más bajo disponible en la capa «modo de usuario», también conocida como Ring3, todas las llamadas de la aplicación pasan desde KiFastSystemCall, que redirige todos esos controles al Kernel de Windows a través de una instrucción llamada SYSENTER.
A continuación, debido a que los procesos ejecutados por el usuario no pueden hacer todo, como escribir en la memoria de explorer.exe, el malware intenta usar SeDebugPrivilege y llama a ZwAdjustTokenPrivilege para escalar a privilegios del sistema. Llama a la API SetEnvironmentVariableW para guardar la ruta completa del bot original a la variable de entorno. Luego viene el proceso de inyección, dependiendo de si tienes un sistema operativo de 32 o 64 bits, el malware lanzará una versión sagrada de msiexe.exe e inyectará su código allí:
Inyección de código:
El proceso de inyección consta de varios pasos:
Al igual que con las versiones anteriores, el malware llama a CreateFile para obtener el identificador del archivo que desea inyectar. Luego obtiene su identificador de sección llamando a ZwCreateSection, que ZwMapViewOfSection utiliza para obtener la imagen del archivo en la memoria. De esta imagen, extrae el tamaño de la imagen y la dirección del punto de entrada del encabezado PE.
Se crea una dirección de memoria del mismo tamaño que la de la imagen del archivo que se quiere inyectar con acceso PAGE_EXECUTE_READWRITE. Luego, la imagen del archivo se copia en esta dirección de memoria.
Se crea otra dirección de memoria con el mismo tamaño que la imagen del archivo bot original, también con acceso PAGE_EXECUTE_READWRITE. Luego, el archivo original se copia a esta nueva dirección de memoria.
Se crea un proceso suspendido del archivo a inyectar. La dirección de memoria que contiene el archivo original no está asignada. Se llama a ZwMapViewOfSection con el identificador de archivo del bot y el identificador de proceso (adquirido al crear el proceso de archivo suspendido). Ahora el identificador de proceso del archivo inyectado tiene una vista de mapa del archivo botnet. El último paso es la llamada a ZwResumeThread, que reanuda el proceso.
Si el usuario es administrador, verifica que con CheckTokenMemberShip, se instala en «%ALLUSERPROFILE%» y se inicia automáticamente usando una ruta de registro poco común «softwaremicrosoftwindowscurrentversionPoliciesExplorerRun», con un nombre de clave aleatorio. De lo contrario, solo se instala en «%USERPROFILE%».
Comunicación CNC:
Antes de establecer una conexión, el bot prepara el mensaje que se enviará al servidor CC. Utiliza el siguiente formato: id:%lu|bid:%lu|os:%lu|la:%lu|rg:%lu
Esta cadena se cifra utilizando RC4 con una clave codificada de longitud 0x además se codifica utilizando base64. Luego el mensaje se envía al servidor. Una vez que se recibe un mensaje, el bot calcula el hash CRC32 del mensaje sin incluir el primer DWORD. Si el hash calculado coincide con el primer DWORD, el mensaje es válido. Posteriormente se descifra utilizando RC4 con VolumeSerialNumber como clave. Después del descifrado RC4, el mensaje tiene el formato gn ([cadena codificada en base64]). Solía ser solo la cadena codificada en base64, pero por alguna razón el autor decidió no hacer que el servidor fuera compatible con las versiones anteriores del bot. Luego decodifica la cadena base64 dentro de los corchetes para obtener el mensaje en texto sin formato.
El primer DWORD del mensaje se utiliza como multiplicador para multiplicar un valor en un desplazamiento fijo. El DWORD en ese desplazamiento se usa como intervalo para retrasar la llamada nuevamente al hilo para establecer otra conexión. El siguiente byte indica qué acción realizar; hay siete opciones:
- Caso 1 (descargar EXE): Conéctese al dominio descifrado del mensaje para descargar un archivo EXE. Guarde el archivo en la ubicación %tmp% con un nombre aleatorio y ejecute el proceso.
- Caso 2 (cargar complementos): conéctese al dominio descifrado del mensaje, instale y cargue complementos. RC4 descifra los complementos utilizando la misma clave de longitud 0x
- Caso 3 (caso de actualización): Conéctese al dominio para obtener el archivo EXE de actualización. Si un nombre de archivo de VolumeSerialNumber está presente en el registro, guarde el archivo PE en la ubicación %tmp% con un nombre aleatorio; de lo contrario, guárdelo en la ubicación actual con el nombre del archivo como VolumeSerialNumber. El archivo en %tmp% se ejecuta, mientras que el proceso actual finaliza. También envía el mensaje ‘kill’ xor’ed por VolumeSerialNumber para finalizar el proceso anterior.
- Caso 4 (descargar DLL): conéctese al dominio y guarde el archivo DLL en la ubicación %alluserprofile%. El archivo se guarda como un archivo .dat con un nombre aleatorio y se carga desde una función de exportación especificada. El registro se modifica para que el bot pueda cargarlo automáticamente.
- Caso 5 (eliminar DLL): elimine y desinstale todas las DLL cargadas e instaladas en el Caso 4.
- Caso 6 (eliminar complementos): desinstale todos los complementos cargados en el Caso 3.
- Caso 7 (desinstalar bot): suspender todos los hilos y desinstalar el bot.
- Después de ejecutar la acción según la instrucción recibida, se envía otro mensaje al servidor para notificarle que la acción se ha completado:
id:%lu|tid:%lu|res:%lu
- id es el número de serie del volumen
- tid es el siguiente byte (id de tarea) después del byte que muestra el número de caso en el mensaje recibido
- res es el resultado de si la tarea se realizó con éxito o no.
Una vez que se ha enviado el mensaje, el hilo sale y espera a que pase el intervalo de retraso antes de volver a conectarse al servidor para recibir instrucciones adicionales.
Conclusión:
Andromeda’s current version 2.09 increased the barriers that it has set up for security researchers. The new features raise additional difficulty for analysis, but are still easy to skip.
Become a certified reverse engineer!
Get live, hands-on malware analysis training from anywhere, and become a Certified Reverse Engineering Analyst. Start Learning
We anticipate that the Andromeda botnet will keep on evolving. Our botnet monitoring system is continuing to track its activities and we will respond immediately when it enters its next generation.
Credits and References:
- https://blog.fortinet.com/post/andromeda-2-7-features