Исследование одного вредоноса
Попался мне недавно вредоносный doc файл, который рассылали с фишинговыми письмами. Я решил, что это неплохой повод поупражняться в реверс-инжиниринге и написать что-то новое на Хабр. Под катом — пример разбора вредоносного макроса-дроппера и не менее вредоносных dll.
Макрос
Sha256 от файла — abb052d9b0660f90bcf5fc394db0e16d6edd29f41224f8053ed90a4b8ef1b519. В самом doc файле на первой странице находится картинка, сообщающая что этот файл защищён и объясняющая как включить макросы, ещё в файле есть две большие таблицы с числами. Числа записаны в десятичной форме, самые длинные — десятизначные, есть и положительные, и отрицательные.
Когда жертва разрешает исполнение макросов (есть те, у кого оно по умолчанию разрешено?), запускается цепочка действий, которая в конце концов выполняет функцию updatedb_network, которая изменяет текущую директорию на временную и создаёт в ней файл «icutils.dll», в который подряд записывает числа из первой таблицы как 32 битные integer со знаком. В резльтате получается корректная dll. Из этой dll импортируется функция clone:
Declare PtrSafe Function clone Lib "icutils.dll" _
(ByVal Saved As String, ByVal Time As Integer) As Boolean
И запускается с двумя параметрами:
R1 = Module1.clone("Cream", 0)
Если вызов clone возвращает False, то файл «icutils.dll» перезаписывается данными из второй таблицы и снова вызывается clone с такими же параметрами.
Забегая вперёд скажу, что первая dll 64 битная и не будет выполняться на 32 битных системах. Таким образом макрос подбирает правильную архитектуру бинарного кода.
Что интересно, в функции updatedb_network есть такой кусок кода, который никакого функционального назначения не имеет:
Sub updatedb_network()
...
Dim query_to_change As Variant
Set query_to_change = CurrentDb.QueryDefs("query_name")
query_to_change.SQL = "SELECT * FROM Table ORDER BY ID Asc"
query_to_change.SQL = "SELECT Field1, Field2 FROM Table ORDER BY ID Asc"
query_to_change.SQL = "SELECT Field1, Field2 FROM Table WHERE Field LIKE Fashion"
query_to_change.SQL = "SELECT Field1, Field2 FROM Table WHERE Field LIKE '" & something & "'"
...
End Sub
Возможно он тут для придания видимости полезной работы для тех, кто быстро пролистает код, увидит какие-то строки на SQL и подумает, что всё ОК? Не знаю. Так же большинство функций и переменных имеют случайные или не относящиеся к реальному назначению имена (как, например, updatedb_network, которая ни с БД, ни с сетью не взаимодействует). Хотя есть, например функция dump_payload, которая сохраняет 4 байта в icutil.dll. Но в любом случае, сразу должно насторожить наличие функции Document_Open, её произвольно переименовать авторы ВПО не могут (правда вместо неё могут использовать другую автоматически запускаемую функцию).
Итак, функционал макроса более-менее понятен, пора выгружать dll и переходить к их анализу.
Первая dll
Первая dll (sha256 7427cc4b6b659b89552bf727aed332043f4551ca2ee2126cca75fbe1ab8bf114) 64 битная.
В списке импортируемых функций есть функции CreateProcessW (запуск программы), CreateRemoteThread (создание потока в другом процессе), VirtualAllocEx (выделение блока памяти в другом процессе), WriteProcessMemory (запись в память другого процесса), что сразу наводит на мысли об инъекции кода в другой процесс. Теперь посмотрим что именно она делает с помощью IDA Free и Ghidra.
Основная точка входа просто возвращает 1, ничего больше не делает. Вторая экспортируемая функция — clone, именно она вызывается макросом и содержит вредоносный код.
Параметры, с которыми она вызывается, вроде ни на что не влияют. Приложение расшифровывает два блока данных. Первый блок данных длиной 0×78 со следующим содержимым (уже расшифрованный):
https://pastebin.com/raw/Jyujxy7z\x00\x00\x00\x00\x00\x00\x00\xf2i\xe0\x1d\x95h\xbc\x03\xe4#\xe0\x1d<\x04\xe0\x1d\xe6\x00\xde\x01\xa4\x17\xbc\x03x\x01\xe0\x1d\xe2\x16x\x07Qy\xbc\x03@Fx\x07Df\xbc\x03\x89a\xde\x01q\x11\xe0\x1d|Ix\x07D@\xbc\x03\x8a\x01\xde\x01^9\xde\x01\xf2i\xe0\x1d\x95h\xbc\x03\xe4#\xe0\x1d\xab
Второй блок данных имеет длину 0×1D4C и содержит исполняемый код.
Кроме того в структуру длиной 0×90 байт записывается указатель на модуль kernel32, результат выполнения функции GetTickCount() и адрес функции ZwDelayExecution() из kernel32.
После чего создаётся процесс (CreateProcessW) «cmd.exe». С помощью VirtualAllocEx в нём выделяются два буфера: с разрешениями RW длиной 0×108 и с разрешениями RWX длиной 0×1D4C. В RW буфер копируется приведённый выше блок с данными и вышеупомянутая структура длиной 0×90. В структуру так же записывается указатель на расшифрованный блок данных (в адресном пространстве дочернего процесса (cmd.exe)). В RWX буфер копируется (WriteProcessMemory) расшифрованный блок данных с кодом.
Потом в процессе cmd.exe создаётся поток (CreateRemoteThread) с точкой входа в начале RWX буфера, в качестве аргумента передаётся указатель на RW буфер. На этом функция clone завершается, действие продолжается в процессе cmd.exe.
Интересно, что в функции clone есть вроде как недостижимый кусок кода, который импортирует (LoadLibraryW) библиотеку »WorkPolyhistor».
Инжектированный в cmd.exe код
Он выполняет следующие действия:
- находит адреса нужных функций из kernel32.dll (её адрес получается от родительского процесса)
- загружает библиотеки ntdll, Ole32, User32
- находит адреса нужных функций в этих библиотеках.
Интересно, что для бОльшей части функций в коде нет имени функции, а только CRC32 от имени (перебираются все имена функций из загруженной библиотеки, пока не найдётся функция с нужным CRC32 от имени). Возможно это защита от получения списка импортируемых функций утилитой strings, правда странно, что код, который хранится в зашифрованном виде имеет такую защиту, в то время как сама dll импортирует функции просто по именам. Всего обнаруживаются следующие функции:
kernel32:
- GetProcAddress
- LoadLibrary
- GlobalAlloc
- GetTempPath
- GetFileAttributesW
- CreateProcessW
- GlobalFree
- GlobalRealloc
- WriteFile
- CreateFileW (находится по имени)
- WriteFile
- CloseHandle
- GetTickCount
- ReadFile
- GetFileSize
ntdll:
- RtlCreateUnicodeStringFromAsciiz
- ZwDelayExecution
- ZwTerminateProcess
- swprintf
ole32:
- CoInitialize (находится по имени)
- CoCreateInstance (находится по имени)
msvcrt:
- rand
- srand
Далее процесс с помощью GetTempPath получает путь к временной директории, создаёт в ней файл с названием вида 26342235.dat, где имя файла — десятичная запись TickCount, полученного от родительского процесса и итерируется на каждой новой попытке (т.е. если не удалось скачать пэйлоад с первой попытки, то на вторую попытку будет создан файл с именем 26342236.dat). После этого загружается библиотека wininet и в ней находятся указатели на следующие функции:
- InternetCloseHandle (crc32: 0xe5191d24)
- InternetGetConnectedState (crc32: 0xf2e5fc0c)
- InternetOpenA (crc32: 0xda16a83d)
- InternetOpenUrlA (crc32: 0×16505e0)
- InternetReadFile (crc32: 0×6cc098f5)
С помощью InternetGetConnectedState проверяется есть ли сеть, если нет — приложение вызывает функцию по несуществующему адресу и падает (такая защита от определения адреса откуда получается пэйлоад с помощью изолированной от сети машины. Это единственный случай, когда приложение завершается нештатно, в остальных — делаются 3 попытки, после чего cmd.exe завершается с помощью ZwTerminateProcess). Если сеть есть, то с помощью найденный функций пэйлоад скачивается с переданного из родительского процесса URL (https://pastebin.com/raw/Jyujxy7z) и сохраняется в созданный ранее файл с расширением .dat.
Далее пэйлоад считывается из .dat файла, декодируется (base64), расшифровывается с помощью XOR с CRC32 от URL, проверятеся, что первые 2 байта расшифрованных данных — 'MZ', если да — результат сохраняется в файл с таким же именем, но расширением .exe
from binascii import crc32
from base64 import b64decode
def decrypt_payload(payload_b64: bytes, url: str):
payload_bin = b64decode(payload_b64.decode())
key = str(crc32(url.encode())).encode()
decrypted = bytearray()
for i, b in enumerate(payload_bin):
decrypted.append(b ^ key[i % len(key)])
return bytes(decrypted)
С помощью функции CreateProcessW сохранённый файл запускается. Если всё удачно — процесс cmd.exe завершается с помощью ZwTerminateProcess. Если что-то пошло не так (кроме отсутствия сети), то всё повторяется заново, максимум делаются 3 попытки, имена dat и exe файлов каждый раз увеличиваются на 1.
Вторая dll
Вторая dll (sha256 006200fcd7cd1e71be6c2ee029c767e3a28f480613e077bf15fadb60a88fdfca) 32 битная.
В ней основной вредоносный функционал реализован в функции clone. Она тоже расшифровывает 2 буфера. Первый буфер имеет размер 0×78 (120) байт, в него расшифровываются и записываются такие данные (в расшифрованном виде):
https://pastebin.com/raw/Jyujxy7z\x00\x00\x00\x00\x00\x00\x00\x1e(\xf0\x0e\xc5r\xc0;\x12)\xc0;Jr\xc0;Y4\xbc\x03/Mx\x07\x038\xde\x01\x9e\x05\xe0\x1d#\x08\xbc\x03\xeeU\xf0\x0e\x18{x\x078\x1a\xf0\x0e\xccg\xf0\x0eze\xde\x01\x89&\xe0\x1d\xf6\x1f\xe0\x1d
Видно, что в начале находится такой же URL, как и в x64 версии.
Второй буфер размером 0×4678 байт выделяется с RWX разрешениями. В него расшифровывается код, после чего из него вызывается функция со смещением 0×4639 от начала буфера.
Подробно разбирать этот код я не стал. Он так же находит функции по CRC32, запускает notepad.exe, инжектирует туда код, который скачивает пэйлоад с того же URL на pastebin.
Пэйлоад с pastebin
Расшифрованный пэйлоад с pastebin — это 32 битный exe файл (sha256 9509111de52db3d9a6c06aa2068e14e0128b31e9e45ada7a8936a35ddfaf155f) Подробно разбирать его я пока не стал в силу нехватки времени.
Заключение
В целом вредонос на меня произвёл впечатление довольно плохо написанного, с большим количеством ошибок (не называю их здесь намеренно). Как будто писали на скорую руку.
dll находит в памяти функции, которые потом нигде не используются. Наверно этот код, так же как макрос, регулярно переписывается злоумышленниками, каждый раз когда он начинает детектироваться антивирусами, в результате чего в коде остаются такие артефакты.
Так же интересно, что «роняемой» на диск x64 версии dll имена «опасных» импортируемых функций ничем не замаскированы (можно хоть strings их увидеть), а в коде, который расшифровывается в памяти и на диск не ложится, они находятся по CRC32 от имени, а не просто по именам.
Пэйлоад с pastebin через несколько дней был удалён.
P.S. КДПВ взята отсюда twitter.com/BroadAnalysis/status/897254224475631616