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

От редакции

Мы решили в качес­тве экспе­римен­та начать зна­комить читате­лей с луч­шими мировы­ми иссле­дова­ниями. Далее — близ­кий к тек­сту перес­каз пос­та Джей­мса Фор­шоу из бло­га Google Project Zero. Эта пуб­ликация дос­тупна без плат­ной под­писки.

Объ­ектно ори­енти­рован­ные тех­нологии уда­лен­ного вза­имо­дей­ствия, такие как 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, что поз­воля­ет переда­вать их и по ссыл­ке. Тут‑то ата­кующий и может сыг­рать на осо­бен­ностях мар­шалин­га. Он отправ­ляет на сер­вер сери­али­зован­ный объ­ект, который при десери­али­зации соз­дает новый экзем­пляр внут­ри сер­верно­го про­цес­са.

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

www

Этот трюк я реали­зовал в сво­ем инс­тру­мен­те ExploitRemotingService.

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

Нап­ример, в 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-про­цесс, что откры­вает очень инте­рес­ные воз­можнос­ти.

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

 

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

Я уже пи­сал (и рас­ска­зывал на кон­ферен­циях) про инъ­екции в Windows Protected Processes. Если хочешь разоб­рать­ся глуб­же, советую перечи­тать тот пост — там под­робно раз­бира­ются пре­дыду­щие ата­ки.

Но вот клю­чевой момент: в 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. Воп­рос лишь в том, какие сер­висы еще оста­лись откры­тыми для этой тех­ники.

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии