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

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

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

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

Обзор

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

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

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

Защита от атаки stack-smashing не может предотвратить некоторые формы атак. Например, такая защита не защищает от переполнения буфера в куче. Нет разумного способа изменить расположение данных внутри структуры, поскольку ожидается, что структуры будут иметь одинаковое расположение данных при использовании в разных модулях, особенно при использовании общих библиотек. Любые данные в структуре невозможно защитить с помощью «канареек», поэтому программисты должны быть очень осторожны при организации переменных и использовании структур.

Термин «канарейка» Канарейки, канареечные слова, предохранители или куки стека — это известные (предопределённые) значения, которые располагаются между буфером и контрольными данными в стеке для отслеживания переполнений буфера. Когда буфер переполняется, первыми же испорченными данными будет канарейка и при проверке несовпадение значения с ожидаемым сигнализирует о переполнении, что позволяет принять меры, например, аннулировать повреждённые данные. Канареечное значение не следует путать со сторожевым значением.

Термин «канарейка» отсылает к исторической практике использования . Поскольку канарейки более чувствительны к токсичным газам, раньше шахтёры использовали их как систему предупреждения. «Канарейки» известны также как куки стека (cookies = куки, в переводе означает печенье), что призвано вызвать образ «сломанной печеньки» при повреждении значения.

Используется три типа «канареек»: terminator (терминатор, канарейка с использованием символов конца строки – подразумевает, что большинство переполнений происходит на строках), random (случайная – обнаруживает все перезаписи памяти при условии, что атакующий не знает значение канарейки) и random XOR (случайная XOR канарейка – используется при условии, если злоумышленник может переписывать память в случайном месте). Текущие версии StackGuard поддерживают все три, в то время как ProPolice поддерживает значения terminator и random.

Канареечное значение terminator «Канарейка» terminator использует наблюдение, что большинство атак на переполнение буфера базируются на определённых строковых операциях, которые завершаются на символе конца строки (терминаторе). Реакцией на это наблюдение является то, что канарейки состоят из , CR, LF и FF. В результате атакующий должен записать нулевой символ перед адресом возврата чтобы избежать изменения канарейки. Это предотвращает атаки с использованием strcpy() и других методов, завершающихся при попадании нулевого символа, хотя нежелательным результатом служит известность значения канарейки. Даже с такой защитой злоумышленник может перезаписать канарейку её известным значением и управлять информацией с несовпадающими значениями. Если атакующий сможет переписать стек несколько раз, то он сможет заменить защищенную контрольную информацию и адрес возврата.

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

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

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

Случайные XOR канарейки имеют те же уязвимости, что и случайные канарейки, за исключением того, что метод получения канарейки путём «чтения из стека» становится несколько сложнее. Атакующий вынужден получить канарейку, алгоритм её получения и управляющие данные, чтобы пересоздать затем канарейку, чтобы обойти защиту.

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

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

Проверка границ

Проверка границ — это техника, основанная на компиляторе, которая добавляет информацию о границах для каждого выделенного блока памяти во время выполнения и проверяет все указатели на соответствие этим границам в процессе работы программы. Для языков C и C++, проверка границ может выполняться на этапе вычисления указателя или на этапе разыменования.

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

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

Исторически тегирование применялось для реализации языков программирования высокого уровня. При соответствующей поддержке со стороны операционной системы тегирование также может использоваться для обнаружения переполнения буфера. Примером является аппаратно реализованный NX бит, поддерживаемый процессорами Intel, AMD и ARM.

Реализации

GNU Compiler Collection (GCC) Защита от атаки stack-smashing была имплементирована расширением StackGuard для GCC в 1997 году и опубликована в 1998 году на симпозиуме по безопасности USENIX. StackGuard была представлена как набор заплат для GCC 2.7 под архитектуру Intel x86. Проект StackGuard поддерживался для дистрибутива Linux с 1998 по 2003 и был расширен для реализации канареек всех трёх типов (terminator, random и random XOR). В материалах саммита GCC 2003 года было предложено включить StackGuard в GCC 3.x, однако это предложение так и не было реализовано.

С 2001 до 2005 компания IBM разрабатывала GCC заплаты для защиты от атак stack-smashing, известные как 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.