Свин API. Изучаем возможности WinAPI для пентестера

Свин API. Изучаем возможности WinAPI для пентестера

the Matrix

Для начала давай запом­ним нес­коль­ко тер­минов — ско­ро они нам при­годят­ся. Кон­текст поль­зовате­ля (r context), он же кон­текст безопас­ности (security context), — набор уни­каль­ных отли­читель­ных приз­наков поль­зовате­ля, слу­жащий для кон­тро­ля дос­тупа. Сис­тема хра­нит све­дения о кон­тек­сте в токене (его так­же называ­ют мар­кером дос­тупа). Рас­смот­рим их чуть более под­робно.

При вхо­де в сис­тему любой поль­зователь вво­дит свой логин и пароль. Затем, если под­клю­чена домен­ная учет­ная запись, эти дан­ные све­ряют­ся с хра­нили­щем учет­ных записей Active Directory на кон­трол­лере домена, которое называ­ется ntds.dit, либо с базой дан­ных локаль­ного компь­юте­ра — SAM.

Ес­ли пароль вер­ный, сис­тема начина­ет собирать све­дения об учет­ной записи. В слу­чае Active Directory так­же собира­ется информа­ция уров­ня домена (нап­ример, домен­ные груп­пы). И незави­симо от типа УЗ находят­ся све­дения, отно­сящи­еся к локаль­ной сис­теме, в том чис­ле перечень локаль­ных групп, в которых сос­тоит поль­зователь. Все эти дан­ные помеща­ются в спе­циаль­ную струк­туру, хра­нящу­юся в объ­екте ядра, который называ­ется токеном дос­тупа.

В сис­темах Windows у мно­гих объ­ектов — груп­пы, домена, поль­зовате­ля — сущес­тву­ет спе­циаль­ный иден­тифика­тор безопас­ности, SID (Security Identifier). Он име­ет вот такой фор­мат:

В этой записи:

При этом сущес­тву­ют и некото­рые стан­дар­тные SID. Они перечис­лены в до­кумен­тации Microsoft. Такие иден­тифика­торы называ­ются хорошо извес­тны­ми (well known).

То­кен же хра­нит внут­ри себя мно­жес­тво раз­личных SID, сре­ди которых мож­но выделить основные:

Дос­таточ­но слож­но, прав­да ведь? Но мож­но про­вес­ти прос­тую ана­логию. Токен — кар­точка сот­рудни­ка ком­пании. SID поль­зовате­ля — имя на этой кар­точке, SID груп­пы — напеча­тан­ная дол­жность. Сис­тема смот­рит на эту кар­точку каж­дый раз, ког­да мы начина­ем с ней вза­имо­дей­ство­вать.

В Windows есть про­цес­сы, а есть потоки. Говоря прос­тыми сло­вами, это некие объ­екты, обла­дающие собс­твен­ным вир­туаль­ным адресным прос­транс­твом. Потоком называ­ют ход выпол­нения прог­раммы. Поток выпол­няет­ся в рам­ках вла­деюще­го им про­цес­са, или, как говорят, в кон­тек­сте про­цес­са. Любое запущен­ное при­ложе­ние пред­став­ляет собой про­цесс, в кон­тек­сте которо­го выпол­няет­ся по край­ней мере один поток.

У про­цес­са есть токен. Чаще все­го исполь­зует­ся токен поль­зовате­ля, запус­тивше­го про­цесс. Ког­да про­цесс порож­дает дру­гие про­цес­сы, все они исполь­зуют этот же токен.

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

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

Ва­риант 1: получить токен опре­делен­ного про­цес­са.

Ва­риант 2: получить токен опре­делен­ного потока.

Пе­репи­сывать MSDN и объ­яснять каж­дый параметр как‑то неп­равиль­но. Если вдруг ты нез­наком с WinAPI, то можешь написать мне, ски­ну матери­ал для изу­чения. Пред­лагаю обра­тить вни­мание лишь на вто­рой параметр — DesiredAccess.

