Сообщений 9    Оценка 730 [+1/-0]         Оценить  
Система Orphus

Механизмы безопасности в .NET

Автор: Тимофей Казаков
The RSDN Group

Источник: RSDN Magazine #4-2003
Опубликовано: 28.03.2004
Исправлено: 10.12.2016
Версия текста: 1.0.1
Основы безопасности в .NET
Безопасность типов (TypeSafety)
Аутентификация (Authentication)
Авторизация (Authorization)
Полномочия (Permissions)
Политики безопасности (Security Policy)
Происхождение кода
Объект PolicyLevel
Создание “безопасных” приложений
Настройка безопасности домена
Загрузка сборки
Реализация CustomPermission
Результат

В настоящее время одним из основных приоритетов в разработке приложений является безопасность. Грамотно организованная система безопасности приложения – это дополнительная гарантия сохранности и конфиденциальности данных, защита от несанкционированного доступа и многое другое. Естественно, что .NET Framework предоставляет для этого все необходимые средства.

Основы безопасности в .NET

Систему безопасности в .NET можно представить в виде нескольких функциональных блоков (рисунок 1). Работа и управление этими службами доступны как администраторам, так и прикладным программистам. Для начала рассмотрим каждую из них.


Рисунок 1. Возможности предоставляемые системой безопасности в .NET.

Безопасность типов (TypeSafety)

Безопасность типов играет одну из ключевых ролей – именно благодаря этому есть возможность изолировать сборки (assemblies) друг от друга, что дает возможность использовать в рамках одного процесса несколько сборок с разным уровнем доверия. Например, загрузив из интернета безопасную сборку, мы можем быть уверены, что она не вызовет WinAPI функцию, которая отформатирует нам жесткий диск. Также у нас есть гарантия того, что типо-безопасный код работает с другими объектами только дозволенным способом (например, не будет пытаться вызывать private методы другого объекта). Само собой разумеется, что есть возможность отключить процесс проверки типо-безопасности, однако, что обнадеживает, для того чтобы это сделать, нужны достаточно серьезные полномочия.

Аутентификация (Authentication)

Аутентификация представляет собой процесс проверки регистрационной информации (identity) пользователя (principal). Приложения в .NET Framework могут использовать большинство из доступных на настоящий момент механизмов аутентификации. Примерами таких механизмов аутентификации являются BASIC, Digest, Passport, аутентификация на основе служб предоставляемых операционной системой (NTLM или Kerberos) либо на основе механизмов, определенных в приложении. Управляемый код может получить регистрационную информацию и роли пользователя через объект Principal (интерфейс IPrincipal), который также содержит ссылку на Identity (IIdentity)

СОВЕТ

В MSDN для обозначения информации о пользователе применяются два термина - Identity и Principal. Первый из них используется для обозначения объекта, инкапсулирующего в себе имя пользователя и тип аутентификации, использовавшийся для проверки пользователя. Второй – Principal - это контекст безопасности, в рамках которого и выполняется код. Учитывая, что устойчивого русского перевода для этих терминов не существует, то я буду использовать термины "identity" и "principal"

Авторизация (Authorization)

Авторизация – это процесс проверки прав текущего пользователя на выполнение запрошенного действия. В процессе авторизации проверяется принадлежность пользователя к определенным ролям, и на основе этой информации принимается решение о предоставлении доступа к ресурсу.

Полномочия (Permissions)

В .NET Framework код может выполнить какое-либо действие только в том случае, если у него на это есть достаточные права. Все это контролируется специальными объектами – Permissions. В свою очередь полномочия покрывают три области:

Тут можно также отметить, что как полномочия, так и пользователи в .NET имеют мало общего с аналогичными сущностями в Windows. Как пример: права на доступ к файлу, лежащему на диске с NTFS – пользователь, запустивший .NET приложение, может иметь соответствующие права на доступ в NTFS, но CLR все равно может не предоставить доступа к диску (если приложение запущено из сети), так и наоборот – CLR может разрешить действие с файлом, но ошибка проявится уже на уровне NTFS.

