Бэкдор PlugX, хорошо известный многим, нацелен на хищение конфиденциальной информации. В 2022 году в рамках одного из расследований наша команда экспертов обнаружила новый сэмпл этого вредоноса. Его главное отличие состояло в том, что он использовался для продвижения в локальной сети заказчика, а не для связи с управляющим сервером. В этом материале мы рассказываем, чем еще интересна новая версия PlugX в сравнении с предыдущей.
Загрузка
Сэмпл состоит из двух файлов:
- dbgeng.dll
- desktop.ini
Файл dbgeng.dll выступает в роли загрузчика полезной нагрузки, которая содержится в desktop.ini. Было обнаружено несколько вариантов dbgeng.dll, но все они имеют одинаковую функциональность: считать файл и передать ему управление. Однако некоторые из них серьезно обфусцированы.
Итак, dbgeng.dll открывает файл С:\ProgramData\Microsoft\DeviceSync\desktop.ini, считывает оттуда данные и передает управление на четвертый байт.
Файл desktop.ini содержит:
- зашифрованную и запакованную полезную нагрузку;
- шелл-код, который расшифровывает ее и передает ей управление.
По смещению 6 от начала файла desktop.ini находится переход к функции расшифровки:
Следом после инструкции перехода по смещению 11 находится структура, которая содержит ключ для расшифровки, размер запакованных и распакованных данных:
Алгоритм расшифровки самописный (код ниже). Алгоритм сжатия – LZNT1.
def __decrypt (buffer, key):
res = []
for i in buffer:
key += 1
res.append((key + (key ^ ((i + (0x100 - key) & 0xff))) & 0xff))
return bytes(res)
В результате распаковки полезной нагрузки в памяти оказывается полноценный PE-файл, где нулевыми байтами затерты:
- dos-заголовок (за исключением указателя на PE-заголовок по смещению 0x3c);
- dos-stub;
- сигнатура PE-заголовка.
Далее PE-файл загружается в память и управление передается на entrypoint выгруженного исполняемого файла.
Работа распакованного файла
Вначале идет попытка установки привилегий SeDebugPrivilege и SeTcbPrivilege. Затем – проверка, в каком контексте запущен исполняемый файл. Если он работает не в контексте msiexec.exe или winlogon.exe, то вредонос запускает процесс msiexec.exe и внедряется в него. Основная нагрузка запускается, если вредоносный код загружен в контексте msiexec.exe. Winlogon.exe используется для работы плагинов от аутентифицированных пользователей. Далее рассмотрены варианты работы данного вредоносного файла в контексте winlogon.exe и msiexec.exe.
Работа в контексте процесса msiexec.exe
На этапе инициализации, вредоносный файл запускает следующие потоки:
- Поток для обработки UDP и установки входящих соединений.
- Поток для мониторинга аутентифицированных сессий. Если находит, то внедряет себя в процесс winlogon.exe.
Также на данном этапе производится проверка ключа реестра "SOFTWARE\Clients\Mail", где хранится случайное значение и инициализируются плагины. Описание плагинов приведем ниже в отдельном разделе.
Далее идет запуск потоков-обработчиков команд, приходящих извне. Количество потоков зависит от количества следующих вшитых структур:
struct port_struct
{
MW_SOCK_TYPE type;
char gap;
__int16 port;
};
enum MW_SOCK_TYPE, width 1 byte
{
TCP = 1,
UDP = 2
};
В данном сэмпле присутствуют две структуры со значениями:
- Тип сокета: TCP, порт: 8081
- Тип сокета: UDP, порт: 5356
На каждую такую структуру запускается поток обработки приема соединения. Разницы в функционале между типами сокетов нет. При поступлении нового соединения запускается поток обработки команд. Данные, приходящие от интерфейса-коннектора, обрабатываются одинаково для каждого из двух типов сокетов. Чтобы это стало возможным, для каждого типа сокета (TCP, UDP) реализован свой класс-коннектор с одинаковым интерфейсом. Ниже подробно описан класс работы по протоколу UDP. Класс, реализующий взаимодействие по протоколу TCP, является примитивным и в основном ограничивается вызовом системных функций.
Код, представленный на рисунке выше, универсальный как для TCP, так и для UDP. Поэтому здесь следует немного уточнить.
При запуске TCP-сервера требуется последовательное выполнение следующих функций или их аналогов: socket(), bind(), listen(), accept().
Здесь же выполняются две функции: listen() и accept().
Функция socket_listen() TCP-класса содержит вызовы следующих команд socket(), bind() и listen(). В данной функции инициализируется сокет, устанавливаются необходимые параметры, сокет привязывается к порту и запускается прослушивание порта. Если рассмотреть вариант UDP-сокета, то для запуска сервера требуется лишь два вызова: socket() и bind(). Также в этот список еще стоит добавить функцию recvfrom(), которая напрямую отвечает за получение данных. Однако в рамках данной реализации функция recvfrom() играет роль функции accept(). Это происходит из-за того, что поверх UDP реализован свой протокол взаимодействия. Таким образом, вызовом socket_listen() инициализируется UDP-сокет и соответствующий ему класс, необходимый для обработки пакетов, поступающих по протоколу UDP, о чем подробнее будет ниже. Функция socket_accept() ожидает появления "входящего соединения". Непосредственно само активное ожидание реализовано в отдельном потоке, который запускается при инициализации вредоноса.
struct socket_class_vt
{
__int64 (__fastcall *socket_listen)(socket_class *this, __int16 port);
__int64 (__fastcall *socket_close)(socket_class *this);
__int64 (__fastcall *socket_accept)(socket_class *this, conn_handler **conn_handler);
};
Интерфейс принятия соединения
Класс-коннектор, осуществляющий взаимодействие по TCP, довольно простой. Он не реализует никаких дополнительных уровней абстракции и протокол взаимодействия с RAT-командами реализован поверх TCP.
1. Обработка команд
После принятия соединения запускается поток с обработкой входящих команд.
Изучаемый сэмпл имеет 5 команд:
Команда |
Описание |
---|---|
0 |
Запустить прокси |
1 |
Отправить команду на другой сервер |
2 |
Отправить основные сведенья о текущем компьютере: имя пользователя, имя компьютера, IP-адрес, MAC-адрес |
3 |
Запустить функции выбранного плагина |
4 |
Перечислить все запущенные RDP-сессии |
5 |
Завершить работу текущего сэмпла |
При запуске прокси взаимодействие с сервером происходит посредством одного из двух классов-коннекторов (в зависимости от выбранного типа соединения). Единичная отправка запроса на другой сервер также использует один из двух классов-коннекторов. Учитывая специфику реализации класса взаимодействия по UDP, можно предположить, что данные две функции служат для установки туннеля с другим схожим ПО. Таким образом, организовывая туннели, злоумышленник получает возможность построить собственную сеть.
Каждая команда имеет заголовок и раздел с данными. Общий формат данных для всех приходящих команд выглядит так:
struct buffer_generic
{
buffer_general_header header;
_BYTE data[header.data_packed_size];
};
struct buffer_general_header
{
_WORD rnd_val;
_WORD gap_2;
command command;
_WORD gap_6;
_WORD data_unpacked_size;
_WORD data_packed_size;
_DWORD session_id;
};
union command
{
_WORD cmd; // кроме плагинов, есть возможность вызвать иные функции вредоноса
cmd_for_union plugin; // для болле корректного отображения в IDA Pro, пришлось создать отдельную структуру, так как в данном WORD, используется старший байт для определения id плагина.
};
struct cmd_for_union
{
char gap;
char plugin_id;
};
В заголовке (16 байт) обязательно передается команда, размер запакованных данных (сколько байт данных необходимо еще получить) и размер распакованных данных (размер данных после распаковки в байтах).
После заголовка идут данные, которые приходят в зашифрованном и запакованном виде.
Алгоритм шифрования: RC4, ключ: хеш-сумма MD5 от заголовка данных (первые 16 байт).
Алгоритм сжатия: lznt1.
Отдельно отмечу: если пришли данные помимо заголовка, то перед обработкой команды из header.command вычитается значение header.sm_rnd_val. Также, учитывая тот факт, что заголовок пакета служит для шифрования передаваемых данных, поле rnd_val нужно, скорее всего, для того, чтобы ключ шифрования отличался при одном и том же заголовке. Можно предположить, что сделано это для усложнения выявления заголовка на основе только лишь дампа сетевого трафика.
2. Класс, реализующий взаимодействие по UDP.
Здесь нужно особое внимание. Поверх UDP реализован протокол, который не связан с вредоносной функциональностью. Он служит для взаимодействия по UDP и отправки данных, что показано на схеме ниже. Далее будем называть его PlugXUDP. Так как он напрямую не отвечает за вредоносную активность, а лишь является дополнительным слоем для взаимодействия сервера и клиента по протоколу UDP, остановимся лишь на некоторых интересных моментах. Далее по тексту текущий экземпляр назовем «сервер». А источник внешнего подключения к серверу – «клиент»
Благодаря протоколу PlugXUDP появляется возможность:
- поддерживать несколько одновременных сессий, которые не пересекаются между собой;
- регулировать размер пакета для принятия данных со стороны изучаемого сэмпла;
- снизить вероятность потери пакета, что актуально при работе по UDP.
Для работы данного протокола, при инициализации ратника, запускается отдельный поток. Он предназначен для получения и отправки пакетов и не связан напрямую с обработкой команд ратника. При запуске потока создается соединение – структура, в которой хранятся тип соединения и другие необходимые данные для работы самописного сетевого протокола. Под типом соединения подразумеваются следующие варианты:
- тип для приема новых подключений - он устанавливается при создании UDP-сокета.
- тип для установленных соединений – он используется для дальнейшего взаимодействия: отправка и прием данных.
На каждый запрос установки соединения создается подобная структура с другим типом соединения.
У PlugXUDP-протокола есть свой набор команд, который помогает устанавливать связь, отправлять и получать данный с сервера. Основные из них приведены ниже:
Команда |
Описание |
---|---|
0x30 |
Установка соединения. Также передаются такие параметры как максимальный размер пакета и идентификатор участника |
0x34 |
Отправка данных |
0x39, 0x35 |
Получение данных |
Тип команды передается первыми двумя байтами. Дальше идут параметры команды:
- Размер фрейма (то есть UDP-пакеты, которые используются для передачи данных). Далее при получении данных с сервера размер фрейма будет ограничен этим значением. Минимальный размер фрейма: 38 байт, где 22 байта отводится на заголовок PlugXUDP протокола, следовательно, минимальный объем передаваемых данных – 16 байт.
- Идентификатор клиента. При установке соединения сервер создает структуру, описывающую соединение. Она содержит множество данных, среди которых есть идентификатор клиента (6 байт). При дальнейшем взаимодействии с сервером необходимо обращаться именно к этой структуре.
- Верхний предел количества отправляемых пакетов. То есть сколько пакетов (отличных друг от друга) может быть передано. Но это значение может быть далее изменено командами запроса данных.
При подсоединении к серверу первой должна быть отправлена команда 0x30. Она отвечает за установку соединения и передачу двух параметров: максимальный размер пакета и идентификатор участника.
В ответ приходит идентификатор созданного соединения, который дальше используется для взаимодействия.
Идентификатора клиента состоит из двух значений, которые имеют следующие размеры: два и четыре байта. Если изучать код, то изначально это похоже на порт и IP-адрес. На рисунке ниже sock_addr_out содержит действительные адрес и порт клиента, которые приходят из функции WSARecvFrom. Поле s_addrinfo_recved_id_44 – идентификатор клиента. Однако не обнаружено, чтобы эти данные использовались где-то, кроме как при проверке выбранной структуры. Фактически на место двух этих полей можно поставить любое значение. Также это дает возможность клиенту организовывать сессии, различая их идентификатором клиента, так как данные значения отправляются сервером при отправке любого пакета.
После установки соединения начинается взаимодействие с возможностью отправлять команды RAT. Для этого используются команды 0x34 и 0x39 (0x35).
Команда 0x34 нужна для отправки данных. В ней передается номер фрейма с данными, что необходимо при фрагментации команды на несколько фреймов. Далее идут передаваемые данные. Причем не обязательно будет заголовок RAT-команды, так как она может быть разделена на несколько пакетов. И в таком случае в начале раздела данных будет идти продолжение RAT команды. Также в начале структуры пакета передается идентификатор клиента и идентификатор соединения, который приходит в ответ на установку соединения (команду 0x30).
Для получения ответа с сервера, если данные не вместились в один фрейм, который приходит в ответ на команду 0x34, необходимо использовать команду 0x39. В ней передается номер фрейма. Также есть возможность увеличить лимит передаваемых фреймов. Здесь стоит отметить, что нельзя отправить команду на получение номера предыдущего фрейма и уменьшить лимит передаваемых фреймов. То есть значение номера фрейма должно быть меньше, чем отправленное ранее значение.
Команды 0x39 и 0x35 отличаются набором необходимых данных. Команда 0x35 позволяет менять некоторые значения, используемые во взаимодействии по протоколу PlugXUDP.
Ниже представлен алгоритм для отправки RAT-команд по UDP.
Работа в контексте процесса winlogon.exe
При работе в контексте процесса winlogon.exe создается именованный пайп с названием \\.\PIPE\X%d, где %d – пид текущего процесса. После инициализируются плагины (этот процесс и их работа описаны ниже). Команды поступают через именованный пайп. Далее команда парсится и вызывается плагин, соответствующий текущей команде. Благодаря такой технике у вредоноса появляется возможность выполнять функционал плагинов от лица аутентифицированного пользователя. Здесь используется тот же формат данных, что и при обработке команд при работе в контексте процесса msiexec.
Плагины
Плагины хранятся в отрытом виде. Доступ к ним производится посредством глобальной переменной, которая в массиве хранит адреса объектов по классам плагинов. Каждый плагин имеет единый интерфейс:
struct generic_plugin_interface
{
void (__fastcall *destructor)(generic_plugin_handler *this, __int64 bool_destroy);
void (__fastcall *init_plugin)(generic_plugin_handler *this, unsigned __int8 *plugin_id_out);
void (__fastcall *run_plugin_handler)(generic_plugin_handler *this, conn_handler_tcp *conn_handler, buffer_generic *buf);
void (__fastcall *graceful_shutdown)(generic_plugin_handler *this);
};
Функция init_plugin вызывается при инициализации плагина. В ней инициализируются поля класса, и, если необходимо, запускаются потоки для дальнейшей корректной работы плагина. После вызова функции ссылка на объект кладется в глобальный массив. Идентификатор плагина – индекс в массиве этой глобальной переменной.
Функция run_plugin_handler выполняет обработку команды для плагина. Команда плагина состоит из двух байт: где младший байт – команда плагина, старший – идентификатор. Именно поэтому при обработке RAT-команд берется старший байт.
Список плагинов и краткое их описание:
Plugin id |
Название |
CMD |
Описание |
---|---|---|---|
2 |
File system management |
0x200 |
Перечислить диски |
|
|
0x201, 0x206 |
Перечислить файлы в директории |
|
|
0x202 |
Открыть файл при помощи вызова функции ShellExecuteExW |
|
|
0x203 |
Операции над файлами: переместить, копировать, удалить |
|
|
0x205 |
Считать файл с заданного отступа |
|
|
0x207 |
Создать файл и продолжить запись в существующий с заданного отступа |
3 |
Interactive shell with PIPES |
0x300 |
Запустить интерактивную командную оболочку |
4 |
Proxy |
0x400 |
Превратить данное соединение в туннель |
|
|
0x401 |
Подключиться к заданной машине |
|
|
0x402 |
Отправить данные на указанное установленное соединение |
|
|
0x403 |
Завершить указанное соединение |
5 |
Screenshot |
0x500 |
Сделать скриншот с указанными размерами |
|
|
0x502 |
Отправить серию скриншотов для мониторинга происходящих действий. Задать размеры изображения также возможно |
7 |
Logs keys and clipboard. |
0x700 |
Прочитать файл ntuser.dat.log1 |
|
|
0x701 |
Прочитать файл ntuser.dat.log2 |
|
|
0x702 |
Стереть содержимое файла ntuser.dat.log1 |
|
|
0x703 |
Стереть содержимое файла ntuser.dat.log2 |
6 |
Interactive shell with CONIN$, CONOUT$ |
0x600 |
Запустить эмуляцию telnet |
Далее перечислены некоторые особенности реализованных плагинов и представлены структуры, которые используются для работы с плагинами.
Плагин "2"
Создан для взаимодействия с файлами: «хождение» по папкам, загрузка и выгрузка файлов. Его отличает возможность дозаписывать данные или писать по определенному отступу, что позволяет восстанавливать запись в результате возможного обрыва соединения.
struct fs_plugin_handler
{
fs_plugin_vt *vt;
};
Плагин "3"
Создаются два именованных пайпа для взаимодействия с процессом =\.\pipe\sI%d= (%d – это адрес объекта класса для взаимодействия с сервером). Далее запускается процесс и два потока, которые обрабатывают взаимодействие клиента и запущенного процесса через созданные именованные пайпы. После это соединение служит для работы в режиме интерактивной командной оболочки, пока не завершится процесс или соединение не прервется. Данные, присылаемые от клиента, приходят ровно в том же формате, в котором происходит взаимодействие с RAT-командами. То есть сохраняются заголовок и шифрование.
struct interactive_shell_plugin_handler
{
interactive_shell_plugin_vt *vt;
_QWORD sI_pipe;
_QWORD sO_pipe;
_QWORD hadnle_to_sI_pipe_IO;
_QWORD hadnle_to_sO_pipe_IO;
PROCESS_INFORMATION process_info;
_QWORD event_handler;
conn_handler_tcp *conn_handler;
};
Плагин "4"
Данный плагин служит для взаимодействия с сетевым окружением сервера. Его отличает возможность работы с несколькими соединениями (до 1024). Каждое из них описывается двумя параметрами: идентификатором сессии, который устанавливается клиентом, и сокетом подключения к сетевому узлу.
Вначале запускается два потока. Первый – обрабатывает данные, приходящие с подключенных сетевых узлов. Второй – обрабатывает команды, приходящие от клиента (подключиться к сетевому узлу, отправить данные и завершить соединение).
struct portmap_plugin_handler
{
portmap_plugin_vt *vt;
conn_handler_tcp *conn_h;
_QWORD event_handler;
CRITICAL_SECTION crit_sec;
CRITICAL_SECTION crit_sec2;
_DWORD sync_counter;
portmap_plugin_session sessions[1024];
};
struct __declspec(align(8)) portmap_plugin_session
{
_QWORD session_id;
_DWORD socket;
};
Плагин "5"
Плагин имеет два режима работы: отправка одного скриншота и трансляция изображения с экрана через создание множества скриншотов. При отправке клиентом запроса на скриншот или видео, можно задать определенный размер изображения для передачи.
struct screen_plugin_handler
{
screen_plugin_vt *vt;
};
Плагин "6"
Предоставляет доступ к интерактивной командной оболочке через объекты "CONIN$" и "CONOUT$", что позволяет эмулировать соединение по Telnet. Это дает возможность управлять дочерними процессами, запущенными в рамках текущей сессии, даже если потоки stdin, stdout и stderr не наследуются дочерним процессом.
struct __declspec(align(8)) console_plugin_handler
{
sixth_plugin_fns *vt_0;
conn_handler_tcp *conn_h_8;
_QWORD event_h_10;
PROCESS_INFORMATION process_info_18;
char input_buffer_30[32]; // to track new input packets
_WORD console_width_50;
_WORD console_height_52;
__declspec(align(8)) _DWORD console_buffer_size_58;
struct _CHAR_INFO *p_to_prev_buf_60;
struct _CHAR_INFO *p_to_cur_buf_68;
};
Плагин "7"
Данный плагин позволяет записывать содержимое буфера обмена, а также нажатия клавиатуры. Для этого запускается отдельный поток при инициализации плагина. Логирование происходит при помощи создания окна (CreateWindowExW) и установки обработчика сообщений (SetWindowLongPtrW). Логирование данных происходит в следующие файлы:
- файл %USERPROFILE%\AppData\Roaming\ntuser.dat.LOG1 – используется для сохранения нажатий клавиш;
- файл %USERPROFILE%\AppData\Roaming\ntuser.dat.LOG2 – используется для сохранения буфера обмена.
Для чтения и очистки каждого файла используются отдельные команды, приведенные в таблице выше – "Инициализация двух плагинов".
struct keylogger_plugin_handler
{
keylogger_plugin_vt *vt;
_QWORD qw_8; // unused field
};
Сравнение с прошлой версией PlugX
Теперь давайте сравним найденный нами вариант PlugX с исследованиями компании "Dr.Web" за 2020 год.
Начнем с загрузки и шифрования. Файл, анализируемый экспертами Dr.Web, также сначала ищет файл по конкретному пути на зараженной системе и после считывания файла передает ему управление. Особенностью «нашего» загрузчика (код, который первым выполняется при передаче управления файлу desktop.ini) является то, что он не обфусцирован. Также не обфусцированы и строки в полезной нагрузке. При расшифровке полезной нагрузки можно наблюдать следующие изменения:
- структура зашифрованной полезной нагрузки стала проще и теперь хранит лишь саму полезную нагрузку,
- при расшифровывании полезной нагрузки теперь требуется ключ, который лежит в начале полезной нагрузки.
Несмотря на появление ключа, алгоритмы шифрования очень похожи. Используется та же «формула», а вместо констант используется ключ, который с каждым шагом инкрементируется.
В итоге также расшифровывается PE-файл со сбитыми заголовками. Единственное отличие в нашем случае: сигнатуры "MZ" и "PE" затерты нулевыми байтами, а не "XV".
При переходе к полезной нагрузке она принимает в качестве аргумента ссылку на большую структуру конфигурации вредоносной программы, где содержатся IP-адреса C&C и иная информация. В нашем сэмпле не наблюдается подобной большой структуры. Это может быть обусловлено тем, что наш изучаемый сэмпл – это сервер, который самостоятельно не инициирует никакого подключения. Также не обнаружено проверки неиспользуемых глобальных переменных или аргументов функции dllMain, что можно интерпретировать как следы наличия единой кодовой базы для клиентского серверного приложения.
В начале инициализации полезной нагрузки процесс внедряется в другие найденные процессы:
- в нашем случае это winlogon;
- в случае Dr.Web это "IEFrame" для версии 28, или запуск "msiexec.exe" с присвоенным маркером доступа для версии 38.
Также отличается протокол сетевого взаимодействия с управляющим сервером:
- ключ для шифрования данных – это дворд, а не весь заголовок целиком;
- также в новой версии указывается размер распакованных данных.
Эти отличия не позволяют старому и новому экземплярам PlugX взаимодействовать между собой.
Плагины в исследуемой версии PlugX утратили названия и временную отметку. В исследуемом нами экземпляре вредоносного ПО структура плагина – это лишь таблица виртуальных методов. Также плагинов гораздо меньше и нет возможности загрузить новый из внешнего файла. Ниже представлена таблица сравнения идентификаторов плагинов нашей версии и версии, описанной Dr. Web:
Plugin id |
Описание |
ID PlugX.28 |
Название плагина |
---|---|---|---|
2 |
File system management |
3 |
Disk |
3 |
Interactive shell with PIPES |
7 |
Shell |
4 |
Proxy |
11 |
Portmap |
5 |
Screenshot |
4 |
Screen |
7 |
Logs keys and clipboard. |
14 |
KeyLogger |
6 |
Interactive shell with CONIN$, CONOUT$ |
0x71 |
Telnet |
Индикаторы компрометации (IOC)
Хеш-суммы файлов:
Filename |
MD5 |
SHA256 |
---|---|---|
|
|
|
dbgeng.dll.0 |
fcdf8aa50123aaf5fb6cbed60263d157 |
f52a69f048b8649a0c404d80d612f63f07fe6af01c1ea6927472acb |
dbgeng.dll.1 |
677cbbcf6e2a6751fcda0b9418c408c8 |
6f8f7cc88d2d8367df63c1fd566044bcca3708f6a6fa9fa4a50608 |
dbgeng.dll.2 |
d0d46a95cf902831874134c35eb4b740 |
d48b8be84b5ccd955aade8a467d33accfbb61b22529dbf3a73e23 |
desktop.ini |
af4103ad7339fc0c12362087c6c424f0 |
3560b8a7c076ed959190cc3910b776d0a4dcf9673aaa03778615cb7 |