Реверсим образец из “дикой природы”
Редактировано: 2025-04-22
Сегодня изучим образец вредоносного ПО, найденный в естественной среде обитания - в глубинах Интернета, куда юзер порой спускается в поисках халявы.
Stage 1
Мой обычный метод охоты на “дикие” образцы малвари - это запросы типа “читы кс2 скачать бесплатно”, “кряк офиса 2025” и тому подобное. К сожалению, попытки найти вредоносный кряк Windows в русскоязычном сегменте не увенчались успехом - несколько выбранных наобум ссылок с 7-9 страниц Google вели к, насколько можно судить, настоящим крякам, без дополнительной “начинки”. (Это не значит, что качать активаторы Windows по первой ссылке поисковика - хорошая идея!).

Впрочем, стоило мне набрать запрос на английском, как я тут же наткнулся на кучу случайных сайтов, предлагающих скачать “KMSpico Activator”. Судя по тому, что все эти сайты имели совершенно разные тематики, не относящиеся к KMS или Windows, а также что страницы для скачивания с них уже пропали (впрочем, их можно открыть через Веб-архив), могу предположить, что эти сайты были в определённый момент времени взломаны для распространения вредоноса.

Попробуем скачать файл. Нас перебрасывает на
freedropsoft.xyz
, оттуда - на тайпосквотнутый веб-сайт
medlafire.co
[sic!], а следом - на mega.nz
. На
Меге мы видим стандартную на сегодняшний день схему - запароленный архив
и рядом файл с паролем в виде года в названии.

(Я уже написал на abuse@mega.nz; связанный аккаунт уже заблокирован)
Внутри архива - самораспаковывающийся SFX-архив. Virustotal реагирует на находящийся внутри KMS активатор. Но есть ли там что-то ещё?


Stage 2
Рядом с KMS, на который реагируют антивирусы, лежит ещё один exe-файл
uniqq.exe
. Detect-it-Easy утверждает, что программа
написана на Delphi, и при помощи YARA-правил мы узнаём, что на деле это
файл, упакованный при помощи пакера на Delphi, известного как BobSoft
Mini Delphi.


Новости, упоминающие этот пакер, датируются ещё 2018-м годом, а сам пакер, скорее всего, ещё старше. Тем не менее, у DIE и PeID возникли трудности с его определением; также, мне не удалось найти ни одного распаковщика. Я попытался вскрыть файл вручную, однако вредонос успешно обходит мои точки останова (даже hardware’ные!) и создаёт и запускает файл в папке Temp, после чего завершается. Очень жаль, мне было бы очень интересно проследить за процессом расшифровки полезной нагрузки.
Resource Hacker показывает, что в ресурсах, по-видимому, и содержится полезная нагрузка:

После того, как дебаггер не дал никаких интересных результатов, я решил прибегнуть к базовому динамическому анализу - сдетонировать малварь и посмотреть, что будет.
Тут стоит упомянуть, что весь анализ я выполнял внутри виртуальной машины без доступа к Интернету. Не повторяйте дома! (вернее, не повторяйте без правильно настроенной ВМ)
Благодаря импортам я заведомо знал, что дроппер (скорее всего) не обращается к Интернету и не пишет в реестр, но всё же запустил inetsim и RegShot.

В целом, эта детонация лишь подтвердила - uniqq.exe
это
дроппер, который просто распаковывает и запускает следующую стадию.
Stage 3
Третья стадия выполняется при помощи легитимной подписанной программы
WinHex, на вход которой процесс uniqq.exe
подаёт
собственный исполняемый файл.
Именно WinHex в данном случае, насколько я могу судить, выполняет
основную полезную нагрузку из uniqq.exe
, который,
по-видимому, пользуется некой arbitrary code execution-уязвимостью. В
любом случае, после загрузки вредоносный код перезаписывает собой память
процесса WinHex.

