Executive Summary

This article details our technical analysis of VVS stealer, also styled VVS $tealer, including its distributors’ use of obfuscation and detection evasion.

The stealer is written in Python and targets Discord users, exfiltrating sensitive information like credentials and tokens stored in Discord accounts. This stealer was once in active development and marketed for sale on Telegram as early as April 2025.

VVS stealer's code is obfuscated by Pyarmor. This tool is used to obfuscate Python scripts to hinder static analysis and signature-based detection. Pyarmor can be used for legitimate purposes and also leveraged to build stealthy malware.

Malware authors are increasingly leveraging advanced obfuscation techniques to evade detection by cybersecurity tools, making their malicious software harder to analyze and reverse-engineer. This article shows how we deobfuscated VVS stealer samples to better understand its operations.

Because Python is easy for malware authors to use and the complex obfuscation used by this threat, the result is a highly effective and stealthy malware family.

Palo Alto Networks customer are better protected through the following products and services:

If you think you might have been compromised or have an urgent matter, contact the Unit 42 Incident Response team.

Related Unit 42 Topics Infostealer, Anti-analysis, Discord, Pyarmor

Introduction

Discord is a social messaging and communications platform that has become a popular target for malware, like VVS stealer. VVS stealer is designed to steal a victim's Discord information and browser data.

Figure 1 shows VVS stealer's advertised capabilities, including:

  • Stealing Discord data (tokens and account information)
  • Intercepting active Discord sessions via injection
  • Extracting web browser data (cookies, passwords, browsing history and autofill details)
Screenshot collage of a computer screen displaying information about "VS Stealer on Telegram," talking about its use as a hacking tool, with features listed and pricing details. Also visible is a Telegram contact link for further communication.
Figure 1. VVS stealer advertisement, focused on Telegram.

The stealer also achieves persistence by automatically installing itself on startup. It operates stealthily by displaying fake error messages and capturing screenshots. For a deeper investigation into the operation, please refer to the article by DeepCode, Investigating VVS $tealer: A Python-Based Discord Malware.

Technical Analysis

This section analyzes a Pyarmor-protected VVS stealer malware sample with the following SHA-256 hash:

  • c7e6591e5e021daa30f949a6f6e0699ef2935d2d7c06ea006e3b201c52666e07

Figure 2 shows a summary diagram illustrating the entire sample analysis workflow.

Flowchart showing the process of extracting Python bytecode from a PyInstaller executable, decompiling it to Python source code, and decrypting Pyarmor bytecode to ELF.
Figure 2. Overview of the workflow for analyzing the VVS stealer malware sample.

Step One: Extracting From the PyInstaller Binary

The sample we analyzed is distributed as a PyInstaller package. PyInstaller is a tool that bundles a Python application and its dependencies into a package to allow execution of a packaged app without installing additional modules.

Any standard PyInstaller installation ships with the built-in utility pyi-archive_viewer. We used this utility to extract and inspect the following files from our sample:

  • The Python bytecode file named vvs
  • The Pyarmor runtime dynamic-link library (DLL) file named pyarmor_runtime.pyd, located under subfolder pyarmor_runtime_007444
    • The accompanying __init__.py file within this same subfolder, which includes the following information:
      • Pyarmor version: 9.1.4 (Pro)
      • Unique license number: 007444
      • Timestamp: 2025-04-27T11:04:52.523525
      • Product name: vvs
  • Python 3.11 DLL file named python311.dll
    • The file version information indicates the Python version is 3.11.5

PyInstaller stores Python bytecode (listed as 1.) in its raw form. This raw form refers to the bytecode sequence beginning with the value e3. The value e3 is a combination of both flag and type, combined via the constant FLAG_REF.

The type represented by the value e3 is computed as: type = e3 & ~FLAG_REF. This means the value e3 is actually the type 0x63 (the letter c), also known as the enumeration constant TYPE_CODE. The full implementation of this derivation can be found in the CPython 3.11 codebase.

Figure 3 below shows this code object serialized by the marshal module is bare, missing an accompanying 16-byte header (marked in blue). To provide enough Python for the decompiler not to reject the file, we need to restore at least one of the header values (Python 3.11.5 magic number in 4-byte, little-endian format) prior to decompilation, because the Python decompiler expects a valid Python bytecode (.pyc) file as its input.

Hexadecimal data visualization showing rows of hex codes with some values highlighted.
Figure 3. Python bytecode (.pyc) file named vvs, with its header restored.

We begin our analysis by decompiling the Python bytecode .pyc) file named vvs to recover its equivalent Python source code (.py).

