Vidar 3.9 versus 3.8: configuration and exfiltration changes
Comparison of one Vidar 3.9 native executable with two 3.8 specimens identifies changes in configuration protection, file submission and C2 request construction. The 3.9 configuration selects a custom four-word ARX expansion with ciphertext feedback; both 3.8 samples use repeating XOR. The older XOR decoder remains available in 3.9.
File submission now combines a session token, build identifier, Base64 filename and raw file bytes inside one transformed content field. The value includes the 16-byte key needed to reverse this application-layer transform. An authorized plaintext capture therefore contains the information needed to decode this layer; TLS remains a separate transport protection.
The bootstrap builder appends /api/v2/collect. Bootstrap and action requests rename multipart fields and shuffle their construction order, while the header builder randomizes User-Agent insertion. API resolution, string decoding and the broad collection architecture largely continue from 3.8.
| Area | Examined Vidar 3.8 | Examined Vidar 3.9 | Analytical consequence |
|---|---|---|---|
| Embedded configuration | Per-field repeating XOR | Mode 2 adds ARX-derived masking and ciphertext feedback; XOR remains available | Identify the complete decoding relationship rather than one key or constant |
| File submission | Filename and file data occupy separate Base64 multipart fields |
A single content field carries the hexadecimal key followed by the hexadecimal transformed body |
Reconstruct the combined body before interpreting its contents |
| Bootstrap destination | Locator-derived URL used directly |
/api/v2/collect appended |
Correlate the path with request semantics, not the path alone |
| Multipart fields | Fixed legacy names and template order | Renamed fields and shuffled construction order | Match field sets and roles without requiring a stable order |
| Header construction | Fixed insertion sequence | User-Agent insertion position varies | Header ordering is a weak standalone discriminator |
| Collection and obfuscation | Browser/application collectors, API hashing and 14-handler string VM | Same broad architecture with regenerated parameters and local changes | Separate protocol changes from build variation and existing capabilities |
Analyzed samples and scope
| Role | SHA-256 | Native size | Decoded version and build |
|---|---|---|---|
| Primary 3.8 |
5d5bfc5673ca8413045c59c0215b099926eb14a5418b43487f9c488ef501dab4 |
1,409,024 bytes |
3.8 / 267ec8afc82d3275606aa7560126a0c7 |
| Control 3.8 |
9740b930422ab73acc46ac378aac258fb82ece0ba9955651d403ede01c7e2deb |
1,438,656 bytes |
3.8 / d813994ce1dbd4db71c3b48d6cf58bfc |
| Compared 3.9 |
2c8617853320e7bf4416aec1f1d1f7108dca0fe0b88a9581f883b3cad76cfc3b |
1,439,731 bytes |
3.9 / ec141392be2e474ab700bf353b3b9f2c |
The 3.9 native executable was recovered from a 98,718,064-byte Go wrapper with SHA-256 f469e311f2e87458a6989ae61004f4a7bc1cee463a46dbc3adf444cfa352bba7. Its complete native output includes 57,331 bytes after the declared PE raw extent. Those bytes were preserved. The wrapper's packing scheme is separate from the Vidar-native differences discussed here.
Findings apply to these binaries rather than every build carrying the same version. All analytical addresses are relative virtual addresses (RVAs), and intervals use an exclusive end. Execution confirmed the selected export resolver and layered string decode; protocol construction and the wider collection comparison remain static findings. No real C2 exchange or complete theft-and-upload sequence was performed.
Contents
Configuration protection adds a feedback mode
The configuration change is substantial at the byte-transform level, while the surrounding field layout remains closely related. Version, build identifier and locator records still occupy a compact embedded table. Each locator contains a URL, a marker and a User-Agent string. The decoder exposes those values through cached getters, allowing the rest of the program to operate on ordinary strings after initialization.
The distinction between table contents and protection mode matters. A table containing three locator records is not, by itself, a 3.9 feature: the second 3.8 specimen already contains three. Likewise, version text recovered from an incidental getter is weaker evidence than the version field consumed by the configuration initializer. Here the selected tables, their consumers and the resulting identities agree.
The existing table structure
The relevant table occupies 0x1050 bytes. Its layout explains why otherwise different builds can share most of the initialization logic:
| Table offset | Meaning |
|---|---|
0x0000–0x000f |
Embedded 16-byte key |
0x0010–0x002f |
Encoded version bytes; length at 0x0030 |
0x0031–0x0070 |
Encoded build identifier; length at 0x0071 |
0x0072 + i × 0x0243 |
Encoded locator record, stride 579 bytes |
Record +0x000–0x0ff |
URL; length at +0x100 |
Record +0x101–0x140 |
Marker; length at +0x141 |
Record +0x142–0x241 |
User-Agent; length at +0x242 |
0x1047 |
Locator count |
0x1048 |
Protection-mode byte used by the 3.9 dispatcher |
0x104c–0x104f |
Signed initialization sentinel |
Decoded locator records use 576 bytes: URL at offset 0, marker at 256 and User-Agent at 320. The initializer limits the record count to seven and skips empty fields. These are implementation bounds, not evidence that every possible slot is populated.
In 3.9, the table begins at 0x152030. The count byte at 0x153077 is three, the mode at 0x153078 is two, and the sentinel at 0x15307c starts as -1. The selector at 0x6a10 coordinates initialization. The initializer at 0x6e10–0x71fb decodes the identity and locator fields through the mode dispatcher at 0x7200–0x7309. Version getter 0x69e4, build getter 0x6858 and locator getter 0x6884 expose the cached results.
Repeating XOR in 3.8
For both examined 3.8 tables, each field starts again at key index zero:
P[i] = C[i] XOR K[i mod 16]
The primary 3.8 key is:
17 48 46 96 b8 72 ed 6c fc 0b bd d4 fd 4a 14 eb
Its version bytes 24 66 7e become 33 2e 38, or 3.8. The native loop indexes the key with the low four bits of the field position, XORs one input byte and writes the result. The same reset rule applies independently to version, build and each nonempty locator field. Concatenating the fields before decoding would therefore produce incorrect results after the first boundary.

The 3.9 dispatcher tests specifically for mode two. Other mode values take its XOR branch and terminate the output with NUL. A detector or extractor that assumes all 3.9 configurations use the new transform would miss a path that the executable still supports.

0x730c. The older XOR branch remains immediately below it.The four-word ARX expansion
Mode two first derives 32 mask bytes from the table's embedded 16-byte key. The key is interpreted as four little-endian 32-bit words. Define the following operation, with addition reduced modulo 2^32:
Q(a, b, c, d):
a = a + b; d = ROL32(d XOR a, 16)
c = c + d; b = ROL32(b XOR c, 12)
a = a + b; d = ROL32(d XOR a, 8)
c = c + d; b = ROL32(b XOR c, 7)
The expander at 0x6514–0x6855 applies Q 32 times, swapping the roles of b and c after every operation. It serializes the resulting four words to the first 16 output bytes. Starting from that state, it performs eight more operations without the swap and serializes another 16 bytes.
The rotations resemble a familiar quarter-round construction, but the complete algorithm is not standard ChaCha. Its state has four words, the word-role swap is part of the first phase, and it lacks the standard constants, nonce/counter arrangement and original-state feedforward. Giving it a standard cipher name would conceal precisely the details needed to reproduce it.
For the examined 3.9 table, the key is:
c5 5c fa 1a b8 37 17 a2 48 0a 51 20 95 74 65 51
The resulting 32-byte stream is:
9b ae a3 fd 97 aa fc 7b 3e 82 17 f5 dc d7 e8 22
99 df 9a 83 78 98 4b bb 8d 4c 3d 22 f1 58 7e 7e

This expansion has no separate salt, nonce or authentication tag. Its key is embedded in the same table as the encoded fields. The purpose demonstrated by the code is to obscure static strings, not to maintain confidentiality against an analyst who has the complete executable.
Index masking and ciphertext feedback
The field decoder at 0x730c–0x752c combines three inputs: the derived stream, an index-dependent mask and the previous original ciphertext byte. Its state resets at the start of every field:
previous = K[0] XOR K[15]
for i in 0 .. field_length - 1:
original = C[i]
P[i] = original XOR S[i mod 32] XOR ((155 * i) mod 256) XOR previous
previous = original
P[field_length] = 0
The native arithmetic uses the low byte of a negative multiply in places; -101 × i and 155 × i are equivalent modulo 256. The critical dependency is the feedback byte. The next iteration uses the original ciphertext rather than the just-produced plaintext; the configuration decoder accepts separate source and destination buffers. At 0x7444, the code preserves the input byte before the store and advances feedback from that preserved value. Output termination follows at 0x748c.
For the version field, the first three iterations are particularly easy to inspect:
| Index | Ciphertext | Stream byte | Index mask | Previous ciphertext or initial feedback | Plaintext |
|---|---|---|---|---|---|
| 0 |
3c |
9b |
00 |
94 = c5 XOR 51 |
33 (3) |
| 1 |
27 |
ae |
9b |
3c |
2e (.) |
| 2 |
8b |
a3 |
36 |
27 |
39 (9) |


The clear call at 0x750f targets the temporary expansion, through 0x62b0. It does not erase the embedded key or every cached plaintext field. Configuration strings remain available to the code that needs them.
File submission changes from separate fields to a combined envelope
The upload change affects both interpretation and detection. In the primary 3.8 executable, the file serializer at 0x1319d0 sends separate multipart fields for token, build_id, file_name and file_data. The filename and file data have separate Base64 representations. A parser can recover those values independently once it understands the multipart template.
The 3.9 serializer at 0x2543c–0x25aba instead constructs one inner byte string, transforms it and places its encoded representation under content. The session token, build identifier, filename and file data are inside that transformed value. Looking only for the old field names would lose both the submission context and the file representation.

Inner-body layout
The 3.9 inner body is:
P = token || LF || build || LF || Base64(logical_filename) || LF || raw_file_bytes
Here LF is one byte, 0x0a. The token comes from context offset 768 and the build string from offset 1028. The filename is a logical collection or archive name; it need not describe the original on-disk source. The file payload remains raw bytes at this stage rather than becoming a second Base64 field.
The filename encoder is called at 0x25639. Copy sequences at 0x25705, 0x25723, 0x2574b and 0x2576d construct the fields and separators. For LF-free token and build values, the first three LF bytes delimit the metadata, and all remaining bytes are file data, including embedded NULs or newlines. The serializer copies metadata verbatim without escaping LF, so arbitrary server-supplied token strings need not permit that unambiguous split.
The code then gathers sixteen low bytes from the generator at 0x38d50. This generator maintains a global 32-bit state at 0x28ef40 and applies xorshift steps (13, 17, 5). If the state is zero, it is initialized from the low part of RDTSC. If the first resulting key byte is zero, that byte alone is changed to one. The code does not apply this adjustment to every zero byte in the key.

