Сообщений 31    Оценка 1455 [+2/-1]         Оценить  
Система Orphus

.NET Framework 4.0. Code Access Security

Отсекая лишнее

Автор: Щербунов Нейл Анатольевич
Источник: RSDN Magazine #4-2010
Опубликовано: 15.12.2010
Исправлено: 10.12.2016
Версия текста: 1.0
Введение.
Code Access Security, начало
Code Access Security, продолжение
Transparent Code
Sandboxing или игры в песочнице
А теперь – вместе
Строим замки. Из песка
Вносим сумятицу. Полно доверенные сборки в частично доверенных доменах
Детализируем защиту. Ограничения доступа к отдельным методам
Углубляем детализацию защиты. Доступ к отдельным методам в отдельных доменах
Заключение. И это все?

Введение.

Код примеров к статье.

Вопросы безопасности почти всегда являются сложнейшим компонентом любой IT-системы, будь то системы операционные, управления реляционными базами данных (более известные как СУБД), или, как в случае данной статьи, платформы разработки и построения прикладного программного обеспечения. Непростым делом является глубокое понимание этого компонента в любой из обозначенных систем, а грамотная его реализация является, с моей точки зрения, некой «печатью» (hallmark, как выражаются наши коллеги на англоязычных форумах) заверяющей высочайшую квалификацию разработчика или команды, такое приложение создавших. Дело усугубляется еще и тем, что в типичном сценарии обсуждаемый вопрос находится на стыке «зон ответственности»: мало того, что приложение должно быть грамотно «обезопасено» тем самым квалифицированным разработчиком, так еще на целевой машине ее администратор (желательно не менее квалифицированный) должен настроить не менее безопасное окружение, в котором это приложение будет выполняться. Потому как если последний возьмет на вооружение простой и понятный (хоть и совершенно неправильный) подход «всем можно все», то можно уверенно сказать: разработчик зря потратил недели и месяцы своего труда, настраивая и проверяя все нюансы безопасной работы своего приложения. Администратор может быть также приверженцем противоположного подхода, который утрировано можно выразить как «никому нельзя ничего». Кстати, вполне здравый подход, по крайней мере как первая фаза построения безопасной IT-инфраструктуры предприятия. Как бы то ни было, разработчику, пишущему код управления безопасностью приложения, все время приходится держать в голове вопросы типа «а что такого может выставить в настройках администратор целевой машины, что вся моя стройная система перестанет работать»?

В .NET Framework, одной из систем, подверженных все тем же трудностям security-«состыковки», с самого начала было приложено очень немало усилий, дабы совместная работа двух обозначенных выше IT-профессионалов протекала максимально гладко, но не в ущерб безопасности кода как такового. Был разработан даже целый новый «вектор» приложения усилий в этом направлении – Code Access Security (он же CAS), авторизующий на доступ к защищаемому ресурсу не пользователя (классический подход), а сам код как таковой. Идея была зело хороша, а вот ее реализация… Ну, скажем так, получилась неоднозначной. Почему оно так получилось, пойдет речь в следующей главе, Code Access Security, начало. Но, как бы то ни было, все так и катилось вплоть до последней, 4-й версии платформы .NET Framework, в которой CAS получил буквально второе рождение, так как, фактически, был пересмотрен и перестроен чуть ли не с самого фундамента. Самое главное, с точки зрения автора, что “CAS ver. 4.0” стал гораздо понятнее, а вследствие того, гораздо практичнее для применения даже в относительно простых проектах, авторы которых до того не считали возможным применения этого механизма в силу как сложности его реализации, так и последующей поддержки на конечной машине. В новой версии была пересмотрена сама идеология ограничения доступа к коду и, что приятно, удалены множественные сущности, фигурировавшие в предыдущих версиях этой технологии, и делающие ее столь сложной для изучения. Новый вариант CAS-безопасности значительно компактнее, логичнее, более управляем, да и просто более “прозрачен”.

В данной статье автор сначала кратко рассмотрит архитектуру CAS версий предыдущих (до 4.0), дабы читатели «со стажем» могли вспомнить (а может, и просто узнать) исходную точку, с которой все началось. После такого легкого «экскурса в прошлое» мы перейдем к Code Access Security в версии .NET 4.0 и сравним – что же нового появилось и, что может даже важнее, что же было удалено из этой подсистемы безопасности. Нас также ждут довольно интенсивные практические работы с кодом (все примеры написаны на C#), призванные полнее раскрыть концепции нового подхода и показать «подводные камни» его применения к реальным приложениям. Не обойдется и без этаких «мини-тестов», когда надо будет предсказать поведение демонстрационного приложения без его запуска. Правильный ответ подтвердит корректное понимание теории того или иного аспекта безопасного кода. Разумеется, после ответа «вслепую» нужно запустить приложение и проверить догадку практикой.

Надеюсь, что знакомство с «новым-старым» инструментом безопасности будет приятным и, самое главное, понятным для читателей практически любого уровня знакомства с платформой .NET как версии текущей, так и любой из предыдущих. Разумеется, автор не рискнет предложить данную статью как базовый учебник C#/.NET Framework, все же вопросы безопасности всегда входили (и продолжают) в категорию «advanced». Тем не менее, материал будет излагаться настолько простым языком, насколько это в принципе возможно при изложении непростых концепций. Интересного и познавательного чтения!

Code Access Security, начало

Давайте бросим беглый взгляд на дела «давно минувших дней» и постараемся очень кратко обрисовать вопрос – как все было раньше? Хотя, возможно, более правильно даже сказать «как есть сейчас»? Поскольку .NET 4-й еще не настолько «заматерел», чтобы безоговорочно выпихнуть из сектора разработки ПО своих младших собратьев… Ну, как бы то ни было, но вплоть до последней версии платформы компонент CAS оперировал несколькими основополагающими сущностями:

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

[FileIOPermission(SecurityAction.Demand, Unrestricted=true)]
public calss MyClass 
{
    public MyClass()    {...} // ВСЕ эти методыpublicvoid MethA() {...} // требуют неограниченного доступа publicvoid MethB() {...} // к файловой системе целевой машины
}
      public
      void MethA() 
{
  // ...что-то происходит…
  FileIOPermission myPerm = new FileIOPermission(PermissionState.Unrestricted);
  myPerm.Demand(); 
  // дальше этой строчки мы не уйдем,   // если запрос в предыдущей окончится неудачей
}

По сути, оба подхода выполняли одну и ту же процедуру – выясняли, какие именно разрешения предоставил сборке администратор целевого компьютера. А как и чем он их назначал? Специально для второго участника вопросов безопасности была предусмотрена утилита (более точно – оснастка консоли) по имени Mscorcfg.msc (полный путь к ней – %Systemroot%\Microsoft.NET\Framework\versionNumber\Mscorcfg.msc, или она же под вывеской ‘.NET Framework <номер_версии_Framework> Configuration’ в списке административных инструментов, Administrative Tools). Ею и пользовались, рисунок 1.


Рисунок 1. Административная утилита конфигурирования Code Access Security

Интересно заметить, что до версии платформы 2.0Mscorcfg.msc входил в .NET Framework redistributable package, с версии 2.0 и до 4.0 его перебросили в .NET Framework Software Development Kit (SDK), а в версии 4.0 он попросту исчез отовсюду! Что с ним стряслось, и можем ли мы рассчитывать на его возвращение? Читайте статью далее.

Действия администратора в описываемой утилите называли «настройкой политик CAS (CAS Policy)». По сути, такая политика декларирует, какой набор разрешений получит каждая из управляемых сборок на целевом компьютере. НО! Необходимо было учитывать, что существовало 4 уровня (levels) таких политик. На рисунке 1 мы можем видеть 3 из них – Enterprise, Machine,User, а «за кадром» осталась политика Application Domain. У каждого из уровней были, как это следует из того же рисунка, свои кодовые группы со своими, разумеется, условиями членства в них. Дальше каждая кодовая группа имела специфичный набор разрешений, а каждая сборка, опираясь на свидетельства, могла принадлежать к нескольким кодовым группам одновременно, что приводило к объединению всех их наборов в пределах данного уровня политики, а затем еще пересечению наборов между различными уровнями. Что такое? Чувствуете легкое головокружение? Это нормальное состояние при попытке мысленно охватить описываемый механизм. :) И это еще мы не упомянули о возможности назначить один из уровней политик "финальным" или, наоборот, "эксклюзивным"... В общем – тяжелое было время. :)

Что даже еще хуже, так это тот факт, что все время и все усилия, затраченные администратором в окне оснастки консоли с рисунка 1 были направлены на настройку безопасности только управляемых (managed) приложений. Приложения неуправляемые Mscorcfg.msc вообще никак не затрагивал, да и затронуть не мог, ведь ядро функциональности CAS находилось (и продолжает) в среде исполнения CLR. Так что после настройки политик администратору рекомендовалось не расслабляться, а напротив – заняться тем же самым, но теперь для native-приложений Windows, с помощью отдельных инструментов, описание которых уж совсем выходит за рамки данной статьи. Кроме того, даже настройка только управляемых приложений через эту утилиту требовала от системного администратора если и не глубоких, то, по крайней мере, изрядных знаний по платформе .NET. То есть чтобы создать грамотную, продуманную, действительно безопасную политику, администратор должен был, по-хорошему, быть этаким универсальным “system-administrator-NET-developer”. Что, понятно, на практике встречалось нечасто…

Итого, какова же эффективность CAS-политик как инструмента безопасности? Квалификацию от персонала они требуют если и не высочайшую, то повышенную – точно. Трудоемкость реализации – значительна. Трудоемкость настройки на целевой машине – еще более значительна и усугублена ее обособленностью от настройки большинства других приложений (ведь на стандартном компьютере приложений .NET меньше, чем приложений обычных, неуправляемых). Все это привело к тому, что описываемая методология была скорее «витриной», нежели повседневным инструментом.

Code Access Security, продолжение

Итак, изменения в структуру и идеи безопасного кода просто напрашивались, если хотелось сделать этот компонент массовым продуктом, а не чем-то, применяемым только избранными «гуру» разработки. И в версии 4.0 эти изменения были, наконец-то, реализованы. В чем главнейшие отличия новой версии? Их два:

  1. В .NET 4.0 политики безопасности убраны совершенно (хотя такая сущность как разрешение, permission – оставлена).
  2. В .NET 4.0 механизм, который вынуждает код сборки обращаться только к разрешенным ресурсам (и препятствует обращению к ресурсам запрещенным), изменен с императивно/декларативного подхода на совершенно иную концепцию по имени Прозрачный для системы безопасности код, он же Security-Transparent Code.

