Zscaler Blog
Erhalten Sie die neuesten Zscaler Blog-Updates in Ihrem Posteingang
Vidar Adds Virtual Machine and Custom Stream Ciphers For String Obfuscation
Introduction
Vidar is an information stealer that was first observed in 2018. Across its iterations, Vidar has continued to improve its string obfuscation to make detection and analysis more difficult by changing deobfuscation algorithms, constants, and primitives. From May through early September 2026, Zscaler ThreatLabz tracked Vidar’s string obfuscation as it evolved from basic XOR to ChaCha20, and more recently, to a custom virtual machine (VM), which is executed via a lightweight bytecode interpreter that is combined with a custom stream cipher that changes with each build.
In this blog post, ThreatLabz covers Vidar’s string obfuscation methods from version 2.0 to the latest version 3.3.
Key Takeaways
- Vidar is an information stealer that was first observed in 2018 that initially obfuscated strings with XOR-based encryption.
- The Vidar developer later migrated to more advanced string encryption algorithms based on ChaCha20.
- In June 2026, ThreatLabz observed Vidar introduce a virtual machine to protect the malware’s strings with different opcodes that change per build.
- In addition to the virtual machine, a new custom stream cipher algorithm was added with operations that vary across builds.
- These per-build string obfuscation techniques are designed to hinder static and automated analysis.
Evolution Timeline
Earlier versions of Vidar used single-byte XOR operations to obfuscate strings. Beginning with internal version 1.5, the developer adopted ChaCha20. Starting in versions 1.8, the ChaCha20 cipher was modified to make detection and decryption harder. Starting with internal version 2.0, Vidar shifted to a different approach that combines a custom virtual machine and custom stream cipher per build. The figure below shows a timeline of these changes.

Figure 1: Evolution of Vidar’s string obfuscation from early May to early September 2026.
ANALYST NOTE: The version number used in this analysis refers to internal versions identified by ThreatLabz.
Technical Analysis
The following sections examine Vidar’s string deobfuscation process, including a lightweight virtual machine and custom stream cipher.
Virtual machine deobfuscation
In order to obfuscate the malware’s strings in versions 2.x and 3.x, the Vidar developer introduced a simple virtual machine that is executed via a bytecode interpreter. The bytecode interpreter consists of a fetch–decode–execute loop with the VM code provided as a byte array. Each opcode byte indexes a sparse 256-entry dispatch table. The matching handler mutates a one-byte accumulator and emits an output byte when a specific opcode is encountered. The dispatch table is initialized on first use and populated with 14 opcode handlers. All other slots in the dispatch table are null and terminate execution. The bytecode interpreter is relatively simple and does not implement a stack or use registers beyond an accumulator. Each handler uses simple primitives such as XOR, addition, subtraction, rotation, bitwise negation, multiplication, and substitution via a lookup table.
The interpreter uses a context structure that is passed to each handler and executes until the bytecode or the output buffer is exhausted as shown in the code below.
typedef struct bytecode_context {
uint8_t acc; // accumulator
uint8_t prev_out; // decoded byte
const uint8_t * code_ptr; // bytecode
uint32_t code_len; // bytecode_length
uint32_t index; // instruction_pointer
uint8_t* out_ptr; // output
uint32_t out_len; // output buffer size
uint32_t out_index; // output_position
uint32_t xor_key; // accumulator seed and used as an xor key
} bytecode_context;
void vidar_bytecode_intrepreter(
const uint8_t *input_bytes,
uint32_t input_len,
uint8_t *output_bytes,
uint32_t output_len)
{
bytecode_context ctx = {0};
init_vm_dispatch_table();
ctx.acc = get_seed_value();
ctx.prev_out = 0;
ctx.code_ptr = input_bytes;
ctx.code_len = input_len;
ctx.code_index = 0;
ctx.out_ptr = output_bytes;
ctx.out_len = output_len;
ctx.out_index = 0;
ctx.xor_key = get_seed_value();
while (ctx.code_index < ctx.code_len &&
ctx.out_index < ctx.out_len)
{
uint8_t opcode = ctx.code_ptr[ctx.code_index++];
void (*handler)(bytecode_context *) =
opcode_handlers[opcode];
if (handler == NULL)
break;
handler(&ctx);
}
}The hardcoded XOR key is a 4-byte value that changes with each build. The key also serves as a seed for initializing the bytecode interpreter’s accumulator. The opcodes, constants, and substitution tables change across builds, making automated analysis more difficult.
The following table provides examples of Vidar’s VM opcodes and their corresponding operations.
Opcode | Description | Operation |
|---|---|---|
0xF1 | XOR accumulator with a constant value. | acc ^= 0x8F |
0xC5 | Add the next byte to the accumulator. | acc += bytecode[code_index++] |
0xC7 | Rotate the accumulator right by 2 bits. | acc = ROR8(acc, 2) |
0x58 | Subtract the next byte from the accumulator. | acc -= bytecode[code_index++] |
0x16 | XOR the accumulator with the hardcoded XOR key (xor_key). | acc ^= (xor_key >> ((out_index%4)*8)) & 0xFF |
0x39 | XOR the accumulator with the next byte. | acc ^= bytecode[code_index++] |
0xE6 | Subtract a constant value from the accumulator. | acc -= 0x75 |
0x51 | XOR the accumulator with the next byte, write the decoded byte to the output buffer, save the decoded byte (prev_out), and XOR the accumulator with the decoded byte. | t = acc ^ bytecode[code_index++]; out[out_index++] = t; prev_out = t; acc ^= t |
0x33 | XOR the accumulator with the previous decoded byte (prev_out). | acc ^= prev_out |
0xB4 | Rotate the accumulator left by 3 bits. | acc = ROL8(acc, 3) |
0x1A | Perform a bitwise NOT operation on the accumulator. | acc = ~acc |
0x96 | XOR the accumulator with the output index (out_index). | acc ^= out_index |
0x3F | Load a byte from the substitution table. | acc = sbox[acc] |
0x3B | Multiply the accumulator with a constant value. | acc = (acc * 0x53) & 0xFF |
Table 1: Examples of VM opcodes and corresponding operations from a Vidar version 3.1 sample.
Vidar’s VM is used in two different ways:
- For direct deobfuscation: The bytecode is interpreted to create the final deobfuscated string.
- To decrypt a key and nonce for a string encrypted with a custom stream cipher (described later): For Vidar versions 2.0 and 2.1, the first 9 bytes form the key. From version 2.2 onwards, the first 8 bytes are used as the key. In all versions of Vidar, the last 4 bytes are used as the nonce. The custom stream cipher then uses the key and nonce to decrypt an additional byte array containing the actual encrypted string.
The table below provides examples of the VM bytecode that is interpreted to produce deobfuscated strings.
Bytecode | Deobfuscated string |
|---|---|
c54fc7512439f1e6f151cfc55f161651fef1163f510a3b3396515ae6e6392e51de3931c5ce516d33e63b51e81ae65173b41ac7519a1a161a511bc71a518ec73351c41ab4516c1ac75135b4b451cd3b3f5171c5f51ac751d833393e5188f1163b5122f13b513739273b51e016c751d1e63b5104163b516f339696510fe616517239c1333b51ee3be61651c0e6c50851831a963b51733f1a51d81a33514ae61651dab41a519716c71651253333510596163b5188c54cc5dbc51651a1 | браузеров найдено: %d |
e6c5a0c5cd5199c5d61a51b0169651afe639155140333fc751c2f1c7513716b45156c7b4f1515139e3b451af1633c751b23bb4516516b4512f3f3351e3c73b3b510dc556163351a71a1a3f519ec7e651b13f96f1511816b439f65108333b51243932335176b433c7511039911a51c5394b165169f1392051783bc54a51e31696c5c451ed399cb4515a333f51b816b4511b3b3b5143b43b51d9c79651863b16511db4c59c3f51961639919651ac3fc7e65108c73f51f7e6161651533b96514f3f33f1512016c73b512f3b33516f39283f3f51391a3351afc7f151e63f3f510916c73351d2169651031a3f51d3e6f1965190 | Loader: не удалось запустить %s |
b496b45150f11ac7512833398051ea3bf15123b4c5fa3f516bc7c751c1b4f151f29633c59751781639933b51ec3bb45157163b3f51f0b4f139fc51d41ab4c506517a963b51f133f11a51c533b4f15188f1c7c7515516b4b451c6e6165120e6c73351b83b1a512233c7e6510cf13fc5bf5148e6e63b510916b49651773316c5f2515fb43f1651b4e6b4e651f3b4e6514b3b96f151af3fc7e6515b96161a51e9e61651c13bc5e35105c7e6b4515c39bd1a513ec5a4165106f196c7514fc716b45134e6c545512c3bb496512f3bf1165109f139c139c751a0c541c73960516ae63b1651dbe6965127e6f1f151d71a3b518b331a512ef1c7b4516fc71a5104f1331651ec3fc751b6f1161a516cf13b51fe96b451273fc546517296c751a1169651f0e6c5c75162b4e651bbc5d9b451c0395a33b451d716f139405107c73b51b6c71a5136e63b3b5189b4c53851f3333f51273b396f165179397eb45154e6b4b4519233c5f95184f1b4b451e2161a5163f1392239895167c53a399ec5055161b43333516f161ae6516c163f16512eb433516dc50d3bb4510116b4c598517d969633517333c55f51171a1651443316b4511396b43b5129c7163b5131c56b165166e61651c4331651b796c73f51b6c5df3f519233f15114b4163f51d4161ae6515ce6398e5145e6f1517f33e6c751dd331651091ac751dc96c751ccc7c5c051913b1a3b51d139dac5ed9651e33be616514fe6c559335129b4c73979510ec7e6b45114 | browsers: %d (%d rules), wallets: %d (%d rules), plugins: %d (%d wallet / %d plugins / %d soft), grabber: %d |
Table 2: Examples of Vidar’s bytecode and the resulting deobfuscated strings.
Deobfuscation using custom stream ciphers
In addition to the VM-based obfuscation, Vidar implements a custom stream cipher to decrypt some of the malware’s strings. Although the stream cipher interface remains consistent across builds, the underlying implementation varies. ThreatLabz identified two general custom stream cipher implementations: a modified ChaCha-based cipher and an add-rotate-XOR (ARX) based cipher.
ChaCha-based cipher
Vidar versions 2.0 and 2.1 use a ChaCha-based cipher with a number of modifications that include a custom 128-bit initial state, 8-byte key, 4-byte nonce, and different quarter-round rotations that change between samples.
ARX-based stream cipher
Vidar version 2.2 and later use a custom stream cipher that initializes the state, followed by ARX transformations to produce a keystream that is initialized using the previously decrypted key and nonce (from the VM). The initialization and decryption steps are described in the following subsections.
State initialization with the key and FNV-1a
The cipher first incorporates the key material into a 32-bit state value using multiplication with the FNV-1a prime constant, as shown in the example below.
state = SEED_CONSTANT; // Per-build 32-bit seed.
for (i = 0; i < key_length; i++) {
state ^= key[i];
state *= 0x01000193; // 32-bit FNV-1a prime.
}The per-build seed, SEED_CONSTANT, is the only element of this initialization step that changes between builds.
State initialization with the nonce and golden ratio-related constant
The second phase incorporates the nonce material into the state using the fixed constant 0x9E3779B9 (the 32-bit representation of the fractional part of the golden ratio), as shown in the example below.
for (i = 0; i < nonce_length; i++) {
state ^= nonce[i] * 256;
state += 0x9E3779B9;
}
Keystream generation and decryption
Every build uses a different set of ARX based transformations with unique constants to construct the keystream, which is then used to decrypt the ciphertext. The example below shows the algorithm observed in a Vidar version 2.4 sample, which applies various ARX operations. After these operations are executed, the state is folded into a keystream byte that is then used to decrypt the corresponding byte from the input array to produce the output byte, as shown in the example below.
for (i = 0; i < input_length; i++) {
// Transform the 32-bit state.
state *= 0xC5C8C2C9;
state ^= state >> 16;
state += 0xE476B81E;
state ^= 0x907A30A0;
state *= 0xC967295D;
state ^= 0xA72A764F;
// Fold the 32-bit state into a single keystream byte.
keystream_byte =
(state ^ (state >> 8) ^ (state >> 16) ^ (state >> 24)) & 0xFF;
// XOR the keystream byte with the input.
output[i] = input[i] ^ keystream_byte;
}The example below shows a different implementation of the ARX algorithm that uses a different combination of operations and a new set of constants. This implementation was extracted from a Vidar version 2.5 sample.
for (i = 0; i < input_length; i++) {
state -= 0x43ACCA67;
state += 0x871A3B01;
state += 0x1D24B66B;
state = ROL32(state, 22);
state ^= state >> 16;
state += 0x9EC4A92E;
state ^= state >> 16;
state += 0x9DDE39F6;
keystream_byte =
(state ^ (state >> 8) ^ (state >> 16) ^ (state >> 24)) & 0xFF;
output[i] = input[i] ^ keystream_byte;
}The table below provides examples of strings decrypted using Vidar’s VM together with one of Vidar’s custom stream ciphers from a version 3.1 sample.
Bytecode | Ciphertext | Decrypted string |
|---|---|---|
e6c7514a33f19651d6331651f63935b41a51ce963ff151de333b1a51b2963351843ff1e65134f1b439b9512cf1c7c5475117393ac7c5c7516ff1f13b51b5 | 6925d83d2a99d43a726ebd063fa1b2c307f7c332ba24 | Loader: write failed |
e63b51c0961ac52e51cc333fc53251d53be6518d16c52bb4513d1a1ac7513f1a1ab45178f13b51a7c5803f511ef13b1a51be1ab4511b169651d1 | a2eb729fb01a9739e20feb6a1605f56928aa3f568c66b8929a9e58a23b8259b7a46352d1c8a704388daa8080948b21d0308e10 | Не удалось сгенерировать HWID |
c7c73b5133e69651b73f96513e1a1616510fc7c751f5e61ac75129963982f15153b416f15196963f5190c73f517016163f51a3f1163f51d7 | 19e22068ae2dc7519044263c46f643225cae82e9f8418a82fdbfd0beb7c5be9df605e9e215610fb6a72a6d6b37c21446cc771af0597f5d0c1d52950911d9120df050776dccae62777767581a0f1b57002c2abc493b3f18f6c13e040c8079c06a8e42861062ba8e9e898f46e954332c91a0b29c79eb0f302e7078a9f09d525b6d7b3fc727e93930d6240337834a1d700e149d | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n |
Table 3: Example of strings decrypted using Vidar’s VM and custom stream cipher from a version 3.1 sample.
Conclusion
The Vidar family is constantly evolving, with new changes and functionality along with additional obfuscation to make analysis and detection more difficult. The obfuscation is designed to look different across builds, while performing essentially the same function. Vidar keeps the algorithms simple and the interfaces stable while continuously randomizing the constants and micro-operations that static signatures rely on. This highlights the need for network and endpoint detection systems that can keep pace with these innovative techniques.
Zscaler Coverage
Zscaler’s multilayered cloud security platform detects indicators related to Vidar at various levels. The figure below depicts the Zscaler Cloud Sandbox, showing detection details for Vidar.

