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

Защита от переполнения буфера (англ. buffer overflow protection) — это совокупность технологий и методик, применяемых при разработке программного обеспечения для повышения безопасности исполняемых программ посредством обнаружения переполнений буфера в переменных, выделяемых на стеке, с целью предотвращения некорректной работы программы или возникновения серьёзных уязвимостей безопасности. Переполнение буфера на стеке возникает, когда программа записывает данные по адресу памяти в своём стеке вызовов вне границ предназначенной для этого структуры данных (обычно — буфера фиксированной длины). Ошибки такого рода происходят, если программа записывает в буфер, размещённый на стеке, больше данных, чем ему выделено. Это почти всегда приводит к повреждению соседних данных на стеке, что может вызывать аварийное завершение программы, некорректную работу или проблемы с безопасностью. Современные стратегии защиты включают аппаратные механизмы (например, контроль целостности потока управления и теневой стек)[1].[2][3]

Описание

Обычно защита от переполнения буфера заключается в модификации организации данных на стеке: между буфером и управляющими данными размещается специальное значение — канарейка, которое разрушается при переполнении предшествующего ему в памяти буфера. Проверяя значение канарейки, программа может завершить выполнение, если обнаружено переполнение, что предотвращает повреждение или захват управления. К другим методам защиты относят контроль границ (bounds checking), при котором операции с памятью ограничиваются выделенной областью, а также тегирование, позволяющее помечать участки памяти так, чтобы они не могли содержать выполняемый код.

Переполнение буфера на стеке чаще влияет на выполнение программы, чем переполнение буфера в куче, поскольку стек содержит адреса возврата всех активных вызовов функций. Тем не менее существуют и похожие реализации защиты для предотвращения переполнений в куче. По данным MITRE CWE Top 25 за 2025 год, переполнение буфера на стеке (CWE-121) и в куче (CWE-122) остаются одними из самых распространённых и опасных программных слабостей[4].

Существуют различные реализации защиты от переполнения буфера, такие как для компилятора GCC, LLVM, Microsoft Visual Studio и других компиляторов.

Обзор

Переполнение буфера на стеке возникает, когда программа записывает данные по адресу памяти в стеке вызовов вне границ выделенного для этого буфера фиксированной длины. Это приводит к повреждению соседних данных на стеке и, если это произошло случайно, может вызвать сбой программы или её неправильную работу. Переполнение буфера на стеке является частным случаем более общей ошибки программирования — переполнение буфера. Так как стек содержит адреса возврата всех активных функций, переполнение буфера на стеке несёт больший риск повреждения логики выполнения программы, чем переполнение в куче[5].

Переполнение буфера на стеке может быть вызвано специально в результате атаки, известной как разрушение стека. Если программа работает с повышенными привилегиями или получает данные из ненадёжных источников (например, с общедоступного веб-сервера), такая ошибка обеспечивает уязвимость, позволяя злоумышленнику внедрить и выполнить вредоносный код. Это одна из старейших и надёжных техник повышения привилегий и несанкционированного доступа.

Обычно защита от переполнения буфера модифицирует структуру кадра стека каждой функции, добавляя «канарейку» — специальное контрольное значение, повреждение которого свидетельствует о переполнении буфера. Это позволяет предотвратить целый класс атак. По данным исследований, влияние этих техник на производительность минимально.

Защита от разрушения стека не способна предотвратить все возможные виды атак. Например, она не предотвращает переполнения буферов в куче, а также не защищает данные внутри структуры, где контрольное значение невозможно разместить безопасно между буферами. Поэтому программисты должны тщательно организовывать размещение переменных и структуру данных.

Современные стратегии предотвращения уязвимостей также включают концепцию «Secure by Design», продвигаемую NIST и CISA, которая заключается в системном предотвращении уязвимостей на этапе разработки путём перехода на языки программирования с безопасной работой с памятью.

Канарейки

Канарейки, canary words или stack cookies — это известные значения, размещаемые между буфером и управляющими данными на стеке для обнаружения переполнения буфера. При переполнении буфера первым повреждается именно канарейка, а неудачная верификация контрольного значения сигнализирует об ошибке переполнения, что позволяет, например, прервать выполнение или аннулировать повреждённые данные. Не следует путать канарейку с сторожевым значением.

Название отсылает к исторической практике: канареек использовали в шахтах, чтобы раньше людей ощущать ядовитые газы — так канарейки служили биологической системой раннего предупреждения. Также термин stack cookie («стековая печенька») подчёркивает, как повреждение данного значения образует «поломанную печеньку».