Строго говоря, «новинка №2» не является таковой в полном смысле этого слова. Сама концепция была представлена еще в .NET 2.0. Но там это был скорее механизм внутренний, используемый по преимуществу самой Microsoft для написания кода базовой библиотеки платформы. Идея широко не освещалась, не продвигалась, и если вы впервые видите термин Security-Transparent Code в данной статье, то это неудивительно. Четвертая версия платформы эту идею существенно расширила и внесла в нее заметные коррективы. Чтобы отличать две эти «под-версии» прозрачного кода, тот вариант, что появился во 2 версии, назвали Security-Transparent Code, Level 1, а тот, что в 4-й – Security-Transparent Code, Level 2. Дабы статья не переросла в монографию и ее материал не был переусложнен нюансами технологий прошлых дней, мы вообще не будем касаться первого подхода, т.е. речь пойдет только о том варианте, что появился вместе с .NET Framework 4.0.

Какое практическое влияние на разработчиков и администраторов оказали эти две модернизации? По моему мнению, не побоюсь этого слова – колоссальное. У администраторов исчез из обихода Mscorcfg.msc, к которому они нежных чувств, мягко говоря, не испытывали. Теперь к их услугам инструмент Software Restriction Policies, а в самых последних версиях Windows (Windows 7 / Windows 2008 Server R2) его продвинутый аналог – AppLocker. Оба являются инструментами куда как более «администраторскими», нежели Mscorcfg.msc. А что еще важнее – и там, и там возможно конфигурирование как управляемого, так и неуправляемого кода! Правда, конфигурация управляемого кода построенного по концепции Security-Transparent Code, Level 2 практически и не требует вмешательства администратора. Все нюансы безопасности учитываются разработчиком при написании подобного кода. Причем и они, разработчики, от введения прозрачного кода только выиграли, т.к. планирование и реализация подсистем безопасности их приложений стало делом куда как более простым и понятным.

Резюмируем и подведем итоги этой краткой, но чрезвычайно важной части статьи. В модели безопасного кода .NET Framework версии 4.0 мы:

С первым пунктом, полагаю, все ясно. Уменьшение сущностей – это то, к чему обязан стремиться любой разработчик, независимо от типа приложения, им создаваемого. Пусть это даже, как в нашем случае, платформа для разработки других приложений. Но – как нам теперь жить с одними только лишь разрешениями, и что такое, в конце концов, этот прозрачный код? Вот здесь есть что обсудить.

Transparent Code

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

Механизм security transparency делит весь код, который только может быть создан разработчиком, на 3 понятных категории (или слоя, если хотите):

Иными словами имеем такую картинку, рис. 02:


Рисунок 2. Возможные обращения друг к другу кода разных слоев

Стрелочками на рисунке показаны все разрешенные обращения (т.е. вызовы кода) от слоя к слою, за исключением «внутри слоевых» вызовов, которые, разумеется, не имеют никаких ограничений. Сразу предупрежу, что не следует смешивать сборки (assembly) со слоями – это совершенно разные концепции! Да, зачастую в реальном решении весь код сборки будет принадлежать одному слою, однако в общем случае в одной и той же сборке код может представлять собой этакий «слоеный пирог», т.е. быть и критическим, и прозрачным, и safe critical. Это потому, что разграничение идет по классам, методам и даже полям класса. И обратное тоже верно – код одного слоя может быть «размазан» по многим сборкам.

Таким образом, реальную головную боль и повышенное внимание от разработчика требует только Safe Critical-код. Transparent-код в принципе ничего ужасного сделать не может, Critical-код может что угодно, однако он должен и писаться, и тестироваться, и планироваться именно исходя из этого факта. Также он может полагаться на тот факт, что любой Transparent-код не будет обращаться к нему напрямую, а лишь через посредника – Safe Critical-код.

Как технически разграничить код на эти 3 категории? Для этого используются атрибуты. Слоев (категорий) у нас сколько? Столько же будет и атрибутов:

Давайте уже разберемся в разнице полномочий critical- и transparent-кода. Что такого можно первому, чего нельзя второму? Список этих ограничительных операций не так велик. Transparent-коду нельзя:

Здесь и далее ссылка на код, рассматриваемый в данный момент дается как SolutionNN\ProjectMM, где NN/MM номер очередного решения и проекта в нем, соответственно. Не забывайте делать нужный проект текущим ("Set as StartUp Project" в контекстном меню нужного проекта в окне Solution Explorer). Весь код может быть загружен по ссылке в самом начале статьи.

Итак, Solution01\Project01. Все наши эксперименты будут проходить на консольных приложениях, дабы мы могли сосредоточиться именно на вопросах безопасности. Первое из таких приложений является элементарным и просто вызывает (через механизмы interoperability) неуправляемую функцию GetVersion(). Изначально вся сборка помечена как critical, и никаких проблем при запуске не возникает. Теперь сделаем сборку «прозрачной», для чего закомментируем первый атрибут (строка 5) и раскомментируем второй (строка 6), т.е. приведем указанные строки к такому виду:

      //[assembly: SecurityCritical]
[assembly: SecurityTransparent]

Запускаем – получаем:

      System.MethodAccessException was unhandled
      Message=Attempt by security transparent method 'Project01.Program.Main()' to call native code through method 'Project01.Program.GetVersion()' failed.
      Methods must be security critical or security safe-critical to call native code.
    

Вроде как все работает ожидаемо.

Хороший вопрос, который можно задать к этому моменту – а где во всей этой истории разрешения, которые permissions? Ну да, определенному коду (сборке) нельзя выполнять «опасные» операции. А где «по-ресурсная» защита? Что, если мы хотим ограничить свой код так, чтобы он имел доступ к единственной папке на диске C:\, да еще только на чтение? Или то же самое для ключа реестра? Ведь даже прозрачный код имеет полный доступ к тому и к другому. Ну не входят эти ресурсы в список «ограничительных» операций! Что и доказывает Solution01\Project02 – «прозрачная» сборка творит на диске C:\ все, что захочет…

Или, что даже еще интереснее – а если нам принесли готовую управляемую .DLL с документацией по методам в ней содержащимся, но исходного кода у нас нет, а источник происхождения этой .DLL весьма туманен? Вот тут нам уже вообще никакие атрибуты не помогут, мы же не можем декорировать код, не имея исходников! Что тогда – мы не можем ограничить деятельность методов этой сборки отдельной папкой диска C:\?

Все возможно, но для этого нам предстоит ознакомиться с концепцией песочницы или, на языке оригинала, sandboxing. К этому и перейдем.

Sandboxing или игры в песочнице

Итак, нам со всей очевидностью требуется механизм, который займется раздачей прав «по-сборочно» и «по-ресурсно». Т.е. заменит собой те самые CAS-политики, раз уж их больше нет.

Новая концепция безопасности предлагает такой механизм в виде гомогенного (т.е. однородного) домена приложения (homogeneous application domain). Что такое домен приложения? Да в принципе хорошо нам знакомый AppDomain. А давно ли он стал homogeneous и каким он был до того? Стал недавно, с версии 4.0, а до этого был, очевидно, противоположным – гетерогенным (разнородным, heterogeneous). Разница в этих двух типах доменов сводится к тому, что в разнородный домен можно было загрузить сколько угодно сборок, но каждая из них могла обладать своим набором прав, который назначался (кстати – в момент загрузки сборки!) CAS-политикой. А в однородный домен можно загрузить сколько угодно сборок, но каждая из них будет обладать идентичным набором разрешений. Ровно тем, что «присвоена» самому домену. И вот такой-то «унифицированный» в вопросе разрешений домен и называют песочницей. Или sandbox, кому как нравится.

А вот непосредственно описание метода, создающего такие песочницы:

      public
      static AppDomain CreateDomain(
  string friendlyName,
  Evidence securityInfo,
  AppDomainSetup info,
  PermissionSet grantSet,
  params StrongName[] fullTrustAssemblies
)

Смысл параметров такой:

После создания домена мы можем загружать в него сборки сомнительного происхождения (да что там сомнительного, мы можем туда сборки с вирусами и троянами загружать, практически :) ) и выполнять их со спокойным сердцем. Автор сборки может изгаляться как угодно, обвешать свою сборку атрибутами SecurityCritical со всех сторон и требовать предоставить ему самые толстые полномочия для выполнения. Получит он ровно то, что мы сами указали в параметре grantSet.

Еще раз. Все и любые сборки (два особых случая пока опускаем), загруженные в домен-песочницу, получают разрешения этой песочницы, только разрешения этой песочницы, и ничего кроме разрешений этой песочницы. И никакими ухищрениями это правило не обойти. Точка, end-of-story.

К этому моменту должен родиться следующий правильный вопрос. Пусть у нас есть основное приложение Console.exe и библиотека Lib.dll. Согласно только что описанному краеугольному правилу раздачи прав мы можем хоть первой же строчкой кода exe-приложения создать песочницу, ограничить ее в правах, загрузить туда Lib.dll и спокойно с ней работать. Все выглядит великолепно, не понятно только – а кто для Console.exe песочницу-то создаст? И права ей назначит? И вот тут мы подходим к концепции хостируемых (hosted) и нехостируемых (unhosted) управляемых приложений.

Хостируемое приложение загружается в любезно подготовленный кем-то домен-песочницу и исполняется в нем. Кто и что может выступать в роли этого «кого-то»? Да почти что угодно: ASP.NET run-time, SQL CRL run-time, браузер IE, наша Console.exe в конце концов…

А нехостируемое приложение поступает «на разбор» непосредственно CLR, им же загружается и им же запускается на выполнение.

Итого, в нашем примере, как все уже догадались, Console.exe – unhosted, а Lib.dll – hosted приложение.

Строго говоря, любое управляемое приложение, запущенное пользователем будет unhosted. Домен ему будет предоставлен средой исполнения. А какие разрешения будет иметь этот домен? Ну – с этим совсем просто: он будет иметь все разрешения. Без исключения. Так как unhosted-приложение есть суть приложение с полным доверием (full trusted). А вот те библиотеки (более формально – сборки), что главное приложение подгрузит по ходу своей работы, могут быть как unhosted (если приложение не будет заморачиваться песочницами и просто загрузит их в свой же домен; кстати – физическое расположение библиотек на диске/в сети не будет играть никакой роли, важен лишь целевой домен их загрузки), так и hosted (если песочница все же будет создана). В первом случае библиотеки станут вновь полностью доверенными, во втором – частично доверенными (partially trusted). У полностью доверенных приложений есть лишь один шаблон обладания правами – обладать ими всеми без исключения. У частично доверенных приложений будет почти бесконечное множество сочетаний тех прав, которыми они обладают, но у всех этих наборов будет одна характерная черта – они все будут не полными. А иначе такое приложение автоматически перейдет в категорию full trusted, не так ли? Можно сказать, что на концептуальном уровне новый механизм оставил всего 2 permissions set: полный набор (full) и не полный набор (partially).

ПРИМЕЧАНИЕ

Если вы спросите меня – «а что, если в Windows Explorer зайти на local network share и двойным щелчком запустить приложение оттуда – неужто и оно запустится как full trusted?» – то я отвечу утвердительно. Именно как full trusted такое сетевое приложение и запустится. Правильно ли это? Ну, автор этих строк полагает что да, пожалуй, все верно. По сути, момент запуска приложения – это вопрос доверия запускающего его пользователя программисту/издателю этого приложения. Не знаешь? Не уверен? Не запускай!!! А вот если доверие программисту есть, то пользователь выражает уверенность, что все будет хорошо, вне зависимости от того, где сборка расположена. Разумеется, этот «аттракцион неслыханной щедрости» раздачи прав не действует в случае Internet Explorer (иначе это был бы уже диагноз :) ). Все, что попытается запуститься в IE, будет запущено в песочнице. Как уже было замечено выше, IE – это один из «хост-провайдеров» или «строителей песочниц», и осмотрительно запускает все, что запрашивает пользователь, в среде с сильно усеченными правами.

Давайте снова займемся программированием, Solution01\Project03. Мы хотим узнать о нашей сборке две вещи:

ПРИМЕЧАНИЕ

Возможно, в свете вышесказанного, вы удивитесь тому, что свидетельства (evidences) все еще остались при новой модели безопасности. Ну ладно раньше, когда они были неотъемлемой частью CAS-политик… Но сейчас? Зачем они нужны? Кто их анализирует? Выясняется, что никто их не анализируют, никакого влияния они на раздачу прав они не оказывают (в чем мы сейчас прямо и убедимся), а оставлены, надо полагать, разве что с целью обратной совместимости.

Код снова элементарен и не требует никаких комментариев, поэтому просто запускаем и видим:

      Zone Evidence: MyComputer
      Is Fully Trusted: True
    

Все более чем ожидаемо. А теперь поступим так: создадим любую папку (скажем C:\MyShare), откроем к ней общий доступ, скопируем в нее Project03.exe из текущего проекта, в Windows Explorer перейдем по адресу \\<имя_компьютера>\<имя_созданной_папки> и двойным щелчком по программе Project03 запустим ее на выполнение еще раз. Видим:

      Zone Evidence: Intranet
      Is Fully Trusted: True
    

Что и требовалось доказать. Отныне есть лишь один критерий назначения набора разрешений коду: тот домен, куда код (сборка) загружается и разрешения этогодомена. У нас в обоих случаях домен создавался непосредственно CLR, или, что эквивалентно, мы имели дело с unhosted-приложением, что означает, что нам полностью доверяли.

А теперь – вместе

К этому моменту мы бегло рассмотрели два механизма нового подхода к безопасности кода:

Закономерный вопрос: а что – эти подходы как-то внутренне связаны, или же это полностью независимые механизмы? Правильным будет второй ответ, и вот почему.

Идея «слоеного кода» состоит в «покомандной» защите. Мы препятствуем коду выполнить набор опасных команд (см. перечисление выше). При этом защита ресурсов (папок, файлов, реестра и т.п.) нас вовсе не интересует. Поясним сказанное примером, Solution01\Project04. На этот раз наше приложение Project04.exe будет вызывать метод из другой сборки – Project04_Lib.dll. Легко заметить, что изначально обе сборки помечены как SecurityTransparent, что совершенно не мешает им обратиться к довольно чувствительному месту системы – реестру. Это потому, что они обе полностью доверенные. Теперь мы решаем, что метод ReadReg() библиотеки Project04_Lib.dll должен принадлежать к списку «опасных команд», и помечаем его атрибутом SecurityCritical, убирая комментарий из строки 11 и одновременно комментируя для этой сборки атрибут assembly: SecurityTransparent, т.к. сборка не может быть прозрачной, если в ней есть хотя бы 1 непрозрачный член. Запуск измененного проекта приводит к исключению:

      System.MethodAccessException was unhandled
      Message=Attempt by security transparent method 'Project04.Program.Main()' to access security critical method 'Project04_Lib.Lib.ReadReg()' failed.
    

При этом обе сборки остались полностью доверенными! Но - «опасная операция», и как следствие - искусственная преграда платформы для вызова критического кода из кода прозрачного. Кстати говоря, чтобы восстановить работоспособность примера, можно, конечно, проделать две обратные операции над атрибутами в коде библиотеки Lib.cs. Но можно и в коде приложения (Program.cs) просто закомментировать атрибут assembly: SecurityTransparent. Это приведет к исчезновению прозрачного кода как такового, а значит, и список опасных команд потеряет свою актуальность. И давайте еще проясним такой вопрос – а каким будет код в сборке, не помеченной никаким из трех возможных атрибутов, определяющих принадлежность тому или иному слою? Как раз ситуация после нашего последнего редактирования файла Program.cs.

Итак, если сборка не помечена никаким из трех атрибутов безопасности:

Я не зря выделил во втором правиле слово "любой". Постоянно держите в голове, что сборка, загружаемая в песочницу, обычно становится частично доверенной. Однако есть два исключения (добраться до разбора которых нам все еще мешают первоочередные вопросы), при которых сборка и в таком домене будет полно доверенной. Тогда правило №2 будет относиться к ней в полной мере.

Вот и объяснение наблюдаемому: после удаления атрибута из Program.cs сборка Project04 стала critical и смогла без проблем вызвать не менее critical метод ReadReg().

Если бы весь существующий в мире .NET-код был нашим и доступным нам в исходниках, на этом можно было бы ставить точку. Описанный механизм – это все, что нам нужно. Предположим, мы создали метод WriteToCFolder() для записи файлов в C:\Folder и считаем, что этот метод потенциально опасен? Пометили его SecurityCritical. Пишем WinForms-приложение и уверены, что ему нечего делать в C:\Folder? Соответственно, во-первых, сами не программируем его таким образом, а во-вторых, для страховки еще и помечаем как SecurityTransparent. После этого вызвать метод WriteToCFolder() у нас не получится никак, ни умышлено, ни по неосторожности. Пишем Web-сервис, который может обратиться к WriteToCFolder(), но очень «осторожно», так как требуется соблюдение ряда условий? Помечаем и его как SecurityTransparent, и дополнительно создаем метод WriteToCFolder_Proxy(), который помечаем атрибутом SecuritySafeCritical. Напрямую сервис к «опасному» методу не обратится, а поток данных через «прокси» будет этим самым прокси же и контролироваться, как и соблюдение входных условий. Кстати, код из этого (safe critical) слоя стоит особняком от своих «коллег». Если transparent / critical это, по большому счету, обычный рабочий код (первый – с рядом ограничений, а второй – просто обычный, без всяких оговорок), то safe critical-код:

Однако, в силу того, что изрядная часть кода нашего приложении приходит «со стороны» (в конце концов, даже Base Class Library самого Framework – уже не наш код) и, как следствие, без исходников, мы вынуждены брать на вооружение идею песочницы. В этом случае нет опасных операций, а есть критичные (или, кстати, если хотите, снова «опасные») ресурсы, то есть это подход, защищающий ресурсы и работающий для любого кода. Даже сборки из Base Class Library будут эту защиту «уважать» (однако снова напоминаю о двух особых случаях, см. далее). И с первым механизмом этот практически не связан, если не брать связь косвенную, возникающую в случае без атрибутных сборок. А так мы можем в нашем коде применять любой из подходов поодиночке, комбинировать их, а также не пользоваться ими вовсе (хоть это и будет несколько обидно – для чего я тут стараюсь? :) ). Справедливости ради замечу, что абсолютный отказ от обоих механизмов невозможен. Даже если мы написали консольное приложение класса “Hello World”, и ничего не знаем о материях, изложенных выше – то уже такой код элементарной сборки становится critical (заодно проверьте себя – можете ли вы объяснить, почему это будет именно так), а значит, механизм №1 уже включен. Другое дело, что чисто критический код лишен какого либо смысла в плане безопасности, смысл появляется, когда он начинает комбинироваться с прозрачным и safe critical-кодом. Но чисто технически механизм все же будет включен. Помните – код .NET 4 всегда «лежит» в одном из слоев, без исключений.

Строим замки. Из песка

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

Давайте сразу приступим к коду и сосредоточимся на проекте Solution01\Project05. Этот проект состоит из основной консольной программы, а также из проекта вспомогательного, Solution01\Project05_Lib. Соответственно, файл Program.cs из первого проекта содержит код приложения, Lib.cs из второго – код библиотеки. К чему будем стремиться: чтобы приложение было загружено CLR в главный домен (я буду условно ссылаться на него как MAIN domain), чтобы оно создало домен вспомогательный, с ограниченными правами (SANDBOX domain), и чтобы библиотечный метод ReadReg() был выполнен именно в последнем. Ввиду далеко не тривиального кода, особенно для сталкивающихся впервые с песочницами, разберем код обоих файлов достаточно подробно.

Program.cs

      public
      class Program : MarshalByRefObject

Прежде всего, обращает на себя внимание, что класс приложения объявлен наследником класса MarshalByRefObject. Этот потому, что стратегия достижения поставленной задачи у нас будет такая:

