Драйвер msagent.sys, который HoneyMyte в этом году занесла в ядро Windows, подписан сертификатом компании Nanjing Ranyi Technology Co., Ltd. Сертификат выдан в августе 2013 года и истёк в сентябре 2014-го. Двенадцать лет спустя он всё ещё открывает нулевое кольцо – и это не оплошность Microsoft, а её собственное задокументированное правило.
«Лаборатория Касперского» разобрала свежий вариант бэкдора CoolClient и вытащила из него ровно ту деталь, ради которой стоит читать весь отчёт: группа перестала прятаться в юзермоде и посадила себе в ядро персонального охранника. Драйвер прячет процесс малвари, защищает её файлы и ключи реестра от удаления, а адрес управляющего сервера вырезает из ответов сетевого стека – так, что netstat на заражённой машине покажет чистый список соединений. Всё это держится на поддельной папке Windows Defender, хуке в Nsiproxy и сертификате, которому давно вышел срок, – и в той же связке видно, где машину собирали руки вендорского класса, а где доделывали кое-как.
Сразу оговорим границу. Как HoneyMyte попадает в сеть – в этом отчёте не сказано ни слова, и додумывать не будем. Точка отсчёта здесь – машина, на которой уже работает PlugX: старый, годами обкатанный имплант группы, который в этих кампаниях играет роль первичного плацдарма. CoolClient приезжает вторым эшелоном, PlugX его и раскладывает. То есть весь дальнейший разбор – это post-compromise: что группа делает на хосте, куда уже зашла, а не как она туда зашла.
Такое разделение труда само по себе показательно. PlugX светится в индустрии с начала 2010-х, его сигнатуры есть у всех, и держать его на переднем крае группа не стала бы. А вот использовать его как одноразовую лестницу, чтобы завести на хост свежий, ещё не разобранный CoolClient, – рационально: спалится лестница, а не то, что по ней занесли.
Первое, что делает актор, – готовит себе легенду на диске. В Microsoft Defender добавляются два исключения: на папку и на файл.
wmic /Node:localhost /Namespace:\\Root\Microsoft\Windows\Defender Path MSFT_MpPreference call Add ExclusionPath="$programfiles\Microsoft\Windows Defender"
wmic /Node:localhost /Namespace:\\Root\Microsoft\Windows\Defender Path MSFT_MpPreference call Add ExclusionPath="$programfiles\Microsoft\Windows Defender\defender.exe"
Тонкость в самом каталоге. Настоящий Defender живёт в Program Files\Windows Defender. Актор создаёт рядом Program Files\Microsoft\Windows Defender – на одно Microsoft\ глубже – и xcopy-ит туда содержимое настоящей папки целиком:
xcopy "$programfiles\Windows Defender\*" "$programfiles\Microsoft\Windows Defender" /a /s /v /e /f
Расчёт простой: аналитик, пробежавший глазами по списку процессов и путей, видит Windows Defender, defender.exe, знакомую обвязку – и проскакивает мимо. Каталог мимикрирует не под случайное имя, а под самый доверенный процесс в системе. Это дешёвый, но работающий психологический трюк: прятаться не в тени, а на самом виду, под вывеской того, кого проверяющий меньше всего готов заподозрить.
В этот каталог кладётся легитимный подписанный бинарь Sangfor – обычно Sang.exe – переименованный в defender.exe. Он и станет носителем: рядом лежит вредоносная libngs.dll, которую подписанный Sangfor честно подгрузит через классический DLL sideloading. Тот же почерк мы разбирали, когда Mustang Panda протаскивала бэкдор через подписанный Solid PDF Creator: не искать дыру в доверенной программе, а взять доверенную программу, которая сама подхватит чужую DLL из своей папки.
Персистентность №1 вешается тут же – запланированной задачей, стартующей с системными правами при загрузке:
schtasks /create /sc onstart /tn "\Microsoft\Windows\Windows Defender Advanced Threat Protection Service" /tr "\"$programfiles\Microsoft\Windows Defender\defender.exe\"" /ru "system" /F
Имя задачи – «Windows Defender Advanced Threat Protection Service». Опять та же ставка на маскировку под имя, которое не хочется трогать.
Компонентов в связке шесть, и роли поделены чётко:
defender.exe / Sang.exe — легитимный Sangfor, носитель сайдлоадинга
libsrapc.dll — настоящая зависимость Sangfor, чтобы тот стартовал штатно
libngs.dll — первая стадия: расшифровывает и грузит следующую
loadcert.ini — вторая стадия: команды, инъекция, драйвер, персистентность
cert.ini — финальный имплант: C2 и бэкдор-функции
time.ini — конфиг
Тем, кто разбирал прежние варианты CoolClient, имена loadcert.ini и cert.ini ничего не скажут – раньше это были loader.dat и main.dat. Переименование косметическое, но показательное: под расширение .ini (якобы конфиги) прячут исполняемые DLL. На диске лежит «настройка», в памяти исполняется код.
Первая стадия, libngs.dll, отдельно заслуживает взгляда. Чтобы сойти за настоящую библиотеку Sangfor, она экспортирует полную таблицу функций с правдоподобными именами – а каждая из них не делает ничего, кроме вызова OutputDebugStringA с собственным именем и немедленного ExitProcess.
ngs_input_send_keyboard proc near
push ebp
mov ebp, esp
push offset "_ngs_input_send_keyboard"
call ds:OutputDebugStringA
push 0
call ds:ExitProcess
Экспорты – бутафория для того, кто откроет DLL в дизассемблере и пробежит по таблице. Настоящая логика живёт в DllMain, спрятана за control flow flattening и лесом безусловных переходов, а сводится к одному действию: прочитать loadcert.ini, расшифровать повторяющейся XOR-гаммой длиной 0x32 байта (сид 0xA4) и передать управление в память.
Здесь стоит назвать вещи своими именами. XOR с повторяющимся ключом – это не шифрование, это кодирование. Оно останавливает грепа по сигнатуре и статический антивирус, но разворачивается аналитиком в пару строк на Python, как только ключ и длина известны. Группа, которая через двадцать минут будет динамически искать смещения в EPROCESS, здесь обходится гаммой, которую снимают на коленке. Тяжёлая инженерия в ядре и курсовая криптография в загрузчике живут на одном бинаре, и глаз цепляется за этот шов не в последний раз.
Диспетчер второй стадии управляется тремя параметрами командной строки – install, work, passuac – и определяет, где он исполняется, по имени текущего модуля: если это synchost.exe, идёт «инъектированной» веткой (драйвер плюс финальный имплант), иначе – первичная настройка. В прежних вариантах целью инъекции был write.exe; смена на synchost.exe – мелочь, но именно из таких мелочей детект и собирает поведенческие правила, поэтому группа их и меняет от версии к версии.
Персистентность №2 – AutoRun под пользователя:
HKCU\Software\Microsoft\Windows\CurrentVersion\Run
goopdate = Sang.exe work
Имя значения goopdate мимикрирует под апдейтер Google. Тот же приём именования, что и с задачей-Defender'ом.
Дальше loadcert.ini расшифровывается уже другим ключом – XOR-гамма 0x32 байта из сида 0x4D – и инъектируется в свежесозданный synchost.exe в состоянии suspended: аллокация памяти, запись payload'а, правка контекста потока на точку входа, ResumeThread, и ExitProcess в исходном процессе. С этого момента вся жизнь малвари – внутри процесса с системным именем.
Отдельная, третья по счёту персистентность – установка себя службой media_updaten через CreateServiceA и запуск sc start media_updaten из-под WinExec. Но интересна не сама служба, а две проверки перед ней. Малварь убеждается, что у неё есть доступ к Service Control Manager, – и что в системе не запущен ни один процесс 360 Total Security:
360sd.exe
zhudongfangyu.exe
360desktopservice64.exe
Это тот самый китайский антивирус. Обратите внимание, кого именно опасается код: он проверяет наличие 360, но нигде по цепочке не проверяет наличие западного EDR. Для группы, чьи жертвы – Мьянма, Монголия, Пакистан и Россия, это логично: она калибрует уклонение под ту защиту, которую реально встречает на целях, а не под ту, что стоит в лабораториях аналитиков. Заодно это косвенный маркер происхождения: так осторожничают с 360 те, кто сам вырос в его экосистеме.
Если прав администратора нет, малварь их поднимает – и делает это красиво, через RPC-создание процесса (публично описанное Google Project Zero) в связке с подменой PPID.
Сначала проверяется присутствие escanmon.exe (компонент антивируса eScan – ещё одна оглядка на конкретное окружение). Затем малварь коннектится к локальному ncalrpc-эндпоинту 201ef99a-7fa0-444c-9399-19ba84f12a1a и через NdrAsyncClientCall запускает winver.exe. Дальше – аккуратная работа с debug-объектом: у порождённого winver.exe он снимается через NtQueryInformationProcess, отвязывается NtRemoveProcessDebug, сам процесс убивается, а вот объект переиспользуется. Тем же RPC-способом запускается computerdefaults.exe (классический auto-elevate-бинарь), к текущему потоку через DbgUiSetThreadDebugObject привязывается сохранённый debug-объект, ловится WaitForDebugEvent, и NtDuplicateObject выдаёт хэндл процесса с полными правами.
Финальный аккорд: малварь перезапускает себя как Sang.exe passuac, но через расширенный список атрибутов старта, где PROC_THREAD_ATTRIBUTE_PARENT_PROCESS (выставленный UpdateProcThreadAttribute) назначает новому процессу поддельного родителя. В результате свежий элевейтнутый экземпляр в дереве процессов выглядит порождением доверенного системного процесса, а не малвари.
Оценим ход по гамбургскому счёту. Спуфинг PPID – это не только про elevation, это в первую очередь про обман EDR: поведенческий детект строит деревья «родитель–потомок» и ищет аномалии вроде «Office породил PowerShell». Здесь родитель подделан на уровне структуры процесса в ядре, поэтому дерево выглядит стерильно. Это работа зрелого класса – и тем разительнее, что финальную нагрузку в тот же вечер расшифровывали однобайтовой логикой.
Последний штрих юзермода: перед запуском финального cert.ini малварь перебирает активные WinStation-сессии, выбирает интерактивную пользовательскую, дублирует её токен и создаёт synchost.exe через CreateProcessAsUserA – чтобы имплант жил в контексте живого пользователя, а не в невидимой сессии, где аномальное поведение заметнее.
Вот мы и у главного. Прежде чем ставить драйвер, loadcert.ini проверяет два условия: полный доступ к SCM и наличие привилегии SeTcbPrivilege («act as part of the operating system»). Нет их – деплой драйвера молча пропускается, и малварь остаётся юзермодным бэкдором. Есть – из loadcert.ini достаётся LZMA-сжатый драйвер, распаковывается, пишется на диск как msagent.sys рядом с cert.ini, и регистрируется службой msagent.
И теперь – почему Windows вообще соглашается этот драйвер загрузить. Начиная с Windows 10 version 1607, система не грузит новые драйверы режима ядра, не подписанные через Microsoft Dev Portal. Казалось бы, сертификат частной китайской фирмы из 2013 года здесь бесполезен. Но в той же политике Microsoft прописано дословное исключение: кросс-подписанные драйверы по-прежнему допускаются, если сертификат конечной сущности выдан до 29 июля 2015 года и выстраивает цепочку к поддерживаемому кросс-подписанному центру сертификации. Сертификат Nanjing Ranyi Technology, действовавший с августа 2013 по сентябрь 2014, попадает в это окно с запасом.
Это фундаментально сильнее классического BYOVD. В BYOVD (мы разбирали его на банде Gentlemen) атакующий тащит легитимный, но уязвимый драйвер и эксплуатирует в нём дыру, чтобы дотянуться до кольца 0. Здесь дыры нет вообще: msagent.sys – это сразу вредоносный драйвер, который заходит в ядро по парадной, потому что его подпись формально удовлетворяет правилу совместимости. Не «нашли и проэксплуатировали уязвимость», а «нашли и удовлетворили условие политики». Актив здесь – не эксплойт, а сам старый сертификат с сохранённым закрытым ключом.
И тут возникает вопрос, который в отчёте не поднят, а стоило бы. Кросс-подписанные драйверы работают, только если подпись поставлена, пока сертификат ещё валиден, – а большинство кросс-сертификатов Microsoft истекли в июле 2021 года. Значит, либо msagent.sys несёт метку времени той эпохи, когда всё это ещё было живым, либо у группы есть закрытый ключ и техническая возможность проставить подходящий таймстемп. Отчёт значение таймстемпа не публикует, поэтому утверждать не станем – но с точки зрения атакующего именно связка «старый сертификат + валидная метка времени» и есть настоящая ценность, которую HoneyMyte бережёт. Кстати, о происхождении драйвера красноречиво говорит и невычищенный путь к его PDB-файлу: E:\work\南京实验室\2024项目\张雪杰云南m\研发\FTool\Tool\x64\Release\FTool.pdb – «Нанкинская лаборатория», проект 2024 года. OSINT по этим строкам ничего публичного не дал, но для группы такого класса такой след в боевом драйвере – странная небрежность.
Загрузившись, драйвер открывается юзермоду через CreateFileW на \\.\msagent и принимает три инициализирующих IOCTL:
0x222120 — зарегистрировать текущий процесс CoolClient как доверенный
0x2221E0 — передать IPv4-адрес C2
0x2220F0 — зарегистрировать пути ФС и реестра под защиту/сокрытие
Первый – ключевой для всей модели: он помечает процесс малвари флагом «доверенный», и с этого момента именно он один свободно ходит к защищённым объектам, тогда как всем остальным драйвер эти объекты закрывает. Модель безопасности переворачивается: в системе появляется процесс, для которого правила не действуют, а сами правила невидимы.
Конфигурацию сокрытия драйвер читает из ключа \REGISTRY\MACHINE\SYSTEM\RNG. Два REG_DWORD управляют режимом – Hid_State (включение руткит-функций) и Hid_StealthMode, – а шесть REG_MULTI_SZ перечисляют, что именно скрывать и защищать: каталоги, файлы, ключи и значения реестра, процессы-исключения и процессы под защитой. Имена значений стоит привести дословно, потому что они сами по себе – характеристика авторов:
Wid_H1deF5Dirs — каталоги, которые прятать
Wid_H1deF5Files — файлы
Wid_H1deRegKeys — ключи реестра
Wid_H1deRegValues — значения реестра
Hid_IgnoredImages — процессы-исключения
Hid_ProtectedImages — процессы под защитой
H1de, F5 вместо Files – это лит-написание из подросткового скрипткиддинга, зашитое в конфиг софта, который на соседнем слое динамически парсит структуры ядра. Диссонанс тот же, что и с XOR-гаммой: инженерия высокого класса, культура кода – из читерских форумов.
Дальше идёт работа, которую в малвари встречаешь нечасто. Чтобы прятать процесс, драйверу нужно поле ActiveProcessLinks в структуре EPROCESS – а его смещение меняется от сборки к сборке Windows. Дешёвый путь – зашить константу под конкретную версию, и потом ловить синие экраны на всех остальных. msagent.sys идёт дорогим: сначала проверяет набор известных смещений, а если ни одно не подошло – линейно сканирует EPROCESS, отыскивая правильную раскладку. Это ровно тот подход к переносимости, что закладывают вендоры EDR в свои же ядерные компоненты, чтобы один бинарь жил на всём парке Windows без пересборки.
Найденное смещение используют процедуры сокрытия процесса: unlink и relink записи в списке активных процессов (IOCTL 0x22219C и 0x2221A0). Классический DKOM – прямая правка объектов ядра: процесс вырезается из двусвязного списка, по которому Task Manager и большинство инструментов и перечисляют задачи, но продолжает исполняться, потому что планировщик ходит по другим структурам.
Поверх этого драйвер строит быстрые AVL-деревья из реестрового конфига и вешает три типа коллбэков ядра:
ObRegisterCallbacks с altitude 1203 – на PsProcessType и PsThreadType. Перехватывая попытку открыть хэндл к защищённому процессу, драйвер не блокирует её грубо, а срезает права в выдаваемом хэндле: снимает те, что нужны для завершения процесса и инъекции. Инструмент аналитика получает хэндл, думает, что всё в порядке, а TerminateProcess по нему тихо не срабатывает. В разобранном образце под этой защитой – код CoolClient, инъектированный в synchost.exe.PsSetCreateProcessNotifyRoutineEx и image-load через PsSetLoadImageNotifyRoutine – по мере старта каждый процесс сверяется с тремя списками из конфига (Hid_IgnoredImages, Hid_ProtectedImages, Hid_HideImages) и получает одну из трёх судеб: игнорировать, защищать или прятать. Чтобы не упустить процессы, работавшие ещё до загрузки драйвера, тот разово энумерует все активные и добавляет подходящие в свои структуры.Файлы и каталоги прячет файловый минифильтр. Здесь – ещё одна инженерная деталь: чтобы встроиться в стек фильтров, нужен уникальный altitude, и драйвер не хардкодит его, а перебирает значения, повторяя FltRegisterFilter, пока регистрация не пройдёт без конфликта. При обращении к файлу минифильтр сверяет путь со своими списками и на совпадении отдаёт отказ – защищённые файлы для пользователя и приложений просто не существуют.
Реестр закрывает коллбэк CmRegisterCallbackEx с altitude 320000. На запросах перечисления он вырезает защищённые ключи и значения из результата ещё до возврата в юзермод; на прямых операциях открытия, правки или удаления возвращает STATUS_ACCESS_DENIED. И снова – доверенный процесс, зарегистрированный через IOCTL 0x222120, всю эту фильтрацию проходит насквозь. Регистратор reg-редактора не увидит ключей малвари, а сама малварь работает с ними свободно.
Самое интересное – сокрытие сети. Драйвер создаёт устройство \Device\ToolTool и хукает системный драйвер Nsiproxy – тот, через который юзермод (в том числе netstat) получает информацию о сетевых соединениях. Хук ставится так:
Драйвер берёт объект \Driver\Nsiproxy через ObReferenceObjectByName, атомарно (InterlockedExchange64) подменяет один из указателей обработчика на свою функцию NsiproxyFilterHandler, сохранив оригинал. Дальше его фильтр сверяет каждую строку в возвращаемых данных со списком зарегистрированных через IOCTL 0x2221E0 адресов C2 и вычёркивает совпадения до того, как данные уйдут наверх. Практический итог: на заражённой машине netstat, монитор ресурсов и любой инструмент, читающий соединения через Nsiproxy, покажут соединения с управляющим сервером как отсутствующие. Трафик идёт, сокет живёт, но в таблице его нет.
Сокрытие тут двухслойное. Даже вытащи аналитик C2-адрес в обход Nsiproxy – со шлюза, из дампа памяти – сами домены управления собраны так, чтобы не зацепить взгляд: часть сидит на бесплатных динамических DNS (giize.com, freeddns.org, webredirect.org), часть косит под бренды вроде us.lenovoappstore[.]com. Один слой прячет соединение на хосте, другой – в списке доменов у того, кто до хоста ещё не добрался.
Замыкает картину сокрытие самого драйвера: адрес непубличной переменной ядра PsLoadedModuleList резолвится в рантайме через MmGetSystemRoutineAddress, после чего запись модуля можно unlink'нуть из списка загруженных – и msagent.sys исчезает из перечисления драйверов.
И вот здесь – самое красноречивое наблюдение по всему отчёту. Драйвер регистрирует 33 IOCTL-обработчика, а разобранный образец CoolClient в штатной работе использует три. Остальные тридцать – не мёртвый код, а спящий арсенал. Среди невызванных:
0x222134 — инъекция шеллкода в процесс через NtCreateThreadEx
0x222138 — сокрытие модуля unlink'ом из PsLoadedModuleList
0x222194 — снятие защиты PPL с процесса
0x2221AC — перечисление и восстановление kernel notify-коллбэков
0x2221B0 — отключение (и восстановление) kernel notify-коллбэков
0x2221B4 — ручная загрузка второго драйвера
0x2221BC — запись по произвольному адресу ядра
Прочитайте этот список как ТЗ. Снятие PPL и отключение kernel-коллбэков – это ровно то, что нужно для убийства EDR из ядра: PPL – механизм, которым антивирус защищает себя от юзермода, а notify-коллбэки – то, чем EDR вообще узнаёт о событиях в системе. Всё это уже лежит в msagent.sys и ждёт одной команды. То есть текущая кампания – разведка и шпионаж на тихом рутките, но тот же самый драйвер по одному IOCTL превращается в инструмент активного подавления защиты. Возможность уже в ядре; не хватает только решения ей воспользоваться.
Класс. Заход в ядро без единой уязвимости, через задокументированное исключение политики подписи, – архитектурно чище любого BYOVD: нечего патчить вендору, нечего заносить в блок-лист, кроме конкретного сертификата. Динамический поиск ActiveProcessLinks, перебор altitude минифильтра, срезание прав в хэндле вместо грубой блокировки, хук Nsiproxy ровно на том слое, где C2 читает netstat, спуфинг PPID на уровне структуры процесса – всё это работа людей, которые понимают и внутренности ядра, и то, как устроен детект по ту сторону.
Кое-как. И на этом же бинаре – XOR с повторяющимся ключом вместо шифрования, лит-написание H1deF5 в конфиге, невычищенный путь к PDB с китайскими каталогами, уехавший в боевой драйвер. А ещё – три независимых механизма персистентности (задача-Defender, AutoRun goopdate, служба media_updaten) у малвари, которая только что научилась прятать свой процесс, файлы и ключи реестра. Каждый лишний механизм закрепления – это лишний артефакт в аудите; имея драйвер, способный скрыть один надёжный якорь, держать три шумных – избыточно.
Вывод из этой асимметрии практический, а не эстетический. Ядро и обвязку здесь почти наверняка писали разные руки: одни – с квалификацией разработчика системного ПО, другие – по остаточному принципу. Для защиты это и есть точка опоры. Ловить msagent.sys в ядре, когда он уже режет вам netstat и TerminateProcess, поздно и трудно – это его домашняя территория. А вот периферия оставляет следы на каждом шагу до входа в кольцо 0: два wmic-исключения Defender подряд, xcopy системной папки в Program Files\Microsoft\Windows Defender, задача с системными правами под именем ATP-сервиса, sc start media_updaten. Это громкие, наблюдаемые события в юзермоде, и любое из них ловится телеметрией до того, как драйвер получит свои SeTcbPrivilege и уйдёт в ядро гасить наблюдение изнутри.
HoneyMyte уже приносила kernel-функциональность в ToneShell; теперь тот же ход повторён в CoolClient. Направление читается однозначно: группа системно переносит центр тяжести в ядро – туда, где её видно хуже всего, а сделать с ней можно меньше всего. Ирония в том, что дверь в это ядро ей открыл не хитрый эксплойт, а сертификат, срок действия которого истёк, когда актуальной версией Windows была 8.1.
Разбор нового варианта CoolClient и индикаторы компрометации – «Лаборатория Касперского».
Автор: hacker@shifry.local