Политики безопасности (Security Policy)

Политики безопасности представляют собой набор правил на основе которых принимается решение о том, какие действия могут выполняться кодом, а какие нет. Каждый раз, когда загружается сборка CLR проверяет ее происхождение и на основе этой информации и политики безопасности принимается решение о том, какие полномочия предоставить этой сборке.

Один из самых простых способов управления политиками безопасности – это через оснастку “Microsoft .NET Framework 1.1 Configuration ”. Рассмотрим простейшее Windows.Forms приложение на котором можно будет увидеть работу политик безопасности в действии. Для этого создадим пустую форму запустив которую в большинстве случаев мы увидим примерно следующее (Рисунок 2)


Рисунок 2 Приложение запущено c полными правами.

        public
        class InternetForm : System.Windows.Forms.Form
{
  private System.ComponentModel.Container components = null;

  public InternetForm()
  {
    InitializeComponent();
  }


  privatevoid InitializeComponent()
  {
    // // InternetForm// this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
    this.ClientSize = new System.Drawing.Size(292, 266);
    this.Name = "InternetForm";
    this.Text = "InternetForm";

  }

  [STAThread]
  staticvoid Main() 
  {
    Application.Run(new InternetForm());
  }
}

Теперь, используя оснастку (рисунок 3), изменим настройки безопасности. Для этого в ветке Runtime Security Policy\User\Code Groups\All_Code создадим новую группу “Internet Code” и настроим ее.


Рисунок 3. Оснастка .NET Configuration 1.1

Также укажем для этой группы необходимые настройки (рисунок 4).


Рисунок 4. Настройка Политики безопасности.

После этого скопируем исполняемый файл в папку е:\ (можно указать любую другую папку, главное – следить за тем, чтобы это не затрагивало уже существующие приложения, т.к. эти настройки могут отрицательно сказаться на их работоспособности) и запустим приложение (рисунок 5). И первое, что мы увидим – это предупреждение от .NET Framework, что приложение запущено из непроверенного места.


Рисунок 5. Приложение запущено с набором прав «Internet».

Как работают политики безопасности? В .NET Framework определено четыре группы политик (PolicyLevel): Enterprise, Machine, User и Application Domain каждый из уровней содержит свой набор правил, на основе которых определяется, какие права можно предоставить коду.

Зона безопасности Предоставляемые права
MyComputer Для всех сборок, загруженных с локального компьютера предоставляется полный доступ ко всем ресурсам
LocalIntranet_Zone Сборки запущенные из этой зоны получают ограниченный доступ к ресурсам компьютера. Кстати, именно с действиями именно этой группы можно столкнуться начав программировать в .NET. Эта зона не определяет полномочия SqlClientPermission и приложение, которое запускали с локального диска – замечательно работало с SQL Server, а когда его запустили с сетевой папки (пусть будет так: \\SomeServer\Folder\DBApp.exe) стало кидать исключения. Разумеется, что возможны и другие ситуации, и один из выходов – это либо менять настройки зоны, либо увеличивать права предоставляемые конкретному приложению
Internet_Zone Эта зона предоставляет самый ограниченный набор прав (ее действие мы уже видели на рисунке 9). Все веб сайты которые не попали ни в одну из зон автоматически попадают в Internet_Zone
Trusted_Zone В Trusted_Zone попадают все сайты которые отмечены как обладающие высоким уровнем доверия. Что стоит отметить, что права предоставляемые этой зоной совпадают с правами Internet_Zone. Считается, что администраторы должны их настроить самостоятельно.
Restricted_Zone Зона с закрытым доступом. Здесь не предоставляется никаких прав. И, как результат – загруженный из этой зоны код запущен не будет.
Таблица 1.

Для определения принадлежности URL к какой-либо зоне используется объект Internet Explorer’а – IInternetSecurityManager.

Уровень Personal. Используется для управления настройками безопасности текущего пользователя. По умолчанию, как и на уровне Enterprise, всем сборкам даются полные права .

