Курс — Хакинг на Rust. #8 Система владения (Ownership). Как это предотвращает уязвимости (например, use-after-free)
Здравствуйте, дорогие друзья.
Система владения в Rust — это не просто абстрактная концепция. Это механизм, который физически блокирует целые классы уязвимостей , включая use-after-free, double free и data races. Для хакеров это означает:
- Инструменты, которые вы пишете, защищены от эксплуатации.
- Возможность находить уязвимости в чужом коде, анализируя нарушения правил владения.
Разберем, как это работает.
2.1 Use-after-free: как возникает и почему опасен
Use-after-free — ситуация, когда программа использует указатель на уже освобожденную память. Это приводит к:
- Утечкам информации (например, чтение остатков памяти).
- Выполнению произвольного кода (перезапись освобожденного буфера).
Пример на C :
1 2 3 4 5 6 7 8 9 10 |
#include <stdlib.h> #include <string.h> int main() { char *data = malloc(10); strcpy(data, "secret"); free(data); printf("%s\n", data); // Use-after-free: выводит мусор или крашится return 0; } |
Последствия :
- Злоумышленник может перехватить контроль, если в освобожденной памяти окажутся shellcode или ROP-гаджеты.
2.2 Как Rust предотвращает use-after-free
В Rust владение данными строго отслеживается. Когда переменная покидает область видимости, ее память освобождается, а все ссылки на нее становятся недействительными.
Пример :
1 2 3 4 5 6 7 |
fn main() { let data = String::from("secret"); let ref_data = &data; println!("{}", ref_data); // Ok } // `data` уничтожается, `ref_data` больше не существует // Попытка использовать `ref_data` после освобождения `data` приведет к ошибке компиляции |
Что происходит :
- Ссылки (
&T
,&mut T
) не могут пережить данные, на которые указывают. - Компилятор проверяет все возможные пути выполнения, чтобы гарантировать отсутствие висячих указателей.
2.3 Другие уязвимости, которые блокирует Ownership
Double Free
Попытка дважды освободить одну и ту же память:
1 2 3 4 |
// C-код: char *data = malloc(10); free(data); free(data); // Неопределенное поведение |
В Rust :
1 2 3 |
let data = Box::new(42); drop(data); // Явное освобождение // drop(data); // Ошибка: `data` уже перемещено |
Data Races
Когда два потока одновременно:
- Получают доступ к данным.
- Хотя бы один из них — на запись.
- Нет синхронизации.
В Rust :
1 2 3 4 5 6 7 8 |
use std::thread; let mut data = vec![1, 2, 3]; let handle = thread::spawn(move || { data.push(4); }); // data.push(5); // Ошибка: данные уже перемещены в поток handle.join().unwrap(); |
2.4 Владение и низкоуровневые операции
Даже в unsafe
коде Rust сохраняет базовые гарантии:
1 2 3 4 5 6 7 8 |
let mut data = vec![0xde, 0xad, 0xbe, 0xef]; let ptr = data.as_mut_ptr(); unsafe { *ptr.add(2) = 0xff; // Ok: изменяем третий байт } println!("{:?}", data); // [0xde, 0xad, 0xff, 0xef] |
Что нельзя сделать :
- Создать висячую ссылку:
1 2 3 4 5 6 |
let ptr; { let x = Box::new(42); ptr = &*x as *const i32; } unsafe { println!("{}", *ptr); } // Неопределенное поведение, но компилятор предупредит |
2.5 Практический анализ: use-after-free в C vs Rust
C-код :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
typedef struct { char *buffer; } Packet; void process(Packet *p) { free(p->buffer); p->buffer = NULL; } int main() { Packet p = { .buffer = malloc(10) }; process(&p); free(p.buffer); // Double free, если забыли проверить NULL } |
Rust-аналог :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
struct Packet { buffer: Vec<u8>, } impl Packet { fn process(&mut self) { self.buffer.clear(); } } fn main() { let mut p = Packet { buffer: vec![0; 10] }; p.process(); // Невозможно вызвать free() — память управляется автоматически } |
2.6 Владение и сетевые атаки
Парсинг пакетов без риска use-after-free:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
use std::net::TcpStream; fn parse_packet(stream: &mut TcpStream) -> Option<Vec<u8>> { let mut buffer = [0u8; 1024]; stream.read(&mut buffer).ok()?; Some(buffer.to_vec()) // Владение передается в вызывающий код } fn main() { let mut stream = TcpStream::connect("127.0.0.1:8080").unwrap(); let packet = parse_packet(&mut stream); // `buffer` внутри parse_packet уничтожен, но данные сохранены в `packet` } |
2.7 Как использовать это в хакинге
- Анализ чужого кода :
Ищите места, где разработчики вынуждены использоватьunsafe
для обхода компилятора. Это потенциальные уязвимости. - Эксплойты :
Знание правил владения помогает находить слабые места в legacy-системах, написанных на C/C++. - Безопасные инструменты :
Сканеры портов, снифферы и фаззеры на Rust защищены от случайных ошибок.
2.8 Практическое задание: найдите уязвимость
1 2 3 4 5 6 |
fn main() { let data = String::from("secret"); let slice = &data[0..3]; drop(data); println!("{}", slice); // Что будет? } |
Ответ :
Ошибка компиляции: slice
ссылается на данные, которые уже уничтожены вызовом drop(data)
.
Итог
Система владения в Rust делает use-after-free и другие уязвимости физически невозможными на уровне языка. Это не просто «проверка на ошибки» — это перепроектирование подхода к управлению памяти. Для хакеров это означает:
- Инструменты, которые не ломаются из-за случайных ошибок.
- Способность находить уязвимости в других языках, понимая, как их предотвращать.
В следующих главах мы углубимся в unsafe
, сетевые операции и создание эксплойтов. А пока — попробуйте написать функцию, которая принимает Vec<u8>
, обрабатывает его и возвращает подмножество данных без нарушения правил владения.

На этом все. Всем хорошего дня!