Курс — Хакинг на Rust. #11 Типы данных и структуры: Обработка ошибок: Result и Option
Здравствуйте, дорогие друзья.
В кибербезопасности ошибка — это не просто баг. Это потенциальная уязвимость. Rust заставляет обрабатывать ошибки явно, используя типы Result
и Option
. Это предотвращает целые классы уязвимостей, включая логические ошибки и утечки информации.
3.1 Result<T, E>
: безопасность вместо исключений
В отличие от языков вроде Python или C++, где ошибки обрабатываются через исключения, Rust возвращает Result
:
Ok(T)
— успешный результат.Err(E)
— ошибка.
Пример: подключение к сокету
1 2 3 4 5 6 7 8 |
use std::net::TcpStream; fn connect(host: &str, port: u16) -> Result<TcpStream, String> { match TcpStream::connect((host, port)) { Ok(stream) => Ok(stream), Err(e) => Err(format!("Ошибка подключения: {}", e)), } } |
Обработка :
1 2 3 4 |
match connect("192.168.1.1", 80) { Ok(stream) => println!("Успех!"), Err(e) => eprintln!("Провал: {}", e), } |
Защита от уязвимостей :
- Невозможно «забыть» обработать ошибку — компилятор выдаст предупреждение.
- Нет скрытых исключений, которые могут нарушить логику атаки.
3.2 Option<T>
: нет null
, есть предсказуемость
В Rust нет null
. Вместо этого используется Option
:
Some(T)
— значение присутствует.None
— значение отсутствует.
Пример: поиск админа в списке пользователей
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
struct User { name: String, is_admin: bool, } fn find_admin(users: &[User]) -> Option<&User> { users.iter().find(|u| u.is_admin) } // Использование let users = vec![ User { name: "Alice".into(), is_admin: false }, User { name: "Bob".into(), is_admin: true }, ]; match find_admin(&users) { Some(admin) => println!("Админ: {}", admin.name), None => println!("Админ не найден"), } |
Почему это важно :
- Нет риска разыменования
null
(как в C). - Код явно обрабатывает оба варианта.
3.3 Комбинаторы: map
, and_then
, unwrap_or
Для удобства работы с Result
и Option
Rust предоставляет комбинаторы:
map
— преобразование успешного значения
1 2 |
let port: Result<u16, &str> = Ok(8080); let doubled = port.map(|p| p * 2); // Ok(16160) |
and_then
— цепочка операций
1 2 3 4 5 6 7 8 |
fn parse_port(s: &str) -> Result<u16, String> { s.parse::<u16>() .map_err(|_| format!("Неверный порт: {}", s)) } let port = parse_port("80").and_then(|p| { if p < 1024 { Ok(p) } else { Err("Порт > 1024".into()) } }); |
unwrap_or
— значение по умолчанию
1 |
let timeout = get_config_timeout().unwrap_or(30); // 30 секунд, если ошибка |
3.4 Опасные методы: unwrap
и expect
unwrap()
: Возвращает значение изOk
/Some
, иначе паникует.
1 |
let data = read_file().unwrap(); // Краш, если файл не прочитан |
expect("msg")
: То же, но с сообщением.
Когда использовать :
- В прототипах или тестах.
- Когда ошибка теоретически невозможна.
Почему это опасно :
- Паника может быть использована для DoS-атаки.
- Всегда обрабатывайте ошибки в боевом коде.
3.5 Пользовательские типы ошибок
Для сложных сценариев определяйте свои типы ошибок:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#[derive(Debug)] enum ExploitError { NetworkError(std::io::Error), InvalidPayload, PermissionDenied, } impl From<std::io::Error> for ExploitError { fn from(e: std::io::Error) -> Self { ExploitError::NetworkError(e) } } fn exploit() -> Result<(), ExploitError> { let stream = TcpStream::connect("127.0.0.1:80")?; // `?` автоматически преобразует ошибки Ok(()) } |
3.6 Паттерн-матчинг и безопасность
match
гарантирует, что все варианты обработаны:
1 2 3 4 5 6 7 |
fn check_port(port: u16) -> Result<(), String> { match port { 0..=1023 => Err("Системный порт".into()), 1024..=65535 => Ok(()), _ => unreachable!(), // Компилятор проверяет, что это невозможно } } |
3.7 Практическое задание: парсинг пакета
Задача : Напишите функцию, которая извлекает IP-адрес из бинарных данных.
Решение :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
fn parse_ip(data: &[u8]) -> Option<[u8; 4]> { if data.len() < 4 { return None; } Some([data[0], data[1], data[2], data[3]]) } // Использование let packet = &[192, 168, 1, 1, 0xde, 0xad]; match parse_ip(packet) { Some(ip) => println!("IP: {}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3]), None => eprintln!("Неверный формат"), } |
3.8 Как это предотвращает уязвимости
- Непроверенные ошибки :
В C:
1 2 3 4 |
int *data = malloc(100); if (data == NULL) { // Забыли проверить — краш или переполнение } |
В Rust:
1 |
let data = allocate_buffer()?; // `?` обязателен, иначе код не скомпилируется |
Логические ошибки :
В Python:
1 2 |
user = find_user() # Может вернуть None print(user.name) # AttributeError, если user == None |
В Rust:
1 2 3 |
if let Some(user) = find_user() { println!("{}", user.name); } |
ИтогResult
и Option
— это не просто типы. Это:
- Защита от человеческого фактора : компилятор заставляет обрабатывать ошибки.
- Предсказуемость : нет скрытых
null
или исключений. - Безопасность : ошибки становятся частью контракта функции.
Для хакеров это означает инструменты, которые не ломаются на неожиданных данных, и код, который сложно эксплуатировать из-за отсутствия «дыр». В следующих главах мы увидим, как эти принципы работают в связке с низкоуровневыми операциями и эксплойтами. А пока — попробуйте написать функцию, которая читает конфигурационный файл и возвращает Result<HashMap<String, String>, io::Error>
.

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