Figure 2: Zscaler Cloud Sandbox Report for Vidar.
In addition to sandbox detections, Zscaler’s multilayered cloud security platform detects indicators related to Vidar at various levels with the following threat names:
Zscaler MDR detects Vidar using these detection analytics:
- WIN-BIN-NETCONN-TO-TELEGRAM-SHORTENED-URL
- WIN-WEBBROWSER-UNUSUAL-PARENT
- WIN-STEALER-FILEMOD
Indicators Of Compromise (IOCs)
IOC | Description |
|---|---|
1628bb03db87f67661349e169d73ee14ed490bdbf22abfbda08ccc9ebe237974 | Vidar v2.0 |
625a381981fc2d4c25c981d98b1d66bb2cf5da2dde2f590add0673a857d5b074 | Vidar v2.5 |
2d43d592630ad1e012da63ef7279f95dd4a8e94964e12ca2f996051875574fa6 | Vidar v3.1 |
979048a749d8f28d877c7068b1b336ecd1e349869dfb1d7c68118f90e4099bc4 | Vidar v3.4 |
War dieser Beitrag nützlich?
Haftungsausschluss: Dieser Blog-Beitrag wurde von Zscaler ausschließlich zu Informationszwecken erstellt und wird ohne jegliche Garantie für Richtigkeit, Vollständigkeit oder Zuverlässigkeit zur Verfügung gestellt. Zscaler übernimmt keine Verantwortung für etwaige Fehler oder Auslassungen oder für Handlungen, die auf der Grundlage der bereitgestellten Informationen vorgenommen werden. Alle in diesem Blog-Beitrag verlinkten Websites oder Ressourcen Dritter werden nur zu Ihrer Information zur Verfügung gestellt, und Zscaler ist nicht für deren Inhalte oder Datenschutzmaßnahmen verantwortlich. Alle Inhalte können ohne vorherige Ankündigung geändert werden. Mit dem Zugriff auf diesen Blog-Beitrag erklären Sie sich mit diesen Bedingungen einverstanden und nehmen zur Kenntnis, dass es in Ihrer Verantwortung liegt, die Informationen zu überprüfen und in einer Ihren Bedürfnissen angemessenen Weise zu nutzen.
Erhalten Sie die neuesten Zscaler Blog-Updates in Ihrem Posteingang
Mit dem Absenden des Formulars stimmen Sie unserer Datenschutzrichtlinie zu.


