Курс — Хакинг на Rust. #13 Низкоуровневое программирование. Взаимодействие с памятью — стек vs. куча
Здравствуйте, дорогие друзья.
Понимание работы памяти — ключевой навык для любого хакера. В Rust, несмотря на автоматическое управление ресурсами, низкоуровневые операции требуют чёткого разделения данных между стеком и кучей . Эти две области памяти служат разным целям, а их неправильное использование может привести к уязвимостям или крахам программ. Разберём, как они устроены, и как их эксплуатировать (или защищаться от таких эксплойтов).
4.1 Стек: Быстрая, но ограниченная память
Стек — это область памяти, которая работает по принципу LIFO (Last In, First Out). Данные добавляются и удаляются строго в порядке «последним пришёл — первым вышел».
Особенности стека:
- Автоматическое управление: Память выделяется при входе в область видимости и освобождается при выходе.
- Высокая скорость: Операции с стеком выполняются за константное время O(1).
- Ограниченный размер: Обычно стек составляет несколько мегабайт (зависит от ОС).
Пример размещения данных в стеке:
1 2 3 4 5 |
fn main() { let x = 42; // Переменная в стеке let y = "Hello"; // Строковый слайс (статические данные) println!("{x}, {y}"); // 42, Hello } |
Когда использовать стек:
- Для переменных с известным размером на этапе компиляции.
- В коротких функциях, где данные не требуется сохранять долго.
Риски:
- Переполнение стека (Stack Overflow): Возникает при выделении слишком больших данных (например, массива из 1 млн элементов).
4.2 Куча: Гибкость за счет сложности
Куча — это область памяти для динамического выделения данных. Здесь хранятся объекты, размер которых неизвестен заранее или которые должны существовать дольше, чем их область видимости.
Особенности кучи:
- Ручное управление: В Rust память освобождается, когда владелец данных выходит из области видимости.
- Медленнее стека: Выделение и освобождение требуют работы аллокатора.
- Неограниченный размер: Ограничен только физической памятью системы.
Пример работы с кучей через Box
:
1 2 3 4 |
fn main() { let x = Box::new(42); // Выделение в куче println!("{}", *x); // Разыменование: 42 } // Память автоматически освобождается |
Сценарии использования кучи:
- Контейнеры с динамическим размером (
Vec
,String
). - Объекты, переживающие создающую их функцию (например, синглтоны).
4.3 Ручное управление памятью через unsafe
Для низкоуровневых операций Rust предоставляет инструменты для прямого доступа к памяти.
Выделение памяти в куче:
1 2 3 4 5 6 7 8 9 10 11 |
use std::alloc::{alloc, dealloc, Layout}; fn main() { unsafe { let layout = Layout::array::<u8>(10).unwrap(); let ptr = alloc(layout); // Выделяем 10 байт *ptr = 42; // Записываем значение println!("{}", *ptr); dealloc(ptr, layout); // Освобождаем память } } |
Работа с сырыми указателями:
1 2 3 4 5 6 7 8 |
fn main() { let mut data = 42; let ptr = &mut data as *mut i32; unsafe { *ptr = 100; // Изменяем данные через указатель } println!("{}", data); // Выведет 100 } |
Опасности:
- Use-after-free: Использование указателя после освобождения памяти.
- Double Free: Попытка освободить уже освобождённую память.
4.4 Практические примеры для хакеров
Пример 1: Переполнение буфера
Хотя Rust предотвращает многие виды buffer overflow, использование unsafe
может обойти защиту:
1 2 3 4 5 6 7 8 |
fn vulnerable_function() { let mut buffer = [0u8; 4]; let ptr = buffer.as_mut_ptr(); unsafe { // Записываем 8 байт в буфер размером 4 std::ptr::write_bytes(ptr, 0x41, 8); // "AAAAAAA" } } |
Пример 2: Чтение памяти процесса
Для анализа уязвимостей можно читать память по произвольному адресу (только в учебных целях!):
1 2 3 4 5 6 7 8 |
fn read_memory(address: usize) -> u32 { unsafe { *(address as *const u32) } } fn main() { let addr = 0x7ffee4a5c9a0_usize; // Пример адреса println!("Данные: {:x}", read_memory(addr)); } |
4.5 Инструменты для анализа памяти
- Valgrind: Проверка на утечки и невалидные операции.
- AddressSanitizer: Обнаружение buffer overflow и use-after-free.
- GDB: Отладка с просмотром содержимого стека и кучи.
Заключение
Стек и куча — основа работы с памятью в Rust. Понимание их различий и нюансов критически важно для написания безопасного кода и анализа уязвимостей. В следующих главах мы применим эти знания для создания эксплойтов, анализа бинарников и защиты систем.
Задачи для самостоятельного решения:
- Напишите функцию, которая копирует данные из стека в кучу.
- Создайте пример buffer overflow в
unsafe
блоке и проверьте его через AddressSanitizer. - Реализуйте дамп памяти процесса с помощью сырых указателей.

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