Umair Sabir
← Back to blog
Nov 11, 2026·Malware Analysis, Reverse Engineering, DFIR

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.

Redacted LinkedIn job lure and overview of the Word, Node.js and Donut stages
Fig. 1.Editorial graphic based on the received message. Names and the application URL are redacted. The screenshot was reformatted for presentation; this is not a forensic original. The account owner was another victim.

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.

LayerVerification
update.datBase64/zlib decoding reproduces update_decoded.js exactly
support.icoXOR → RC4 → Base64/zlib reproduces stage3.js exactly
stage3.jsBase64 literal decodes to the supplied 589,151-byte shellcode
Donut instanceChaskey CTR reproduces all 576,900 decrypted bytes
Embedded PE571,392 bytes; SHA-256 and PE/CLR structures verified
Managed resourcesAll four byte arrays match their supplied exports
C2 / theft / remote controlNot 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.dll from a disk copy, consistent with an attempt to remove user-mode hooks.
  • Process-creation attributes that select explorer.exe as 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.

PairName exportData exportData bytes
0rec0: DECOY:…Document.docxrec1: decoy DOCX55,772
1rec2: SearchIndexer.exerec3: host (excluded)65,568,976
2rec4: update.datrec5: script wrapper5,373
3rec6: koffi.noderec7: addon (excluded)1,022,464
4rec8: support.icorec9: encrypted script790,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:

MechanismObserved construction
Run valueHKCU … CurrentVersion/Run: MicrosoftEdgeUpdate
TaskWindowsDefenderCacheVerify; hidden; PT5M / P999D
Task policyIgnoreNew; description: Font cache maintenance
ShortcutFirst 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:

FieldValue
Instance start in shellcode0x5
Instance length0x8CFC0 = 577,472
Key at instance +0x4b6834ea6be6cfbf14423a625e5be6f95
Counter at instance +0x1401b23652601fd333e94b22e839f57045
Encrypted regionInstance +0x23C; shellcode +0x241
Encrypted-region length577,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 fieldObserved value
DOS / PE signaturesMZ; PE signature at file offset 0x80
Machine / optional header0x014C (I386); 0x010B (PE32)
Sections.text, .rsrc, .reloc
CLR directoryRVA 0x2008; size 0x48; file offset 0x208
CLR flags0x3 (ILONLY and 32BITREQUIRED)
MetadataRVA 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 resourceOffset in resource areaBytesMatch
MPdrB4P7jF5djsh28V.IjKIOP9tPG0difdnB90211,649Exact
Mtb7al5gakrS5xeEO9.bn8CZ63qY8rZE71Rld211,65310,623Exact
iXw09rCLJt8nSV7x0U.mXnsCubMdeKoeQ5tGf222,2802,464Exact
P0hldaWcFviNQs6vh7.ZuUPbHkdWeZRZttcuV224,748394Exact

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:

  1. A Node.js host named SearchIndexer.exe executing update.dat, especially with conssnet or --lnk. The filename alone is not malicious.
  2. MicrosoftEdgeUpdate under HKCU\Software\Microsoft\Windows\CurrentVersion\Run referencing that host/script combination. Inspect the actual working path, not just the cache directory.
  3. WindowsDefenderCacheVerify task registration with description “Font cache maintenance,” a five-minute repetition interval, and the same host/script action.
  4. A Desktop .lnk targeting the loader and retaining the original browser path after --lnk. One modified shortcut can be the expected outcome of this routine.
  5. The co-location of update.dat, support.ico, and koffi.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.

For more posts like this, visit the blog or subscribe to the RSS feed.