// 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 musl and 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:

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

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

bash
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:

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

Need custom tooling built for your engagement? ← Back to resources
↑