Реверсим образец из “дикой природы”

April 21, 2025 — visilii

Редактировано: 2025-04-22

Сегодня изучим образец вредоносного ПО, найденный в естественной среде обитания - в глубинах Интернета, куда юзер порой спускается в поисках халявы.


Stage 1

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

Первая страница Google

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

Легальная активация Windows, Microsoft рвут на себе волосы

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

Архив обновлялся автором регулярно, если судить по дате

(Я уже написал на abuse@mega.nz; связанный аккаунт уже заблокирован)

Внутри архива - самораспаковывающийся SFX-архив. Virustotal реагирует на находящийся внутри KMS активатор. Но есть ли там что-то ещё?

Просто KMS…
…или нет?

Stage 2

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

На момент обнаружения - относительно немного детектов
YARA

Новости, упоминающие этот пакер, датируются ещё 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, сравнивающей ответ сервера с заданным значением.

Вот участок кода, который определяет соответствие полученного значения:

Нам необходима ветвь 0x00401fcd
Функция-обёртка в Ghidra

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

Шалость удалась

…и видим, что троян отправил следующий запрос!

Стоит отметить, что остановка внутри функции-обёртки происходит только в случае, когда длина сравниваемых строк равна - проверка на это происходит раньше и при желании её также можно перехватить, например, при помощи условной точки останова, но в целях экономии времени я немного “подыграл” вредоносу и сразу задал ответ длиной 1 символ.

Теперь нетрудно отыскать, какой ответ ожидает от нас ВПО и какой запрос совершает следом, и мы можем постепенно восстановить протокол общения с C2.

Можно ли было сделать это исключительно при помощи статического анализа? Да, несомненно. Однако это было бы куда более времязатратно - взгляните на рутину, внутри которой содержится основная логика трояна:

538 строк отборного псевдокода

Если учесть мой довольно скромный опыт анализа декомпилированного 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

Дополнительные материалы

badge: proud member of the 250KB Club badge: proud member of the darktheme.club
previous no ai webring siteno ai webringrandom no ai webring sitenext no ai webring site
Кнопки
Don't feed the AI! uBlock Origin Now! Edited with Vim I miss XP!
Piracy Now! Best viewed with Open Eyes built listening to Winamp CSS is difficult

Домен предоставлен FreeDNS