Vidar Adds Virtual Machine and Custom Stream Ciphers For String Obfuscation
ThreatLabz shows Vidar 2.x and 3.x hide strings with a bytecode virtual machine and custom stream cipher.
Zscaler ThreatLabz analyzed how Vidar versions 2.x and 3.x hide strings. A simple bytecode interpreter uses a 256-entry dispatch table with 14 handlers that mutate a one-byte accumulator using XOR, addition, subtraction, rotation, bitwise negation, multiplication, and substitution. A per-build four-byte value seeds the accumulator and is reused as an XOR key, while opcodes and tables change between builds. The virtual machine either emits the plaintext string or recovers a key and nonce so a custom stream cipher can decrypt it; from version 2.2 the key is the first eight bytes and the nonce is the last four.
- Vidar 2.x and 3.x decode strings with a 14-opcode bytecode virtual machine.
- Handlers use XOR, arithmetic, rotation, negation, multiplication, and substitution.
- Opcodes, constants, and tables change per build around a four-byte XOR key.
- The VM can also recover a key and nonce for a custom stream cipher.
- From version 2.2 the cipher key is eight bytes and the nonce is four bytes.
Full article1,331 words · extracted from zscaler.com · click to collapse
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.