Возвратно-ориентированное программирование


Возвратно-ориентированное программирование (англ. return oriented programming, ROP) — метод эксплуатации уязвимостей в программном обеспечении, используя который атакующий может выполнить необходимый ему код при наличии в системе защитных технологий, например, технологии, запрещающей исполнение кода с определённых страниц памяти. Метод заключается в том, что атакующий может получить контроль над стеком вызовов, найти в коде последовательности инструкций, выполняющие нужные действия и называемые «гаджетами», выполнить «гаджеты» в нужной последовательности. «Гаджет», обычно, заканчивается инструкцией возврата и располагается в оперативной памяти в существующем коде (в коде программы или в коде разделяемой библиотеки). Атакующий добивается последовательного выполнения гаджетов с помощью инструкций возврата, составляет последовательность гаджетов так, чтобы выполнить желаемые операции. Атака реализуема даже на системах, имеющих механизмы для предотвращения более простых атак.

История

Атака на переполнение буфера

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

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

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

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

При атаке возврата в библиотеку также эксплуатируется переполнение буфера. Адрес возврата перезаписывается точкой входа нужной библиотечной функции. Также перезаписываются ячейки над адресом возврата, чтобы передать функции параметры или связать в цепочку несколько вызовов. Эта техника была впервые представлена Александром Песляком (известным как Solar Designer) в 1997 году, а затем была расширена, позволяя произвести неограниченную цепочку вызовов функций.

Заимствование кусков кода

С распространением 64-разрядного оборудования и операционных систем стало сложнее производить атаку возврата в библиотеку: в используемых в 64-разрядных системах соглашениях о вызове первые параметры передаются функции не в стеке, а в регистрах. Это усложняет подготовку параметров для вызова в процессе атаки. Кроме того, разработчики разделяемых библиотек стали убирать из библиотек или ограничивать «опасные» функции, такие как обёртки системных вызовов.

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

Атака методом возвратно-ориентированного программирования

Возвратно-ориентированное программирование расширяет подход заимствования кусков кода, предоставляя атакующему полную по Тьюрингу функциональность, включая циклы и ветвления. Другими словами, возвратно-ориентированное программирование предоставляет атакующему возможность выполнить любую операцию. Ховав Шахам опубликовал описание метода в 2007 году и продемонстрировал его на программе, использующей стандартную библиотеку языка Си и содержащую уязвимость переполнения буфера. Возвратно-ориентированное программирование превосходит другие описанные выше типы атак и по выразительной мощности, и по устойчивости к защитным мерам. Ни один из вышеописанных методов противодействия атакам, включая удаление опасных функций из разделяемых библиотек, не является эффективным против возвратно-ориентированного программирования.

В отличие от атаки возврата в библиотеку, в которой используются функции целиком, в возвратно-ориентированном программировании используются небольшие последовательности инструкций, заканчивающиеся инструкцией возврата, так называемые «гаджеты». Гаджетами являются, например, окончания существующих функций. Однако на некоторых платформах, в частности x86, гаджеты могут возникать «между строк», то есть при декодировании с середины существующей инструкции. Например, следующая последовательность инструкций:

test edi, 7 ; f7 c7 07 00 00 00 setnz byte[ebp-61] ; 0f 95 45 c3

при начале декодирования на один байт позже, даёт

mov dword[edi], 0f000000h ; c7 07 00 00 00 0f xchg ebp, eax ; 95 inc ebp ; 45 ret ; c3

Также гаджеты могут находиться в данных, по тем или иным причинам располагающихся в секции кода. Это связано с тем, что набор инструкций архитектуры x86 достаточно плотен, то есть велика вероятность того, что произвольный поток байтов будет интерпретирован как поток действительных инструкций. С другой стороны, в архитектуре MIPS все инструкции имеют длину 4 байта, а исполнять можно только инструкции, выровненные по адресам, кратным 4-м байтам. Поэтому там нельзя получить новую последовательность «чтением между строк».

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

Примеры

Расширения

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

Автоматическая генерация

Существуют инструменты для автоматического нахождения гаджетов и конструирования атаки. Примером такого инструмента может служить ROPgadget.

Защита от возвратно-ориентированного программирования

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

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

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