Тут мне стоит признать свою ошибку, которая изначально повела меня по ложному следу - я не увидел аргумента командной строки при вызове WinHex, и поэтому посчитал, что имею дело с программой, маскирующейся под WinHex при помощи украденных ключей подписи. Спасибо Стефану Флейшманну из X-Ways за подтверждение, что хэш экзешника соответствует легитимной копии WinHex, а также Squiblydoo - за терпение после моей ложной тревоги об угнанном сертификате :)
Мораль - обдумывать каждое суждение и выбирать наиболее вероятное. Process hollowing встречается чаще, чем кража SSL-сертификатов.
Я сдампил процесс при помощи ProcessDump.
Взглянем на основной файл. Антивирусы его определяют, как принадлежащий семейству троянов Mikey:
В таблице импортов стандартная картина - криптография из
wincrypt
, проверка на дебаггер
IsDebuggerPresent
, связь с Интернетом.
Больше всего меня интересовал протокол связи с C2 - в какой-то момент я понял, что, скорее всего, именно из-за отсутствия подключения троян запустился в AnyRun, но не проявлял активности в моей виртуальной машине.
Я настроил виртуальную машину Windows 10 так, чтобы её было очень трудно обнаружить - только несколько проверок pafish срабатывали в ней - после чего настроил сеть на перенаправление всех сетевых запросов на другую машину с установленной Remnux.
Суть настройки следующая - Remnux VM и Win10 находятся в одной виртуальной сети. В настройках сети в Windows Remnux-машина указана в качестве шлюза и DNS-сервера. На Remnux VM я перенаправляю все входящие маршруты на loopback, перед анализом запускаю inetsim, а для симуляции ответов на HTTP-запросы трояна использую следующий скрипт:
Развернуть
# Adapted from:
# https://gist.github.com/sjb9774/eeabe6401b3c467d9489339e88dae9f0
import time
import datetime
import re
import socket
import select
def file2bytes(path):
with open(path, 'rb') as f:
contents = f.read()
return contents
def respond(url):
url_response_map = {
# regex: response
r"/success.*": b"1",
r"/info.*": b"0123456789a",
r"/update.*": b"1",
}
for reg, resp in url_response_map.items():
if re.search(reg, url):
return resp
return None
if __name__ == "__main__":
# Get socket file descriptor as a TCP socket using the IPv4 address family
listener_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Set some modes on the socket, not required but it's nice for our uses
listener_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
address_port = ("0.0.0.0", 80)
# reserve address and port
listener_socket.bind(address_port)
# listen for connections, a maximum of 1
listener_socket.listen(1)
print("Server listening @ 0.0.0.0:80")
# loop indefinitely to continuously check for new connections
while True:
# Poll the socket to see if there are any newly written data, note excess data dumped to "_" variables
read_ready_sockets, _, _ = select.select(
[listener_socket], # list of items we want to check for read-readiness (just our socket)
[], # list of items we want to check for write-readiness (not interested)
[], # list of items we want to check for "exceptional" conditions (also not interested)
0.2 # prevent high CPU usage
)
# if a value was returned here then we have a connection to read from
if read_ready_sockets:
# select.select() returns a list of readable objects, so we'll iterate, but we only expect a single item
for ready_socket in read_ready_sockets:
# accept the connection from the client and get its socket object and address
client_socket, client_address = ready_socket.accept()
# read up to 4096 bytes of data from the client socket
client_msg = client_socket.recv(4096)
print(f"[{datetime.datetime.now().time()}] Client said: {client_msg.decode('utf-8')}")
# Send a response to the client, notice it is a byte string
if resp := respond(client_msg.decode('utf-8')):
client_socket.sendall(resp)
else:
client_socket.sendall(
bytes(f"""HTTP/1.1 200 OK\r\nContent-type: text/html\r\nSet-Cookie: ServerName=steveserver\r
\r\n
<!doctype html>
<html>
<head/>
<body>
<h1>Welcome to the server!</h1>
<h2>Server address: {address_port[0]}:{address_port[1]}</h2>
<h3>You're connected through address: {client_address[0]}:{client_address[1]}</h3>
<body>
<pre>{client_msg.decode("utf-8")}<pre>
</body>
</body>
</html>
\r\n\r\n
""", "utf-8")
)
try:
# close the connection
client_socket.close()
except OSError:
# client disconnected first, nothing to do
pass
В функции respond
я могу указать
любой эндпоинт при помощи регулярного выражения и задать любой
ответ.
Думаю, в будущем я напишу пост, где опишу весь процесс настройки такой системы виртуальных машин, а пока просто оставлю полезные ссылки в конце этого поста.
Сделаю вид, что не видел общения трояна с C2 в AnyRun, и оставлю в
respond
только один эндпоинт .*
; попробуем
зареверсить протокол с нуля:
= {
url_response_map r"/.*": b"3",
}
Запускаем вредонос и видим, как каждую секунду нам сыпятся GET-запросы:
Запоминаем эндпоинт и ищем ответственный код при помощи Ghidra и
x64dbg. Моя методология заключается в следующем - присоединяем дебаггер
к процессу трояна и ищем интересующий нас код, сверяя его с
дизассемблированным кодом в Ghidra. В данном случае нетрудно найти код,
отвечающий за получение ответа по сети (InternetReadFile, считывающий
полученный ответ в буфер), после чего поставить аппаратную точку
останова на участке памяти, куда считывается ответ нашего самопального
C2-сервера. Так как вредонос написан на C++ и использует
std::string
, то происходит ещё одно копирование строки в
другой участок памяти (который мы тоже начинаем отслеживать), после чего
мы останавливаемся внутри обёртки над memcmp
, сравнивающей
ответ сервера с заданным значением.
Вот участок кода, который определяет соответствие полученного значения:


