Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Недавно мне попался вредонос семейства PikaBot, и при его изучении я наткнулся на старый, но по‑прежнему популярный прием API Hashing. Он позволяет скрывать возможности малвари как от средств защиты, так и от инструментов анализа. Сегодня мы при помощи эмуляции попробуем автоматизировать выявление функций, вызываемых через API Hashing.
Так как API Hashing (а также другие способы сокрытия вредоносных функций) время от времени встречается в моей практике, мне захотелось найти более универсальный способ, чем просто каждый раз писать скрипт, который интерпретирует алгоритм хеширования имени API-функции Win32 из вредоноса. Я подумал, что эмулятор отлично подойдет для быстрого решения этой проблемы. Давай посмотрим, что из этого вышло.
Для начала вспомним, в чем именно заключается техника API Hashing. Вредоносный код импортирует функции из системных библиотек путем анализа PE-заголовка, а точнее — таблицы экспорта этих библиотек.
Определить API Hashing можно, как раз отслеживая поиск функции по хешу от ее имени. То есть малварь перебирает таблицу экспорта системных либ ntdll.dll
или kernel32.dll
, хеширует имя каждой функции и сравнивает с хешем имени искомой функции. В случае совпадения вредонос читает адрес нужной функции для дальнейшего использования.
Алгоритм подсчета хеша может быть любой: например, часто встречаются CRC-32 или MurmurHash2, а также любой кастомный алгоритм одностороннего преобразования, в общем — лишь бы сильно не нагружать цикл вычисления хеша.
При таком подходе функции, которые использует малварь, не будут отражены ни в таблице импорта, ни как строки в самом бинаре. Это позволяет обойти некоторые виды антивирусного сканирования, поэтому техникой до сих пор пользуются вирусописатели и члены красных команд.
info
Подробнее об API Hashing читай в статье «Веселые хеши. Реализуем технику API Hashing, чтобы обдурить антивирус».
Модуль, который применяет API Hashing, я получил, анализируя этот образец PikaBot. Если тебе интересно, можешь попробовать самостоятельно извлечь семпл или можешь сэкономить время и скачать уже извлеченный.
В дизассемблере первый пример API Hashing ты встретишь по адресу 0x10011B78
:
.text:10011B6C mov [ebp-24h], eax .text:10011B6F push 0D57B2h .text:10011B74 mov edx, [ebp-24h] .text:10011B77 push edx .text:10011B78 call sub_10012470
Функция sub_10012470
как раз таки реализует поиск адреса функции по хешу. В данном случае хеш искомой функции — это значение 0D57B2h
, которое кладется на стек тремя инструкциями выше. Первый же аргумент функции — адрес библиотеки kernel32.dll
, который получен в результате вызова sub_10012410
по адресу 0x10011B64
.
То есть получается, что первым аргументом передается адрес библиотеки, а вторым — ее хеш. Если ты знаком с WinAPI, то это сильно напомнит тебе функцию GetProcAddress с той разницей, что вторым аргументом передается название функции в явном виде, а не хеш от него.
Итак, у нас есть хеш и нам нужно узнать, от названия какой функции WinAPI он был получен. Как правило, в таких случаях для получения искомого названия функции нужны три ингредиента:
kernel32.dll
и ntdll.dll
(можно добавить и другие либы, но вряд ли они используются на этом этапе работы малвари). Рецепт простой — перебирать по списку все имена функций, хешируя их по интерпретированному алгоритму, пока не получим совпадение с искомым хешем. Впрочем, это только один из возможных способов, все ограничивается только твоей фантазией.
Так как наша цель — получить названия всех хешированных функций, не тратя время на интерпретацию алгоритма хеширования, мы не будем погружаться в анализ механизма получения адресов системных либ, перебора их экспорта и деталей алгоритма хеширования, а перейдем сразу к использованию эмуляции.
В качестве средства эмуляции я выбрал специализированный фреймворк Qiling, который построен на движке эмуляции Unicorn. Последний эмулирует разные процессоры, но при этом в нем нет никакого контекста операционной системы, то есть просто взять и запустить исполняемые файлы какой‑то ОС не получится.
При этом можно взять интересующий нас набор инструкций, определить для них состояние используемых регистров и участков памяти и выполнить нужный участок. Qiling же помогает исполнить бинарь целиком: он полноценно загружает файл в эмулятор и обрабатывает все вызовы API и сисколы. Так мы можем не заморачиваться всякими тонкостями вроде значений регистров. Если, конечно, исполнение без нашей помощи дойдет до нужного участка малвари, но на начальных этапах ветвление встречается редко, так что по этому поводу можно не переживать.
Правда, как ты увидишь дальше, слова «полноценно загружает» и «обрабатывает все вызовы API и сисколы» не совсем соответствуют действительности.
Переходим к практике. Напишем скрипт, который будет при помощи Qiling эмулировать малварь, получать результаты поиска по хешу и пытаться резолвить их, чтобы получить по возвращаемому адресу функции ее имя.
Перед тем как попытаться загрузить какой‑нибудь исполняемый файл винды в эмулятор, тебе нужно собрать системные либы и даже реестр с помощью скрипта из состава Qiling.
Для начала попробуем просто загрузить малварь в эмулятор:
from qiling import *from qiling.const import *ql = Qiling(["./examples/rootfs/x86_windows/163520.exe"],"./examples/rootfs/x86_windows")
163520.exe
— это название знакомого нам бинаря PikaBot.
А вот по этому пути хранятся наши системные либы для x86-версии и ветки реестра:
./examples/rootfs/x86_windows
Если не указывать значение параметра verbose
при объявлении объекта Qiling, ты увидишь вывод, который соответствует уровню QL_VERBOSE.DEFAULT
(можешь посмотреть другие варианты). В нем указывается, что удалось и не удалось загрузить из системных либ и функций или найти в них.
Теперь давай попробуем добавить хук на выход из функции sub_10012470
, чтобы отслеживать, какие адреса функций она возвращает. Также попробуем их резолвить, чтобы получить имена. Определим функцию хука:
def hook(ql: Qiling) -> None: if ql.arch.regs.read("EAX") in ql.loader.import_symbols: entry = ql.loader.import_symbols[ql.arch.regs.read("EAX")] print('[!] Founded API: ' + entry['name'].decode("utf-8"))
Источник: xakep.ru