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.
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:

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.
This section analyzes a Pyarmor-protected VVS stealer malware sample with the following SHA-256 hash:
Figure 2 shows a summary diagram illustrating the entire sample analysis workflow.

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:
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.

We begin our analysis by decompiling the Python bytecode .pyc) file named vvs to recover its equivalent Python source code (.py).
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:
This will produce the decompiled Python source code shown in Figure 4.

We then analyze the last function argument, which can be extracted via Python 3's ast.NodeVisitor.
The payload begins with the Pyarmor header shown in Figure 5.

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:
|
| 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 (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:

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

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.
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).
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.
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.
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.
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:
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.)
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.

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.

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

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.

Thereafter, the malware sample restarts a compromised Discord application process via Update.exe, which it does with the command-line switch --processStart.
The malware sample targets a list of web browser applications, including:
To these targets, the malware sample extracts the following data, where present:
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.
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.
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.

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 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:
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.
SHA-256 hashes of malware samples:
Discord webhook URLs