R0 CREW

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

Источник: habrahabr.ru

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

Вступление

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

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

Сам драйвер, по своей структуре почти идентичен обычным динамическим библиотекам. И чтоб загрузить его в ring3, нужно изменить пару полей в PE-заголовке, а именно:

  • Тип Subsystem с Native на Windows GUI. (В PeTools кнопка Optional Header).
  • В поле IMAGE_FILE_HEADER.Characteristics выставить атрибут Dll. (В PeTools кнопка File Header, а затем Characteristics).

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

В моем случае список следующий:

Рис. 1. Список API до патча

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

Рис. 2. Список API после патча

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

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

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

Рис. 3. Код после 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).

Рис. 4. PEB и TIB

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

Рис. 5

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

Рис. 6

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

Рис. 7

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

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

Аккуратно можно заменить на

HGLOBAL WINAPI GlobalAlloc(UINT uFlags, SIZE_T dwBytes);
HGLOBAL WINAPI GlobalFree(HGLOBAL hMem);

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

Рис. 8

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

Рис. 9

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

Рис. 10

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

Рис. 11

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

Алгоритм распаковки я не стал изучать, потому что на вид пожатые данные мне показались похожи на вариант LZW.

Рис. 12

Распаковщик не использует никаких API, поэтому прогоняется быстро и без проблем. Собственно, сразу после этого можно делать дамп региона с чистым драйвером. Но мне было интересно, на сколько можно будет продвинуться в анализе, находясь в ring3.

Функция PrepareImage подготавливает распакованный образ: делает ремап секций по необходимым смещениям, получает адреса API из импорта, производит пересчет адресов по таблице relocations.

Очередные палки в колеса нам сует цикл поиска функций для IAT, который не только запрашивает модули, которых у нас нет (ntoskrnl, hal и др), но и соответственно функции.

Рис. 13

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

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

До:

Рис. 14

…и после:

Рис. 15

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

В очередной раз убеждаюсь, что моя лень заставляет извращаться еще дольше, чем это можно было бы сделать решением «в лоб»