Выделяют три типа канареек: terminator, random и random XOR. Современные реализации StackGuard поддерживают все три типа; ProPolice реализует terminator и random-канарейки.

Основными методами обхода канареек являются утечка информации и атаки полным перебором (brute-force) в дочерних процессах при вызове fork. Для защиты от подобных атак применяются перерандомизация канарейки при fork и изоляция эталонного значения в недоступной для пользователя памяти.

Канарейки-терминаторы

Канарейки-терминаторы используют тот факт, что многие атаки основаны на строковых функциях, прекращающих копирование по встрече терминатора строки. Поэтому такие канарейки формируются из специальных байтов — нулевой байт, CR, LF, FF. Чтобы не испортить значение терминатора при атаке на возвратный адрес, злоумышленник должен правильно разместить нулевой символ перед адресом возврата. Это препятствует атакам средствами strcpy() и подобными, но делает значение канарейки известным, что позволяет обойти эту защиту при определённых условиях.

Случайные канарейки

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

Случайные XOR-канарейки

Случайные XOR-канарейки формируются так же, как и случайные, но дополнительно XOR-кодируются с частью управляющих данных. Если значения канарейки или контрольных данных изменяются, итоговое значение становится некорректным.

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

XOR-канарейки также защищают от некоторых атак с изменением указателей внутри структуры, но не могут защитить сами указатели, например, если функция использует переполнение для внедрения шоколада через перезапись указателя.

Контроль границ

Контроль границ (bounds checking) — это компиляторная технология, в которой для каждого выделенного участка памяти поддерживается информация о границах и производится контроль всех операций доступа к памяти в рантайме. Для языков C и C++ контроль границ может выполняться как на стадии вычисления указателя[6], так и при разыменовании указателя[7][8][9].

Возможны реализации с централизованным хранилищем, описывающим каждую область памяти[6][7][8], или с использованием толстых указателей[9], которые одновременно содержат адрес и сведения о границах.

Современные компиляторы (например, LLVM и Rust) используют статический анализ (в частности, Scalar Evolution) для исключения избыточных проверок границ (bounds check elimination) в циклах, что повышает производительность без ущерба для безопасности. Кроме того, в Clang разрабатывается флаг -fbounds-safety для комплексной проверки границ в языке C.

Тегирование

Тегирование[10] — компиляторная или аппаратная (требует тегированная архитектура) методика, позволяющая пометить тип данных в памяти для осуществления контроля типов. За счёт маркировки отдельных областей памяти как невыполняемых предотвращается внедрение и исполнение кода в выделенной под данные памяти. Помимо этого можно помечать отдельные блоки как невыделенные, предотвращая переполнения буфера.

Исторически тегирование использовалось в реализации высокоуровневых языков программирования[11]; при поддержке операционной системы возможно и обнаружение переполнений буфера. Пример аппаратной реализации — NX bit, поддерживаемый процессорами Intel, AMD и ARM.

К современным аппаратным реализациям тегирования относится механизм Intel Linear Address Masking (LAM), который позволяет использовать старшие биты 64-битного указателя для хранения метаданных (тегов), что аппаратно ускоряет обнаружение переполнений буфера.

Аппаратные механизмы защиты

Современные аппаратные архитектуры предоставляют встроенные механизмы для защиты от переполнения буфера и контроля целостности потока выполнения[12].

В архитектуре RISC-V реализовано несколько уровней защиты:

  • Защита физической памяти (PMP) — механизм, позволяющий задавать права доступа (чтение, запись, исполнение) для различных регионов физической памяти, что обеспечивает изоляцию критически важных областей от пользовательских приложений[13].
  • Расширения для контроля целостности потока выполнения (CFI):

Zicfiss — реализует аппаратный теневой стек для защиты обратных переходов. При вызове функции адрес возврата дублируется в защищённую область памяти, а перед возвратом сверяется с основным стеком, предотвращая подмену адреса[14].[15] Zicfilp — обеспечивает защиту прямых переходов, гарантируя, что передача управления возможна только на специально разрешённые участки кода («посадочные площадки»)[14].[15]

Фундаментальным подходом к обеспечению безопасности памяти является архитектура CHERI (Capability Hardware Enhanced RISC Instructions). Она заменяет традиционные указатели на аппаратно-защищённые «возможности» (capabilities). Такие структуры данных объединяют адрес в памяти с точными границами выделенного участка и правами доступа. Любая операция с памятью аппаратно проверяется на соответствие этим границам и разрешениям, что делает невозможным выход за границы буфера[16].

Языки с безопасной работой с памятью

Встроенный контроль границ и система владения в языке Rust обеспечивают безопасность работы с памятью по умолчанию с минимальными накладными расходами, в отличие от C++, где для аналогичного контроля во время выполнения требуются внешние инструменты со значительным снижением производительности.

С целью структурного исключения ошибок управления памятью Rust внедряется в ядро Linux для разработки драйверов и подсистем. В апреле 2026 года, с выходом ядра Linux версии 7.0, статус поддержки языка для этих задач был изменён на «полностью поддерживаемый»[17]. Использование системы типов и механизма проверок на этапе компиляции позволяет избежать целых классов уязвимостей, характерных для языка C, таких как переполнение буфера, разыменование нулевых указателей и использование памяти после освобождения[18].

Защита от использования после освобождения

Технология MiraclePtr, разработанная компанией Google для браузера Chrome, предназначена для защиты от уязвимостей, связанных с использованием памяти после её освобождения (use-after-free). Она представляет собой обёртку над «сырыми» указателями, которая выполняет дополнительные проверки. При попытке обращения к уже освобождённой памяти MiraclePtr аварийно завершает работу процесса, предотвращая тем самым эксплуатацию уязвимостей[19].[20]

Реализации

GNU Compiler Collection (GCC)

Первая практическая реализация защиты от разрушения стека появилась в технологии StackGuard в 1997 году; публикация о ней состоялась на конференции USENIX Security Symposium в 1998 году[21]. StackGuard был реализован как патч к x86 бэкенду GCC 2.7. С 1998 по 2003 год поддерживался и развивался для дистрибутива Linux Immunix, включая реализации всех видов канареек. В 2003 году предлагался к включению в GCC 3.x, но этого не произошло.

С 2001 по 2005 год IBM разработала патчи для GCC, известные как ProPolice[22]. ProPolice улучшил подход StackGuard, размещая буферы после локальных указателей и аргументов функций, снижая риск повреждения указателей и получения доступа к произвольной памяти.

В 2005 году инженеры Red Hat выявили проблемы в ProPolice и заново реализовали защиту для включения в GCC 4.1[23][24]. Введены флаги -fstack-protector (защита только уязвимых функций) и -fstack-protector-all (защита всех функций)[25].

В 2012 году инженеры Google реализовали флаг -fstack-protector-strong, который даёт баланс между безопасностью и производительностью[26]. Этот флаг входит в GCC начиная с версии 4.9[27].

Все пакеты Fedora компилируются с -fstack-protector с версии Fedora Core 5 и с -fstack-protector-strong с Fedora 20[28][29]. Большинство пакетов Ubuntu собирается с этим флагом с версии 6.10[30]. Все пакеты Arch Linux с 2011 года используют -fstack-protector[31], а с 4 мая 2014 года — -fstack-protector-strong. В Debian 13 и 14 флаг -fstack-protector-strong включён по умолчанию через dpkg-buildflags, а во FreeBSD 14 и 15 по умолчанию используется базовая защита SSP, а не strong. В ряде ОС (например, OpenBSD[32], Hardened Gentoo[33] и DragonFly BSD) защита является стандартной.

StackGuard и ProPolice не защищают от переполнений внутри структур, ведущих к перезаписи указателей функций. ProPolice старается разместить такие структуры перед указателями. Механизм отдельной защиты указателей — PointGuard[34], который реализован в Microsoft Windows[35].

В версии GCC 15 был добавлен атрибут counted_by для защиты гибких членов массива (Flexible Array Members, FAM) внутри структур, что позволяет более точно отслеживать выход за пределы выделенной памяти.

Кроме того, для защиты от перезаписи адреса возврата поддерживается генерация кода для аппаратного Shadow Stack (Intel CET) и программного механизма ShadowCallStack[1].[2]

Microsoft Visual Studio

Компиляторный пакет Microsoft реализует защиту от переполнения буфера с версии 2003 года с помощью ключа /GS, по умолчанию включённого с версии 2005. Отключить защиту можно флагом /GS-.

Актуальной и рекомендуемой технологией защиты целостности потока управления в Windows является Control Flow Guard (CFG). Более строгая технология eXtended Flow Guard (XFG) была отменена.

Компилятор IBM

Защиту от разрушения стека можно активировать флагом -qstackprotect.

Clang/LLVM

Clang поддерживает те же флаги -fstack-protector, что и GCC[36], а также более гибкую систему «безопасного стека» (-fsanitize=safe-stack), не оказывающую существенного влияния на производительность[37]. Clang предоставляет три детектора переполнения буфера: AddressSanitizer (-fsanitize=address)[8], UBSan (-fsanitize=bounds)[38], а также неофициальный SafeCode (разработан до LLVM 3.0)[39], который был заброшен и не поддерживается в современных версиях LLVM.

Эти системы различаются по производительности, расходу памяти и обнаруживаемым классам ошибок. В ряде ОС (например, OpenBSD)[40] защита стека включена по умолчанию.

В компиляторе реализована технология ShadowCallStack, активно используемая в Android на архитектуре AArch64, а в iOS применяется аппаратная технология Pointer Authentication Codes (PAC). Кроме того, Clang поддерживает программные и аппаратные механизмы контроля целостности потока выполнения (Control-Flow Integrity, CFI)[3].

Компилятор Intel

Компиляторы Intel C и C++ поддерживают защиту от разрушения стека опциями, аналогичными GCC и Microsoft Visual Studio.

Fail-Safe C

Fail-Safe C[9] — это свободный компилятор ANSI C с контролем границ на основе толстых указателей и объектно-ориентированным доступом к памяти[41].

StackGhost (аппаратная реализация)

Технология StackGhost, изобретённая Майком Франценом, использует аппаратные возможности архитектуры SPARC (отложенные spilling/filling регистровых окон), чтобы обнаруживать изменения указателей возврата и защищать все приложения без изменений исходного кода. Производительность почти не страдает (меньше 1%). Поддержка была интегрирована и оптимизирована в OpenBSD/SPARC.

