Защита от переполнения буфера¶
Переполнение буфера (buffer overflow) — одна из самых распространённых уязвимостей в программах на C и C++. Атакующий записывает данные за пределы выделенного буфера, перезаписывая соседние области памяти: адрес возврата, указатели функций или другие критические данные. Это может привести к выполнению произвольного кода с привилегиями атакованного процесса.
Классическая уязвимость¶
void vulnerable(const char *input) {
char buffer[64];
strcpy(buffer, input); /* нет проверки длины — опасно! */
}
Если input длиннее 64 байт, strcpy запишет данные за пределы buffer, затирая соседние области стека, в том числе
адрес возврата. Атакующий может поместить в входную строку специально сформированный машинный код (shellcode) и
перенаправить управление на него.
Stack layout при переполнении buffer[64] без защиты:
высокие адреса
┌──────────────────────────────┐
│ return address │ ◀── перезаписывается атакующим!
├──────────────────────────────┤
│ saved rbp │ ◀── затирается
├──────────────────────────────┤
│ buffer[63] │
│ ... │
│ buffer[0] ◀── rsp │ strcpy пишет сюда и дальше ──▶
└──────────────────────────────┘
низкие адреса
input = "AAAA...AAA" + fake_rbp + shellcode_addr
^64+ байта^ ^^^^ управление передаётся на shellcode
Средства защиты¶
Современные системы применяют несколько взаимодополняющих механизмов:
- Stack Canary — специальное значение между буфером и адресом возврата;
- ASLR — рандомизация адресного пространства;
- NX / DEP — запрет выполнения данных;
- RELRO — защита таблиц перемещений от записи;
- CFI — проверка целостности потока управления;
- Безопасные функции —
strlcpy,snprintfвместоstrcpy,sprintf.
Stack Canary¶
Stack Protector (или stack canary) — наиболее распространённый аппаратно-программный механизм. Его суть в следующем:
- При входе в функцию между локальными переменными и адресом возврата помещается случайное значение — «канарейка» ( canary), взятое из потокового локального хранилища (TLS).
- Перед возвратом из функции canary сравнивается с исходным значением.
- Если canary изменился — буфер был переполнен, программа аварийно завершается.
Stack layout с canary (нормальное выполнение):
высокие адреса
┌──────────────────────────────┐
│ return address │ rbp+8
├──────────────────────────────┤
│ saved rbp │ rbp+0 ◀── rbp
├──────────────────────────────┤
│ canary (случайное значение)│ rbp-8 ← сравнивается с %fs:40 перед ret
├──────────────────────────────┤
│ buffer[63] │
│ ... │
│ buffer[0] │ ◀── rsp
└──────────────────────────────┘
низкие адреса
Stack layout при переполнении buffer:
┌──────────────────────────────┐
│ return address ← затёрт │ ✗ атакующий мог бы перенаправить ret
├──────────────────────────────┤
│ saved rbp ← затёрт │ ✗
├──────────────────────────────┤
│ canary ← затёрт │ ✗ значение изменилось!
├──────────────────────────────┤
│ AAAAAAAAAAAAAAAAAAAAAAAAAAAA │
│ AAAAAAAAAAAAAAAAAAAAAAAAAAAA │ ← переполнение идёт снизу вверх
│ buffer[0] │
└──────────────────────────────┘
Проверка: canary ≠ %fs:40 ──▶ call __stack_chk_fail ──▶ abort
Ассемблерный вид функции с canary:
func:
push %rbp
movq %rsp, %rbp
subq $40, %rsp
# Загрузить canary из TLS (сегментный регистр fs)
movq %fs:40, %rax
movq %rax, -8(%rbp) # сохранить canary в стеке
# ... тело функции ...
# Проверить canary перед возвратом
movq -8(%rbp), %rax
xorq %fs:40, %rax # rax ^= canary из TLS
je .stack_ok # если 0 — всё хорошо
call __stack_chk_fail # иначе — аварийное завершение
.stack_ok:
leave
ret
Управление stack protector'ом через флаги GCC:
gcc -fstack-protector-all prog.c # защита всех функций
gcc -fstack-protector-strong prog.c # защита функций с буферами (по умолчанию)
gcc -fno-stack-protector prog.c # отключить защиту
Демонстрация: Stack smashing detected¶
#include <stdio.h>
#include <string.h>
void vulnerable(const char *input) {
char buffer[8];
strcpy(buffer, input); /* намеренное переполнение */
}
int main(void) {
vulnerable("AAAAAAAAAAAAAAAA"); /* 16 байт > 8 */
return 0;
}
Без stack protector:
ASLR (Address Space Layout Randomization)¶
ASLR — механизм операционной системы, который при каждом запуске процесса случайно смещает базовые адреса:
- стека;
- кучи;
- разделяемых библиотек;
- (при PIE) самого исполняемого файла.
Цель: даже если атакующий знает структуру памяти, он не может предугадать конкретные адреса и не может прицельно перейти на shellcode или gadget.
Проверка и управление ASLR в Linux:
cat /proc/sys/kernel/randomize_va_space
# 0 = отключено
# 1 = частичная рандомизация (стек, VDSO)
# 2 = полная рандомизация (стек, куча, библиотеки, PIE)
sudo sysctl -w kernel.randomize_va_space=2 # включить (рекомендуется)
sudo sysctl -w kernel.randomize_va_space=0 # отключить (только для отладки)
Запустить конкретный процесс без ASLR (без прав суперпользователя):
GDB по умолчанию отключает ASLR, чтобы адреса оставались стабильными между сеансами отладки.
NX / DEP (No-Execute / Data Execution Prevention)¶
NX (Non-Executable) — атрибут страниц виртуальной памяти, запрещающий процессору исполнять код из страниц, помеченных как «данные». Реализован на аппаратном уровне через бит NX в записях таблицы страниц.
С NX даже если атакующий записал shellcode в стек или кучу, процессор откажется его выполнять и сгенерирует исключение (
PF с флагом NX).¶
Компоновать исполняемый файл без NX (отключить, только для экспериментов):
gcc -z execstack prog.c -o prog # разрешить выполнение стека
gcc prog.c -o prog # NX включён по умолчанию
Проверить наличие NX:
readelf -l prog | grep GNU_STACK
# RW — NX включён (только чтение/запись, не выполнение)
# RWE — NX отключён (чтение/запись/выполнение)
RELRO (RELocation Read-Only)¶
RELRO — механизм защиты секций ELF, содержащих таблицы перемещений (.got, .got.plt). Таблица GOT (Global Offset
Table) хранит адреса функций разделяемых библиотек и является популярной мишенью для атак перезаписи указателей.
Partial RELRO (по умолчанию): только .got переводится в режим read-only; .got.plt остаётся перезаписываемым (
нужен для ленивой компоновки).
Full RELRO: все таблицы перемещений разрешаются при старте программы, после чего весь .got.plt переводится в
read-only.
Проверить тип RELRO:
readelf -l prog | grep RELRO
# GNU_RELRO — присутствует partial RELRO
checksec --file=prog
# RELRO: Full RELRO / Partial RELRO / No RELRO
Минус Full RELRO — незначительное замедление запуска программы из-за немедленного разрешения всех символов.
Взаимодействие механизмов защиты¶
Механизмы образуют эшелонированную защиту:
- Stack canary обнаруживает переполнение буфера ещё до возврата из функции.
- ASLR лишает атакующего знания конкретных адресов стека, кучи и библиотек.
- NX/DEP запрещает исполнение инъецированного кода даже при успешной перезаписи адреса возврата.
- PIE распространяет рандомизацию ASLR на сам исполняемый файл.
- RELRO закрывает GOT от перезаписи, блокируя атаки через подмену указателей функций.
- CFI (Control Flow Integrity, например
-fsanitize=cfiв Clang) проверяет допустимость каждого косвенного перехода во время выполнения.
Обойти все уровни одновременно несравнимо сложнее, чем каждый по отдельности.
Связанные темы¶
- Стековые фреймы и вызовы функций — устройство стекового фрейма и адрес возврата
- Защита памяти — NX-бит, ASLR, атрибуты страниц на уровне ядра
- ELF-формат — секции GOT/PLT, устройство ELF-файла
Источники¶
man 3 __stack_chk_fail— реализация stack canaryman 8 sysctl— управление параметрами ядраman 1 gcc— флаги-fstack-protector-*,-z execstack- OWASP: Buffer Overflow — https://owasp.org/www-community/vulnerabilities/Buffer_Overflow
- Linux kernel documentation: ASLR — https://www.kernel.org/doc/html/latest/admin-guide/sysctl/kernel.html
checksec— утилита проверки защитных механизмов бинарных файлов