https://www.zscaler.com/blogs/security-research/vidar-adds-virtual-machine-and-custom-stream-ciphers-string-obfuscation Vidar Virtual Machine-Based String Obfuscation | ThreatLabz By Ismael Garcia Perez Published: 2026-09-21 · Archived: 2026-09-23 02:01:52 UTC 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 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 null and terminate execution. The 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 Page 1 of 4 https://www.zscaler.com/blogs/security-research/vidar-adds-virtual-machine-and-custom-stream-ciphers-string-obfuscation analysis more difficult. The following table provides examples of Vidar’s VM opcodes and their corresponding operations. Opcode 0xF1 0xC5 0xC7 0x58 0x16 0x39 0xE6 Description XOR accumulator with a constant value. Add the next byte to the accumulator. Rotate the accumulator right by 2 bits. Subtract the next byte from the accumulator. XOR the accumulator with the hardcoded XOR key (xor_key). XOR the accumulator with the next byte. Subtract a constant value from the accumulator. XOR the accumulator with the next byte, write the decoded byte to the output buffer, 0x51 save the decoded byte (prev_out), and XOR the accumulator with the decoded byte. XOR the accumulator with the previous decoded byte 0x33 (prev_out). 0xB4 Rotate the accumulator left by 3 bits. 0x1A Perform a bitwise NOT operation on the accumulator. 0x96 XOR the accumulator with the output index (out_index). 0x3F Load a byte from the substitution table. 0x3B Multiply the accumulator with a constant value. 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): 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. Page 2 of 4 Operation acc ^= 0x8F acc += bytecode[code_index++] acc = ROR8(acc, 2) acc -= bytecode[code_index++] acc ^= (xor_key >> ((out_index%4)*8)) & 0xFF acc ^= bytecode[code_index++] acc -= 0x75 t = acc ^ bytecode[code_index++]; out[out_index++] = t; prev_out = t; acc ^= t acc ^= prev_out acc = ROL8(acc, 3) acc = ~acc acc ^= out_index acc = sbox[acc] acc = (acc * 0x53) & 0xFF For Vidar https://www.zscaler.com/blogs/security-research/vidar-adds-virtual-machine-and-custom-stream-ciphers-string-obfuscation Bytecode c54fc7512439f1e6f151cfc55f161651fef1163f510a3b3396515ae6e6392e51de3931c5ce516d33e63b51e81ae65173b41ac7519a1a161a511bc71a518ec733 e6c5a0c5cd5199c5d61a51b0169651afe639155140333fc751c2f1c7513716b45156c7b4f1515139e3b451af1633c751b23bb4516516b4512f3f3351e3c73b b496b45150f11ac7512833398051ea3bf15123b4c5fa3f516bc7c751c1b4f151f29633c59751781639933b51ec3bb45157163b3f51f0b4f139fc51d41ab4c50 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 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 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; } Page 3 of 4 -1a prime constant, 0x9E3779B9 (the 32-bit https://www.zscaler.com/blogs/security-research/vidar-adds-virtual-machine-and-custom-stream-ciphers-string-obfuscation 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 C e6c7514a33f19651d6331651f63935b41a51ce963ff151de333b1a51b2963351843ff1e65134f1b439b9512cf1c7c5475117393ac7c5c7516ff1f13b51b5 6 e63b51c0961ac52e51cc333fc53251d53be6518d16c52bb4513d1a1ac7513f1a1ab45178f13b51a7c5803f511ef13b1a51be1ab4511b169651d1 a c7c73b5133e69651b73f96513e1a1616510fc7c751f5e61ac75129963982f15153b416f15196963f5190c73f517016163f51a3f1163f51d7 1 Table 3: Example of strings decrypted using Vidar’s VM and custom stream cipher from a version 3.1 sample. Source: https://www.zscaler.com/blogs/security-research/vidar-adds-virtual-machine-and-custom-stream-ciphers-string-obfuscation Page 4 of 4