В этой статье мы рассмотрим, каким образом DTrace (фреймворк динамической трассировки) может быть эффективно использован для выполнения задач реверс-инжиниринга.
Tiller Beauchamp David Weston Science Applications International Corporation {Tiller.L.Beauchamp,David.G.Weston}@saic.com
Sun Microsystems описывает DTrace как “фреймворк динамической трассировки, использующийся для устранения системных неполадок в реальном времени”. DTrace состоит из нескольких компонентов в ядре ОС и пользовательском пространстве, связанных вместе при помощи скриптового языка D. Динамическая трассировка позволяет увидеть почти всю активность в системе по запросу при помощи встроенных программных датчиков. OS X Leopard и Solaris предоставляет тысячи возможных датчиков различных уровней, от ядра до приложений пользовательского уровня, как веб-браузеры и чат-программы. Подобная всесторонняя видимость предоставляет информацию, требующуюся системному администратору, разработчику или пользователю для понимания динамического и комплексного взаимодействия между программными компонентами.
Ответы на подобные вопросы могут быть получены запросом данных, собранных датчиками DTrace при помощи скриптов на языке D. D – Это интерпретируемый язык программирования, созданный для DTrace. Синтаксис D можно описать как C-подобный, но по структуре больше похож на синтаксис Awk. Динамический аспект DTrace заключается в том, что датчики могут быть включены по требованию и удалены, как только требуемые данные будут получены. Это достаточно ненавязчивый метод наблюдения за системой или процессом.
DTrace был первым программным компонентом компании Sun, выпущенным под их собственной Общей лицензией на разработку и распространение (CDDL). Открытость исходного кода DTrace позволила добраться фреймворку и до других операционных систем. Тем не менее, скептическое отношение к CDDL замедлило попытки портирования DTrace на FreeBSD. В RedHat решили вступить в конкуренцию вместе с их продуктом SystemTap. DTrace был портирован на Apple OS X 10.5 Leopard, выпущенную в октябре 2007 года. Двумя неделями позже было объявлено о порте DTrace на QNX. Сообщество DTrace продолжает динамично развиваться.
поставщик:модуль:функция:имя
Поставщик: Модель ядра DTrace, логически группирующая вместе различные родственные датчики. В качестве примера можно привести: fbt, следящий за функциями ядра; pid, следящий за процессами пользовательского пространства и syscall; наблюдающий за системными вызовами.
Модуль: Местоположение группы датчиков. Это может быть имя модуля ядра, где находится датчик, либо пользовательская библиотека. Например: libsc.so библиотека или ufs модуль ядра.
Функция: Определяет функцию, при которой этот датчик должен сработать. Это может быть частной функцией в библиотеке, например printf() или strcpy().
Имя: Обычно это отражает назначение датчика. Например «entry» или «return» для функции или «start» для I/O датчика. Для трассировки на уровне команд указывается смещение внутри функции.
Знание синтаксиса DTrace позволяет понять назначение определенного датчика. Вы можете получить список всех датчиков в DTrace отсортированных по поставщику, введя команду “dtrace –l”. Датчики будут отображены в формате, описанном выше.
probe definition
|
Для предоставления двусторонней связи между пользовательским пространством и ядром в DTrace существует канал в виде общей библиотеки libtrace.
Рисунок 2. Общий вид архитектуры DTrace
Библиотека libtrace компилирует D скрипт в промежуточный вид. Как только программа скомпилирована, она отправляется в ядро операционной системы при помощи модулей ядра DTrace для выполнения. Именно в это время датчики, указанные в вашем скрипте активируются. После окончания выполнения скрипта, включенные датчики удаляются и система возвращается в состояние нормальной работы.
Сильная сторона DTrace заключается в масштабе и точности получаемой информации с использованием относительно простых D скриптов. Реверс-инженер может узнать много всего о кусочке программы благодаря одному или двум правильно расставленным датчикам. Это ставит DTrace в категорию окружений “быстрой разработки” для реверс-инженеров.
В оставшейся части статьи будет рассказано об использовании DTrace для выполнения различных общих задач реверс-инжиниринга. Сначала мы объясним, как можно использовать DTrace для обнаружения и точного указания места программы при переполнения буфера в стеке. Далее, мы рассмотрим обнаружение переполнения буфера в куче и другие проблемы управления динамически распределяемой памятью. Затем мы посмотрим, как использовать DTrace вместе с IDA Pro для визуализации процедуры покрытия кода. Наконец, мы обсудим возможности обнаружения вторжения при помощи DTrace и различные способы ухода от DTrace.
Самым простым решением является наблюдение за регистром-указателем (EIP) на предмет известной некорректной величины, например 0x41414141 или определенной величины, которую вы хотите проанализировать (0xdeadbeef например). Это потребует активации только одного датчика для каждого входа функции. Количество датчиков может быть значительным. В таблице ниже содержатся некоторые общие приложения и количество входных датчиков, доступных в OS X для этих приложений, включая функции библиотек.
Программа |
Датчики |
Firefox |
202561 |
Quicktime |
218404 |
Adium |
223055 |
VMWare Fusion |
205627 |
cupsd |
91892 |
sshd |
59308 |
ftp клиент |
6370 |
Рисунок 3. Количество входных датчиков общих приложений в OS X 10.5
Тем не менее, мы не можем точно оценить влияние каждого датчика на скорость работы приложения, т.к. датчики влияют на скорость работы только при срабатывании. Приложение может использовать много библиотек, но вызывать только несколько функций. И наоборот, приложение может вызывать одну функцию циклически, сильно снижая производительность во время трассировки. Во избежание снижения производительности мы должны сначала убедиться в том, что наши датчики не ведут трассировку неважных модулей и функций, вызываемых слишком часто. Скрипт DTrace, показанный на рисунке 4, может быть использован для отображения наиболее часто вызываемых функций.
#!/usr/sbin/dtrace -s
|
Рисунок 4. Скрипт для подсчета вызовов функций
Когда данный скрипт запущен вместе с Firefox и QuickTime Player, становятся очевидными функции и библиотеки, которые могут быть исключены из трассировки. В QuickTime Player мы увидели большое количество вызовов функции __i686.get_pc_thunk.cx. Оба приложения большинство своих вызовов обращают к функциям в модуле libSystem.B.dylib. Исключая эти часто вызываемые функции и библиотеки, мы заметим значительный прирост производительности во время трассировки данных приложений. Наш опыт в работе с DTrace показал наибольшую эффективность при написании уникальных скриптов, использующих ограниченное количество датчиков, чем при написании универсального DTrace скрипта, применимого к любой ситуации.
Как только было выбрано подмножество приложений для трассировки, можно использовать простой DTrace скрипт, показанный на рисунке 4, для проверки значения следующей команды.
Датчик сработает, когда величина EIP будет равна 0x41414141. В обычной ситуации, это приведет к сбою и закрытию приложения. Но с DTrace мы можем остановить выполнение приложения до того момента, как оно попытается выполнить инструкцию по адресу 0x41414141. Это позволит нам извлечь такие данные как значения регистров процессора и параметров функций, либо задействовать традиционный отладчик для изучения стека.
#/usr/sbin/dtrace -s
|
Рисунок 5. Проверка EIP на предмет некорректного значения
В этом примере мы предположили, что переполнение стека произойдет при значении EIP 0x41414141. Этого будет достаточно для простого анализа, но эффективный детектор должен обнаруживать переполнение в более общем виде. Достигнуть этого можно, записывая адрес возврата в стековом фрейме, созданном во время входа в функцию. Записанный адрес возврата позже можно сравнить с возвращаемой величиной. Мы не сравниваем значение EIP с сохраненной возвращаемой величиной из-за того, что DTrace использует оптимизацию хвостовой рекурсии. [2] DTrace будет возвращать результат к месту вызова функции. Тем не менее, значение EIP при возврате значения функции будет являться первой инструкцией, вызываемой функции, а не возвращаемым значением, сохраненным в стековом фрейме. Монитор целостности произведет сравнение сохраненных возвращенных значений со значением EIP. Наш детектор предупредит, если сохраненный адрес возврата отличается от текущего адреса возврата и EIP равняется текущему адресу возврата.
Вышеописанная логика подходит для большинства приложений. Однако нужно пояснить некоторые особенности работы DTrace. В частности, DTrace не может отслеживать функции, использующие таблицы переходов. Когда DTrace не может определить, что происходит в функции, он прекращает наблюдение. По этой причине, вы можете закончить на функции, для которой существует входной датчик, но отсутствует выходной. Это как раз тот случай, когда DTrace не может полностью отследить поведение функции по причине использования ею таблиц перехода. Если происходит вызов подобной функции и об этом сообщает наш монитор стека, но не возвращает значения, наш список сохраненных адресов возврата перестает соответствовать стеку. Эти функции должны быть проигнорированы во время трассировки для правильного мониторинга стека. Команда “-l” в DTrace может быть использована для вывода списка датчиков, соответствующих их определению. Список входных датчиков можно сравнить со списком выходных датчиков для выявления функций, мониторинг которых не следует выполнять.
При выполнении этих соображений, наш монитор переполнения стека смог обнаружить RTSP переполнение в QuickTime Player. Краткий вывод показан ниже. Полный вывод программы включает в себя трассировку вызовов.
# ./eiptrace.d -q -p 4450
|
Рисунок 6. Обнаружено переполнение стека
Монитор будет обнаруживать переполнения стека, основываясь на замену адреса возврата. В большинстве случаев переполнения будут изменять не только адрес возврата, но и остальные данные в стеке. В результате это может привести к попыткам неправильного доступа к памяти во время попытки функции получить значение по указателю перед возвратом. Дополнительный скрипт DTrace может быть использован для точного указания инструкции, вызывающей переполнение. Для этого необходимо произвести трассировку каждой команды в уязвимой функции, и после каждого ее выполнения проверить возвращаемое значение в стеке. Как только будет обнаружено переполнение, мы будем знать, что последнее значение EIP и является инструкцией, вызвавшей переполнение.
Стоит изучить и остальные способы использования DTrace для обнаружения переполнения стека. Подобным образом, как и при мониторинге переполнения кучи, описанном ниже, могут записываться размеры параметров функции и адресы. Затем их можно сравнить во время операции копировании функциями bcopy, memcpy, strcpy. Другой подход заключается в записи границ стекового фрейма и проверки правильности параметра копирования во время выполнения функций bcopy, memcpy или strcpy. Это поле для дальнейшей деятельности.
pid$target::malloc:entry{
|
Nemo с FelineMenace.org опубликовал фундаментальное исследование "Exploiting Mac OS X heap overflows (Использование уязвимости переполнение кучи в Mac OS X)" в журнале Phrak 63 [11]. Его метод основывается на изменении объема и частоты размещений в куче (в OS X называемой "зонами") в сочетании с переполнением буфера кучи для замены указателей функции, содержащихся в начальной структуре кучи (“malloc_zone_t"). Структура, содержащая указатели к различным функциям, таким как malloc(), calloc(), realloc() и т.д., загружается в пространство процессов. Когда адреса этих функций заменены, следующий вызов может разрешить выполнение произвольного кода. Это только один из многих методов использования уязвимости кучи.
Появление кучи как одного из главных направлений атак привело к необходимости в инструментах, способных помочь реверс-инженерам понять работу структуры кучи.
В настоящий момент существует множество недавно выпущенных инструментов, сосредоточенных на понимании механизма работы кучи с точки зрения реверс-инжиниринга. Immunity Debugger на платформе Windows имеет мощный API, предоставляющий множество инструментов для понимания механизма работы кучи. На платформе Linux и Solaris существует инструмент Core Security's HeapTracer, написанный Gerado Richarte. Он использует ltruss или ltrace для мониторинга системных вызовов, размещающих динамическую память. Разработанный на основе этой же идеи монитор переполнения кучи, включенный RE:Trace, следит за её состоянием, перехватывая функции динамического размещения. Heap Smash Detector входящий в состав RE:Trace не просто отслеживает размещения в куче. Он идет на шаг впереди, перехватывая функции, пытающиеся разместить данные в куче.
RE:Trace Heap Smash Detector создает хеш Ruby, отслеживающий запрашиваемый функцией malloc() объем, используемый в качестве величины. Указатель возврата используется в качестве ключа. Heap Smash Detector также хранит запись всех вызовов calloc(), realloc(), free() соответственно. Второй хеш следит за стековым фреймом, в котором расположен оригинальный блок памяти. Например, RE:Trace захватывает вызов стандартной функции C strncpy(), адрес и параметры объема сравниваются с хешем malloc(). Если размер strncpy() больше выделенного блока, то мы знаем, что произошло переполнение кучи. Heap Smasher определил с высокой точностью место переполнения и стековый фрейм, выполнивший вызов malloc(). Неплохо для относительно небольшого скрипта!
Рисунок 8. Перехвачена функция strncpy()
Схожая техника определения уязвимости была создана с использованием пакета Microsoft Detours [14]. Инструмент "VulnTrace" использует DLL, внедренную в пространство процессов, перехватывающую функции, которые импортируются в таблицу IAT таким образом, что инструмент может изучать аргументы на предмет ошибок безопасности. Этот метод намного более трудоемкий, чем метод, используемый в RE:Trace. Метод требует индивидуальной доработки для каждого приложения. Из-за использования дополнительной DLL возможно снижения производительности. DTrace реализован в качестве компонента операционной системы и не вмешивается в работу тестируемого ПО.
Есть несколько предостережений касательно алгоритма работы распределения зон в OS X и их нужно принять во внимание при реализации Heap Smash Detector. Как упомянул Nemo в его статье"Exploiting Mac OS X heap overflows (Использование уязвимости переполнение кучи в Mac OS X)", OS X хранит раздельные «зоны» или кучи для размещений разных объемов. Приведенная ниже таблица из книги “Mac OS X Internals (Внутренности Mac OS X)”, написанной A. Singh, показывает разделение «зон» в зависимости от объема размещений.
Тип зоны |
Размер зоны |
Размер распределения (байты) |
Величина распределения |
Маленькая |
2 Мб |
< 1993 |
32 байта |
Небольшая |
8 Мб |
1993 - 15,359 |
1024 байта |
Большая |
- |
15,360—16,773,120 |
1 страница (4096 байтов) |
Огромная |
- |
> 16,773,120 |
1 страница (4096 байтов) |
Рисунок 9. Типы зон в OS X Leopard
Мы можем использовать раздельные хеши для каждой зоны. Один интересный аспект маленькой и небольшой зоны заключается в том, что их размер ограничен 2 и 8 Мб. Это позволяет с легкостью рассчитать количество памяти, выделенной каждой зоне.
Еще один интересный факт работы зон в OS X, описанный Nemo, заключается в том, что алгоритм распределения в большинстве случаев не будет выполнять функцию free() дважды, если зона расположена по тому же адресу [11]. При данных обстоятельствах (т.е. free() того же размера и указатель free() все еще существует) эта уязвимость может быть использована, и это состояние должно быть обнаружено. Мы можем наблюдать за этим состоянием при помощи RE:Trace Heap Smash Detector. Будущие дополнения в фреймворке могут включать интеграцию с IDA для автоматизированного создания датчиков.
Трассировка на уровне функций может дать нам некоторые наводки на то, какие части кода были выполнены во время работы программы. Трассировка на уровне функций может быть весьма полезной для понимания того, как определенная уязвимая функция может себя повести. Но в контексте покрытия кода, наблюдение на уровне кода в лучшем случае можно назвать туманным. Мы не увидим ни сложности функции, ни даже количество кода внутри выполняемой функции. На уровне функций мы сможем узнать только, что делает приложение, но мы не сможем получить информацию о том, насколько качественно мы протестировали программу.
Трассировка на уровне блоков может предоставить нам намного больше информации о покрытии кода. Блок кода – это набор инструкций, работающий таким образом, что при выполнении первой инструкции выполняются все инструкции в блоке. Операции условного перехода разделяют блоки инструкций. Это представлено на схеме дизассемблера IDA Pro, показанной ниже.
Рисунок 10. Схема дизассемблера IDA Pro
Стрелки между блоками представляют возможные пути кода при его выполнении. При тестировании приложения нам нужно знать, сколько блоков кода мы можем выполнить. DTrace может предоставить нам эту информацию благодаря его возможности трассировки на уровне инструкций. Это дает нам возможность увидеть, какие блоки в функции могут быть выполнены, а какие нет. Мы сможем получить ответы на такие вопросы, как процент выполненных, процент выполненных блоков, сколько раз был выполнен блок, и какие блоки не выполнялись. Это предоставляет важные показатели тестировщикам программного обеспечения.
С точки зрения производительности, наблюдение за датчиками в каждой инструкции в большом приложении может быть накладным. Это помогает сосредоточиться на отдельной библиотеке или на самом коде приложения. Дальнейшие усовершенствования можно выполнить при помощи статического анализа. Только одна инструкция должна снабжаться датчиком в каждом блоке. С трассировкой на уровне инструкций в DTrace датчики определенных инструкций заданы в качестве смещения внутри функции, так же как и адрес памяти относительно начала библиотеки или глобального адреса инструкций в виртуальной памяти. Например, приведенный ниже датчик сработает на инструкции со смещением 3f в функции getloginname программы /usr/bin/login:
pid4573:login:getloginname:3f {}
DTrace является инструментом анализа исключительно во время работы программы и не имеет представления о блоках кода. Для определения адресов первой инструкции в каждом блоке нужно применить статический анализ с использованием дизассемблера. Как только список адресов для наблюдения будет определен, они должны быть преобразованы из глобальных адресов к смещению внутри их функций для использования в DTrace.
Мы используем несколько технологий для соединения DTrace и IDA Pro для визуализации процедуры покрытия кода в реальном времени. Ruby-dtrace используется для сворачивания libtrace, позволяя использовать Ruby для программирования действий, выполняемых при срабатывании определенных датчиков [4]. IDARub используется в качестве удаленного интерфейса для доступа к API IDA Pro [5]. IDA Pro запускается на системе Windows и окружение Ruby отправляет команды в IDA по сети. Когда датчик срабатывает, сигнализируя о выполнении инструкции в приложении, эта инструкция высвечивается в IDA Pro. В поле комментария этой инструкции может быть показано, сколько раз эта команда была выполнена. Рисунок 11 показывает представление операции покрытия кода. Красные блоки содержат выполненный код, белые блоки - еще невыполненный код.
Рисунок 11. Представление покрытия кода в IDA
Визуализация процесса покрытия кода позволяет легко увидеть, когда большие куски кода не выполняются. Ручной анализ может быть выполнен для определения условий, необходимых для запуска пропущенного кода.
DTrace позволяет пользователю производить такой же перехват системных вызовов как и коммерческие продукты McAfee и Cisco в почти такой же ненавязчивой форме. Подобную систему обнаружения атак, основанную на анализе системных вызовов будет несложно реализовать на языке D [10].
Используя публично доступный эксплойт Subreption для QuickTime 7.3, основанный на RTSP переполнении стека, в качестве образца, мы можем увидеть? как можно быстро создать HIDS систему при помощи D скрипта.
Эксплойт Subreption для QuickTime 7.3 на Leopard OS X 10.5.1 использует классическую атаку “возврата в библиотеку” для переполнения стека. Атака “возврата в библиотеку” использует переполнение буфера для установки произвольных аргументов в стеке перед возвратом в функцию System() для выполнения системного вызова. Это, вероятно, наиболее популярная техника использования уязвимости на платформах с неисполняемым стеком. Полезность большинства этих атак зависит от последовательности системных вызовов, обычно включающих в себя вызов "/bin/sh" или "/bin/bash". Если мы хотим защитить уязвимый QuickTime 7.3 от эксплойта "возврата в библиотеку", нам нужно сначала изучить нормальное поведение QuickTime с помощью системных вызовов. Для этого можно использовать DTrace вместе со скриптом, показанным на рисунке 12.
#!/usr/sbin/dtrace -q –s
|
Рисунок 12. Изучение системных вызовов QuickTime
Как только мы изучили системные вызовы, мы можем начать создавать отличительные признаки атаки. Простое “занесение атак в черный список”, основанных на одном или двум эксплойте, не может являться достаточным для “промышленных” систем HIDS, но послужит примером создания подобных систем, основанных на DTrace. Для получения подробной информации ознакомьтесь с патентом # 20070107058 на HIDS систему, разработанную компанией Sun на основе DTrace.
Сравнивая вывод системных вызовов простого D скрипта во время нормальной работы и поведение во время использования уязвимости, мы можем определить, какие системные вызовы могут служить в качестве отличительных признаков для нашего D скрипта системы HIDS. После анализа системных вызовов атаки "возвращения в библиотеку" становится очевидным, что QuickTime Player обычно не будет выполнять системный вызов для запуска “/bin/sh” во время нормальной работы (разумеется, это банальный пример). Использование следующего утверждения: "“execname == “QuickTime Player” & args[0] == “/bin/sh”/” будет достаточным для создания общего D скрипта, способного обнаружить эксплойт Subreption и его варианты. После обнаружения эксплойта при помощи датчика syscall, событие необходимо занести в журнал, вывести на экран или остановить атакуемый процесс. Весь скрипт, показанный на рисунке 13, занимает всего несколько строк.
#!/usr/sbin/dtrace -q -s
|
Рисунок 13. D скрипт HIDS системы QuickTime
Несмотря на то, что приведенный выше пример является простым, он может быть улучшен путем добавления отличительных признаков других атак. Существует несколько преимуществ создания "собственной" HIDS системы. Первое заключается в невозможности протестировать эффективность специальной HIDS системы, не выполнив атаку. Коммерческие готовые системы могут быть изучены в управляемом окружении для того, чтобы убедиться в возможности эксплойта уклониться от обнаружения. Второе преимущество состоит в том, что HIDS систему можно подогнать под другое приложение во избежание ошибочного результата. Использование Ruby-DTrace для реализации HIDS системы позволит разработчику хранить отличительные признаки в реляционной базе данных и использовать интерфейс Ruby-On-Rails.
Для достижения этого Apple использует тот же метод, что и в GDB для предотвращения отладки некоторых приложений. Как объясняет Landon Fuller: “PT_DENY_ATTACH – нестандартный запрос ptrace(), предотвращающий отладку некоторых процессов” [10].
Обе версии GDB и DTrace, написанные Apple, проверяют наличие этого флага перед отладкой и трассировкой процесса. Landon Fuller является также автором расширения ядра (kext) для XNU, позволяющего наблюдение за любым процессом при помощи DTrace. Заменяя указатель функции ptrace в системной структуре внутри ядра XNU на указатель к специальной обертке PTrace, Fuller сделал возможным использовать DTrace для любых процессов.
В его презентации на Chaos Computer Congress, названной “B.D.S.M The Solaris 10 way” Archim рассказал об огромной проведенной работе, позволяющей его руткиту “SInAR” прятаться от DTrace на платформе Solaris. Проблема для автора руткита заключается в поставщике DTrace fbt, хранящем список всех модулей, загруженных в ядре. Поэтому даже если вы нашли способ спрятать приложение от mbd, ps, и т.д., опытный администратор до сих пор сможет обнаружить руткит на базе ядра. Даже модули, имеющие значения mod_loaded и mod_installed равные 0, все равно видны для DTrace:
“При комбинировании вызовов dtrace_sync() и, затем dtrace_condense(&fbt_provider), вы будете удалены из списка модулей-поставщиков в DTrace.”
Это заставит DTrace удалить руткит из внутреннего списка поставщиков и деактивирует все его датчики. В настоящее время версия 0.3 SInAR на vulndev.org работает только на SPARC. На сегодняшний день нет известного руткита, имеющего возможность спрятаться от DTrace на OS X Leopard или Solaris 10 x86.
Появление DTrace в мире реверс-инжиниринга предоставило множество возможностей для совершенствования методов выполнения общих задач. Мы показали, как DTrace может быть использован для обнаружения переполнения стека или кучи и указания точного места в программе, и визуализации операции покрытия кода. Мы также обсудили использование DTrace в качестве инструмента обнаружения атак, и его обход. Существует много других интересных областей для исследования в будущем. Это включает в себя реализацию автоматизированной обратной связи фаззера, основанной на результатах покрытия кода или значениях параметров, обнаружение руткитов при помощи расчетов тайминга DTrace и точное указание местонахождения ошибок в ядре.