Не­дав­но мне попал­ся вре­донос семей­ства PikaBot, и при его изу­чении я нат­кнул­ся на ста­рый, но по‑преж­нему популяр­ный при­ем API Hashing. Он поз­воля­ет скры­вать воз­можнос­ти мал­вари как от средств защиты, так и от инс­тру­мен­тов ана­лиза. Сегод­ня мы при помощи эму­ляции поп­робу­ем авто­мати­зиро­вать выяв­ление фун­кций, вызыва­емых через API Hashing.

Так как API Hashing (а так­же дру­гие спо­собы сок­рытия вре­донос­ных фун­кций) вре­мя от вре­мени встре­чает­ся в моей прак­тике, мне захоте­лось най­ти более уни­вер­саль­ный спо­соб, чем прос­то каж­дый раз писать скрипт, который интер­пре­тиру­ет алго­ритм хеширо­вания име­ни API-фун­кции Win32 из вре­доно­са. Я подумал, что эму­лятор отлично подой­дет для быс­тро­го решения этой проб­лемы. Давай пос­мотрим, что из это­го выш­ло.

 

Кратко об API Hashing

Для начала вспом­ним, в чем имен­но зак­люча­ется тех­ника API Hashing. Вре­донос­ный код импорти­рует фун­кции из сис­темных биб­лиотек путем ана­лиза PE-заголов­ка, а точ­нее — таб­лицы экспор­та этих биб­лиотек.

Оп­ределить API Hashing мож­но, как раз отсле­живая поиск фун­кции по хешу от ее име­ни. То есть мал­варь переби­рает таб­лицу экспор­та сис­темных либ ntdll.dll или kernel32.dll, хеширу­ет имя каж­дой фун­кции и срав­нива­ет с хешем име­ни иско­мой фун­кции. В слу­чае сов­падения вре­донос чита­ет адрес нуж­ной фун­кции для даль­нейше­го исполь­зования.

Ал­горитм под­сче­та хеша может быть любой: нап­ример, час­то встре­чают­ся CRC-32 или MurmurHash2, а так­же любой кас­томный алго­ритм односто­рон­него пре­обра­зова­ния, в общем — лишь бы силь­но не наг­ружать цикл вычис­ления хеша.

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

 

Анализ образца

Мо­дуль, который при­меня­ет 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 он был получен. Как пра­вило, в таких слу­чаях для получе­ния иско­мого наз­вания фун­кции нуж­ны три ингре­диен­та:

  1. Спи­сок имен фун­кций kernel32.dll и ntdll.dll (мож­но добавить и дру­гие либы, но вряд ли они исполь­зуют­ся на этом эта­пе работы мал­вари).
  2. Ал­горитм хеширо­вания, реали­зован­ный на удоб­ном нам язы­ке (возь­мем любимый все­ми Python).
  3. Хеш иско­мой фун­кции.

Ре­цепт прос­той — переби­рать по спис­ку все име­на фун­кций, хешируя их по интер­пре­тиро­ван­ному алго­рит­му, пока не получим сов­падение с иско­мым хешем. Впро­чем, это толь­ко один из воз­можных спо­собов, все огра­ничи­вает­ся толь­ко тво­ей фан­тази­ей.

Так как наша цель — получить наз­вания всех хеширо­ван­ных фун­кций, не тра­тя вре­мя на интер­пре­тацию алго­рит­ма хеширо­вания, мы не будем пог­ружать­ся в ана­лиз механиз­ма получе­ния адре­сов сис­темных либ, перебо­ра их экспор­та и деталей алго­рит­ма хеширо­вания, а перей­дем сра­зу к исполь­зованию эму­ляции.

 

Применение эмуляции

В качес­тве средс­тва эму­ляции я выб­рал спе­циали­зиро­ван­ный фрей­мворк 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».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии