COM в ловушке. Атакуем Windows через объекты Trapped COM

Добро пожаловать на наш форум!

Спасибо за посещение нашего сообщества. Пожалуйста, зарегистрируйтесь или войдите, чтобы получить доступ ко всем функциям.


Gibby

Автор
Команда проекта

Регистрация
Сообщений
1,635
Репутация
45
Сделок
3.jpg

COM-объекты в Windows могут застревать в неожиданных местах — и это не баг, а фича, которой можно воспользоваться. Через интерфейс IDispatch и библиотеки типов можно не просто управлять удаленными объектами, а внедрять код в защищенные процессы (PPL), обходя механизмы безопасности Microsoft.

Объектно ориентированные технологии удаленного взаимодействия, такие как DCOM и .NET Remoting, позволяют без особых заморочек организовать доступ к сервисам, пересекающим границы процессов и уровни безопасности. Фишка в том, что они поддерживают не только встроенные объекты, но и любые другие, которые можно передавать удаленно.


Например, захотелось тебе пробросить XML-документ через клиент‑серверный барьер? Не проблема — берешь готовую COM- или .NET-библиотеку, заворачиваешь объект и отправляешь клиенту. По умолчанию объект маршализуется по ссылке. Это значит, что сам он остается на сервере, а клиент лишь управляет им дистанционно.

Но за такую гибкость приходится платить, и именно об этом пойдет речь — о классе багов, который я назову «ловушка для объектов». Дело в том, что не все объекты, которые можно передавать удаленно, безопасно так передавать. Например, те же XML-библиотеки в COM и .NET поддерживают выполнение произвольного кода в контексте XSLT-документа. А теперь представь: если XML-объект доступен за пределами процесса, клиент может запустить код прямо внутри серверного окружения. Итог — повышение привилегий или даже удаленное выполнение кода. Красиво? Да. Опасно? Еще как!

Сценариев, которые могут привести к этой уязвимости, хватает. Самый частый — когда небезопасный объект случайно становится доступным для удаленного использования. Классический пример — CVE-2019-0555.

Этот баг появился, когда разработчики Windows Runtime захотели работать с XML-документами. Они не стали изобретать велосипед, а просто прикрутили нужные интерфейсы к уже существующему COM-объекту XML DOM Document v6. Казалось бы, все четко: новые интерфейсы не поддерживали выполнение XSLT-скриптов, значит, передавать объект через границы привилегий безопасно. Но не тут‑то было! Злой хакер мог спокойно запросить старый интерфейс IXMLDOMDocument, который все еще оставался доступным, и с его помощью выполнить XSLT-скрипт, вырвавшись из песочницы. Итог — дырка в безопасности и повышение привилегий.

Еще один сценарий — использование асинхронного маршалинга. Это когда объект может передаваться как по значению, так и по ссылке, но платформа по умолчанию выбирает передачу по ссылке.

Например, в .NET классы FileInfo и DirectoryInfo сериализуемые, значит, их можно передавать в .NET Remoting по значению. Но есть нюанс: они также наследуются от MarshalByRefObject, что позволяет передавать их и по ссылке. Тут‑то атакующий и может сыграть на особенностях маршалинга. Он отправляет на сервер сериализованный объект, который при десериализации создает новый экземпляр внутри серверного процесса.

Дальше — хитрый ход: если атакующий может обратно получить этот объект, рантайм автоматически отправит его по ссылке, оставляя его «запертым» в серверном окружении. Итог? Злоумышленник сможет спокойно вызывать методы этого объекта, например создавать файлы, причем с привилегиями сервера. И вот тебе элегантное повышение прав.
Последний (и, пожалуй, самый интересный) сценарий — это злоупотребление встроенными механизмами поиска и создания объектов в системе удаленного взаимодействия. Здесь мы заставляем сервер порождать неожиданные объекты, которые можно использовать в своих интересах.

Например, в COM достаточно найти способ вызвать CoCreateInstance с произвольным CLSID, получить объект в ответ — и вот у нас уже выполняется произвольный код на сервере. Классический пример — CVE-2017-0211. Эта бага позволяла передавать через границу безопасности объект Structured Storage, который поддерживал интерфейс IPropertyBag. А вот этот интерфейс уже можно было использовать для создания любого COM-объекта в контексте сервера.

Что можно было провернуть? Все тот же XML DOM Document! Создаем его на сервере, получаем обратно по ссылке и... включаем XSLT-магию для выполнения произвольного кода. Вуаля — повышение привилегий на ровном месте.


А что насчет IDispatch?

IDispatch — это часть OLE Automation, одной из первых фич COM, созданной для удобной автоматизации. С его помощью клиент может динамически вызывать методы объекта без строгой типизации — удобно для скриптовых языков типа VBA и JScript.

Фишка в том, что IDispatch прекрасно работает даже через границы процессов и уровней привилегий. Хотя чаще его используют внутри одного процесса, например в компонентах ActiveX. Но если такой объект вдруг окажется доступен в удаленном взаимодействии, можно ожидать веселых последствий...

Чтобы клиент мог динамически вызывать методы объекта COM, сервер должен как‑то сообщить ему, какие параметры передавать и как их упаковывать. Для этого используется type library — файл с описанием типов, который хранится на диске. Клиент может запросить эту информацию через метод GetTypeInfo интерфейса IDispatch.

Теперь начинается самое интересное. В COM интерфейс для работы с библиотекой типов маршализуется по ссылке. То есть, когда клиент получает ITypeInfo, он на самом деле управляет объектом, который остается на сервере. А это значит, что все вызовы методов на этом интерфейсе выполняются в контексте сервера.

Если этот механизм использовать хитро, можно заставить сервер делать вещи, о которых его разработчики и не подозревали.

Интерфейс ITypeInfo открывает две заманчивые дверцы для атакующего: методы Invoke и CreateInstance.

С Invoke облом — он не поддерживает удаленный вызов, потому что требует загрузки type library в текущий процесс. А вот CreateInstance — совсем другая история. Он вполне ремотабельный и вызывает CoCreateInstance для создания COM-объекта по CLSID.

И тут самое вкусное: создаваемый объект будет жить в серверном процессе, а не у клиента. То есть, если атакующий провернет этот трюк, он сможет заспаунить на сервере любой COM-объект, который ему нужен. Дальше — дело техники.

Но стоп, если заглянуть в документацию CreateInstance, там нигде не видно параметра CLSID. Так как же тогда type library понимает, какой объект создать?

Фишка в том, что ITypeInfo описывает любой тип, который есть в type library. Когда клиент вызывает GetTypeInfo, он получает только информацию об интерфейсе, который хочет использовать. Если потом попробовать дернуть CreateInstance, ничего не выйдет — просто ошибка.

Но тут на сцену выходит CoClass. В type library можно хранить не только интерфейсы, но и CoClass-объекты, а вот они уже содержат CLSID нужного COM-объекта. И если CreateInstance вызывается на CoClass, все срабатывает: создается объект с заданным CLSID прямо внутри серверного процесса.

Как же нам превратить объект с информацией об интерфейсе в объект, представляющий класс? Все просто: у ITypeInfo есть метод GetContainingTypeLib, который возвращает ссылку на ITypeLib — интерфейс библиотеки типов. А уже через него можно перебрать все классы, определенные в type library.

И вот тут начинается веселье: среди этих классов может оказаться что‑то совершенно небезопасное при удаленном использовании. Если такой объект можно создать удаленно, это открывает дорогу к повышению привилегий или даже запуску кода на сервере.

Давай разберем это на практике! Сначала с помощью моего PowerShell-модуля OleView.NET найдем подходящие COM-сервисы, которые поддерживают IDispatch. Если среди них окажется что‑то уязвимое... Ну, ты знаешь, что делать!