Здесь ты дол­жен ука­зать, какой тип дос­тупа к токену хочешь получить. Это зна­чение пре­обра­зует­ся в мас­ку дос­тупа, на осно­ве которой Windows опре­деля­ет, мож­но выдавать токен или нель­зя. WinAPI пре­дос­тавля­ет для такой мас­ки некото­рые стан­дар­тные зна­чения.

Об­рати вни­мание, что прос­то так засунуть TOKEN_ALL_ACCESS нель­зя: сис­тема баналь­но не выдаст токен, так как в эту мас­ку вхо­дит и TOKEN_ADJUST_SESSIONID, который тре­бует наличие при­виле­гии SeTcbPrivilege. Такой при­виле­гией обла­дает лишь сис­тема.

При этом дан­ную ошиб­ку допус­кают очень час­то. Нап­ример, лишь в вер­сии 4.7 инс­тру­мен­та Cobalt Strike был исправ­лен этот недочет.

Ча­ще все­го для наших задач мы будем ука­зывать при­виле­гию TOKEN_DUPLICATE, что­бы исполь­зовать фун­кцию DuplicateTokenEx(), которую мы раз­берем поз­же.

Ва­риант 3: зап­росить токен поль­зовате­ля, если мы зна­ем его логин и пароль.

То­кен так­же содер­жит информа­цию о при­виле­гиях поль­зовате­ля. У самих при­виле­гий в Windows есть два пред­став­ления:

Для про­вер­ки мож­но исполь­зовать сле­дующую фун­кцию:

Сам код может быть при­мер­но сле­дующий (при­нима­ет токен, в котором надо про­верить наличие при­виле­гии, и ее имя. Допус­тим, SE_DEBUG_NAME):

До­пус­тимые изме­нения делят­ся на две груп­пы:

Для боль­шинс­тва ситу­аций мож­но вос­поль­зовать­ся этой фун­кци­ей:

Ко­неч­но же, в токене воз­можно изме­нить далеко не все парамет­ры. Ниже опи­саны допус­тимые клас­сы информа­ции для SetTokenInformation(), а так­же при­виле­гии и мас­ки дос­тупа, которые для это­го тре­буют­ся.

Нап­ример, что­бы вклю­чить вир­туали­зацию UAC, исполь­зуй сле­дующий код:

При этом мы можем изме­нить и при­виле­гии, содер­жащи­еся в токене! Но тре­бует­ся знать, как получить из прог­рам­мно­го име­ни при­виле­гии ее LUID. Это поз­воля­ет сде­лать сле­дующая фун­кция:

Сле­дующим шагом мы дол­жны выз­вать AdjustTokenPrivilege():

Эта фун­кция может как вклю­чить при­виле­гии, так и отклю­чить их. Не знаю, почему Microsoft не реали­зова­ла что‑нибудь подоб­ное:

Этой фун­кции тре­бует­ся передать токен, прог­рам­мное имя при­виле­гии и булево зна­чение, TRUE или FALSE, то есть вклю­чить при­виле­гию или вык­лючить ее. При этом токен дол­жен иметь мас­ку TOKEN_ADJUST_PRIVILEGES.

Ча­ще все­го про­цесс исполь­зования получен­ного токена начина­ется с вызова фун­кции DuplicateTokenEx():

Она прос­то соз­дает новый токен, который дуб­лиру­ет ранее получен­ный. При этом мы дол­жны переда­вать токен, который был зап­рошен с мас­кой TOKEN_DUPLICATE. В dwDesiredAccess ты дол­жен ука­зать новую мас­ку дос­тупа. Допус­кает­ся исполь­зовать пре­доп­ределен­ные зна­чения из до­кумен­тации Microsoft.

И вновь может воз­никнуть путани­ца с ImpersonationLevel. Это дей­стви­тель­но уро­вень имперсо­нации, то есть он опре­деля­ет, нас­коль­ко токен может оли­цет­ворять объ­ект. Сущес­тву­ют сле­дующие вари­анты:

Сле­дующим шагом идет вызов CreateProcessWithTokenW(). Эта фун­кция соз­дает про­цесс, а затем при­вязы­вает к нему ука­зан­ный токен:

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

Смо­жешь догадать­ся, почему он не зарабо­тал? Ошиб­ка такая же, как и у Cobalt Strike. Для успешной экс­плу­ата­ции дос­таточ­но зап­росить мас­ку TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_QUERY в вызове OpenProcessToken().

Мож­но при­вязать токен и к опре­делен­ному потоку про­цес­са. Для это­го сущес­тву­ет сле­дующая фун­кция:

Нап­ример:

Ес­ли мы зна­ем учет­ные дан­ные поль­зовате­ля, то мож­но получить его токен и работать с ним вот так:

Для заимс­тво­вания прав под­клю­чен­ного поль­зовате­ля может слу­жить фун­кция ImpersonateLoggedOnUser(), которую мы уже рас­смот­рели, либо ImpersonateSelf():

Она про­дуб­лиру­ет токен нашего про­цес­са, соз­даст новый с ука­зан­ным типом имперсо­нации и свя­жет его с вызыва­ющим потоком.

Су­щес­тву­ет воз­можность имперсо­нации кли­ента пай­па:

Но обра­ти вни­мание, что для вызова этой фун­кции пот­ребу­ется при­виле­гия SeImpersonatePrivilege. Так­же мы получим токен с уров­нем имперсо­нации мень­шим, чем SecurityImpersonation, нап­ример SecurityIdentification или SecurityAnonymous, поэто­му необ­ходимо будет выз­вать DuplicateTokenEx().

Ес­ли нам тре­бует­ся про­вес­ти имперсо­нацию кли­ента, с которым мы вза­имо­дей­ству­ем, то мож­но исполь­зовать SSP (Security Support Proer). Самые популяр­ные средс­тва из этой катего­рии в Windows — NTLMSSP и Kerberos. Для прог­рамми­рова­ния с SSP исполь­зует­ся SSPI (Security Support Provider Interface). Он соз­дает так называ­емые бло­бы (security blobs) — пакеты дан­ных, которые переда­ются кли­ентом сер­веру и в обратном нап­равле­нии.

SSP поз­воля­ет нам выс­тро­ить кон­текст. С помощью выс­тро­енно­го кон­тек­ста мы смо­жем получить токен.

Сна­чала сле­дует перечис­лить все дос­тупные для текуще­го хос­та SSP. Это мож­но сде­лать с помощью сле­дующей фун­кции:

Нап­ример:

В pcPackages содер­жится количес­тво про­токо­лов защиты, которые были получе­ны, а ppPackageInfo будет содер­жать под­робную информа­цию. Он явля­ется экзем­пля­ром струк­туры SecPkgInfo, при этом сама струк­тура выг­лядит вот так:

Да­лее тре­бует­ся получить собс­твен­ные рек­визиты для SSP и опре­делить­ся с про­токо­лом защиты. В этом поможет сле­дующая фун­кция:

Нап­ример:

В pszPrincipal ука­зывай имя объ­екта, для которо­го мы получа­ем рек­визиты. NULL будет озна­чать, что нам тре­буют­ся рек­визиты для токена текуще­го потока. В pszPackage мы про­писы­ваем про­токол защиты, который будем исполь­зовать. Мож­но ука­зать параметр Name из струк­туры SecPkgInfo (смот­ри фун­кцию выше). Либо воз­можны сле­дующие вари­анты:

В про­цес­се аутен­тифика­ции кли­ент и сер­вер ведут себя по‑раз­ному, поэто­му нач­нем с раз­бора того, что дела­ет кли­ент.

Пер­вым делом кли­ент ини­циирует исхо­дящий кон­текст безопас­ности из дес­крип­тора учет­ных дан­ных, получен­ных в резуль­тате вызова фун­кции AcquireCredentialsHandle(). Обыч­но эта фун­кция вызыва­ется в цик­ле до тех пор, пока не будет уста­нов­лен дос­таточ­ный кон­текст безопас­ности.

Про­цесс пос­ледова­тель­ных вызовов ука­зан­ной фун­кции име­ет сле­дующие осо­бен­ности:

Пос­ледова­тель­ность вызовов сто­ит выпол­нять, ана­лизи­руя воз­вра­щаемое зна­чение фун­кции. Если она вер­нула SEC_I_CONTINUE_NEEDED, то кли­ент вновь дол­жен ее выз­вать. Воз­врат SEC_E_OK озна­чает, что кон­текст удач­но пос­тро­ен.

