Многоразрядные шелл-коды. Пишем Ring0-shellcode под windows x64Для тебя наверняка не секрет, что ядро 64-битной Windows подверглось значительным изменениям. Это, в первую очередь, касается ряда системных структур и функций. А значит, на 64-битной винде привычные способы написания шелл-кодов становятся совершенно бесполезными, посему приходится брать дизассемблер и адаптироваться к новым условиям. Копаться в этих самых структурах, сравнивать и анализировать. Чем мы сегодня с тобой и займемся! Что примечательно — никто до меня эту тему не расписывал. Да и я сам за все время существования x64 видел только пример шелл-кода ring3 (inj3ct0r.com/exploits/9740). Под нулевое кольцо мне пока ничего увидеть не довелось. Будем это дело исправлять — ведь, в конце концов, область применения позиционно-независимого кода чрезвычайно широка: от вполне легальных пакеров/протекторов до руткитов и эксплойтов. Что нужно для работы?Для компиляции драйвера (мы ведь пишем шелл-код ring0) тебе потребуется Macro Assembler x64 (ml64). Его можно утянуть из WDK. Сам WDK доступен для скачивания на сайте Мелкософта по ссылке: microsoft.com/whdc/DevTools/WDK/WDKpkg.mspx. После компиляции драйвер можно будет загрузить с помощью набора консольных утилит из примеров для FASM (install_drv.exe, start_drv.exe и пр.). И только потому, что мне было лень написать свою утилиту (сваяю в ближайшее время). Скачать их можно по ссылке: flatassembler.net/examples/win64_drivers.zip. Помимо всего прочего тебе понадобятся Microsoft Debugging Tools (64-битные версии) + livekd Руссиновича (нужен для просмотра смещений в ядерных структурах). Первое и второе можно скачать на сайте Microsoft: microsoft.com/whdc/devtools/debugging/install64bit.mspx и technet.microsoft.com/ru-ru/sysinternals/bb897415.aspx. Чтобы лицезреть отладочный вывод своего новоиспеченного драйвера, тебе потребуется DbgView. Он себя (что очень приятно) прекрасно чувствует на 64-битной винде, как, впрочем, и livekd (technet.microsoft.com/en-us/sysinternals/bb896647.aspx). Дизассемблировать ядерные модули будем с помощью IDA x64. Ну и для успешного восприятия того, что здесь написано, ты должен быть знаком с форматом PE и хотя бы в общих чертах понимать, что представляет собой 64-битный ассемблер. К вышесказанному надо еще добавить желание разобраться в тонкостях 64-битного пикодинга :). План действийОбрисую задачи, которые нам необходимо будет решить при написании базонезависимого кода. Чтобы в голове все уложилось, всегда лучше следовать определенному плану.
Итак, начнем по порядку... Поиск базы ядраПервым делом нам необходимо получить базу ядра. Я рассмотрю три способа получения:
Конечно же, способы получения адреса начала ядра не исчерпываются только этими тремя. Более того, здесь все ограничивается только фантазией кодера; в мои цели входит лишь расставить путеводные флажки для твоих дальнейших исследований. Что ж, приступим! Способ №1Суть как этого, так и других способов в том, что мы должны отыскать какой-то адрес, принадлежащий ntoskrnl. Такой адрес можно получить из, скажем, обработчика прерывания int 3 из idt. Выбор допустимого вектора определяется тем, принадлежит ли он обычно ядру. Я говорю «обычно», так как могут стоять различные хуки. Хотя на x64 idt проверяется Patch Guard, его защиту можно без труда обойти. В вопросе поиска подходящего обработчика нам поможет livekd и его команда !idt, которая дампит таблицу прерываний.
Итого 20+ возможных вариантов, что не так уж мало. Но, чтобы определить адрес обработчика, нам потребуется адрес начала idt. Один из способов ее получения — чтение поля KPCR.IdtBase. О виде структуры PCR спросим, как обычно, у livekd.
Как ты можешь заметить, указатели расширились до 64 бит, ну и имена полей (сравнивая с 32-разрядной Виндой) тоже поменялись. Но это не самое главное, так как для нас в данный момент важно смещение поля IdtBase относительно начала _PCR. Хорошо, допустим, структуру x64 PCR мы мало-мальски знаем (честь и хвала livekd). А откуда мы достанем указатель на KPCR? Глянем в код функции из hal.dll HalInitializeProcessor.
Как видим, указатель на PCR для текущего процессора мы можем утянуть из gs:[18h]. В 32-битной Винде юзали сегментный регистр fs, а теперь gs :). Так, а что стало с дескрипторами шлюзов в idt? Нам ведь надо достать адрес обработчика int 3 (ну или другого, по вкусу), а у нас только дескрипторы шлюзов, по которым «размазано» смещение обработчика. Они, конечно же, в x64 тоже поменялись. Как именно? Изменения можно (и нужно) смотреть в оригинальных мануалах производителя (AMD, Intel), но у нас сейчас под рукой только livekd. И с его помощью тоже можно узнать нужную инфу! Вводим dt _KIDTENTRY64, чтобы вытянуть из отладочных символов структуру дескриптора шлюза.
Из этого дампа мы должны понять, где внутри дескриптора располагаются кусочки адреса обработчика (он же Offset). Адрес, как ты уже понял, тоже 64-битный. Вот, собственно, и все, что нам нужно. Ниже привожу код с комментариями, который и получает базу ядра этим способом.
Способ №2Этот метод также основывается на idt. Он несколько лаконичней, чем предыдущий, но также имеет ряд особенностей по сравнению с аналогичным способом для 32-разрядной версии Windows. Основная причина различий — изменение формата регистра idtr. Поле лимита не поменялось (как было 2 байта, так и осталось), а вот поле базы стало равным 8 байтам. Структура idtr на x64 следующая: Получив базу idt, действуем инструкцией sidt так же, как и в предыдущем примере. Обнуляем младшие 12 бит адреса и, вычитая по 1000h, проверяем сигнатуры MZ и PE. СПОСОБ № 3 Еще один красивый способ заключается в получении адреса
KiSystemCall64 (так называется обработчик в 64-битном ядре) из msr
регистра lstar (его адрес 0xC0000082). Этот регистр (как, вероятно, тебе
После этого в паре регистров edx:eax будет содержаться адрес KiSystemCall64. Кстати, вовсе необязательно читать именно регистр lstar. Вообще, инструкция syscall в long mode (режим процессора, в котором работает 64-разрядная винда) юзает 2 msr регистра: cstar и lstart для compatibility и 64-bit mode соответственно. Так к чему это я веду? В msr cstar (0xC0000083) тоже лежит адрес, принадлежащий диапазону адресов ntoskrnl! — это адрес процедуры KiSystemCall32. Далее по уже известной тебе схеме получаем адрес начала ядра. Разбор экспорта в windows x64Теперь приступаем к получению адресов нужных функций. Так как мы имеем дело с форматом PE32+, надо учитывать его специфику. Обрисую основные моменты, которые поменялись. Если ты имеешь опыт написания шелл-кодов для win32, то наверняка знаешь, что получить указатель на директорию экспорта можно, прибавив к адресу начала IMAGE_NT_HEADERS 78h. В win64 мы прибавляем 88h. Увеличение значения величины смещения обусловлено увеличением некоторых полей в PE32+ по сравнению с PE32. Взглянем на структуру _IMAGE_NT_HEADERS64:
То есть изменения коснулись полей опционального заголовка, из-за этого пришлось поменять смещения. Кстати IMAGE_EXPORT_ DIRECTORY (как и IMAGE_DOS_HEADER) не поменялась, что играет нам на руку. В заключение привожу код разбора таблицы экспорта для PE32+:
Как видишь, этот код имеет два существенных недостатка. Во-первых, я не хеширую имя функции, а использую посимвольное сравнение. Во-вторых, отсутствует оптимизация.
Теперь компилируем драйвер с помощью ml64, а отладочный вывод смотрим в DbgView.
Правильность полученного адреса можно проверить в livekd. Для этого введем команду: ЗаключениеВот мы и разобрались с ядерным шелл-кодингом под 64-битную Винду. На первый взгляд ничего сложного. Вообще, надо сказать, что 64-разрядные процессоры очень привлекательны для написания пи-кода. Все дело в таких нововведениях, как rip-relative addressing и большее количество регистров. Правда, некоторые вещи я намеренно упустил из виду — не приводить же совсем законченное решение :). С этим тебе, читатель, еще предстоит разобраться, и да поможет тебе дизассемблер! |