Очевидно, в последнем пункте неизбежно пересечение границ домена (AppDomain boundaries), что достижимо только через механизм маршаллинга (marshalling), который и включается указанным наследованием.

      static
      void Main()
{
    PrintInfoAboutDomainAndAssemblies();

Первое что мы делаем – пытаемся распечатать главную информацию по стартовому домену, MAIN domain. Нас интересует:

Возьмите себе на заметку свойства IsSecurityTransparent, IsSecurityCritical, IsSecuritySafeCritical класса Type. Во многих случаях они помогут вам разобраться с неочевидными моментами при работе с новой системой безопасности.

      //we need, at least, set the path to the assembly to load
AppDomainSetup setup=new AppDomainSetup 
{ 
  ApplicationBase=Environment.CurrentDirectory 
};

Переходим к формированию песочницы. Первое, что нам потребуется – настройка будущего домена, а в ней, как минимум, один параметр: путь к загружаемой сборке. В нашем случае такая сборка (Project05_Lib.dll) «лежит» рядом с основной программой (Project05.exe).

      //
      создание нужного набора разрешений
    PermissionSet permissions=new PermissionSet(null);

Центральная часть нашего кода – определение набора разрешений, которые будет иметь новый домен и все сборки, в него загруженные. Начинаем, как положено, с совершенно пустого набора.

      //permissions.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
      //permissions.AddPermission(new RegistryPermission(RegistryPermissionAccess.Read, Registry.CurrentConfig.Name));
    

Со временем мы добавим в этот набор пару разрешений. Однако пока сборкам нельзя будет делать в таком домене абсолютно ничего.

      // создание домена-песочницы
    AppDomain appDomain=AppDomain.CreateDomain(

Непосредственно создаем новый домен. Обращу ваше внимание на то, что метод AppDomain.CreateDomain() имеет 6 перегрузок, из которых для изготовления песочниц годится один(!) – с параметром PermissionSet в подписи. Только он готов назначить новому домену набор разрешений, а это – ядро нового подхода.

      "SANDBOX domain", // дружественное имя домена

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

      null, // evidence указывать не нужно

По идее, можно передать evidence, т.е. свидетельство. Однако, как подсказывает старый-добрый .NET Reflector, вы можете передавать в этом параметре что хотите, на обработку уйдет все равно null. :) Оно и понятно – некому больше анализировать свидетельства, политики-то закончились…

            setup, // инициализирующая информация для домена, или хотя бы путь к загружаемым сборкам

Настройки домена с, как минимум, путем к загружаемым библиотекам.

            permissions); // набор разрешений, предоставляемых всем сборкам, загружаемым в этот домен

О важности и уникальности этого параметра было сказано только что.

      // загрузка сборки в новый домен; создание нового экземпляра типа 'Program' в этом (новом) домене
    Program p=(Program)(Activator.CreateInstance(appDomain, "Project05", "Project05.Program").Unwrap());

К этому моменту новый домен создан. Мы переходим к загрузке в него той сборки, функциональность которой нам требуется. Нам нужен экземплярный метод RunMeInSandbox() (см. ниже) класса Program, для чего мы в новом домене создаем экземпляр указанного класса и сразу же получаем в главном домене прокси к этому экземпляру, дабы иметь возможность им управлять. Параметры создания удаленного объекта p:

      // Этот метод будет исполняться в новом (частично доверенном) домене
    p.RunMeInSandbox();

И – кульминация, вызов метода в соседнем домене. Как мы знаем p – это прокси к удаленному объекту, а значит указанный метод RunMeInSandbox() выполнится именно в песочнице, а не в MAIN domain!

    Console.ReadKey(true);
}

void RunMeInSandbox()
{
    Project05_Lib.Lib.ReadReg();
}

Собственно – первая строка метода RunMeInSandbox() и есть тот код, с которого начнется работа вспомогательного домена. Какова функциональность указанного метода (а точнее, его единственной строки), станет ясно из разбора файла исходного кода библиотеки.

Lib.cs

Мы видим, что вся деятельность метода, которым мы только что завершили разбор кода файла Program.cs, сведется к вызову указанного метода из сборки библиотеки. Разумеется, для этого последняя должна будет загрузиться в какой-то домен. По умолчанию загрузка пойдет в тот домен, из которого произошел вызов, а, стало быть – в SANDBOX domain. Не менее само собой разумеющимся представляется факт, что вызов RunMeInSandbox()->Project05_Lib.Lib.ReadReg() будет локальным, т.е. случится полностью в границах упомянутого домена.

Посмотрим, что делает библиотека.

      public
      static
      void ReadReg()
{
    PrintInfoAboutDomainAndAssemblies();

Совершенно аналогично одноименному методу предыдущего файла, только по отношению к SANDBOX domain.

    RegistryKey rk = Registry.CurrentConfig;
    string[] names = rk.GetSubKeyNames();

Пробуем выполнить операцию запрещенную, по идее, в рамках не уполномоченного на то домена.

    Console.WriteLine("Read the Registry Keys - done!");

Получилось? Рапортуем об успехе!

}

Ну вот, вроде код разобран построчно, давайте «поиграем» с ним. Запускаем первую версию приложения.

При запуске Project05.exe из Visual Studio 2010 рекомендую пользоваться пунктом меню Debug->Start Without Debugging или, что проще и эквивалентно, нажать Ctrl+F5. Это позволит видеть в консольной распечатке только реальных «участников спектакля». При запуске по F5 в целом поведение не изменится, но Visual Studio подгрузит в домены еще порядка 5-7 совершенно не интересных нам сборок для поддержки своей инфраструктуры. Далее весь вывод тестовых прогонов приводится для рекомендуемой методики запуска.

Ага, получили исключение. Жмем Close the program в окне сообщения об ошибке и, игнорируя частичный вывод нашей программы в консоль, сосредоточимся на информации об исключении:

      Unhandled Exception: System.IO.FileLoadException: Could not load file or assembly 'Project05, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies.
      PolicyException thrown. (Exception from HRESULT: 0x80131416) ---> System.Security.Policy.PolicyException: Execution permission cannot be acquired.
    

Анализируем. Вроде как сообщение намекает нам, что есть такой Execution permission, которого у нас нет? Да – все верно. Даже если сборке нужно в новом домене выполнить только сложение двух целых, уже находящихся в регистрах CPU (я уж молчу про такие «крупномасштабные» операции, как вывод на консоль), ей УЖЕ требуется разрешение. Причем именно то, на которое жалуется исключение. OK – предоставим, для чего уберем один комментарий со строки 18 файла Program.cs. Ctrl+F5 и получим новое исключение, на этот раз:

      Unhandled Exception: System.Security.SecurityException: Request for the permission of type 'System.Security.Permissions.RegistryPermission, mscorlib, Version=4.0.0.0, Culture=neutral,
      PublicKeyToken=b77a5c561934e089' failed.
    

Ну, тут-то без вопросов. Как мы и ожидали, для доступа к реестру нужно свое разрешение. В нашем случае требуется, как минимум, разрешение на чтение ключа \\HKEY_CURRENT_CONFIG\ и его подключей. Дадим и его, убирая комментарий со строки 19 того же файла. Ctrl+F5 и – полный успех! Осталось проанализировать вывод на консоль:

      Info About MAIN domain. Is this domain  Full Trusted: True
      2 assemblies loaded in this domain. Info about:
    

В наш главный домен, который, безусловно, является полно доверенным, загружены две сборки:

      * Simple Name: mscorlib
               **Is this assembly Is Full Trusted: True
    

Сборка, имеющая место быть в любом домене, потому как это ядро системы. Полное доверие к ней сомнения не вызывает.

      * Simple Name: Project05
               **Is this assembly Is Full Trusted: True
    

Сборка нашего консольного приложения. Тоже полностью доверенная, тоже без вопросов.

      Code in this domain belong to Critical layer
    

Код, исполняемый в текущий момент, т.е. код класса Program, является критическим. Верно, мы же никаких атрибутов безопасности не применяли.

      Info About SANDBOX domain. Is this domain Is Full Trusted: False
      3 assemblies loaded in this domain. Info about:
    

Новый домен. Песочница. Уже НЕ полностью доверенный – это хорошо.

      * Simple Name: mscorlib
               **Is this assembly Is Full Trusted: True
    

Снова ядро системы. И снова полностью доверенная?? В частично доверенном домене? Такое возможно? Так, а я вас предупреждал! Пока примем как факт, а к механике процесса обратимся позже.

      * Simple Name: Project05
               **Is this assembly Is Full Trusted: False
    

Снова сборка нашего приложения, т.е. содержащая Project05.exe. Однако на этот раз – частично доверенная! Как вам? Одна и та же сборка, в одном и том же процессе (не путаем процессы и домены, OK?), в течении одной и той же сессии исполнения имеет различные привилегии! По-моему - гениальная задумка. :)

      * Simple Name: Project05_Lib
               **Is this assembly Is Full Trusted: False
    

Наконец информация не вызывающая эмоций: библиотека, частично доверенная. Ровно как заказывали.

      Code in this domain belong to Transparent layer
    

По той же причине, что ранее привел к помещению кода класса Program в критический слой, код класса Lib поместился в слой прозрачный.

      Read the Registry Keys - done!
    

Ну и наконец – чтение ключей реестра кодом, который, фактически, только это и может сделать. Поползновения на доступ к файловой системе или попытки установить соединение с SQL Server будут неуклонно пресекаться. Только и именно чтение, только указанного ключа. И еще раз заметьте – код библиотеки в нашем примере мало того что крайне ужат в дозволенной функциональности, так он еще и прозрачен. И после этого кто-то будет утверждать что .NET Framework 4.0 потерял в стойкости защитных механизмов?

Но что, если библиотеку нам принесли «со стороны», в виде готовой .dll, а ее автор был настолько хитроумен, что догадался снабдить свою сборку атрибутом SecurityCritical? Не пойдет ли прахом только что выстроенный и кажущимся таким надежным барьер вокруг ненадежного кода? Ведь если библиотеке удастся стать critical, она сможет вызвать «наши» методы, т.е. код класса Program!

Проверим. Встанем на место такого «продвинутого» писателя библиотек и уберем комментарий со строки 6 файла Lib.cs.

ПРЕДУПРЕЖДЕНИЕ

Мини-тест 1: не запуская отредактированный вариант проектов Project05/Project05_Lib предсказать его поведение. Что будет напечатано (если вообще будет) на консоли?

