Свин 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
👁 Пробить человека? Легко через нашего бота: Мистер Пробиватор