Уровень AppDomain. Это специальный уровень, и его настройки можно изменить только программно (далее мы остановимся на этом более подробно).

В момент загрузки кода происходит определение набора прав, которые предоставляются на каждом уровне, и после этого вычисляется минимальное множество прав, которое и будет ему гарантировано. И, если не менять никаких настроек, все реальные права определяются на уровне Machine (все остальные уровни предоставляют полный доступ).

Естественно, сразу возникает вопрос – как определить, к какой зоне принадлежит загружаемая сборка? Если она загружается с какого-то URL, все просто, но как быть если она была скачана из интернета и сохранена на диске, или от неизвестного автора пришел по почте кусок кода, и мы хотим попробовать его в действии? Очевидно, что запускать такое с полными правами – безответственно. Именно для этого существует специальный класс Evidence.

Происхождение кода

Как уже упоминалось, для назначения прав загруженному коду нужна информация о его происхождении. В некоторых случаях нам достаточно знать только то, что код загружен из проверенного источника, иногда мы хотим доверять определенному издателю, а иногда нам нужно поднять уровень доверия для собственной сборки, и мы хотим использовать что-то более надежное, чем ее текстовое имя. В любом случае, независимо от того, что мы используем – все это происхождение кода и эта информация описывается специальным объектом Evidence. С точки зрения среды исполнения, объект Evidence – это коллекция идентификаторов, каждый из которых предоставляет разнообразную информацию о происхождении кода; она может быть двух видов: предоставленной тем, кто загружает сборку, или содержащейся внутри сборки.

СОВЕТ

Информацию о происхождении можно получить, используя методы Evidence.GetHostEnumerator и Evidence.GetAssemblyEnumerator. Главное – помнить, что система безопасности использует только Host Evidence, а все, что указано на уровне сборки (Evidence.AddAssembly), отдано на откуп сторонним разработчикам.

В .NET есть несколько стандартных классов, используя которые можно описать большинство необходимых ситуаций (таблица 2).

Класс Назначение
Zone Использование зоны безопасности (IE Securtity Zone) как информации о происхождении.
Url Url адрес, с которого получен код сборки.
Site Web сайт с которого получен код сборки.
StrongName Использование “строгого имени” как информации о происхождении.
Publisher Происхождение, основанное на использовании цифровых подписей “Authenticode X.509v3”.
Hash Хеш сборки как информация о происхождении.
PermissionRequestEvidence Специальный класс, описывающий запрос привилегий. Обычно он используется в ситуациях, когда можно заранее определить необходимые для запуска кода привилегии.
ApplicationDirectory Этот тип Evidence предоставляется только в том случае, когда домен приложения связан с папкой файлов. Например, для ASP.NET это будет папка, где лежит базовая страница.
Таблица 2. Классы для описания Evidence сборки.

Итак, если у нас есть объект Evidence, то как определить тот набор прав, который будет нам предоставлен? Информацию об этом может предоставить специальный объект – PolicyLevel.

Объект PolicyLevel

Как уже упоминалось, в .NET Framework есть четыре уровня политик. Каждый уровень организован в виде иерархического каталога (рисунок 6), каждый элемент которого идентифицирует определенную группу кода (CodeGroup).


Рисунок 6. Структура объекта PolicyLevel.

Для чего нужны объекты CodeGroup? Группы кода – это своеобразная отправная точка для построения политики безопасности. Именно этот объект позволяет нам связать Evidence объекта с необходимым ему набором полномочий. Чтобы принять решение о предоставлении определенному Evidence запрашиваемых полномочий, CodeGroup использует объект-условие MembershipCondition (таблица 4), который и помогает принять ему это решение.

Условие Описание
AllMembershipCondition Данному условию удовлетворяет любой код. Обычно данное условие используется только на корневом уровне (All_Code).
ZoneMembershipCondition Условие для проверки происхождения типа “Zone”. Если в предоставленном Evidence содержится объект “Zone”, то он будет проверен на совпадение с требуемой SecurityZone.
SiteMembershipCondition Это условие аналогично ZoneMembershipCondition, но в данном случае информацией о происхождении будет служить “Site”.
StrongNameMembershipCondition Условие для проверки “строгого имени” сборки. И что радует – обязательным для указания является только PublicKeyBlob. Поля Name и Version можно оставить пустыми – что позволяет включить в данное условие все сборки подписанные определенным ключом
UrlMembershipCondition Условие для проверки “Url”. В качестве условия можно использовать как точное имя, так и групповые (указав «*» в последней позиции).
ApplicationDirectoryMembershipCondition Это условие проверяет сборку на предмет принадлежности к ApplicationDirectory. Главное – помнить, что для успешной проверки в Evidence должны присутствовать объекты “ApplicationDirectory” и “Url”.
HashMembershipCondition Условие для проверки хеша сборки.
PublisherMembershipCondition Условие для проверки на основе сертификата Authenticode X.509v3.
Таблица 3. Возможные условия для проверки Evidence.

Кроме MembershipCondition, объект CodeGroup связан с объектом PolicyStatement. Зачем это понадобилось, и почему нельзя было просто указать нужный набор прав (объект PermissionSet)? Основная причина – это дополнительная возможность более тонко влиять на выдаваемые полномочия. Для этого у PolicyStatement есть дополнительный параметр PolicyStatementAttribute, принимающий следующие значения:

ПРИМЕЧАНИЕ

Мы уже использовали этот флаг (рисунок 4) – именно он дал нам возможность использовать полномочия, указанные во вновь созданной группе, перекрыв полномочия, определенные для SecurityZone = MyComputer

