Устранение гаджетов: противодействие возвратно-ориентированному программированию путем рандомизации кода на месте
" ); wnd.document.close(); wnd.focus(); }
В данном документе мы представляем рандомизацию кода на месте – практическую технику противодействия ВОП-атакам, которая может напрямую применяться к сторонним приложениям.
Авторы: Vasilis Pappas, Michalis Polychronakis, Angelos D. Keromytis
Краткий обзор
Широкое внедрение защиты, базирующейся на неисполняемых страницах, в недавних версиях популярных операционных систем привело к увеличению количества атак, использующих возвратно-ориентированное программирование (ВОП) для выполнения произвольного кода без его внедрения. Существующие средства защиты от ВОП-эксплоитов либо требуют исходный код или символическую отладочную информацию, либо вносят большие издержки во время выполнения, что ограничивает их применимость для защиты сторонних приложений.
В данном документе мы представляем рандомизацию кода на месте – практическую технику противодействия ВОП-атакам, которая может напрямую применяться к сторонним приложениям. Наш метод использует различные сильно локализованные трансформации кода, которые можно применять статически, не меняя положение базовых блоков. Это позволяет проводить безопасную рандомизацию даже частично дизассемблируемых двоичных файлов без отладочной информации. Данные трансформации эффективно устраняют около 10% и вероятностно разрушают около 80% полезных последовательностей инструкций для большого набора PE-файлов. Поскольку дополнительный код не внедряется, рандомизация на месте не вносит каких-либо значимых издержек выполнения, что позволяет с легкостью использовать ее совместно с существующими методами борьбы с эксплоитами вроде рандомизации адресного пространства. Проведенная нами оценка на основе свободно доступных ВОП-эксплоитов и двух наборов инструментов для генерации ВОП-кода демонстрирует, что наш метод предотвращает применение существующих эксплоитов к протестированным приложениям для Windows 7, включая Adobe Reader. Он также предотвращает автоматическое построение альтернативных полезных нагрузок ВОП, целью которых является обход рандомизации кода на месте путем использования лишь тех последовательностей инструкций, которые не были затронуты в ходе рандомизации.
I. Введение
Технологии предотвращения атак на основе атрибута страниц памяти No eXecute (NX), которые предотвращают запуск внедренного в процесс вредоносного кода, сейчас поддерживаются большинством процессоров и операционных систем [1]. Широкое внедрение данных защитных механизмов привело к распространению новой техники эксплоитов, широко известной как возвратно-ориентированное программирование (ВОП) [2], которая позволяет атакующему обходить запрет на выполнение страниц без внедрения кода. Используя ВОП, атакующий может скомпоновать небольшие фрагменты кода, называемые гаджетами, которые уже существуют в образе процесса уязвимого приложения. Каждый гаджет заканчивается инструкцией косвенной передачи управления, которая передает управление следующему гаджету, в соответствии с последовательностью адресов гаджетов, внедренной в стек или другую область памяти. По существу, вместо внедрения двоичного кода атакующий внедряет лишь данные, включающие адреса выполняемых гаджетов и необходимые аргументы.
Несколько исследовательских работ продемонстрировали огромный потенциал данной техники для обхода средств защиты вроде доступной только для чтения памяти [3], механизмов защиты целостности кода ядра [4], и реализаций неисполняемой памяти в мобильных устройствах [5] и операционных системах [6]-[9]. Поэтому применение ВОП в реальных атаках было только вопросом времени. Недавние эксплоиты популярных приложений использовали ВОП-код для обхода противодействия эксплоитам даже в свежих версиях ОС, включая Windows 7 SP1. ВОП-эксплоиты включены в наиболее распространенные наборы эксплоитов [10], [11], и активно используются для проведения атак типа drive-by download.
Атакующий гарантировано может подобрать нужные куски кода, поскольку части образов кода уязвимых приложений остаются неизменными в различных инсталляциях. Рандомизация адресного пространства (ASLR) [1] предназначена для предотвращения такого повторного использования кода путем рандомизации положения исполняемых сегментов при запуске процесса. Однако, как в Linux, так и в Windows, некоторые части адресного пространства не меняются из-за приложений с фиксированным адресом загрузки [12] или разделяемых библиотек, несовместимых с ASLR [6]. Более того, в некоторых эксплоитах базовый адрес DLL можно вычислить динамически через утечку указателя [9], [13] или перебором [14].
Другие дополняющие ASLR защиты от атак, повторно использующих код, включают расширения компилятора [15], [16], рандомизацию кода [17]–[19], проверку целостности потока управления [20] и динамические решения, работающие при выполнении [21]-[23]. На практике, однако, большинство из этих подходов почти никогда не применяются для защиты COTS-программ, являющихся сейчас мишенью ВОП-атак, либо из-за нехватки исходного кода или отладочной информации, либо из-за издержек выполнения. В частности, вышеописанные техники, которые оперируют напрямую со скомпилированными двоичными файлами, например, путем перестановки порядка функций [18], [19] или двоичного инструментирования [20], требуют точного и полного извлечения всего кода и данных из исполняемых секций двоичного файла. Это возможно, только если доступна соответствующая символическая отладочная информация, которая, однако, обычно удаляется из двоичных файлов, предназначенных для конечного использования. С другой стороны, методы, которые работают с двоичными файлами без отладочной информации с помощью инструментирования динамических библиотек [21]–[23], вносят значительные издержки выполнения, которые ограничивают их применение. В то же время, рандомизация системы команд (ISR) [24], [25] не может предотвратить атаки с повторным использованием кода, и текущие реализации также полагаются на тяжеловесное инструментирование во время выполнения или фреймворки эмуляции кода.
Задавшись целью найти практическое средство противодействия ВОП-атакам, ставшим столь популярными, в данном документе мы представляем новый метод рандомизации кода, который может препятствовать применению ВОП в сторонних приложениях. Наш подход основан на сильно локализованных изменениях сегментов кода исполняемых файлов с помощью массива техник преобразований кода, которые мы в совокупности будем называть рандомизацией кода на месте. Данные преобразования применяются статично, консервативным образом и изменяют лишь код, который может быть надежно извлечен из скомпилированных двоичных файлов, не полагаясь на символическую отладочную информацию. Сохраняя длину инструкций и базовых блоков, данные модификации не нарушают семантику кода и позволяют проводить рандомизацию даже частично дизассемблируемых двоичных файлов без отладочной информации. Целью данного процесса рандомизации является устранение или вероятностная модификация как можно большего количества гаджетов из адресного пространства уязвимого процесса. Поскольку ВОП-код полагается на корректный запуск всех гаджетов из заданной цепочки, изменение даже небольшого их числа вероятно сделает ВОП-код неэффективным.
Наш анализ с использованием реальных ВОП-эксплоитов для различных широко используемых приложений вроде Adobe Reader показал эффективность и практичность нашего подхода, поскольку во всех случаях рандомизированные версии приложений смогли противостоять эскплоитам. Два инструмента автоматического построения полезных нагрузок для ВОП-эксплоитов, Q [26] и Mona [27], при попытке обойти примененную рандомизацию кода не смогли сгенерировать функциональный код эксплоита на основе гаджетов, оставшихся нерандомизированными.
Хотя рандомизация кода на месте сама по себе является достаточно эффективным средством защиты, это не решение, обеспечивающее полную защиту от ВОП-эксплоитов: она предлагает лишь вероятностную защиту и, потому, не может ничего гарантировать. Однако, она может применяться совместно с любыми существующими методами рандомизации для введения в процесс большего многообразия. Этому способствуют почти нулевые издержки применения трансформаций и простота применения к существующим сторонним приложениям.
Вот основные результаты нашей работы:
- Мы представляем рандомизацию кода на месте, новый и практичный подход для защиты сторонних приложений от ВОП-атак. Мы детально описываем разнообразные сильно локализованные трансформации кода, которые не меняют семантику существующего кода и которые можно надежно применить к скомпилированным двоичным файлам без символической отладочной информации.
- Мы реализовали рандомизацию кода на месте для исполняемых файлов формата x86 PE и экспериментально проверили надежность примененных преобразований кода для сторонних приложений с помощью исчерпывающих тестов.
- Мы проводим подробный анализ того, как рандомизация кода на месте влияет на имеющиеся гаджеты, используя большой набор из 5,235 PE-файлов. В среднем примененные преобразования устраняют около 10% и вероятностно разрушают около 80% гаджетов в тестовых файлах.
- Мы оцениваем наш подход с помощью свободно доступных ВОП-эксплоитов и типичных ВОП-начинок, а также двух инструментов построения ВОП-начинок. Во всех случаях рандомизированные версии исполняемых файлов нарушают вредоносный ВОП-код и предотвращают автоматизированное построение альтернативных полезных нагрузок из гаджетов, оставшихся нетронутыми.
II Предварительные сведения
Появление неисполняемых страниц памяти привело к разработке метода эксплуатации уязвимостей return-to-libc [28]. С помощью этого метода уязвимость к повреждению памяти может быть эксплуатирована путем передачи управления коду, который уже существует в адресном пространстве уязвимого процесса. Путем перехода на начало библиотечной функции вроде system() атакующий может, например, породить командную оболочку без необходимости внедрения какого-либо кода. Однако, часто, особенно в случае удаленных эксплоитов, вызова одной единственной функции недостаточно. В этих случаях множественные вызовы return-to-libc могут соединяться в цепочку путем предварительного возврата в короткую последовательность инструкций вроде pop reg; pop reg; ret;
[29], [30]. Когда нужно передать аргументы через регистры, несколько коротких последовательностей инструкций, завершающихся инструкцией ret, могут быть напрямую связаны в цепочку, чтобы установить желаемые значения регистров перед вызовом библиотечной функции [31].
В описанных методах повторного использования кода исполняемый код состоит из одной или нескольких коротких последовательностей инструкций, за которыми следует большой блок кода, принадлежащий библиотечной функции. Hovav Shacham продемонстрировал, что, используя только тщательно выбранный набор коротких последовательностей инструкций, завершающихся инструкцией ret (называемых гаджетами), возможно добиться запуска произвольного кода, что устраняет необходимость вызова библиотечных функций [2]. Эта мощная техника, названная возвратно-ориентированным программированием, по существу дает атакующему без внедрения какого-либо кода тот же уровень гибкости, что и внедрение произвольного кода – внедренная полезная нагрузка включает в себя лишь последовательность адресов гаджетов, чередующихся с необходимыми агрументами.
В типичном ВОП-эксплоите атакующему нужно контролировать как программный счетчик, так и указатель стека: первый для запуска первого гаджета, а последний, чтобы инструкция ret могла передавать управление последующим гаджетам. В зависимости от уязвимости, если ВОП-начинка внедряется не в стек, тогда указатель стека сначала должен быть выравнен на начало начинки через так называемый stack pivot [6], [32]. В своей работе Checkoway и пр. продемонстрировали, что гаджеты, используемые в ВОП-эксплоитах, не обязательно должны заканчиваться инструкцией ret, они могут заканчиваться любой другой инструкцией косвенной передачи управления.
ВОП-код, используемый в недавних эксплоитах Windows-приложений, как правило, основан на гаджетах, заканчивающихся инструкцией ret, что удобно для манипуляции как программным счетчиком, так и указателем стека, хотя пара гаджетов, заканчивающихся на call или jmp, также может использоваться для вызова библиотечных функций. До сих пор во всех свободно доступных Windows-эксплоитах атакующим не приходилось полагаться на реализацию вредоносного кода, полностью основанную на ВОП. Вместо этого, ВОП-код использовался только как первый этап для обхода DEP [1]. Обычно после захвата потока управления ВОП-код выделяет область памяти с разрешениями на запись и выполнение, путем вызова библиотечной функции вроде VirtualAlloc, копирует в нее некоторый шеллкод, включающий вектор атаки, и в итоге переходит на скопированный шеллкод, который уже имеет право на запуск [32].
III. Подход
Наш подход основан на рандомизации секций кода двоичных исполняемых файлов, которые являются частью сторонних приложений, с помощью массива методов преобразования двоичного кода. Целью процесса рандомизации является нарушение семантики гаджетов, которые присутствуют в исполняемых сегментах памяти запущенного процесса, без изменения семантики кода самой программы.
Выполнение гаджета имеет определенный набор последствий для состояния процессора и памяти атакуемого процесса. Атакующий выбирает, как соединить различные гаджеты на основании того, какие регистры, флаги или участки памяти изменяет каждый из гаджетов, и каким образом он это делает. Поэтому выполнение последующих гаджетов зависит от исхода выполнения всех предыдущих гаджетов. Даже если результат выполнения одного единственного гаджета оказался не тем, который ожидал атакующий, это повлияет на выполнение всех последующих гаджетов и, вероятно, сильно скажется на логике вредоносного возвратно-ориентированного кода.
A. Почему на месте?
Концепция диверсификации программ [34] является основой для широкого круга средств защиты от эксплуатации уязвимостей к повреждению памяти. Помимо рандомизации адресного пространства (ASLR) [1], многие техники сосредоточены на внутренней рандомизации сегментов кода исполняемого файла и могут сочетаться с ASLR для увеличения диверсификации процесса [17]. Метаморфные преобразования [35] могут сдвинуть гаджеты с их исходных смещений и изменить множество их инструкций, сделав их бесполезными. Другой более простой и возможно более эффективный подход состоит в переупорядочивании существующих блоков кода либо на уровне функций [18], [19], [36], [37], или на более низком уровне базовых блоков [38], [39]. Если все блоки кода переупорядочены так, что ни один не располагается на исходной позиции, тогда все прежние смещения гаджетов в секциях кода процесса, на которые полагался атакующий, теперь будут соответствовать совершенно другому коду.
Данные преобразования требуют точного представления всех объектов кода и данных, содержащихся в исполняемых секциях PE-файла, включая перекрестные ссылки, поскольку существующий код должен сдвигаться или перемещаться. Из-за вычисляемых переходов и смешивания кода с данными [40], полное дизассемблерное покрытие возможно, только если двоичный файл содержит информацию о переадресации и символическую отладочную информацию (например, файлы PDB) [19], [41], [42]. К сожалению, отладочная информация обычно устраняется из финальных сборок для компактности и защиты интеллектуальной собственности.
Для Windows-программ, в частности, PE-файлы (DLL и EXE) обычно содержат информацию о переадресации, даже если не содержат никакой отладочной информации [43]. Загрузчику нужна эта информация в случае, когда DLL должна быть загружена по адресу, отличному от ее предпочтительного базового адреса, например, потому что туда уже была отображена другая библиотека или из-за ASLR. В отличие от разделяемых библиотек Linux и исполняемых файлов PIC, которые содержат независимый от положения код, двоичные файлы Windows содержат абсолютные адреса, например в виде конкретных операндов инструкций или инициализированных указателей данных, которые корректны, только если исполняемый файл был загружен по предпочтительному базовому адресу. Секция .reloc PE-файла содержит список относительных смещений для каждой из секций PE-файла, которые соответствуют всем абсолютным адресам, к которым нужно прибавить дельта-значение в случае, когда фактический адрес загрузки секции отличается от предполагаемого.
Однако, одной лишь информации о переадресации недостаточно для получения полной картины кода в секциях PE-файла [38], [41]. Хотя расположение объектов, которые достижимы только через косвенные переходы, может быть получено из информации о переадресации, без символической отладочной информации, содержащейся в PDB-файлах, их действительный тип – код или данные – остается неизвестным. В некоторых случаях настоящий тип этих объектов может быть установлен с помощью эвристики, основанной на распространении констант, но подобные методы могут привести к тому, что данные будут проинтерпретированы как код и наоборот. Даже небольшой сдвиг или увеличение размера одного объекта в секции PE-файла приведет к каскадным сдвигам следующих за ним объектов. Обычно неопознанный объект, который на самом деле содержит код, будет содержать PC-относительные ветви на другие кодовые объекты. При отсутствии отладочной информации, содержащейся в PDB-файлах, перемещение такого неопознанного блока кода (или любого объекта, на который он относительно ссылается) без исправления сдвигов всех его относительных инструкций ветвления, которые ссылаются на другие объекты, приведет к некорректному коду.
Учитывая вышеприведенные ограничения, мы решили использовать только те преобразования двоичного кода, которые не изменяют размер и положение объектов кода и данных в пределах исполняемого файла, что позволяет проводить рандомизацию сторонних PE-файлов без символической отладночной информации. Хотя данное ограничение не позволяет нам применять протяженные преобразования кода вроде переупорядочивания базовых блоков или метаморфизма, мы все же достигаем частичной рандомизации кода путем сильно локализованных модификаций, которые могут надежно применяться даже без полного дизассемблерного покрытия. Это может быть достигнуто путем небольших изменений, совершаемых на месте для корректно определенных частей кода, которые не меняют общей структуры базовых блоков или функций, но которых достаточно для изменения результатов выполнения коротких последовательностей инструкций, могущих выступать в роли гаджетов.
B. Модификация и извлечение кода
Хотя абсолютно точное дизассемблирование двоичных файлов, использующих систему команд x86 и лишенных отладочной информации, невозможно, передовые дизассемблеры достигают хорошего покрытия кода, сгенерированного большинством распространенных компиляторов, используя комбинацию различных алгоритмов дизассемблирования [40], идентификацию специфических конструкций кода [45] и простой анализ потока данных [46]. Для реализации нашего прототипа мы использовали IDA Pro [47], чтобы извлечь код и идентифицировать функции исполняемых PE-файлов. IDA Pro эффективен для определения границ даже для функций, не являющихся непрерывными блоками кода и широко использующих разделение базовых блоков [48]. Кроме того, этот дизассемблер пользуется информацией о переадресации, присутствующей в DLL-файлах Windows.
Однако, как правило, без символической информации из PDB-файлов часть функций в PE-файле не определяется и некоторые части кода остаются неисследованными. Наши трансформации кода применяются консервативно, только к тем частям кода, которые были точно дизассемблированы. Например, IDA Pro теоретически способна дизассемблировать блоки кода, которые достижимы только через вычисляемые переходы, пользуясь содержащейся в PE-файлах информацией о переадресации. Однако, мы не применяем подобные эвристические методы извлечения кода, чтобы избежать неудачных модификаций из-за потенциально неправильной идентификации кода. На практике для кода, сгенерированного большинством компиляторов, информация о переадресации также гарантирует, что правильно определенные базовые блоки не имеют других точек входа, кроме своей первой инструкции. Аналогично, некоторые преобразования, основанные на правильной идентификации функций, применяются только к коду верно распознанных функций. Наша реализация отделена от используемого фреймворка для извлечения кода, что означает, что IDA Pro может быть заменен или дополнен альтернативными способами извлечения кода [41], [49], [50], дающими лучшее дизассемблерное покрытие.
После извлечения кода дизассемблированные инструкции сначала преобразуются в наше собственное внутреннее представление, которое содержит дополнительную информацию вроде неявно используемых регистров и регистров и флагов, считываемых или изменяемых инструкцией. В целях корректности мы также отслеживаем использование регистров общего назначения даже в инструкциях для чисел с плавающей точкой, а также инструкциях из наборов MMX и SSE. Хотя данные типы инструкций имеют свои собственные наборы регистров, они используют регистры общего назначения для ссылок на участки памяти (например, инструкция fmul на рис. 1). Далее мы применяем преобразования кода на месте, обсуждаемые в следующем разделе. Они применяются только к частям исполняемых сегментов, содержащим (преднамеренно или случайно [2]) последовательности инструкций, которые можно использовать как гаджеты. В результате некоторых таких преобразований инструкции могут перемещаться с их исходных позиций в пределах своего базового блока. В таких случаях для инструкций, содержащих в некоторых своих операндах абсолютные адреса, соответствующие записи в секциях .reloc рандомизируемого PE-файла заполняются новыми смещениями для новых положений данных абсолютных адресов.
Наша опытная реализация обрабатывает каждый PE-файл индивидуально и генерирует множество рандомизированных копий, которые затем могут заменить оригинал. Учитывая невысокую сложность анализа, требуемую для генерации набора рандомизированных экземпляров поданного на вход файла (в наших тестах – в среднем порядка нескольких минут), это позволяет оффлайновую генерацию пула рандомизированных PE-файлов для заданного приложения. Отметим, что для большинства протестированных Windows-приложений рандомизация была необходима лишь нескольким DLL, поскольку остальные, как правило, были защищены ASLR (впрочем, они тоже могут быть рандомизированы для улучшения защиты). При промышленном развертывании системная служба или модифицированный загрузчик могут впоследствии подбирать различные рандомизированные версии требуемых PE-файлов при каждом запуске приложения, следуя тем же путем, что и инструменты вроде EMET [51].
IV. Преобразования кода на месте
В данном разделе мы подробно опишем различные преобразования кода, используемые для рандомизации кода на месте. Хотя некоторые из этих преобразований вроде переупорядочивания инструкций и переназначения регистров также используются компиляторами и полиморфическими движками для оптимизации кода [52] и обфускации [35], применение их на двоичном уровне – без доступа к высокоуровневым структурам и семантической информации, имеющем место в данных случаях – вызывает значительные трудности.
A. Подстановка атомарных инструкций
Один из основных принципов обфускации и метаморфизма кода [35] заключается в том, что одни и те же вычисления можно выполнить с помощью бесчисленного множества комбинаций инструкций. Применительно к рандомизации кода, подстановка в гаджет иной, но функционально эквивалентной последовательности инструкций не повлияет на ВОП-код, который использует этот гаджет, поскольку результат его выполнения не изменится. Однако, модифицируя инструкции исходного программного кода, данное преобразование по существу модифицирует определенные байты образа кода программы, а, следовательно, может серьезно изменить структуру последовательностей случайно образованных инструкций, которые перекрываются подставленными инструкциями.
Множество используемых в ВОП-коде гаджетов состоят из невыравненых инструкций, которые не были выпущены компилятором, но которые присутствуют в образе кода процесса из-за плотности и переменной длины инструкций, характерной для системы команд x86. В примере, показанном на рис. 1(a) действительный код, сгенерированный компилятором, состоит из инструкций mov; cmp; lea
, начиная с байта со значением B01. Однако, дизассемблируя код со следующего байта, можно найти полезный случайно образованный гаджет, заканчивающийся на ret.
Скомпилированный код сильно оптимизирован, поэтому замена единственной инструкции исходного программного кода обычно требует для достижения того же результата либо более длинной инструкции, либо комбинации из нескольких инструкций. Учитывая, что нашей целью является рандомизация кода двоичных файлов без отладочной информации, даже небольшое увеличение размера базового блока невозможно, что делает неприменимыми в нашем случае наиболее распространенные методы подстановки инструкций.
В определенных случаях, однако, возможно заменить одну инструкцию другой, функционально ей эквивалентной и имеющей ту же длину, благодаря гибкости, предлагаемой обширной системой команд x86. Помимо очевидных замен, основанных на замещении сложения вычитанием противоположного числа и наоборот, существуют также некоторые инструкции, существующие в различных формах с разными опкодами, в зависимости от поддерживаемых типов операндов. Например, add r/m32,r32 хранит результат сложения в регистре или операнде памяти (r/m32), а add r32,r/m32 хранит результат в регистре (r32). Хотя эти две формы имеют различные опкоды, обе инструкции эквивалентны, когда оба операнда являются регистрами. Многие арифметические и логические инструкции имеют подобные пары эквивалентных форм, а в некоторых случаях могут существовать до пяти эквивалентных инструкций (например, test r/m8,r8, or r/m8,r8, or r8,r/m8, and r/m8,r8, and r8,r/m8, влияют на флаги регистра EFLAGS одинаково, если оба операнда являются одним и тем же регистром). В нашей опытной реализации мы применяли наборы эквивалентных инструкций, используемых в Hydan [54], инструменте для сокрытия информации в исполняемых файлах x86, добавив еще одного набора, который включает эквивалентные версии инструкции xchg.
Рисунок 1. Пример подстановки атомарной инструкции. Эквивалентная, но другая форма инструкции cmp не меняет первоначальный код программы (a), но делает случайно образованный гаджет бесполезным (b).
Как показано на рис. 1(b), оба операнда инструкции cmp являются регистрами, поэтому инструкция может быть заменена своей эквивалентной формой, имеющей другие байты опкода и ModR/M [55]. Хотя сам программный код не меняется, инструкция ret, "входящая" в инструкцию cmp, теперь исчезла, сделав гаджет бесполезным. В данном случае преобразование полностью уничтожает гаджет и, поэтому, будет применено во всех экземплярах рандомизированного двоичного файла. Если же подстановка не влияет на финальный косвенный переход гаджета, тогда она применяется вероятностно.
B. Переупорядочивание инструкций
В определенных случаях возможно изменить порядок инструкций небольших независимых фрагментов кода, не влияя на правильность работы программы. Данное преобразование может значительно повлиять на структуру случайно образованных гаджетов, а также сделать несостоятельными предположения атакующего о гаджетах, являющихся частью действительного машинного кода.
1) Переупорядочивание в базовом блоке. Выбор порядка инструкций компилятором на этапе генерации кода зависит от многих факторов, включающих стоимость инструкции в циклах процессора и применяемые методы оптимизации кода [52]. Поэтому код базового блока зачастую представляет лишь один из нескольких эквивалентных порядков инструкций. На основании этого факта мы можем частично модифицировать код в базовом блоке путем переупорядочивания некоторых инструкций.
Основой для получения альтернативного порядка инструкций является определение упорядочивающих связей между инструкциями, которые всегда должны сохраняться для поддержания корректности кода. Граф зависимостей базового блока отражает внутренние зависимости между инструкциями, ограничивающие набор возможных порядков [56]. Поскольку базовый блок содержит код, выполняемый в прямом порядке, его граф зависимостей является ациклическим орграфом с машинными инструкциями в качестве вершин и зависимостями между ними в качестве ребер. Мы применяем анализ зависимостей к коду дизассемблированных базовых блоков, чтобы построить их графы зависимостей путем адаптации стандартного алгоритма построения зависимостей машинного кода, DAG [56, Fig. 9.6]. Применение анализа зависимостей напрямую к машинному коду требует аккуратной интерпретации зависимостей между инструкциями системы команд x86. По сравнению с анализом кода, представленного в промежуточной форме, такой анализ включает идентификацию зависимостей не только между содержимым регистров и операндов памяти, но также между значениями флагов процессора, неявно используемых регистров и участков памяти.
Для каждой инструкции i мы составляем множества use[i] и def[i], содержащие регистры, используемые и определяемые данной инструкцией. Помимо регистров-операндов и регистров, используемых для вычисления эффективных адресов, они включают любые неявно используемые регистры. Например, множества use и def для инструкции pop eax
равны {esp} и {eax, esp}, а для rep stosb
2 – {ecx, eax, edi} и {ecx, edi}, соответственно. Изначально мы предполагаем, что все инструкции базового блока зависят друг от друга, а затем проверяем каждую пару на предмет зависимостей чтение-после-записи (RAW), запись-после-чтение (WAR) и запись-после-записи (WAW). Например, i1 и i2 связаны зависимостью RAW, если выполнено любое из следующих условий:
- def[i1] ∩ use[i2] ≠ ∅,
- операнд-назначение i1 и операнд-источник i2 оба являются адресами памяти,
- i1 изменяет хотя бы один флаг, считываемый i2.
Отметим, что условие 2) является достаточно консервативным, учитывая, что i2 на самом деле зависит от i1, только если i2 считывает тот же участок памяти, в который записывает i1. Однако, за исключением случая, когда оба операнда памяти используют абсолютные адреса, трудно статически определить, указывают ли оба эффективных адреса на один и тот же участок памяти. В нашей будущей работе мы планируем использовать простой анализ потока данных для ослабления этого условия. Помимо инструкций с операндами из памяти данное условие также должно проверяться для инструкций, которые неявно обращаются к участкам памяти, вроде push
и pop
. Условия для зависимостей WAR и WAW аналогичны. Если между двумя инструкциями не найдено конфликта, тогда нет ограничений на порядок их выполнения.
Рисунок 2. Пример того, как переупорядочивание инструкций внутри базового блока может повлиять на случайно образованный гаджет.
Рисунок 2(a) показывает код базового блока, содержащего случайно образованный гаджет, а рисунок 3 – соответствующий ему граф зависимостей DAG. Инструкции, не соединенные ребром напрямую являются независимыми, и для относительного порядка их выполнения не существует ограничений. Если дан граф DAG базового блока, возможные порядки инструкций этого блока соответствуют различным вариантам топологической сортировки графа [57]. Рисунок 2(b) показывает один из возможных альтернативных порядков инструкций исходного кода. Положение всех инструкций, кроме одной, и значения всех байтов, кроме одного, в этом случае изменились, что устранило случайно образованный в первоначальном коде гаджет. Хотя двумя байтами позднее в блоке образовался новый гаджет (опять же заканчивающийся инструкцией ret в байте C3), атакующий не может надеяться на данный гаджет, поскольку альтернативные порядки сдвинут его на другие позиции и некоторые из его инструкций всегда будут меняться (в данном примере исчезла полезная инструкция pop ecx
). Фактически, инструкция ret также может быть устранена с помощью подстановки атомарной инструкции.
Рисунок 3. Граф зависимостей для кода из рис. 2.
В основе данной трансформации лежит предположение о том, что границы базового блока не изменятся во время выполнения. Если вычисленная инструкция передачи управления нацелится не на первую инструкцию базового блока, тогда переупорядочивание может нарушить семантику кода. Хотя это может показаться существенным ограничением, мы отметили, что в ходе нашей оценки мы ни разу не встретились с подобным случаем. Для сгенерированного компилятором кода IDA Pro может вычислить все цели переходов даже для вычисляемых переходов на основании информации о переадресации из PE-файла. В наиболее консервативном случае пользователи могут отключить переупорядочивание инструкций и все еще воспользоваться прочими методами рандомизации – раздел V показывает результат для каждого отдельно взятого метода.
Рисунок 4. Пример переупорядочивания кода сохранения регистров.
2) Переупорядочивание кода сохранения регистров. Соглашение вызова, которому следует большинство Windows- и Linux-компиляторов для платформы x86, утверждает, что регистры ebp, esi, edi
и ebx
должны сохраняться вызываемой стороной [58]. Остальные регистры общего назначения, известные как временные регистры, могут свободно использоваться вызываемой стороной без ограничений. Обычно функция, которой нужно использовать не только доступные временные регистры, сохраняет все прочие регистры перед их изменением путем сохранения их значений в стеке. Это обычно делается в начале функции цепочкой инструкций push
, как в примере на рис. 4(a), который показывает начало и окончание функции. В конце функции соответствующая цепочка инструкций pop
восстанавливает сохраненные значения из стека. Последовательности инструкций pop
, за которыми следует ret
, являются наиболее широко используемыми гаджетами, находимыми в ВОП-эксплоитах, поскольку они позволяют атакующему загружать в регистры значения, входящие в состав внедренной полезной нагрузки [59]. Порядок инструкций pop
критичен для инициализации каждого регистра правильным значением.
Как видно, в начале функции компилятор заносит в стек значения сохраняемых вызывающей стороной регистров в произвольном порядке, и иногда инструкции push
перемежаются с инструкциями, использующими ранее сохраненные регистры. В конце функции сохраненные значения выталкиваются из стека в обратном порядке так, чтобы они попали в правильные регистры. Поэтому, если сохраненные значения восстанавливаются в правильном порядке, их порядок в стеке значения не имеет. На основании данного факта мы можем рандомизировать порядок инструкций push
и pop
кода сохранения регистров путем поддержания порядка первым-вошел-последним-вышел сохраненных значений, как показано на рис. 4(b). В данном примере существует 6 возможных порядков трех инструкций pop
, а, значит, любое предположение, которое атакующий может сделать о том, какие регистры будут содержать два предоставленных значения, будет верным с вероятностью 1/6 (или 1/3 если нужно инициализировать только один из регистров). В случае сохранения лишь двух регистров существует два возможных порядка, что позволяет гаджетам корректно оперировать каждый второй раз.
Данное преобразование применяется окнсервативно, только к функциям с точно дизассемблированным началом и окончанием кода. Чтобы убедиться, что мы правильно сопоставили инструкции push
и pop
, которые сохраняют и восстанавливают заданный регистр, мы отслеживаем приращения указателя стека по всей функции, как показано во втором столбце рис. 4(a). Если приращения в начале и окончании не совпадают, например, из-за вызовов функций с неизвестными соглашениями вызова или косвенной манипуляции с указателем стека, тогда рандомизация не применяется. Как показано на рис. 4(b), любые инструкции в начале функции, не относящиеся к сохранению регистров, переупорядочиваются вместе с инструкциями push
, сохраняя все внутренние зависимости, как обсуждалось в предыдущем разделе. Для функций со множеством точек выхода код восстановления во всех возможных окончаниях должен соответствовать коду сохранения значений. Отметим, что для одного и того же регистра может быть несколько пар push-pop
в случае, если регистр сохраняется только в некоторых ветвях выполнения функции.
C. Переназначение регистров
Хотя точки программы, в которых определенная переменная должна быть сохранена в регистре или вытеснена в память, выбираются компилятором с помощью сложных алгоритмов размещения, фактическое имя регистра общего назначения, который примет определенное значение выбирается почти произвольно. На основании данного факта мы можем переопределить имена операндов-регистров в существующем коде согласно иному, но эквивалентному назначению регистров, не влияя на семантику первоначального кода. При рассмотрении каждого гаджета как автономной последовательности инструкций, данное преобразование может изменить результат выполнения многих гаджетов, которые станут читать и модифицировать не те регистры, которые предполагал атакующий.
Из-за более высокой стоимости обращения к памяти по сравнению с обращением к регистру, компиляторы пытаются отобразить как можно больше переменных на доступные регистры. Поэтому в любой точке большой программы обычно одновременно используется или является живыми (live) множество регистров. Пусть дан граф потока управления (CFG) скомпилированной програмы, тогда регистр r является живым в точке программы p, если существует путь из p, который использует r без его предварительной перезаписи. Область жизни r определяется как множество точек программы, где r является живым и может быть представлен как подграф CFG [60]. Поскольку один и тот же регистр может хранить различные переменные в разных точках программы, регистр может иметь множество непересекающихся регионов жизни в пределах одного CFG.
Для каждой корректно идентифицированной функции мы вычисляем области жизни всех регистров, используемых в ее теле, путем проведения анализа времени жизни [52] прямо в машинном коде. Имея CFG функции и множества use[i] и def[i] для каждой инструкции i мы получаем наборы in[i] и out[i] из регистров, которые являются живыми на входе (live-in) и живыми на выходе (live-out) для каждой инструкции. Для определения этих множеств мы используем модифицированную версию стандартного алгоритма определения времен жизни переменных [52, рис. 9.16], который вычисляет множества in и out на уровне инструкций, вместо уровня базовых блоков. Данный алгоритм вычисляет два этих множества путем итеративного нахождения неподвижных точек для следующих уравнений потока данных: in[i] = use[i] ∪ (out[i] − def[i]) и out[i] = ∪ {in[s] : s ∈ succ[i]}, где succ[i] – это множество всех возможных инструкций, следующих за инструкциейi.
Рисунок 5. Области жизни регистров eax
и edi
во фрагменте функции. Эти два регистра можно заменить друг на друга во всех инструкциях в их параллельных, замкнутых регионах a0 и d1 (строки 3–12).
Рисунок 5 показывает часть CFG функции и соответствующие области жизни для регистров eax
и edi
. Сначала мы предполагаем, что все регистры являются живыми, поскольку все они могут иметь значения, установленные вызывающей стороной. В данном примере edi
является живым при входе в функцию, а инструкция push
на строке 2 сохраняет (т. е. использует) его текущее значение в стеке. Следующая инструкция mov
инициализирует (определяет) edi
, завершая его предыдущую область жизни (d0). Отметим, что хотя область жизни является подграфом CFG, мы иллюстрируем и ссылаемся на разные области жизни как на линейные регионы ради удобства.
Следующее определение edi
находится в строке 15, что означает, что последнее использование его предыдущего значения на строке 11 также завершает его предыдущий регион жизни d1. Регион d1 является замкнутым, поскольку внутри него мы можем быть уверены, что edi
хранит одну и ту же переменную. Регистр eax
также имеет замкнутый регион жизни (a0), который располагается параллельно d1. Концептуально эти два региона жизни можно расширить до одинаковых границ. Поэтому регистры edi
и eax
можно заменить друг на друга во всех инструкциях, находящихся в пределах отмеченных регионов, не поменяв семантику кода.
Инструкция call eax
на строке 12 может быть легко использована атакующим для вызова библиотечной функции или другого гаджета. Если переназначить регистры eax
и edi
в их параллельных регионах жизни, любой ВОП-код, который будет зависеть от значения eax
для передачи управления следующему фрагменту кода, теперь будет переходить в неправильный участок памяти и, вероятно, приводить к аварийному завершению. Для фрагментов кода с лишь двумя параллельными регионами жизни атакующий сможет верно угадывать регистр в половине случаев. Однако, во многих случаях существуют три или более регистров общего назначения с параллельными регионами жизни или прочие доступные регистры, которые являются живыми до или после региона жизни другого регистра, что позволяет большее количество возможных переназначений регистров.
Регистры, используемые в первоначальном коде, могут быть переназначены путем модификации байтов ModR/M и (иногда) SIB соответствующих инструкций. Как и ранее описанные преобразования кода, измененяя операнды инструкций в существующем коде, эти модификации также могут повлиять на перекрываемые инструкции, которые могут быть частью случайно образованных гаджетов. Отметим, что неявно используемые регистры в определенных инструкциях не могут быть замещены. Например, однобайтовая инструкция "move data from string to string" (movs
) всегда использует esi
и edi
как операнд-источник и операнд-назначение, и не существует другой однобайтовой инструкции, выполняющей ту же операцию, используя другой набор регистров [55]. Поэтому, если такая инструкция является частью региона жизни одного из ее неявно используемых регистров, то этот регистр в данном регионе не может быть заменен. По той же причине мы исключаем esp
из анализа времен жизни. Наконец, хотя соглашения вызова выполняются для большинства функций, они выполняются не всегда, так как компиляторы имеют право использовать любое специальное соглашение вызова для закрытых или статических функций. Большинство из этих случаев консервативно покрывается восходящим анализом вызовов, который находит специальные регистровые аргументы и регистры возврата значений.
Сначала все описания внешних функций, находящихся в таблице импорта DLL, помечаются как функции нулевого уровня. IDA Pro может эффективно различать различные соглашения вызова, которым эти внешние функции могут следовать, и сообщать их объявления на языке C. Таким образом, в большинстве случаев регистровые аргументы и регистр возврата (если таковой присутствует) для каждой из функций нулевого уровня известны. Для любой инструкции вызова (call
) функции нулевого уровня регистровые аргументы функции добавляются к набору неявно читаемых регистров инструкции call
, а регистры возврата значений функции добавляются к набору неявно перезаписываемых регистров инструкции call
.
На следующем этапе функции первого уровня определяются как набор функций, которые вызывают только функции нулевого уровня. Любые регистры, считываемые функцией первого уровня без предварительной перезаписи помечаются как регистровые аргументы данной функции. Подобным образом, регистры, которые перезапиываются и не считываются перед инструкцией возврата помечаются как регистры возврата значений. Набор неявно читаемых и изменяемых регистров всех инструкций вызова функций первого уровня обновляется соответствующим образом. Аналогично, функции второго уровня – те, которые вызывают только функции первого или нулевого уровня и т. д. Этот процесс повторяется, пока не найдется ни одной функции очередного уровня. Данный подход основан на идее о том, что закрытые функции, которые могут использовать нестандартные соглашения вызова, вызываются из других функций из той же DLL и, в большинстве случаев, не через вычисляемые инструкции вызова.
1 Код всех примеров данного документа взят из icucnv36.dll, входящей в состав Adobe Reader v9.3.4. Данная DLL использовалась в ВОП-коде эксплоита, обходящего DEP, для CVE-2010-2883 [53] (см. таблицу II).
2 stosb
(Store Byte to String) копирует наименее значащий байт из регистра eax
в участок памяти, на который указывает регистр edi
, и увеличивает значение edi
на 1. Префикс rep
повторяет данную инструкцию, пока значение ecx
не достигнет 0, уменьшая его после каждой итерации.
18 октября, 2012
Местные провайдеры на один день заблокировали доступ ко всем ресурсам, содержащим слово ubuntu.
15 октября, 2012
Помимо темы жестких дисков, Торвальдс указал на правовые излишества в сфере патентов.
22 октября, 2012
По мнению министра, отрасль остро нуждается в квалифицированных кадрах, которые необходимо привлекать...
19 октября, 2012
Госдума должна рассмотреть ряд законопроектов, устанавливающих лимит на хранение в Сети пользовательских...
16 октября, 2012
В результате мошеннической деятельности пострадавшему был нанесен ущерб в размере 7,7 млн руб.
18 октября, 2012
Истцы утверждают, что Роскомнадзор бездействовал, пока лицензиат не выполнял требования, предусмотренные...
19 октября, 2012
Великобритания набирает 18-летних любителей видеоигр для защиты компьютерных систем страны.
18 октября, 2012
По мнению правообладателей, социальная сеть распространяет определенный контент, не имея на это их согласия.
19 октября, 2012
Правоохранительные органы выиграли суд по блокировке доступа к сайтам с запрещенной книгой.
19 октября, 2012
ФАС возбудила против компании уголовное дело после получения жалобы от одного из пользователей услуг...
22 октября, 2012
19 октября, 2012
17 октября, 2012
15 октября, 2012
12 октября, 2012
Источник: http://www.securitylab.ru/analytics/431636.php
Дайджест новых статей по интернет-маркетингу на ваш email
Новые статьи и публикации
- 2024-11-26 » Капитан грузового судна, или Как начать использовать Docker в своих проектах
- 2024-11-26 » Обеспечение безопасности ваших веб-приложений с помощью PHP OOP и PDO
- 2024-11-22 » Ошибки в Яндекс Вебмастере: как найти и исправить
- 2024-11-22 » Ошибки в Яндекс Вебмастере: как найти и исправить
- 2024-11-15 » Перенос сайта на WordPress с одного домена на другой
- 2024-11-08 » OSPanel 6: быстрый старт
- 2024-11-08 » Как установить PhpMyAdmin в Open Server Panel
- 2024-09-30 » Как быстро запустить Laravel на Windows
- 2024-09-25 » Next.js
- 2024-09-05 » OpenAI рассказал, как запретить ChatGPT использовать содержимое сайта для обучения
- 2024-08-28 » Чек-лист: как увеличить конверсию интернет-магазина на примере спортпита
- 2024-08-01 » WebSocket
- 2024-07-26 » Интеграция с Яндекс Еда
- 2024-07-26 » Интеграция с Эквайринг
- 2024-07-26 » Интеграция с СДЕК
- 2024-07-26 » Интеграция с Битрикс-24
- 2024-07-26 » Интеграция с Travelline
- 2024-07-26 » Интеграция с Iiko
- 2024-07-26 » Интеграция с Delivery Club
- 2024-07-26 » Интеграция с CRM
- 2024-07-26 » Интеграция с 1C-Бухгалтерия
- 2024-07-24 » Что такое сторителлинг: техники и примеры
- 2024-07-17 » Ошибка 404: что это такое и как ее использовать для бизнеса
- 2024-07-03 » Размещайте прайс-листы на FarPost.ru и продавайте товары быстро и выгодно
- 2024-07-01 » Профилирование кода в PHP
- 2024-06-28 » Изучаем ABC/XYZ-анализ: что это такое и какие решения с помощью него принимают
- 2024-06-17 » Зачем вам знать потребности клиента
- 2024-06-11 » Что нового в работе Яндекс Метрики: полный обзор обновления
- 2024-06-11 » Поведенческие факторы ранжирования в Яндексе
- 2024-06-11 » Скорость загрузки сайта: почему это важно и как влияет на ранжирование
Всегда храни верность своему начальнику - следующий, может быть еще хуже... |
Мы создаем сайты, которые работают! Профессионально обслуживаем и продвигаем их , а также по всей России и ближнему зарубежью с 2006 года!
Как мы работаем
Заявка
Позвоните или оставьте заявку на сайте.
Консультация
Обсуждаем что именно Вам нужно и помогаем определить как это лучше сделать!
Договор
Заключаем договор на оказание услуг, в котором прописаны условия и обязанности обеих сторон.
Выполнение работ
Непосредственно оказание требующихся услуг и работ по вашему заданию.
Поддержка
Сдача выполненых работ, последующие корректировки и поддержка при необходимости.