Код:
PS> $cls = Get-ComClass -Service
PS> $cls | % { Get-ComInterface -Class $_ | Out-Null }
PS> $cls | ? { $true -in $_.Interfaces.InterfaceEntry.IsDispatch } |
Select Name, Clsid
Name                                       Clsid
---- -----
WaaSRemediation                            72566e27-1abb-4eb3-b4f0-eb431cb1cb32
Search Gathering Manager                   9e175b68-f52a-11d8-b9a5-505054503030
Search Gatherer Notification               9e175b6d-f52a-11d8-b9a5-505054503030
AutomaticUpdates                           bfe18e9c-6d87-4450-b37c-e02f0b373803
Microsoft.SyncShare.SyncShareFactory Class da1c0281-456b-4f14-a46d-8ed2e21a866f

Флаг -Service в Get-ComClass позволяет вытащить COM-классы, которые работают в локальных сервисах. Дальше мы запрашиваем у каждого класса список поддерживаемых интерфейсов. Вывод этой команды нам не нужен — вся инфа уже сохраняется в свойстве Interfaces.

Теперь самое интересное: отфильтруем только те классы, которые поддерживают IDispatch. В итоге у нас остается пять кандидатов.

Возьмем первого — WaaSRemediation — и заглянем в его type library. Кто знает, может, там затаилось что‑то интересное...

Код:
PS> $obj = New-ComObject -Clsid 72566e27-1abb-4eb3-b4f0-eb431cb1cb32
PS> $lib = Import-ComTypeLib -Object $obj
PS> Get-ComObjRef $lib.Instance | Select ProcessId, ProcessName
ProcessId ProcessName
--------- -----------
27020 svchost.exe
PS> $parsed = $lib.Parse()
PS> $parsed
Name               Version TypeLibId
---- -------- ---------
WaaSRemediationLib 1.0      3ff1aab8-f3d8-11d4-825d-00104b3646c0
PS> $parsed.Classes | Select Name, Uuid
Name                          Uuid
---- ----
WaaSRemediationAgent          72566e27-1abb-4eb3-b4f0-eb431cb1cb32
WaaSProtectedSettingsProvider 9ea82395-e31b-41ca-8df7-ec1cee7194df

Скрипт сначала создает COM-объект, а затем с помощью Import-ComTypeLib получает интерфейс библиотеки типов.

Чтобы убедиться, что объект реально работает вне процесса, мы маршализуем его через Get-ComObjRef и достаем инфу о процессе. Ожидаемо объект живет внутри svchost.exe — общего исполняемого файла для сервисов Windows.

Разбираться с type library вручную — то еще удовольствие, поэтому упрощаем себе жизнь: используем Parse, чтобы превратить ее в удобную объектную модель. Теперь можно спокойно вывалить список классов и посмотреть, есть ли среди них что‑нибудь интересное (и опасное).

Облом! В случае с этим COM-объектом все классы в type library уже зарегистрированы для работы в сервисе, так что ничего нового мы не получили.

Но не беда — нам нужен другой вариант: класс, который зарегистрирован только для работы в локальном процессе, но при этом описан в type library удаленного сервиса.

Такое бывает, потому что type library может использоваться одновременно и для локальных (in-process) компонентов, и для сервисов, работающих вне процесса. Если мы найдем такой класс, то сможем заставить удаленный сервис создать его у себя — и тут уже возможны очень интересные сценарии.

Четыре оставшихся COM-класса тоже не дали ничего полезного (один вообще оказался криво зарегистрирован и даже не экспортируется сервисом). На этом можно было бы сдаться... Но нет!

Дело в том, что type library может ссылаться на другие библиотеки типов, а их тоже можно просканировать с помощью тех же интерфейсов. Так что скрытые классы все‑таки есть — просто они не лежат на поверхности.

Давай копнем глубже и посмотрим, что откопается.

Код:
PS> $parsed.ReferencedTypeLibs
Name   Version TypeLibId
---- ------- ---------
stdole 2.0     00020430-0000-0000-c000-000000000046
PS> $parsed.ReferencedTypeLibs[0].Parse().Classes | Select Name, Uuid
Name       Uuid
---- ----
StdFont    0be35203-8f91-11ce-9de3-00aa004bb851
StdPicture 0be35204-8f91-11ce-9de3-00aa004bb851
PS> $cls = Get-ComClass -Clsid 0be35203-8f91-11ce-9de3-00aa004bb851
PS> $cls.Servers
Key Value
--- -----
InProcServer32 C:\Windows\System32\oleaut32.dll

