» Защита объектов в Windows Borland Delphi. Win Api. windows. Блог программистов


Блог программистов




201012 Dec

Защита объектов в Windows

Здравствуйте читатели блога программистов. После долгого перерыва, который длился более чем полгода на блоге новая статья. В этой статье я расскажу про защиту объектов в операционных системах Windows. Данная статья будет полезна каждому, который начал изучать системное программирование в системах Windows, а также тем которые хотят разобраться в механизмах защиты в Windows. В статье речь, конечно, будет идти про системы Windows NT. Всё, что будет здесь сказано, будет справедливо для систем Win 2000, XP, Vista и Se7en. Итак, приступим.

   Многие объекты в операционной системе Windows являются защищаемыми. Понятие защищаемый объект подразумевает, что доступ к нему контролируется операционной системой и в зависимости от привилегий или прав может быть разрешён или запрещён. К защищаемым объектам в Windows относятся следующие объекты: процессы, потоки, объекты файлового мэпинга, разделы реестра, службы, объекты синхронизации (мьютексы, семафоры и т.д.), маркеры доступа, ну и конечно файлы и каталоги (если используется файловая система NTFS) и ещё несколько типов объектов.
Второе не менее важное понятие – это идентификатор защиты или SID. SID уникальный идентификатор, переменной длины, который однозначно определяет пользователя или группу пользователей. Именно этими идентификаторами, а не именами пользователей руководствуется система при разграничении доступа к объектам.
   Начнём с самого главного и ключевого объекта участвующего в механизме защиты объектов в Windows – маркера доступа. Маркер доступа (access token, или просто token) содержит информацию безопасности сеанса входа, определяющую пользователя, группы пользователей и привилегии. Операционная система использует маркер доступа для контроля доступа к защищаемым объектам и контролирует возможность выполнения пользователем различных связанных с системой операций на локальном компьютере. Иными словами маркер доступа однозначно определяет, кем является пользователь, что можно и что нельзя делать пользователю на данной системе.
   Если рассматривать маркер доступа как объект, то маркер доступа создаётся в момент, когда пользователь вводит свой логин и пароль (при вызове функции LogonUser). Маркер доступа можно создать, модифицировать, удалить, т.е. это такой же объект, как и любой другой (процесс, файл, семафор и т.д.). После авторизации пользователя, его маркер доступа присваивается всем процессам, запускаемым в контексте этого пользователя. Каждый процесс имеет свою собственную копию маркера доступа, таким образом, даже если модифицировать маркер доступа процесса, то это никак не повлияет на другие процессы, работающие в контексте данного пользователя. Тоже самое, относится и к потокам. Маркер доступа является неотъемлемым атрибутом любого процесса и любого потока. С маркерами доступа работают такие функции как OpenProcessToken, SetTokenInformation, AdjustTokenGroups, AdjustTokenPrivileges и т.д.
  Маркер доступа содержит следующую информацию:
1.SID пользователя
2.SID’ы групп в которых состоит пользователь
3.SID ассоциированный с текущей сессией
4.SID владельца
5.SID первичной группы
6.Список привилегий
7.Список DACL по-умолчанию
8.Тип маркера: первичный или олицетворение
9.Текущий уровень олицетворения
10.Статистика
Из всего этого списка важны всего лишь два поля SID пользователя и список привилегий. Каждая привилегия в списке привилегий может иметь два состояния: включенное или выключенное. Если привилегия находится в выключенном состоянии, то нельзя выполнять действия, которые она разрешает. Зачем нужна эта мера не совсем понятно, так как на деле это не более чем дополнительная формальность, что-то вроде бюрократии в системе защиты Windows. Включить или выключить привилегию не составляет никакого труда, более важным является само наличие привилегии, так как если привилегия отсутствует, то и включить её не получится.
   Второе не менее важное понятие это описатель защиты или security descriptor. Описатель защиты это свойство защищаемого объекта и притом неотъемлемое свойство. Описатель защиты указывается при создании объекта, и даже если при создании объекта мы его не указываем, то система это делает за нас и назначает описатель защиты по-умолчанию. Описатель защиты содержит SID владельца объекта, SID первичной группы, список DACL и список SACL.
В большинстве случаев важны всего лишь две вещи: SID владельца и DACL. Так как SID может задавать как пользователя, так и группу, следовательно, владельцем может быть как конкретный пользователь, так и целая группа пользователей. Список DACL задаёт разрешения и запреты для различных пользователей или групп, притом для каждого пользователя или группы могут задаваться различные права доступа. Список SACL отвечает за логирование попыток доступа к искомому объекту. SID первичной группы не используется всеми Win32 (или Win64) программы, это поле было введено для поддержки UNIX-приложений работающих в подсистеме posix, этот параметр описателя защиты можно смело игнорировать при написании программ для Windows.

   Подытожим вышесказанное. Маркер доступа запрещает или разрешает выполнение некоторых действий, таких как возможность отладки процессов, изменение системного времени, загружать/выгружать драйвера, изменять квоты процесса и т.д. Дескриптор защиты в свою очередь указывает, кому можно обращаться к искомому объекту, а кому нельзя.
  &nbspРассмотрим весь процесс работы с объектами в Widows с самого начала. Всё начинается с того, что мы вводим наше имя и пароль, даже если мы их не вводим, они вводятся автоматически. В обоих случаях происходит вызов функции LogonUser. Функция извлекает информацию о пользователе и его правах из базы данных SAM (если компьютер не состоит в домене) или из базы данных Active Directory и создаёт маркер доступа на основе полученных данных. Функция LogonUser в случае успеха возвращает хендл тоукена (он же хендл маркера доступа). Далее с помощью функции CreateProcessAsUser и имеющегося тоукена происходит запуск процесса userinit.exe, который производит запуск всех процессов отвечающих за интерфейс пользователя, в частности процесса explorer.exe, который является основой интерфейса Windows. После этого любой запущенный нами процесс выполняется от нашего имени, так как маркер доступа наследуется от процесса explorer.exe.
Как происходит работа с объектами. Например, некоторый процесс хочет переименовать некоторый ключ реестра (именно ключ, а не значение, так значению не может быть присвоен описатель защиты). Для возможности работы с некоторым объектом должна получить его хендл (OpenProcess, OpenMutex, OpenEvent, RegOpenKeyEx, CreateFile и т.д.). При получении хендла объекта программа указывает флаги доступа (чтение, запись, выполнение, удаление и т.д.). При попытке открытия объекта (более правильно получении хендла объекта) система проверяет, имеет ли пользователь право на доступ к этому объекту по списку DACL. Например, программа, которая работает в контексте пользователя A, хочет получить доступ к ключу реестра с чтением и записью (проще говоря, хендл с чтением и записью), и в списке DACL пользователю A разрешено только чтение, то система откажет в доступе к этому объекту, проще говоря, не выдаст программе хендл.
  &nbspВ списке DACL могут находиться как разрешающие, так и запрещающие правила, притом если для пользователя есть разрешающие и запрещающие правила, то приоритет имеют запрещающие. В большинстве случаев запрещающие правила не используются и на первый взгляд кажутся лишней и совершенно не нужной возможностью, но тем не менее они придают большую гибкость системе защиты.
   Почему в системах Vista, Server 2008 R2 и Se7en чтобы выполнять некоторые действия надо запускать программу от администратора. В отличие от Win 2000/XP/2003 в системах Windows Vista и старше при входе пользователя в систему создаётся два маркера доступа: один маркер с ограниченными правами другой со всеми правами (разумеется, которыми обладает пользователь). По-умолчанию, все программы запускаются с ограниченными маркером доступа, а при выборе пункта «Запуск от администратора» программа запускается с маркером доступа со всеми правами (которыми обладает пользователь). Таким образом, пользователя ограждают действий (случайных или не случайных), которые могут нанести вред системе, например, при случайном запуске вируса, то он ничего «криминального» сделать не сможет, так как будет работать с ограниченными правами. При попытке запуска программы с максимальными правами система выведет сообщение для подтверждения этого действия. Данная мера является не более чем защитой о случайных действий «пользователей-чайников», так как чаще всего именно их действия приводят к неработоспособности операционной системы. В общем случае в системах Windows Vista и выше программа всегда обладает только теми правами, которые ей нужны.

   Для начала напишем функцию, которая получает имя пользователя по SID. Получение информации о пользователе через SID осуществляет функция LookupAccountSid, следующая функция возвращает указатель на имя пользователя через SID.


function GetNamebySID(destSystem: PChar; sid: PSID):PChar;
var
  _userName : PChar;
  _Domain: PChar;
  _Needed : DWORD;
  _DomLen : DWORD;
  _use  : SID_NAME_USE;
begin
  Result := 0;
  _Needed := 0;
  _DomLen := 0;
  LookupAccountSid(destSystem, sid, 0, _Needed, 0, _DomLen,  _use);

  if GetLastError() = ERROR_INSUFFICIENT_BUFFER then
   begin
    Result := HeapAlloc(GetProcessHeap(), 0, _Needed);
    _Domain:= GetMemory(_DomLen);
    LookupAccountSid(destSystem, sid, Result, _Needed, _Domain, _DomLen, _use);
    FreeMemory(_Domain);
   end;
end;

   В начале происходит вызов функции LookupAccountSid с нулевыми размерами выходных буферов и если мы получаем код ошибки свидетельствующий именно о том что размеры буферов не подходят, то выделяем необходимые буферы для имени пользователя и имени домена. Так как имя домена нас не интересует мы его сразу удаляем.
   С маркером доступа в общем случае «побаловаться» не получится, если нам что-то запрещено, значит запрещено, если разрешено, то разрешено. Маркер доступа чаще используется для получения вспомогательной или статистической информации. Например, имея тоукен какого-либо процесса можно узнать под каким пользователем он работает. Следующая функция получает имя пользователя по хендлу процесса.


function GetProcessUserName(Process:THandle):PChar;
var
  _Token:THandle;
  _Info:PTOKEN_USER;
  _Needed:DWORD;
begin
  Result:=0;
  if not OpenProcessToken(Process, TOKEN_QUERY, _Token) then exit;
  _Needed:=0;
  GetTokenInformation(_Token, TokenUser, 0, 0, _Needed);
  if GetLastError() = ERROR_INSUFFICIENT_BUFFER then
   begin
    _Info := HeapAlloc(GetProcessHeap(), 0, _Needed);
    if GetTokenInformation(_Token, TokenUser, _Info, _Needed, _Needed) then
     Result:=GetNamebySID(0, _Info^.User.Sid);
    HeapFree(GetProcessHeap(),0, _Info);
   end;
end;

Имея в арсенале вышенаписанную функцию нетрудно получить имя текущего пользователя.


function GetCurrentUserName():PChar;
begin
  Result:=GetProcessUserName(GetCurrentProcess());
end;
. . . .
MessageBox(0, GetCurrentUserName(), 'Current user', MB_ICONINFORMATION);

   Для нас более интересна работа с описателем защиты. Описатель защиты. В подавляющем большинстве случаев при создании объектов (файлов, процессов, ключей реестра и т.д.) программы не указывают описатель защиты, и система присваивает объекту описатель защиты по-умолчанию. При создании объектов в user mode описатель защиты указывается через структуру SECURITY_ATTRIBUTES. (Вспоминаем, что при создании процессов, ключей реестра, мьютексов, семафоров, файлов и других системных объектов мы указывали NULL вместо указателя на структуру SECURITY_ATTRIBUTES).
   Дескриптор защиты можно указать при создании объекта, можно изменить у уже существующего объекта через функцию SetSecurityInfo или SetNamedSecurityInfo (разумеется, если нам это разрешает текущий описатель защиты объекта). Отличие функций состоит в том, что первая функция получает хендл объекта, а вторая имя. Во втором случае мы избавляемся от необходимости получать хендл объекта, но при этом мы теряем возможность работать с безымянными объектами. Де-факто, функция SetNamedSecurityInfo является всего лишь оболочкой вокруг функции SetSecurityInfo. Получить атрибуты защиты можно через аналогичные функции GetSecurityInfo или GetNamedSecurityInfo.
Рассмотрим общий случай работы с атрибутами защиты: получение и изменение атрибутов у уже существующего объекта.
   Чтобы получить информацию о безопасности объекта по имени надо вызвать функцию GetNamedSecurityInfo, указать имя объекта и его тип. Получить имя владельца и имя первичной группы не сложно, имея в арсенале функцию GetNameBySID, которую мы написали выше. Остаётся только получить в читабельном виде список DACL. Чтобы получить DACL необходимо указать в флаг DACL_SECURITY_INFORMATION в третьем параметре функции GetNamedSecurityInfo.
Список DACL состоит из массива ACE. Каждый ACE в DACL имеет следующую структуру:


typedef struct _ACCESS_ALLOWED_ACE { // aaace
    ACE_HEADER Header;
    ACCESS_MASK Mask;
    DWORD SidStart;
} ACCESS_ALLOWED_ACE;

   Поле Mask содержит флаги доступа, которые разрешены или запрещены указанному пользователю или группе. Поле SidStart содержит не указатель на SID как с первого взгляда может показаться, а начальные четыре байта идентификатора защиты (остальные данные SID идут сразу после этого поля). Поэтому для того чтобы получить указатель на SID достаточно получить указатель на само поле SidStart, а не его содержимое. Разрешающие и запрещающие ACE совершенно идентичны по структуре и различаются только элементом Header.AceType. В ACE также можно указать SID ассоциированный с текущей сессией и тогда это правило будет действовать только для текущего пользователя и только до момента его выхода из системы.
   Получить некоторый ACE из DACL можно чер ез функцию GetAce. Функция принимает три параметра указатель на DACL (или SACL), номер элемента в списке и указатель на структуру ACCESS_ALLOWED_ACE или ACCESS_DENIED_ACE. Если указанный номер элемента больше чем количество элементов в списке, то функция просто вернёт ошибку. Для получения всего списка надо просто вызывать функцию GetAce, увеличивая каждый раз индекс, пока не будет получена ошибка.
   Следующий кусок кода выводит список пользователей в ListBox, которые указаны в разрешающих ACE, в описателе защиты объекта, имя которого указано в ObjectNameEdit.Text.


_Res:=GetNamedSecurityInfo(PAnsiChar(ObjectNameEdit.Text), _Objtype,
                       OWNER_SECURITY_INFORMATION or
                       GROUP_SECURITY_INFORMATION or
                       DACL_SECURITY_INFORMATION,
                       @_Owner, @_Group,
                       @_DACL, 0,
                       _SecDescr);
  if _ResERROR_SUCCESS then
   begin
    ShowError(_Res);
    Exit;
   end;

. . . . .
  UsersAllowListBox.Clear;

  _i:=0;
  while GetAce(_DACL^, _i, pointer(_ACE)) do
   begin
    inc(_i);
    if _ACE.AceType = ACCESS_ALLOWED_ACE_TYPE then
     begin
      _name : =GetNamebySID(0, @(PACCESS_ALLOWED_ACE(_ACE).SidStart));
      UsersAllowListBox.Items.AddObject(_name,TObject(PACCESS_ALLOWED_ACE(_ACE).Mask));
      if _name=nil then Continue;
      HeapFree(GetProcessHeap(),0, _name);
     end;
   end;

При добавлении имён в UsersAllowListBox «попутно» в него добавляется маска доступа для этого пользователя или группы, чтобы потом иметь возможность вывести его в компонент TCheckListBox.
   За установку атрибутов защиты (в нашем случае списка DACL) отвечает функция SetNamedSecurityInfo (Маска доступа для пользователей содержится в массиве UsersAllowListBox.Items.Objects)


  _SIDList:=TList.Create();
  _ACLSize :=  sizeof(ACL) + UsersAllowListBox.Count*sizeof(ACCESS_ALLOWED_ACE);
  for _i:=0 to UsersAllowListBox.Count-1 do
   begin
    _SIDList.Add(GetSIDbyName(0, PChar(UsersAllowListBox.Items.Strings[_i])));
    _ACLSize:=_ACLSize+GetLengthSid(pointer(_SIDList.Items[_i]));
   end;

  _newDACL:=HeapAlloc(GetProcessHeap(), 0, _ACLSize);
  if InitializeAcl(_newDACL^, _ACLSize, ACL_REVISION) then
   begin
    for _i:=0 to _SIDList.Count-1 do
     AddAccessAllowedAce(_newDACL^,ACL_REVISION, ACCESS_MASK(UsersAllowListBox.Items.Objects[_i]),  pointer(_SIDList.Items[_i]));

    _Res:=SetNamedSecurityInfo( PAnsiChar(ObjectNameEdit.Text), _Objtype, DACL_SECURITY_INFORMATION, 0, 0, _newDACL, 0);
    if _ResERROR_SUCCESS then ShowError(_Res);
   end;

   Работать вручную с атрибутами защиты это конечно хорошо, но это лишние телодвижения (надо учесть очень много мелочей и нюансов), да и не очень-то благодарная работа. А нет ли менее трудоёмкого способа редактирования настроек защиты объекта? Все кто, когда-то администрировал системы Windows и имел дело с Active Directory знает стандартное окно редактирования настроек безопасности объекта.
Стандартное окно редактирования настроек безопасности объекта
   Функция EditSecurity принимает всего лишь два параметра хендл родительского окна (окна относительно которого оно будет модальным) и указатель на COM-интерфейс с несколькими методами. Вроде всё просто, но это только с первого взгляда. Вдаваться в подробности не будем, описание работы с COM-интерфейсами не цель этой статьи, поэтому разберём только самые важные моменты. Искомый интерфейс называется ISecurityInformation и содержит он следующие методы:
1. GetObjectInformation – получение общей информации об искомом объекте
2. GetSecurity – получение описателя защиты объекта
3. SetSecurity - установка описателя защиты объекта
4. GetAccessRights – получение списка флагов которые будут выведены в нижней части окна (там где галочки надо ставить/снимать)
5. MapGeneric – сопоставление общих флагов или флагов специфичных данному типу объектов
6. GetInheritTypes – получении информации об ACE которые могут быть унаследованы дочерними объектами
7. PropertySheetPageCallback – функция обратного вызова для оповещения программы о состоянии окна диалога настроек
   Для нормальной работы диалога следует реализовать как минимум 3 метода GetObjectInformation, GetSecurity, и GetAccessRights, без них ничего не сделаешь, а если надо чтобы диалог мог применить изменения надо реализовать метод SetSecurity. Объявим класс, который будет реализовывать методы интерфейса ISecurityInformation.


type
  TSecurityInformation = class(TInterfacedObject, ISecurityInformation)
    function GetObjectInformation(out pObjectInfo: SI_OBJECT_INFO): HRESULT; stdcall;
    function GetSecurity(RequestedInformation: SECURITY_INFORMATION;
      out ppSecurityDescriptor: PSECURITY_DESCRIPTOR; fDefault: BOOL): HRESULT; stdcall;
    function SetSecurity(SecurityInformation: SECURITY_INFORMATION; pSecurityDescriptor: PSECURITY_DESCRIPTOR): HRESULT; stdcall;
    function GetAccessRights(pguidObjectType: LPGUID; dwFlags: DWORD; out ppAccess: PSI_ACCESS; out pcAccesses, piDefaultAccess: ULONG): HRESULT; stdcall;
    function MapGeneric(pguidObjectType: LPGUID; pAceFlags: PUCHAR; pMask: PACCESS_MASK): HRESULT; stdcall;
    function GetInheritTypes(out ppInheritTypes: PSI_INHERIT_TYPE; out pcInheritTypes: ULONG): HRESULT; stdcall;
    function PropertySheetPageCallback(hwnd: HWND; uMsg: UINT;      uPage: SI_PAGE_TYPE): HRESULT; stdcall;
    destructor Destroy; override;
  . . .
   end;

   Функции GetObjectInformation и GetSecurity тривиальны, от нас требуется только выдать необходимую информацию.


function TSecurityInformation.GetObjectInformation(
  out pObjectInfo: SI_OBJECT_INFO): HRESULT;
begin
  ZeroMemory(@pObjectInfo, sizeof(SI_OBJECT_INFO));
  pObjectInfo.dwFlags := SI_EDIT_ALL or SI_ADVANCED;

  pObjectInfo.pszServerName := FServerName;
  pObjectInfo.pszObjectName := FObjectName;

  Result:=S_OK;
end;

function TSecurityInformation.GetSecurity( RequestedInformation: SECURITY_INFORMATION; out ppSecurityDescriptor: PSECURITY_DESCRIPTOR; fDefault: BOOL): HRESULT;
var
  _ObjType:SE_OBJECT_TYPE;
begin
  Result:=E_FAIL;
  if not fDefault then
   begin
    GetNamedSecurityInfo(PChar(WideCharToString(FObjectName)), FObjectType, RequestedInformation, nil, nil, nil, nil, ppSecurityDescriptor);
    Result:=S_OK;
   end;
end;

   В методе GetObjectInformation надо указать, что можно будет редактировать в диалоге редактирования настроек, имя объекта и имя сервера.
   Особого внимания заслуживает параметр fDefault, если он равен TRUE, то необходимо вернуть не текущий дескриптор, а дескриптор по-умолчанию. Параметр fDefault будет равен TRUE только в том случае, если в методе GetObjectInformation будет указан флаг SI_RESET (в нашем случае это не нужно).
   Метод GetAccessRights должен вернуть массив структур SI_ACCESS


function TSecurityInformation.GetAccessRights(pguidObjectType: LPGUID; dwFlags: DWORD;
  out ppAccess: PSI_ACCESS; out pcAccesses,
  piDefaultAccess: ULONG): HRESULT;