Надеюсь, что к этому моменту ответ на тестовый вопрос будет для вас если и не очевидным, то уж задуматься на очень долго не заставит. Ну и конечно, давайте нажмем Ctrl+F5 и проверим себя. Да, как и ожидалось, абсолютно никаких изменений. Вывод на консоль совершенно идентичен только что проанализированному. Более того, исполняющая среда попросту игнорирует всю «слое-безопасную» атрибутику любой частично доверенной сборки! Только fully trusted-сборки могут «двигать» свой код между слоями. Partially trusted-сборки со всем своим содержимым трактуются как прозрачные, только прозрачные, и никогда иначе.

Вносим сумятицу. Полно доверенные сборки в частично доверенных доменах

Пришло время разобрать тонкий момент, который автор упоминал снова, и снова, и снова по ходу изложения материала, а именно: почему возможна ситуация, когда домен – частично доверенный, а сборка, в него загруженная – пользуется полным доверием? Но сначала краткое вступление в тему на предмет, зачем вообще оставили такую «лазейку» вместо тотального затыкания всех “back-doors”.

Возьмем для примера последний из разобранных проектов, Solution01\Project05(Project05_Lib). В нем, как вы помните, основная сборка Project05.exe успевает побывать «единой в двух лицах» - и как полно доверенная сборка в основном домене, и как сборка с частичным доверием в песочнице. С первым все ясно, а со вторым может быть и «напряг» – что, если указанная сборка действительно должна иметь полные права для своей нормальной работы и при этом я, как ее автор, конечно же, доверяю ей безоговорочно? Тогда, если мне придется загрузить ее в частично доверенный домен, как в нашем случае, мы будем иметь несколько искусственное ограничение, во-первых (к чему ограничивать на 100% безопасную сборку?), и просто потеряем нужную нам функциональность, во-вторых. Кроме того, есть сборки (та же mscorlib.dll) которые просто обязаны быть полно доверенные в любом домене, опять же по причине возможности потери части своего функциональности со всеми вытекающими.

Вступление завершено, нам предстоит научиться пользоваться означенной возможностью, а заодно выяснить – почему это mscorlib.dll (и не только она, заметим) сама пользуется ею, не требуя от нас никакого «тюнинга» в этом вопросе?

Итак, формально говоря, есть 2 пути загрузить сборку в любой домен как полно доверенную, фактически игнорируя права этого домена. Разберем с помощью проектов Solution01\Project06(Project06_Lib) путь первый.

Указанная пара проектов дублирует предыдущую пару, в том виде, как мы ее оставили, т.е. с доменом-песочницей, имеющей всего пару разрешений. Отличие лишь одно, и код как таковой оно совершенно не затрагивает, т.к. сводится к подписыванию обоих проектов своими парами ключей. Как вы, несомненно, знаете, это означает, что обе сборки обзавелись строгими именами (strong name). Однако запуск проектов на исполнение не выявляет никаких дивидендов от такого строгого имени. Все работает, но вывод на консоль совершенно аналогичен выводу последнего испытания предыдущей пары проектов: в частично доверенном домене все сборки (за исключением mscorlib.dll) являются точно такими же с точки зрения доверия к ним. Т.е. пока отличий ровно никаких.

Что ж – давайте теперь выполним несложную операцию и поместим сборку нашей библиотеки Project06_Lib.dll в глобальный кэш сборок, больше известный по своей аббревиатуре GAC. Для этого я на своей машине выполнил такую команду в Visual Studio Command Prompt (2010):

      C:\>gacutil /nologo /if c:\VSprojects\Solution01\Project06_Lib\bin\Debug\Project06_Lib.dll
    

И проверил факт успешной инсталляции сборки в GAC:

      C:\>gacutil /nologo /l Project06_Lib
      The Global Assembly Cache contains the following assemblies:
        Project06_Lib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d2ac21307c3cd6e0, processorArchitecture=MSIL
      Number of items = 1
    

Все отлично, возвращаемся в студию и без всяких перекомпиляций (код-то мы не меняли) жмем Ctrl+F5. Хмм…

      System.MethodAccessException was unhandled Message=Attempt by security transparent method 'Project06.Program.RunMeInSandbox()' to access security critical method Project06_Lib.Lib.ReadReg()' failed.
    
ПРЕДУПРЕЖДЕНИЕ

Мини-тест 2: попробовать объяснить наблюдаемое. Далее приводится ответ, так что если вы хотите проверить себя – поставьте чтение статьи «на паузу», запишите свой вариант ответа, а затем сравните с объяснением автора.

На текущем этапе разбора новых механизмов безопасности ‘Мини-тест 2’ будет довольно заковырист, ответить на него правильно можно только по косвенным намекам в нескольких предыдущих абзацах…

Итак – верный ответ. Во-первых, какая именно сборка библиотеки теперь была загружена в песочницу? Была загружена именно GAC-сборка. То, что ее копия «лежит» рядом с Project06.exe, ровным счетом ничего не меняет, грузится именно GAC-вариант – он приоритетен с точки зрения CLR. Во-вторых (самое главное), с каким доверием была загружена эта GAC-сборка? Вот тут как раз и выясняется – с полным, то бишь fully trusted. Это все потому, что любая сборка из глобального кэша в любом домене имеет неограниченные права. Прояснилась для вас «магия» mscorlib.dll? Считаю только что описанный принцип оправданным на все 100%. В нормальных условиях сборка, которой мы не доверяем хотя бы в малом, не может очутиться в столь… кхм, «интимном» :) месте нашей машины. Ясно, что уж кому-кому, а GAC-сборкам мы верим без оглядки. Тогда к чему контроль за ними? Однако продолжим и заметим, что со сборкой Project06.exe вообще ничего нового не происходило – как она грузилась в песочницу partially trusted, так и продолжает. А тогда, в-третьих, в каких «слоях» окажутся в песочнице коды приложения и библиотеки? По причинам, изложенным ранее, код первого будет transparent, а второго – critical. И, наконец, в-четвертых – что мы имеем при попытке вызвать второе из первого (именно это происходит при вызове из RunMeInSandbox() библиотечного метода Lib.ReadReg())? Да именно то, что наблюдаем!

Как же нам восстановить работоспособность примера? Наиболее релевантным для нашей текущей темы будет путь загрузки и приложения тоже как fully trusted сборки. Тогда, как вы понимаете, обе сборки станут critical и договорятся друг с другом без проблем. Что ж, давайте реализуем. Вообще-то, можно было бы и Project06.exe «затолкать» в GAC. И кстати, знаете ли вы, что начиная с .NET версии 2.0 исполняемые модули без проблем размещаются в кэше? Но это будет выглядеть странно, совершенно нетипично и, что важнее, не прольет свет на второй способ достижения полного доверия в домене с доверием частичным. А посему мы выберем совсем иную реализацию.

Дело в том, что разбирая реальные примеры создания песочниц методом CreateDomain(), мы до сих пор совершенно игнорировали его последний параметр – fullTrustAssemblies. Поскольку данный параметр имеет модификатор params, то при вызове метода его можно просто не указывать, что эквивалентно передаче в нем значения new StrongName[0]. Именно так мы и поступали. Однако пришел и его черед. Как явственно следует из декларации параметра, он ждет от нас массив строгих имен сборок. Благодаря наличию метода Evidence.GetHostEvidence<T>(), появившегося очень кстати в .NET 4.0, извлечение требуемого из загруженной сборки может быть выполнено в одну команду. Скажем, строгое имя текущей сборки получить очень просто:

StrongName thisStrongName = 
  Assembly.GetExecutingAssembly().Evidence.GetHostEvidence<StrongName>();

Давайте передадим массив из одного строгого имени нашего приложения, для чего раскомментируем строку 27 файла Program.cs. Запускаем – полный успех!

Осталось проанализировать вывод на консоль. Для главного, стартового, домена не изменилось абсолютно ничего, все и вся выполняются с полным доверием, кругом critical-код. А вот для песочницы…

      Info About SANDBOX domain. Is this domain Is Full Trusted: 
      
        False
      
    

Домен по прежнему НЕ полно доверенный!

      3 assemblies loaded in this domain. Info about:
      * Simple Name: mscorlib
               **Is this assembly Is Full Trusted: True
    

Здесь без изменений, ядру – полное доверие всегда!

      * Simple Name: Project06
               **Is this assembly Is Full Trusted: 
      
        True
      
    

Наше приложение, загруженное в домен без Full Trusted, исполняется в нем как полно доверенное!

      * Simple Name: Project06_Lib
               **Is this assembly Is Full Trusted: 
      
        True
      
    

Совершенно аналогично для кода библиотеки

      Code in this domain belong to Critical layer
      Read the Registry Keys - done!
    

Код теперь, вполне ожидаемо, относится к critical-слою.

Промежуточный итог. Чтобы сборка могла претендовать на полное доверие в домене с доверием частичным, необходимо и достаточно выполнения любого из двух условий:

ПРЕДУПРЕЖДЕНИЕ

Микро-тест 1: какой ярко выраженной общей чертой будут обладать все подобные сборки?

Ответ будет приведен в конце данной главы.

Замечу, что наш SANDBOX-domain, хоть и сильно усечен в своих правах, но изначальную пару permissions все же имеет. Интересно будет проверить работу приложения БЕЗ предоставления домену хоть каких-то прав.

ПРЕДУПРЕЖДЕНИЕ

Мини-тест 3: закомментируйте строки 19 и 20 файла Program.cs по отдельности, а затем совместно. Без запуска приложения предскажите его поведение в каждой из трех новых редакций.

Честно говоря, "Мини-тест 3", пожалуй, будет самым сложным во всей статье. Предсказать поведение при комментировании каждого из разрешений по отдельности, а также обоих сразу, очень не просто.

Приведем сперва просто сухой отчет:

Давайте разбираться по порядку. Начнем с RegistryPermission. Тут есть очень тонкий нюанс: полно доверенная сборка в домене с частичным доверием (это наш пример) и такая же сборка в домене с полным доверием. Дело в том, что в первом случае код сборки может (потенциально) делать все, что захочет. Но для этого он должен заявить об этом явно, утверждая самому себе требуемые привилегии (метод PermissionSet.Assert()). А просто, без лишних телодвижений, ему, как и коду с частичным доверием, по-прежнему доступен лишь набор операций, утвержденных доменом. В случае же домена с полным доверием все разрешения уже утверждены. Можете, если хотите, запомнить такое правило: полно доверенная сборка в домене с частичным доверием не делает авто-поднятие привилегий под каждую требуемую операцию; полно доверенный же домен занимается именно этим для любой и каждой сборки, в него загруженной.

Итого, если домен-песочница не утвердил доступ к ключу реестра, то мы обязаны в своей сборке сделать это сами, например, убрав комментарий со строки 14 в файле Lib.cs. Только не забудьте еще поместить эту новую версию библиотеки в GAC, заменив предыдущую. Для этого нужно выполнить в консоли всю ту же команду:

      C:\>gacutil /nologo /if c:\VSprojects\Solution01\Project06_Lib\bin\Debug\Project06_Lib.dll
    

Так – а что насчет SecurityPermission? Почему те же рассуждения не действуют на него? Ведь похоже, что частично доверенная сборка именно что автоматически повышает привилегии для этого разрешения?! Нет, здесь дело не в ошибочности предыдущего рассуждения, оно верно без всяких исключений – частично доверенная сборка не выполняет никаких неявных поднятий разрешений, изначально прописанных в настройках домена. Здесь уже в игру вступают принципиально разные сущности двух этих разрешений. RegistryPermission – это такой "классический" образчик разрешений, известных большинству разработчиков. Кто-то, где-то, обычно внутри библиотек самой платформы, проверяет (PermissionSet.Demand()) его, начинается обход стека вызова вверх до границ домена и если хоть один метод в стеке разрешения не имеет (или любой из методов ниже не утвердил его упомянутым PermissionSet.Assert()) – генерируется исключение. SecurityPermission же (по крайней мере для ряда своих флагов из перечисления SecurityPermissionFlag; интересный нам флаг Execution входит именно в этот ряд) не вызывает обход стека. То есть вообще. Он оценивается не в момент исполнения кода сборки, а в момент ее загрузки в домен. И тут возможны всего два варианта:

      Unhandled Exception: System.IO.
      
        FileLoadException
      
      : Could not load file or assembly 'Project06,...’ or one of its dependencies. Execution permission cannot be acquired.
    

Короткий вывод из наблюдаемого: если вы пишете библиотеку – не забывайте утверждать нужные вашему коду разрешения! Всегда исходите из того, что кто-то из ее пользователей может загрузить ваш код в домен с частичным доверием, но саму сборку назначить полно доверенной. С другой стороны – не злоупотребляйте. Можете обойтись без утверждений в вашей сборке – безусловно, обходитесь. Пользователи смогут грузить ее в частично доверенный домен как partially trusted-сборку, и она будет прекрасно работать в прозрачном слое. И вам хорошо, и им спокойнее.

С другой стороны, если ваш код помещен в прозрачный слой (свойство IsSecurityTransparent к вашим услугам, помните?), даже не пытайтесь что-либо утверждать (имеется в виду метод PermissionSet.Assert()). Почему? Еще раз ознакомьтесь со списком «опасных» операций для такого кода. Правда, это не значит, что вы в такой ситуации гарантированно не можете выполнить обращение к реестру, запись файла и т.п. Тут все будет зависеть от настроек домена с частичным доверием. Как всегда, очень не помешает приложить к вашей библиотеке «справку по безопасности», «user guide» и т.п. Благодаря ей гораздо выше вероятность, что ваша сборка попадет в правильно настроенный домен и сможет обойтись вообще без каких-либо утверждений.

В завершение раздела приведу, как и обещал, ответ на Микро-тест 1: все сборки, претендующие на полное доверие в доменах, отличных от full trusted, обязаны иметь… верно, строгие имена. :)

Детализируем защиту. Ограничения доступа к отдельным методам

Давайте на минутку представим себя авторами кода центральной библиотеки платформы - mscorlib.dll. Если бы в качестве тестового задания при поступлении на работу в Microsoft нам бы предложили написать ее код :) - каким из трех атрибутов безопасности вы бы пометили такую сборку? При размышлениях на эту тему необходимо учитывать:

Итого – что пишем в качестве атрибута безопасности? SecurityCriticalAttribute или, что в данном случае эквивалентно, просто опускаем все атрибуты? Не вариант. Будущие разработчики программ на нашей платформе станут помещать неизвестные им библиотеки в песочницы, чтобы сделать их код transparent, а этот последний будет вызывать наши методы «нулевой опасности», как мы их обозначили в списке выше (классы Math, Random и т.д.). И это будет вполне допустимые операции, но приводящие к исключению "Attempt by security transparent method to access security critical method". Тогда – SecurityTransparentAttribute? Не вариант. При применении этого атрибута к сборке (а цели мельче сборки он не принимает) весь, без исключения, код ее становится прозрачным. Это не устраивает уже нас – как мы сами-то будем потенциально опасные операции выполнять из нашей библиотеки? И вообще, нам требуется, совершенно очевидно, гранулированный подход к защите. Часть методов части классов должны быть прозрачными, еще одна часть – критическими, а чтобы внешний прозрачный код мог контролируемо обращаться к этим последним, нам требуется третья разновидность методов – критически важные для безопасности (safe critical, атрибут SecuritySafeCriticalAttribute).

Давайте посмотрим, как к вопросу подошли разработчики реальной библиотеки mscorlib.dll, для чего обратимся к очередной паре проектов Solution01\Project07(Project07_Lib). Код почти идентичен таковому из проектов 05-06, только в качестве «опасной» операции мы возьмем метод Directory.Exists(), как раз реализованный в библиотеке mscorlib.dll. Наша собственная библиотека будет обращаться именно к нему из домена-песочницы. Поскольку на этот раз Project07_Lib будет лишь частично доверенной, мы должны дать разрешение на все операции, ею осуществляемые. Разрешение на исполнение мы ей предоставляем (традиционный new SecurityPermission(SecurityPermissionFlag.Execution)), а отдельного разрешения на проверку существования директории не требуется.

Вот еще одно отличие от предыдущей пары проектов. Мы уже интенсивно использовали свойство IsSecurityTransparent класса Type. Оно подсказывало нам, в каком слое находится код того или иного класса в данном домене. Выясняется, что одноименное свойство есть и у класса MethodBase, и делает оно то же самое, только уже на уровне отдельного метода. Вот и давайте проконтролируем – к какому слою принадлежит метод SearchFiles() библиотеки Project07_Lib, а к какому – Directory.Exists(). Запускаем разбираемый проект, читаем:

      Code of the method 'SearchFiles' of 'Lib' class belong to Transparent layer
      Code of the method 'Exists' of 'Directory' class belong to Critical layer
    

Очень, очень любопытное заявление! С учетом того, что первый вызывает второй, а также запрета на обращение transparent-кода к critical-коду возникает стойкое ощущение недопонимания. Но нет причин для беспокойства – все вскоре прояснится.

Открываем mscorlib.dll во всем знакомом Reflector-е, смотрим, какими атрибутами декорирована сборка (рисунок 3):


Рисунок 3. Атрибуты сборки mscorlib.dll

Атрибутов, как видим, изрядно. Тот, за которым мы «охотимся», выделен на рисунке. Итак – встречайте, AllowPartiallyTrustedCallersAttribute, почти всегда сокращаемый в литературе до аббревиатуры APTCA. Суть работы этого атрибута сводится к трем простым, но чрезвычайно важным правилам:

Вот и все. Если вы скажете, что APTCA крайне похож на SecurityTransparentAttribute, я с вами полностью соглашусь. Собственно, если из предыдущего списка удалить последний пункт, то это будет описание функциональности второго атрибута. Однако его наличие делает первый атрибут и сборку, его применившую, несравненно более гибкими в выборе стратегии безопасности! Мы, наконец-то, можем по-разному защищать различные классы и даже различные методы одного и того же класса. SecurityTransparentAttribute попросту делает ВСЕ (без исключения) классы, методы, поля и т.д. прозрачными и ставит на этом точку.

Продолжим наши изыскания и посмотрим через Reflector на вызываемый нами метод из разбираемой библиотеки, рисунок 4:


Рисунок 4. Атрибут метода Directory.Exists()

Вот все и прояснилось. Наш прозрачный метод SearchFiles() обращается не к критическому коду (что было бы нарушением основ новой системы безопасности), а к safe critical-коду, что является операцией вполне легитимной. Алгоритм нашего приложения лишь делит код на прозрачный / критический, не проводя дополнительную дифференциацию последнего на просто critical и safe critical. Отсюда и недоразумение.

ПРИМЕЧАНИЕ

Напомню, что свойства IsSecurityCritical, IsSecuritySafeCritical снимают любую неоднозначность в вопросе «слоя-принадлежности» кода.

Ради интереса копнем еще глубже и посмотрим – соблюдает ли сам Microsoft правило «хорошего тона», им же рекомендованное: назначение safe critical-кода – служить мостом между transparent- и critical-кодом. То есть по идее Directory.Exists()не должен осуществлять сам запрос от метода SearchFiles() и заниматься выяснением существования папки, а должен лишь «пробросить» его (после опциональных входных проверок передаваемых параметров) в некий метод критического слоя, который и выполнит реальную работу. Анализируем, рисунок 5:


Рисунок 5. Атрибут метода Directory.InternalExists()

Все сходится: ядро функциональности находится в методе Directory.InternalExists(), а он, вполне грамотно, помечен атрибутом SecurityCritical. Обратите внимание, как новая система безопасности органично «вплетается» в существующие с самого начала платформы модификаторы доступа, образуя этакую «вторую линию обороны». Мало того, что код потенциально опасного метода InternalExists() помечен как internal, делая его вызов для злоумышленного кода как минимум не элементарным, так еще и принудительное помещение его же в critical-слой делает вызов практически невозможным, если только осмотрительный пользователь / разработчик поместит потенциально зловредный код в песочницу и, таким образом, оформит его как прозрачный. Собственно, всем авторам библиотек кода остается только прислушаться к рекомендациям и взять описанный шаблон проектирования на вооружение. Кстати, давайте займемся реализацией библиотек по такому шаблону прямо сейчас.

Итак, на очереди очередной проект все того же демонстрационного решения. Точнее, группа из трех проектов:

