Курс — Хакинг на Rust. #12 Низкоуровневое программирование. Работа с указателями и сырыми данными (unsafe)
Здравствуйте, дорогие друзья.
Rust славится своей системой безопасности памяти, которая предотвращает целый класс ошибок, таких как use-after-free или гонки данных. Однако для реализации некоторых низкоуровневых операций, взаимодействия с аппаратным обеспечением или интеграции с кодом на других языках (например, C) приходится выходить за рамки этих ограничений. Здесь на сцену выходит ключевое слово unsafe
— инструмент, который даёт разработчику больше контроля, но требует повышенной осторожности. В этом разделе мы разберём, как использовать unsafe
для работы с указателями, сырыми данными и внешними библиотеками, а также обсудим связанные риски.
4.1 Сырые указатели: *const T
и *mut T
Сырые указатели (raw pointers
) в Rust похожи на указатели в C/C++. Они не подчиняются правилам владения и заимствования, что делает их мощным, но опасным инструментом.
Основные характеристики:
- Небезопасны: Разыменование сырых указателей требует блока
unsafe
. - Могут быть нулевыми: В отличие от ссылок (
&T
,&mut T
), сырые указатели могут указывать наnull
. - Не гарантируют валидность: Указатель может ссылаться на освобождённую память или неверный адрес.
Пример создания и разыменования:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
fn main() { let mut num = 42; // Создание сырого указателя let raw_ptr: *mut i32 = &mut num as *mut i32; unsafe { // Разыменование указателя println!("Значение: {}", *raw_ptr); *raw_ptr = 100; println!("Новое значение: {}", num); // Выведет 100 } } |
Когда использовать:
- Взаимодействие с аппаратным обеспечением (например, запись в регистры памяти).
- Оптимизация критических участков кода.
- Интеграция с C-библиотеками (см. раздел 4.3).
4.2 Управление памятью: стек vs. куча
В Rust память автоматически освобождается при выходе переменной из области видимости (стек) или через систему владения (куча). Однако в unsafe
коде можно вручную управлять памятью, что полезно для эксплойтов или обработки бинарных данных.
Работа с кучей через Box<T>
и сырые указатели:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
use std::alloc::{alloc, dealloc, Layout}; fn main() { unsafe { // Выделение памяти в куче let layout = Layout::new::<i32>(); let ptr = alloc(layout) as *mut i32; // Запись значения *ptr = 42; println!("Значение: {}", *ptr); // Освобождение памяти dealloc(ptr as *mut u8, layout); } } |
Опасности:
- Утечки памяти: Если забыть вызвать
dealloc
, данные останутся в куче. - Повреждение данных: Запись за пределы выделенного блока (аналог buffer overflow в C).
4.3 FFI: Интеграция с C-библиотеками
Rust позволяет вызывать функции из C-библиотек через Foreign Function Interface (FFI). Это особенно полезно для использования существующих инструментов (например, OpenSSL) или работы с системными вызовами.
Пример вызова функции strlen
из libc:
1 2 3 4 5 6 7 8 9 10 11 |
extern "C" { fn strlen(s: *const u8) -> usize; } fn main() { let s = "Hello, Rust!"; let len = unsafe { strlen(s.as_ptr()) }; println!("Длина строки: {}", len); // Выведет 12 } |
Правила работы с FFI:
- Используйте
extern "C"
для указания C ABI (Application Binary Interface). - Преобразуйте Rust-типы (например,
&str
) в сырые указатели. - Проверяйте валидность передаваемых данных (например, нулевые указатели).
4.4 Риски и этические аспекты
Хотя unsafe
расширяет возможности Rust, его неосторожное использование может привести к уязвимостям:
- Use-after-free: Доступ к памяти после её освобождения.
- Data races: Одновременная запись в разделяемые данные из нескольких потоков.
- Переполнение буфера: Запись за пределы выделенного блока памяти.
Рекомендации:
- Минимизируйте использование
unsafe
, инкапсулируя его в безопасные функции. - Проверяйте код с помощью инструментов статического анализа (например,
miri
). - Документируйте, почему использование
unsafe
оправдано в конкретном случае.
4.5 Практический пример: Чтение памяти процесса
Для демонстрации возможностей unsafe
напишем простой сниппет, который считывает байты из произвольного адреса памяти (только для образовательных целей!):
1 2 3 4 5 6 7 8 9 10 11 |
fn read_memory(address: usize) -> u8 { unsafe { let ptr = address as *const u8; *ptr // Разыменование указателя } } fn main() { let data = 0x7ffee4a5c9a0 as usize; // Пример адреса println!("Байт по адресу {:x}: {}", data, read_memory(data)); } |
Важно: Такой код может вызвать сегментацию памяти (SEGFAULT) или нарушить работу системы. Используйте его только в контролируемых средах (например, в песочнице).
Заключение
Работа с unsafe
и сырыми данными — это мощный инструмент в арсенале хакера на Rust. Она позволяет обходить ограничения языка для реализации эксплойтов, анализа бинарников или оптимизации критичного кода. Однако с этой силой приходит ответственность: всегда проверяйте код на уязвимости и используйте unsafe
только тогда, когда это действительно необходимо. В следующих главах мы рассмотрим, как эти низкоуровневые техники применяются для создания эксплойтов и анализа уязвимостей.
Задачи для самостоятельного решения:
- Напишите функцию, которая копирует данные из одного блока памяти в другой, используя сырые указатели.
- Создайте FFI-обёртку для функции
malloc
из libc и проверьте её работу. - Реализуйте простой шелл-код (в учебных целях), используя
unsafe
для манипуляции памятью.

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