// resources · engineering
Building Memory-Safe Offensive Tooling in Rust
Offensive tools fail at the worst possible moment — mid-engagement, against a live target. Here's why we write ours in Rust, and the patterns that keep them reliable under fire.
The tool that segfaults at 2 a.m.
Every operator has a story about the custom scanner that died halfway through an authorized engagement — a buffer overrun in a hand-rolled parser, a use-after-free in a threaded socket handler, a heap corruption that only shows up against one weird target. When your own tooling crashes against a live production system, you don't just lose time. You lose the evidence trail, you risk leaving the target in an odd state, and you spend the rest of the engagement wondering whether the last result was real.
Memory-unsafe languages put the entire burden of correctness on the person writing the tool at 2 a.m. under deadline. Rust moves most of that burden into the compiler, where it belongs.
What memory safety actually buys an offensive engineer
This isn't about ideology. It's about a tool that behaves the same on its tenth run as its first:
- No use-after-free, double-free, or buffer overrun in safe code — the exact bug classes that make hand-rolled C tooling flaky against hostile input.
- Fearless concurrency. Fan out thousands of concurrent probes without data races; the borrow checker rejects the shared-mutable-state mistakes that cause heisenbugs.
- One static binary. Target
musland you get a single file you can drop onto a jump host with no runtime, no interpreter, no dependency hell. - A type system that models protocols. Parse once, validate at the boundary, and the rest of the tool can trust its own data.
A recon worker that doesn't fall over
Async Rust makes high-concurrency scanning cheap. A single probe is a few lines, with an explicit timeout so a black-holed host can never hang the run:
use tokio::{net::TcpStream, time::{timeout, Duration}};
async fn probe(addr: &str) -> bool {
matches!(
timeout(Duration::from_millis(800), TcpStream::connect(addr)).await,
Ok(Ok(_))
)
}
Wrap that in a Semaphore to bound concurrency, spawn one task per target, and you have a scanner that saturates the network without exhausting file descriptors or racing on shared state. The failure mode of a slow target is a clean false, not a hung process.
Parsing hostile input safely
The most dangerous code in any offensive tool is the part that parses what the target sends back — banners, protocol responses, file formats you don't control. In C, a malformed banner is a crash waiting to happen. In Rust, you parse into a typed structure and the error path is explicit:
fn parse_banner(buf: &[u8]) -> Option<Service> {
let text = std::str::from_utf8(buf).ok()?;
let (proto, version) = text.split_once('/')?;
Some(Service {
proto: proto.trim().into(),
version: version.trim().into(),
})
}
No pointer arithmetic, no length miscalculation, no silent overrun. A bad banner returns None — it does not corrupt the process or hand an attacker control of your own tool.
Ship a single binary
Distribution is where a lot of tooling falls apart. A static build removes the problem entirely:
cargo build --release --target x86_64-unknown-linux-musl
Cross-compile for the target architecture, copy one file across, run it. No Python runtime to install on a hardened host, no library versions to fight. On a locked-down box, "it's a single static binary" is often the difference between running your tool and not.
Modularity: the Rustsploit approach
The framework I maintain, Rustsploit, treats every capability — a scanner, an exploit, a post-exploitation module — as a trait object behind one common interface. New modules plug into the same runner, share the same reporting, and the compiler guarantees they honor the contract:
pub trait Module {
fn name(&self) -> &str;
fn run(&self, ctx: &Context) -> Result<Findings, ModuleError>;
}
Adding a module cannot silently break the others, because the type system enforces the boundary between them. That is what lets a framework grow to hundreds of modules without collapsing under its own weight.
Discipline still matters
Memory safety makes a tool reliable; it does not make it authorized. Every engagement is scoped in writing, every target is inside the rules of engagement, and the client keeps the source to whatever we build. Rust makes a tool trustworthy to run. The paperwork makes it legal to run. Both are non-negotiable.
Bottom line
The right language for an offensive tool is the one that behaves predictably exactly when it matters most — under load, against hostile input, on an unfamiliar host. For us that is Rust: memory-safe by default, fast enough that performance is never the excuse, and portable down to a single file you can trust on a client's network.