Rust Basics
Tip
Learn & practice AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking:HackTricks Training Azure Red Team Expert (AzRTE)
Support HackTricks
- Check the subscription plans!
- Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
- Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.
Ownership of variables
Memory is managed through a system of ownership with the following rules that the compiler checks at compile time:
- 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 will be dropped.
fn main() {
let student_age: u32 = 20;
{ // Scope of a variable is within the block it is declared in, which is denoted by brackets
let teacher_age: u32 = 41;
println!("The student is {} and teacher is {}", student_age, teacher_age);
} // when an owning variable goes out of scope, it will be dropped
// println!("the teacher is {}", teacher_age); // this will not work as teacher_age has been dropped
}
Generic Types
Create a struct where 1 of their values could be any type
#![allow(unused)]
fn main() {
struct Wrapper<T> {
value: T,
}
impl<T> Wrapper<T> {
pub fn new(value: T) -> Self {
Wrapper { value }
}
}
Wrapper::new(42).value
Wrapper::new("Foo").value, "Foo"
}
Option, Some & None
The Option type means that the value might by of type Some (there is something) or None:
#![allow(unused)]
fn main() {
pub enum Option<T> {
None,
Some(T),
}
}
You can use functions such as is_some() or is_none() to check the value of the Option.
Result, Ok & Err
Used for returning and propagating errors
#![allow(unused)]
fn main() {
pub enum Result<T, E> {
Ok(T),
Err(E),
}
}
You can use functions such as is_ok() or is_err() to check the value of the result
The Option enum should be used in situations where a value might not exist (be None).
The Result enum should be used in situations where you do something that might go wrong
Macros
Macros are more powerful than functions because they expand to produce more code than the code you’ve written manually. For example, a function signature must declare the number and type of parameters the function has. Macros, on the other hand, can take a variable number of parameters: we can call println!("hello") with one argument or println!("hello {}", name) with two arguments. Also, macros are expanded before the compiler interprets the meaning of the code, so a macro can, for example, implement a trait on a given type. A function can’t, because it gets called at runtime and a trait needs to be implemented at compile time.
macro_rules! my_macro {
() => {
println!("Check out my macro!");
};
($val:expr) => {
println!("Look at this other macro: {}", $val);
}
}
fn main() {
my_macro!();
my_macro!(7777);
}
// Export a macro from a module
mod macros {
#[macro_export]
macro_rules! my_macro {
() => {
println!("Check out my macro!");
};
}
}
Iterate
#![allow(unused)]
fn main() {
// Iterate through a vector
let my_fav_fruits = vec!["banana", "raspberry"];
let mut my_iterable_fav_fruits = my_fav_fruits.iter();
assert_eq!(my_iterable_fav_fruits.next(), Some(&"banana"));
assert_eq!(my_iterable_fav_fruits.next(), Some(&"raspberry"));
assert_eq!(my_iterable_fav_fruits.next(), None); // When it's over, it's none
// One line iteration with action
my_fav_fruits.iter().map(|x| capitalize_first(x)).collect()
// Hashmap iteration
for (key, hashvalue) in &*map {
for key in map.keys() {
for value in map.values() {
}
Recursive Box
#![allow(unused)]
fn main() {
enum List {
Cons(i32, List),
Nil,
}
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
Conditionals
if
#![allow(unused)]
fn main() {
let n = 5;
if n < 0 {
print!("{} is negative", n);
} else if n > 0 {
print!("{} is positive", n);
} else {
print!("{} is zero", n);
}
}
match
#![allow(unused)]
fn main() {
match number {
// Match a single value
1 => println!("One!"),
// Match several values
2 | 3 | 5 | 7 | 11 => println!("This is a prime"),
// TODO ^ Try adding 13 to the list of prime values
// Match an inclusive range
13..=19 => println!("A teen"),
// Handle the rest of cases
_ => println!("Ain't special"),
}
let boolean = true;
// Match is an expression too
let binary = match boolean {
// The arms of a match must cover all the possible values
false => 0,
true => 1,
// TODO ^ Try commenting out one of these arms
};
}
loop (infinite)
#![allow(unused)]
fn main() {
loop {
count += 1;
if count == 3 {
println!("three");
continue;
}
println!("{}", count);
if count == 5 {
println!("OK, that's enough");
break;
}
}
}
while
#![allow(unused)]
fn main() {
let mut n = 1;
while n < 101 {
if n % 15 == 0 {
println!("fizzbuzz");
} else if n % 5 == 0 {
println!("buzz");
} else {
println!("{}", n);
}
n += 1;
}
}
for
#![allow(unused)]
fn main() {
for n in 1..101 {
if n % 15 == 0 {
println!("fizzbuzz");
} else {
println!("{}", n);
}
}
// Use "..=" to make inclusive both ends
for n in 1..=100 {
if n % 15 == 0 {
println!("fizzbuzz");
} else if n % 3 == 0 {
println!("fizz");
} else if n % 5 == 0 {
println!("buzz");
} else {
println!("{}", n);
}
}
// ITERATIONS
let names = vec!["Bob", "Frank", "Ferris"];
//iter - Doesn't consume the collection
for name in names.iter() {
match name {
&"Ferris" => println!("There is a rustacean among us!"),
_ => println!("Hello {}", name),
}
}
//into_iter - COnsumes the collection
for name in names.into_iter() {
match name {
"Ferris" => println!("There is a rustacean among us!"),
_ => println!("Hello {}", name),
}
}
//iter_mut - This mutably borrows each element of the collection
for name in names.iter_mut() {
*name = match name {
&mut "Ferris" => "There is a rustacean among us!",
_ => "Hello",
}
}
}
if let
#![allow(unused)]
fn main() {
let optional_word = Some(String::from("rustlings"));
if let word = optional_word {
println!("The word is: {}", word);
} else {
println!("The optional word doesn't contain anything");
}
}
while let
#![allow(unused)]
fn main() {
let mut optional = Some(0);
// This reads: "while `let` destructures `optional` into
// `Some(i)`, evaluate the block (`{}`). Else `break`.
while let Some(i) = optional {
if i > 9 {
println!("Greater than 9, quit!");
optional = None;
} else {
println!("`i` is `{:?}`. Try again.", i);
optional = Some(i + 1);
}
// ^ Less rightward drift and doesn't require
// explicitly handling the failing case.
}
}
Traits
Create a new method for a type
#![allow(unused)]
fn main() {
trait AppendBar {
fn append_bar(self) -> Self;
}
impl AppendBar for String {
fn append_bar(self) -> Self{
format!("{}Bar", self)
}
}
let s = String::from("Foo");
let s = s.append_bar();
println!("s: {}", s);
}
Tests
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
#[test]
fn you_can_assert() {
assert!(true);
assert_eq!(true, true);
assert_ne!(true, false);
}
}
}
Threading
Arc
An Arc can use Clone to create more references over the object to pass them to the threads. When the last reference pointer to a value is out of scope, the variable is dropped.
#![allow(unused)]
fn main() {
use std::sync::Arc;
let apple = Arc::new("the same apple");
for _ in 0..10 {
let apple = Arc::clone(&apple);
thread::spawn(move || {
println!("{:?}", apple);
});
}
}
Threads
In this case we will pass the thread a variable it will be able to modify
fn main() {
let status = Arc::new(Mutex::new(JobStatus { jobs_completed: 0 }));
let status_shared = Arc::clone(&status);
thread::spawn(move || {
for _ in 0..10 {
thread::sleep(Duration::from_millis(250));
let mut status = status_shared.lock().unwrap();
status.jobs_completed += 1;
}
});
while status.lock().unwrap().jobs_completed < 10 {
println!("waiting... ");
thread::sleep(Duration::from_millis(500));
}
}
Security Essentials
Rust provides strong memory-safety guarantees by default, but you can still introduce critical vulnerabilities through unsafe code, dependency issues or logic mistakes. The following mini-cheatsheet gathers the primitives you will most commonly touch during offensive or defensive security reviews of Rust software.
Unsafe code & memory safety
unsafe blocks opt-out of the compiler’s aliasing and bounds checks, so all traditional memory-corruption bugs (OOB, use-after-free, double free, etc.) can appear again. A quick audit checklist:
- Look for
unsafeblocks,extern "C"functions, calls toptr::copy*,std::mem::transmute,MaybeUninit, raw pointers orffimodules. - Validate every pointer arithmetic and length argument passed to low-level functions.
- Prefer
# or#[deny(unsafe_op_in_unsafe_fn)](1.68 +) to fail compilation when someone re-introducesunsafe.
Example overflow created with raw pointers:
#![allow(unused)]
fn main() {
use std::ptr;
fn vuln_copy(src: &[u8]) -> Vec<u8> {
let mut dst = Vec::with_capacity(4);
unsafe {
// ❌ copies *src.len()* bytes, the destination only reserves 4.
ptr::copy_nonoverlapping(src.as_ptr(), dst.as_mut_ptr(), src.len());
dst.set_len(src.len());
}
dst
}
}
Running Miri is an inexpensive way to detect UB at test time:
rustup component add miri
cargo miri test # hunts for OOB / UAF during unit tests
Auditing dependencies with RustSec / cargo-audit
Most real-world Rust vulns live in third-party crates. The RustSec advisory DB (community-powered) can be queried locally:
cargo install cargo-audit
cargo audit # flags vulnerable versions listed in Cargo.lock
Integrate it in CI and fail on --deny warnings.
cargo deny check advisories offers similar functionality plus licence and ban-list checks.
Code coverage with cargo-tarpaulin
cargo tarpaulin is a code coverage reporting tool for the Cargo build system
cargo binstall cargo-tarpaulin
cargo tarpaulin # no options are required, if no root directory is defined Tarpaulin will run in the current working directory.
On Linux, Tarpaulin’s default tracing backend is still Ptrace and will only work on x86_64 processors. This can be changed to the llvm coverage instrumentation with --engine llvm. For Mac and Windows, this is the default collection method.
Supply-chain verification with cargo-vet (2024)
cargo vet records a review hash for every crate you import and prevents unnoticed upgrades:
cargo install cargo-vet
cargo vet init # generates vet.toml
cargo vet --locked # verifies packages referenced in Cargo.lock
The tool is being adopted by the Rust project infrastructure and a growing number of orgs to mitigate poisoned-package attacks.
Fuzzing your API surface (cargo-fuzz)
Fuzz tests easily catch panics, integer overflows and logic bugs that might become DoS or side-channel issues:
cargo install cargo-fuzz
cargo fuzz init # creates fuzz_targets/
cargo fuzz run fuzz_target_1 # builds with libFuzzer & runs continuously
Add the fuzz target to your repo and run it in your pipeline.
References
- RustSec Advisory Database – https://rustsec.org
- Cargo-vet: “Auditing your Rust Dependencies” – https://mozilla.github.io/cargo-vet/
Tip
Learn & practice AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking:HackTricks Training Azure Red Team Expert (AzRTE)
Support HackTricks
- Check the subscription plans!
- Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
- Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.


