LinkedIn Job-Lure Malware: Static Reconstruction of a Node.js / Donut / .NET Chain
Byte-for-byte verification of the JavaScript decoding stages, Donut Chaskey instance, and embedded .NET module—with offsets, hashes, persistence control flow, and detection pivots.
By Umair Sabir
A LinkedIn recruitment message offered flexible, remote, part-time work and directed me to a Google Form. The sender's account had also been compromised. The associated role-information archive led to a multi-stage loader; this article reconstructs the extracted stages without executing them.
Evidence revision — 11 November 2026. This version replaces the earlier provisional account of the encrypted stages with byte-for-byte verification. Both JavaScript decoding paths, the shellcode, the Chaskey-decrypted Donut instance, the embedded managed PE, and four managed resources were checked against the supplied evidence. The C2 remains an external-report correlation.

Scope and provenance
The follow-up bundle contains exported PDF name/data fields, JavaScript resources, shellcode, a decrypted Donut instance, decoy-document XML, and managed-resource exports. EXE entries and the native addon were excluded from this review. The embedded PE was parsed as a byte buffer inside the decrypted-instance data, without writing or loading an executable.
The original delivery ZIP, malicious DLL, and complete PDF are absent from this bundle. Their signatures, hashes, and initial loader behavior below come from the earlier analysis notes. They were not independently rechecked in this pass. No malware JavaScript, shellcode, native addon, or managed code was executed, and no delivery or C2 endpoint was contacted.
| Layer | Verification |
|---|---|
| update.dat | Base64/zlib decoding reproduces update_decoded.js exactly |
| support.ico | XOR → RC4 → Base64/zlib reproduces stage3.js exactly |
| stage3.js | Base64 literal decodes to the supplied 589,151-byte shellcode |
| Donut instance | Chaskey CTR reproduces all 576,900 decrypted bytes |
| Embedded PE | 571,392 bytes; SHA-256 and PE/CLR structures verified |
| Managed resources | All four byte arrays match their supplied exports |
| C2 / theft / remote control | Not established from this sample; related reporting only |
Reconstructed chain
LinkedIn recruitment message → application-form link
[complete redirect path not independently preserved]
Role-information archive (initial analysis)
→ renamed WINWORD.EXE → AppvIsvSubsystems32.dll
→ ~WRDCA68.pdf container
→ SearchIndexer.exe / update.dat / koffi.node
Extracted stages verified in this review:
update.dat (5,373 bytes)
→ Base64 + zlib → update_decoded.js (11,336 bytes)
→ support.ico (790,806 bytes)
→ XOR + RC4 → JavaScript decompression wrapper
→ Base64 + zlib → stage3.js (786,335 bytes)
→ Base64 → Donut shellcode (589,151 bytes)
→ Chaskey CTR → decrypted instance (576,900 bytes)
→ embedded PE at +0x104C (571,392 bytes)
→ managed resources; ultimate behavior unresolved
The archive was named EXP_fef6cf_Role_Information_Document_20261018.zip. The screenshot establishes the message and form link, not every redirect to that archive. Receiving the message is not equivalent to executing the package. The employer named in the lure is being impersonated, not attributed involvement.
Stage 1: a legitimate executable loads the malicious DLL
EXP_Role_Information_Document.exe is identified in the analysis as a genuine WINWORD.EXE, version 16.0.19127.20302, with a valid Microsoft signature. Its filename invites the recipient to treat it as role information even though it is an executable.
The malicious component is AppvIsvSubsystems32.dll, placed beside it. The executable imports the App-V DLL, allowing the adjacent attacker-controlled library to be loaded in this package's configuration. This is DLL sideloading: trusted executable identity does not make a neighboring DLL trustworthy.
The DLL carries Microsoft-looking version information and a certificate chain, but the recorded signed digest does not match the computed digest. A certificate embedded in a modified file is not a valid signature for that file. The included msvcp140.dll and vcruntime140.dll were recorded as genuine Microsoft runtime libraries.
The loader's static features include:
- API-name decoding and a flattened control-flow dispatcher.
- Checks for virtual-machine processes and registry artifacts, plus debugger checks using
NtQueryInformationProcess. - Code that restores portions of the in-memory
ntdll.dllfrom a disk copy, consistent with an attempt to remove user-mode hooks. - Process-creation attributes that select
explorer.exeas the parent. - Extensive export-table noise and approximately 145 MiB of appended high-entropy data.
The analysis found no loader references to that appended DLL data, making padding the working interpretation. Its purpose cannot be proved from size alone, although file inflation can complicate scanning and analysis. Similarly, unhooking code indicates an evasion attempt; it does not demonstrate that every endpoint control was bypassed.
Stage 2: resolving the PDF record layout
The earlier notes record a 69,884,934-byte PDF container with data beginning at 0x1ED. The decoded JavaScript independently confirms decimal offset 493 and a loop that reads one name/data pair per iteration (update_decoded.js, lines 128–138).
The exports named rec0 through rec9 are five name/data pairs, not ten records. The earlier six-row table incorrectly separated the decoy marker from its data.
| Pair | Name export | Data export | Data bytes |
|---|---|---|---|
| 0 | rec0: DECOY:…Document.docx | rec1: decoy DOCX | 55,772 |
| 1 | rec2: SearchIndexer.exe | rec3: host (excluded) | 65,568,976 |
| 2 | rec4: update.dat | rec5: script wrapper | 5,373 |
| 3 | rec6: koffi.node | rec7: addon (excluded) | 1,022,464 |
| 4 | rec8: support.ico | rec9: encrypted script | 790,806 |
The first name is DECOY:EXP_Role_Information_Document.docx. The parser consumes:
uint32_le pair_count
repeat pair_count times:
uint32_le name_length
byte[name_length] name
uint32_le data_length
byte[data_length] data
It skips the DECOY: pair, skips three non-decoy pairs, then writes the remaining data. With the exported ordering, that final pair is support.ico. The pair interpretation is supported by both code and exports; the original PDF's header and complete boundaries cannot be re-read because the PDF is absent.
The extracted DOCX XML is headed “Expeditors Advertising Specialist Job Description” and carries a June 2026 update label. Its position profile says full-time unless otherwise stated, while the message offers part-time work. This is a visible inconsistency in the lure, not standalone proof of maliciousness.
Stage 3: the Node.js script, precisely
The initial notes identify the host as signed Node.js 20.20.2 used in a SEA chain. That binary was excluded here, so its version, signature, and SEA packaging were not revalidated. The extracted script does directly show use of koffi.node for native API calls.
First decoding boundary
update.dat is a JavaScript wrapper, not a raw compressed stream. Its Base64 literal is passed to zlib.inflateSync, and the resulting text goes to an evaluation sink. A separate data-only decoder reproduced the 11,336-byte update_decoded.js exactly:
update.dat SHA-256:
259e524643817b4ac87239b47d96cee1c838d4456d4022fbf05b30a709af5800
Decoded JavaScript SHA-256:
bc35f1058a54429a1aa5fcef3f08da25dbaac33cec6eb54dd2f7345aaaf32b70
Installation versus execution
The script derives wd from its own path and branches on process.argv[2]. The key control flow is:
argv[2] == "conssnet":
open _DC decoy if present
uptime < 300 seconds → exit immediately
otherwise block for 180,000 ms
extract support.ico; copy working files to cache
attempt Run value, task and shortcut persistence
schedule detached relaunch with _H=1
other invocation:
_H absent → spawn detached copy with _H=1, then exit
--lnk present → launch the saved browser target
attempt EtwEventWrite patch
decode and evaluate the support.ico script
The exact gate at line 121 is:
if(os.uptime()<300)process.exit(0);
It does not wait for uptime to reach five minutes. The three-minute blocking delay occurs only after that check passes. The detached relaunch is scheduled after three seconds; shortcut modification after two seconds; the installation process schedules its own exit after ten seconds. These are code paths and timer requests, not measured execution timings.
Persistence and path semantics
The decoded script attempts these changes:
| Mechanism | Observed construction |
|---|---|
| Run value | HKCU … CurrentVersion/Run: MicrosoftEdgeUpdate |
| Task | WindowsDefenderCacheVerify; hidden; PT5M / P999D |
| Task policy | IgnoreNew; description: Font cache maintenance |
| Shortcut | First successfully saved Chrome/Edge/Firefox match |
| Cache copy | %LOCALAPPDATA%/Microsoft/TextInput/Cache |
The Run command, task action, and shortcut target are all built from wd, the running script directory. Copying files into the cache does not change wd. Consequently, the presence of a cache copy does not establish that autorun targets the copied files. Hunt the actual command values and paths.
_lnk() enumerates the user Desktop followed by the public Desktop. It preserves the original target, arguments, and icon, then points the link at the loader with --lnk and the saved browser path. After a successful save, line 70 sets done=true; both loops stop. This routine modifies at most one shortcut successfully per invocation, not necessarily every installed browser. In the execution branch, the saved browser is started before decryption continues.
Several native operations discard return values or catch exceptions without reporting them. Static inspection proves that persistence is attempted, not that registry access, task registration, or shortcut saving succeeded.
ETW patch and cleanup conditions
_etw() resolves ntdll!EtwEventWrite and attempts to overwrite its entry with 31 C0 C3 (xor eax,eax; ret) using the current-process pseudo-handle. The protection calls specify two bytes, while the write specifies three; results are not checked to establish success. This is a process-local patch attempt, not evidence that all ETW or endpoint monitoring is disabled.
Cleanup depends on pdir = process.argv[3] || ''. The removal timer is only scheduled when that argument is nonempty and exists. The commonly quoted command below supplies no fourth argument, so it does not establish that cleanup executes:
SearchIndexer.exe update.dat conssnet
Second decoding boundary: XOR, RC4, then another wrapper
The constants at lines 168–169 decode to:
XOR key, ASCII (9 bytes): redteamvn
RC4 key (16 bytes): d2 6d e4 61 49 1a 71 df
92 10 06 c6 55 ec 5e f2
RC4 key, hex: d26de461491a71df921006c655ec5ef2
redteamvn is the XOR key in this script, not the RC4 key. XOR is applied cyclically, then RC4. This reproduces the supplied 790,806-byte support_decoded_rc4.bin, but that output is another JavaScript Base64/zlib wrapper, not yet the final stage3.js text. Extracting its literal and inflating it reproduces all 786,335 bytes of stage3.js.
support.ico:
d71bde27c76fe7a8216ebe239d15da1d43edfed8100f91c77fa4323a4b253261
XOR + RC4 output:
685183fa0f7776299a8008e64ea6f04e35297d5240e69c0ed298af970562868a
After Base64 + zlib (stage3.js):
cba2696f7bde7a97b0f9a802076ee42539d69ed529606c8ac979d860edf3bfd9
No eval, require, or native binding from either sample script was invoked during these checks.
Stage 4: shellcode and Chaskey instance reconstruction
The Base64 literal assigned to sc in stage3.js decodes to exactly 589,151 bytes and matches donut_shellcode.bin. The launcher requests executable memory with VirtualAlloc, copies the bytes with WriteProcessMemory(-1, ...), and passes that address to CreateThread. This describes a current-process shellcode launch. It is not proof of injection into a separate process.
The shellcode begins with E8 C0 CF 08 00: a relative call whose destination is offset 0x8CFC5. The data immediately after the five-byte instruction forms the instance. The following values were read directly:
| Field | Value |
|---|---|
| Instance start in shellcode | 0x5 |
| Instance length | 0x8CFC0 = 577,472 |
| Key at instance +0x4 | b6834ea6be6cfbf14423a625e5be6f95 |
| Counter at instance +0x14 | 01b23652601fd333e94b22e839f57045 |
| Encrypted region | Instance +0x23C; shellcode +0x241 |
| Encrypted-region length | 577,472 − 572 = 576,900 bytes |
A data-only Chaskey implementation was checked against the block test vector in Donut's upstream encryption source. Applying its counter-mode construction to this region reproduced the supplied decrypted-instance hash exactly. The procedure performed arithmetic on bytes; it did not run the shellcode or compile the Donut project.
Shellcode SHA-256:
cb0004d5cead2c6b41b9272bbc144a972cee48ffe2d39c956bc48535850b922b
Decrypted instance SHA-256:
1138f7234752dba59a2b34baab601c7b90feb9683444c52b9d7e0cf317a95ac1
Readable instance fields include ole32;oleaut32;wininet;mscoree;shell32, runtime v4.0.30319, and domain string 44CPCT3F. Donut's format definitions distinguish a .NET executable module from a native executable and define compression value 1 as no compression; numeric values should not be interpreted as “compressed” merely because they are nonzero.
Stage 5: the embedded managed PE
At offset 0x104C within the decrypted region, a 571,392-byte PE spans to the end of the 576,900-byte buffer. Its SHA-256 matches the final-payload hash recorded in the evidence notes:
32e766ccff6e88717bf8143914c8597af8cc41e70f04939b251b1b96e87ec693
That is a verification of the embedded module bytes, not execution of an EXE entry from the ZIP. Header inspection establishes:
| PE/CLR field | Observed value |
|---|---|
| DOS / PE signatures | MZ; PE signature at file offset 0x80 |
| Machine / optional header | 0x014C (I386); 0x010B (PE32) |
| Sections | .text, .rsrc, .reloc |
| CLR directory | RVA 0x2008; size 0x48; file offset 0x208 |
| CLR flags | 0x3 (ILONLY and 32BITREQUIRED) |
| Metadata | RVA 0x3D8F4; 99,008 bytes; v4.0.30319 |
| Metadata streams | #~, #Strings, #US, #GUID, #Blob |
The metadata contains Pkrizlmbayo.Properties, AesCryptoServiceProvider, GetManifestResourceStream, and GetDelegateForFunctionPointer. These names support cryptographic/resource-loading capabilities, not a demonstrated credential-theft routine. The P/Invoke rows parsed in this review identify LoadLibrary and GetProcAddress from kernel32; the broader API list in the earlier notes should not be presented as a verified import table.
The metadata parser also reported a truncated-stream warning. It returned Dsnfy.exe/Dsnfy names alongside additional module/assembly rows, whereas the notes called the internal executable Pkrizlmbayo.exe. Neither name is used here as a family identifier. Protected or malformed metadata requires more care than selecting one plausible-looking string. The earlier .NET Reactor label remains an assessment from the supplied notes, not a protector identification independently established by this review.
Managed resources
All four manifest resources were resolved from the embedded PE and compared byte-for-byte with the supplied exports:
| Manifest resource | Offset in resource area | Bytes | Match |
|---|---|---|---|
| MPdrB4P7jF5djsh28V.IjKIOP9tPG0difdnB9 | 0 | 211,649 | Exact |
| Mtb7al5gakrS5xeEO9.bn8CZ63qY8rZE71Rld | 211,653 | 10,623 | Exact |
| iXw09rCLJt8nSV7x0U.mXnsCubMdeKoeQ5tGf | 222,280 | 2,464 | Exact |
| P0hldaWcFviNQs6vh7.ZuUPbHkdWeZRZttcuV | 224,748 | 394 | Exact |
Each successive offset includes the preceding resource's four-byte length prefix. Matching these exports confirms their relationship to the embedded assembly; it does not establish what the opaque resources do. No reported C2 IP was found as an ASCII or UTF-16LE literal in the module. That negative string check does not prove the address is absent from an encoded configuration.
Campaign correlation and C2 confidence
CyberArmor's July 2026 report describes a similar Word/Node.js/Donut chain and reports a protected .NET stealer/backdoor communicating with 15.235.156[.]143:56001. Its sample filenames and hashes differ. The overlap supports comparison, not a proven common operator or identical final behavior.
Cyble's research describes compromised LinkedIn accounts and recruitment-themed delivery in PXA activity. It provides social-engineering context; it does not identify this module as PXA Stealer.
The following indicators are external-report correlations only:
Reported C2: 15.235.156[.]143:56001 (TCP)
Reported short URL: hxxps://tr[.]ee/Gwj7Vr
Reported drop URL:
hxxps://apexglobals1607.talaphantien.workers[.]dev/share/6620e42d1225d70e108e6220?cid=50rzk37oq4j3
They were not recovered as this sample's configuration or contacted during analysis. Infrastructure reuse and redteamvn do not establish geographic or actor attribution. A shared shortener or hosting domain should not be blocked wholesale on the strength of one campaign URL.
Detection pivots from verified code
The strongest pivots combine process identity with script arguments, module location, or persistence targets:
- A Node.js host named
SearchIndexer.exeexecutingupdate.dat, especially withconssnetor--lnk. The filename alone is not malicious. MicrosoftEdgeUpdateunderHKCU\Software\Microsoft\Windows\CurrentVersion\Runreferencing that host/script combination. Inspect the actual working path, not just the cache directory.WindowsDefenderCacheVerifytask registration with description “Font cache maintenance,” a five-minute repetition interval, and the same host/script action.- A Desktop
.lnktargeting the loader and retaining the original browser path after--lnk. One modified shortcut can be the expected outcome of this routine. - The co-location of
update.dat,support.ico, andkoffi.node, coupled with the verified hashes above.
From the initial loader notes, Word loading AppvIsvSubsystems32.dll from a user-writable application directory is an additional pivot. The following original-file hashes are transcribed, not recomputed in this review:
Original delivery ZIP:
8a2cbe7cad2b77a98e40cde1a0d000668af6564bc7ebed15b57678c248bedbc2
Malicious AppvIsvSubsystems32.dll:
4659cf9ebd12482ffd490e1bd02d762187513106a30a4fd7b5b51133a992c115
Original ~WRDCA68.pdf:
c9a52b717e6f35a946659424431fffa05aff6f32eb01f34912ca13e361cc144d
The reported %APPDATA%\Microsoft\Protect\UserData path and C:\Users\Public\rt39_debug.txt string also originate in those earlier DLL notes. Their presence on an endpoint must be investigated, not assumed. Legitimate Word/runtime hashes are not included as malicious-file blocking indicators.
Response and practical precautions
Verify unexpected recruitment through the employer's official careers channel. Do not execute a file presented as a job description merely because a familiar account sent it or a signed program is inside the download.
If the package was executed, isolate the affected endpoint and involve incident response. From a clean device, secure potentially exposed accounts and revoke active sessions. Preserve evidence before cleanup; check Run values, task actions, and desktop shortcut targets. Deleting the original ZIP does not remove these mechanisms or establish that the host is clean.
Reproducibility and remaining limits
The static verification results record sizes, hashes, transformation matches, and the embedded PE header checks. Managed-resource verification records the four exact resource matches and the metadata-parser limitation. These are data-only reports; executable samples and decoded malware scripts are not published with the article.
The chain from the supplied update.dat and support.ico resources through the embedded .NET module is now reproduced statically. What remains unresolved is the module's ultimate functionality and decoded configuration, including any C2; the full redirect path; and revalidation of the absent original DLL/PDF. Successful compromise, persistence, exfiltration, and attribution cannot be inferred from a decryption result alone.