Greetings, fellow hackers and aspiring pen testers! Today, we’re going to 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!

Introduction

Rust is a systems programming language that aims to provide memory safety, concurrency, and performance. It was designed to prevent common issues like buffer overflows, null-pointer dereferences, and use-after-free vulnerabilities. By doing so, Rust brings security and safety to a whole new level, making it a perfect choice for security-minded hackers.

Originally developed by Graydon Hoare at Mozilla Research, Rust has gained significant traction since its first stable release in 2015. With a strong focus on performance, reliability, and productivity, Rust has been used in various projects, including operating systems, web browsers, game engines, and more.

But enough with the history lesson; let’s get our hands dirty with some Rust code!

Getting Started with Rust

Before we dive into Rust’s syntax and capabilities, let’s first set up our environment. Installing Rust is a breeze, thanks to the rustup tool. To install Rust, simply follow the instructions on the official Rust website.

Once you have Rust installed, you can start writing Rust code with any text editor. However, I recommend using an Integrated Development Environment (IDE) like Visual Studio Code with the Rust extension for a more interactive and productive experience.

Now that we’ve got our environment set up, let’s start exploring Rust’s basic building blocks.

Basic Syntax

Variables and Data Types

Variables

In Rust, variables are immutable by default, which means their values cannot be changed after being assigned. This design choice enforces a discipline of thinking carefully about the data you are working with and prevents accidental modifications. To declare a mutable variable, use the mut keyword.

fn main() {
    let x = 5;
    println!("The value of x is: {}", x);
    x = 6; // This will result in a compilation error
}

To fix the compilation error, we need to declare x as mutable:

fn main() {
    let mut x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}

Data Types

Rust is a statically typed language, which means it checks the types of your variables at compile time. It has several built-in data types, including integers, floating-point numbers, booleans, characters, and strings.

Integers

Integers in Rust can be either signed (i8, i16, i32, i64, i128) or unsigned (u8, u16, u32, u64, u128). The default integer type is i32, as it offers the best performance on most systems.

fn main() {
    let a: i8 = -128;
    let b: u16 = 65535;
    let c: i32 = 2147483647;
}

Floating-Point Numbers

Rust has two floating-point types: f32 (single-precision) and f64 (double-precision). The default type is f64, as it provides more precision and is roughly as fast as f32 on modern hardware.

fn main() {
    let d: f32 = 3.14;
    let e: f64 = 2.71828;
}

Booleans

Booleans in Rust are represented by the bool type and can have a value of either true or false.

fn main() {
    let f: bool = true;
    let g: bool = false;
}

Characters

Characters in Rust are represented by the char type and use single quotes. Rust’s char is a 4-byte Unicode scalar value, which allows it to represent a wide range of characters.

fn main() {
    let h: char = 'a';
    let i: char = '🚀';
}

Strings

There are two types of strings in Rust: String and &str. The String type is a growable, mutable, and UTF-8 encoded string, while the &str type is a string slice, which is a view into a String.

fn main() {
    let j: String = String::from("Hello, Rust!");
    let k: &str = "Hello, world!";
}

Compound Types

Rust also provides compound data types like tuples and arrays.

Tuples

A tuple is a collection of values with different types. They have a fixed length, and you can access their elements using dot notation and an index.

fn main() {
    let l: (i32, f64, char) = (42, 3.14, 'Z');

    let (x, y, z) = l; // Destructuring a tuple
    println!("The value of y is: {}", y);

    println!("The value of z is: {}", l.2); // Accessing tuple elements using dot notation
}
Arrays

Arrays in Rust are fixed-length collections of elements with the same type. They are stored on the stack and cannot grow or shrink.

fn main() {
    let m: [i32; 5] = [1, 2, 3, 4, 5];
    let n: [i32; 3] = [0; 3]; // Creates an array of 3 elements with the value 0

    println!("The value of m[0] is: {}", m[0]);
}

Operators

Rust supports various arithmetic, comparison, logical, and bitwise operators.

Arithmetic Operators

Rust provides the standard arithmetic operators: addition (+), subtraction (-), multiplication (*), division (/), and remainder (%).

fn main() {
    let a = 5 + 3;
    let b = 5 - 3;
    let c = 5 * 3;
    let d = 5 / 3;
    let e = 5 % 3;
}

Comparison Operators

Comparison operators in Rust include: equal to (==), not equal to (!=), less than (<), less than or equal to (<=), greater than (>), and greater than or equal to (>=).

fn main() {
    let a = 5 == 3;
    let b = 5 != 3;
    let c = 5 < 3;
    let d = 5 <= 3;
    let e = 5 > 3;
    let f = 5 >= 3;
}

Logical Operators

Rust has three logical operators: AND (&&), OR (||), and NOT (!).

fn main() {
    let a = true && false;
    let b = true || false;
    let c = !true;
}

Bitwise Operators

Bitwise operators in Rust are used for performing operations on binary representations of integers. The available bitwise operators are: AND (&), OR (|), XOR (^), left shift (<<), and right shift (>>).

fn main() {
    let a = 5 & 3;
    let b = 5 | 3;
    let c = 5 ^ 3;
    let d = 5 << 1;
    let e = 5 >> 1;
}

Control Structures

Control structures are essential for directing the flow of your program. Rust provides various control structures, including conditionals and loops.

Conditionals

Rust uses the standard if and else constructs for conditionals. Additionally, Rust supports else if for chaining multiple conditions.

fn main() {
    let a = 5;

    if a < 3 {
        println!("a is less than 3");
    } else if a > 3 {
        println!("a is greater than 3");
    } else {
        println!("a is equal to 3");
    }
}

Loops

Rust has three types of loops: loop, while, and for.

Loop

The loop construct creates an infinite loop. Use the break keyword to exit the loop.

fn main() {
    let mut counter = 0;

    loop {
        counter += 1;

        if counter >= 5 {
            break;
        }
    }
}
While

The while loop executes a block of code as long as the given condition is true.

fn main() {
    let mut counter = 0;

    while counter < 5 {
        counter += 1;
    }
}
For

The for loop is used for iterating over a range or a collection of elements, like arrays or iterators.

fn main() {
    let arr = [1, 2, 3, 4, 5];

    for element in arr.iter() {
        println!("The value is: {}", element);
    }

    for number in (1..4).rev() {
        println!("{}!", number);
    }
}

Functions

Functions in Rust are declared with the fn keyword, followed by the function name, a parameter list, a return type, and a block of code.

fn main() {
    let x = 5;
    let y = 3;

    let sum = add(x, y);
    println!("The sum of x and y is: {}", sum);
}

fn add(a: i32, b: i32) -> i32 {
    a + b // No semicolon at the end because it's an expression
}

Function Parameters and Return Values

Function parameters in Rust are declared with their names and types, separated by a colon. Multiple parameters are separated by commas. The return type is specified using the -> symbol, followed by the type.

fn add(a: i32, b: i32) -> i32 {
    a + b
}

Expressions and Statements

In Rust, an expression is a piece of code that returns a value, whereas a statement is a piece of code that performs an action and does not return a value.

fn main() {
    let x = 5; // Statement
    let y = { // Expression
        let z = 3;
        z + 1
    };
}

Rust for Pen Testing and Red Teaming

Now that we have a solid grasp of Rust’s basic concepts, let’s dive into how we can use Rust for pen testing and red teaming. Rust is not only powerful and efficient, but it also provides a safe and reliable environment for building security tools and exploits.

Building a Simple Port Scanner

A common task for pen testers and red teamers is to scan a target’s open ports. Let’s build a simple port scanner using Rust to showcase its potential in security tasks. We’ll be using the tokio and tokio-util crates to handle asynchronous tasks and timeouts.

First, add the required dependencies to your Cargo.toml file:

[dependencies]
tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.6", features = ["timeout"] }

Now let’s create our port scanner:

use std::env;
use std::net::SocketAddr;
use tokio::net::TcpStream;
use tokio_util::time::timeout;

