Análisis del código fuente del ransomware de Conti (locker)

Conti, uno de los más temibles grupos de ransomware de los últimos tiempos, declaró públicamente su apoyo a Rusia en la invasión a Ucrania y el resultado no se hizo esperar: un investigador supuestamente ucraniano con el nick ContiLeaks publicó numerosos ficheros json con conversaciones internas del grupo desde junio de 2020, detalles de su infraestructura y código fuente de su panel administración, la API de BazarBackdoor y hasta del propio ransomware (encryptor, decryptor y constructor) que estaba en un zip con contraseña que fue posteriormente crackeada.

En este post vamos a hacer un repaso al análisis de Cluster25 del código del ransomware, muy bien modularizado y administrado. El proyecto está desarrollado en C++ sobre una versión de Visual Studio 2015 con el conjunto de herramientas Windows XP Nplatform (v140_xp). La plataforma de destino especificada es 10.0 (Windows 10). La estructura del proyecto está organizada en diferentes subcarpetas, donde cada una maneja un módulo específico del ransomware (como la carpeta "locker" para las operaciones de cifrado).

Para operaciones específicas (como el mecanismo de cifrado), esto utiliza diferentes subprocesos simultáneos manejados por la API de Windows CreateIoCompletitionPort y diferentes colas que son manejadas por GetQueuedCompletitionStatus y PostQueuedCompletitionStatus.

La función WinMain (main.cpp) comienza con la resolución dinámica de la API LoadLibraryA a través de una inspección manual del kernel32.dll importado (una implementación manual de la API GetProcAddress).

Después de eso, se invoca el módulo “API” para ejecutar una técnica anti-DBI/anti-sandbox con el fin de deshabilitar todos los posibles hooks en las DLL conocidas. De hecho, las siguientes DLL se cargan a través de la API LoadLibraryA recién resuelta:

  • kernel32.dll
  • ws2_32.dll
  • advapi32.dll
  • ntdll.dll
  • rstrtmgr.dll
  • ole32.dll
  • oleaut32.dll
  • netapi32.dll
  • iphlpapi.dll
  • shlwapi.dll
  • shell32.dll


Para cada archivo DLL cargado, CreateFileMappingW y MapViewOfFile se invocan para acceder a la vista asignada en el espacio de direcciones del proceso de llamada. 


Esta vista se utiliza para acceder manualmente al encabezado NT y al directorio de exportación interno. Del directorio de exportación se extrae cada dirección de las funciones exportadas y se comprueban los primeros bytes de la función exportada para identificar una posible instrucción JMP/NOP/RET que identifique un hook externo.

Si la función actual está hookeada, VirtualProtect y RtlCopyMemory API se invocan para sobrescribir los primeros bytes de la función hookeada. Al continuar con la ejecución de WinMain, se crea un mutex llamado "kjsidugidf99439" para verificar posibles ejecuciones simultáneas del mismo payload. Si otro subproceso tiene la propiedad del mutex, la ejecución termina aquí.


Después de eso, los argumentos de las líneas de comando se verifican desde la API GetCommandLineW.

Este ransomware acepta los siguientes argumentos de la línea de comandos:

    -h: especifica un archivo que contiene el IPv4 de los hosts para escanear en busca de cifrado de redes/comparticiones (separados por \n\r);
    -p: especifica un archivo que contiene la ruta del sistema para buscar el cifrado de archivos (separados por \n\r);
    -m: especifica el modo de cifrado
    “all”: encripta archivos locales y de red
    “local”: encripta solo archivos locales
    “net”: encripta solo archivos de red
    "backups": no implementado
    -log: si contiene el valor "habilitado", registra las acciones/errores del ransomware en el archivo local C:\\CONTI_LOG.txt

Luego, se invoca la API GetNativeSystemInfo para extraer la cantidad de procesadores y el módulo "threadpool" se usa para instanciar number_of_processors * 2 hilos (tanto para el cifrado local como de red, según los indicadores especificados).

Cada subproceso asigna su propio búfer para el próximo cifrado e inicializa su propio contexto de criptografía a través de la API CryptAcquireContextA y una clave pública RSA.

Luego, cada subproceso espera en un bucle infinito una tarea en la cola TaskList (compartida por cada subproceso y a la que accede la API EnterCriticalSection). En caso de que haya una nueva tarea disponible, el nombre de archivo para cifrar se extrae de la tarea y, si el nombre de archivo corresponde a “stopmarker”, se concluye la ejecución del hilo.

En cualquier otro caso, se invoca el módulo "locker" para cifrar el archivo actual.

