|
|
|||||||||||||||||||||||||||||
|
Защита объектов в NTИсточник: delphikingdom Юрий Спектор
Автор: Юрий Спектор, Королевство Delphi
Системы линейки 9x не являются многопользовательскими в том понимании, что не позволяют разграничить доступ к ресурсам, а лишь позволяют выбрать профиль - способ отображения данных в соответствии с настройками того или иного пользователя. В системах линейки NT доступ к объектам управляется операционной системой. Защищаемыми объектами могут быть файлы, устройства, почтовые ящики, каналы, задания, процессы, потоки, объекты синхронизации, порты завершения ввода-вывода, разделы общей памяти, сетевые ресурсы, разделы реестра и др. Механизмы, о которых пойдет речь далее, применимы только к системам линейки NT. В данной статье описаны в основном только механизмы работы. Несмотря на то, что в статье присутствуют примеры, поясняющие те или иные моменты, использующиеся в примерах функции подробно не описываются. Функций, так или иначе касающихся безопасности, очень много. Привести полный их перечень и описание в рамках данной статьи невозможно. Предполагается, что читатель умеет пользоваться справочной системой и при необходимости сможет ознакомиться с ними самостоятельно. Пользователи и группыКак уже было сказано, Windows NT является многопользовательской системой и позволяет управлять доступом к своим объектам между несколькими потребителями. Для каждого зарегистрированного пользователя система создает учетную запись - запись, содержащую сведения о данном пользователе. Учетные записи всех пользователей хранятся в некой системной базе данных. Она представляет собой таблицу, схематически показанную на рисунке 1. Рисунок 1. База данных учетных записей Для каждой учетной записи система хранит имена, пароли и уникальные идентификаторы - SID (Security Identifier) . Последний используется системой в дальнейшем везде, где нужно однозначно сослаться на ту или иную учетную запись. Структуру SID рассматривать не будем, можете ознакомиться самостоятельно, для нас пока важна лишь возможность с его помощью идентифицировать учетную запись. База данных учетных записей содержит сведения не только о пользователях, но и группах пользователей (например, "Администраторы"), которые также имеют SID. Группы позволяют нескольким пользователям задать общие права доступа. Управление учетными записями с помощью групп позволяет упростить работу администратора по контролю доступа пользователей к ресурсам. Ниже приведен фрагмент кода, позволяющий получить строковое представление SID пользователя. На входе функции нужно задать имя компьютера (пустая строка - локальный компьютер) и пользователя. Описания API приводиться не будут, вы сами сможете найти их в MSDN. function ConvertSidToStringSid(Sid: PSID; var StringSid: PChar): BOOL; stdcall; external advapi32 name 'ConvertSidToStringSidA'; function GetUserSIDStr(SystemName, AccountName: String): String; var PSID, PRef: Pointer; SIDSize, RefSize, peUse: Cardinal; sSID: PChar; begin Result:=''; SIDSize:=0; RefSize:=0; // Первый вызов функции позволяет получить необходимые размеры буферов // для SID и имени домена LookupAccountName(PChar(SystemName),PChar(AccountName),nil,SIDSize,nil, RefSize,peUse); GetMem(PSID,SIDSize); GetMem(PRef,RefSize); try // Получаем SID учетной записи if not LookupAccountName(PChar(SystemName),PChar(AccountName),PSID, SIDSize, PRef,RefSize,peUse) then RaiseLastOSError; // Конвертируем SID в строковое представление if ConvertSidToStringSid(PSID,sSID) then begin SetLength(Result,StrLen(sSID)); StrCopy(PChar(Result),sSID); LocalFree(Cardinal(sSID)); end; finally FreeMem(PRef); FreeMem(PSID); end; end; Кроме того, рекомендую самостоятельно ознакомиться с функциями GetUserName, InitializeSid, AllocateAndInitializeSid, GetSidLengthRequired, CopySid, EqualSid, FreeSid, GetLengthSid, IsValidSid, GetSidIdentifierAuthority, GetSidSubAuthority, GetSidSubAuthorityCount, LookupAccountSid, ConvertStringSidToSid, CreateWellKnownSid. Маркер доступаТеперь давайте рассмотрим, что происходит, когда пользователь входит в систему. После успешной проверки подлинности имени пользователя и пароля система создает так называемый маркер доступа (Access token). Маркер доступа представляет собой объект, который содержит кроме всего прочего SID пользователя, прошедшего аутентификацию, и SID групп, в которые этот пользователь входит. Рисунок 2. Аутентификация. Для каждого процесса, созданного данным пользователем, система создает копию маркера доступа и прикрепляет ее к процессу. Маркер доступа является как бы пропуском, удостоверяющим личность пользователя, создавшего процесс. Кроме SID пользователя и групп, маркер доступа содержит и другие элементы, (такие как список привилегий, например). В рамках данной статьи они рассматриваться не будут. А сейчас приведем код, позволяющий получить маркер доступа текущего процесса и извлечь из него список SID пользователя и групп, в которые он входит. type PTokenUser = ^_TOKEN_USER; _TOKEN_USER = record User : TSidAndAttributes; end; function SIDToStr(SID: PSID): String; var sSID: PChar; begin Result:=''; if ConvertSidToStringSid(SID,sSID) then begin SetLength(Result,StrLen(sSID)); StrCopy(PChar(Result),sSID); LocalFree(Cardinal(sSID)); end; end; procedure GetSIDStrList(List: TStringList); var hToken, Len: Cardinal; pTU: PTokenUser; pTG: PTokenGroups; i: Integer; begin List.Clear; // Получаем маркер доступа процесса if not OpenProcessToken(GetCurrentProcess,TOKEN_QUERY,hToken) then RaiseLastOSError; try // Получаем указатель на SID пользователя и добавляем строковое // представление в List GetTokenInformation(hToken,TokenUser,nil,0,Len); GetMem(pTU,Len); try if not GetTokenInformation(hToken,TokenUser,pTU,Len,Len) then RaiseLastOSError; List.Add(SIDToStr(pTU^.User.Sid)); finally FreeMem(pTU); end; // Получаем указатели на SID групп и добавляем строковые представления // в List GetTokenInformation(hToken,TokenGroups,nil,0,Len); GetMem(pTG,Len); try if not GetTokenInformation(hToken,TokenGroups,pTG,Len,Len) then RaiseLastOSError; for i:=0 to pTG^.GroupCount - 1 do begin List.Add(SIDToStr(pTG^.Groups[i].Sid)); end; finally FreeMem(pTG); end; finally // Закрываем дескриптор маркера доступа CloseHandle(hToken); end; end; В данном коде упрощена проверка ошибок, чтобы не загромождать его лишними вызовами. По-хорошему, при получении размеров буферов нужно проверять код ошибки на равенство ERROR_INSUFFICIENT_BUFFER. Если код ошибки другой, то ее причина не связана с недостаточным размером буфера. С помощью функции GetTokenInformation можно получить и другие параметры маркера доступа. С ее возможностями вы сможете ознакомиться самостоятельно, как и с другими функциями, работающими с маркером доступа. Защита объектов, дескрипторы безопасностиПереходим непосредственно к защите объектов. На данном этапе, вы должны знать, что пользователи и группы идентифицируются с помощью SID, и каждый процесс в системе знает, от чьего имени он выполняется - эта информация содержится в маркере доступа процесса. Как уже было сказано, маркер доступа представляет собой что-то вроде пропуска, теперь давайте выясним, кому этот пропуск нужно предъявлять. Для того чтобы ограничить доступ к объекту, его нужно снабдить дескриптором безопасности (Security descriptor) . Это и есть тот охранник, стоящий на страже объекта и не пропускающий к нему никого чужого. Дескриптор безопасности представляют собой структуру следующего вида: _SECURITY_DESCRIPTOR = record Revision: Byte; Sbz1: Byte; Control: SECURITY_DESCRIPTOR_CONTROL; Owner: PSID; Group: PSID; Sacl: PACL; Dacl: PACL; end; Несмотря на то, что дескриптор безопасности - это структура, работать напрямую ее полями не следует. Нужно использовать специальные функции, которые будут рассмотрены чуть позже. А пока ознакомимся с полями этой структуры. Поле Revision - это номер версии, на данный момент может иметь только значение SECURITY_DESCRIPTOR_REVISION. Sbz1 служит для выравнивания и должно содержать нули. Control - набор флагов, описывающих свойства дескриптора безопасности. Owner - SID владельца объекта. Владелец объекта имеет право изменять настройки доступа к объекту, даже если в это явно запретить в DACL (см. далее). SID первичной группы (параметр Group) нужен исключительно для совместимости со стандартом POSIX. Поля Sacl и Dacl являются указателями на соответствующие списки контроля доступа, которые для нас (особенно DACL) представляют наибольшее значение. Чуть позже мы перейдем к более подробному их обсуждению, а пока рассмотрим некоторую ситуацию. Допустим, мы пишем серверное приложение, выполняющееся под системной учетной записью, и нам нужно создать объект (например, событие Event), который будут совместно использовать и наш сервер, и клиенты, выполняющиеся под учетными записями пользователей. Для этого в первую очередь нужно присвоить объекту имя и поместить его в глобальное пространство имен, чтобы другие пользователи могли его увидеть. hEvent:=CreateEvent(nil,true,false,'Global\evMyServerObject'); Однако если мы попытаемся получить дескриптор этого объекта с помощью OpenEvent в приложении, выполняющемся под ограниченной учетной записью, мы получим отказ в доступе. Почему? Потому что мы не задали атрибуты безопасности для этого объекта (первый параметр) и не указали в них дескриптор безопасности. В результате, система создала объект и назначила ему дескриптор безопасности по умолчанию, который открывает полный доступ только создателю объекта и всем членам группы администраторов, а остальным - закрывает. А как система узнала, что мы не имеем права доступа к этому объекту? Наверняка она проверила наш пропуск (маркер доступа) и на этом основании сделала вывод, что мы права доступа к объекту не имеем. А где написано, кто имеет доступ к объекту, а кто не имеет? Ответ - эта информация записана в дескрипторе защиты, а именно - в одном из его списках контроля доступа. Списки контроля доступа (ACL). DACLКак видно из структуры дескриптора безопасности, он содержит два поля, содержащих в своем названии 'ACL' (Access-Control List) - список контроля доступа . Таких списков два: Discretionary Access-Control List , (DACL) - список управления избирательным доступом и System Access-Control List (SACL) - системный список управления доступом . Их структура схожа, однако они выполняют совершенно разные функции. В данном разделе будет более подробно рассмотрен DACL, так как он имеет для нас наибольшее значение. Именно DACL формирует правила, кому разрешить доступ к объекту, а кому - запретить. Поэтому все, что будет сказано о списках контроля доступа и его элементах, в большей степени относится именно к DACL. SACL позволяет лишь управлять аудитом (об этом - ниже). Каждый список контроля доступа (ACL) представляет собой набор элементов контроля доступа (Access Control Entries, или ACE) . Чтобы не было путаницы в терминах, изобразим схематически (рисунок 3). Рисунок 3. ACL и ACE ACE бывает двух типов (разрешающий и запрещающий доступ) и обязательно содержит три поля:
Таким образом, ACL, изображенный на рисунке 3, (если это не просто ACL, а DACL) устанавливает следующие правила: пользователю SID1 разрешить доступ на чтение объекта, но запретить доступ на запись, а пользователю SID2 - разрешить полный доступ к объекту. Кроме того, к дескриптору безопасности применимы следующие правила:
Возвращаясь к примеру из прошлого раздела, покажем, как нужно было создать событие, доступ к которому может иметь любой пользователь. var sa: TSecurityAttributes; sd: TSecurityDescriptor; hEvent: THandle; begin // Создаем дескриптор безопасности InitializeSecurityDescriptor(@sd,SECURITY_DESCRIPTOR_REVISION); // DACL не установлен - объект незащищен SetSecurityDescriptorDacl(@sd,true,nil,false); // Настраиваем атрибуты безопасности, передавая туда указатель на // дескриптор безопасности sd и создаем объект-событие sa.nLength:=SizeOf(TSecurityAttributes); sa.lpSecurityDescriptor:=@sd; sa.bInheritHandle:=false; hEvent:=CreateEvent(@sa,true,false,'Global\evMyServerObject'); Система сопоставляет маркер доступа и дескриптор защиты не при каждом обращении к объекту, а только при получении его дескриптора (рисунок 4). Например, если имеется защищенный объект-мьютекс, система проверяет права только при вызове процессом функции OpenMutex. В этой же функции процесс сразу же указывает, какие виды доступа ему в дальнейшем потребуются. Если все указанные виды доступа разрешены - процесс получает дескриптор объекта и может его использовать сколь угодно долго в соответствии с запрошенными правами. Даже если DACL объекта изменится. После того как процесс получил дескриптор, система не сможет его забрать. Если же система принимает решение отказать в доступе, процесс просто не получит дескриптор объекта. Рисунок 4. Запрос доступа к объекту. Таким образом, продолжая аналогию, дескриптор безопасности - это охранник, маркер доступа - это пропуск, а DACL - это приказ охраннику, кому давать доступ, а кому - отказывать. Ну и в заключение главы отметим, что ядро обращается к объектам не через дескрипторы, а через указатели. Права доступа в режиме ядра не проверяются, другими словами, система полностью сама себе доверяет и всегда имеет доступ к объекту. Работа с ACEРассмотрим такую ситуацию. А что, если два ACE противоречат друг другу? Например, один ACE дает полный доступ членам определенной группы, а другой - запрещает доступ определенному пользователю из этой группы. Получит ли этот пользователь доступ к объекту? А это зависит от того, в каком порядке ACE расположены. Когда процесс запрашивает определенный вид доступа к защищенному дескриптором безопасности объекту, система действует по следующему алгоритму:
Чтобы было понятнее, поясним на примере. Допустим, процесс, запущенный от имени пользователя User1, входящего в группу Group1, запрашивает доступ к объекту на чтение и запись. DACL дескриптора безопасности объекта имеет следующий вид:
Доступ к объекту с таким DACL на чтение и запись будет разрешен, несмотря на последний элемент. Так как проход по DACL начинается сверху вниз, а первый элемент предоставит право на чтение, а второй - на запись. На этом система и остановится. А если последний элемент поместить наверх, то доступ будет запрещен. А теперь поставим обратную задачу: в системе зарегистрировано 5 пользователей: User1..User5. User1 и User2 входят в группу Group1. User3, User4 и User5 входят в группу Group2. Необходимо составить такой DACL, который:
DACL будет выглядеть, например, так:
Теперь на небольшом примере продемонстрируем работу с DACL и его элементами. Создадим небольшое приложение, которое по нажатию на одну кнопку запускает процесс (Windows-калькулятор) и назначает ему дескриптор защиты, разрешающий всем пользователям любой вид доступа, кроме PROCESS_TERMINATE (доступ на завершение процесса). По нажатию на другую кнопку, попробуем получить дескриптор этого процесса, затребовав запрещенный вид доступа. Если все сделать правильно, на экране появится окошко, сообщающее об отказе в доступе. Сразу скажу, что для простоты в данном примере опущена всяческая обработка ошибок, однако в "боевых" приложениях не стоит ее игнорировать. // Эта функция будет работать только на системах, начиная с WinXP function CreateWellKnownSid(WellKnownSidType: Cardinal; DomainSid, Sid: PSID; var cbSID: Cardinal): Bool; stdcall; external advapi32 name 'CreateWellKnownSid'; const WinWorldSid = 1; aclSize = 1024; ACL_REVISION = 2; // По неизвестным причинам, ее нет в Windows.pas (D7) var pi: TProcessInformation; procedure TForm1.Button1Click(Sender: TObject); var si: TStartupInfo; sa: TSecurityAttributes; sd: TSecurityDescriptor; dacl: PACL; SID: PSID; SIDLength: Cardinal; begin // Инициализируем дескриптор безопасности InitializeSecurityDescriptor(@sd,SECURITY_DESCRIPTOR_REVISION); GetMem(dacl,aclSize); try // Инициализируем DACL. Выделяем буфер заведомо большего размера // В принципе, можно точно рассчитать необходимый размер буфера. // Как это сделать написано в MSDN в описании функции InitializeAcl InitializeAcl(dacl^,aclSize,ACL_REVISION); SIDLength:=0; // Получаем SID группы, включающей всех пользователей CreateWellKnownSid(WinWorldSid,nil,nil,SIDLength); GetMem(SID,SIDLength); try CreateWellKnownSid(WinWorldSid,nil,SID,SIDLength); // Добавляем в DACL ACE, разрешающий любой вид доступа, кроме // PROCESS_TERMINATE всем пользователям. Запрещающий ACE добавляется // с помощью функции AddAccessDeniedAce AddAccessAllowedAce(dacl^,ACL_REVISION,PROCESS_ALL_ACCESS and not PROCESS_TERMINATE,SID); // Устанавливаем DACL в дескриптор безопасности SetSecurityDescriptorDacl(@sd,true,dacl,false); // Инициализируем атрибуты безопасности и создаем процесс sa.nLength:=SizeOf(TSecurityAttributes); sa.lpSecurityDescriptor:=@sd; sa.bInheritHandle:=false; FillChar(si,SizeOf(TStartupInfo),0); si.cb:=SizeOf(TStartupInfo); CreateProcess('C:\WINDOWS\system32\calc.exe',nil,@sa,nil,false,0, nil,nil,si,pi); CloseHandle(pi.hThread); finally FreeMem(SID); end; finally FreeMem(dacl); end; end; procedure TForm1.Button2Click(Sender: TObject); var hProcess: Cardinal; begin hProcess:=OpenProcess(PROCESS_TERMINATE,false,pi.dwProcessId); if hProcess = 0 then RaiseLastOSError else TerminateProcess(hProcess,0); end; В примере используется функция CreateWellKnownSid. С помощью нее мы получаем указатель на SID группы 'everyone' (SID этой группы всегда один и тот же - S-1-1-0), которая включает всех пользователей. Эта функция в данном случае мне показалась наиболее простой и удобной, однако на системах младше XP работать не будет. Вы можете использовать другой способ, например, с помощью AllocateAndInitializeSid. Кроме функции для работы с ACL и ACE, использующихся в примере, рекомендую самостоятельно ознакомиться с AddAccessDeniedAce, AddAce, DeleteAce, GetAce, GetAclInformation, IsValidAcl, SetAclInformation, SetEntriesInAcl. Системный список управления доступом (SACL). АудитДескриптор безопасности содержит указатель на System Access-Control List (SACL) - список, похожий на DACL, но отвечающий не за разрешение или запрет на доступ, а за аудит (протоколирование в журнале безопасности) успешных и безуспешных попыток доступа к объекту. Благодаря системе аудита, администратор может узнать, кто, каким образом и когда пользовался (или пытался пользоваться, но получил отказ в доступе) интересующими его ресурсами. Для работы с SACL используются как общие для всех ACL функции, так и специфичные - AddAuditAccessAce, AddAuditDeniedAce, GetSecurityDescriptorSacl, SetSecurityDescriptorSacl. Изменение дескриптора безопасности существующего объекта. Абсолютные и относительные дескрипторы безопасностиВ данной главе мы вернемся к примеру с процессом, защищенным от завершения, попытаемся снять это ограничение и завершить его. А заодно познакомимся с функциями, позволяющими получать и изменять дескриптор безопасности уже существующего объекта. Для того чтобы изменить DACL объекта, процесс должен обладать правом WRITE_DAC, однако, во-первых, мы не запрещали его в DACL (PROCESS_ALL_ACCESS включает это право), а даже если бы запретили - оно все равно у нас было бы на правах владельца (поле Owner в дескрипторе безопасности), так как владелец всегда имеет право изменять DACL. Для получения дескриптора безопасности процесса можно воспользоваться функцией GetKernelObjectSecurity. Но использование этой функции порождает некоторые проблемы. Дело в том, что при вызове функции InitializeSecurityDescriptor, получается так называемый абсолютный (absolute) дескриптор безопасности. Свое название такой тип получил из-за того, что указатели, содержащиеся в нем, представляют собой абсолютный адрес в памяти процесса и указывают на структуры памяти, не входящие в сам дескриптор. При связывании дескриптора с объектом, система объединяет все данные, относящиеся к дескриптору в одну компактную структуру. Такой тип дескриптора называется самоопределяющийся относительный (self-relative) . Название данный тип получил из-за того, что указатели в нем теперь содержат не абсолютный адрес, а относительное смещение элемента в данной структуре. Такой тип дескриптора удобен, если его нужно сбросить в какое-либо хранилище, (например - в файл) или переслать по сети. Также файловая система NTFS хранит дескриптор безопасности для каждого файла или папки именно в таком формате. Проверить формат дескриптора можно с помощью GetSecurityDescriptorControl. Когда мы вызываем GetKernelObjectSecurity (или другие подобные функции), система возвращает именно относительный дескриптор. Многие функции, такие как SetSecurityDescriptorDacl, требуют дескриптор безопасности именно в абсолютном формате. Имеются специальные функции, позволяющие преобразовывать дескриптор из одной формы в другую (MakeAbsoluteSD, MakeSelfRelativeSD), однако их использование делает код очень громоздким (функция MakeAbsoluteSD имеет 11 параметров!). Другая неприятность заключается в том, что для работы с разными типами объектов нужно использовать разные функции: для объектов ядра, таких как процесс, поток, мьютекс и др. - Get(Set)KernelObjectSecurity, для файлов - Get(Set)FileSecurity, для user-объектов - Get(Set)UserObjectSecurity, для ключей реестра - RegGet(Set)KeySecurity и др. К счастью, у этих проблем есть одно простое и элегантное решение - функции GetSecurityInfo и SetSecurityInfo. Их можно использовать на системах, начиная от NT 4.0. Для того чтобы продемонстрировать преимущества этих функций, напишем с их помощью код, снимающий запрет на завершение процесса. В модуле Windows.pas этих функций нет (по крайней мере, на Delphi 7), можете импортировать их самостоятельно или воспользоваться заголовочными файлами JEDI API - http://jedi-apilib.sourceforge.net/. Итак, бросаем на форму из примера с защищенным процессом еще одну кнопку и прописываем ей такой обработчик щелчка: uses JwaAclApi, JwaAccCtrl; // JEDI procedure TForm1.Button3Click(Sender: TObject); var hProcess: Cardinal; begin // Открываем процесс. Запрашиваем доступ на изменение DACL hProcess:=OpenProcess(WRITE_DAC,false,pi.dwProcessId); if hProcess <> 0 then begin // Устанавливаем указатель на DACL равным nil - объект незащищен SetSecurityInfo(hProcess,SE_KERNEL_OBJECT,DACL_SECURITY_INFORMATION, nil,nil,nil,nil); CloseHandle(hProcess); end else RaiseLastOSError; end; Вот и все. В функции SetSecurityInfo мы просто устанавливаем объекту nil в качестве DACL. Для сравнения, без использования этой функции нам нужно было бы:
Даже не буду пытаться привести код для сравнения, так как выигрыш при использовании SetSecurityInfo очевиден. Хотя, именно в данном случае можно было не получать установленный объекту дескриптор и преобразовывать его в абсолютный, а создать новый с нуля. Однако в большинстве случаев нужно сохранить многие параметры существующего дескриптора, которые заранее неизвестны. Теперь запускаем пример на выполнение. Щелкаем сначала на первую кнопку - создаем процесс, затем на вторую - получаем отказ в доступе, на третью - снимаем с процесса защиту, и снова вторую - вуаля! Процесс завершился. Хочу также обратить ваше внимание еще на две функции, подобные SetSecurityInfo и GetSecurityInfo - SetNamedSecurityInfo и GetNamedSecurityInfo. Их основное отличие от упомянутых ранее в том, что вместо дескриптора объекта нужно указать его имя. ЗаключениеВ данной статье были рассмотрены основные принципы защиты объектов на системах линейки NT. Многие моменты, такие как запуск процессов от имени другой учетной записи, олицетворение или имперсонация (выполнение отдельных потоков процесса от имени другой учетной записи), привилегии (особые полномочия, перечисленные в маркере доступа, предоставляющие их владельцам исключительные права на доступ к объектам, а также позволяющие выполнять привилегированные операции) и многие другие, не были рассмотрены, однако надеюсь, что изложенный в статье материал поможет вам самим при необходимости с ними разобраться. Благодарности: SLoW - за советы, конструктивную критику и просто поддержку при написании данной статьи.
Ссылки по теме
|
|