Resumen Ejecutivo

Este artículo detalla nuestro análisis técnico de VVS stealer, también estilizado como VVS stealer, incluyendo el uso de ofuscación y evasión de detección por parte de sus distribuidores.

El stealer está escrito en Python y tiene como objetivo a los usuarios de Discord, exfiltrando información sensible como credenciales y tokens almacenados en cuentas de Discord. Este stealer estuvo en desarrollo activo y se comercializó para su venta en Telegram desde abril de 2025.

El código de VVS stealer está ofuscado mediante Pyarmor. Esta herramienta se utiliza para ofuscar scripts de Python con el fin de dificultar el análisis estático y la detección basada en firmas. Pyarmor puede usarse con fines legítimos y también aprovecharse para crear malware sigiloso.

Los autores de malware están aprovechando cada vez más técnicas de ofuscación avanzadas para evadir la detección de las herramientas de ciberseguridad, haciendo que su software malicioso sea más difícil de analizar y aplicar ingeniería inversa. Este artículo muestra cómo desofuscamos las muestras de VVS stealer para comprender mejor sus operaciones.

Debido a que Python es fácil de usar para los autores de malware y a la compleja ofuscación utilizada por esta amenaza, el resultado es una familia de malware altamente efectiva y sigilosa.

Los clientes de Palo Alto Networks están mejor protegidos a través de los siguientes productos y servicios:

Si cree que podría haber sido comprometido o tiene un asunto urgente, comuníquese con el equipo de Respuesta a Incidentes de Unit 42.

Temas relacionados de Unit 42 Infostealer, Anti-analysis, Discord, Pyarmor

Introducción

Discord es una plataforma de mensajería social y comunicaciones que se ha convertido en un objetivo popular para el malware, como VVS stealer. VVS stealer está diseñado para robar la información de Discord y los datos del navegador de la víctima.

La Figura 1 muestra las capacidades anunciadas de VVS stealer, que incluyen:

  • Robo de datos de Discord (tokens e información de la cuenta)
  • Intercepción de sesiones activas de Discord mediante inyección
  • Extracción de datos del navegador web (cookies, contraseñas, historial de navegación y detalles de autocompletado)
Collage de capturas de pantalla de una computadora que muestra información sobre "VS Stealer en Telegram", describiendo su uso como herramienta de hacking, con una lista de características y detalles de precios. También se puede ver un enlace de contacto de Telegram para facilitar la comunicación.
Figura 1. Anuncio de VVS stealer, enfocado en Telegram.

El stealer también logra persistencia instalándose automáticamente al inicio. Opera sigilosamente mostrando mensajes de error falsos y capturando capturas de pantalla. Para una investigación más profunda sobre la operación, consulte el artículo de DeepCode, Investigating VVS $tealer: A Python-Based Discord Malware.

Análisis Técnico

Esta sección analiza una muestra del malware VVS stealer protegida por Pyarmor con el siguiente hash SHA-256:

  • c7e6591e5e021daa30f949a6f6e0699ef2935d2d7c06ea006e3b201c52666e07

La Figura 2 muestra un diagrama resumen que ilustra el flujo de trabajo completo del análisis de la muestra.

Diagrama de flujo que muestra el proceso de extracción de bytecode de Python de un ejecutable de PyInstaller, su descompilación a código fuente de Python y el descifrado de bytecode de Pyarmor a ELF.
Figura 2. Descripción general del flujo de trabajo para analizar la muestra de malware VVS stealer.

Paso Uno: Extracción desde el binario de PyInstaller

La muestra que analizamos se distribuye como un paquete de PyInstaller. PyInstaller es una herramienta que empaqueta una aplicación Python y sus dependencias en un paquete para permitir la ejecución de una aplicación empaquetada sin instalar módulos adicionales.

