Fundamentos do Rust

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks

Propriedade de variáveis

A memória é gerida através de um sistema de propriedade com as seguintes regras que o compilador verifica em tempo de compilação:

  1. Cada valor em Rust tem uma variável que é chamada de seu proprietário.
  2. Só pode haver um proprietário por vez.
  3. Quando o proprietário sai do escopo, o valor será descartado.
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
}

Tipos Genéricos

Crie uma struct em que um de seus valores pode ser de qualquer tipo

#![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

O tipo Option significa que o valor pode ser do tipo Some (há algo) ou None:

#![allow(unused)]
fn main() {
pub enum Option<T> {
None,
Some(T),
}
}

Você pode usar funções como is_some() ou is_none() para verificar o valor do Option.

Result, Ok & Err

Usados para retornar e propagar erros

#![allow(unused)]
fn main() {
pub enum Result<T, E> {
Ok(T),
Err(E),
}
}

Você pode usar funções como is_ok() ou is_err() para verificar o valor do resultado

O enum Option deve ser usado em situações onde um valor pode não existir (ser None). O enum Result deve ser usado em situações onde você faz algo que pode dar errado

Macros

Macros são mais poderosas do que funções porque elas se expandem para produzir mais código do que o código que você escreveu manualmente. Por exemplo, a assinatura de uma função precisa declarar o número e o tipo de parâmetros que a função possui. Macros, por outro lado, podem aceitar um número variável de parâmetros: podemos chamar println!("hello") com um argumento ou println!("hello {}", name) com dois argumentos. Além disso, macros são expandidas antes que o compilador interprete o significado do código, então uma macro pode, por exemplo, implementar um trait em um dado tipo. Uma função não pode, porque ela é chamada em tempo de execução e um trait precisa ser implementado em tempo de compilação.

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!");
};
}
}

Iterar

#![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() {
}

Box Recursivo

#![allow(unused)]
fn main() {
enum List {
Cons(i32, List),
Nil,
}

let list = Cons(1, Cons(2, Cons(3, Nil)));
}

Condicionais

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 (infinito)

#![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

Criar um novo método para um tipo

#![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);
}

Testes

#![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

Um Arc pode usar Clone para criar mais referências ao objeto para passá-las para as threads. Quando o último ponteiro de referência para um valor sai do escopo, a variável é descartada.

#![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

Neste caso, vamos passar para a thread uma variável que ela poderá modificar

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));
}
}

Noções Essenciais de Segurança

Rust fornece fortes garantias de segurança de memória por padrão, mas você ainda pode introduzir vulnerabilidades críticas através de código unsafe, problemas em dependências ou erros de lógica. O mini-cheatsheet a seguir reúne os primitivos com os quais você mais comumente irá lidar durante revisões de segurança ofensivas ou defensivas de software Rust.

Código unsafe & segurança de memória

unsafe blocks desativam as verificações de aliasing e de limites do compilador, então todos os bugs tradicionais de corrupção de memória (OOB, use-after-free, double free, etc.) podem reaparecer. Checklist rápido de auditoria:

  • Procure por unsafe blocks, extern "C" functions, chamadas para ptr::copy*, std::mem::transmute, MaybeUninit, ponteiros brutos ou módulos ffi.
  • Valide cada aritmética de ponteiros e cada argumento de comprimento passado para funções de baixo nível.
  • Prefira #![forbid(unsafe_code)] (crate-wide) ou #[deny(unsafe_op_in_unsafe_fn)] (1.68 +) para fazer a compilação falhar quando alguém reintroduzir unsafe.

Exemplo de overflow criado com ponteiros brutos:

#![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
}
}

Executar o Miri é uma forma barata de detectar UB em tempo de teste:

rustup component add miri
cargo miri test  # hunts for OOB / UAF during unit tests

Auditing dependencies with RustSec / cargo-audit

A maioria das vulns reais em Rust está em crates de terceiros. O RustSec advisory DB (mantido pela comunidade) pode ser consultado localmente:

cargo install cargo-audit
cargo audit              # flags vulnerable versions listed in Cargo.lock

Integre-o no CI e faça com que falhe usando --deny warnings.

cargo deny check advisories oferece funcionalidade semelhante, além de verificações de licença e de ban-list.

Cobertura de código com cargo-tarpaulin

cargo tarpaulin é uma ferramenta de relatório de cobertura de código para o sistema de build Cargo

cargo binstall cargo-tarpaulin
cargo tarpaulin              # no options are required, if no root directory is defined Tarpaulin will run in the current working directory.

No Linux, o backend de tracing padrão do Tarpaulin ainda é o Ptrace e funciona apenas em processadores x86_64. Isso pode ser alterado para a instrumentação de cobertura do llvm com --engine llvm. No Mac e no Windows, esse é o método de coleta padrão.

Verificação da cadeia de fornecimento com cargo-vet (2024)

cargo vet registra um hash de revisão para cada crate que você importa e evita atualizações não percebidas:

cargo install cargo-vet
cargo vet init      # generates vet.toml
cargo vet --locked  # verifies packages referenced in Cargo.lock

A ferramenta está sendo adotada pela infraestrutura do projeto Rust e por um número crescente de organizações para mitigar ataques poisoned-package.

Fuzzing da superfície da sua API (cargo-fuzz)

Fuzz tests detectam facilmente panics, estouros de inteiros e bugs de lógica que podem se tornar problemas DoS ou side-channel:

cargo install cargo-fuzz
cargo fuzz init              # creates fuzz_targets/
cargo fuzz run fuzz_target_1 # builds with libFuzzer & runs continuously

Adicione o fuzz target ao seu repositório e execute-o no seu pipeline.

Referências

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks