Как стать автором
Обновить

Приемы анализа malware: Распаковка драйверов в Ring3

Время на прочтение 4 мин
Количество просмотров 4.8K
В повседневной работе в анализе троянов и всякой малвари, с достаточной периодичностью попадаются экземпляры, которые дропают шифрованные драйвера. Но человек я в меру ленивый и привыкший работать в ring3, поэтому покажу один из способов распаковки драйверов, не прибегая к помощи низкоуровневых отладчиков.


Вступление

Для начала, стоит сказать, что данный способ подходит для драйверов, которые импортируют из ядра и системных библиотек небольшое число API. Порядка пары-тройки десятков функций. В противном случае нас ждет довольно муторная работа, этого не стоящая.

Подготовка драйвера

Сам драйвер, по своей структуре почти идентичен обычным динамическим библиотекам. И чтоб загрузить его в ring3, нужно изменить пару полей в PE-заголовке, а именно:
  • Тип Subsystem с Native на Windows GUI. (В PeTools кнопка Optional Header);
  • В поле IMAGE_FILE_HEADER.Characteristics выставить атрибут Dll. (В PeTools кнопка File Header, а затем Characteristics);

Теперь нужно отвязать драйвер от системных низкоуровневых библиотек. Для этого напишем свою dll (или несколько, кому как угодно) с заглушками необходимых функций. Открываем список импортируемых API и ищем их прототипы в MSDN или в других информационных ресурсах.
В моем случае список следующий:

Список API до патча
Чтобы драйвер мог подгрузить именно нашу dll, а не системную, пропатчим имена импортируемых модулей, на подготовленные нами.

Список API после патча
Теперь наш драйвер можно загрузить в отладчик пользовательского режима, например OllyDbg.

Распаковка и решение проблем по мере выполнения

В данном случае пакер примитивный — xor и разновидность алгоритма LZ. Рассматривать xor-декриптор я не буду, не смотря на то, что он немного замусорен. После дешифровки попадаем на следующий код:

Код после xor-декриптора
Вот и первая неприятность. Если просто отпустить код выполняться, то сразу словим exception ACCESS_VIOLATION. Код берет из служебных структур адрес, находящийся внутри ntoskrnl.exe и находит ImageBase модуля. Но так как мы находимся в ring3, то структура, находящаяся в сегменте FS отличается от ядерной. И если потрассировать код, то из fs:[38] считается 0, а на следующей команде будет чтение по адресу 0+4. Естественно, никакого ntoskrnl у нас в памяти тоже нет, поэтому предположим, что обойдемся адресом ntdll (большая часть её API совпадает с ядерными функциями).

Открываем карту памяти и смотрим что находится в сегменте FS. Должны увидеть TIB — Thread Information Block. Немного посмотрев, можно увидеть в ней и указатель на PEB — Process Environment Block. Выбираем любой подходящий адрес в ntdll (я выбрал PEB.FastPebLock).

PEB и TIB
Можно просто заNOPить код, и по ходу трейса подменить адрес на ntdll. Но мы поступим по другому — изменим смещения.


Следующая проблема с которой мы сталкиваемся по ходу трейса — распаковщик динамически получает адреса необходимых ядерных функций. На скриншоте видно цикл перебора списка имен с псевдофункцией xGetProcAddress, которая аналог системной. Её начало, где она парсит MZ-заголовок, можно увидеть в нижней части.


При этом EDX указывает на список имен необходимых функций


Внимательные заметят чуть ниже пожатый MZ-заголовок, но об этом позже.
Вроде ничего необычного. Все было бы хорошо, да только в ntdll нет некоторых необходимых API, или хотя бы похожих по прототипу. Но если немного подумать, то найдутся таковые в kernel32.dll.

  1. PVOID ExAllocatePool(POOL_TYPE PoolType, SIZE_T NumberOfBytes);
  2. VOID ExFreePool(PVOID P);

Аккуратно можно заменить на 
  1. HGLOBAL WINAPI GlobalAlloc(UINT uFlags, SIZE_T dwBytes);
  2. HGLOBAL WINAPI GlobalFree(HGLOBAL hMem);

Меняем имена несуществующих функций, на любые имеющиеся, чтобы просто корректно отработал xGetProcAddress. Я их заменил на NtClose.


Аккуратно трассируя, подменяем в регистрах адрес NtClose, на необходимые нам адреса из kernel32.dll. После отработки цикла, все необходимые адреса получены, как видно на изображении ниже. С этой проблемой покончено, следуем дальше.


Убеждаемся, что подмененная нами ExAllocatePool на GlobalAlloc стабильно отрабатывает.


Незаметно подошли к распаковке.


Вероятно код писался на assembler’е, так как нет ничего лишнего. Код на скрине делает выделение памяти, распаковку в нее, потом делает подготовку образа, зануляет и чистит память, и в случае успеха прыгает на OEP.
Алгоритм распаковки я не стал изучать, потому что на вид пожатые данные мне показались похожи на вариант LZW.


Распаковщик не использует никаких API, поэтому прогоняется быстро и без проблем. Собственно, сразу после этого можно делать дамп региона с чистым драйвером. Но мне было интересно, на сколько можно будет продвинуться в анализе, находясь в ring3.
Функция PrepareImage подготавливает распакованный образ: делает ремап секций по необходимым смещениям, получает адреса API из импорта, производит пересчет адресов по таблице relocations.
Очередные палки в колеса нам сует цикл поиска функций для IAT, который не только запрашивает модули, которых у нас нет (ntoskrnl, hal и др), но и соответственно функции.


Как видно я уже попался и вошел в цикл, но поменяв EIP на 0x008982d7, уменьшив ESP и установив в EAX = 0, более-менее корректно вышел из него. Правка reloc’ов не приносит нам каких-либо неприятностей, и мы наконец выходим на OEP. Но на этом придется остановиться, так как адреса импорта не восстановлены, а писать очередную dll с заглушками я не вижу смысла. Чистый код можно уже проанализировать статически в дизассемблере.

Вместо вывода

До:


…и после:


Чтобы не мучать Вас вбиванием строк с нижнего скриншота в поиск, скажу сразу, что это одна из версий Rustock’а

В очередной раз убеждаюсь, что моя лень заставляет извращаться еще дольше, чем это можно было бы сделать решением «в лоб».
Теги:
Хабы:
+44
Комментарии 12
Комментарии Комментарии 12

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн