soft :: ос

Безопасность объектов, находящихся в памяти, для Windows NT

Безопасность объектов, находящихся в памяти, для Windows NT

Всем памятен тот шум, который летом прошлого года был поднят вокруг "страшной дыры в Windows". Ее не обошли вниманием ни Bugtraq, ни SecurityFocus, ни другие маститые сайты, посвященные компьютерной безопасности. Осенью разработчики из Microsoft выпустили заплатку, после установки которой эксплойт для winlogon, дающий возможность локальной эскалации привилегий, перестал работать. Однако существует немало других уязвимых приложений, работающих с повышенными привилегиями, поэтому тема о "страшном баге" продолжает оставаться популярной. Не так давно "Сетевые решения" (№6 2003 г.) опубликовали статью, где первооткрыватель ставшей знаменитой "дыры" объясняет способ ее использования. Но есть в этой статье одно очень подозрительное для программиста место: каким образом пользователь с привилегиями гостя может отладчиком читать память сервиса? И если уж может, что виновато: сервис или система?

Мнение автора — виноват сервис. Просто потому, что запретить пользователю лезть в чужую область памяти достаточно просто. Более того: при грамотном подходе по ограничению доступа сервис нередко можно заменить на процесс, стартующий с учетной записью пользователя. В первую очередь это можно и нужно делать тогда, когда программа оформлялась как сервис исключительно с целью не дать пользователю вмешиваться в ее работу.
Немного теории. Как известно, Windows NT использует ACL (Access Control Lists, списки контроля доступа) для определения, какие операции пользователь может совершать над объектом. Каждый ACL состоит из набора ACE (Access Control Entry, элементов списка контроля доступа). В свою очередь, каждый ACE состоит из SID (Security IDentifier) и битовой маски, определяющей права пользователя, с которым ассоциирован этот SID.
С каждым объектом, поддерживающим разграничение доступа, система связывает Security Descriptor — дескриптор безопасности, содержащий информацию о владельце объекта, группе пользователей, к которой принадлежит владелец, и собственно списки доступа. Большинство функций Win32 используют не непосредственно SD, а атрибуты безопасности — структуру, содержащую, помимо дескриптора безопасности, поле ее размера и флаг наследования атрибутов дочерними объектами.

Для установки параметров безопасности объектов файловой системы используется функция SetFileSecurity, принимающая в качестве одного из параметров имя файла; для работы же с объектами в памяти используются функции SetUser ObjectSecurity и SetKernelObject Security, использующие дескриптор требуемого объекта. Поскольку имя файла доступно всегда (если, конечно, не запрещен доступ к каталогу, где он находится), а объект в памяти для работы с ним надо открыть, причем при открытии запрошенный уровень доступа сравнивается с разрешенным ACL, возникает парадоксальная ситуация: запретив себе доступ к объекту, вернуть его уже невозможно. Казалось бы, очевидная недоработка. Однако ею можно воспользоваться.
В первую очередь, необходимо вспомнить, что среди объектов, поддерживающих разграничение доступа, — не только проекции файлов в память или объекты синхронизации. К ним относятся и окна, потоки и процессы. Следовательно, для того, чтобы получить "бессмертный" процесс, совершенно необязательно создавать сервис. Достаточно создать пустой ACL и связать его с требуемым процессом. Это делается так:

var
lpSecDesc: pointer;
lpACL: PACL;
dwSecurityInfo: DWORD;
begin
GetMem(lpSecDesc, SE-CURITY_DESCRIPTOR_MIN_LENGTH);
InitializeSecurityDes-criptor(lpSecDesc, 1);
GetMem(lpACL, SizeOf (TACL));
InitializeACL(lpACL^, SizeOf(TACL), 2);
SetSecurityDescriptor DACL(lpSecDesc, True, lp ACL, False);
dwSecurityInfo:= DACL_SECURITY_INFORMATION;
SetKernelObjectSecuri-ty(GetCurrentProcess, dw SecurityInfo, lpSecDesc);
FreeMem(lpACL);
FreeMem(lpSecDesc);
end;

