О ROP цепочках слышали многие, кто в той или иной степени связан с реверсингом вредоносных приложений. Между тем, возвратно-ориентированное программирование остается довольно интересным направлением в разработке вредоносных приложений. В этой статье мы начнем разбираться с тем, что из себя представляет этот метод.
Итак, возвратно-ориентированное программирование (return oriented programming, ROP) — это метод эксплуатации уязвимостей в программном обеспечении, используя который атакующий может выполнить необходимый ему код при наличии в системе защитных технологий, например, технологии, запрещающей исполнение кода с определённых страниц памяти.
С помощью ROP атакующий может получить контроль над стеком вызовов, найти в коде последовательности инструкций, выполняющие нужные действия и называемые «гаджетами», выполнить «гаджеты» в нужной последовательности.
Ищем гаджеты
Думаю, теории будет достаточно и далее мы можем на практике посмотреть, о чем идет речь. Пусть у нас есть некоторый набор инструкций (слева). А справа мы можем видеть их опкоды, то есть то, как эти инструкции представлены в виде машинных кодов.
Казалось бы, ничего необычного. Но что произойдет, если мы попробуем начать выполнения кода не с первого байта, а со второго.
Тогда мы получаем уже совсем другой набор инструкций и соответственно совсем другую логику работы кода. Это простейший пример гаджета ROP. Важным элементом любого гаджета является наличие инструкции RET в конце. Именно благодаря RET возможен возврат из гаджета к исходной программе.
Напомню, что после перехода по инструкции CALL возврат к следующей после вызова команде осуществляется, когда при выполнении кода встречается RET.
В реальности такие гаджеты могут находиться в данных, по тем или иным причинам располагающихся в секции кода. Это связано с тем, что набор инструкций архитектуры x86 достаточно плотен, то есть велика вероятность того, что произвольный поток байтов будет интерпретирован как поток действительных инструкций. А это и есть то, что нужно злоумышленнику при написании какого-либо вредоноса.
Рассмотрим пример гаджета в реальной программе. Здесь перед инструкцией RET мы видим некий набор инструкций, выполняющих различные операции с регистрами и содержимым памяти.
Сместились на один байт назад и набор инструкций уже несколько изменился. Возможно, злоумышленнику в его программе требуется выполнить аналогичный набор команд.
Таким образом, задача разработчика вредоноса построить цепочку из вызовов блоков нужных ему инструкций. То есть «программа», написанная с помощью гаджетов будет выглядеть так
CALL 0X111111
CALL 0X222222
CALL 0X333333
....
И с точки зрения средств защиты здесь ничего подозрительного нет, хотя по факту результат работы ROP цепочки может иметь, например, такой вид:
О том, как можно на практике получить подобный набор инструкций мы подробно поговорим в следующей статье, а здесь мы рассмотрим некоторые виды цепочек и их предназначение.
Итак, основная цель ROP цепочек заключается в том, чтобы позволить выполнить нужный набор команд с помощью вызовов по адресам из стека. То есть, наш потенциальный вредонос не будет содержать инструкций, выполняющих каких-то логических действий, обнаруживать которые приучены антивирусы и сетевые анализаторы. По сути основной код вредоноса состоит из вызовов чужого кода, который уже вызывает нужные фрагменты нужного кода.
ROP можно сравнить с вырезанными из газет словами для составления анонимного письма.
Остается только разобраться с тем, какими способами мы можем построить такие ROP цепочки.
Вот наиболее известные разновидности ROP:
Jump-oriented programming
String-oriented programming
Sigreturn-oriented programming
Blind Return Oriented Programming
В этой статье мы подробно рассмотрим первые две разновидности.
Программирование по прыжкам
При использовании Jump-oriented programming (JOP) злоумышленник полностью отказывается от использования стека и ret для обнаружения гаджета и создания цепочки, вместо этого используя не более чем последовательность инструкций косвенного перехода. Поскольку почти все известные методы защиты от ROP зависят от использования им стека или инструкций ret, многие из них неспособны обнаруживать этот новый подход или защищаться от него.
Подобно ROP, строительными блоками JOP по-прежнему являются короткие кодовые последовательности, называемые гаджетами. Однако вместо того, чтобы заканчиваться символом ret, каждый такой гаджет заканчивается косвенным JMP.
Некоторые из этих инструкций jmp намеренно генерируются компилятором. Другие для этого не предназначены, но присутствуют в связи с плотностью инструкций x86. Однако, в отличие от ROP, где гаджет ret может естественным образом вернуть обратно управление на основе содержимого стека, гаджет jmp выполняет однонаправленную передачу потока управления к своей цели, что затрудняет восстановление управления обратно для цепочки выполнения следующего гаджета, ориентированного на переход.
Решением этой проблемы является предложение гаджета нового класса, гаджета-диспетчера. Такой гаджет предназначен для управления потоком управления между различными блоками кода, ориентированными на переход. Более конкретно, если мы рассматриваем другие гаджеты как функциональные гаджеты, выполняющие примитивные операции, этот диспетчерский гаджет специально выбран для определения того, какой следующий гаджет будет вызван. Естественно, диспетчерский гаджет может поддерживать внутреннюю таблицу диспетчеризации, которая явно определяет поток управления функциональными гаджетами. Кроме того, это гарантирует, что завершающая команда jmp в функциональном гаджете всегда будет передавать управление обратно в диспетчерский гаджет. Таким образом, становится возможным вычисление, ориентированное на переход.
Ниже приводится сравнение Return Oriented Programming и Jump Oriented Programming.
На практике реализация JOP может иметь следующий вид.
String-oriented programming
String-oriented programming, SOP основывается на существующей ошибке форматирования строк в приложении и перерастает в любую возможную атаку с внедрением кода или атаку без использования управляющих данных (ROP или JOP). В этом разделе представлены различные строительные блоки, необходимые для настройки SOP, и эти векторы атак соотносятся с механизмами защиты, которые являются стандартом безопасности современных приложений.
Успешная атака перенаправляет управление потока приложения в альтернативное местоположение, которое в противном случае было бы недоступно (т.е. в приложение вводится новый код) или выполняет уже существующий код в другом контексте (т.е. существующий код выполняется с другими - вредоносными - данными).
Среда выполнения должна разрешать перенаправление потока управления в альтернативные местоположения с использованием инструкции передачи потока управления. Инструкции передачи потока управления - это инструкции перехода, косвенные инструкции перехода, условные инструкции перехода, инструкции вызова, инструкции непрямого вызова, инструкции возврата, прерывания и системные вызовы.
Косвенные передачи потока управления (непрямые переходы, косвенные вызовы и инструкции возврата) считывают абсолютный целевой адрес из области данных или регистра. Злоумышленник может перенаправить законную передачу потока косвенного управления, управляя либо ячейкой памяти, либо указанным регистром (в зависимости от кодировки потока косвенного управления передача). Эксплойты либо перезаписывают регистр значением, указанным злоумышленником, либо перезаписывают область данных, содержащую целевой указатель, для достижения первоначального перенаправления потока управления: для инструкций возврата перезаписывается EIP в стеке, для косвенных вызовов либо указатель функции в куче.
Также, эксплойт должен вводить некоторую форму полезной нагрузки в приложение. Атаки с использованием управляющих данных вводят инструкции машинного кода в исполняемую область памяти приложения. Эти инструкции выполняются после первоначального перенаправления потока управления. Атаки на основе данных, такие как ROP или JOP, изменяют структуры данных приложения, разделяемой библиотеки или стандартного загрузчика для выполнения своей вредоносной полезной нагрузки.
Эксплойт успешен только в том случае, если выполняются оба требования.
Атака с SOP использует то, что злоумышленник управляет первым параметром функции семейства printf (все функции, которые принимают строку формата в качестве параметра, например, printf, fprintf, sprintf и vprintf). Семейство printf анализирует аргумент format string для управляющих токенов (вида %T), чтобы определить количество параметров переменной, которые следуют за ним. Токен определяет, как выходные данные на n-й позиции в стеке форматируются в строке. Многие программисты забывают проверять управляемые пользователем строки для этих управляющих токенов и передайте строку непосредственно функции (например, printf(usr str)). Безопасная реализация использовала бы статический параметр для передачи одной строки
(например, printf("%s", usr str)).
Вредоносная строка формата может использовать токены типа %p для чтения определенных указателей в стеке и %s для чтения определенных адресов стека в виде строк. Злоумышленник использует эти параметры для получения информации о приложении во время построения атаки с использованием строки формата.
Токен %n изменяет порядок ввода на противоположный и записывает количество уже напечатанных символов для указанного указателя.
Любой аргумент в стеке может быть использован в качестве целевого адреса для %n, например, %4$hn записывает 2 байта в указанный указатель 4∗4 = на 16 байт больше в стеке. Строка формата сама по себе может использоваться для хранения указателей на определенные адреса, если она помещена в стек. Количеством записанных байт можно управлять с помощью дополнительных параметров (например, printf("%NNc"); печатает NN байт) и увеличивает счетчик, используемый для %n. Например, printf("AAAA%1$49391c%6$hn") записывает 0xc0f3 (2 байта, 0xc0f3 − 4 = 49391) в 0x41414141, если строка сам по себе находится на 6-м слоте в стеке. В этом примере входная строка длиной 18 байт используется для генерации контролируемой злоумышленником записи в память объемом 2 байта. Атаки с форматированием строк записывают произвольные значения в произвольные ячейки памяти.
Эти контролируемые злоумышленником записи в память используются, например, для перенаправления потока управления на введенный код.
Ниже приведен пример программы, уязвимой к SOP.
void foo ( char ∗ a r g ) {
char text [1024] ;
if ( strlen ( arg ) >= 1024 ) return;
strcpy( text , arg ) ;
printf ( text ) ;
puts ( ”logged in ” ) ;
}
. . .
foo ( user_str ) ;
. . .
Заключение
В этой статье мы начали рассматривать возвратно-ориентированное программирование. Конечно, современные средства защиты умеют эффективно выявлять вредоносы, построенные по подобному принципу, однако сама концепция является довольно интересной, и в следующей статье мы продолжим рассмотрение ROP.
Пользуясь случаем, напоминаю про открытый урок «Введение в анализ эксплуатаций: как найти уязвимости», который пройдет 18 октября. На данном открытом уроке будет рассмотрено на примере one-day уязвимости, как можно ее найти и проэксплуатровать.
Урок пройдет в рамках курса "Reverse engineering", записаться можно по ссылке.
Комментарии (3)
ne-nark
18.09.2024 15:37Исключения удобно обрабатывать с помощью перезаписи адреса возврата, полезная фишка для написания безопасного кода.
d00m911
"С помощью ROP атакующий может получить контроль над стеком вызовов, найти в коде последовательности инструкций, выполняющие нужные действия и называемые «гаджетами», выполнить «гаджеты» в нужной последовательности"
С помощью ROP? Я думал, это как раз для реализации эксплуатации на базе ROP, а не с помощью.
notwithstanding
Текст или криво переведен, или сгенерирован. В любом случае, перед публикацией его даже не вычитали, судя по количеству ошибок.