La rutina de cifrado para un archivo específico comienza con una generación de clave aleatoria (utilizando la API CryptGetRandom) de una clave de 32 bytes y otra generación aleatoria de un IV de 8 bytes.

Posteriormente, la clave aleatoria y el IV aleatorio se almacenan en una estructura FIleInfo personalizada y la clave aleatoria se cifra utilizando la clave RSA previamente decodificada.

Antes de la fase de cifrado, si se carga la DLL del administrador de reinicio (rstrtmgr.dll), se invocan las API RmStartSession, RmGetList y RmShutdown para finalizar cada aplicación que utiliza este recurso específico o tiene un identificador abierto en ese recurso.

Luego, según la extensión del archivo, el contenido del archivo se cifra total o parcialmente (cifrado al 20 %). En particular, se invoca el método CheckForDataBases para verificar un posible cifrado completo contra las siguientes extensiones:

    .4dd, .4dl, .accdb, .accdc, .accde, .accdr, .accdt, .accft, .adb, .ade, .adf, .adp, .arc, .ora, .alf, .ask, .btr , .bdf, .cat, .cdb, .ckp, .cma, .cpd, .dacpac, .dad, .dadiagrams, .daschema, .db, .db-shm, .db-wal, .db3, .dbc, .dbf, .dbs, .dbt, .dbv, .dbx, .dcb, .dct, .dcx, .ddl, .dlis, .dp1, .dqy, .dsk, .dsn, .dtsx, .dxl, .eco , .ecx, .edb, .epim, .exb, .fcd, .fdb, .fic, .fmp, .fmp12, .fmpsl, .fol, .fp3, .fp4, .fp5, .fp7, .fpt, . frm, .gdb, .grdb, .gwi, .hdb, .his, .ib, .idb, .ihx, .itdb, .itw, .jet, .jtx, .kdb, .kexi, .kexic, .kexis, .lgc, .lwx, .maf, .maq, .mar, .mas.mav, .mdb, .mdf, .mpd, .mrg, .mud, .mwb, .myd, .ndf, .nnt, .nrmlib, .ns2, .ns3, .ns4, .nsf, .nv, .nv2, .nwdb, .nyf, .odb, .ogy, .orx, .owc, .p96, .p97, .pan, .pdb, .p dm, .pnz, .qry, .qvd, .rbf, .rctd, .rod, .rodx, .rpd, .rsd, .sas7bdat, .sbf, .scx, .sdb, .sdc, .sdf, .sis, .spg, .sql, .sqlite, .sqlite3, .sqlitedb, .te, .temx, .tmd, .tps, .trc, .trm, .udb, .udl, .usr, .v12, .vis, .vpd , .vvv, .wdb, .wmdb, .wrk, .xdb, .xld, .xml ff, .abcddb, .abs, .abx, .accdw, .adn, .db2, .fm5, .hjt, .icg, .icr, .kdb, .lut, .maw, .mdn, .mdt


De lo contrario, se invoca el método CheckForVirtualMachines para verificar un posible cifrado parcial del 20% ((file_size / 100) * 7) contra las siguientes extensiones:

     vdi, .vhd, .vmdk, .pvm, .vmem, .vmsn, .vmsd, .nvram, .vmx, .raw, .qcow2, .subvol, .bin, .vsv, .avhd, .vmrs, .vhdx, .avdx, .vmcx, .iso


En otros casos, se sigue el siguiente patrón:

  • Si el tamaño del archivo es inferior a 1,04 GB: realiza un cifrado completo.
  • Si el tamaño del archivo está entre 1,04 GB y 5,24 GB: realiza un cifrado de encabezado (cifra solo los primeros 1048576 bytes).
  • De lo contrario, realiza un cifrado parcial del 50 % ((file_size / 100) * 100).

Después de elegir el método de cifrado, los primeros bytes del contenido del archivo se sobrescriben (antes del cifrado) con la información sobre el modo de cifrado y la clave utilizada para el cifrado. Luego, el contenido del archivo se cifra utilizando la clave aleatoria previamente cifrada con RSA y la extensión del archivo se cambia a .EXTEN.

Ahora veamos cómo se invocan estos subprocesos desde los métodos de enumeración que regresan a la ejecución de WinMain.

En primer lugar, se utiliza un bypass de COM para eliminar las shadow copies desde el Instrumental de administración de Windows (WMI).