При этом дан­ная фун­кция не вер­нет управле­ние до тех пор, пока сер­вер, к которо­му кон­нектят­ся, не вызовет AcceptSecurityContext().

Для работы с бло­бами исполь­зуют­ся вход­ные и выход­ные буферы. При их исполь­зовании тре­бует­ся соз­дать мас­сив перемен­ных SecBuffer, которые дол­жны ука­зывать на выделен­ные нами буферы памяти. Затем мы прис­ваиваем адрес это­го мас­сива экзем­пля­ру типа SecBufferDesc. Он ука­зыва­ет, сколь­ко буферов содер­жится в мас­сиве.

Что­бы сис­тема сама выдели­ла мес­то под эти буферы, в вызове фун­кции InitializeSecurityContext() в парамет­ре fContextReq тре­бует­ся ука­зать ISC_REQ_ALLOCATE_MEMORY.

На­конец, ито­говая фун­кция на кли­енте будет выг­лядеть сле­дующим обра­зом:

Спе­шу заметить, что мы исполь­зуем здесь фун­кции SendData() и RecvData(). Это может быть любая фун­кция для вза­имо­дей­ствия, хоть сокеты WSA с их WsaRecv(), WSASend(), хоть ReadFile(), WriteFile(). SSP незави­сим от спо­соба переда­чи дан­ных, кон­текст мож­но выс­тро­ить хоть на голубях 🙂

Пос­ле того как на кли­енте запуще­на фун­кция InitializeSecurityContext(), она не будет воз­вра­щать управле­ние до тех пор, пока сер­вер не запус­тит свою фун­кцию AcceptSecurityContext():

Здесь исполь­зуют­ся все те же парамет­ры, что и в InitializeSecurityContext(), кро­ме двух зарезер­вирован­ных и име­ни сер­вера. Понят­но, что в рас­смат­рива­емом слу­чае пос­леднее не нуж­но, так как эта фун­кция запус­кает­ся на самом сер­вере. Роль такая же — фун­кция дол­жна запус­кать­ся в цик­ле до тех пор, пока не вер­нет SEC_E_OK. У нее есть два отли­чия:

Вро­де бы все оди­нако­вое, но получен­ный от этой фун­кции кон­текст обла­дает бОль­шими воз­можнос­тями, чем резуль­тат, получен­ный от InitializeSecurityContext(). Этот кон­текст мы смо­жем исполь­зовать для имперсо­нации кли­ента.

При­мер пос­тро­ения кон­тек­ста на сер­вере так­же дос­таточ­но прост:

Пос­ле того как у нас успешно отра­бота­ли фун­кции AcceptSecurityContext() и InitializeSecurityContext(), мы получим на сер­вере хендл пол­ного кон­теста. Его мож­но исполь­зовать сле­дующим обра­зом.

Фун­кция ImpersonateSecurityContext поз­воля­ет сер­веру оли­цет­ворять кли­ента с помощью хен­дла кон­тек­ста, ранее получен­ного вызовом AcceptSecurityContext() или QuerySecurityContextToken(). Фун­кция ImpersonateSecurityContext() дает сер­веру воз­можность выс­тупать от лица кли­ента при всех про­вер­ках прав дос­тупа:

Поз­воля­ет прек­ратить оли­цет­ворение вызыва­юще­го объ­екта и вос­ста­новить собс­твен­ный кон­текст безопас­ности:

С помощью этой фун­кции мы можем дос­тать токен поль­зовате­ля, кон­текст которо­го получен из фун­кции AcceptSecurityContext():

То­кены — это один из стол­пов безопас­ности в сис­темах Windows. Сегод­ня ты получил пред­став­ление лишь об осно­вах работы с ними. Если инте­рес­но пог­рузить­ся глуб­же, то поп­робуй изу­чить огра­ничен­ные токены (CreateRestrictedToken()) и их осо­бен­ности.


Источник

Наши проекты:

- Кибер новости: the Matrix
- Хакинг: /me Hacker
- Кодинг: Minor Code
👁 Пробить человека? Легко через нашего бота: Мистер Пробиватор

Report Page