Cualquier instalación estándar de PyInstaller incluye la utilidad integrada pyi-archive_viewer. Utilizamos esta utilidad para extraer e inspeccionar los siguientes archivos de nuestra muestra:

  • El archivo de bytecode de Python llamado vvs
  • El archivo de biblioteca de vínculos dinámicos (DLL) del entorno de ejecución de Pyarmor llamado pyarmor_runtime.pyd, ubicado en la subcarpeta pyarmor_runtime_007444
    • El archivo __init__.py adjunto dentro de esta misma subcarpeta, que incluye la siguiente información:
      • Versión de Pyarmor: 9.1.4 (Pro)
      • Número de licencia único: 007444
      • Marca de tiempo: 2025-04-27T11:04:52.523525
      • Nombre del producto: vvs
  • Archivo DLL de Python 3.11 llamado python311.dll
    • La información de la versión del archivo indica que la versión de Python es 3.11.5

PyInstaller almacena el bytecode de Python (listado como 1.) en su forma cruda (raw). Esta forma cruda se refiere a la secuencia de bytecode que comienza con el valor e3. El valor e3 es una combinación tanto de bandera (flag) como de tipo (type), combinada a través de la constante FLAG_REF.

El tipo representado por el valor e3 se calcula como: type = e3 & ~FLAG_REF. Esto significa que el valor e3 es en realidad el tipo 0x63 (la letra c), también conocido como la constante de enumeración TYPE_CODE. La implementación completa de esta derivación se puede encontrar en el código base de CPython 3.11.

La Figura 3 a continuación muestra que este objeto de código serializado por el módulo marshal está desnudo (bare), le falta un encabezado acompañante de 16 bytes (marcado en azul). Para proporcionar suficiente contexto de Python y que el descompilador no rechace el archivo, necesitamos restaurar al menos uno de los valores del encabezado (el número mágico de Python 3.11.5 en formato little-endian de 4 bytes) antes de la descompilación, porque el descompilador de Python espera un archivo de bytecode de Python válido (.pyc) como entrada.

Visualización de datos hexadecimales que muestra filas de códigos hex con algunos valores resaltados
Figura 3. Archivo de bytecode de Python (.pyc) llamado vvs, con su encabezado restaurado.

Comenzamos nuestro análisis descompilando el archivo de bytecode de Python (.pyc) llamado vvs para recuperar su código fuente de Python equivalente (.py).

Paso Dos: Descompilación a código fuente de Python

Pycdc es un descompilador de bytecode de Python escrito en C++. Es parte del proyecto Decompyle++. Admite la descompilación de bytecode de Python 3.11 "de vuelta a código fuente de Python válido y legible por humanos". (Fuente: GitHub). PyLingual es otro descompilador de bytecode de Python.

Después de clonar el repositorio de código y compilar el código base, el ejecutable generado se puede invocar de la siguiente manera para descompilar el bytecode de Python a código fuente de Python a través de Pycdc:

  • pycdc.exe -c -v "3.11.5" "vvs.pyc" > "vvs.py"

Esto producirá el código fuente de Python descompilado que se muestra en la Figura 4.

Captura de pantalla de una línea de código Python que incluye una declaración de importación de una biblioteca llamada "pyarmor", con texto adicional oculto.
Figura 4. Código fuente de Python vvs descompilado.

Luego analizamos el último argumento de la función, que se puede extraer a través de ast.NodeVisitor de Python 3.

Paso Tres: Desentrañando la ofuscación de Pyarmor

La carga útil (payload) comienza con el encabezado de Pyarmor que se muestra en la Figura 5.

Una captura de pantalla que muestra una sección de código hexadecimal con caracteres ASCII en el lado derecho, incluida una cadena visible "PY00744...".
Figura 5. Encabezado de Pyarmor, con campos particulares de interés resaltados.

La criptografía se realiza en todo momento utilizando el algoritmo Estándar de Cifrado Avanzado (AES) con una clave de 128 bits, operando en modo Contador (CTR) con un valor inicial de dos (es decir, AES-128-CTR). La Tabla 1 muestra el desglose de los campos.

Desplazamientos (Offsets) Valores Descripción
0x00 … 0x07 PY007444 Firma del archivo que contiene el número de licencia única
0x09 03 Versión mayor de Python
0x0a 0b Versión menor de Python
0x14 09 Tipo de protección:

  • 09 si el modo BCC de Pyarmor (explicado brevemente en la siguiente sección) está habilitado
  • 08 en caso contrario