В этом примере мы используем свойство ReferencedTypeLibs, чтобы посмотреть, какие библиотеки подтягиваются автоматически при разборе type library. В нашем случае видим единственную запись — stdole, которая почти всегда импортируется.

Но если повезет, можно найти и другие подключенные библиотеки, которые стоит исследовать. Давай разберем stdole и посмотрим, какие классы она экспортирует. Выясняется, что там всего два класса и один из них — StdFont.

Если посмотреть, где этот класс может быть создан, то окажется, что он зарегистрирован только для работы в локальном процессе. Отлично! Это наша цель для поиска уязвимостей.

Теперь, чтобы получить удаленный интерфейс к stdole, нам нужен тип, который на нее ссылается. Причина проста: базовые интерфейсы типа IUnknown и IDispatch определены именно в этой библиотеке, а значит, нужно найти объект, который их использует.

Погнали, попробуем создать объект StdFont прямо в удаленном COM-сервисе!

Код:
PS> $iid = $parsed.Interfaces[0].Uuid
PS> $ti = $lib.GetTypeInfoOfGuid($iid)
PS> $href = $ti.GetRefTypeOfImplType(0)
PS> $base = $ti.GetRefTypeInfo($href)
PS> $stdole = $base.GetContainingTypeLib()
PS> $stdole.Parse()
Name   Version TypeLibId
---- ------- ---------
stdole 2.0     00020430-0000-0000-c000-000000000046
PS> $ti = $stdole.GetTypeInfoOfGuid("0be35203-8f91-11ce-9de3-00aa004bb851")
PS> $font = $ti.CreateInstance()
PS> Get-ComObjRef $font | Select ProcessId, ProcessName
ProcessId ProcessName
--------- -----------
27020 svchost.exe
PS> Get-ComInterface -Object $Obj
Name                 IID                                  HasProxy   HasTypeLib
---- ---                                  --------   ----------
...
IFont                bef6e002-a874-101a-8bba-00aa00300cab True       False
IFontDisp            bef6e003-a874-101a-8bba-00aa00300cab True       True

Чтобы добраться до нужной библиотеки, мы используем хитрую комбинацию методов: GetRefTypeOfImplType и GetRefTypeInfo. Они позволяют вытащить базовый тип у существующего интерфейса. Затем с помощью GetContainingTypeLib получаем интерфейс подключенной библиотеки типов.

Разбираем ее и убеждаемся, что это действительно stdole. Все идет по плану. Теперь достаем type info для класса StdFont и вызываем CreateInstance.

Дальше самое важное: проверяем, где же создался объект. И бинго! Он заперт внутри процесса сервиса. В качестве финальной проверки запрашиваем его интерфейсы — и точно, это объект шрифта. Осталось понять, как заставить его делать что‑нибудь интересное.

Нам нужно найти способ эксплуатировать один из этих классов. Но есть проблема: доступным остается только StdFont. Второй класс, StdPicture, имеет встроенную проверку, которая блокирует его использование вне процесса.

Я не нашел явных уязвимостей в StdFont, но, честно говоря, не копал слишком глубоко. Так что если у кого‑то чешутся руки — дерзайте! Вполне возможно, там прячется какой‑нибудь баг, который можно использовать для повышения привилегий или удаленного выполнения кода.

Исследование системы сервисов зашло в тупик — по крайней мере, в плане атак на системные COM-сервисы. Возможно, есть COM-сервер, доступный из песочницы, но беглый анализ AppContainer не дал очевидных целей.

Однако, немного поразмыслив, я понял: этот метод все еще может быть полезен — для внедрения кода в процесс с тем же уровнем привилегий.

Как? Очень просто: перехватываем COM-регистрацию для StdFont! Достаточно поменять его CLSID в реестре, используя ключ TreatAs, и указать на любой другой класс, который можно эксплуатировать. Например, загружаем движок JScript в целевой процесс и исполняем произвольный скрипт.

Получается элегантный способ внедрить код в доверенный процесс, даже если нет прямых уязвимостей.

Я обычно не публикую техники инъекций — это уже ближе к теме малвари. Но есть один крайне интересный сценарий, где такой трюк может иметь серьезные последствия для безопасности.

Что, если с его помощью можно внедриться в Windows Protected Process?

По иронии только что разобранный нами WaaSRemediationAgent может оказаться идеальным пропуском в защищенный процесс. Давай разберемся, как это можно провернуть.

Код:
PS> $cls = Get-ComClass -Clsid 72566e27-1abb-4eb3-b4f0-eb431cb1cb32
PS> $cls.AppIDEntry.ServiceProtectionLevel
WindowsLight

Когда мы проверили уровень защиты хостового сервиса, оказалось, что он работает на уровне PPL-Windows (Protected Process Light — Windows)!

Это значит, что сервис запущен в защищенном режиме и просто так в него не влезть. Но если мы сможем как‑то использовать наши находки, то получится внедрить код в PPL-процесс, что открывает очень интересные возможности.

Давай попробуем выжать максимум из этого исследования!



Инъекция в защищенный процесс

В Microsoft не считают PPL жесткой границей безопасности. Это значит, что такие уязвимости обычно не закрываются срочными патчами. Вместо этого их могут исправить только в новой версии Windows, если вообще обратят на них внимание. А это открывает интересные возможности для атак.

Идея до безобразия проста: мы подменяем регистрацию класса StdFont, чтобы он указывал на другой класс. Когда мы создаем его через type library, объект будет исполняться в защищенном процессе (PPL).

Почему StdFont? Он универсален. Даже если WaaSRemediationAgent исчезнет в будущих обновлениях, этот метод все равно будет работать с другим COM-сервером.

Осталось только найти подходящий класс, который позволит нам запустить произвольный код и не сломается внутри PPL. Если такой найдется, тогда это уже не просто уязвимость, а шикарный механизм обхода защиты Windows.

К сожалению, скриптовые движки типа JScript сразу отпадают. Если перечитать мой предыдущий пост, узнаешь, что Code Integrity блокирует их загрузку в Protected Process. Так что этот путь закрыт.

Значит, нужен класс, который, во‑первых, доступен вне процесса, во‑вторых, разрешен для загрузки в защищенный процесс.

Тут у меня появилась идея: .NET DCOM!

Я уже писал, что .NET-компоненты в DCOM — это дырявая конструкция, которой не стоит пользоваться. Но в этом случае нам как раз нужна вся эта кривизна! Если мы сможем загрузить класс .NET COM внутрь PPL, то получим мощный вектор для выполнения произвольного кода.

Дальше — ищем нужный класс .NET COM и запускаем веселье.

Я уже как‑то рассказывал про атаки через сериализацию в .NET, но оказалось, что есть вариант гораздо проще и мощнее.

Фишка в том, что через DCOM можно создать объект System.Type. А имея доступ к объекту Type, можно делать произвольные рефлексивные вызовы — вызываем любые методы, загружаем любые классы.

А знаешь, какой самый жирный трюк? Загрузка сборки прямо из массива байтов! Это обходит проверку цифровой подписи и позволяет загружать любой код в защищенный процесс (PPL).

Итог: полный контроль над защищенным процессом без эксплоитов в самом PPL. Просто хакерская магия с DCOM и .NET.

Да, в Microsoft закрыли эту дыру, но оставили бэкдор — параметр AllowDCOMReflection. Если его включить, старая уязвимость возвращается, как ни в чем не бывало.

Поскольку мы не повышаем привилегии, а уже работаем с админскими правами (иначе нельзя изменить регистрацию COM-класса), то просто записываем в реестр

Код:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\AllowDCOMReflection = 1 (DWORD)