These details distinguish the key generator from a cryptographic random API. They do not, on their own, establish an exploitable prediction attack: the body explicitly carries the key, making the simpler decoding relationship more relevant to incident response.
The transform and its direction
Function 0x25d90 expands the key using the same four-word construction as configuration mode two. Its optimized implementation groups four operations at a time. Eight such groups produce the 32 swapped-role operations; two groups produce the eight fixed-role operations. This unrolling changes the appearance of the code, not the resulting 32-byte stream.
Function 0x260fc–0x261c2 then transforms the body in place:
previous = K[0] XOR K[15]
for i in 0 .. L - 1:
C[i] = P[i] XOR S[i mod 32] XOR ((155 * i) mod 256) XOR previous
previous = C[i]
This is the enciphering direction of the configuration-feedback construction. The implementation reloads the just-written byte into feedback. To reverse it, a decoder must preserve each original ciphertext byte before replacing it with plaintext. Applying the enciphering routine twice is not a general inverse, because the feedback history changes.

content value.
Exact outer encoding and security meaning
The lowercase hexadecimal encoder at 0x24250 produces:
content = hex(K[0:16]) || hex(C[0:L])
The first 32 characters represent the 16-byte key. The remainder represents the transformed inner body, with total value length 32 + 2L characters. There is no separator between these parts; their boundary follows from the fixed key length. The transform adds no independent authentication tag or separate nonce.
Consequently, a complete authorized capture of this application value contains the information needed to recover its plaintext. This observation applies to the body-packing layer. It does not bypass TLS, expose traffic that was not captured in plaintext, or establish an authentication failure in the transport.
The implementation frees the body allocation and clears the temporary 32-byte stream after use. That is narrower than complete erasure of every key or plaintext copy. The program necessarily passes plaintext collection data through buffers before serialization, and some values also exist in the surrounding context.
A separate diagnostic channel should not be conflated with this change
Vidar already has diagnostic logging that uses an AES-GCM representation. The fixed 64-hex-character diagnostic key recovered from a layered string getter in 3.9 is also present in the primary 3.8 specimen. Its associated format uses a 12-byte nonce, ciphertext and a 16-byte tag, and its serializer has its own mode and fields.
That existing channel is different from the new content envelope. The file-body transform is the custom ARX-derived feedback operation described above, carries a per-body key inline and has no corresponding tag. Neither the presence of AES-related strings nor the layered recovery of the diagnostic key identifies the file uploader's cipher. Keeping these paths distinct avoids attributing an old encrypted logger to a new exfiltration feature.
Bootstrap, action fields and header construction change
The configuration locators feed a bootstrap path that remains recognizable across versions. Each decoded record provides a URL, a marker and a User-Agent. The program can extract a destination using the marker from retrieved content, with a direct-locator fallback. It normalizes the scheme and adds an HTTPS prefix when the expected HTTP or HTTPS prefix is absent.
The difference appears after this locator processing. The primary 3.8 bootstrap at 0x13c5d8 copies the resulting URL directly. The 3.9 function at 0x12a40 obtains /api/v2/collect from getter 0x61578, calls it at 0x12ddb, and appends the path through 0x14066c at 0x12def.


/api/v2/collect getter, appends the path and begins initialization of the randomized multipart order.Initial request fields
The two bootstrap representations carry related values under different names:
| Role | Primary 3.8 field | 3.9 field | 3.9 value source |
|---|---|---|---|
| Host or endpoint identity |
hwid |
endpoint |
Context offset 964 |
| Build identifier |
build_id |
tag |
Build argument saved on entry |
| Response-format selection |
format |
output |
Literal json |
The 3.9 builder creates identifiers for these three cases, then uses its source RNG to swap their positions. The resulting order is not a fixed template. Field roles and values remain stable even though insertion order varies.
Native instructions are important here because the initial pseudocode view omits some variadic arguments. Before the formatting call, RCX holds the destination, RDX a post-call length limit, R8 the format string and R9 the multipart boundary. The field name and value occupy stack argument slots at SP+0x20 and SP+0x28. The output=json case reaches the call at 0x12f4c; later cases load tag and endpoint. The tag value reloads the incoming third argument saved at 0x12a4a, while the endpoint case reloads the context-derived identity.

output=json branch places the value and field-name pointers in the two stack slots beyond the four register arguments before the variadic formatter call. The beginning of the tag branch appears below it.
The formatter at 0x1418e8 must be described precisely. Its cached target resolves to wvsprintfA, and the indirect call passes destination, format and va_list; it does not pass a destination capacity to the callee. The wrapper subsequently clamps the returned length and writes a NUL. This is a variadic formatter with post-call clipping, not an snprintf-style bounded call. The primary 3.8 counterpart at 0x28f0 has the same broad behavior, so this is not a demonstrated 3.9 regression. No vulnerability assessment is implied by identifying the API.
The URL append helper at 0x14066c is separate: its append loop checks the remaining write limit and assumes an existing terminated destination. Its behavior should not be inferred from the formatter's prototype.
Ordinary actions use a second renamed field set
The action serializer at 0x12560, corresponding to primary 3.8 function 0x13c228, shuffles four field cases:
| Role | Primary 3.8 | 3.9 |
|---|---|---|
| Submission token |
token |
sid |
| Build identifier |
build_id |
tag |
| Action selector |
mode |
action |
| Response-format selection |
format |
output |
The token comes from context offset 768, the build from 1028, and the action value from the caller. A separate six-argument append helper at 0x1174c combines destination, limit, used length, boundary, name and value. This path should not be conflated with the bootstrap's variadic call.
The selector values connect these renamed requests to existing collection phases. The code supplies 3 for wallet definitions and 4 for grabber definitions; 2 and 21 distinguish Chromium and Firefox plugin-related definitions. Completion paths include 301 for crypto, 401 for grabber and 101 for browser work. Loader selectors use 5 and 501, and final completion uses 6. The implementation changes the surrounding names and ordering without establishing a new meaning for every selector.
A collector worker can send its completion selector even when that collector produced no data. Seeing a completion request, therefore, would not by itself establish successful collection from the corresponding application. The local control flow and a server's response are separate evidence.
Header placement and transport
The common-header builder at 0x24acc chooses the User-Agent insertion position using the RNG value masked with three. Other common headers include Accept, Accept-Language and Connection. The primary 3.8 counterpart at 0x131214 inserts User-Agent and the other headers in a fixed sequence.
This is a change to local header construction. WinHTTP or intermediaries may affect the final wire representation, and on-wire order was not measured. A detector requiring a deterministic header order would nevertheless be making an assumption that the 3.9 source no longer supports.
The transport still uses WinHTTP, selecting POST when it has a positive body length and GET otherwise. It supplies a configured User-Agent or a Mozilla/5.0 fallback. The HTTPS path sets option 31 to 0x3300; that assignment should not be summarized as proof of strict certificate validation. Application-response markers can trigger cleanup or termination independently of the underlying HTTP operation.
Not every channel adopts the new names. The environment-report path at 0x17658 still serializes legacy token, build_id, mode, data and format=json fields. Diagnostic logging also keeps a dedicated serializer. Protocol identification must therefore consider the request's role rather than applying one renamed template to the whole executable.
Bootstrap retry behavior is familiar
The retry coordinator at 0x11f1c corresponds to primary 3.8 function 0x13bba8. It waits roughly 3–5.5 seconds for a bootstrap worker, can terminate an overdue worker and move to another locator, and places delays between attempts. Its outer attempt cap is randomized in the approximate 400–800 range, with inter-attempt delays of roughly 1–4 seconds.
These limits are part of the malware's own control flow. They explain how an unavailable locator can lead to repeated attempts rather than immediate exit, and why the first decoded address need not be the eventual communication destination. They do not establish that any listed destination was reachable or active during this research.
Server settings and execution phases remain compatible
The changed submission format feeds a settings parser with substantial continuity. Function 0x137b98, corresponding to 0x13e4a0 in primary 3.8, allocates a buffer, Base64-decodes the response and passes it to a JSON parser. It requires an object, represented internally by type five. The token may be a string or an integer converted to decimal text; empty values and values beginning with < are rejected.
The parsed settings populate the same general context layout:
| Setting | Context offset | Relevant behavior |
|---|---|---|
cryptocurrency |
912 | Enables the wallet/crypto collection phase |
telegram, steal_discord, steam |
916, 920, 924 | Application collection controls |
azure |
928 | Enables Azure-related artifact collection |
screenshot |
932 | Controls screenshot collection |
antivm |
936 | Enables environment probing/reporting |
debug |
944 | Diagnostic behavior |
loader |
948 | Secondary-execution behavior and ordering |
| Grabber size | 952 | Configured value shifted by ten; zero defaults to 5 MiB |
| ZIP threshold | 956 | Values at least two are shifted by ten; zero and one keep special meanings |
| Thread count | 960 | Zero defaults to eight |
A context setting is not proof that every later pool uses that value. The scheduler includes call sites with explicit worker-count arguments, including four and eight. Reading only the JSON labels would overstate the direct relationship between a configuration field and every thread pool.
The high-level functions also align across the primary specimens:
| Responsibility | 3.8 RVA | 3.9 RVA |
|---|---|---|
| Overall orchestration |
0x1411f0 |
0x28eb0 |
| Context and bootstrap preparation |
0x140478 |
0x2852c |
| Identity/definition setup |
0x142630 |
0x29f20 |
| Bootstrap retry coordinator |
0x13bba8 |
0x11f1c |
| Server settings parser |
0x13e4a0 |
0x137b98 |
| Definition fetch/decode/callback |
0x13dfe0 |
0x1376bc |
| Browser phase |
0x13f414 |
0x27394 |
| Remaining collection and loader phase |
0x13fb68 |
0x27bc4 |
| Finalization |
0x140034 |
0x28160 |
These matches are based on consumers, argument flow and operations, not a global address delta. Local flattening, auxiliary arithmetic and different function boundaries prevent a simple address translation. A shared function count or a similar-looking basic block would be insufficient to establish behavioral correspondence.
The definition-fetch helper remains important to scope. It retrieves a response, Base64-decodes it, invokes a parser callback and frees the temporary buffer. Some collection targets are therefore supplied through definitions rather than appearing as a complete static list in the executable. Conversely, the existence of a definition request does not prove that a particular response was obtained.
Browser and application collection show continuity
The comparison does not establish a new major collector in 3.9. It instead shows familiar collection components connected to the changed submission layer. Browser database staging, Local State processing, memory key searches, APC-assisted attempts, wallet rules, application artifacts and archive workers all have counterparts in primary 3.8.
Browser files are staged as files
Function 0x19700 combines responsibilities found in primary 3.8 table setup 0x12c160 and coordinator 0x12d9c8. Its browser-artifact list covers five Chromium-style paths and six Firefox-style paths:
| Chromium-style artifacts | Firefox-style artifacts |
|---|---|
Login Data |
logins.json |
Login Data For Account |
key4.db |
Network\Cookies |
cookies.sqlite |
History |
places.sqlite |
Web Data |
formhistory.sqlite |
| — |
cert9.db |
The worker at 0x18864, corresponding to 0x12caf0, copies files, falls back to handle-based access in some conditions, and can defer work into an early-bird path. It checks existence and size before staging the result. These are whole-file collection paths; they are not, by themselves, local SQLite row decryptors.
Early-bird handling is already present in the 3.8 comparison. Some branches distinguish Avast-related conditions, but the observed special case does not justify a claim that the program universally bypasses endpoint protection. The useful behavioral finding is that the collector has several access paths for files that ordinary copying may not retrieve.
Local State processing and DPAPI
The Local State consumer at 0x26a30, corresponding to 0x13d7e0, accepts files from one byte through 4 MiB. It searches for a 15-byte marker and limits the extracted Base64 text to 1,023 characters. After decoding, it recognizes a five-byte DPAPI prefix or strips a four-byte APPB prefix, then attempts CryptUnprotectData with flags zero and one.
The consumer retains at most 64 bytes of the DPAPI result and accepts a candidate when the retained length is at least 16 bytes. Longer DPAPI outputs can therefore be accepted after truncation. These conditions describe the implemented policy more accurately than a general claim of browser encryption bypass. Recognizing APPB-formatted material, attempting DPAPI and obtaining a usable application-bound key are different outcomes. The code also contains process-memory paths, reflecting that it does not depend on one local file alone.
Memory search and APC-assisted key attempts
The memory scanner at 0x812c, corresponding to 0xce2c, considers committed private read/write regions between 4 KiB and 100 MiB. It caps the number of regions at 4,096 and reads chunks of 1 MiB with 31 bytes of overlap, sufficient to preserve a candidate 32-byte sequence crossing a chunk boundary. Candidate-address retention is limited to 64 entries.
The 3.9 browser-key worker at 0x13da14 and its coordinator at 0x13f6e8 connect these candidates to an APC-assisted path; their primary 3.8 counterparts are 0x1a120 and 0x1b9d0. Wrapper 0x17e70 calls the lower-level routine at 0x4568, corresponding to primary 3.8 functions 0x13d4f0 and 0xf730. It opens threads, tries up to eight, queues work with a reserve value of one and waits after a nonnegative NTSTATUS.
An important naming distinction comes from the actual resolved API hash. The initializer at 0x13d590 uses 0xd8813a9d, which resolves to NtQueueApcThreadEx under this specimen's hash arithmetic. NtQueueApcThreadEx2 produces a different value, 0x30272067. A diagnostic spelling is therefore not evidence of an Ex2-specific capability.
The worker compares a 32-byte region before and after the attempt. At 0x13e5ae, the comparison feeds a conditional selection at 0x13e5c7: unchanged data selects the pre-attempt bytes. A restart path has equivalent treatment. Candidate storage is deduplicated and capped at sixteen entries, and reprotection is conditional on a changed result. Termination and restart may be attempted up to three times, followed by cleanup; this pattern is not new to the examined 3.9 executable.
These stages should not be collapsed into a single successful-decryption claim. A memory match is a candidate; an accepted APC call is not proof of a usable browser key; a plausible key is not proof that a cookie record authenticated under GCM. The comparison establishes the recovery attempts and their selection rules, not universal success against every browser configuration.
Collection scheduling and application targets
The scheduler at 0x2a860, corresponding to 0x14322c, builds 40-byte descriptors containing context, callback, argument, label and completion mode. Crypto collection depends on context offset 912; Azure collection depends on 928. Grabber, social and FTP/SSH paths are also scheduled. Relevant callbacks include wallet collection at 0x13bbb0, grabber work at 0x33748, social collection at 0x2f984, FTP/SSH at 0x23d7c and Azure at 0x53ac.
The Azure collector checks its setting and an .azure location, then stages names including accessTokens.json, azureProfile.json, msal_token_cache.json and TokenCache.dat when present. Wallet collection consumes rules and supports copy and handle-based fallbacks. These are implemented consumers rather than conclusions drawn solely from plaintext labels.
The Discord path at 0x3b4b4 connects Local State processing, LevelDB records, GCM handling and validation/deduplication before writing Soft\Discord\tokens.txt. That concrete chain supports a token-collection interpretation for this path. It should not be generalized into proof that every staged browser cookie database is decrypted locally. Telegram and Steam consumers, and screenshot handling controlled by context offset 932, likewise fit the existing collection architecture.
No dedicated Claude or Anthropic session collector was established. The reconstructed strings and reviewed consumers did not identify a Claude-specific session-key or replay path. Generic browser cookie-file collection can expose sessions under suitable conditions, but that broader risk does not establish an application-specific feature. Server-supplied definitions also limit what can be concluded from the embedded target set alone.
Archive handling and secondary execution
The 3.9 archive queue at 0x1476a0 and worker at 0x147b70 correspond to primary 3.8 functions 0x23204 and 0x237b0. Descriptors can represent memory buffers, disk files or direct ZIP-related work. The worker handles retries, approximately three to five depending on the path, before releasing its resources. These archive components feed the changed file serializer; the existence of the new body format does not imply that every upstream collector was rewritten.
The loader chain at 0x22e70, 0x23b70 and 0x23250 has counterparts at 0x14aae0, 0x14b400 and 0x14ade4. It accepts JSON-array tasks with URL and function-name fields. A downloaded body must exceed a minimum size, and EXE/DLL paths apply a larger size and MZ check. It generates an eight-character basename and supports executable, DLL and PowerShell execution forms.
The source constructs command lines equivalent to launching a staged executable through cmd /c start, invoking a DLL through rundll32.exe, or running a PowerShell file with -ExecutionPolicy Bypass -WindowStyle Hidden -NoProfile. The DLL template appends optional function text with a space; it does not establish an inferred comma-separated syntax. CreateProcessA uses 0x08000000, and loader settings affect phase order. These are local secondary-execution capabilities, not evidence that an attacker payload was downloaded or run during the comparison.
Environment reporting also continues. Function 0x137b0, corresponding to 0x145ea4, performs twelve probes. A positive result can lead to an environment report with selector 701, after which host collection continues. The setting and report do not establish a universal anti-VM exit condition.
API resolution keeps its design while parameters change
Both versions resolve APIs through linked module discovery, name hashing, export enumeration and forwarding logic. The arithmetic is regenerated, but the architecture is consistent. This is useful for behavioral matching: an isolated multiplier or seed is much less informative than the call relationship between an export enumerator and its name-hash kernel.
Name-hash recurrence
For a case-sensitive NUL-terminated export name, the kernel computes:
h = seed
for each non-NUL byte b:
h = ((h XOR b) * multiplier) mod 2^32
The terminating NUL is not included. Module-name normalization is handled by a separate lowercase-copy helper; it should not be silently applied to export names.
| Specimen | Kernel RVA interval | Seed | Multiplier | First completed update |
|---|---|---|---|---|
| Primary 3.8 |
0x203d4–0x20720 |
0xc2d224f9 |
0x01000589 |
0x2057a |
| Control 3.8 |
0x13cda8–0x13cfe4 |
0x102b36fb |
0x01000387 |
0x13ce5c |
| 3.9 |
0xfb3c–0xfe4d |
0xb4b8026f |
0x010003c9 |
0xfbf7 |
The two 3.8 rows are a useful guard against over-attribution: different constants and instruction layouts already occur within that version. In 3.9, the byte XOR is at 0xfbdd, multiplication at 0xfbf0, and 0xfbf7 marks completion of the first update.


0xfbf7. Surrounding auxiliary arithmetic does not change the identified name-hash recurrence.The resolver roles and their boundaries
Six roles establish the complete resolver relationship:
| Role | Primary 3.8 | Control 3.8 | 3.9 |
|---|---|---|---|
| PEB acquisition |
0x1fbe8–0x1fd17 |
0x13c6fc–0x13c81b |
0xf480–0xf519 |
| Module traversal |
0x217c0–0x21ae5 |
0x13e19c–0x13e548 |
0x10edc–0x1124f |
| Lowercase-copy helper |
0x21af0–0x21d00 |
0x13e550–0x13e6f9 |
0x11250–0x114b9 |
| Name-hash kernel |
0x203d4–0x20720 |
0x13cda8–0x13cfe4 |
0xfb3c–0xfe4d |
| Export-name resolver |
0x20e74–0x21622 |
0x13d7c0–0x13e07b |
0x10720–0x10d9a |
| Forwarder handling |
0x1fd20–0x2010d |
0x13c81c–0x13cb5f |
0xf520–0xf991 |
The 153-byte 3.9 PEB helper has no .pdata row. It reads the TEB self-pointer through GS:[0x30], then reaches the PEB pointer at offset 0x60 through split offset arithmetic. This is obfuscation around a familiar Windows structure relationship, not a new access primitive.
The export resolver validates MZ/PE structure, walks AddressOfNames, hashes a name, translates a matching name ordinal and reads the corresponding function address. If that address lies within the export directory, the forwarder path parses a module/name form and resolves it recursively. The examined implementation supports the named-forwarder relationship; an ordinal-forwarder parser was not established.
The hash kernel serves several callers. At 0x111d4, it consumes a module-related name. At 0x10b2a, it consumes an API export name and returns to the comparison at 0x10b2f. Forwarder logic has additional module and export call sites. An analyst who selects the module caller as the API-name observation point would identify real hashing code but the wrong semantic operation.

0x10b2a returns to a hash comparison, then name-ordinal and function-table lookups. This consumer distinguishes API-name resolution from module-name hashing.
The ordered path 0x10720 → 0xfbf7 means entry into the export resolver followed by the first completed export-name hash update. It is not a contiguous numeric interval: the kernel is located below its caller. PEB acquisition is a separate earlier role. Control-flow flattening and arithmetic noise occur around these routines, and some intermediate writes are real; treating every unfamiliar operation as dead code would obscure their dependencies.
String protection retains VM and scalar layers
The VM is another strong continuity point. The examined executables use a small bytecode interpreter, a cached seed, a 48-byte state and fourteen handler entries representing thirteen operation classes. Build-specific opcode assignments, seeds, substitution tables and scalar constants change. Those changes should be separated from the more stable interpreter design.
Zscaler ThreatLabz's “Vidar Adds Virtual Machine and Custom Stream Ciphers For String Obfuscation”, published on 21 September 2026 by Ismael Garcia Perez, describes string protection in earlier Vidar versions. The comparison here concerns the actual 3.8 and 3.9 implementations rather than a claim that this architecture first appeared in 3.9.
Dispatcher, seed and state
| Component | Primary 3.8 | Control 3.8 | 3.9 |
|---|---|---|---|
| VM dispatcher |
0x27000 |
0x38a24 |
0x3bcf4 |
| Cached-seed accessor |
0x270d0 |
0x390e0 |
0x3bdc4 |
| Seed selection |
0x27530 |
0x39110 |
0x3badc |
| Handler-table initializer |
0x275d8 |
0x38efc |
0x3c0ac |
| Normal seed |
0x7fa75da9 |
0x4ba15d21 |
0x6d0d8b43 |
| Alternate seed |
0xdeadbeef |
0xdeadbeef |
0xdeadbeef |
The 3.9 calling convention supplies the bytecode pointer in RCX, bytecode length in EDX, destination in R8 and output capacity in R9D. The state layout is:
| Offset | State member |
|---|---|
0x00 |
Accumulator byte |
0x01 |
Previous emitted byte |
0x08 |
Program pointer |
0x10 |
Program length, 32 bits |
0x14 |
Program cursor, 32 bits |
0x18 |
Output pointer |
0x20 |
Output capacity, 32 bits |
0x24 |
Output cursor, 32 bits |
0x28 |
Seed, 32 bits |
The structure occupies 48 bytes, including alignment and trailing padding. Initialization places the seed's low byte in the accumulator and clears the previous-byte state. The dispatcher fetches an opcode, selects a handler and passes the state pointer. It stops at the program-length or output-capacity boundary, or when the table has no handler for the opcode.


+0x28. Program length and output capacity bound the main dispatch loop.These main-loop checks do not imply an extra operand-length check inside every immediate-consuming handler. The VM also does not necessarily append a terminator; callers can do so after decoding. Its incidental return register is not a reliable output length. A 59-byte program in the layered example below yields twelve output bytes even though its return register contains 59.
The seed selector checks PEB fields associated with the image base at +0x10 and process heap at +0x30, then caches its result. These are not reads of BeingDebugged or NtGlobalFlag, and they do not constitute a general hypervisor detector. Substituting a familiar anti-debug explanation for the actual predicate would mischaracterize both the selected seed and the alternate path.
Handler semantics and build variation
The instruction set includes seed-byte XOR, substitution, rotations, output-index XOR, constant XOR, complement, multiplication, previous-byte XOR, emission, immediate subtraction/addition/XOR and constant addition. Two handler entries implement rotations, giving fourteen entries for thirteen operation classes.
For 3.9, notable opcode assignments include 0x06 for seed-byte XOR, 0x03 for substitution, 0x78 and 0x83 for rotations by seven and one, 0xdc for index XOR and 0x0b for emission. The constant-XOR handler uses 157, multiplication uses 105 and constant addition uses 107. These assignments differ from both 3.8 specimens.
The seed-byte operation selects a seed byte using the low two bits of the output cursor. Index XOR uses the output cursor's low byte. Emission combines the accumulator with an immediate, stores the emitted byte as previous state, advances the output cursor and updates the accumulator using that byte. All examined substitution tables are 256-byte permutations, but their contents differ.
These details support a semantic detector that recognizes state operations and emission dependencies. A byte signature built from one opcode map would be much less portable.
Scalar follow-up and a concrete layered getter
Some VM outputs are final strings; others are binary material for another decoder. In 3.9, scalar function 0x3bb84 starts from 0x1d336507, folds eight key bytes and four nonce bytes, then advances a 32-bit state for each output byte:
s = 0x1d336507
for b in key8:
s = ((s XOR b) * 0x01000193) mod 2^32
for b in nonce4:
s = ((s XOR (b << 8)) + 0x9e3779b9) mod 2^32
for each ciphertext byte:
s = s XOR (s >> 16)
s = s + 0x58768973
s = ROL32(s, 27)
s = s XOR (s >> 16)
s = s + 0xd02e2af5
s = ROL32(s, 30)
s = s + 0xab01b560
plaintext_byte = ciphertext_byte XOR byte0(s) XOR byte1(s) XOR byte2(s) XOR byte3(s)
All arithmetic is modulo 2^32. The primary 3.8 scalar at 0x273c0 begins from 0x69376541 and follows another recipe. The control's 0x38af4 starts from 0x1d93d3eb and differs again. A new scalar constant set therefore does not, by itself, identify a new string-protection architecture.
Getter 0x61a14–0x622d2 demonstrates the complete 3.9 relationship. It calls the VM at 0x621e7, returns at 0x621ec, and supplies a 59-byte program with a twelve-byte output capacity. The result is binary material:
95 e2 60 51 35 82 35 41 5d 7b ac 5f
The first eight bytes become the scalar key and the last four its nonce. The call at 0x6222c, returning at 0x62231, decodes 64 bytes. The resulting hexadecimal text is the diagnostic AES key already present in the primary 3.8 specimen, not a newly introduced key purpose. The getter allocates 65 bytes, copies the result, writes NUL at offset 64 and caches the pointer.

Linking native getters and VM programs to their consumers allows protocol labels, paths and collection names to be interpreted where they are used, instead of being treated as an unqualified string list. A literal's presence alone does not establish an implemented capability.
Execution confirmation
Contained execution of the unchanged 3.9 native executable confirmed export-name resolution and the selected layered string decode along natural control flow. The export resolver at 0x10720 was resolving GetProcAddress; its first enumerated name was instead AcquireSRWLockExclusive. That first name produced first-update value 0x3a80401e and complete name hash 0xd409d92c, after which enumeration continued and returned the requested export. Separately, the VM produced the twelve bytes above and the scalar layer produced the expected 64-byte text. The PEB helper was not separately observed in that run. Protocol construction and the wider collection comparison remain static findings; no real C2 exchange or complete theft-and-upload sequence was performed.
Detection and response implications
The clearest detection changes concern serialization rather than a new inventory of stolen applications. Existing rules that insist on file_name plus file_data, or a fixed token/build_id/mode/format order, should be reviewed against the 3.9 builders. The executable can construct several request families, and legacy environment or diagnostic serializers remain present alongside the renamed bootstrap and action paths.
For authorized application-layer captures, the following relationships are more useful than one isolated string:
| Observation | Stronger correlation | Important boundary |
|---|---|---|
/api/v2/collect |
Locator processing followed by bootstrap fields endpoint, tag, output |
A path alone is not a unique family identifier |
sid, tag, action, output |
Matching values and phase-specific selector semantics, allowing reordered fields | Not every channel uses these names |
A long hexadecimal content value |
First 32 characters decode to a key; the remainder reverses to metadata and raw bytes | Requires a complete plaintext body; first-three-LF parsing assumes LF-free token/build values |
| ARX quarter-round-like instructions | Four-word state, 32 swapped-role operations, eight fixed-role operations and the feedback consumer | Rotation constants alone are too generic |
| API XOR/multiply loop | Export-name table enumeration, ordinal translation and function-address lookup | Module-name hashing can use the same kernel |
| Small VM dispatcher | 48-byte state, output-cursor-dependent handlers and fourteen table entries | Seeds/opcodes vary within the same version |
The content envelope offers a practical forensic opportunity after lawful capture: its key is in the value itself, its expected length is 32 + 2L, and reversing it recovers the combined metadata and raw file bytes. Interpreting the first three LF bytes as metadata boundaries requires LF-free token/build values. A decoder should validate the hex representation and structural boundaries rather than accepting any long hexadecimal string as a Vidar submission. It should also preserve the distinction between an archive's logical filename and the endpoint's original source paths.
On hosts, collector chains remain valuable. Browser database copies combined with Local State access, process-memory reads, thread opening/APC attempts and subsequent archive submission form a more meaningful pattern than one filename. Azure cache staging, Discord LevelDB processing and loader command construction provide additional context. These observations are not all unique to Vidar, so correlation with the resolver, configuration or protocol is preferable to treating one API or artifact name as decisive.
Static family recognition benefits from semantic anchors. The selected configuration relationship ties key storage, field lengths, mode dispatch, stream expansion and per-field reset together. The resolver relationship ties PEB/module traversal to a case-sensitive name hash and PE export consumers. The VM relationship ties a compact state to its handler table and output dependencies. Each survives some parameter regeneration while remaining more specific than a generic list of arithmetic constants.
Within-version variability is central to this approach. The control 3.8 executable already changes locator count, hash constants, VM seed and scalar recipe relative to the primary 3.8 specimen. Those differences explain why a detector should not treat every changed immediate or field count as evidence of 3.9. The strongest deltas here are the selected configuration mode and the concrete request and file-body constructions verified in their consumers.
Conclusion and limits
The examined Vidar 3.9 executable evolves the boundary between collection and communication. Its selected configuration uses an additional ARX-derived feedback mode; file submission combines metadata and raw bytes inside an inline-key hexadecimal envelope; the bootstrap adds a path, while bootstrap and action requests change field names and construction order. Those changes matter to extractors and network parsers even though much of the collector and obfuscation architecture continues from 3.8.
The comparison does not establish a new Claude-specific collector, a universal browser application-bound-encryption bypass, or a new API-resolution or VM design. It also does not establish when every change first entered the family. Server-supplied definitions can extend the embedded target set, and only the stated resolver and layered-string operations were confirmed through execution. Broader collection and protocol conclusions describe implemented code, not a successful transaction with attacker infrastructure.
Analysis approach
The analysis combines original native bytes, IDA disassembly and pseudocode, decoded configuration fields, protected-string consumers and targeted contained execution. Function matches were checked through their inputs, outputs, call relationships and instruction behavior. Native argument setup was used where pseudocode omitted variadic values or distorted a prototype. Cryptographic descriptions follow the implemented arithmetic, field resets and feedback direction rather than resemblance to a standard primitive.
Protected strings were reconstructed offline and associated with their getters and callers. A decoded word or path was treated as evidence of a capability only when a consumer implemented the corresponding operation. The second 3.8 specimen served as a control against mistaking build configuration or regenerated constants for a version-wide change.
The figures are authentic IDA views of the analyzed code. No embedded attacker destination was contacted to establish the findings.