Вот и все. Вызываем диспетчер задач, пытаемся завершить наш процесс и любуемся окошком Отказано в доступе. Если теперь запретить пользователю доступ к папке Автозагрузка, то наша программа будет запускаться и работать независимо от желания пользователя. Также теперь наш процесс, помимо всего прочего, защищен от внедрения DLL (скорее всего, не будет работать клавиатурный хук).
Но главное — не переусердствовать. Во-первых, если не обрабатывать такие сообщения, как WM_ENDSESSION, при выходе пользователя из системы наша программа не будет завершена сразу и вызовет предупреждающее окно о том, что "для завершения программы, возможно, потребуется дополнительное время" с кнопкой немедленного закрытия. Кнопка эта работоспособна, т.к. на саму систему ограничения доступа не распространяются, но все-таки лучше, чтобы это раздражающее окошко не появлялось.
Во-вторых, поскольку дескриптор безопасности может наследоваться, процесс, доступ к которому запрещен, не указав при создании дочерних объектов непустые атрибуты безопасности, рискует сам не получить к ним доступ. Поэтому лучше сразу же четко определить, какие виды доступа к объектам могут потребоваться и почему. Приведенный ниже пример создает список доступа, разрешающий пользователю чтение, а системе — полный доступ. Такие параметры доступа можно использовать, например, для события, следящего за наложенным вводом-выводом.

function GetAccountSID (Account: PChar): pSID;
var
dwDomainNameSize: DWORD;
dwSIDSize: DWORD;
dwSIDNameUse: SID_NAME_ USE;
lpDomainName: PChar;
begin
dwSidSize:= 0;
dwDomainNameSize:= 0;
LookupAccountName('', Account, nil, dwSIDSize, nil, dwDomainNameSize, dwSIDNa-meUse);
GetMem(result, dwSIDSi-ze);
GetMem(lpDomainName, dw DomainNameSize);
LookupAccountName('', Account, result, dwSIDSi-ze, lpDomainName, dwDomain NameSize, dwSIDNameUse);
FreeMem(lpDomainName);
end;

var
lpACL: PACL;
lpUserSID: pSID;
lpSystemSID: pSID;
dwUserNameSize: DWORD;
dwACLSize: DWORD;
szUserName: array [0..256] of Char;
const
ACCESS_ALLOWED_ACE_SIZE = 12;
begin
dwUserNameSize:= 256;
GetUserName(szUserName, dwUserNameSize);
lpUserSID:= GetAccount SID(szUserName);
lpSystemSID:= GetAcco-untSID('SYSTEM');
dwACLSize:= SizeOf (TACL) + 2*ACCESS_ALLOWED_ ACE_SIZE — SizeOf(DWORD) + GetLengthSID(lpUserSID) + GetLengthSID(lpSystem SID);
GetMem(lpACL, dwACLSi-ze);
InitializeACL(lpACL^, dwACLSize, 2);
AddAccessAllowedACE (lpACL^, 2, GENERIC_READ, lpUserSID);
AddAccessAllowedACE (lpACL^, 2, STANDARD_RIGHTS_ ALL or SPECIFIC_RIGHTS_ALL or MAXIMUM_ALLOWED, lpSys temSID);

Как ни печально, но в большинстве случаев необходимые права доступа к объектам можно подбирать только методом проб и ошибок. Ненужные права записи могут позволить злонамеренному пользователю исказить ваши данные; недостаток прав приведет к тому, что компоненты программы не смогут общаться между собой и с системой.
В заключение хочется отметить, что ограничение доступа к различным объектам защищенной программы — условие необходимое, но далеко не достаточное для уверенности в ее надежности.
Существует огромное количество "правил хорошего тона", позволяющих избежать таких неприятных и не делающих чести разработчику уязвимостей, как переполнение буфера или атака вида "format string". О некоторых из них — в следующий раз.

Денис Подковырин,
sector@gala.net




© компьютерная газета