И загружаем .NET Framework внутрь защищенного процесса (PPL).

Чтобы успешно внедрить код в защищенный процесс, делаем следующее:

  1. Включаем DCOM Reflection в реестре.
  2. Подменяем COM-регистрацию StdFont, добавляя ключ TreatAs, чтобы он указывал на System.Object.
  3. Создаем объект WaaSRemediationAgent, чтобы работать через его type library.
  4. Получаем TypeInfo для класса StdFont через type library.
  5. Создаем объект StdFont через CreateInstance, но реально это загрузит .NET Framework и вернет нам экземпляр System.Object.
  6. Используем .NET Reflection для вызова System.Reflection.Assembly::Load(byte[]), чтобы загрузить кастомную сборку без проверки подписи.
  7. Создаем объект из загруженной сборки, чтобы сразу запустить произвольный код в защищенном процессе (PPL).
  8. Чистим реестр, убираем изменения, чтобы замести следы.

Итог: мы получили полный контроль над защищенным процессом Windows, используя хитрую комбинацию DCOM, .NET Reflection и COM-редиректа.

Важно написать этот эксплоит не на .NET-языке, иначе механизмы сериализации пересоздадут reflection-объекты в вызывающем процессе и инъекция не сработает. Оптимальный вариант — C++, но можно попробовать Python с pywin32 или ctypes. В своем PoC я использовал C++, и он схож с эксплоитом, который я писал для CVE-2014-0257, где подробно показано, как использовать DCOM Reflection.

Еще один нюанс — по умолчанию объекты .NET COM запускаются через .NET Framework v2, который больше не устанавливается в новых версиях Windows. Можно либо настроить запуск через .NET v4, что сложнее, либо просто установить v2 через Windows Components Installer, как сделал я. В итоге получаем C++ PoC, который позволяет выполнить произвольный код в защищенном процессе Windows, используя комбинацию DCOM, .NET Reflection и манипуляций с COM-регистрацией.

Мой PoC сработал с первого раза на Windows 10, но, к сожалению, на Windows 11 24H2 затея провалилась. Мне удалось создать .NET-объект, но при вызове любого метода возникала ошибка TYPE_E_CANTLOADLIBRARY.

Можно было бы на этом остановиться, ведь сам факт уязвимости уже доказан, но мне стало интересно, что именно ломается на Windows 11. Давай разберемся, почему это происходит, и посмотрим, можно ли что‑то сделать, чтобы обойти это ограничение и заставить PoC работать на последней версии Windows.



Проблема с Windows 11

Я смог доказать, что ошибка связана именно с защищенными процессами (PPL). Если изменить настройки сервиса и запустить его без защиты, PoC снова начинает работать. Значит, что‑то конкретно блокирует загрузку библиотек в защищенном режиме.

При этом type libraries в целом загружаются нормально, например stdole работает без проблем. Значит, ограничение касается именно .NET, а не всех COM-библиотек. Теперь нужно разобраться, что именно программисты Microsoft поменяли в обработке .NET внутри PPL на Windows 11.

Анализ работы PoC с Process Monitor показал, что во время выполнения загружается библиотека mscorlib.tlb. Она используется для создания stub-класса на сервере. Однако по какой‑то причине загрузка не срабатывает, из‑за чего stub не создается, а это, в свою очередь, ломает любые вызовы к объекту.

Тут у меня появилась догадка. В одном из прошлых постов я рассказывал, как атаковать процесс NGEN COM: модификация type library приводила к type-confusion, что позволяло перезаписать хендл KnownDlls и подгрузить произвольную DLL в память.

Но в Microsoft в последние годы серьезно занялись защитой KnownDlls — об этом писал Клемент Лабро и другие исследователи. Я заподозрил, что Windows 11 получила еще одно исправление, блокирующее атаку через подмену type library.

Теперь надо выяснить, как именно это ограничение работает и можно ли его обойти.

Погрузившись в oleaut32.dll, я нашел фикс, который нам мешает. Это метод VerifyTrust. Вот как он выглядит:

