Прежде чем мы начнем понимать возвратно-ориентированное программирование, давайте сначала поймем необходимость ориентированного на возврат программирования.

Необходимость программирования, ориентированного на возврат

Давайте сначала начнем с самой простой техники переполнения буфера, используемой при эксплуатации машин.

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

Ванильная перезапись EIP

Чтобы понять концепцию перезаписи EIP, рассмотрим следующий пример:

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

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

Теперь, чтобы понять техническую сторону деталей -

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

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

3. Теперь, когда вы пишете простую программу hello world на C, внутри вашей системы создается стек, и соответствующий ассемблерный код ЗАПИСЫВАЕТСЯ внутрь стека. Стек — это, по сути, структура данных, в которой данные хранятся и извлекаются организованным образом — «первым пришел — первым обслужен» (FIFO).

Обратитесь к этой диаграмме ниже, чтобы понять работу метода перезаписи EIP.

В основной функции после вызова функции do_something() сохраняется указатель возврата, который указывает на следующую функцию/инструкцию в функции main(). Этот указатель содержит адрес следующего оператора в методе main() {do_something_else}. Следовательно, если мы сможем перезаписать адрес функции в сохраненном указателе возврата (путем переполнения буфера foo) в нужное нам место в стеке, т.е. получить контроль над этим указателем — мы сможем направить выполнение куда угодно в стеке, в основном для нашей вредоносной полезной нагрузки. Если вам интересно, как написать вредоносный код на ассемблере, на данный момент мы можем использовать несколько инструментов, которые могут генерировать обратную оболочку (msfvenom или многие другие). Пробуя это в отладчике, мы перезаписываем «EIP» инструкцией перехода. В ассемблере инструкция перехода используется для перехода в любое место стека. Следовательно, следующая инструкция, которая может быть выполнена, — это наш оператор перехода, выполняющий переход EIP к вредоносному коду. Это вредоносный код, который при выполнении будет отвечать за предоставление нам оболочки :)

Смягчение этой техники -

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

В контексте вышеприведенного примера подумайте об этом так: прежде чем уйти встречать посетителя у двери, вы подчеркиваете своеобразный текст на этой странице и уходите, в этом случае, даже если ваш брат достает закладку и поместит его на другую страницу, вы все равно сможете это понять — ок, подождите. Что-то не так — на странице нет этого подчеркнутого слова и прекратите читать книгу дальше. Следовательно, в структуре памяти используется стек cookie, аналогичный отметке слова на странице книги.

Cookie стека был вставлен прямо перед EBP и EIP. При запуске приложения вычисляется и сохраняется основной файл cookie для всей программы (псевдослучайное число). В эпилоге этот файл cookie снова сравнивается с основным файлом cookie для всей программы. Если он отличается, делается вывод, что произошло повреждение, и программа завершается. Это немного усложнило получение контроля над EIP.

SEH — обходной путь

Хотя мы не сможем перезаписать EIP, так как мы не уверены в расположении файла cookie стека в do_something(), существуют различные методы, в которых используются методы перехода, идея заключается в том, что везде, где мы можем найти инструкцию перехода, а не когда программа переходит к своим типичным местам в стеке (куда должен перейти поток программы), мы заставляем ее перейти к нашему вредоносному шелл-коду для использования машины.

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

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

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

Когда мы переполним этот буфер этой строки foo здесь, как и в предыдущем сценарии, эти данные переполнят этот буфер и начнут спускаться по стеку, как мы это делали в предыдущем примере. Но в дополнение к перезаписи всего этого, он перезапишет адрес в структурированном обработчике исключений, который программа будет использовать, чтобы сказать ей перейти к некоторому фрагменту кода. Просто перезаписать это само по себе недостаточно для управления выполнением. Вы должны создать фактическое условие ошибки, чтобы вызвать просмотр одной из этих записей SEH. Однако самый распространенный и простой способ сделать это, конечно, — просто продолжать записывать эти неверные данные до тех пор, пока вы не достигнете конца стека. Что произойдет, так это то, что он сгенерирует исключение, потому что вы попытались записать последний адрес памяти, к которому ему разрешен доступ, создав условие ошибки, которое должно быть немедленно обработано структурированной цепочкой обработчиков исключений.

Когда мы контролируем это значение, нам нужно будет вернуть нас обратно в часть памяти, которую мы контролируем. Как правило, в перезаписи структурированного обработчика исключений вы увидите, что это значение будет указывать на адрес известного набора инструкций, которые обычно называются POP POP RET или POP, POP и Return. Что происходит, если не вдаваться в подробности, так это то, что набор инструкций немного изменит стек и вернет выполнение прямо туда, где находится эта запись nSEH, которую мы также перезаписали. Что происходит, так это то, что мы помещаем здесь определенный адрес к существующему коду, который существует в программе, которая выполняет тот набор инструкций, который нам нужен, который возвращает выполнение здесь. Отсюда, в зависимости от того, сколько места у нас есть выше или ниже, мы перезаписываем это значение набором инструкций, которые сообщают ему о переходе вперед или назад после записи SEH. Затем, при переполнении нашего буфера, мы просто помещаем наш шеллкод полезной нагрузки в эту конкретную область.

Совершенно нормально, если вы этого не поняли — обратитесь к этой схеме, чтобы получить наглядное представление о том, как это работает.

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

Техника смягчения-

SafeSEH

Windows представила механизм защиты SafeSEH, в котором проверенные обработчики исключений регистрируются и хранятся в таблице. Адреса в этой таблице проверяются перед выполнением данного обработчика исключений, чтобы гарантировать, что он считается «безопасным». В результате адрес POP+POP+RET, используемый для перезаписи записи SEH, полученной из модуля, скомпилированного с помощью SafeSEH, не будет отображаться в таблице, и эксплойт SEH завершится ошибкой.

SafeSEH эффективно предотвращает эксплойты на основе SEH, если адрес перезаписи SEH (например, POP+POP+RET) поступает из модуля, скомпилированного с помощью SafeSEH. Хорошая новость (с точки зрения возможности эксплуатации) заключается в том, что модули приложений обычно не компилируются с SafeSEH по умолчанию. Даже если это большинство из них, любой модуль, загруженный приложением, которое не было скомпилировано с SafeSEH, может быть использован для перезаписи SEH. В компиляторы была добавлена ​​дополнительная защита, помогающая предотвратить злоупотребление перезаписью SEH. Этот механизм защиты активен для всех модулей, скомпилированных с параметром /safeSEH.

Защита на уровне ОС

DEP — предотвращение выполнения данных

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

Этот метод основан на идее обработки входных данных в стеке как неисполняемых, т. е. думать таким образом, чтобы стек не имел возможностей для выполнения данных. Мы можем думать об дифференциации данных с кодом, думая, что код — это данные, которые выполняются. Следовательно, как и в случае с правами доступа к файлам в операционной системе Linux — (чтение, запись, выполнение), даже стек должен иметь права на выполнение и неисполнение. Теперь, если в системе включена функция DEP, стек не может выполнять хранящиеся в нем данные. Теперь, даже если нам каким-то образом удастся внедрить наш шелл-код в стек, это не будет иметь значения, пока шелл-код не будет выполнен системой. Это обеспечивается включением чего-то, известного как бит NX (No-Execute). Если это включено — стек становится неисполняемым.

Программирование, ориентированное на возврат

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

Теперь, после рассмотрения метода смягчения — DEP, нашей главной заботой было каким-то образом получить права на выполнение в стеке, кроме того, мы не можем выполнить любой наш произвольный код, который был внедрен в стек. Следовательно, мы пытаемся вызвать один из API-интерфейсов Windows, который может каким-то образом предоставить исполняемые разрешения для стека. API-интерфейсы Windows — это то, как мы взаимодействуем с ядром Windows, эти API-интерфейсы представляют собой функции, написанные для определенных библиотек DLL. Эти библиотеки DLL (динамически связанные библиотеки) вызываются приложением во время работы — отсюда и термин «динамически». Эти библиотеки DLL отвечают за выполнение вызовов API Windows по мере необходимости для приложения. Теперь, чтобы вызвать любой из API, нам нужно сделать так, чтобы EIP указывал на его адрес, это можно сделать, перейдя на его адрес.

Теперь это возможно путем поиска адреса API, который мы хотим вызвать через DLL. Одним из таких доступных Windows API является функция Virtual Protect. Эта функция доступна через kernel32.dll, которая загружается каждый раз при запуске любого приложения в Windows. Кроме того, Linux-эквивалентом этого является Return-to-libc, в котором, аналогичным образом, вызовы функций могут быть сделаны для того, чтобы сделать стек исполняемым.

По сути, это начинает работать так, что у вас есть обычный шелл-код. Но где бы вы ни оказались, где бы вы ни заменили сохраненный указатель возврата или запись SEH, что бы вы ни использовали для управления выполнением, вы перейдете к гаджету ROP. Теперь в разделе перезаписи структурированного обработчика исключений мы кратко рассказали об этой инструкции pop-pop-ret, которая на самом деле представляет собой набор из трех инструкций, это на самом деле гаджет ROP. Это серия инструкций, которая завершается инструкцией ret или return. И что всегда делает инструкция return, так это то, что она возвращается к следующему, она возвращается к любому указателю next в спецификации — посмотрите на рисунок, представленный ниже.

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