Схема со связью MembershipCondition ( PolicyStatement достаточно хороша, но ей недостает гибкости, так как PermissionSet – это статический набор прав. Иногда принять решение о том, что можно предоставить коду, а что нельзя, можно только на основе динамически изменяющихся данных. В этом случае нам могут помочь наследники класса CodeGroup (таблица 4).

Класс Описание
UnionCodeGroup Пожалуй, самый распространенный тип групп. Обеспечивает объединение своих полномочий с полномочиями своих “детей”.
NetCodeGroup Поведение этой группы совпадает с поведением UnionCodeGroup с одним отличием – результирующий набор прав содержит динамически сформированный набор полномочий WebPermission. Так, если в Evidence сборки есть Url = http://www.rsdn.ru/application/sample.exe, то приложение получит доступ к http://www.rsdn.ru/* и https://www.rsdn.ru/*. Однако если приложение изначально запустить через HTTPs протокол, то доступа к “небезопасному” ресурсу оно не получит
FileCodeGroup Поведение этой группы также совпадает с поведение UnionCodeGroup, но в отличие от NetCodeGroup этот класс динамически формирует набор FileIOPermission. Естественно, что права добавляются только в том случае, если Url указывает на file:// протокол или UNC путь
FirstMatchCodeGroup Как и следует из названия – эта группа возвращает объединение из своего набора полномочий и полномочий своего первого дочернего элемента (у которого MembershipCondition подошел к переданному Evidence)
Таблица 4. Возможные типы CodeGroup.

Однако оставим управление политиками безопасности через оснастки панели управления системным администраторам. Для настоящих программистов это не лучший путь, поэтому рассмотрим небольшой пример программной настройки безопасности в приложении.

Создание “безопасных” приложений

В качестве примера создадим простейшее Windows.Forms-приложение, на основе которого рассмотрим:

Настройка безопасности домена

Первое, что нужно сделать для настройки безопасности – это создать PolicyLevel и назначить его домену приложения. С этого и начнем:

        private
        void SetupAppDomainPolicy(string location)
{
  AppDomain appDomain = AppDomain.CreateDomain("SecureDomain");
  PolicyLevel policyLevel = PolicyLevel.CreateAppDomainLevel();

  ////  Создадим корневую группу кода.//  По умолчанию никаких прав предоставлять ей не будем//
  CodeGroup rootGroup = new UnionCodeGroup(
      new AllMembershipCondition(), new PolicyStatement( new PermissionSet(PermissionState.None)));
  
  ////  Создадим группу для SecurityZone.MyComputer//  и дадим ей неограниченные права//
  CodeGroup myComputer = new UnionCodeGroup(
    new ZoneMembershipCondition(SecurityZone.MyComputer), 
      new PolicyStatement(
        new PermissionSet(PermissionState.Unrestricted)));
  
  ////  Создадим PermissionSet для сборок с "ограниченным" набором прав//  
  PermissionSet untrustedSet = new PermissionSet(PermissionState.None);
  
  //// Право на исполнение нужно дать обязательно - // в противном случае не запустится
  untrustedSet.AddPermission(
    new SecurityPermission(SecurityPermissionFlag.Execution));
  
  //// Ограниченные права на UI - удобно контролировать результат//
  untrustedSet.AddPermission(
    new UIPermission(UIPermissionWindow.SafeTopLevelWindows));

  //// Создадим группу кода для ограниченных сборок.// Ограниченные сборки будут лежать в подпапке Untrusted.// Естественно, нужно указать PolicyStatementAttribute.Exclusive // - в противном случае группа с // ZoneMembershipCondition(SecurityZone.MyComputer) испортит нам все // старания своим new PermissionSet(PermissionState.Unrestricted)
  CodeGroup untrusted = new UnionCodeGroup(
    new UrlMembershipCondition(location + @"Untrusted/*"), 
      new PolicyStatement(untrustedSet, PolicyStatementAttribute.Exclusive));

  ////  Группа для сборок с полным доступом. //  По правде говоря, эту группу можно было не создавать //  - группа myComputer уже предоставляет все необходимые права.//  Но пусть будет – для дальнейшего развития :)//
  CodeGroup fulltrust = new UnionCodeGroup(
      new UrlMembershipCondition(location + @"Fulltrust/*"), 
        new PolicyStatement(
          new PermissionSet(PermissionState.Unrestricted), 
            PolicyStatementAttribute.Exclusive));

  rootGroup.AddChild(myComputer);
  rootGroup.AddChild(fulltrust);
  rootGroup.AddChild(untrusted);

  policyLevel.RootCodeGroup = rootGroup;

  //  Вот здесь назначим текущему домену новый PolicyLevel
  AppDomain.CurrentDomain.SetAppDomainPolicy(policyLevel);
}
ПРЕДУПРЕЖДЕНИЕ

Важно! Самое главное сделать это до того, как в домен будут загружены сборки, на которые распространяются политики безопасности. Если это сделать позже, ничего не выйдет.

Загрузка сборки

Загрузка сборки – это самый простой этап. В данном примере для загружаемой сборки укажем только один параметр evidence – URL.

        public SecureForm()
{
  InitializeComponent();

  Application.ThreadException += new ThreadExceptionEventHandler(ThreadException);

  string basePath = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;

  SetupAppDomainPolicy(basePath);

  // т.к. это просто пример, будем считать, что в каталоге должна // какая-то конкретная сборка
  fullTrustAssembly = LoadAssembly(basePath + @"FullTrust\SecureModule.dll");
  untrustedAssembly = LoadAssembly(basePath + @"Untrusted\SecureModule.dll");
}

// Загружаем сборку с заданным Evidenceprivate Assembly LoadAssembly(string location)
{
  Evidence evidence = new Evidence();
  evidence.AddHost(new Url(location));

  // Привет пользователям .NET Framework 1.0 :)return Assembly.LoadFile(location, evidence);
}

// Запускаем тест из загруженной сборкиprivatevoid RunTest(Assembly assembly)
{
  // Запрещаем CustomPermissionif (!customPermission.Checked) 
  {
    new CustomPermission(PermissionState.Unrestricted).Deny();
  }

  //  В загруженной сборке должен быть класс SecureModule.ModuleForm
  MarshalByRefObject mbr = (MarshalByRefObject) 
    assembly.CreateInstance("SecureModule.ModuleForm");

  using ((IDisposable)mbr) 
  {
    MethodInfo mi = mbr.GetType().GetMethod("RunTest", 
    new Type[] { typeof(EventHandler) });
    mi.Invoke(mbr, newobject[] { new EventHandler(SecureMethod) });
  }
}

// Просто Callback-метод для демонстрацииprivatevoid SecureMethod(object sender, EventArgs args)
{
  // Требуем, чтобы у вызывающего кода были необходимые праваnew CustomPermission(PermissionState.Unrestricted).Demand();
  LogEvent("SecureMethod", "OK");
}

// Запускаем тест из Trusted-сборкиprivatevoid TrustedTest_Click(object sender, System.EventArgs e)
{
  RunTest(fullTrustAssembly);
}

// Запускаем тест из Untrusted сборкиprivatevoid UntrustedTest_Click(object sender, System.EventArgs e)
{
  RunTest(untrustedAssembly);
}

Реализация CustomPermission

Для реализации собственного полномочия достаточно создать класс-наследник CodeAccessPermission и реализовать несколько абстрактных методов.

        // Полномочие собственной разработки 
        // Это полномочие не зависит от каких либо внешних параметров и 
        // работает по принципу - либо оно есть, либо его нет. 
        // Учитывая, что никакой особой реализации тут нет, то и 
        // комментировать тут особо ничего не будем
[Serializable]
publicclass CustomPermission : CodeAccessPermission, 
      IUnrestrictedPermission,
      ISecurityEncodable          
{
  PermissionState state;

  public CustomPermission(PermissionState state)      
  {
    this.state = state;
  }

  publicbool IsUnrestricted()
  {
    returnthis.state == PermissionState.Unrestricted;
  }

  publicoverride IPermission Copy()
  {
    returnnew CustomPermission(state);
  }

  publicoverride IPermission Intersect(IPermission target)
  {
    if (target == null) 
    {
      returnnull;
    }
    CustomPermission perm = (CustomPermission) target;

    returnnew CustomPermission(state & perm.state);
  }

  publicoverridebool IsSubsetOf(IPermission target)
  {
    if (target == null) 
    {
      returnfalse;
    }

    CustomPermission perm = (CustomPermission) target;

    if (perm.IsUnrestricted()) 
    {
      returntrue;
    }
    elseif (this.IsUnrestricted()) 
    {
      returnfalse;
    }
    
    returnthis.state == perm.state;
  }

  publicoverridevoid FromXml(SecurityElement elem)
  {
    try 
    {
      this.state = Convert.ToBoolean(elem.Attribute("Unrestricted")) == true ?
        PermissionState.Unrestricted : PermissionState.None;
    }
    catch 
    {
      this.state = PermissionState.None;
    }
  }

  publicoverride SecurityElement ToXml()
  {
    SecurityElement esd = new SecurityElement(this.GetType().Name);      
    if (IsUnrestricted()) 
    {
      esd.AddAttribute( "Unrestricted", "true" );
    }
    return esd;
  } 
}

Результат

Итак, было создано приложение которое дважды загружает одну и ту-же сборку, но с разным Evidence. Как можно видеть (рисунок 7), для сборки, загруженной с неограниченными правами, никаких внешних изменений не произошло (было-бы удивительно, если-бы что-то случилось). Код из сборки смог успешно вызвать переданный Callback-метод.


Рисунок 7. Форма из "Trusted"-сборки.

Однако если попытаться использовать класс из Untrusted-сборки (рисунок 8), вид формы кардинально изменится (появится уже известное предупреждение), не удастся вызвать и CallBack-метод – для него в списке предоставленных полномочий не оказалось CustomPermission.


Рисунок 8. Форма из "Untrusted"-сборки.

Конечно, это не полноценное приложение, а только пример. Однако, надеемся, что с его помощью нам удалось показать главное – то, что использование механизмов безопасности в .NET более чем просто.


Эта статья опубликована в журнале RSDN Magazine #4-2003. Информацию о журнале можно найти здесь
    Сообщений 9    Оценка 730 [+1/-0]         Оценить