begin
  ppAccess:=@DefaultAccessList;
  pcAccesses := DefaultAccessListItemCount;
  Result:=S_OK
end;

   Внимание следует уделить самому формированию массива. В каждом элементе надо указать, текст элемента-флага в списке, флаг доступа за который он отвечает и на какой странице он будет выведен (на основной или расширенной).


  DefaultAccessList[0].pguid := 0;
  DefaultAccessList[0].dwFlags :=  SI_ACCESS_GENERAL;
  DefaultAccessList[0].pszName := 'Чтение атрибутов защиты';
  DefaultAccessList[0].mask := READ_CONTROL;

  DefaultAccessList[1].pguid := 0;
  DefaultAccessList[1].dwFlags :=  SI_ACCESS_GENERAL;
  DefaultAccessList[1].pszName := 'Удаление';
  DefaultAccessList[1].mask := _DELETE;

  DefaultAccessList[2].pguid := 0;
  DefaultAccessList[2].dwFlags :=  SI_ACCESS_GENERAL;
  DefaultAccessList[2].pszName := 'Изменение владельца';
  DefaultAccessList[2].mask := WRITE_OWNER;

  DefaultAccessList[3].pguid := 0;
  DefaultAccessList[3].dwFlags :=  SI_ACCESS_GENERAL;
  DefaultAccessList[3].pszName := 'Изменение атрибутов защиты';
  DefaultAccessList[3].mask := WRITE_DAC;

  DefaultAccessList[4].pguid := 0;
  DefaultAccessList[4].dwFlags :=  SI_ACCESS_GENERAL;
  DefaultAccessList[4].pszName := 'синхронизация';
  DefaultAccessList[4].mask := SYNCHRONIZE;

   В методе SetSecurity тоже в целом всё тривиально, но надо точно узнать какие параметры защиты надо установить, извлечь их из полученного дескриптора и после этого вызвать функцию SetSecurityInfo/SetNamedSecurityInfo, указав нужные параметры.


function TSecurityInformation.SetSecurity(SecurityInformation: SECURITY_INFORMATION;
  pSecurityDescriptor: PSECURITY_DESCRIPTOR): HRESULT;
var
  _newDACL,_newSACL:PACL;
  _newOwner,_newPG:PSID;
  _bool:LongBool;
begin
  Result:=E_FAIL;
  _newDACL := 0;
  _newSACL := 0;
  _newOwner:=0;
  _newPG:=0;
  if IsFlagPresented(SecurityInformation, DACL_SECURITY_INFORMATION) then
   GetSecurityDescriptorDacl(pSecurityDescriptor, _bool, _newDACL, _bool);
  if IsFlagPresented(SecurityInformation, SACL_SECURITY_INFORMATION) then
   GetSecurityDescriptorSacl(pSecurityDescriptor, _bool, _newSACL, _bool);
  if IsFlagPresented(SecurityInformation, OWNER_SECURITY_INFORMATION) then
   GetSecurityDescriptorOwner(pSecurityDescriptor, _newOwner, _bool);
  if IsFlagPresented(SecurityInformation, GROUP_SECURITY_INFORMATION) then
   GetSecurityDescriptorGroup(pSecurityDescriptor, _newPG, _bool);

  SetNamedSecurityInfo(PChar(WideCharToString(FObjectName)), FObjectType, SecurityInformation, _newOwner, _newPG, _newDACL, _newSACL);
  Result:=S_OK;
end;

   Полный исходный код программы находится в архиве, прилагающемся к данной статье. Программа разрабатывалась в Delphi 7 (я бы написал на Delphi 2009 вот только скомпилированная в этой среде программа начинает выдавать ошибки в самых неожиданных местах). Внимание! Программа работает только разрешающими правилами, если объект имел запрещающие правила, то в случае применения новых правил, они будут потеряны (к стандартному диалогу настроек не осносится). Будьте осторожны при работе с системными объектами, так как программа может работать неправильно и возможны необратимые изменения в системных объектах, что может привести в неработоспособности системы в целом.

Скачать архив с программой-примером и исходниками

Комментарии

  1. mrbelyash
    October 27th, 2011 | 23:26

    ни лаботает ;(

  2. rpy3uH
    April 20th, 2012 | 14:53

    заходи на форум, задавай вопросы излагай проблемы, будем думать

  3. May 25th, 2012 | 10:58

    Немного сложновато для меня. Но буду разбираться. Спасибо за статью!!!

Ответить