0x1c … 0x1f 40 00 00 00 Inicio del payload ELF, en formato little-endian
0x24 … 0x27 12 c9 06 00 Primeros cuatro bytes del nonce AES-128-CTR
0x2c … 0x33 dc d2 98 a1 ea 11 fd f4 Ocho bytes restantes del nonce AES-128-CTR
0x38 … 0x3b a0 7f 02 00 Fin del payload ELF, en formato little-endian

Tabla 1. Desglose de campos presentes en el encabezado de Pyarmor.

Este mismo patrón (resaltado en amarillo) se repite una vez más después del final del payload ELF, para extraer y descifrar el payload de bytecode de Pyarmor.

Modo BCC

El modo BCC (probablemente una abreviatura de ByteCode-to-Compilation) convierte la mayoría de las "funciones y métodos en los scripts a funciones C equivalentes. Esas funciones C se compilarán directamente a instrucciones de máquina, luego serán llamadas por los scripts ofuscados." (Fuente: Documentación de Pyarmor).

El modo BCC se invoca de la siguiente manera: pyarmor gen --enable-bcc script.py.

Estas funciones C convertidas se almacenan en un archivo ELF separado, producido junto con el bytecode serializado (marshaled) por Pyarmor.

El mapeo de constantes de Python a funciones BCC se puede obtener utilizando esta implementación. Por ejemplo, en el método de Python get_encryption_key(browser_path), la constante __pyarmor_bcc_58580__ se asigna a la función BCC bcc_180, cuyo cuerpo de función se encuentra en el desplazamiento 0x4e70 del archivo ELF.

Haciendo referencia a este análisis del contenido del archivo ELF, especialmente la estructura bcc_ftable, la Figura 6 muestra parte de la función BCC bcc_180 descompilada:

Captura de pantalla que muestra dos imágenes lado a lado de ejemplos complejos de código Python en una pantalla de computadora.
Figura 6. Descompilación de la función BCC bcc_180.

Podemos recuperar aproximadamente un equivalente del código original del método de Python get_encryption_key, como se muestra en la Figura 7.

Captura de pantalla de código Python en un editor de texto, que muestra una función para recuperar la clave de descifrado para navegadores Chromium con sintaxis resaltada.
Figura 7. Código Python equivalente del método get_encryption_key.

Formato de Bytecode Serializado (Marshaled)

El bytecode serializado de Pyarmor 9 difiere del bytecode estándar de Python 3.11 en varios aspectos. En primer lugar, el bit 0x20000000 se establece en el campo co_flags para indicar que está ofuscado por Pyarmor. En segundo lugar, hay un campo de datos adicional, cuya longitud se denota por el valor de su primer byte.

Además, deopt_code() debe estar deshabilitado para que la secuencia de bytecode se descifre con éxito. Discutiremos los parámetros criptográficos en una sección posterior de este artículo.

Estructura del Objeto de Código

Los objetos de código de Pyarmor están diseñados especialmente, en el sentido de que deben contener ciertos artefactos. Es común esperar encontrar la instrucción LOAD_CONST __pyarmor_enter_*__ en el preámbulo y la instrucción LOAD_CONST __pyarmor_exit_*__ en el cierre del desensamblado. Estas dos instrucciones envolverían el bytecode cifrado, como se muestra en la Tabla 2.

Operación Argumento
LOAD_CONST __pyarmor_enter_58592__
LOAD_CONST \x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x20\x16\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
… secuencia de bytecode cifrada (a examinar en la siguiente sección) …
LOAD_CONST __pyarmor_exit_58593__
LOAD_CONST \x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x20\x16\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00

Tabla 2. Instrucciones relacionadas con Pyarmor en el desensamblado de <module>.

Una vez descifrada la secuencia de bytecode cifrada, podría revelar cadenas cifradas o invocaciones de funciones BCC. Las cadenas cifradas (revisadas en una sección posterior de este artículo) están precedidas por una instrucción LOAD_CONST __pyarmor_assert_*__. También existe la instrucción LOAD_CONST __pyarmor_bcc_*__ para invocar una función BCC (revisada anteriormente en este artículo).

Cifrado del Objeto de Código

Las secuencias de bytecode entre el marcador de inicio (__pyarmor_enter_*__) y el marcador final (__pyarmor_exit_*__) están cifradas con AES-128-CTR. La clave AES asociada (273b1b1373cf25e054a61e2cb8a947b8) se extrae de la DLL del entorno de ejecución de Pyarmor vinculada al número de licencia único.

Por otro lado, la clave XOR del nonce AES correspondiente (2db99d18a0763ed70bbd6b3c) solo es específica para el payload de bytecode de Pyarmor, para lo cual hay una implementación de la lógica para extraer este valor. Esta clave se somete a XOR con los 12 bytes en el marcador final (__pyarmor_exit_*__) para producir el nonce AES correcto utilizado en el descifrado.

Cifrado de Cadenas

De manera similar, las constantes de cadena de más de ocho caracteres están cifradas con AES-128-CTR (conocido como "mixto" en la terminología de Pyarmor). La clave AES asociada también es 273b1b1373cf25e054a61e2cb8a947b8, pero esta vez, el nonce AES correspondiente (692e767673e95c45a1e6876d) se calcula desde la DLL del entorno de ejecución de Pyarmor vinculada al número de licencia único.

Además, un valor de prefijo 0x81 denota que la constante de cadena está cifrada. De lo contrario, se utiliza un valor de prefijo 0x01 en su lugar.

Ahora que la protección de Pyarmor está desarmada, procederemos a cubrir algunas de las capacidades clave de VVS stealer en la siguiente sección.

Capacidades del Malware

Con las capas de ofuscación de Pyarmor —incluyendo el modo BCC y el cifrado de cadenas AES-128-CTR— eliminadas con éxito, pudimos exponer la lógica de Python subyacente. Este código desofuscado reveló un stealer diseñado no solo para la exfiltración de datos, sino también para el secuestro activo de sesiones y la persistencia. La siguiente sección detalla las capacidades operativas específicas de VVS stealer que se descubrieron durante este análisis.

La muestra de malware expira después del 2026-10-31 23:59:59. Dejará de funcionar terminándose prematuramente.

La muestra de malware realiza todas las solicitudes HTTP enviando la cadena fija de User-Agent Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36.

Ahora proporcionaremos una descripción general de las principales capacidades del malware, tal como se anuncian en Telegram.

Datos de Discord

La muestra de malware primero busca posibles tokens de Discord cifrados. Los tokens de Discord cifrados son cadenas que comienzan con el prefijo dQw4w9WgXcQ:. La muestra de malware utiliza expresiones regulares para formar un patrón a partir de este prefijo de cadena. Luego utiliza este patrón para buscar dentro del contenido de archivos con las extensiones .ldb o .log, almacenados dentro del directorio LevelDB.

A continuación, la muestra de malware descifra el valor encrypted_key en el archivo Local State, a través de la Interfaz de Programación de Aplicaciones de Protección de Datos (DPAPI). Con este valor encrypted_key descifrado como parámetro de clave AES, la muestra de malware aplica el algoritmo AES, operando en modo Galois/Counter Mode (GCM), sobre los tokens de Discord cifrados para descifrarlos.

Luego, la muestra de malware utiliza los tokens de Discord descifrados para consultar varios puntos finales (endpoints) de la interfaz de programación de aplicaciones (API) de Discord para obtener información del usuario, incluyendo:

  • Suscripción Nitro (características Premium de Discord)
  • Métodos de pago
  • ID de usuario
  • Nombre de usuario
  • Correo electrónico
  • Número de teléfono
  • Amigos
  • Gremios (Guilds)
  • Estado de autenticación multifactor (MFA)
  • Configuración regional (Locale)
  • Estado de verificación
  • Imagen de avatar
  • Dirección IP (a través del servicio ipify)
  • Nombre de la computadora

Después de recopilar toda esta información, la muestra de malware procede a exfiltrarla en formato de Notación de Objetos de JavaScript (JSON). La exfiltración se realiza a través de solicitudes HTTP POST a los endpoints de webhook predefinidos (variable de entorno %WEBHOOK% y URL de respaldo codificadas).

