В первой части статьи мы рассмотрели общий вид работы конвейера виртуальной машины, а также немного коснулись возможных подходов для анализа виртуальной машины.
Следуя описанной в конце первой части последовательности мер, начну описывать, что я пытался делать на каждом из этапов, с чем возникли сложности, какие из них я решил обойти и что в итоге получилось.
Напомню, что в этой статье не приводится гарантированного способа снятия виртуализации, я просто хочу поделиться опытом анализа ВМ который позволяет более-менее понять что-то о работе ВМ и может быть полезен при анализе схожих реализаций.
Применение Triton
Так как о том, что использование символьного представления для снятия обфускации обработчиков иснтрукций ВМ с помощью Miasm (как это описано в статье ESET ) мне не помогло, так как мешались различные переходы и нужно было снимать трассу каждого обработчика, чтобы передать его на анализ, я решил попробовать избавиться от обфускации с помощью Triton.
Напомню, что на этом этапе анализа мне хотелось найти наиболее быстрый и доступный способ проанализировать обфусцированные обработчики виртуальных инструкций, чтобы описать (дизассемблировать) байткод ВМ и, таким образом, понять заложенный в виртуализованный участок кода функционал.
Я начал смотреть, с какой стороны лучше попробовать применить Tenet и наткнулся на любимую директорию examples, а именно на пример dead_store_elimination. В этом примере в качестве исходного обфусцированного кода даже приводится фрагмент с обфускацией VMProtect, очень здорово.
Не изобретая велосипед, я решил воспользоваться указанным примером, в котором, по сути, все сводится к применению метода simplify из TritonContext. Сразу вспоминается это:
При этом возникает такая же проблема, как и с Miasm — нужно как-то получить полный листинг обработчика инструкции ВМ. Наиболее доступным способом мне показалось воспользоваться трассой, снятой с исполнения кода обработчика. На тот момент у меня была трасса, снятая для инструмента Tenet (это инструмент для анализа трассы, о нем я расскажу при описании следующего этапа). К сожалению, в этой трассе нет самих дизассемблированных команд внутри обработчика, но были необходимые eip. Трасса для Tenet в виде текста выглядит так:
Таким образом, у нас есть значения eip (то есть адреса каждой выполненной в ходе работы анализируемого ПО инструкции). Если мы хотим получить трассу конкретного обработчика ВМ, очевидно, что нам надо читать значения в eip c того места, где eip равен адресу обработчика инструкции ВМ. Массив с адресами обработчиков мы нашли в начале первой части статьи. В анализируемом мною образце есть обработчик, который расположен по адресу 0x50260c. В качестве последней инструкции трассы работы обработчика возьмем инструкцию, предшествующую MAIN_LOOP (упоминается в первой части статьи, в моем случае MAIN_LOOP расположен по адресу 0x50281a). Таким образом, из трассы Tenet извлекаем такой участок:
Участок
esp=0x2f1f51c,eip=0x50260c,mr=0x2f1f4f0:0c265000// начало трассы обработчика 0x50260c edx=0x502621,eax=0x2d,eip=0x50260f eip=0x502610 eax=0xf4,eip=0x502612,mr=0x4ccbdf:f4 edx=0x502608,eip=0x502615 eax=0xc2,eip=0x502617 esp=0x2f1f518,eip=0x502618,mw=0x2f1f518:82020000 esp=0x2f1f514,eip=0x50261d,mw=0x2f1f514:a2a1d06c eip=0x503a1b esi=0x4ccbe0,eip=0x503a1c edx=0x5000c2,eip=0x503a20 edx=0x500000,eip=0x503a23 eax=0xc3,eip=0x503a25 eip=0x503a26 eax=0x3d,eip=0x503a28 edx=0x5000ff,eip=0x503a2a esp=0x2f1f510,eip=0x503a2e,mr=0x2f1f518:82020000,mw=0x2f1f510:82020000 esp=0x2f1f4f0,eip=0x503a2f,mw=0x2f1f4f0:1cf5f102e0cb4c00e0f5f10210f5f10236fe7f08ff0050002ac44c003d000000 edx=0x5000fe,eip=0x503a31 eax=0x3c,eip=0x503a33 edx=0x50c4fe,eip=0x503a35 esp=0x2f1f4ec,eip=0x5030c5,mw=0x2f1f4ec:3a3a5000 edx=0x5031fe,eip=0x5030c7 edx=0x500001,eip=0x5030cb ebx=0x87ffe0a,eip=0x5030cd eip=0x5030cf edx=0x500002,eip=0x5030d0 edx=0x500200,eip=0x5030d4 edx=0xf7ccccd8,eip=0x5030d7,mr=0x2f1f5e0:d8ccccf7 esp=0x2f1f4e8,eip=0x5030da,mr=0x2f1f4ec:3a3a5000,mw=0x2f1f4e8:3a3a5000 esp=0x2f1f4e4,eip=0x5034f9,mw=0x2f1f4e4:df305000 eip=0x5034fa eip=0x5034fe ebp=0x2f1f5e4,eip=0x503501 esp=0x2f1f4e0,eip=0x502759,mw=0x2f1f4e0:06355000 esp=0x2f1f4dc,eip=0x50275a,mw=0x2f1f4dc:06020000 eip=0x502760,mw=0x2f1f4dc:d610 eip=0x502763,mw=0x2f1f558:d8ccccf7 esp=0x2f1f4d8,eip=0x502764,mw=0x2f1f4d8:d8ccccf7 eip=0x502767,mw=0x2f1f4d8:d8ccccf7 esp=0x2f1f4d4,eip=0x50276c,mw=0x2f1f4d4:3d582af5 eip=0x502770,mw=0x2f1f4d4:d4f4 esp=0x2f1f51c,eip=0x502774// конец трассы обработчика 0x50260c eip=0x50281a// eip указывает на MAIN_LOOP
Чтобы получить выполненные машинные команды в виде массива из последовательностей байт (так как пример Triton по упрощению кода работает именно с таким форматом), воспользуемся IDAPy:
Скриптик
import idautils import idaapi import idc def GetInsnLen(ea): insn = ida_ua.insn_t() inslen = ida_ua.decode_insn(insn, ea) if inslen: return inslen return 0 eips = '''0x50260c 0x50260f 0x502610 0x502612 0x502615 0x502617 0x502618 0x50261d 0x503a1b 0x503a1c 0x503a20 0x503a23 0x503a25 0x503a26 0x503a28 0x503a2a 0x503a2e 0x503a2f 0x503a31 0x503a33 0x503a35 0x5030c5 0x5030c7 0x5030cb 0x5030cd 0x5030cf 0x5030d0 0x5030d4 0x5030d7 0x5030da 0x5034f9 0x5034fa 0x5034fe 0x503501 0x502759 0x50275a 0x502760 0x502763 0x502764 0x502767 0x50276c 0x502770 0x502774''' eips_list = eips.split('\n') for i in range(len(eips_list)): eips_list[i] = int(eips_list[i], 16) print(idc.get_bytes(eips_list[i], GetInsnLen(eips_list[i])))
На выходе мы получаем каждую команду из трассы обработчика инструкции ВМ, чтобы далее передать эти команды в скрипт Triton. При этом все инструкции перехода (JMP, JZ, JNE и т.д.) нужно удалить, а инструкции вызова (CALL) заменить на PUSH (как будто на стек кладется адрес возврата).
Результат далёк от успеха
Видно, что попытка удаления мусорного кода не привела к желаемому результату, как минимум остались инструкции работы со стеком, которые больше выглядят как мусорные, на это также указывает инструкция LEA, которая, как видно, очищает стек, загружая сохраненный указатель.
Несмотря на то что чистую и доступную версию обработчика инструкции ВМ мы все-таки не получили, все равно у нас теперь есть листинг этого обработчика, можно попытаться посмотреть, что же в нем происходит (сделаем это на немного “упрощенной” версии, не зря же мы использовали скрипт из примеров Triton ?).
Напомню, что в первой части статьи мы узнали, что esi является VM_EIP, то есть указателем на байт в байткоде. Акцентируя внимание на ключевых моментах листинга можно заметить, что в AL загружается значение из [ESI] (1), далее с ним происходят какие-то преобразования (2), спустя ряд преобразований EAX используется как смещение (3), по которому записывается значение, полученное из [EBP] (4), при этом EBP также увеличивается на 4 (5):
Листинг
0x50260c: xadd al, dl 0x50260f: mov al, byte ptr [esi]// 1 0x502611: shl dl, 3 0x502614: xor al, bl// 2 0x502616: pushfd 0x502617: push 0x6cd0a1a2 0x50261c: pushal 0x50261d: movzx dx, al 0x502621: sete dl 0x502624: inc al 0x502626: neg al 0x502628: dec dl 0x50262a: push dword ptr [esp + 4] 0x50262e: pushal 0x50262f: dec al 0x502631: push dword ptr [esp + 4] 0x502635: push 0x6cd0a1a2 0x50263a: xor bl, al 0x50263c: mov edx, dword ptr [ebp]// 4 0x50263f: push dword ptr [esp] 0x502642: push dword ptr [esp + 4] 0x502646: push 0x6cd0a1a2 0x50264b: add ebp, 4// 5 0x50264e: push dword ptr [esp + 4] 0x502652: push 0x6cd0a1a2 0x502657: pushfd 0x502658: mov word ptr [esp], 0x10d6 0x50265e: mov dword ptr [eax + edi], edx// 3 0x502661: push edx 0x502662: mov dword ptr [esp], edx 0x502665: push 0xf52a583d 0x50266a: mov word ptr [esp], sp 0x50266e: lea esp, [esp + 0x48]
Посмотрев на указанные участки, можно предположить, что в указанном обработчике читается один операнд размером 1 байт, этот операнд используется как указатель на виртуальном стеке (VM_ESP), по которому кладется значение из [EBP]. Инструкцию можно записать примерно так:
MOV VM_EIP:OP1, [EBP]
Таким образом, примерно стали ясны ключевые компоненты, применяемые при работе обработчика инструкции ВМ. Теперь стало понятнее, что именно нужно искать, чтобы понять, что делают остальные обработчики, поэтому я решил не пытаться с помощью Triton до конца снять обфускацию, а воспользоваться ручным анализом работы каждого обработчика, используя Tenet.
Анализ трассы с помощью Tenet и без помощи Tenet
Как уже было сказано выше, Tenet это инструмент для анализа трассы с помощью IDA. В репозитории подробно описано, как снимать трассу анализируемого ПО, я использовал Intel Pin, для него уже собран необходимый модуль.
В результате снятия трассы в нужном формате и передачи ее плагину Tenet мы получаем очень удобную возможность перемещаться в оба направления по трассе, внимательно отслеживая интересующие нас изменения:
Теперь, после того как мы немного разобрались в работе обработчиков инструкций ВМ, а также получили возможность удобно анализировать трассу, можно приступить к анализу каждого обработчика, чтобы разобраться в выполняемом байткоде.
Предварительно я также записал все выполненные инструкции виртуальной машины —
то есть снял трассу ВМ. Это можно сделать с помощью отладчика x64dbg или другими способами (например, тем же Pin). Если делать это через x64dbg, то нужно не забыть установить плагин ScyllaHide и выбрать профиль защиты от VMProtect. Это можно сделать, установив точку останова инструкции RET, которая совершает переход с обработчику инструкции ВМ (об этом подходе рассказывалось в первой части), и добавив запись в лог значения, лежащего на стеке (адрес обработчика очередной команды из байткода ВМ):
Оглядываясь назад я бы еще добавил запись в лог значение ESI, так как это VM_EIP и с ним удобнее смотреть перемещения по байткоду, хотя понять, что происходит, получилось и так.
В итоге мы получим трассу ВМ, адреса в которой необходимо перевернуть (преобразовать из-за Little Endian):
Конечно, нам также нужно получить количество уникальных адресов, т.е. количество всех обработчиков, используемых в трассе, чтобы потом, например, отмечать, какие мы проанализировали и что именно в них происходит. Получилось 31, не очень много.
Постепенно анализируя обработчики и разбираясь, что в них происходит, мы начинаем описывать трассу, приводя к подобному виду:
Конечно, хотелось бы еще увидеть конкретные значения операндов, но для этого нужно было реализовать декодирование, которое происходит в каждом обработчике, поэтому я решил пока посмотреть трассу в представленном виде.
В ходе анализа обработчиков инструкций ВМ мне наиболее интересными мне показались инструкции, интерпретирующие запись в память, вызовы и переходы, а также реализация подсчета контрольной суммы.
Запись в память в моем примере реализовывается обработчиком 0x502b1c, вот пример участка трассы, где запись реализовывается 4 раза подряд:
Если посмотреть внутрь обработчика, то можно увидеть, что сама запись в память происходит при выполнении MOV [EAX], EDX. Значения обоих операндов берутся из [EBP+0] и [EBP+4] соответственно, которые заполняются на предыдущих командах ВМ (видно на трассе).
Tenet позволяет нам полистать каждое выполнение инструкции записи, поэтому мы можем увидеть, что же именно записывается. Например, в примере ниже после четырех вызовов записалась строка “kernel32.dll”:
Таким же образом записываются другие строки с названиями функций, например, CreateFileA или MapViewOfFile. Где-то ранее они, очевидно, расшифровываются. Так мы уже понимаем, какие функции WinAPI вредонос будет использовать и для чего.
Функция перехода характеризуется просто записью в ESI значения из [EBP+0], по которому также происходит запись при выполнении инструкций ВМ ранее. Переходы помогают нам найти циклы, то есть какие-то повторяющиеся операции вроде проверок, записей, расшифровки и т.д.:
Подсчет контрольной суммы происходит для выбранного участка памяти в цикле внутри обработчика инструкции ВМ с помощью арифметических операций (например, XOR или SHL, в анализируемом мною образце это обработчик по адресу 0x50288e).
Если внимательнее посмотреть на то, какие функции WinAPI вызываются и для чего используется проверка целостности, то становится ясно, что вредонос осуществляет мапинг своего образа в память, а затем сравнивает контрольные суммы участков образа с содержимым собственных секций, чтобы установить наличие точек останова или других изменений.
При успешном завершении проверки осуществляется запись в секцию .text распакованного участка вредоноса, реализующего основной функционал. Это можно увидеть, проанализировав, как вызывались инструкции ВМ по записи в память. На скриншоте видно, что между первыми инструкциями ВМ по записи (в которых была работа со строками с названиями функций) и последующими инструкциями ВМ по записи есть разрыв в примерно в 28 тысяч инструкций ВМ:
То есть в этом разрыве проводилась проверка и распаковка кода с основным функционалом, после чего началась запись распакованного кода в секции .text и .data (инструкции записи теперь вызываются интервалом в 4-5 шагов и в значительно бОльшем объеме).
Так, изучив функционал каждого (или большинства) обработчиков инструкций ВМ, мы разобрались в том, что делает виртуализованный код — проводит мапинг собственного образа, проверяет его целостность, распаковывает основной участок. Если снять дамп после распаковки, можно увидеть понятные строки как признак того, что распаковка прошла (в частности, имена функций, указывающих на работу с ядром):
Также заметно, что само дизассемблированное и декомпилированное представления в районе OEP и далее стали гораздо более читаемыми:
Теперь можно продолжить анализ исследуемого образца.
Заключение
Вы будете смеяться, но получается, что в данном случае достаточно было просто поставить точку останова на выполнение на секции кода и включить профиль VMProtect в ScyllaHide, чтобы получить распакованную версию ?. Но целью анализа было разобраться именно в работе виртуальной машины, чтобы понимать, как можно подходить к исследованию образцов, защищенных с помощью ВМ более сложным образом (при виртуализации основного функционала). К тому же возможно, что некоторые функции распакованного кода остались виртуализованы.
Таким образом, при анализе работы ВМ мы использовали понемногу несколько инструментов, при этом если постараться, то скорее всего можно было бы решить задачу только одним из них.
ссылка на оригинал статьи https://habr.com/ru/articles/835768/
Добавить комментарий