Введение
Техники безопасности программного обеспечения. Защита от переполнения буфера – это один из множества методов, используемых при разработке программного обеспечения для повышения безопасности исполняемых программ путем обнаружения переполнения буфера в переменных, выделенных в стеке, и предотвращения нештатного поведения программы или возникновения серьезных уязвимостей в системе безопасности. Переполнение буфера стека происходит, когда программа записывает данные по адресу памяти в стеке вызовов программы за пределами предназначенной структуры данных, которая обычно представляет собой буфер фиксированной длины. Ошибки переполнения буфера стека возникают, когда программа записывает в буфер, расположенный в стеке, больше данных, чем было для него выделено. Это почти всегда приводит к повреждению соседних данных в стеке, что может вызвать сбой программы, некорректную работу или проблемы с безопасностью. Как правило, защита от переполнения буфера изменяет организацию данных, выделенных в стеке, добавляя в нее «канареечное» значение (canary value), которое, будучи изменено в результате переполнения буфера стека, указывает на то, что буфер, расположенный перед ним в памяти, был переполнен. Проверяя «канареечное» значение, можно остановить выполнение затронутой программы, предотвращая ее нештатное поведение или передачу управления атакующему. Другие методы защиты от переполнения буфера включают проверку границ, которая контролирует доступ к каждому выделенному блоку памяти, чтобы предотвратить выход за пределы выделенного пространства, и тегирование, которое гарантирует, что память, выделенная для хранения данных, не может содержать исполняемый код. Переполнение буфера, выделенного в стеке, с большей вероятностью повлияет на выполнение программы, чем переполнение буфера в куче, поскольку в стеке хранятся адреса возврата для всех активных вызовов функций. Однако существуют также аналогичные, специфичные для реализации, средства защиты от переполнения, основанного на куче. Существует несколько реализаций защиты от переполнения буфера, включая реализации для GNU Compiler Collection, LLVM, Microsoft Visual Studio и других компиляторов.
Buffer overflow protection is any of various techniques used during software development to enhance the security of executable programs by detecting buffer overflows on stack allocated variables, and preventing them from causing program misbehavior or from becoming serious security vulnerabilities. A stack buffer overflow occurs when a program writes to a memory address on the program's call stack outside of the intended data structure, which is usually a fixed length buffer. Stack buffer overflow bugs are caused when a program writes more data to a buffer located on the stack than what is actually allocated for that buffer. This almost always results in corruption of adjacent data on the stack, which could lead to program crashes, incorrect operation, or security issues. Typically, buffer overflow protection modifies the organization of stack allocated data so it includes a canary value that, when destroyed by a stack buffer overflow, shows that a buffer preceding it in memory has been overflowed. By verifying the canary value, execution of the affected program can be terminated, preventing it from misbehaving or from allowing an attacker to take control over it. Other buffer overflow protection techniques include bounds checking, which checks accesses to each allocated block of memory so they cannot go beyond the actually allocated space, and tagging, which ensures that memory allocated for storing data cannot contain executable code. Overfilling a buffer allocated on the stack is more likely to influence program execution than overfilling a buffer on the heap because the stack contains the return addresses for all active function calls. However, similar implementation specific protections also exist against heap based overflows. There are several implementations of buffer overflow protection, including those for the GNU Compiler Collection, LLVM, Microsoft Visual Studio, and other compilers.
Обзор
Переполнение буфера стека происходит, когда программа записывает данные по адресу памяти в стеке вызовов программы за пределы выделенной структуры данных, которая обычно представляет собой буфер фиксированной длины. Ошибки переполнения буфера стека возникают, когда программа записывает в буфер, расположенный в стеке, больше данных, чем для него выделено. Это почти всегда приводит к повреждению соседних данных в стеке, и в случаях, когда переполнение произошло случайно, часто вызывает сбой программы или её некорректную работу. Переполнение буфера стека является разновидностью более общей программной ошибки, известной как переполнение буфера (или переполнение). Переполнение буфера в стеке с большей вероятностью нарушит выполнение программы, чем переполнение буфера в куче, поскольку стек содержит адреса возврата для всех активных вызовов функций. Переполнение буфера стека может быть вызвано намеренно в рамках атаки, известной как «разбиение стека». Если уязвимая программа выполняется с повышенными привилегиями или принимает данные от недоверенных сетевых хостов (например, публичного веб-сервера), то эта ошибка представляет собой потенциальную уязвимость безопасности, позволяющую злоумышленнику внедрить исполняемый код в работающую программу и получить контроль над процессом. Это один из старейших и наиболее надёжных методов для злоумышленников для получения несанкционированного доступа к компьютеру. Обычно защита от переполнения буфера изменяет организацию данных в стековой рамке вызова функции, добавляя «канарское» значение, которое, будучи изменено, указывает на переполнение буфера, расположенного перед ним в памяти. Это позволяет предотвратить целый класс атак. По мнению некоторых исследователей, влияние этих методов на производительность незначительно. Защита от разбиения стека не может защитить от определённых видов атак, например, от переполнения буфера в куче. Не существует разумного способа изменить структуру данных внутри структуры; структуры должны быть одинаковыми между модулями, особенно при использовании общих библиотек. Любые данные в структуре, расположенные после буфера, невозможно защитить с помощью «канареек»; поэтому программисты должны тщательно продумывать организацию переменных и использование структур.
Канарские птицы
Канарские значения, или стек-куки, — это известные значения, которые размещаются между буфером и управляющими данными в стеке для обнаружения переполнения буфера. При переполнении буфера первыми повреждаются, как правило, канарские значения, и неудачная проверка этих данных сигнализирует о переполнении, которое затем может быть обработано, например, путем отбрасывания поврежденных данных. Канарское значение не следует путать со стражевым значением. Терминология отсылает к исторической практике использования канареек в угольных шахтах, поскольку они реагировали на токсичные газы раньше шахтеров, таким образом, служа биологической системой предупреждения. Канарские значения также известны как стек-куки, что призвано вызвать образ "сломанного печенья" при их повреждении. Существуют три типа канарских значений: терминатор, случайное и случайное XOR. Текущие версии StackGuard поддерживают все три типа, а ProPolice — терминатор и случайные канарские значения.
Канарские терминаторы
Терминаторные канарейки используют наблюдение о том, что большинство атак переполнения буфера основаны на определенных строковых операциях, завершающихся символами-терминаторами строк. В ответ на это канарейки строятся из нулевых терминаторов, CR, LF и FF. В результате, злоумышленнику необходимо записать нулевой символ перед записью адреса возврата, чтобы избежать изменения канарейки. Это предотвращает атаки с использованием strcpy и других методов, которые прекращают работу при копировании нулевого символа, однако нежелательным следствием является то, что значение канарейки известно. Даже при наличии этой защиты злоумышленник потенциально может перезаписать канарейку её известным значением и контролировать информацию с использованием несоответствующих значений, тем самым успешно проходя проверку канарейки, которая выполняется непосредственно перед инструкцией возврата из вызова процессора.
Случайные канарские птицы
Случайные значения "канарейки" генерируются случайным образом, как правило, с помощью демона сбора энтропии, чтобы предотвратить возможность узнать их атакующему. Обычно логически невозможно или маловероятно прочитать значение "канарейки" для использования в эксплойте; "канарейка" представляет собой защищённое значение, известное только тем, кому оно необходимо – в данном случае, коду защиты от переполнения буфера. Как правило, случайное значение "канарейки" генерируется при инициализации программы и сохраняется в глобальной переменной. Эта переменная обычно дополняется не отображёнными страницами памяти, чтобы любая попытка её прочитать с использованием уязвимостей для считывания данных из оперативной памяти приводила к ошибке сегментации и завершению программы. Тем не менее, прочитать "канарейку" всё ещё может быть возможно, если злоумышленник знает её местоположение или сможет заставить программу читать данные из стека.
Случайные XOR канарские птицы
Случайные XOR-канарейки — это случайные значения, зашифрованные операцией XOR с использованием всех или части управляющих данных. Таким образом, если канарейка или управляющие данные будут повреждены, значение канарейки станет неверным. Случайные XOR-канарейки имеют те же уязвимости, что и обычные случайные канарейки, за исключением того, что способ получения канарейки путем чтения из стека становится немного сложнее. Злоумышленнику необходимо получить значение канарейки, алгоритм и управляющие данные, чтобы воссоздать исходное значение канарейки, необходимое для обхода защиты. Кроме того, случайные XOR-канарейки могут защитить от определенного типа атаки, при которой переполнение буфера в структуре приводит к перезаписи указателя, чтобы он указывал на часть управляющих данных. Благодаря XOR-шифрованию, канарейка будет неверной, если управляющие данные или возвращаемое значение будут изменены. Благодаря указателю, управляющие данные или возвращаемое значение могут быть изменены без переполнения канарейки. Хотя эти канарейки защищают управляющие данные от изменения через поврежденные указатели, они не защищают другие данные или сами указатели. Особенно проблематичны указатели на функции, так как их можно переполнить, и при вызове они могут выполнить shellcode.
Проверка границ
Проверка границ — это техника, основанная на компиляторе, которая добавляет информацию о границах во время выполнения для каждого выделенного блока памяти и проверяет все указатели относительно этих границ во время выполнения. Для языков C и C++ проверка границ может выполняться во время вычисления указателя или во время разыменования. Реализации этого подхода используют либо центральный репозиторий, описывающий каждый выделенный блок памяти, либо технику, основанную на компиляторе или аппаратном обеспечении (требующую архитектуру с тегами), для маркировки типа данных в памяти, используемую в основном для проверки типов. Отмечая определенные области памяти как неисполняемые, можно эффективно предотвратить размещение исполняемого кода в памяти, выделенной для хранения данных. Кроме того, некоторые области памяти могут быть помечены как невыделенные, предотвращая переполнение буфера. Ранее тегирование использовалось для реализации языков программирования высокого уровня; при соответствующей поддержке операционной системы тегирование также может использоваться для обнаружения переполнения буфера. Примером является аппаратная функция NX bit, поддерживаемая процессорами Intel, AMD и ARM.
GNU Compiler Collection (GCC) - коллекция компиляторов GNU
Защита от переполнения стека была впервые реализована StackGuard в 1997 году и опубликована на Симпозиуме по безопасности USENIX 1998 года. StackGuard был представлен как набор патчей для бэкенда Intel x86 GCC 2.7. StackGuard поддерживался для дистрибутива Immunix Linux с 1998 по 2003 год и был расширен реализациями для терминаторов, случайных и случайных XOR-канареек. StackGuard был предложен для включения в GCC 3.x на саммите GCC 2003 года, но этого так и не было достигнуто. С 2001 по 2005 год IBM разработала патчи для GCC, обеспечивающие защиту от переполнения стека, известные как ProPolice. ProPolice улучшила идею StackGuard, размещая буферы после локальных указателей и аргументов функций в стековом фрейме. Это помогало избежать повреждения указателей, предотвращая доступ к произвольным областям памяти. Инженеры Red Hat выявили проблемы с ProPolice, и в 2005 году реализовали защиту от переполнения стека для включения в GCC 4.1. В этой работе был представлен флаг `fstack-protector`, который защищает только некоторые уязвимые функции, и флаг `fstack-protector-all`, который защищает все функции, независимо от необходимости такой защиты. В 2012 году инженеры Google реализовали флаг `fstack-protector-strong`, чтобы достичь лучшего баланса между безопасностью и производительностью. Этот флаг защищает больше типов уязвимых функций, чем `fstack-protector`, но не все функции, обеспечивая при этом лучшую производительность, чем `fstack-protector-all`. Он доступен в GCC начиная с версии 4.9. Все пакеты Fedora компилируются с использованием `fstack-protector` с момента Fedora Core 5, а с Fedora 20 – с использованием `fstack-protector-strong`. Большинство пакетов в Ubuntu компилируются с использованием `fstack-protector` начиная с версии 6.10. Каждый пакет Arch Linux компилируется с использованием `fstack-protector` с 2011 года. Все пакеты Arch Linux, созданные после 4 мая 2014 года, используют `fstack-protector-strong`. Защита стека используется только для некоторых пакетов в Debian и только для базовой системы FreeBSD начиная с версии 8.0. Защита стека является стандартной в некоторых операционных системах, включая OpenBSD, Hardened Gentoo и DragonFly BSD. StackGuard и ProPolice не могут защитить от переполнения в автоматически выделенных структурах, которые приводят к переполнению указателей функций. ProPolice, по крайней мере, изменяет порядок выделения памяти, чтобы такие структуры выделялись перед указателями функций. Отдельный механизм защиты указателей был предложен в PointGuard и доступен в Microsoft Windows.
Microsoft Visual Studio (всего лишь один раз)
Комплект компиляторов Microsoft реализует защиту от переполнения буфера, начиная с версии 2003, с помощью переключателя командной строки, который включен по умолчанию, начиная с версии 2005. Использование отключает эту защиту.
IBM компилятор
Защита от переполнения стека может быть включена флагом компилятора qstackprotect.
Кланг/LLVM
Clang поддерживает те же опции защиты fstack, что и GCC, а также более надежную систему "безопасного стека" с сопоставимо небольшим влиянием на производительность. Clang также располагает тремя средствами обнаружения переполнения буфера, а именно AddressSanitizer (fsanitize=address) и неофициальный SafeCode (последнее обновление для LLVM 3.0). Эти системы имеют различные компромиссы с точки зрения снижения производительности, потребления памяти и типов обнаруживаемых ошибок. Защита стека является стандартной функцией в некоторых операционных системах, включая OpenBSD.
and the unofficial SafeCode (last updated for LLVM 3.0). These systems have different tradeoffs in terms of performance penalty, memory overhead, and classes of detected bugs. Stack protection is standard in certain operating systems, including OpenBSD.
Компьютерная версия
Компилятор Intel C и C++ поддерживает защиту от переполнения стека с опциями, аналогичными предоставляемым GCC и Microsoft Visual Studio.
StackGhost (на основе аппаратного обеспечения)
StackGhost, разработанный Майком Франценом, представляет собой простое изменение в подпрограммах сброса/восстановления окон регистров, которое значительно усложняет эксплуатацию переполнений буфера. Он использует уникальную аппаратную особенность архитектуры SPARC от Sun Microsystems (а именно: отложенный сброс/восстановление окон регистров кадров в стек) для прозрачного обнаружения изменений указателей возврата (распространенный способ перехвата потока выполнения эксплойтом), автоматически защищая все приложения без необходимости внесения изменений в бинарный код или исходный текст. Влияние на производительность пренебрежимо мало, менее одного процента. Возникшие проблемы с gdb были решены Марком Кеттенисом спустя два года, что позволило активировать эту функцию. Впоследствии код StackGhost был интегрирован (и оптимизирован) в OpenBSD/SPARC.