En detalle:

  • El objeto COM se inicializa a través de la API CoInitializeEx.
  • Los niveles de seguridad COM se cambian a través de la API CoInitializeSecurity y el parámetro cAuthSvc es igual a -1 para deshabilitar la autenticación.
  • La API de CoCreateInstance se utiliza para ubicar el WMI a través del CLSID "CLSID_WbemLocator".
  • Se accede a WMI y WQL (WMI Query Language) a través del método IWbemLocator::ConnectServer.
  • Los niveles de seguridad del proxy WMI se cambian a través de la API CoSetProxyBlanket para establecer el flag RPC_C_AUTHZ_NONE y evitar la autenticación.
  • Se invoca la consulta "SELECT * FROM Win32_ShadowCopy" para identificar los ID de las shadow copies y se utiliza una ejecución de línea de comandos para eliminar cada shadow copy “cmd.exe /c C:\\Windows\\System32\\wbem\\WMIC.exe shadowcopy where \”ID=’%s’\” delete

Finalmente, comienza el proceso de enumeración. En primer lugar, se iteran las rutas del sistema de archivos especificadas a través del flag -p y, para cada ruta, la nota de ransomware (R3ADM3.txt, no disponible en esta versión leakeada) se coloca en el directorio especificado. Después de eso, las API FindFirstFileW y FindNextFileW se usan para iterar dentro de cada directorio ignorando los archivos especiales (como “.” o “..”).

El malware utiliza una lista blanca tanto para directorios como para archivos para evitar el cifrado de datos innecesarios. Los siguientes nombres de directorios y archivos se evitan durante el proceso de enumeración:

  • Directorios: “tmp”, “winnt”, “temp”, “thumb”, “$Recycle.Bin”, “$RECYCLE.BIN”, “Información del volumen del sistema”, “Arranque”, “Windows”, “Trend Micro”
  • Archivos: “.exe”, “.dll”, “.lnk”, “.sys”, “.msi”, “R3ADM3.txt”, “CONTI_LOG.txt”

Si el archivo a cifrar es un directorio, el proceso descrito se repite recursivamente para todos los subdirectorios y subarchivos. Finalmente, el archivo para cifrar se pasa al primer subproceso disponible para el proceso de cifrado que llena la cola de la lista de tareas. La siguiente enumeración inspecciona todas las unidades lógicas del sistema infectado.

De hecho, además de las rutas especificadas por el indicador -p, la API GetLogicalDriveStringsW se utiliza para obtener la lista de unidades. Luego, para cada unidad lógica, se extrae la ruta raíz y se repite el proceso anterior para cada subdirectorio y subarchivo.

    

El último proceso de enumeración se utiliza para enumerar los recursos compartidos del sistema Windows infectado. De hecho, la API de NetShareEnum se utiliza para recuperar información sobre cada recurso compartido. Para cada recurso, si el recurso representa una unidad de disco, un recurso compartido especial (p. ej., comunicaciones $IPC, administraciones remotas ADMIN$, recursos compartidos administrativos) o un recurso compartido temporal, se extrae la ruta del recurso compartido (p. ej., \\\\$IP\\$SHARE_NAME).

Luego, cada ruta compartida se utiliza como directorio para el proceso de cifrado de directorios y archivos descrito anteriormente.


Además de la enumeración de recursos compartidos o shares, este ransomware presenta un componente de subprocesos múltiples para buscar otras IP en las redes accesibles para un cifrado en clave de movimiento lateral. En particular, se invocan las API WSAStartup y WSAIoctl para obtener un controlador para LPFN_CONNECTEX para enlaces y conexiones de bajo nivel.

Luego, se invoca la API GetIpNetTable para recuperar la tabla ARP del sistema infectado. Para cada entrada de la tabla ARP, las direcciones IPv4 especificadas se comparan con las siguientes máscaras:

  • 172.*
  • 192.168.*
  • 10.*
  • 169.*

Si el ARP IPv4 actual respeta una de estas máscaras, la subred IP se extrae y se agrega a la cola de una subred. A partir de esta enumeración, se crean dos subprocesos simultáneos. El primer subproceso es responsable del escaneo de subredes: para cada dirección posible (de .0 a .255) en cada subred extraída, el malware intenta una conexión en esa IP en el puerto SMB (445) usando el protocolo TCP. Por cada conexión exitosa, este primer hilo guarda las IP válidas en una cola y repite el escaneo cada 30 segundos.

El segundo hilo espera alguna IP válida en la cola de IP y para cada IP enumera los recursos compartidos utilizando la API de NetShareEnum repitiendo el proceso descrito para la enumeración de recursos compartidos. El hexadecimal 0xFFFFFFFF se usa como la última dirección IP en la cola para eliminar ambos subprocesos y concluir la segunda y última parte de la enumeración de la red.



Al concluir la ejecución del ransomware, se invoca la API WaitForSingleObject en cada subproceso para esperar la finalización de las operaciones de encriptación y enumeración antes de cerrar el proceso principal.


Fuentes:

Comentarios