В повседневной работе в анализе троянов и всякой малвари, с достаточной периодичностью попадаются экземпляры, которые дропают шифрованные драйвера. Но человек я в меру ленивый и привыкший работать в ring3, поэтому покажу один из способов распаковки драйверов, не прибегая к помощи низкоуровневых отладчиков.
Для начала, стоит сказать, что данный способ подходит для драйверов, которые импортируют из ядра и системных библиотек небольшое число API. Порядка пары-тройки десятков функций. В противном случае нас ждет довольно муторная работа, этого не стоящая.
Сам драйвер, по своей структуре почти идентичен обычным динамическим библиотекам. И чтоб загрузить его в ring3, нужно изменить пару полей в PE-заголовке, а именно:
Теперь нужно отвязать драйвер от системных низкоуровневых библиотек. Для этого напишем свою dll (или несколько, кому как угодно) с заглушками необходимых функций. Открываем список импортируемых API и ищем их прототипы в MSDN или в других информационных ресурсах.
В моем случае список следующий:
Чтобы драйвер мог подгрузить именно нашу dll, а не системную, пропатчим имена импортируемых модулей, на подготовленные нами.
Теперь наш драйвер можно загрузить в отладчик пользовательского режима, например OllyDbg.
В данном случае пакер примитивный — xor и разновидность алгоритма LZ. Рассматривать 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).
Можно просто заNOPить код, и по ходу трейса подменить адрес на ntdll. Но мы поступим по другому — изменим смещения.
Следующая проблема с которой мы сталкиваемся по ходу трейса — распаковщик динамически получает адреса необходимых ядерных функций. На скриншоте видно цикл перебора списка имен с псевдофункцией xGetProcAddress, которая аналог системной. Её начало, где она парсит MZ-заголовок, можно увидеть в нижней части.
При этом EDX указывает на список имен необходимых функций
Внимательные заметят чуть ниже пожатый MZ-заголовок, но об этом позже.
Вроде ничего необычного. Все было бы хорошо, да только в ntdll нет некоторых необходимых API, или хотя бы похожих по прототипу. Но если немного подумать, то найдутся таковые в kernel32.dll.
Аккуратно можно заменить на
Меняем имена несуществующих функций, на любые имеющиеся, чтобы просто корректно отработал 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’а
В очередной раз убеждаюсь, что моя лень заставляет извращаться еще дольше, чем это можно было бы сделать решением «в лоб».
Вступление
Для начала, стоит сказать, что данный способ подходит для драйверов, которые импортируют из ядра и системных библиотек небольшое число API. Порядка пары-тройки десятков функций. В противном случае нас ждет довольно муторная работа, этого не стоящая.
Подготовка драйвера
Сам драйвер, по своей структуре почти идентичен обычным динамическим библиотекам. И чтоб загрузить его в ring3, нужно изменить пару полей в PE-заголовке, а именно:
- Тип Subsystem с Native на Windows GUI. (В PeTools кнопка Optional Header);
- В поле IMAGE_FILE_HEADER.Characteristics выставить атрибут Dll. (В PeTools кнопка File Header, а затем Characteristics);
Теперь нужно отвязать драйвер от системных низкоуровневых библиотек. Для этого напишем свою dll (или несколько, кому как угодно) с заглушками необходимых функций. Открываем список импортируемых API и ищем их прототипы в MSDN или в других информационных ресурсах.
В моем случае список следующий:
Чтобы драйвер мог подгрузить именно нашу dll, а не системную, пропатчим имена импортируемых модулей, на подготовленные нами.
Теперь наш драйвер можно загрузить в отладчик пользовательского режима, например OllyDbg.
Распаковка и решение проблем по мере выполнения
В данном случае пакер примитивный — xor и разновидность алгоритма LZ. Рассматривать 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).
Можно просто заNOPить код, и по ходу трейса подменить адрес на ntdll. Но мы поступим по другому — изменим смещения.
Следующая проблема с которой мы сталкиваемся по ходу трейса — распаковщик динамически получает адреса необходимых ядерных функций. На скриншоте видно цикл перебора списка имен с псевдофункцией xGetProcAddress, которая аналог системной. Её начало, где она парсит MZ-заголовок, можно увидеть в нижней части.
При этом EDX указывает на список имен необходимых функций
Внимательные заметят чуть ниже пожатый 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.
Аккуратно трассируя, подменяем в регистрах адрес 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’а
В очередной раз убеждаюсь, что моя лень заставляет извращаться еще дольше, чем это можно было бы сделать решением «в лоб».