On July 13, 2026, new-blog.artlist[.]io served an obfuscated script at the top of its HTML. The primary artlist[.]io site was clean when checked. The injected code queried a Polygon smart contract, recovered auth-code-check[.]info , and loaded a fake reCAPTCHA page that instructed visitors to paste a PowerShell command into Windows Terminal.
The last DLL is a full remote-access payload. Its mapped command paths cover browser-store collection, protected outbound reporting, resumable file transfer, process control, shell commands, remote desktop, keylogging, clipboard access, SOCKS bridging, service installation, and in-memory PE delivery. Its endpoint scheduler can rotate between a C2-supplied direct table, four embedded IPv4 fallbacks, and four .onion addresses reached through a dynamically delivered Tor module.
A controlled replay also exposed state in the delivery backend. An old archive token returned 410 Gone one second after its /p/ marker was called. A fresh token delivered the archive when requested before /p/ . The marker is a strong candidate for consuming or closing delivery state, but the replay did not isolate it from token age, request history, source address, visitor ID, or timing.
This report follows the bytes from the injected page to the final native command surface. The recovered artifacts were analyzed statically. They were not executed.
This investigation was conducted with Hudson Rock (opens in new tab) . Thank you to their team for the opportunity to work the case and publish these technical findings. Their companion report (opens in new tab) covers the infostealer evidence and credential-compromise context that led to the campaign's discovery.
The same injected script appeared at the top of multiple pages on the WordPress subdomain. That placement indicates an injection upstream of individual post content. A theme, plugin, tag manager, early rendering hook, CDN rule, edge worker, or publishing component could all produce that shape. The affected HTML proves broad placement on the observed subdomain; it does not identify the compromised component.
The script inserted at the top of the page used a small decoder:
After base64 decoding, each byte is XORed with 0x50 . TextDecoder turns the result into JavaScript and new Function executes it. The decoded loader sets a page-level guard before doing any network work:
The loader does not carry the -stage host directly. It sends an eth_call with selector 0xb68d1809 to Polygon contract 0xB6bC9e1D0b2fB96Ab7C47E04Cb0BE477410bC1f2 . Eight Polygon RPC endpoints provide failover, including Tenderly, dRPC, QuickNode, 1RPC, Ankr, PublicNode, and Blast.
The contract returned:
The loader appended the campaign token and a minute-based cache buster:
That is the EtherHiding part of the chain. The compromised page stores the contract, selector, and RPC list. The contract stores the replaceable delivery host. Moving the stage does not require another edit to the injected page.
Two minute-versioned api.php responses had different wrapper hashes but decoded to the same 24 KB script structure. The wrapper's XOR key rotated between responses.
The decoded script creates a full-page Shadow DOM overlay that imitates reCAPTCHA. It localizes the prompt, blocks the context and common inspection shortcuts, and walks the visitor through Windows + X , Terminal, Ctrl + V , and Enter . A host- and campaign-specific localStorage key limits repeated display.
The script also creates a visitor ID, sends dl , cb , and check telemetry to api.php , and prefers AES-GCM-protected JSON before falling back to query parameters. The visible verification step is theater. The clipboard receives PowerShell:
The -w h option hides the PowerShell window. irm retrieves the ray-specific response and iex executes it.
The HTTPS ray response is another XOR-obfuscated PowerShell wrapper. It creates a WinHttp.WinHttpRequest.5.1 COM object and pivots to an explicit HTTP URL:
The second response unwraps the downloader. It waits 20 seconds, creates a random temporary directory, downloads 7z.exe , retrieves the archive, and extracts it with password Arsen33 . It searches recursively for fdupdate.exe , calls Start-Process when it finds the file, deletes the archive, and finally requests /p/ .
The fresh paths were:
The /p/ request is unconditional after the launch block. It marks the attempt, not proven successful execution.
The first archive replay failed because the request order did not match the downloader.
At 20:11:03 UTC , /p/ returned 200 ok . One second later, the archive endpoint returned 410 Gone . An action=check request later reported both fetched and consumed as true. Changing the User-Agent, scheme, query, and callback shape did not restore the old token.
A fresh browser registration produced a new ray and archive token. This time the replay preserved the 20-second delay, fetched /7z , requested the archive before /p/ , and received the 32,713,067-byte ZIP.
No successful response in that sequence set a cookie. A failed .zip suffix variant alone returned 403 and set xdv_sess , which appears to be a separate denial route.
The replay proves that delivery was active and that the backend tracked request state. It does not prove that /p/ alone consumed the token. Token age, request history, source address, visitor ID, and timing remain possible inputs.
fdupdate.exe contains coherent updater code, a StruSoft mutex, references to fdupd.ini and version.ini , and normal WinINet update routines. The malicious behavior enters through its local dependency graph:
renderengine.dll imports LibBind.dll::group_by_u . LibBind.dll also reaches the same function from a worker thread created during process attach.
The LibBind.dll entry point at 0x1800059cc reaches helper 0x180001328 , which creates a thread at 0x180001a64 . That thread calls group_by_u at 0x1800017e0 behind a one-time guard.
The loader opens Stream.Toolkit.dat , seeks to offset 0x4c26a , reads 0x533a3 bytes, and XORs every byte with 0xe5 . The decoded 340,899-byte artifact has SHA-256:
TBFVSS_DLL_SRV_64.dll::data_process allocates 0x208000 bytes and initially fills the region with random data. LibBind.dll copies the decoded payload into an offset within that allocation. Export Ux changes the allocation to PAGE_EXECUTE_READ and passes the decoded pointer to EnumTimeFormatsEx as its callback.
The code is not hiding behind an exotic execution primitive. It is using a Windows enumeration API as a clean indirect call into decoded shellcode.
The decoded shellcode manually resolves APIs and searches module-derived paths for files with these extensions:
Candidates are passed to a custom storage validator with key 0x1b51b9fd . Face.dat is the file that passes.
Its table contains 1,178 records. The table begins at 0x78 , uses a 0x58 -byte stride, and ends at 0x19568 . Record payloads follow in table order and consume the file exactly. Every record in this sample enables a reversible byte transform driven by metadata byte +0x23 :
Most records carry no payload or eight bytes. Two contain almost everything useful:
The script waits two seconds, repeatedly repairs HKCU Run persistence every 125 seconds, repairs scheduled-task persistence every 675 seconds, loads the @input resource through previewer::load_exe , and then waits indefinitely. Both persistence records use the display/value string:
The surrounding shellcode provides the interpreter behind that script. Its mapped command groups include HKCU and HKLM Run values, Startup-folder shortcuts, Windows services, Task Scheduler, native in-memory loading, CLR hosting, anti-VM checks, and timer queues. The embedded script selects only a subset for this sample. Capability in the interpreter is not the same as activation by the recovered script.
Record 1168 decodes to an unsigned Delphi x64 executable reporting the name act.exe . Its entry path resolves CryptoAPI and decrypts an embedded blob at file offset 0x42670 , length 0x592340 .
Static replay produced a valid ZIP containing one LZMA-compressed member:
The raw member is a position-independent loader. It walks the PEB for kernel32.dll , resolves exports with a CRC-32-like hash, and decodes an embedded image at offset 0xc48 . The loop processes 0x873c00 bytes with:
That transform produces the final PE32+ DLL:
The raw loader allocates an RWX image, copies headers and sections, resolves imports, applies DIR64 relocations, registers unwind metadata with RtlAddFunctionTable , runs TLS callbacks, and calls the mapped DLL entry point.
From the injected page to this point, the chain has crossed six distinct decoding or loading boundaries. Each one removes useful metadata. The final DLL puts the behavior back on the table.
The DLL supports rundll32 , regsvr32 , service, and svchost -style startup. Its main worker can register a service table or create a hidden window. The window procedure at 0x00b46070 accepts encrypted WM_COPYDATA , validates a fixed 0x1c7 -byte header, and dispatches internal commands.
Command 6 starts worker 0x00b4e470 , which launches scheduler 0x00b2dd00 . The scheduler rotates connection records across three endpoint families:
A successful 0x119 -byte record is cached and retried before the normal rotation.
The initializer sets subtractor 0x43 . Applying it to the encoded constants produces:
The Tor seeder writes four candidates:
The Tor module is not embedded in the static DLL. A non-32-byte response to request 0xfd0 / 1 is stored as encrypted event type 9 . Fallback worker 0x00b29780 retrieves that event, decrypts it, manually maps the returned PE, resolves tor_main , seeds the .onion records, and starts the local SOCKS listener. The default local port field is 0x235a .
The client sends a 0x94 -byte protected outer header followed by a protected body. Before encryption, the header carries:
For each protected body, the client generates a fresh AES-256 key, encrypts the data, wraps the AES key with the recipient RSA public key, and appends the wrapped material. The handshake generates a 1024-bit RSA exchange pair, sends the client public key under an embedded server key, and verifies a signed server response before marking the session active.
This establishes client-side encryption, framing, signature verification, and length bounds. It does not reveal how the server stores private keys or names fields internally.
The network dispatcher at 0x00b1f010 reads its selector from command-frame offset +0x18 . The mapped branches are:
Selector 0x34 gives the clearest collection path. Worker 0x00b1dd80 emits tagged records for Chromium Web Data and Login Data , payment and autofill material, token-service data, Firefox-style cookies.sqlite , and host inventory. It then sends the fixed session context and serialized report over the protected mode- 2 channel.
The static code proves that these stores are targeted, packaged, and sent. The exact recovered fields depend on the victim's browser state and host protections. This analysis did not demonstrate successful decryption of App-Bound or other host-protected browser secrets.
Other xref-backed paths install a WH_KEYBOARD_LL hook, read Unicode or ANSI clipboard contents, capture desktop images and AVI video, create auto-start services, and allocate and write memory in remote processes. PFXExportCertStoreEx and NCryptExportKey paths can export private-key-capable PFX material, but no activation route from the mapped command surface reaches that code. It remains capability without a recovered trigger.
The injected page is only the front door. Its EtherHiding loader keeps the replaceable delivery host in a Polygon contract, while the ClickFix overlay moves execution through the visitor's clipboard. The PowerShell backend then uses ray and archive tokens to deliver a signed updater bundle under request-state controls.
Inside the archive, the chain trades names and formats repeatedly: updater DLLs, an XOR slice in Stream.Toolkit.dat , a custom record store in Face.dat , a Delphi executable, an encrypted ZIP, raw position-independent code, and finally a manually mapped DLL. The layers are noisy. The handoffs are stable.
For hunting, chase those handoffs: the Polygon contract and selector, the auth-code-check[.]info ray/token grammar, password Arsen33 , the LibBind.dll to Stream.Toolkit.dat offset and XOR, the Face.dat key, the act.exe AES seed, and the final 0x43 ^ index PE decode. Any one filename can move. That sequence is much harder to repaint.
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
