Курс — Хакинг на 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>, обрабатывает его и возвращает подмножество данных без нарушения правил владения.

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