В программном обеспечении переполнение буфера стека или переполнение буфера стека происходит, когда программа записывает данные по адресу памяти в стеке вызовов программы за пределами предполагаемой структуры данных, которая обычно является буфером фиксированной длины . [1] [2] Ошибки переполнения буфера стека возникают, когда программа записывает больше данных в буфер, расположенный в стеке, чем фактически выделено для этого буфера. Это почти всегда приводит к повреждению смежных данных в стеке, а в случаях, когда переполнение было вызвано ошибкой, часто приводит к сбою программы или ее неправильной работе. Переполнение буфера стека — это тип более общей программной ошибки, известной как переполнение буфера (или переполнение буфера). [1] Переполнение буфера в стеке с большей вероятностью сорвет выполнение программы, чем переполнение буфера в куче, поскольку стек содержит адреса возврата для всех активных вызовов функций.
Переполнение буфера стека может быть вызвано преднамеренно как часть атаки, известной как разбивание стека . Если затронутая программа работает с особыми привилегиями или принимает данные от ненадежных сетевых хостов (например, веб-сервера ), то ошибка является потенциальной уязвимостью безопасности . Если буфер стека заполнен данными, предоставленными ненадежным пользователем, то этот пользователь может повредить стек таким образом, чтобы внедрить исполняемый код в работающую программу и получить контроль над процессом. Это один из старейших и наиболее надежных методов, с помощью которых злоумышленники могут получить несанкционированный доступ к компьютеру. [3] [4] [5]
Канонический метод эксплуатации переполнения буфера на основе стека заключается в перезаписи адреса возврата функции указателем на контролируемые злоумышленником данные (обычно на самом стеке). [3] [6] Это проиллюстрировано strcpy()
в следующем примере:
#include <строка.h> void foo ( char * bar ) { char c [ 12 ]; strcpy ( c , bar ); // проверка границ отсутствует } int main ( int argc , char ** argv ) { foo ( argv [ 1 ]); return 0 ; }
Этот код берет аргумент из командной строки и копирует его в локальную переменную стека c
. Это отлично работает для аргументов командной строки длиной менее 12 символов (как показано на рисунке B ниже). Любые аргументы длиной более 11 символов приведут к повреждению стека. (Максимальное количество символов, которое безопасно, на один меньше размера буфера, поскольку в языке программирования C строки завершаются символом нулевого байта. Таким образом, для хранения двенадцатисимвольного ввода требуется тринадцать байтов, за вводом следует контрольный нулевой байт. Затем нулевой байт перезаписывает область памяти, которая находится на один байт дальше конца буфера.)
Программа работает foo()
с различными входными данными:
На рисунке C выше, когда аргумент больше 11 байтов предоставляется в командной строке, foo()
он перезаписывает локальные данные стека, сохраненный указатель кадра и, что самое важное, адрес возврата. При foo()
возврате он извлекает адрес возврата из стека и переходит по этому адресу (т.е. начинает выполнять инструкции с этого адреса). Таким образом, злоумышленник перезаписал адрес возврата указателем на буфер стека char c[12]
, который теперь содержит предоставленные злоумышленником данные. В реальном эксплойте переполнения буфера стека строка «A» вместо этого будет шелл-кодом, подходящим для платформы и желаемой функции. Если бы эта программа имела особые привилегии (например, бит SUID, установленный для запуска от имени суперпользователя ), то злоумышленник мог бы использовать эту уязвимость, чтобы получить привилегии суперпользователя на уязвимой машине. [3]
Атакующий также может изменять внутренние значения переменных, чтобы использовать некоторые ошибки. В этом примере:
#include <string.h> #include <stdio.h> void foo ( char * bar ) { float My_Float = 10.5 ; // Адрес = 0x0023FF4C char c [ 28 ]; // Адрес = 0x0023FF30 // Выведет 10.500000 printf ( "My Float value = %f \n " , My_Float ); /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Карта памяти: @ : c выделенная память # : My_Float выделенная память *c *My_Float 0x0023FF30 0x0023FF4C | | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@##### foo("моя строка слишком длинная !!!!! XXXXX"); memcpy поместит 0x1010C042 (прямой порядок байтов) в значение My_Float. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ memcpy ( c , bar , strlen ( bar )); // проверка границ не производится... // Выведет 96.031372 printf ( "Мое значение Float = %f \n " , My_Float ); } int main ( int argc , char ** argv ) { foo ( "моя строка слишком длинная !!!!! \x10\x10\xc0\x42 " ); return 0 ; }
Обычно для изменения сохраненного адреса в стеке используются два метода — прямой и косвенный. Злоумышленники начали разрабатывать косвенные атаки, которые имеют меньше зависимостей, чтобы обойти меры защиты, которые были приняты для сокращения прямых атак. [7]
Ряд платформ имеют тонкие различия в реализации стека вызовов, которые могут повлиять на то, как будет работать эксплойт переполнения буфера стека. Некоторые архитектуры машин хранят верхний адрес возврата стека вызовов в регистре. Это означает, что любой перезаписанный адрес возврата не будет использоваться до более поздней раскрутки стека вызовов. Другим примером специфической для машины детали, которая может повлиять на выбор методов эксплуатации, является тот факт, что большинство архитектур машин в стиле RISC не допускают невыровненный доступ к памяти. [8] В сочетании с фиксированной длиной машинных кодов операций это ограничение машины может сделать технику перехода к стеку практически невозможной для реализации (за единственным исключением, когда программа фактически содержит маловероятный код для явного перехода к регистру стека). [9] [10]
В рамках темы переполнений буфера стека часто обсуждаемой, но редко встречающейся архитектурой является та, в которой стек растет в противоположном направлении. Это изменение архитектуры часто предлагается как решение проблемы переполнения буфера стека, поскольку любое переполнение буфера стека, которое происходит в том же стековом кадре, не может перезаписать указатель возврата. Однако любое переполнение, которое происходит в буфере из предыдущего стекового кадра, все равно перезапишет указатель возврата и позволит злонамеренно эксплуатировать ошибку. [11] Например, в приведенном выше примере указатель возврата для foo
не будет перезаписан, поскольку переполнение фактически происходит в стековом кадре для memcpy
. Однако, поскольку буфер, который переполняется во время вызова , memcpy
находится в предыдущем стековом кадре, указатель возврата для memcpy
будет иметь численно более высокий адрес памяти, чем буфер. Это означает, что вместо foo
перезаписываемого указателя возврата для memcpy
будет перезаписан указатель возврата для . В лучшем случае это означает, что рост стека в противоположном направлении изменит некоторые детали того, как переполнение буфера стека может быть использовано, но это не приведет к значительному сокращению количества уязвимостей, которые могут быть использованы. [ необходима цитата ]
За прошедшие годы было разработано несколько схем целостности потока управления для предотвращения вредоносной эксплуатации переполнения буфера стека. Обычно их можно разделить на три категории:
Канарейки стека, названные по аналогии с канарейкой в угольной шахте , используются для обнаружения переполнения буфера стека до того, как может произойти выполнение вредоносного кода. Этот метод работает путем помещения небольшого целого числа, значение которого случайно выбирается при запуске программы, в память непосредственно перед указателем возврата стека. Большинство переполнений буфера перезаписывают память с нижних адресов памяти на верхние, поэтому для того, чтобы перезаписать указатель возврата (и, таким образом, взять под контроль процесс), значение канарейки также должно быть перезаписано. Это значение проверяется, чтобы убедиться, что оно не изменилось, прежде чем процедура использует указатель возврата в стеке. [2] Этот метод может значительно повысить сложность эксплуатации переполнения буфера стека, поскольку он заставляет злоумышленника получить контроль над указателем инструкций некоторыми нетрадиционными способами, такими как повреждение других важных переменных в стеке. [2]
Другой подход к предотвращению эксплуатации переполнения буфера стека заключается в применении политики памяти в области памяти стека, которая запрещает выполнение из стека ( W^X , "Write XOR Execute"). Это означает, что для выполнения шелл-кода из стека злоумышленник должен либо найти способ отключить защиту выполнения из памяти, либо найти способ поместить полезную нагрузку шелл-кода в незащищенную область памяти. Этот метод становится все более популярным теперь, когда аппаратная поддержка флага no-execute доступна в большинстве процессоров для настольных ПК.
Хотя этот метод предотвращает канонический эксплойт, разрушающий стек, переполнение стека может эксплуатироваться другими способами. Во-первых, обычно находят способы хранить шелл-код в незащищенных областях памяти, таких как куча, и поэтому очень мало нужно менять в способе эксплуатации. [12]
Другая атака — так называемый метод возврата к libc для создания шелл-кода. В этой атаке вредоносная полезная нагрузка загрузит стек не шелл-кодом, а надлежащим стеком вызовов, так что выполнение будет направлено на цепочку вызовов стандартной библиотеки, обычно с эффектом отключения защиты выполнения памяти и разрешения шелл-коду работать как обычно. [13] Это работает, потому что выполнение на самом деле никогда не направляется на сам стек.
Вариантом return-to-libc является возвратно-ориентированное программирование (ROP), которое устанавливает ряд адресов возврата, каждый из которых выполняет небольшую последовательность тщательно отобранных машинных инструкций в существующем программном коде или системных библиотеках, последовательность, которая заканчивается возвратом. Эти так называемые гаджеты выполняют некоторые простые манипуляции с регистрами или аналогичное выполнение перед возвратом, и их объединение достигает целей злоумышленника. Можно даже использовать "возвратно-ориентированное" возвратно-ориентированное программирование, эксплуатируя инструкции или группы инструкций, которые ведут себя во многом как инструкция возврата. [14]
Вместо того чтобы отделять код от данных, другой метод смягчения заключается в том, чтобы ввести рандомизацию в пространство памяти исполняемой программы. Поскольку злоумышленнику необходимо определить, где находится исполняемый код, который может быть использован, либо предоставляется исполняемая полезная нагрузка (с исполняемым стеком), либо она создается с использованием повторного использования кода, например, в ret2libc или возвратно-ориентированном программировании (ROP). Рандомизация структуры памяти, как концепция, не позволит злоумышленнику узнать, где находится какой-либо код. Однако реализации, как правило, не будут рандомизировать все; обычно сам исполняемый файл загружается по фиксированному адресу, и, следовательно, даже когда ASLR (рандомизация структуры адресного пространства) сочетается с неисполняемым стеком, злоумышленник может использовать эту фиксированную область памяти. Поэтому все программы должны быть скомпилированы с PIE (позиционно-независимые исполняемые файлы), так что даже эта область памяти будет рандомизирована. Энтропия рандомизации различается от реализации к реализации, и достаточно низкая энтропия сама по себе может стать проблемой с точки зрения перебора рандомизированного пространства памяти.
Предыдущие меры по смягчению усложняют этапы эксплуатации. Но все еще возможно эксплуатировать переполнение буфера стека, если присутствуют некоторые уязвимости или если выполнены некоторые условия. [15]
Злоумышленник может использовать уязвимость форматной строки для раскрытия местоположений памяти в уязвимой программе. [16]
Когда включено предотвращение выполнения данных , чтобы запретить любой доступ к стеку для выполнения, злоумышленник все равно может использовать перезаписанный адрес возврата (указатель инструкций), чтобы указать на данные в сегменте кода ( .text в Linux) или любом другом исполняемом разделе программы. Цель состоит в том, чтобы повторно использовать существующий код. [17]
Заключается в перезаписи указателя возврата немного перед инструкцией возврата (ret в x86) программы. Инструкции между новым указателем возврата и инструкцией возврата будут выполнены, а инструкция возврата вернет полезную нагрузку, контролируемую эксплуататором. [17] [ необходимо разъяснение ]
Программирование, ориентированное на переходы, — это метод, который использует инструкции перехода для повторного использования кода вместо инструкции возврата. [18]
Ограничением реализации ASLR на 64-битных системах является то, что она уязвима для атак раскрытия памяти и утечки информации. Злоумышленник может запустить ROP, раскрыв один адрес функции, используя атаку утечки информации. В следующем разделе описывается аналогичная существующая стратегия для взлома защиты ASLR. [19]
{{cite web}}
: CS1 maint: числовые имена: список авторов ( ссылка )