Код:
NTSTATUS VerifyTrust(LoadInfo *load_info) {
  PS_PROTECTION protection;
  BOOL is_protected;


  CheckProtectedProcessForHardening(&is_protected, &protection);
  if (!is_protected)
    return SUCCESS;
  ULONG flags;
  BYTE level;
  HANDLE handle = load_info->Handle;
  NTSTATUS status = NtGetCachedSigningLevel(handle, &flags, &level,
                                            NULL, NULL, NULL);
  if (FAILED(status) ||
     (flags & 0x182) == 0 ||
     FAILED(NtCompareSigningLevels(level, 12))) {
    status = NtSetCachedSigningLevel(0x804, 12, &handle, 1, handle);
  }
  return status;
}

Этот метод вызывается во время загрузки type library и проверяет уровень подписи файла (signing level). Это снова связано с механизмом кешированного уровня подписи.

Если у файла нет уровня подписи 12 (Windows Signing Level), код пытается принудительно установить его через NtSetCachedSigningLevel. Если это не удается, система считает, что файл нельзя загрузить в защищенный процесс, и выдает ошибку. В итоге type library не загружается и PoC ломается.

Кстати, похожий фикс блокирует атаку через Running Object Table, где можно было ссылаться на out-of-process type library. Но в данном случае нас интересует именно защита PPL. Теперь вопрос: можно ли как‑то обойти проверку подписи и заставить Windows загрузить нужную библиотеку?

Судя по выводу Get-AuthenticodeSignature, файл mscorlib.tlb действительно подписан, хоть и каталожной подписью. Сертификат — Microsoft Windows Production PCA 2011, тот же самый, что и у .NET Runtime DLL. Логично, что у него должен быть Windows Signing Level, так что что‑то тут не сходится.

Попробуем обойти систему! Достанем мой NtObjectManager для PowerShell и вручную установим кешированный уровень подписи. Может, это даст какие‑то подсказки, что именно идет не так. Если получится подделать подпись — дорога к инъекции снова открыта.

Код:
PS> $path = "C:\windows\Microsoft.NET\Framework64\v4.0.30319\mscorlib.tlb"
PS> Set-NtCachedSigningLevel $path -Flags 0x804 -SigningLevel 12 -Win32Path
Exception calling "SetCachedSigningLevel" with "4" argument(s): "(0xC000007B) - {Bad Image}
%hs is either not designed to run on Windows or it contains an error. Try installing the program again using the
original installation media or contact your system administrator or the software vendor for support. Error status 0x"
PS> Format-HexDump $path -Length 64 -ShowAll
00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F  - 0123456789ABCDEF
-----------------------------------------------------------------------------
00000000: 4D 53 46 54 02 00 01 00 00 00 00 00 09 04 00 00  - MSFT............
00000010: 00 00 00 00 43 00 00 00 02 00 04 00 00 00 00 00  - ....C...........
00000020: 25 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00  - %...............
00000030: 2E 0D 00 00 33 FA 00 00 F8 08 01 00 FF FF FF FF  - ....3...........

Попытка вручную установить уровень подписи обломилась с ошибкой STATUS_INVALID_IMAGE_FORMAT. Заглядываем в первые 64 байта mscorlib.tlb и обнаруживаем, что это сырая type library, а не PE-файл. А вот это уже проблема.

Обычно, даже если файл имеет расширение .TLB, он все равно запакован в PE как ресурс. Но тут, похоже, не тот случай. Windows, судя по всему, просто не умеет устанавливать кешированный уровень подписи файлам, которые не являются PE-образами.

Короче говоря, если мы не сможем заставить Windows поверить, что этот файл подписан, то он не загрузится в защищенный процесс. А без него мы не сможем создать stub-класс и вызвать .NET-интерфейсы через DCOM.

Не повезло! Но всегда есть обходные пути. Вопрос только в том, какой из них сработает.