Давайте посмотрим на структуру «главной» библиотеки, Project08_Core. Она состоит из двух публичных методов:

Какими атрибутами безопасности декорированы три упомянутых метода, можно видеть прямо в тексте файла Core.cs. Для удобства продублирую эти ключевые строки:

      public
      static
      void StringOut

т.е. данный метод не нуждается в усилении защиты, атрибут пропущен.

[SecuritySafeCritical]
publicstaticint SoundOut(string password_to_play)
...
[SecurityCritical]
privatestaticbool SoundOut_Internal()

Замечу также, что вся библиотека Project08_Core.dll пока не будет помечаться никаким атрибутом уровня сборки.

Наибольший интерес вызывает реализация «моста». Помните – его единственная, пожалуй, задача сводится к определению легитимности запроса от потенциального прозрачного (читай - опасного) кода к коду критического слоя. В данном случае миссия такого метода (а его имя, напомню, SoundOut()) – определить безвредность значения входного параметра и по этому критерию либо разрешить обращение к неуправляемой функции, либо запретить. Сам критерий мог бы быть чем-то вполне логичным и жизненным, вроде пути к MP3-файлу, или его размеру, или значению тэга этого файла, или комбинацией всего перечисленного… Но я использую критерий гораздо более простой: если в параметре передается верный «пароль» – пищать динамиком можно.

Далее наши рассуждения должны двигаться в таком направлении: наиболее типичный сценарий использования нашей «главной» библиотеки – каков он? Очевидно, примерно такой, какой и реализован в рассматриваемом наборе проектов, а именно:

Таким образом, прозрачный код библиотек с частичным доверием либо сможет выполнять опасные операции запрограммированные в Project08_Core.dll, либо нет. Это будет зависеть от настроек домена-песочницы, организуемого приложением. В контексте метода SoundOut() «узкое место» лишь одно – позволит домен вызов неуправляемого кода или не позволит? Очень и очень неплохой идеей будет проверить этот факт методом PermissionSet.Demand(). Который любезно сгенерирует исключение, если соответствующий набор разрешений не предоставлен. Как обычно, функциональность этого метода сводится к проходу всего стека вызова до границ домена и проверка того факта, что у всех вызывающих методов разрешения есть. Если песочница такового разрешения не предоставит, то взяться ему у «промежуточных» библиотек (B.dll/C.dll) просто неоткуда. Так вот, идея проконсультироваться с PermissionSet.Demand() не только очень здрава. Почти все SecuritySafeCritical методы реальных базовых библиотек платформы поступают именно так. Скажем, предпоследняя на рисунке 4 строка метода Directory.Exists() в действительности выглядит так:

      new FileIOPermission(FileIOPermissionAccess.Read, newstring[] { demandDir }, false, false).Demand();

Ну, думаю, идею все уловили. Если хотите, можете преобразовать метод SoundOut() под ту же логику, убрав комментарий со строки 23 файла Core.cs (только тогда и блок if, следующий непосредственно за ней, свою актуальность теряет, его лучше будет наоборот - закомментировать). Я же с целью демонстрации принял на вооружение такой не бесспорный, но технически реализуемый тезис: если вызывающий код знает пароль, передаваемый в единственном параметре метода SoundOut(), то ему можно обратиться к неуправляемой функции! Поэтому я пароль проверяю первой же строкой этого метода, и если он совпал – следует утверждение (метод PermissionSet.Assert()) полномочий на вызов неуправляемого кода. Далее следует вызов приватного метода SoundOut_Internal(), уже непосредственно общающегося с неуправляемым кодом, а потому помеченным атрибутом SecurityCritical. Вообще-то и этот вызов можно было бы включить в блок if, однако я с целью показа текста исключения при обращении к SoundOut() не уполномоченного на то кода оставил его за этим блоком. Также обратите внимание, как «мост» выполняет вторую важную задачу – фильтрацию значений, передаваемых из критического кода обратно в код прозрачный. В нашем случае критический метод SoundOut_Internal() возвращает true либо false. По причинам, которые не суть важны, я почему-то считаю опасным, если не доверенный код получит такие явные «намеки» на причину неудачного вызова. Потому-то я и подменяю их парой целых значений 0/-500, которые несут значительно меньше подсказок о внутреннем устройстве нашей полно доверенной библиотеки.

И, наконец, код «промежуточной» библиотеки Project08_Lib и приложения Project08 в комментариях практически не нуждается. Первый просто пытается обратиться к обоим публичным методам из состава «главной» библиотеки (заметьте, что в настоящий момент «пароль» в метод SoundOut() передается неверный, присутствует лишний пробел), а второй просто организует домен-песочницу и загружает в нее обе библиотеки. Причем сам домен имеет единственное разрешение – на исполнение управляемого кода. Также заметьте, что Project08_Lib загружается с частичным, а Project08_Core – с полным доверием. Возможно, для достижения последнего и в связи с позиционированием Project08_Core на уровне чуть ли не библиотеки ядра более наглядным был бы подход с помещением соответствующей сборки в GAC. Однако реализован иной подход, но тоже нам знакомый – сборка объявляется полно доверенной через последний параметр метода CreateDomain().

Что же, давайте протестируем, что у нас получилось. Запуск приложения в том состоянии как оно есть на текущий момент приводит к исключению:

      Unhandled Exception: System.MethodAccessException: Attempt by security transparent method 'Project08_Lib.Lib.DoPrint()' to access security critical method 'Project08_Core.Core.StringOut(System.String)' failed.
    

Ну – чему же тут удивляться? Ведь сейчас ВСЕ методы «главной» библиотеки «лежат» в каком слое? Правильно, в критическом. О чем и было рапортовано. Кстати – делаем очевидный, но от этого не менее важный вывод: сборка с частичным доверием может обратиться к сборке с полным доверием ТОЛЬКО если последняя помечена атрибутом APTCA ИЛИ атрибутом SecurityTransparentAttribute. Давайте опробуем первый подход. Удалим комментарий со строки 9 файла Core.cs. Запустим и получим следующее: «неопасный» метод StringOut() отработал, а опасный – нет. Новое исключение имеет вид:

      Unhandled Exception: System.Security.SecurityException: Request for the permission of type 'System.Security.Permissions.SecurityPermission, mscorlib, Version=4.0.0.0, Culture=neutral,
      PublicKeyToken=b77a5c561934e089' failed.
    

Тоже особо не удивляет. Пароль неправильный, стало быть, разрешение на вызов неуправляемого кода не утверждено. А попытка вызова SoundOut_Internal() все равно происходит. И где-то там внутри, когда дело доходит до Platform Invoke, платформа сама интересуется – а всем ли там на стеке это можно? Выясняется, как следует из последнего сообщения, что нет, не всем. Кстати, анализ того самого стека, идущего сразу за сообщением исключения, показывает, что даже если библиотека «промежуточная» через рефлектор или еще каким (полу-)хакерским способом узнает о существовании приватного метода SoundOut_Internal() и умудрится его вызвать (что почти невозможно в силу «разнослойности» вызывающего и вызываемого), то не поможет и это. Как мы видим из анализа стека – был вызван этот приватный метод. Был вызван даже метод внутри его, MessageBeep(). И все равно, и это не помогло. Т.е. хакеру сначала придется обойти проблему модификатора доступа к методу, затем проблему вызова критического кода из его кода, прозрачного, а затем еще проблему отсутствия полномочий на выполнение опасной операции.

Исправим «пароль» вызова метода SoundOut() на правильный, для чего в строке 22 файла Lib.cs уберем лишний пробел. Очередной запуск – полный успех, все работает. Platform Invoke по прежнему интересуется наличием разрешения на вызов неуправляемого кода у всех методов на стеке, однако «тормозит» не на границе домена (как это было в предыдущем запуске), а достигнув метода SoundOut(). Метод PermissionSet.Assert() прерывает обход стека на том методе, в котором он был исполнен, что и случилось при последнем запуске.

Хорошо, вернем в «пароль» лишний пробел (проверьте, что вновь появилось последнее из упомянутых исключений) и подумаем вот над чем: а что мешает коду «промежуточной» библиотеки, не знающей пароль для легитимного доступа к методу SoundOut(), утвердить то же самое разрешение? Что ж, попробуем. Убираем комментарий со строки 21 файла Lib.cs.

ПРЕДУПРЕЖДЕНИЕ

Мини-тест 4: снова предположите поведение приложения без его запуска.

Ну – наверно с этим тестом ни у кого проблем не возникло. Мы же знаем, что код из файла Lib.cs помещается в прозрачный слой, а это значит, что ему нельзя выполнять ряд операций. Первая же строчка указанного ряда (напомню, что он приводился в разделе «Transparent Code») дает нам ответ. Но давайте все же себя проверим. Ctrl+F5, и

      Unhandled Exception: System.InvalidOperationException: Cannot perform CAS Asserts in Security Transparent methods
    

Текст исключения идеально ложится на наше предположение, именно что «нельзя проводить утверждения (Asserts) в прозрачных методах».

Углубляем детализацию защиты. Доступ к отдельным методам в отдельных доменах

Как показала предыдущая часть статьи, у разработчика теперь есть выбор – позволить сборке с частичным доверием обращаться к сборке полно доверенной или предотвратить такое обращение вовсе. В первом случае он декорирует сборку с полным доверием атрибутом APTCA, во втором делать этого не станет.

Хорошо, что теперь насчет такой идеи: в принципе разрешить подобное обращение, получаемое через указанный атрибут, но только при условии, что обе сборки (и полно-, и частично- доверенная) находятся в определенном домене? Например, в домене, созданном именно нашим приложением. А если домен будет другим, то подобное обращение запретить. Т.е. нам нужно разделение на домены «опасные» (не доверенный код имеет, пусть и контролируемый, но все же, доступ к методам полностью доверенной сборки) и домены относительно безопасные, где к доверенным сборкам могут обращаться лишь такие же доверенные сборки. Также это может пригодиться, когда мы заранее знаем: чтобы вызов из не доверенной сборки к нашей библиотеке был успешным, домен должен быть настроен неким специальным образом. В доменах с настройками по умолчанию лучше такое обращение запретить. Идея, полагаю, ясна. Как с ее реализацией? Короткий ответ такой: не столь просто, как хотелось бы, но возможно. А к ответу развернутому мы переходим прямо сейчас.

