Blog de Zscaler

Reciba en su bandeja de entrada las últimas actualizaciones del blog de Zscaler

Security Research

Vidar Adds Virtual Machine and Custom Stream Ciphers For String Obfuscation

image
ISMAEL GARCIA PEREZ
septiembre 21, 2026 - 11 min read

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.

Evolution of Vidar’s string obfuscation from early May to early September 2026.

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.

Zscaler Cloud Sandbox Report 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

form submtited
Gracias por leer

¿Este post ha sido útil?

Exención de responsabilidad: Este blog post ha sido creado por Zscaler con fines informativos exclusivamente y se ofrece "como es" sin ninguna garantía de precisión, integridad o fiabilidad. Zscaler no asume ninguna responsabilidad por errores u omisiones ni por las acciones que se tomen basándose en la información proporcionada. Cualquier sitio web o recurso de terceros enlazado en esta publicación de blog se proporciona únicamente por conveniencia, y Zscaler no se hace responsable de su contenido ni de sus prácticas. Todo el contenido está sujeto a cambios sin previo aviso. Al acceder a este blog, acepta estos términos y reconoce ser el único responsable de verificar y utilizar la información de manera adecuada según sus necesidades.

Reciba en su bandeja de entrada las últimas actualizaciones del blog de Zscaler

Al enviar el formulario, acepta nuestra política de privacidad.