Step Two: Decompiling to Python Source Code

Pycdc is a Python bytecode decompiler written in C++. It is part of the Decompyle++ project. It supports decompiling Python 3.11 bytecode “back into valid and human-readable Python source code.” (Source: GitHub.) PyLingual is another Python bytecode decompiler.

After cloning the code repository and compiling the codebase, the generated executable can be invoked as follows to decompile Python bytecode to Python source code via Pycdc:

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

This will produce the decompiled Python source code shown in Figure 4.

Screenshot of a line of Python code involving an import statement from a library named "pyarmor," with obscured additional text.
Figure 4. Decompiled vvs Python source code.

We then analyze the last function argument, which can be extracted via Python 3's ast.NodeVisitor.

Step Three: Unraveling Pyarmor Obfuscation

The payload begins with the Pyarmor header shown in Figure 5.

A screenshot displaying a section of a hexadecimal code with ASCII characters on the right side, including a visible string "PY00744...
Figure 5. Pyarmor header, with particular fields of interest highlighted.

Cryptography is performed throughout using the Advanced Encryption Standard (AES) algorithm with a 128-bit key, operating in Counter (CTR) mode with an initial value of two (i.e., AES-128-CTR). Table 1 shows the breakdown of the fields.

Offsets Values Description
0x00 … 0x07 PY007444 File signature containing the unique license number
0x09 03 Python major version
0x0a 0b Python minor version
0x14 09 Protection type:

  • 09 if Pyarmor BCC mode (briefly explained in the next section) is enabled
  • 08 otherwise
0x1c … 0x1f 40 00 00 00 Start of the ELF payload, in little-endian format
0x24 … 0x27 12 c9 06 00 First four bytes of the AES-128-CTR nonce
0x2c … 0x33 dc d2 98 a1 ea 11 fd f4 Remaining eight bytes of the AES-128-CTR nonce
0x38 … 0x3b a0 7f 02 00 End of the ELF payload, in little-endian format

Table 1. Breakdown of fields present in the Pyarmor header.

This same pattern (highlighted in yellow) repeats itself once again after the end of the ELF payload, for extracting and decrypting the Pyarmor bytecode payload.

BCC Mode

BCC (likely an abbreviation of ByteCode-to-Compilation) mode converts most “functions and methods in the scripts to equivalent C functions. Those C functions will be compiled to machine instructions directly, then called by obfuscated scripts.” (Source: Pyarmor documentation.)

BCC mode is invoked as follows: pyarmor gen --enable-bcc script.py.

These converted C functions are stored in a separate ELF file, produced alongside the Pyarmor-marshaled bytecode.

The mapping of Python constants to BCC functions can be obtained using this implementation. For instance, in the Python method get_encryption_key(browser_path), the constant __pyarmor_bcc_58580__ maps to the BCC function bcc_180, whose function body is located at offset 0x4e70 of the ELF file.

Referencing this analysis of the ELF file contents, especially the bcc_ftable structure, Figure 6 shows part of the BCC function bcc_180 decompiled:

Screenshot depicting two side-by-side images of complex Python code examples on a computer screen.
Figure 6. Decompilation of the BCC function bcc_180.

We can roughly recover an equivalent of the original code of the Python method get_encryption_key, as shown in Figure 7.

Screenshot of Python code in a text editor, showing a function to retrieve the decryption key for Chromium browsers with highlighted syntax.
Figure 7. Equivalent Python code of the get_encryption_key method.

Marshaled Bytecode Format

Pyarmor 9 marshaled bytecode differs from standard Python 3.11 bytecode in several ways. Firstly, the 0x20000000 bit is set in the co_flags field to indicate that it is Pyarmor obfuscated. Secondly, there is an extra data field, whose length is denoted by the value of its first byte.

Moreover, deopt_code() needs to be disabled for the bytecode sequence to be successfully decrypted. We will discuss the cryptographic parameters in a later section of this article.

Code Object Structure

Pyarmor code objects are specially crafted, in that they should contain certain artifacts. It is common to expect to find the LOAD_CONST __pyarmor_enter_*__ instruction in the preamble and the LOAD_CONST __pyarmor_exit_*__ instruction in the trailer of the disassembly. These two instructions would wrap the encrypted bytecode, as shown in Table 2.

Operation Argument
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
… encrypted bytecode sequence (to be examined in the next section) …
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

Table 2. Pyarmor-related instructions in the disassembly listing of <module>.