Los webhooks son "una forma de bajo esfuerzo para publicar mensajes en canales de Discord. No requieren un usuario bot ni autenticación para usarse." (Fuente: Portal de Desarrolladores de Discord).

Inyección en Discord

El código responsable de esta funcionalidad está en la clase Inj, probablemente una abreviatura de Injection (Inyección).

En esta clase, la muestra de malware primero elimina los procesos de la aplicación Discord en ejecución, si hay alguno ejecutándose. Luego descarga el payload de JavaScript (JS) desde un archivo remoto llamado injection-obf.js (el sufijo -obf probablemente significa una versión ofuscada del script), reemplazando la URL del endpoint del webhook y discord_desktop_core, dentro del directorio de la aplicación Discord. Este archivo JS está ofuscado por la Herramienta de Ofuscación de JavaScript y puede ser desofuscado a través del Desofuscador de Obfuscator.io.

Algunas de las funcionalidades principales del código JS inyectado se destacan en las siguientes capturas de pantalla, comenzando con su configuración y fragmentos de código de exfiltración, que se muestran en la Figura 8.

Captura de pantalla de un archivo de configuración de JavaScript que incluye URL y rutas relacionadas con una API de Discord y una puerta de enlace de autorización remota. El código se muestra en un editor de texto con resaltado de sintaxis
Figura 8. Configuración y exfiltración del JS inyectado.

La Figura 8 muestra el código JS inyectado responsable de establecer persistencia en la aplicación Discord, basado en el framework Electron. Este framework utiliza archivos de formato Atom Shell Archive (ASAR) para agrupar todo el código base de la aplicación en un solo archivo, como se muestra en la Figura 9.

Captura de pantalla de un fragmento de código relacionado con una función de inicialización de software, que menciona rutas y configuración para "app.js", "index.js" y "discord.js". El código está escrito en JavaScript.
Figura 9. Código JS inyectado para realizar persistencia.

La Figura 10 muestra el código JS inyectado responsable de monitorear el tráfico de red a través del Protocolo de DevTools de Chrome (CDP).

Captura de pantalla de código de software en un editor, que muestra una función de JavaScript relacionada con la red.
Figura 10. Código JS inyectado para monitorear el tráfico de red.

La Figura 11 muestra funciones de utilidad de apoyo y hooks de eventos en el código JS inyectado. Los hooks de eventos son funciones de devolución de llamada (callbacks) que se ejecutan cuando el usuario de la aplicación Discord realiza una acción específica. Las acciones de interés son cuando el usuario ve sus códigos de respaldo, cambia su contraseña o agrega un método de pago. Las funciones callback vinculadas a estas acciones son capaces de recopilar la cuenta de usuario de Discord y la información de facturación.

Captura de pantalla de un editor de código informático que muestra varias líneas de código JavaScript, que incluye funciones relacionadas con el manejo de datos de usuario y solicitudes de API.
Figure 11. Injected JS code of utility functions and event hooks.

Posteriormente, la muestra de malware reinicia un proceso de aplicación Discord comprometido a través de Update.exe, lo cual hace con el modificador de línea de comandos --processStart.

Datos del Navegador Web

La muestra de malware ataca una lista de aplicaciones de navegador web, incluyendo:

  • Chrome
  • Edge
  • 7Star
  • Amigo
  • Brave
  • CentBrowser
  • Discord
  • Epic Privacy Browser
  • Iridium
  • Kometa
  • Lightcord
  • Mozilla Firefox
  • Opera
  • Orbitum
  • Sputnik
  • Torch
  • Uran
  • Vivaldi
  • Yandex

De estos objetivos, la muestra de malware extrae los siguientes datos, donde estén presentes:

  • Autocompletado
  • Cookies
  • Historial
  • Contraseñas

Una vez extraídos estos datos, la muestra de malware los prepara para la exfiltración comprimiéndolos en un solo archivo ZIP llamado <USERNAME>_vault.zip. Luego exfiltra este archivo a través de solicitudes HTTP POST a los endpoints de webhook predefinidos, similar al proceso de exfiltración de datos de Discord.

Persistencia en el Inicio

La muestra de malware se copia a sí misma en la carpeta %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup para lograr persistencia en el inicio. El malware permanece en el dispositivo del usuario, permitiéndole continuar exfiltrando datos si, por ejemplo, el usuario intenta instalar una copia nueva de la aplicación Discord.

Error Falso

La muestra de malware utiliza la API Win32, específicamente la función MessageBoxW en la biblioteca User32.dll, para mostrar un cuadro de mensaje modal sobre un error fatal falso que requiere reiniciar la computadora. Un cuadro de mensaje modal es una pequeña ventana de diálogo que requiere la interacción del usuario antes de que la aplicación pueda continuar, como se muestra en la Figura 12.

Cuadro de diálogo de mensaje de error que muestra "Fatal Error" con el código de error 0x80070002 y una sugerencia para reiniciar la computadora. Hay un botón "OK" presente para confirmar.
Figura 12. Un cuadro de mensaje falso que indica a la víctima que reinicie la computadora.

Conclusión

VVS stealer demuestra cómo herramientas como Pyarmor, que pueden usarse con fines legítimos, también pueden aprovecharse para crear malware sigiloso destinado a secuestrar credenciales para plataformas populares como Discord. Su aparición señala la necesidad de que los defensores fortalezcan el monitoreo en torno al robo de credenciales y el abuso de cuentas.

Protección y Mitigación de Palo Alto Networks

Los clientes de Palo Alto Networks están mejor protegidos contra las amenazas discutidas anteriormente a través de los siguientes productos:

Los modelos de aprendizaje automático y las técnicas de análisis de Advanced WildFire han sido revisados y actualizados a la luz de los indicadores compartidos en esta investigación.

Advanced URL Filtering y Advanced DNS Security identifican los dominios y URL conocidos asociados con esta actividad como maliciosos.

Cortex XDR y XSIAM previenen las amenazas descritas en este artículo empleando el Malware Prevention Engine. Este enfoque combina varias capas de protección, incluyendo Advanced WildFire, Protección contra Amenazas de Comportamiento (Behavioral Threat Protection) y el módulo de Análisis Local, para evitar que tanto el malware conocido como el desconocido causen daño a los endpoints.

Si cree que puede haber sido comprometido o tiene un asunto urgente, póngase en contacto con el equipo de Respuesta a Incidentes de Unit 42 o llame al:

  • Norteamérica: Llamada gratuita: +1 (866) 486-4842 (866.4.UNIT42)
  • Reino Unido: +44.20.3743.3660
  • Europa y Medio Oriente: +31.20.299.3130
  • Asia: +65.6983.8730
  • Japón: +81.50.1790.0200
  • Australia: +61.2.4062.7950
  • India: 000 800 050 45107
  • Corea del Sur: +82.080.467.8774

Palo Alto Networks ha compartido estos hallazgos con nuestros miembros compañeros de la Cyber Threat Alliance (CTA). Los miembros de la CTA utilizan esta inteligencia para implementar rápidamente protecciones para sus clientes y para interrumpir sistemáticamente a los actores cibernéticos maliciosos. Obtenga más información sobre la Cyber Threat Alliance.

Indicadores de Compromiso

Hashes SHA-256 de muestras de malware:

  • 307d9cefa7a3147eb78c69eded273e47c08df44c2004f839548963268d19dd87
  • 7a1554383345f31f3482ba3729c1126af7c1d9376abb07ad3ee189660c166a2b
  • c7e6591e5e021daa30f949a6f6e0699ef2935d2d7c06ea006e3b201c52666e07

URLs de webhook de Discord

  • hxxps[://]ptb.discord[.]com/api/webhooks/1360401843963826236/TkFvXfHFXrBIKT3EaqekJefvdvt39XTAxeOIWECeSrBbNLKDR5yPcn75uIqKEzdfs9o2
  • hxxps[://]ptb.discord[.]com/api/webhooks/1360259628440621087/YCo9eVnIBOYSMn8Xr6zX5C7AJF22z26WljaJk4zr6IiThnUrVyfWCZYs6JjSC12IC8c0

Recursos Adicionales

Enlarged Image