Начиная с восьмой версии в Windows появился встроенный механизм контейнеров, которые позволяют изолировать процессы путем значительного усечения их прав. Этому системному механизму дали название Less Privileged App Container (LPAC), он поддерживается некоторыми приложениями, например браузером Chrome. В этой статье я покажу, как использовать его в своих программах.
Сендбокс-изоляция часто применяется в защитных приложениях, а также на ее основе строятся компоненты антивирусов, называемые HIPS (Host-based Intrusion Prevention System), и отдельные приложения для изолированных сред типа Sandboxie. Такие защитные механизмы реализованы через драйвер-фильтр режима ядра. Он сложен в написании и тестировании, имеет громадное количество шаблонного кода и должен перехватывать значительное количество функций NTAPI, чтобы менять их параметры на лету, таким образом создавая песочницу внутри файловой системы.
Существует более простой способ устроить изоляцию произвольных приложений. Инженеры Microsoft уже позаботились об этом и интегрировали интересный механизм в ядро Windows. Его суть заключается в том, что система жестко ограничивает доступ к устройствам (таким как микрофон, камера, GPS или модуль 4G), файлам в системе (иногда — даже для чтения) и процессам (ограничиваются межпроцессные взаимодействия). Также ограничения накладываются на работу с сетью (например, на открытие портов или сокетов), обращения к сетевому реестру и оконному интерфейсу других приложений.
Когда приложение запущено внутри LPAC, все разрешения ему требуется выдавать явно. Это весьма полезный механизм с точки зрения безопасности, если в приложении есть уязвимости, которые злоумышленник может использовать для повышения привилегий и доступа к другим ресурсам. Если уязвим, например, браузер, то такую атаку можно выполнить удаленно. Кроме того, когда запускаешь неизвестное приложение, неплохо было бы обезопасить себя от несанкционированных действий с его стороны, научившись запускать приложения в контейнере LPAC.
Прежде чем запускать приложение, нам нужно создать сам контейнер. В этом нам поможет функция WinAPI CreateAppContainerProfile
. Вот ее прототип:
HRESULT WINAPI CreateAppContainerProfile( _In_ PCWSTR pszAppContainerName, _In_ PCWSTR pszDisplayName, _In_ PCWSTR pszDescription, _In_ PSID_AND_ATTRIBUTES pCapabilities, _In_ DWORD dwCapabilityCount, _Out_ PSID *ppSidAppContainerSid );
И сам код создания контейнера:
WCHAR sandbox_name[] = L"SandboxLPAC"; WCHAR sandbox_desc[] = L"My SandboxLPAC"; PSID sid = NULL; HRESULT status; result = CreateAppContainerProfile(sandbox_name, sandbox_name, sandbox_desc, NULL, 0, &sid);
В случае ошибки неплохо было бы проверить, не создан ли наш контейнер ранее; если создан, то мы получим его SID. Вот прототип функции WinAPI, которая выясняет SID уже созданного контейнера:
HRESULT WINAPI DeriveAppContainerSidFromAppContainerName( _In_ PCWSTR pszAppContainerName, _Out_ PSID *ppsidAppContainerSid );
Далее код реализации проверки. Как видишь, он очень прост.
if (HRESULT_CODE(status) == ERROR_ALREADY_EXISTS) status = DeriveAppContainerSidFromAppContainerName(sandbox_name, &sid);
Так или иначе мы получаем SID контейнера.
Security Identifier (SID) — идентификатор безопасности, структура данных в Windows, которая может идентифицировать системные объекты, например элементы управления доступом (Access Control Entries, ACE), токены доступа (Access Token), дескрипторы безопасности (Security Descriptor). SID всегда начинается с буквы S, далее идут числа, которые обозначают номер редакции ОС, источники выдачи, удостоверяющие центры и другую информацию.
Насколько просто обойти изоляцию LPAC?
Загрузка ...
Итак, контейнер LPAC создан, SID получен. Теперь наша задача — заставить Windows запустить произвольное приложение в этом контейнере. Но сначала нам необходимо разобрать процесс запуска приложений и понять, как можно задавать определенные атрибуты запуска и какие системные структуры отвечают за это.
Для запуска приложений в Windows используется функция WinAPI CreateProcess
с массой параметров, и именно эта функция имеет ключевое значение в нашей задаче. Давай посмотрим на ее прототип и разберем основные параметры.
BOOL WINAPI CreateProcess( _In_opt_ LPCTSTR lpApplicationName, _Inout_opt_ LPTSTR lpCommandLine, _In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes, _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes, _In_ BOOL bInheritHandles, _In_ DWORD dwCreationFlags, _In_opt_ LPVOID lpEnvironment, _In_opt_ LPCTSTR lpCurrentDirectory, _In_ LPSTARTUPINFO lpStartupInfo, _Out_ LPPROCESS_INFORMATION lpProcessInformation );
Поле lpApplicationName
— это путь к исполняемому файлу, который мы собираемся запускать. Далее идут поля lpCommandLine
, lpProcessAttributes
, lpThreadAttributes
, bInheritHandles
, которые сейчас не представляют для нас интереса. Можем им всем присвоить значение NULL (FALSE).
А вот на поле dwCreationFlags
мы остановимся подробнее. Оно отвечает за флаги, которые устанавливают приоритет процесса и регламентируют его создание. Например, если в это поле передать значение CREATE_NO_WINDOW
для консольного приложения, то оно запустится без создания консольного окна. А если передать значение CREATE_SUSPENDED
, тогда процесс (основной поток) будет создан приостановленным (в состоянии ожидания пробуждения функцией ResumeThread
). Нам же необходимо задать флаг EXTENDED_STARTUPINFO_PRESENT
: он «разрешит» нам расширенные параметры запуска приложения.
Далее идет поле lpStartupInfo
, которое имеет тип LPSTARTUPINFO
. Это указатель на структуру STARTUPINFO
, которая регламентирует параметры основного окна приложения или терминала, а также его дескриптор.
Важный момент. При передаче флага запуска EXTENDED_STARTUPINFO_PRESENT
мы можем вместо стандартной структуры STARTUPINFO
передать ее усовершенствованную версию — структуру STARTUPINFOEX
. Она имеет вид:
typedef struct _STARTUPINFOEX { STARTUPINFO StartupInfo; PPROC_THREAD_ATTRIBUTE_LIST lpAttributeList; } STARTUPINFOEX, *LPSTARTUPINFOEX;
Можно считать, что это стандартная структура STARTUPINFO
(поле StartupInfo
), дополненная списком атрибутов запуска (поле lpAttributeList
). Эти атрибуты можно проинициализировать функцией WinAPI InitializeProcThreadAttributeList
:
BOOL WINAPI InitializeProcThreadAttributeList( _Out_opt_ LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList, _In_ DWORD dwAttributeCount, _Reserved_ DWORD dwFlags, _Inout_ PSIZE_T lpSize );
А теперь добавляем их в список параметров функцией UpdateProcThreadAttribute
:
BOOL WINAPI UpdateProcThreadAttribute( _Inout_ LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList, _In_ DWORD dwFlags, _In_ DWORD_PTR Attribute, _In_ PVOID lpValue, _In_ SIZE_T cbSize, _Out_opt_ PVOID lpPreviousValue, _In_opt_ PSIZE_T lpReturnSize );
Обрати внимание на поле PVOID lpValue
, к нему мы еще вернемся. А теперь переходим к практике. Весь код манипуляций с атрибутами потоков выглядит таким образом:
SIZE_T size_of_attr = 0; STARTUPINFOEX ex_start_info = { 0 }; SECURITY_CAPABILITIES secap = { 0 }; InitializeProcThreadAttributeList(NULL, 1, NULL, &size_of_attr); ex_start_info.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)malloc(size_of_attr); InitializeProcThreadAttributeList(ex_start_info.lpAttributeList, 1, NULL, &size_of_attr); UpdateProcThreadAttribute(ex_start_info.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES, &secap, sizeof(secap), NULL, NULL);
Первые три строки кода (если не считать объявление переменных) создают PROC_THREAD_ATTRIBUTE_LIST
, то есть список параметров запуска. Вызов WinAPI UpdateProcThreadAttribute
модифицирует его должным образом и создает нужную нам структуру STARTUPINFOEX ex_start_info
, которую мы передадим функции CreateProcess
. Четвертое поле этой функции, lpValue
, получает на вход структуру secap
, которая имеет тип SECURITY_CAPABILITIES
и выглядит так:
typedef struct _SECURITY_CAPABILITIES { SID AppContainerSid; PSID_AND_ATTRIBUTES Capabilities; DWORD CapabilityCount; DWORD Reserved; } SECURITY_CAPABILITIES, *PSECURITY_CAPABILITIES;
Чтобы «настроить» ее под свои нужды, заполняем поля перед вызовом UpdateProcThreadAttribute
. Первое поле структуры — это AppContainerSid
, в него мы передаем SID нашего контейнера LPAC. Второе поле — Capabilities
, оно тоже является структурой:
typedef struct _SID_AND_ATTRIBUTES { PSID Sid; DWORD Attributes; } SID_AND_ATTRIBUTES, *PSID_AND_ATTRIBUTES;
Третье поле (SECURITY_CAPABILITIES
) называется CapabilityCount
. Это счетчик параметров процесса, которыми мы его наделяем.
Давай посмотрим на код, который реализует все перечисленное выше и создает нам корректно заполненную структуру SECURITY_CAPABILITIES
.
SID_AND_ATTRIBUTES sid_attr; DWORD sid_size = SECURITY_MAX_SID_SIZE; sid_attr = (SID_AND_ATTRIBUTES *)malloc(sizeof(SID_AND_ATTRIBUTES)); ZeroMemory(secap, sizeof(SECURITY_CAPABILITIES)); ZeroMemory(sid_attr, sizeof(SID_AND_ATTRIBUTES)); sid_attr.Sid = malloc(SECURITY_MAX_SID_SIZE); CreateWellKnownSid(capabili, NULL, sid_attr.Sid, &sid_size); sid_attr.Attributes = SE_GROUP_ENABLED; &secap->Capabilities = sid_attr; &secap->AppContainerSid = sid;
Единственная сложность, которую ты можешь встретить в этом коде, заключается в неизвестной еще функции CreateWellKnownSid
. Она создает SID для значений, которые были предопределены заранее. Самое интересное для нас — это ее первое поле, в котором через переменную capabili
передается перечисление типа WELL_KNOWN_SID_TYPE
, где на момент написания статьи содержится 94 пункта. Они наделяют наш SID различными правами. Ознакомиться с полным списком можно в MSDN по ссылке. Для собственных экспериментов можно выбрать любой по вкусу. ????
Итак, все основные системные структуры созданы, сам контейнер LPAC создан, осталось только запустить блокнот в контейнере. Зададим нужные параметры вызова и наши заранее подготовленные структуры.
PROCESS_INFORMATION pinfo = { 0 }; BOOL ok = CreateProcessA("c:\windows\notepad.exe", NULL, NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, (LPSTARTUPINFOA)&ex_start_info, &pinfo);
Результат работы можно увидеть в программе ProcessExplorer: notepad.exe запустится внутри контейнера Less Privileged App Container.
Какое предустановленное приложение по умолчанию запускается в LPAC?
Загрузка ...
Как проверить, работает ли процесс в контейнере LPAC или нет, программным путем, без использования сторонних приложений? Достаточно получить хендл интересующего нас процесса. Если процесс сторонний, то нам поможет функция WinAPI OpenProcess
, а для своего пригодится GetCurrentProcess
. Далее открываем токен доступа процесса (Access token) и смотрим его TOKEN_INFORMATION_CLASS
, который будет равен TokenIsAppContainer
в том случае, если процесс работает внутри контейнера.
BOOL InLPAC(HANDLE h_proc) { HANDLE proc_token; DWORD len; BOOL lpac = 0; OpenProcessToken(h_proc, TOKEN_QUERY, &proc_token); if (!GetTokenInformation(proc_token, TokenIsAppContainer, &lpac, sizeof(lpac), &return_len)) return false; return lpac; }
Маркер доступа (Access token) — объект Windows, содержащий привилегии учетной записи пользователя, от которого был запущен процесс. Помимо этого, Access token содержит информацию об ограничениях доступа к потоку, здесь же перечислены SID и списки привилегий процесса. Посмотреть структуру маркера доступа можно, введя в WinDbg команду dt_TOKEN
.
На вход этой функции необходимо передать хендл интересующего нас процесса, и она вернет TRUE
, если процесс работает внутри Less Privileged App Container, и FALSE
, если процесс выполняется вне его.
С какими новыми методами защиты совместно работает LPAC?
Загрузка ...
Мы разобрали Less Privileged App Container, встроенную реализацию изолированной среды в Windows. Чтобы раскрыть тему, нам пришлось детально рассмотреть процесс запуска приложений в Windows средствами WinAPI CreateProcess
, а также узнать о нескольких важных системных структурах, без которых ничего бы не получилось. Я надеюсь, что эта статья поможет тебе в исследовании системных механизмов Windows.
Читайте также
Последние новости