Забавный момент: у меня есть виртуалка с Windows 11, где та же самая type library в не DLL-формате все же принимает кешированный уровень подписи. То есть в одном окружении Windows блокирует загрузку, а в другом — спокойно ее пропускает.
Очевидно, я каким‑то образом изменил конфигурацию этой VM, что позволило системе принимать подпись даже для raw type library. Но вот что именно я поменял — понятия не имею.
Рыться дальше мне, если честно, уже лень. Просто фиксирую факт: если бы удалось повторить эту магию на боевой системе, то защита снова бы сломалась.

Можно было бы попытаться найти старую версию type library, которая и подписана правильно, и запакована в PE, но ковыряться в архивах не хочется.

Конечно, наверняка есть и другой COM-объект, который можно загрузить вместо .NET и получить удаленное выполнение кода. Но мне принципиально хотелось добить именно этот способ.

В итоге решение оказалось проще, чем я думал. По какой‑то причине 32-битная версия type library (которая лежит в Framework, а не Framework64) упакована в DLL. А раз так, на нее можно установить кешированный уровень подписи — и вуаля, защита больше не мешает!

Код:
PS> $path = "C:\windows\Microsoft.NET\Framework\v4.0.30319\mscorlib.tlb"
PS> Format-HexDump $path -Length 64 -ShowAll
00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F  - 0123456789ABCDEF
-----------------------------------------------------------------------------
00000000: 4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00  - MZ..............
00000010: B8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00  - ........@.......
00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  - ................
00000030: 00 00 00 00 00 00 00 00 00 00 00 00 B8 00 00 00  - ................
PS> Set-NtCachedSigningLevel $path -Flags 0x804 -SigningLevel 12 -Win32Path
PS> Get-NtCachedSigningLevel $path -Win32Path
Flags : TrustedSignature
SigningLevel : Windows
Thumbprint : B9590CE5B1B3F377EAA6F455574C977919BB785F12A444BEB2...
ThumbprintBytes : {185, 89, 12, 229...}
ThumbprintAlgorithm : Sha256

Значит, чтобы провернуть атаку на Windows 11 24H2, просто меняем путь в реестре для регистрации type library — вместо 64-битной версии подсовываем 32-битную. После этого запускаем эксплоит заново.

Функция VerifyTrust сама автоматически установит кешированный уровень подписи, так что никаких дополнительных танцев с бубном реестром или подписыванием не требуется — все просто работает.

Хотя формально это другая версия type library, на практике это ничего не меняет. Генератор stub-классов продолжает работать без проблем, а значит, наша атака остается полностью рабочей. Ребята из Microsoft, хотите закрыть лазейку? Попробуйте еще раз.



Выводы

В этом исследовании я разобрал интересный класс уязвимостей в Windows, который, впрочем, актуален для любого объектно ориентированного механизма удаленного взаимодействия между процессами. Мы увидели, как можно запереть COM-объект в более привилегированном процессе, используя особенности OLE Automation, в частности интерфейс IDispatch и библиотеки типов.

Этот трюк позволяет обходить ограничения, создавая объекты внутри защищенных процессов и затем управляя ими удаленно. Это не просто локальный баг, а целый класс атак, который Microsoft закрывает уже не в первый раз... но мы снова нашли лазейку.

Хотя мне не удалось продемонстрировать повышение привилегий, я показал, как можно использовать интерфейс IDispatch в классе WaaSRemediationAgent, чтобы внедрить код в процесс с уровнем защиты PPL-Windows. Это, конечно, не максимальный уровень защиты, но дает доступ к большинству защищенных процессов, включая LSASS.

Мы увидели, что в Microsoft действительно стараются закрывать возможности для атак, например через подмену type library, но в нашем случае их защита не должна была мешать — ведь мы не изменяли саму type library.

Да, этот конкретный эксплоит требует администраторских привилегий. Но сама техника в общем случае не зависит от прав администратора. Если удастся найти подходящий COM-сервер, который экспортирует IDispatch, можно провести атаку от имени обычного пользователя, просто изменив локальную регистрацию COM и .NET. Вопрос лишь в том, какие сервисы еще остались открытыми для этой техники.
 
Сверху