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:

id:%lu|tid:%lu|res:%lu

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: