Rust's package registry has long been considered one of the safer corners of the OSS ecosystem. Mandatory Cargo.toml validation, no install scripts, and a small, vigilant maintainer community kept the public incident count low for years. That changed on September 24, 2025, when the crates.io team removed two crates — faster_log and async_println — after a Socket disclosure showed they had been quietly searching developer filesystems for Ethereum and Solana private keys since being published in May. The combined download count was 8,424 by the time of removal, and the crates were among the first explicitly wallet-targeting Rust supply chain incidents to receive a published RustSec advisory.
How did the crates get on the registry?
Both packages were published on May 25, 2025 under the aliases rustguruman and dumbnbased. They typosquatted two legitimate logging crates: fast_log (with an underscore in the same place but no extra "er") and async-println (using a hyphen rather than an underscore — crates.io normalizes both for search but treats them as distinct names). The attacker copied the README, the example code, the feature flags, and even the documentation comments from the legitimate crates verbatim, then injected a single new module that ran during macro expansion. Because Rust's compile-time code execution model gives proc-macros access to the filesystem, the payload could collect data during cargo build without needing an install hook.
What did the payload do?
The malicious code searched every file under the project directory and a small set of common wallet paths for byte patterns that match Ethereum (32-byte secp256k1 private keys) and Solana (64-byte ed25519 keypair seeds). When it found a match, the bytes were posted to a hardcoded webhook in a request body that mimicked a legitimate logging payload — JSON with a level field set to info and the stolen key inside a message field, so a network IDS scanning for "key", "wallet", or "secret" strings would not match.
// Reconstructed scanner behavior (redacted)
fn scan_file(path: &Path) -> Option<Vec<u8>> {
let bytes = fs::read(path).ok()?;
// Ethereum: 32 byte private key entropy heuristic
for window in bytes.windows(32) {
if is_high_entropy(window) && passes_secp256k1_modulus(window) {
return Some(window.to_vec());
}
}
// Solana: ed25519 64-byte secret seed pattern
for window in bytes.windows(64) {
if is_solana_keypair(window) {
return Some(window.to_vec());
}
}
None
}
Why was this campaign different from other typosquats?
Three things stand out. First, the targeting was sophisticated — the attacker did not cast a wide net, did not exfiltrate environment variables, did not phone home unless a wallet was actually found. That makes the campaign noise-free in defender telemetry until the moment of impact. Second, the operator used proc-macro execution rather than build scripts, evading scanners that focus on build.rs. Third, the malicious crates remained available for four full months — May 25 to September 24 — because nothing about their behavior triggered crates.io's automated abuse heuristics, and the legitimate crates being typosquatted have low enough usage that the typosquats did not stand out as fraudulent in download statistics alone.
How was it ultimately caught?
Kirill Boychenko at Socket noticed the crates while triaging an unrelated alert about a Solana developer environment that had outbound traffic to an unexpected webhook. Working backward from the network connection, his team identified the offending proc-macro, then queried crates.io's reverse-dependency index for crates that imported the suspicious helpers. That gave them the full set of artifacts. Socket notified the crates.io team on September 24; the crates were yanked the same day, the publisher accounts frozen, and a RustSec advisory (RUSTSEC-2025-0044) was filed within 24 hours.
What follow-on crates appeared?
The same operator profile reappeared in December 2025 with evm-units (13 versions, 7,257 downloads) and uniswap-utils (14 versions, 7,441 downloads), both targeting Web3 developers and both using a proc-macro payload that downloaded a second-stage binary keyed to the host OS. A separate operator using the alias mukulljangid (the same alias that appeared in the xrpl.js npm compromise of April 2025) published finch_cli_rust, finch-rst, and sha-rst on December 9, impersonating the legitimate finch and finch_cli crates from a Swedish genomics project. By February 2026, the crates.io team had updated its malicious-crate notification policy: routine takedowns no longer get individual blog posts but always receive a RustSec advisory, which is the integration point that automated scanners can consume.
# Hunt for the known-bad crates in your Cargo.lock
grep -E '^name = "(faster_log|async_println|evm-units|uniswap-utils|finch_cli_rust|finch-rst|sha-rst)"' Cargo.lock
# Audit for proc-macro crates that ship with no upstream repository link
cargo metadata --format-version 1 | jq -r '
.packages[] | select(.targets[] | .kind | contains(["proc-macro"]))
| select(.repository == null or .repository == "")
| "\(.name)@\(.version)"'
What should Rust developers do now?
Three immediate actions. First, audit Cargo.lock against the published IOC list and re-run cargo update --dry-run to see whether any transitive bumps would re-introduce a flagged version. Second, treat proc-macro dependencies with the same scrutiny as build scripts: any proc-macro without a verified upstream repository, a Sigstore attestation, or a maintainer with prior release history should fail review. Third, subscribe to RustSec advisories through cargo audit --json in CI — that single integration would have caught all the December 2025 wallet-stealer waves at the moment they were disclosed.
How Safeguard Helps
Safeguard's crates.io provider plugin watches RustSec advisories, OSV, and the Socket malicious-package feed; the four known-bad crates from this campaign were ingested within 90 minutes of the RustSec filing. Griffin AI inspects proc-macro crates for filesystem reads outside the build directory, outbound network calls during macro expansion, and reflective binary downloads — the exact behaviors faster_log used. Policy gates block new top-level Rust dependencies that lack a Sigstore attestation or a verified GitHub repository link, killing the typosquat-with-no-upstream pattern. The malicious-package feed cross-references publisher aliases across npm, PyPI, and crates.io so when one operator's alias appears in a new ecosystem, related historical compromises light up immediately — useful for tracing operators like mukulljangid who jump from npm in April to crates.io in December.