Примечания

  1. 1 2 Enabling Intel CET (Control-flow Enforcement Technology). h3xduck.github.io (26 июня 2025). Дата обращения: 28 мая 2026.
  2. 1 2 ShadowCallStack. clang.llvm.org. Дата обращения: 28 мая 2026.
  3. 1 2 Control Flow Integrity. clang.llvm.org. Дата обращения: 28 мая 2026.
  4. 2025 CWE Top 25 Most Dangerous Software Weaknesses. CWE. MITRE (2025). Дата обращения: 28 мая 2026.
  5. Fithen, William L.; Seacord, Robert VT-MB. Violation of Memory Bounds. US CERT (27 марта 2007). Дата обращения: 28 мая 2026. Архивировано 18 июля 2011 года.
  6. 1 2 Bounds Checking for C. Doc.ic.ac.uk. Дата обращения: 28 мая 2026. Архивировано 26 марта 2016 года.
  7. 1 2 SAFECode: Secure Virtual Architecture. Sva.cs.illinois.edu (12 августа 2009). Дата обращения: 28 мая 2026. Архивировано 13 мая 2025 года.
  8. 1 2 3 google/sanitizers (19 июня 2021). Дата обращения: 28 мая 2026.
  9. 1 2 3 Fail-Safe C: Top Page. Staff.aist.go.jp (7 мая 2013). Дата обращения: 28 мая 2026. Архивировано 7 июля 2016 года.
  10. Tuesday, April 05, 2005. Feustel.us. Дата обращения: 28 мая 2026. Архивировано 23 июня 2016 года.
  11. Steenkiste, Peter; Hennessy, John (1987). “Tags and type checking in LISP: hardware and software approaches”. ACM SIGOPS Operating Systems Review. ACM. 21 (4): 50—59. DOI:10.1145/36204.36183.
  12. Hardware Assisted Buffer Protection Mechanisms for Embedded RISC-V. IEEE Xplore. IEEE. Дата обращения: 28 мая 2026.
  13. RISC-V Memory Protection: Diving Deep into the Complexities. Incore Semiconductor. Дата обращения: 28 мая 2026.
  14. 1 2 Control-Flow Integrity (CFI). RISC-V International. Дата обращения: 28 мая 2026.
  15. 1 2 Control-flow Integrity (CFI) extensions for RISC-V. LWN.net. Дата обращения: 28 мая 2026.
  16. Discover CHERI. CHERI Alliance. Дата обращения: 28 мая 2026.
  17. Торвальдс дал зелёный свет: ядро Linux 7.0 официально поддерживает Rust. CNews (13 апреля 2026). Дата обращения: 28 мая 2026.
  18. Это выигрыш для всех: почему ключевой разработчик Linux больше не хочет писать на C. Proglib (26 февраля 2025). Дата обращения: 28 мая 2026.
  19. MiraclePtr: Protecting users from use-after-free vulnerabilities. security.googleblog.com (январь 2024). Дата обращения: 28 мая 2026.
  20. Google рассказала о технологии MiraclePtr для защиты от 57% уязвимостей use-after-free. OpenNet. Дата обращения: 28 мая 2026.
  21. Papers - 7th USENIX Security Symposium, 1998. Usenix.org (12 апреля 2002). Дата обращения: 28 мая 2026. Архивировано 30 сентября 2000 года.
  22. GCC extension for protecting applications from stack-smashing attacks. Research.ibm.com. Дата обращения: 27 апреля 2014. Архивировано 1 апреля 2004 года.
  23. GCC 4.1 Release Series — Changes, New Features, and Fixes - GNU Project - Free Software Foundation (FSF). Gcc.gnu.org. Дата обращения: 28 мая 2026. Архивировано 19 августа 2025 года.
  24. Richard Henderson - [rfc] reimplementation of ibm stack-smashing protector. Gcc.gnu.org. Дата обращения: 28 мая 2026. Архивировано 15 июня 2006 года.
  25. Optimize Options - Using the GNU Compiler Collection (GCC). Gcc.gnu.org. Дата обращения: 28 мая 2026. Архивировано 14 июня 2025 года.
  26. Han Shen(ææ) - [PATCH] Add a new option "-fstack-protector-strong" (patch / doc inside). Gcc.gnu.org (14 июня 2012). Дата обращения: 28 мая 2026. Архивировано 2 июля 2013 года.
  27. Edge, Jake "Strong" stack protection for GCC. Linux Weekly News (5 февраля 2014). Дата обращения: 28 мая 2026. Архивировано 30 сентября 2025 года.
  28. Security Features. FedoraProject (11 декабря 2013). Дата обращения: 28 мая 2026. Архивировано 13 сентября 2025 года.
  29. #1128 (switching from "-fstack-protector" to "-fstack-protector-strong" in Fedora 20) – FESCo. Fedorahosted.org. Дата обращения: 28 мая 2026. Архивировано 2 октября 2013 года.
  30. Security/Features - Ubuntu Wiki. Wiki.ubuntu.com. Дата обращения: 28 мая 2026. Архивировано 9 октября 2025 года.
  31. FS#18864 : Consider enabling GCC's stack-smashing protection (ProPolice, SSP) for all packages. Bugs.archlinux.org. Дата обращения: 28 мая 2026. Архивировано 27 апреля 2014 года.
  32. OpenBSD's gcc-local(1) manual page. Дата обращения: 28 мая 2026. Архивировано 20 сентября 2025 года.
  33. Hardened/Toolchain - Gentoo Wiki (31 июля 2016). Дата обращения: 28 мая 2026. Архивировано 16 ноября 2025 года.
  34. 12th USENIX Security Symposium — Technical Paper. Дата обращения: 28 мая 2026. Архивировано 1 октября 2004 года.
  35. MSDN Blogs – Get the latest information, insights, announcements, and news from Microsoft experts and developers in the MSDN blogs. (6 августа 2021). Дата обращения: 28 мая 2026. Архивировано 26 октября 2006 года.
  36. Clang mailing list. Clang.llvm.org (28 апреля 2017). Дата обращения: 28 мая 2026. Архивировано 24 декабря 2024 года.
  37. SafeStack — Clang 17.0.0git documentation. clang.llvm.org. Дата обращения: 28 мая 2026. Архивировано 22 января 2025 года.
  38. Clang Compiler User's Manual — Clang 3.5 documentation. Clang.llvm.org. Дата обращения: 28 мая 2026. Архивировано 17 ноября 2025 года.
  39. SAFECode. Safecode.cs.illinois.edu. Дата обращения: 28 мая 2026.
  40. OpenBSD's clang-local(1) manual page. Дата обращения: 28 мая 2026. Архивировано 6 октября 2025 года.
  41. thesis.dvi. Staff.aist.go.jp. Дата обращения: 28 мая 2026. Архивировано 8 апреля 2025 года.

Литература

  • Steenkiste, Peter; Hennessy, John (1987). “Tags and type checking in LISP: hardware and software approaches”. ACM SIGOPS Operating Systems Review. ACM. 21 (4): 50—59. DOI:10.1145/36204.36183.

Категории