Перейти к содержанию

Защита от переполнения буфера

Переполнение буфера (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

Средства защиты

Современные системы применяют несколько взаимодополняющих механизмов:

  1. Stack Canary — специальное значение между буфером и адресом возврата;
  2. ASLR — рандомизация адресного пространства;
  3. NX / DEP — запрет выполнения данных;
  4. RELRO — защита таблиц перемещений от записи;
  5. CFI — проверка целостности потока управления;
  6. Безопасные функцииstrlcpy, snprintf вместо strcpy, sprintf.

Stack Canary

Stack Protector (или stack canary) — наиболее распространённый аппаратно-программный механизм. Его суть в следующем:

  1. При входе в функцию между локальными переменными и адресом возврата помещается случайное значение — «канарейка» ( canary), взятое из потокового локального хранилища (TLS).
  2. Перед возвратом из функции canary сравнивается с исходным значением.
  3. Если 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;
}
gcc -g prog.c -o prog
./prog
# *** stack smashing detected ***: terminated
# Aborted (core dumped)

Без stack protector:

gcc -fno-stack-protector prog.c -o prog
./prog
# Segmentation fault (или неопределённое поведение)

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 (без прав суперпользователя):

setarch x86_64 -R ./prog

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.

gcc -Wl,-z,relro prog.c -o prog            # Partial RELRO
gcc -Wl,-z,relro,-z,now prog.c -o prog     # Full RELRO

Проверить тип 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) проверяет допустимость каждого косвенного перехода во время выполнения.

Обойти все уровни одновременно несравнимо сложнее, чем каждый по отдельности.

Связанные темы

Источники

  • man 3 __stack_chk_fail — реализация stack canary
  • man 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 — утилита проверки защитных механизмов бинарных файлов