#[tokio::main]
async fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() != 3 {
        eprintln!("Usage: port_scanner <target> <port_range>");
        return;
    }

    let target = &args[1];
    let port_range: Vec<u16> = args[2]
        .split('-')
        .map(|s| s.parse().expect("Invalid port range"))
        .collect();

    if port_range.len() != 2 {
        eprintln!("Usage: port_scanner <target> <port_range>");
        return;
    }

    let (start_port, end_port) = (port_range[0], port_range[1]);

    println!("Scanning target: {}", target);
    println!("Port range: {}-{}", start_port, end_port);

    for port in start_port..=end_port {
        let addr = format!("{}:{}", target, port).parse::<SocketAddr>().unwrap();
        let timeout_duration = std::time::Duration::from_secs(1);
        let stream = TcpStream::connect(addr);

        match timeout(timeout_duration, stream).await {
            Ok(Ok(_)) => {
                println!("Port {} is open", port);
            }
            _ => {
                // Port is closed or timed out
            }
        }
    }

    println!("Scan complete");
}

Crafting Custom TCP Packets

Another powerful use case for Rust in pen testing and red teaming is crafting custom TCP packets. We’ll be using the pnet crate to build and send TCP packets. First, add the required dependency to your Cargo.toml file:

[dependencies]
pnet = "0.27"

Now let’s create a simple TCP packet:

use pnet::packet::tcp::TcpPacket;
use pnet::packet::MutablePacket;
use pnet::transport::{self, TransportProtocol::Ipv4};
use pnet::transport::transport_channel;
use std::net::Ipv4Addr;

fn main() {
    let protocol = transport::TransportProtocol::Ipv4(Ipv4(6)); // 6 is the protocol number for TCP
    let (mut tx, _) = transport_channel(4096, protocol).unwrap();

    let source = Ipv4Addr::new(192, 168, 1, 100);
    let destination = Ipv4Addr::new(192, 168, 1, 101);

    let mut buffer = [0u8; 20];
    let mut tcp_packet = MutableTcpPacket::new(&mut buffer).unwrap();

    tcp_packet.set_source(12345);
    tcp_packet.set_destination(80);
    tcp_packet.set_data_offset(5);
    tcp_packet.set_window(1024);

    // Set the SYN flag to initiate a TCP connection
    tcp_packet.set_flags(pnet::packet::tcp::TcpFlags::SYN);

    // Calculate and set the checksum
    let checksum = pnet::packet::tcp::ipv4_checksum(
        &tcp_packet.to_immutable(),
        &source,
        &destination,
    );
    tcp_packet.set_checksum(checksum);

    // Send the TCP packet
    let _ = tx.send_to(tcp_packet, transport::Ipv4Addr(destination));

    println!("Custom TCP packet sent");
}

This example sends a simple TCP SYN packet to the target IP address, which can be used to initiate a TCP connection.

Pros and Cons of Rust for Pen Testers and Red Team Members

While Rust is a powerful and efficient language with many advantages, it also has some drawbacks when it comes to pen testing and red teaming. Let’s explore the pros and cons of using Rust in this field.

Pros

  1. Memory safety: Rust’s ownership model and borrow checker prevent common memory-related bugs like use-after-free and data races, which are common in C and C++.
  2. Performance: Rust is designed for performance, making it an ideal choice for writing high-performance security tools and exploits.
  3. Concurrency: Rust’s asynchronous programming capabilities enable you to build efficient and concurrent tools for pen testing and red teaming tasks.
  4. Cross-platform: Rust supports a wide range of platforms, making it possible to develop tools that work across different operating systems and architectures.
  5. Ecosystem: Rust has a growing ecosystem of libraries and tools that can be used for various security-related tasks.

Cons

  1. Steep learning curve: Rust’s syntax and ownership model may be challenging to learn, especially for developers who are new to systems programming.
  2. Verbosity: Rust can be more verbose than languages like Python or Ruby, which may slow down the development process.
  3. Less mature ecosystem: While Rust’s ecosystem is growing, it may not yet have the same breadth of libraries and tools available for pen testing and red teaming compared to more established languages like Python or C++.

Conclusion

Rust is a powerful and versatile language that offers many benefits for pen testers and red team members. With its focus on memory safety, performance, and concurrency, Rust is an excellent choice for developing secure and efficient security tools and exploits. While the learning curve may be steep and the ecosystem less mature than other languages, the benefits of using Rust in the security field are substantial. Get started with Rust today and unlock its potential for your pen testing and red teaming tasks.