Итак – очередная пара проектов Solution01\Project09(Project09_Lib). Тут приложение выступает одновременно как библиотека, содержащая «опасный» метод PowerMethod_Internal(). «Мостом» к нему для обращений из прозрачного слоя служит safe critical-метод PowerMethod(). Сборка Project09_Lib, как обычно, играет роль не доверенной библиотеки, жаждущей обратиться к функциональности библиотеки доверенной. Библиотека ссылается (reference) на приложение, а приложение уже не может поставить ссылку на библиотеку, т.к. в этом случае мы столкнемся с проблемой циклической зависимости. Поэтому для загрузки библиотеки в песочницу приложение использует немного рефлексии, что, впрочем, совершенно не отражается на изучаемом вопросе. Разумеется, в ту же песочницу приложение грузит и себя, но уже как полно доверенную сборку. Место, на котором стоит сфокусировать внимание – вызов из библиотечного метода PageInit() метода сборки с полным доверием PowerMethod(). Именно тут происходит то самое обращение, которое мы хотим то разрешать, то запрещать.

Чтобы достичь желаемого результата, нужно предпринять два действия. Во-первых, декорировать сборку с полным доверием (Project09.exe) уже знакомым нам атрибутом APTCA, но со специальным значением свойства этого атрибута по имени PartialTrustVisibilityLevel. Есть всего 3 варианта указания этого свойства:

Теперь действие второе, предпринимаемое как раз в тех доменах, которые хотят позволить такое «условное обращение». Такому домену требуется в свойстве PartialTrustVisibleAssemblies своих настроек (а они, напомню, консолидированы в классе AppDomainSetup) указать – какие из «условных» сборок, планируемых к загрузке в этот домен, будут принимать «входящие вызовы» от не доверенного кода. Свойство это принимает на вход массив обычных строк, но вот формат каждого элемента заслуживает, пожалуй, отдельного абзаца.

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

Все элементы идут строго друг за другом, лишние запятые, пробелы, переносы строк и прочая отсебятина не допускается. Формат записи получился несколько, кхм… неудобным (чтобы не сказать жестче), но уж тут что есть, то есть. Если кому-то требуется справка по вопросу «а где берется полный публичный ключ», то его можно узнать программно из строгого имени любой загруженной сборки. Скажем, если такая сборка текущая, то

      string pubKey = Assembly.GetExecutingAssembly().Evidence.GetHostEvidence<StrongName>().PublicKey.ToString();

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

Если сборку грузить не хочется, то к нашим услугам консольная утилита Sn.exe (Strong Name Tool) с ее опцией -Tp <assembly_name>. Скажем, если бы нам в голову пришла идея указать в этом массиве системную сборку System.Security.dll 3 раза, и мы бы узнали ее публичный ключ через Sn.exe, то можно было написать нечто вроде

      string[] assemblies=newstring[] {
"System.Security, PublicKey=002400000480000094000000060200000024000052534...",
"System.Security, PublicKey=002400000480000094000000060200000024000052534...",
"System.Security, PublicKey=002400000480000094000000060200000024000052534..."
};

Я, конечно, не стал перегружать и без того немалый текст статьи указанием полных ключей, а привел лишь их первые несколько байт. Но, повторяю, в реальном коде вы должны указать их полностью. Не менее понятно, что указывать отдельную сборку более одного раза смысла нет никакого, а сборку System.Security.dll нет смысла указывать в этом массиве и один раз – у нее нет условного атрибута APTCA (хотя и есть таковой безусловный). Так что все это не более чем демонстрация правильного формата записей этого массива.

После того как оба действия проведены, цель достигнута, обращения разрешены, но только в домене явно это затребовавшем или позволившем, как вам больше нравится.

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

OK, убираем комментарий в строке 8 файла Program.cs, делая наше приложение доступным для безусловного вызова со стороны не доверенных сборок. Теперь, по причинам также очевидным, все работает. Однако так доступ к приложению будет разрешен в любом домене.

Установка комментария на строку 8 и снятие его со строки 9 того же файла не меняет ровным счетом ничего. Как уже пояснялось, это просто две функционально эквивалентных записи безусловного APTCA-атрибута.

Комментирование предыдущей строки и снятие комментария со строки 10 ведет к более интересным результатам, все опять «ломается». Ведь теперь обращения к доверенной сборке разрешены только из доменов со специальной настройкой, а у нас таковых не наблюдается! Обращает на себя текст исключения. Он не только впрямую заявляет, что:

      Assembly 'Project09, Version=1.0.0.0,...' is a conditionally APTCA assembly which is not enabled in the current AppDomain.
    

но и прямо указывает, что нужно сделать для исправления ошибки:

      To enable this assembly to be used by partial trust or security transparent code, please add assembly name 'Project09, PublicKey=0024…' to the the PartialTrustVisibleAssemblies list when creating the AppDomain.
    

То есть достаточно скопировать весь текст в одинарных кавычках из фрагмента сообщения выше, и указать его в свойстве PartialTrustVisibleAssemblies – и готово! Можно сказать, что это третий (пусть и не слишком корректный) путь узнавания полного публичного ключа.

Нет ли здесь нарушения принципов безопасности? Не сообщается ли избыточная информация, которую сможет использовать злоумышленник с целью взлома системы? Это было бы верное замечание если бы целью обсуждаемого механизма было предоставление выбора сборке того домена, в котором она «согласится» поддерживать «входящие вызовы». Но цель иная. Не сборка фильтрует домены на годные для вызовов и не очень. Как раз таки домен определяет, кому из сборок с условным доступом он позволит принимать потенциально небезопасные вызовы. Так что если сообщение и помогает «хаку», то не сборки, а домена. А настройки последнего все равно всегда доступны для разработчика, запускающего приложение, ведь этот разработчик сам его и создает. Еще раз подчеркиваю – сборка с условным APTCA атрибутом не выбирает домен, в котором к ней будет возможно обращение из partially trusted-кода. Все зависит только от желания домена, а, стало быть, от приложения, «поднимающего» этот домен.

Хорошо, давайте последуем указаниям и внесем требуемый элемент в массив, только сделаем это более изящно, без копирования кучи байт. Убираем комментарий со строк 23-26 включительно все того же файла Program.cs. Нам нужно, чтобы домен разрешил обращение лишь к одной сборке с полным доверием – Project09.exe. Ее параметры и указываем. Запускаем – все вновь работает, цель достигнута.

Интересно отметить, что мы с вами только что написали ASP.NET. Ну не весь, конечно :), но концептуальный подход к безопасности кода этой платформы мы изобразили очень точно. Сопоставьте:

[assembly: AllowPartiallyTrustedCallers(PartialTrustVisibilityLevel=PartialTrustVisibilityLevel.NotVisibleByDefault)]
<partialTrustVisibleAssemblies> 
         <add assemblyName="System.Web" publicKey="00240000048000009400000006020...." />
</partialTrustVisibleAssemblies>

Что это означает? Да ровно то, что ASP.NET, как и мы только что, «поднимает» домен-песочницу, производит его настройку, также подобную нашей, грузит в него сборку с кодом нашей web-странички как частично доверенную, и System.Web.dll как сборку с полным доверием. После этого код странички может обращаться к функционалу платформы сконцентрированному в System.Web.dll. Но все это «прокатывает», если хостом такой песочницы является именно сама ASP.NET, производящая ее первоначальную настройку. Если хост сменить на другой (ClickOnce, Internet Explorer, ....) частично доверенный код уже не сможет пользоваться функционалом указанной библиотеки. Точно как и мы в коде Project09_Lib, загружаемого в домен с частичным доверием, не сможем, допустим, создать новый экземпляр класса System.Web.HttpCookie. Можете проверить это, убрав комментарий со строк 16-17 файла Lib.cs. Это потому, что наш хост (приложение Project09.exe) не делает необходимых настроек песочницы. Чтобы еще раз доказать, что в вопросе разрешения обращений из одного кода в другой от домена зависит все, а от сборки – ничего, произведем необходимые настройки. Убирайте комментарии со строк 28-33 включительно файла Program.cs и используйте всю доступную функциональность ASP.NET в консольном приложении и в домене с частичным доверием!

Заключение. И это все?

Да, все. Вы только что закончили чтение всеобъемлющей и совершенно полной монографии по вопросам безопасности кода на платформе .NET Framework версии 4.0. Ну, или по крайней мере, автору очень хотелось бы так сказать. :) Как и многим техническим писателям до него. На деле же платформа .NET Framework настолько огромна, что даже относительно мелкий вопрос, которым мы с вами занимались на протяжении данной статьи, требует для своего действительно всестороннего освещения десятки публикаций, массу заметок и еще сверх того пару полновесных книг. Так что приходится признать: то, что было нами изучено – не более чем «введение в тему». Правда, введение, осмелюсь полагать, весьма основательное. Базис, основы и концепции нового подхода к защите кода были разобраны весьма тщательно, с освещением одного и того же вопроса в разных аспектах, дабы вся эта довольно-таки разношерстная информация выстроилась в некую цельную картину. Есть все основания полагать, что с опорой на полученные знания дальнейшие изыскания в направлении безопасного кода пойдут куда как эффективнее, нежели при попытке начать их с нуля. А изыскивать, вы уж поверьте, есть что. Вот пара-другая идей насчет продолжения изысканий:

Мне же остается еще раз констатировать: в .NET Framework последней (на момент написания статьи) версии Code Access Security заметно похорошел. Став значительно проще для изучения и реализации, он, в тоже время, не сдал позиций в плане крепости обороны. Даже напротив, последняя стала более «эшелонированной» и трудной для обхода. Также сама идея безопасного вызова одного кода из другого приобрела более стройные и согласованные формы, что позволит проектировать систему защиты приложений с меньшими усилиями, и в тоже время, выбирая даже из большего множества стратегий, чем ранее. В общем, все идет к тому что герой нашего повествования перестанет быть этаким выставочным стендом, к которому водят высоких гостей, чтобы похвастать «отдельными достижениями», а превратится в ежедневный, обыденный и даже несколько заурядный инструмент, применяемый повсеместно – от проектов крошечных до решений многогигабайтного масштаба.

Я традиционно благодарю всех читателей за внимание и за будущие отзывы на данную статью, и надеюсь на новую встречу с ними.


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