Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Skabtsov_N_V_-_Audit_bezopasnosti_informatsionnykh_sistem_-_2018.pdf
Скачиваний:
101
Добавлен:
24.01.2021
Размер:
9 Mб
Скачать

198    Глава 15  •  Переполнение буфера

На рис. 15.7 изображен буфер, в который помещен эксплойт для его переполнения. Используя этот подход, мы увеличиваем вероятность того, что начальный адрес перезапишет адрес возврата. Более того, теперь нам не обязательно перенаправлять выполнение именно на начало шелл-кода, будет достаточно и того, что АЛУ перей­ дет на любую из NOP-инструкций. Когда АЛУ попадет на NOP-инструкции, оно просто будет пропускать их одну за другой до тех пор, пока не дойдет до шелл-кода и не выполнит его.

Третье условие может быть выполнено заменой нуля на символ с кодом 90h, так как машинная инструкция с кодом 90h — это NOP.

Еще один способ избежать нулевого байта — маскирование части шелл-кода. Можно заменить все 0 на 1, а в шелл-код встроить функцию, которая преобразует их обратно.

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

Перезапись указателя фрейма

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

Другой распространенной ошибкой при программировании на языке С является так называемая «off-by-one error», или ошибка на единицу. Чаще всего это происходит в цикле, выполняющем перебор элементов. Например, перебор элементов массива начинается с 1, а не с 0.

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

Представим себе ситуацию, в которой первая локальная переменная во фрейме стека является буфером, уязвимым к атакам типа «off-by-one error» во время обработки пользовательских данных. В случае, когда между данной переменной и указателем фрейма нет других данных (см. рис. 15.4), введенный дополнительный байт может переписать один байт сохраненного указателем фрейма (Х на рис. 15.4).

Хорошая новость состоит в том, что из-за сохраненного адреса возврата (V на рис. 15.4) у злоумышленника не будет возможности выполнить шелл-код, который он мог заранее загрузить в буфер.

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

Атаки, направленные на переполнение буфера    199

что в архитектуре х86 память организована таким образом, что наиболее важным является байт с наименьшим адресом «little endian». Это означает, что из четырех битов, которые составляют слово, первым идет бит, имеющий наименьшую важность, или имеющий самый низкий адрес. Если провести аналогию с десятичной системой, то получится, что числа будут записаны в обратном порядке, например 1234 будет сохранен таким образом, что 4 будет иметь низший адрес в памяти, а 1 — высший.

Па а

А

а а

С а

а

а а

а

Л а •

Па а

А

а а

С а

а

а а

а

Л а •

Д • • ‹ а

func()_good

func()_bad

Па а

А

а а

С а

а

а а

а

func()_bad

А

а а а

«За а»

Ш-

П • • ‹ а

Рис. 15.8. Перезапись указателя фрейма

Теперь предположим, что переполнение происходит в функции bad_func(), которая вызывается функцией good_func(). В ходе атаки злоумышленник сможет изменить низший бит — в нашем примере цифру 4 — сохраненного указателя фрейма функции good_func(). Почему это так важно? Представьте, что порядок битов в памяти будет другим, «big endian». В таком случае любое изменение значения указателя

200    Глава 15  •  Переполнение буфера

фрейма привело бы к тому, что он указал бы на адрес, который находится вне текущего контекста исполнения программы. Например, 1234 поменялось бы на 3234. Но в нашем случае мы меняем низший байт, например 1234 на 1232. И в данном случае у злоумышленника есть все шансы поменять адрес указателя фрейма на такой, который привел бы к выполнению загруженного шелл-кода.

Как уже было сказано, указатель фрейма используется для доступа к параметрам и локальным переменным функции. Первым результатом изменения указателя фрейма функции good_func() будет ее работа с неправильными данными и, как следствие, возвращение неправильного результата. В лучшем случае после того, как функция попытается обратиться к ячейке памяти, находящейся вне допустимого ранее выделенного интервала, программа будет экстренно завершена. При худшем развитии событий будет достигнут эпилог функции good_func(). Как говорилось ранее, на третьем шаге эпилога будет выполнена команда pop, при помощи которой в указатель инструкции будет скопировано значение, находящееся за указателем фрейма.

Итак, единственное, что нужно сделать для выполнения шелл-кода, это изменить сохраненное значение указателя фрейма функции good_func() на такое, которое указывало бы на адресное пространство в области памяти, находящейся на одно слово ниже. Где, собственно, и будет находиться начальная часть шелл-кода.

Атака возврата в библиотеку

Для рассмотрения атак возврата в библиотеку (англ. Return-into-libc) вновь обратимся к рис. 15.2, на котором изображен стек перед вызовом my_func(). Если рассмотреть ситуацию с точки зрения вызываемой функции, то сначала стек будет содержать адрес возврата, который она должна использовать, а уже затем — параметры. Как мы упоминали в посвященном разбиению стека разделе, основной целью атакующего является запуск шелл-кода для получения доступа к системе. Обычно подобные шелл-коды используют в Unix-подобных системах такие функции, как system() или execve(), а в системах под управлением ОС Windows — WinExec(). В качестве параметра вызова данные функции используют имя и/или путь программы.

Как альтернативу разбиению стека используют прямой вызов определенной функции. Данный способ имеет одно большое преимущество — его нельзя предотвратить, используя защищенный от исполнения стек.

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

На рис. 15.9 показано состояние буфера и регистров после проведения атаки возврата в библиотеку, после второго шага эпилога. Нормальное состояние стека на аналогичном шаге показано на рис. 15.5. Начальный адрес целевой функции

Атаки, направленные на переполнение буфера    201

X

 

 

 

 

Р ••

 

 

 

 

 

 

 

 

 

 

 

 

 

IP

 

R

 

 

 

 

 

 

 

 

 

 

 

 

 

SP

 

W

 

 

 

 

 

 

 

 

Y

 

 

 

BP

 

P

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

а а1

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

а а n

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

« а а»

 

 

 

 

 

 

 

 

 

 

 

 

 

 

W Q=&libfunc()

 

 

SP

 

 

 

 

 

 

W’ P

Рис. 15.9. Атака возврата в библиотеку

libfunc(), Q, будет занесен в указатель инструкции на третьем шаге эпилога оператором РОР по адресу R. Если данную ситуацию сравнить с изображенной на рис. 15.5, то можно заметить, что параметры для вызова libfunc() сдвинуты вверх на одно слово, — это необходимо для установки «заглушки». Интересно, что поддельное значение указателя фрейма Р будет снова занесено в стек после выполнения пролога функции libfunc(). В зависимости от ОС, архитектуры и целевой функции процесс нахождения адреса нужной функции может быть затруднен, но, безусловно, это вполне выполнимая задача.

Переполнение динамической области памяти

Возвращаясь к главе 6, вспомним, что при помощи функции malloc() программа может запрашивать необходимое количество памяти в динамической области (heap), а позже, после исполнения, вернуть ОС управление над данной областью при помощи функции free().

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