Логотип ШифрыШифры
ГлавнаяЖурналЛотыУзел ИИВойти
ГлавнаяЖурналЛотыУзел ИИ
Войти
Логотип ШифрыШифры2К26Версияъ1
EmailMAX
Назад к логу

Mustang Panda загнала бэкдор в ядро Windows сертификатом, истёкшим в 2014 году

18 августа 2026 г.•Репа
#атаки#тулзы#хакеры

Драйвер 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, – рационально: спалится лестница, а не то, что по ней занесли.

Поддельный Defender как жилплощадь

Первое, что делает актор, – готовит себе легенду на диске. В 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'ом.

Переезд в synchost.exe и третья персистентность

Дальше 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 те, кто сам вырос в его экосистеме.

Обход UAC через RPC и подмену родителя

Если прав администратора нет, малварь их поднимает – и делает это красиво, через 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 – чтобы имплант жил в контексте живого пользователя, а не в невидимой сессии, где аномальное поведение заметнее.

Порог в ядро: SeTcbPrivilege и сертификат из 2013 года

Вот мы и у главного. Прежде чем ставить драйвер, 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-деревья из реестрового конфига и вешает три типа коллбэков ядра:

  • Object-callbacks через ObRegisterCallbacks с altitude 1203 – на PsProcessType и PsThreadType. Перехватывая попытку открыть хэндл к защищённому процессу, драйвер не блокирует её грубо, а срезает права в выдаваемом хэндле: снимает те, что нужны для завершения процесса и инъекции. Инструмент аналитика получает хэндл, думает, что всё в порядке, а TerminateProcess по нему тихо не срабатывает. В разобранном образце под этой защитой – код CoolClient, инъектированный в synchost.exe.
  • Process-callbacks через 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) получает информацию о сетевых соединениях. Хук ставится так:

Декомпилированная функция InstallNsiProxyHook: получение объекта \Driver\Nsiproxy через ObReferenceObjectByName и подмена указателя обработчика на NsiproxyFilterHandler
Установка хука: объект \Driver\Nsiproxy достаётся по имени, а оригинальный обработчик атомарно подменяется на фильтрующую рутину драйвера

Драйвер берёт объект \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