Изменим нулевой флаг, чтобы отменить условный прыжок и вернуть 1…

…и видим, что троян отправил следующий запрос!
Стоит отметить, что остановка внутри функции-обёртки происходит только в случае, когда длина сравниваемых строк равна - проверка на это происходит раньше и при желании её также можно перехватить, например, при помощи условной точки останова, но в целях экономии времени я немного “подыграл” вредоносу и сразу задал ответ длиной 1 символ.
Теперь нетрудно отыскать, какой ответ ожидает от нас ВПО и какой запрос совершает следом, и мы можем постепенно восстановить протокол общения с C2.
Можно ли было сделать это исключительно при помощи статического анализа? Да, несомненно. Однако это было бы куда более времязатратно - взгляните на рутину, внутри которой содержится основная логика трояна:

Если учесть мой довольно скромный опыт анализа декомпилированного C++, который добавляет кучу собственных рутин, никак не связанных с основной логикой самой программы, это заняло бы огромное количество времени. Совмещение динамического и статического анализа позволяет срезать углы и получить необходимый результат заметно быстрее.
Общий протокол взаимодействия трояна с C2 в итоге выглядит примерно так:
Троян отправляет на заданный при компиляции IP 185[.]156[.]73.98 GET-запрос
/success?substr=two&s=uniq&sub=none
;- В ответ сервер отправляет 1 либо 0. При получении 1 взаимодействие продолжается;
Троян затем отправляет GET-запрос
/info
, ожидая получить ответ длиной от 10 до 100 байт;- Моя догадка - это ключ для расшифровки полученных в следующем этапе данных;
После запроса по URI
/update
троян скачивает с C2 двоичный файл, который, по всей видимости, является следующим этапом заражения. Что происходит дальше - хорошо видно в AnyRun.
Индикаторы компрометации (IoC)
Хэши (SHA1)
7d0fa445c5a71dc6134e8a80d7c18f84e77c34b3
- KMS.exe (stage 1)87e93e430f97bcfd2915678129663cee279492d2
- uniqq.exe (stage 2)
Процессы
uniqq.exe
svchost015.exe
Сетевые подключения
IP:
185.156.73.98
- C2
URI:
/success?substr=two&s=uniq&sub=none
User-agent:
1