Once the encrypted bytecode sequence is decrypted, it could reveal encrypted strings or BCC function invocations. Encrypted strings (reviewed in a later section of this article) are preceded by a LOAD_CONST __pyarmor_assert_*__ instruction. There is also the LOAD_CONST __pyarmor_bcc_*__ instruction to invoke a BCC function (reviewed earlier in this article).

Code Object Encryption

Bytecode sequences between the start marker (__pyarmor_enter_*__) and the end marker (__pyarmor_exit_*__) are AES-128-CTR encrypted. The associated AES key (273b1b1373cf25e054a61e2cb8a947b8) is extracted from the Pyarmor runtime DLL linked to the unique license number.

On the other hand, the corresponding AES nonce exclusive OR (XOR) key (2db99d18a0763ed70bbd6b3c) is only specific to the Pyarmor bytecode payload, for which there is an implementation of the logic for extracting this value. This key is XORed with the 12 bytes at the end marker (__pyarmor_exit_*__) to produce the correct AES nonce used in the decryption.

String Encryption

Similarly, string constants longer than eight characters are AES-128-CTR encrypted (known as "mixed" in Pyarmor terminology”). The associated AES key is also 273b1b1373cf25e054a61e2cb8a947b8, but this time, the corresponding AES nonce (692e767673e95c45a1e6876d) is computed from the Pyarmor runtime DLL linked to the unique license number.

Additionally, a 0x81 prefix value denotes that the string constant is encrypted. Otherwise, a 0x01 prefix value is used instead.

Now that the Pyarmor protection is disarmed, we shall proceed to cover some of the key capabilities of the VVS stealer in the next section.

Malware Capabilities

With the layers of Pyarmor obfuscation — including the BCC mode and AES-128-CTR string encryption — successfully stripped away, we were able to expose the underlying Python logic. This deobfuscated code revealed a stealer designed not just for data exfiltration, but for active session hijacking and persistence. The following section details the specific operational capabilities of the VVS stealer that were uncovered during this analysis.

The malware sample expires after 2026-10-31 23:59:59. It will stop working by terminating itself prematurely.

The malware sample performs all HTTP requests by sending the fixed User-Agent string Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36.

We shall now provide an overview of the main malware capabilities, as advertised on Telegram.

Discord Data

The malware sample first searches for potential encrypted Discord tokens. Encrypted Discord tokens are strings beginning with the prefix dQw4w9WgXcQ:. The malware sample uses regular expressions to form a pattern from this string prefix. It then uses this pattern to search inside the contents of files with the .ldb or .log file extensions, stored within the LevelDB directory.

Next, the malware sample decrypts the encrypted_key value in the Local State file, via the Data Protection Application Programming Interface (DPAPI). With this decrypted encrypted_key value as the AES key parameter, the malware sample applies the AES algorithm, operating in Galois/Counter Mode (GCM) mode, on the encrypted Discord tokens, to decrypt them.

The malware sample then uses the decrypted Discord tokens to query various Discord application programming interface (API) endpoints for user information, including:

  • Nitro subscription (Discord Premium features)
  • Payment methods
  • User ID
  • Username
  • Email
  • Phone number
  • Friends
  • Guilds
  • Multifactor authentication (MFA) status
  • Locale
  • Verification status
  • Avatar image
  • IP address (via the ipify service)
  • Computer name

After gathering all this information, the malware sample proceeds to exfiltrate it in JavaScript Object Notation (JSON) format. The exfiltration takes place via HTTP POST requests to the predefined webhook endpoints (%WEBHOOK% environment variable and hard-coded fall back URLs).

Webhooks are “a low-effort way to post messages to channels in Discord. They do not require a bot user or authentication to use.” (Source: Discord Developer Portal.)

Discord Injection

The code responsible for this functionality is in class Inj, likely an abbreviation of Injection.

In this class, the malware sample first kills running Discord application processes, if any are running. It then downloads the JavaScript (JS) payload from a remote file named injection-obf.js (the -obf suffix likely stands for an obfuscated version of the script), replacing the webhook endpoint URL and discord_desktop_core, into the Discord application directory. This JS file is obfuscated by the JavaScript Obfuscator Tool and can be deobfuscated via the Obfuscator.io Deobfuscator.

Some of the main functionality of the injected JS code is highlighted in the following screenshots, starting with its configuration and exfiltration code snippets, shown in Figure 8.

Screenshot of a JavaScript configuration file involving URLs and paths related to a Discord API and a remote authorization gateway. The code is displayed in a text editor with syntax highlighting.
Figure 8. Injected JS configuration and exfiltration.

Figure 8 shows the injected JS code responsible for establishing persistence in the Discord application, based on the Electron framework. This framework uses Atom Shell Archive Format (ASAR) archives to bundle the entire application's codebase into a single file, shown in Figure 9.

Screenshot of a code snippet related to a software initialization function, mentioning paths and configuration for "app.js", "index.js", and "discord.js". The code is written in JavaScript.
Figure 9. Injected JS code to perform persistence.

Figure 10 shows the injected JS code responsible for monitoring network traffic via the Chrome DevTools Protocol (CDP).

Screenshot of software code in an editor, displaying a network-related JavaScript function.
Figure 10. Injected JS code to monitor network traffic.

Figure 11 shows supporting utility functions and event hooks in the injected JS code. Event hooks are callback functions that execute upon the Discord application user performing a specific action. The actions of interest are when the user views their backup codes, changes their password or adds a payment method. The callback functions linked to these actions are capable of collecting Discord user account and billing information.

Screenshot of a computer code editor displaying multiple lines of JavaScript code, involving functions related to user data handling and API requests.
Figure 11. Injected JS code of utility functions and event hooks.

Thereafter, the malware sample restarts a compromised Discord application process via Update.exe, which it does with the command-line switch --processStart.

Web Browser Data

The malware sample targets a list of web browser applications, including:

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

To these targets, the malware sample extracts the following data, where present:

  • Autofill
  • Cookies
  • History
  • Passwords

Once these data are extracted, the malware sample prepares it for exfiltration by compressing it into a single ZIP archive file named <USERNAME>_vault.zip. It then exfiltrates this file via HTTP POST requests to the predefined webhook endpoints, similar to the Discord data exfiltration process.

Startup Persistence

The malware sample copies itself to the %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup folder to achieve startup persistence. The malware remains on the user’s device, enabling it to continue exfiltrating data if, for example, the user attempts to install a fresh copy of the Discord application.

Fake Error

The malware sample uses the Win32 API, specifically the MessageBoxW function in the User32.dll library, to display a modal message box about a fake fatal error that requires restarting the computer. A modal message box is a small dialog window requiring user interaction before the application can continue, as shown in Figure 12.

Error message dialog box displaying "Fatal Error" with error code 0x80070002 and a suggestion to restart the computer. An "OK" button is present for acknowledgement.
Figure 12. A fake message box instructing the victim to restart the computer.

Conclusion

VVS stealer demonstrates how tools like Pyarmor, which can be used for legitimate purposes, can also be leveraged to build stealthy malware aimed at hijacking credentials for popular platforms such as Discord. Its emergence signals a need for defenders to strengthen monitoring around credential theft and account abuse.

Palo Alto Networks Protection and Mitigation

Palo Alto Networks customers are better protected from the threats discussed above through the following products:

The Advanced WildFire machine-learning models and analysis techniques have been reviewed and updated in light of the indicators shared in this research.

Advanced URL Filtering and Advanced DNS Security identify known domains and URLs associated with this activity as malicious.

Cortex XDR and XSIAM prevents the threats described in this article by employing the Malware Prevention Engine. This approach combines several layers of protection, including Advanced WildFire, Behavioral Threat Protection and the Local Analysis module, to prevent both known and unknown malware from causing harm to endpoints.

If you think you may have been compromised or have an urgent matter, get in touch with the Unit 42 Incident Response team or call:

  • North America: Toll Free: +1 (866) 486-4842 (866.4.UNIT42)
  • UK: +44.20.3743.3660
  • Europe and Middle East: +31.20.299.3130
  • Asia: +65.6983.8730
  • Japan: +81.50.1790.0200
  • Australia: +61.2.4062.7950
  • India: 000 800 050 45107
  • South Korea: +82.080.467.8774

Palo Alto Networks has shared these findings with our fellow Cyber Threat Alliance (CTA) members. CTA members use this intelligence to rapidly deploy protections to their customers and to systematically disrupt malicious cyber actors. Learn more about the Cyber Threat Alliance.

Indicators of Compromise

SHA-256 hashes of malware samples:

  • 307d9cefa7a3147eb78c69eded273e47c08df44c2004f839548963268d19dd87
  • 7a1554383345f31f3482ba3729c1126af7c1d9376abb07ad3ee189660c166a2b
  • c7e6591e5e021daa30f949a6f6e0699ef2935d2d7c06ea006e3b201c52666e07

Discord webhook URLs

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

Additional Resources

Enlarged Image