В базовой методологии ROP у нас будет набор инструкций, которые всегда будут заканчиваться «возвратом», возвращающим обратно к другому указателю, который будет указывать на другой гаджет ROP. И то, что вы пытаетесь сделать, на самом деле находится в новом виртуальном стеке, вы помещаете все аргументы, а затем указатель на функции Windows, которые вам нужно вызывать в стеке таким образом, что, как только вы закончите свою ROP цепочка, этот окончательный возврат фактически вернется к указателю функции Windows, которую вы хотите вызвать, заставляя ее отправлять все те аргументы, которые вы установили в стеке, в VirtualProtect или VirtualAlloc или ряд других методов, которые просто сообщают ему: « Эй, вот эта секция стека, где была остальная часть нашего шелл-кода, мне нужно, чтобы ты сейчас же пометил весь этот исполняемый файл. И поэтому, когда это происходит внезапно, шелл-код, запуск которого DEP предотвратил бы, теперь может выполняться.

Теперь вам может быть интересно, действительно ли утомительно искать все эти ROP-гаджеты (последовательность инструкций, заканчивающихся на «рет»). Ну, для этого есть бесчисленное множество автоматизированных инструментов. Один из них — mona.py. Это печально известный скрипт, который можно интегрировать с помощью Immunity Debugger. Следовательно, это значительно упрощает задачу.

Примечание. Хотя идея ROP реализована только на машинах с Windows, ее концепция возникла в результате первоначальной атаки — return-to-libc, которая работает на аналогичных концепциях, но на машинах на базе Unix. В отличие от Windows, в Unix- все подобные Гаджеты заканчиваются на «рет».

Методы смягчения -

Идея использования ROP-гаджетов и всей методологии впервые была обнаружена в 2012 году, с тех пор было реализовано несколько методов смягчения последствий. Сначала за обработку таких атак отвечала защита Microsoft EMET, хотя к 31 июля 2018 года срок службы EMET истек. Затем методы смягчения последствий были объединены с Microsoft ATP (Advanced Threat Protection).

Мы, как отрасль, можем использовать различные методы для определения того, используется ли ROP.

1. MemProt или защита памяти. Когда Mem Prot обнаруживает вызов VirtualProtect, он проверяет, как этот вызов выполняется, и пытается определить, является ли это законным использованием VirtualProtect или нет. И если Mem Prot увидит, что это, очевидно, вызывается из какого-то шеллкода в стеке, тогда он просто скажет: «Нет, это запрещено». Так что это один из способов, которым он может это сделать.

2. LoadLibrary: не позволяет запущенной программе выполнить вызов для загрузки библиотеки, одной из этих библиотек DLL, или иногда мы называем их модулями, из общей папки на удаленном сетевом ресурсе.

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

Таким образом, все, что делает LoadLibrary, это говорит: «О, эй, вы вызываете LoadLibrary для файла… для DLL в общей папке или для какого-то сетевого ресурса». Я тоже не позволю тебе сделать это.

3. Проверка вызывающего абонента ROP: это еще одна защита для VirtualProtect, VirtualAlloc, любой из тех функций Windows API, которые позволяют вам изменять разрешения раздела памяти. Итак, он срабатывает, когда, скажем, вызывается VirtualProtect, он говорит: «О, вызывается VirtualProtect. Как мы тут оказались? Остановитесь на секунду и оглянитесь. Мы получили этот вызов VirtualProtect из инструкции возврата в сборке?» Если мы это сделали, очень вероятно, что этот вызов VirtualProtect на самом деле является частью цепочки ROP. И снова мы просто скажем: «Нет, это запрещено». Очевидно, что это ROP, поэтому это незаконный вызов VirtualProtect для изменения этих разрешений памяти.

4. Сводка стека. Таким образом, каждый раз, когда загружается новая функция, создается новый фрейм стека. И способ, которым он это делает, состоит в том, что у него есть указатель на вершину и низ этого… верх и низ этого фрейма стека. А поворот стека — это просто набор инструкций для перемещения указателя на вершину стека, где у нас есть больше места, чтобы при выполнении наших ROP-гаджетов все эти аргументы и эти вызовы функций можно было безопасно разместить. стек, не перезаписывая остальную часть нашего кода в стеке.

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

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

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

использованная литература

  • Рапид7

Также выражаем особую благодарность Риязу Валикару за глубокое понимание и предложения для этого блога.

Не стесняйтесь комментировать ниже или напишите мне в Твиттере.