Исследование одного вредоноса

imageПопался мне недавно вредоносный 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

Код для расшифрования на Python
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

© Habrahabr.ru