Приведите пример уязвимости переполнения буфера на C (функция strcpy) и объясните низкоуровневые механизмы эксплуатации (return-oriented programming) и способы защиты на уровне компилятора и ОС
Пример уязвимого к переполнению буфера кода (C): ```c #include
#include
void vuln(char *s) { char buf[64]; // уязвимый буфер strcpy(buf, s); // не проверяет длину -> overflow } int main(int argc, char **argv) { if (argc < 2) return 1; vuln(argv[1]); puts("done"); return 0; } ``` Короткое объяснение низкоуровневого механизма переполнения (стек, сохранённый адрес возврата): - На x86_64 стековая рамка функции обычно содержит подряд: локальные переменные (buf), затем сохранённый регистр кадра (saved RBP), затем сохранённый адрес возврата (saved RIP). - Если буфер размером 646464 байта переписать более чем на 646464 байта, запись дойдёт до saved RBP и далее до saved RIP. Смещение до saved RIP обычно равно 64+8=7264 + 8 = 7264+8=72 байтам, то есть нужно переписать как минимум 727272 байта, чтобы изменить адрес возврата. - При возврате из функции процессор берет значение из saved RIP и продолжает исполнение с этого адреса — если атакующий контролирует saved RIP, он может перенаправить выполнение. Return-Oriented Programming (ROP) — принцип эксплуатации без выполнения инъектированного кода: - NX/DEP запрещают исполнение данных на стеке, поэтому вместо того, чтобы записать и исполнить shellcode, атакующий использует уже существующий исполняемый код в памяти — короткие последовательности инструкций, заканчивающиеся инструкцией ret (gadgets). - Гаджеты собираются в цепочку: каждая инструкция ret попадает на следующий адрес в «цепочке» на стеке, тем самым реализуя произвольный контроль над потоком исполнения. - С помощью правильно выбранных гаджетов можно подставлять значения в регистры по ABI (на x86_64 аргументы syscall/syscall-like вызовов вносятся в RDI, RSI, RDX и т.д.), вызвать libc-функции (ret2libc) или выполнить syscall. - Тонкости: выравнивание стека, сохранённые контексты, необходимость найти подходящие гаджеты в доступных модулях, ограничения ASLR (рандомизация адресов). Защита на уровне компилятора и исполняющей среды (коротко, как работают и что дают): - Stack canaries (флаг компилятора: -fstack-protector): помещают «канарейку» между локальными данными и метаданными стек-кадра; при переполнении значение меняется и до возврата выполняется проверка — при несоответствии процесс аварийно завершает программу. - NX/DEP (неисполняемый стек): помечает страницы стека как «не для исполнения», мешая запуску инъектированного кода; ROP обходит это, так как использует уже исполняемый код. - ASLR (Address Space Layout Randomization): рандомизирует базовые адреса исполняемых модулей (включая libc), затрудняя угадывание адресов гаджетов/функций; обходится с помощью утечек информации (info leak) или неподвижных модулей без ASLR. - PIE (Position Independent Executables): делает сам бинарь рандомизируемым, усиливая ASLR. - RELRO (partial/full): защищает секции динамической загрузки (GOT) от перезаписи; full RELRO делает GOT доступным только для чтения после загрузки. - D.Флаг -D_FORTIFY_SOURCE и проверки библиотек: добавляют проверки размеров в стандартные функции при оптимизации. - Control-Flow Integrity (CFI) и Shadow Stack: продвинутые методы, ограничивающие легитимные переходы управления и/или проверяющие соответствие возвращаемых адресов сохранённым. - Kernel-level контроль: SMAP/SMEP предотвращают исполнение/доступ ядрового кода с пользовательской памяти; LSM и политиками (SELinux) ограничивают возможности. Короткие замечания по обходу и практикам защиты: - ROP и ret2libc служат обходом NX; ASLR и PIE требуют утечек информации или наличия модулей без рандомизации. - Лучшие практики: избегать небезопасных функций (strcpy/gets), использовать проверки размеров (strncpy/strlcpy/safe APIs), включать компиляторные защиты (-fstack-protector, -D_FORTIFY_SOURCE, -fPIE -pie), включать RELRO, использовать ASLR и современные механизмы CFI/Shadow Stack на уровне ОС/компиляции. Если нужно, могу кратко показать, какие компиляторные флаги включить и как изменить пример, чтобы он стал безопасным.
```c
#include #include
void vuln(char *s) {
char buf[64]; // уязвимый буфер
strcpy(buf, s); // не проверяет длину -> overflow
}
int main(int argc, char **argv) {
if (argc < 2) return 1;
vuln(argv[1]);
puts("done");
return 0;
}
```
Короткое объяснение низкоуровневого механизма переполнения (стек, сохранённый адрес возврата):
- На x86_64 стековая рамка функции обычно содержит подряд: локальные переменные (buf), затем сохранённый регистр кадра (saved RBP), затем сохранённый адрес возврата (saved RIP).
- Если буфер размером 646464 байта переписать более чем на 646464 байта, запись дойдёт до saved RBP и далее до saved RIP. Смещение до saved RIP обычно равно 64+8=7264 + 8 = 7264+8=72 байтам, то есть нужно переписать как минимум 727272 байта, чтобы изменить адрес возврата.
- При возврате из функции процессор берет значение из saved RIP и продолжает исполнение с этого адреса — если атакующий контролирует saved RIP, он может перенаправить выполнение.
Return-Oriented Programming (ROP) — принцип эксплуатации без выполнения инъектированного кода:
- NX/DEP запрещают исполнение данных на стеке, поэтому вместо того, чтобы записать и исполнить shellcode, атакующий использует уже существующий исполняемый код в памяти — короткие последовательности инструкций, заканчивающиеся инструкцией ret (gadgets).
- Гаджеты собираются в цепочку: каждая инструкция ret попадает на следующий адрес в «цепочке» на стеке, тем самым реализуя произвольный контроль над потоком исполнения.
- С помощью правильно выбранных гаджетов можно подставлять значения в регистры по ABI (на x86_64 аргументы syscall/syscall-like вызовов вносятся в RDI, RSI, RDX и т.д.), вызвать libc-функции (ret2libc) или выполнить syscall.
- Тонкости: выравнивание стека, сохранённые контексты, необходимость найти подходящие гаджеты в доступных модулях, ограничения ASLR (рандомизация адресов).
Защита на уровне компилятора и исполняющей среды (коротко, как работают и что дают):
- Stack canaries (флаг компилятора: -fstack-protector): помещают «канарейку» между локальными данными и метаданными стек-кадра; при переполнении значение меняется и до возврата выполняется проверка — при несоответствии процесс аварийно завершает программу.
- NX/DEP (неисполняемый стек): помечает страницы стека как «не для исполнения», мешая запуску инъектированного кода; ROP обходит это, так как использует уже исполняемый код.
- ASLR (Address Space Layout Randomization): рандомизирует базовые адреса исполняемых модулей (включая libc), затрудняя угадывание адресов гаджетов/функций; обходится с помощью утечек информации (info leak) или неподвижных модулей без ASLR.
- PIE (Position Independent Executables): делает сам бинарь рандомизируемым, усиливая ASLR.
- RELRO (partial/full): защищает секции динамической загрузки (GOT) от перезаписи; full RELRO делает GOT доступным только для чтения после загрузки.
- D.Флаг -D_FORTIFY_SOURCE и проверки библиотек: добавляют проверки размеров в стандартные функции при оптимизации.
- Control-Flow Integrity (CFI) и Shadow Stack: продвинутые методы, ограничивающие легитимные переходы управления и/или проверяющие соответствие возвращаемых адресов сохранённым.
- Kernel-level контроль: SMAP/SMEP предотвращают исполнение/доступ ядрового кода с пользовательской памяти; LSM и политиками (SELinux) ограничивают возможности.
Короткие замечания по обходу и практикам защиты:
- ROP и ret2libc служат обходом NX; ASLR и PIE требуют утечек информации или наличия модулей без рандомизации.
- Лучшие практики: избегать небезопасных функций (strcpy/gets), использовать проверки размеров (strncpy/strlcpy/safe APIs), включать компиляторные защиты (-fstack-protector, -D_FORTIFY_SOURCE, -fPIE -pie), включать RELRO, использовать ASLR и современные механизмы CFI/Shadow Stack на уровне ОС/компиляции.
Если нужно, могу кратко показать, какие компиляторные флаги включить и как изменить пример, чтобы он стал безопасным.