Содержание статьи
Сегодня мы поговорим о том, о чем вспоминают обычно в последнюю очередь — о
безопасности твоих приложений. Ведь ты же не хочешь читать о том, что в творении
рук твоих — супернавороченной программе — нашли уязвимости, которые поставят под
угрозу работу каких-нибудь важных организаций? Ладно бы, если это простая фирма,
а если банк? Или атомная электростанция?
Введение
Увы и ах — технологии .NET прочно вошли в нашу жизнь, и на сегодняшний день
разработчики C# пользуются неслыханной популярностью на рынке труда. Легкий в
изучении и освоении язык дал программисту неслыханную свободу действий и при
этом позволил расширить круг тех лиц, которые стали гордо именовать себя
"программистами". Столь низкий "порог вхождения в специальность" обусловил тот
факт, что начинающие (и не очень) программисты не стали уделять должного
внимания безопасности своего кода. Но обо всем по порядку.
У общеязыковой исполняющей среды (common language runtime, CLR) в .NET
Framework есть своя модель безопасного выполнения кода, независимая от
ограничений операционной системы, в которой она работает. Более того, в отличие
от старой модели защиты на основе участников безопасности (principal-based
security), CLR реализует политику, исходя из того, откуда поступает код, а не из
того, кто являет ся его пользователем. Эта модель защиты по правам доступа кода
(code access security) имеет больший смысл в современных условиях, поскольку
немалая часть кода устанавливается через интернет, и даже доверенный
пользователь (trusted user) не знает, какой код действительно безопасен. Все это
реализовано в пространстве имен System.Security. "Ээээээ, так ты об этом..." —
разочарованно вздохнет читатель, который, наверняка, вдоль и поперек изучил все
те фичи, которые .NET предлагает программисту для реализации его злобных
замыслов. Спешу огорчить — о System.Security мы сегодня как раз разговаривать не
будем. Это скучно :). Вместо этого мы попробуем взглянуть на проблему
"безопасного кода" с другой стороны — с точки зрения того, в чьи хорошие (или не
очень) руки он попадет. Вне зависимости от того, что предоставляет интерфейс
твоей программы: просто складывает два числа или же управляет атомной
электростанцией.
Что может CLR?
Общеязыковая исполняющая среда (common language runtime, CLR) и Microsoft .NET
Framework предоставляют всем приложениям с управляемым кодом защиту на основе
признаков — это так называемая evidence-based security. В большинстве случаев
при написании кода обеспечивать защиту явным образом не требуется. Тем не менее,
я попытаюсь кратко рассмотреть вопросы безопасности, которые тебе, как
мегакрутому программисту, возможно, понадобится учитывать при написании кода, и
описать те принципы классификации компонентов, позволяющие определить, что нужно
предпринять для гарантированной защиты кода.
Для защиты управляемого кода используются две технологии:
- защита на основе признаков (evidence-based security) позволяет
определять, какие разрешения следует предоставлять коду; - защита по правам доступа кода (code access security) позволяет
проверять, весь ли код в стеке имеет необходимые разрешения на выполнение
каких-либо действий.
Эти две технологии связаны между собой концепцией разрешений.
По признакам и политике безопасности, устанавливаемой администратором,
система защиты определяет, какие разрешения могут быть выданы коду. Программа
сама может запрашивать какое-либо разрешение, влияя на состав окончательного
набора разрешений. Запрос разрешения выражается в виде объявления на уровне
сборки с синтаксисом пользовательских (custom) атрибутов. Однако, в любом
случае, код не может получить более широкие или ограниченные разрешения, чем это
предписано политикой безопасности. Разрешение предоставляется только раз и
определяет права всего кода в сборке. Для просмотра и изменения политики
безопасности используется инструмент настройки .NET Framework (Mscorcfg.msc).
Mscorcfg.msc
Планируем боевые действия
Есть такая японская пословица: "Выходи из дома так, как будто он окружен
тысячей врагов". Понятно, что во времена феодальной Японии, когда туда-сюда
бегали самураи, воевали между собой и искали других приключений, это пословица
была актуальной. Сегодня, позволю себе заметить, эта пословица будет
справедливой и для твоего кода — если ты думаешь, что твой код никому нафиг не
сдался, ты глубоко ошибаешься.
Если твой код — часть приложения, которая не вызывается другим кодом, то его
защита проста и специального программирования не требует. Но учти, что он может
быть вызван злонамеренным кодом. Хотя защита по правам доступа кода препятствует
доступу злонамеренного кода к ресурсам, он все равно способен считать значения
полей или свойств, которые, возможно, содержат ценную информацию.
Ihre ausweiss, bitte!: выдача разрешений
Защита на основе признаков базируется на предположении, что высокий уровень
доверия (с широкими полномочиями) присваивается лишь коду, заслуживающему этого
самого доверия, а злонамеренный код является "малодоверяемым" или вообще не
имеет разрешений. В соответствии с политикой по умолчанию в .NET Framework
разрешения выдаются на основе зон (так, как они определены в Microsoft Internet
Explorer). Ниже приведено упрощенное описание этой политики "по умолчанию":
- Зона локального компьютера (например, C:\app.exe) является
полностью доверяемой. Предполагается, что пользователи помещают на свой
компьютер только код, которому они доверяют, и что большинство пользователей
не собираются разбивать свой жесткий диск на области с разной степенью
доверия. По существу, этот код может делать все что угодно, поэтому от
злонамеренного кода, находящегося в этой зоне, никакой защиты нет. Честно
говоря, по моему скромному мнению, именно это предположение является одной
из огромных дыр в архитектуре безопасности Windows, что приводит к появлению
таких извратов, как UAC, DEP, рандомизация стека, сандбоксов и пр. - Зона интернета (например, http://www.microsoft.com). Коду из этой
зоны предоставляется очень ограниченный набор разрешений, который не опасно
предоставить даже злонамеренному коду. Обычно этому коду нельзя доверять,
поэтому его можно безопасно выполнять только с очень узкими разрешениями, с
которыми он не сумеет нанести ущерб: - WebPermission — доступ к серверу сайта, с которого получен код;
- FileDialogPermission — доступ только к файлам, специально
указанным пользователем; - IsolatedStorageFilePermission — постоянное хранилище,
ограниченное пределами веб-сайта; - UlPermission — возможность записи информации в окно
пользовательского интерфейса; - Зона интрасети (например \\UNC\share). Код из этой зоны
выполняется с чуть большими разрешениями, чем код из интернета, но среди них
все равно нет таких, которые предоставляли бы широкие полномочия; - FilelOPermission — доступ только для чтения к файлам каталога, из
которого загружен код; - DNSPermission — допускается разрешение DNS-имен в IP-адреса;
- FileDialogPermission — доступ только к файлам, специально
указанным пользователем; - Isolated StorageFilePermission — постоянное хранилище (с меньшими
ограничениями);
Продумай свои требования к защите и соответственно измени политику
безопасности. Правда, никакая конфигурация защиты не решит всех проблем:
политика безопасности по умолчанию, в общем-то, рассчитана на запрет
потенциально опасных операций.
В зависимости от способа развертывания, твой код может получать различные
разрешения. Перед выпуском кода в мир убедись, что ему предоставляются
разрешения, достаточные для нормальной работы. Продумывая защиту кода от атак,
посмотри, откуда может быть загружен атакующий код, и как он может получить
доступ к твоему коду.
Не влезай - убьет!
Какие разрешения несут в себе потенциальную опасность? Для выполнения
некоторых защищенных операций .NET Framework предоставляет разрешения,
потенциально позволяющие обойти систему защиты. Эти опасные разрешения следует
предоставлять только коду, заслуживающему доверия, и только при абсолютной
необходимости. Обычно, если злонамеренный код получает такие разрешения, то
защититься нельзя. К опасным разрешениям относятся:
[Security Permission]:
- Unmanaged Code — позволяет управляемому коду вызывать неуправляемый код,
что зачастую весьма опасно; - Skip Verification — код может делать что угодно без всякой верификации;
- ControlEvidence — управление признаками позволяет обмануть систему
защиты; - ControlPolicy — возможность изменять политику безопасности позволяет
отключить защиту; - SerializationFormatter — за счет сериализации можно обойти управление
доступом; - ControlPrincipal — возможность указывать текущего пользователя позволяет
обходить защиту на основе ролей; - ControlThread — возможность манипуляций с потоками опасна, так как с
ними связано состояние защиты;
[ReflectionPermission]:
- MemberAccess — позволяет отключать механизм управления доступом
(становится возможным использование закрытых членов).
Защита доступа к методам
Некоторые методы не стоит открывать для вызова произвольным недоверенным
кодом. Вызов такого метода связан с различными рисками: он может предоставлять
информацию, доступ к которой ограничен, принимать любую передаваемую ему
информацию, не проверять ошибки в параметрах или, получив некорректные
параметры, неправильно работать или даже нанести вред. Учитывай все эти случаи и
предпринимай меры для защиты таких методов.
Утилита графической настройки прав доступа к коду GuiCaspol
Иногда приходится ограничивать доступ к методам, которые не предназначены для
открытого использования, но все равно должны быть объявлены как открытые.
Например, у тебя есть некий интерфейс, вызываемый вашими же DLL, и поэтому он
должен быть открытым, но ты не хочешь, чтобы этот интерфейс был общедоступным,
так как нежелательно, чтобы клиенты могли с ним работать, или чтобы
злонамеренный код воспользовался им как точкой входа в твой компонент. Еще одна
типичная причина ограничения доступа к методу, который не предназначен для
общего использования (но, тем не менее, должен быть открытым) — стремление
избежать документирования и поддержки интерфейса, применяемого исключительно на
внутреннем уровне. Поэтому вот тебе несколько советов, как можно ограничить
доступ к методам в управляемом коде:
- Ограничь область доступности классом, сборкой или производными классами
(если им можно доверять). Это простейший способ ограничения доступа к
методу. Замечу, что вообще-то производные классы могут быть менее
доверяемыми, чем класс-предок, но в некоторых случаях они используют ту же
идентификацию, что и надкласс. В частности, ключевое слово protected не
подразумевает доверия, и его необязательно нужно использовать в контексте
защиты; - Разрешай вызов метода только вызывающим с определенной идентификацией
(обладающим заданными вами признаками); - Разрешай вызов метода только тем, у кого есть требуемые разрешения.
Аналогичным образом декларативная защита позволяет контролировать
наследование классов. С помощью InheritanceDemand можно потребовать наличия
определенной идентификации или разрешения от:
- Всех производных классов;
- Производных классов, переопределяющих те или иные методы.
Защищаем доступ к методу или классу
Следующий пример показывает, как обезопасить открытый метод, ограничив доступ
к нему.
1. Команда sn -k создает пару из закрытого и открытого ключа. Закрытая часть
нужна, чтобы подписать код строгим именем (strong name), и хранится в безопасном
месте издателем кода. Если она станет известной, указать твою подпись в своем
коде сможет кто угодно.
sn -k keypair.dat
csc/r:App1.dll /a. keyfile: keypair.dat App1.cs
sn -p keypair.dat public.dat
sn -tp public.dat >publichex.txt
[StrongNameldentityPermissionAttribute ( SecurityAction . LinkDemand , PublicKey="...hex...",Name="App1",
Version="0. 0.0.0")]
public class MyClass
2. Команда esc компилирует и подписывает Appl, предоставляя ему доступ к
защищенному методу.
3. Следующие две команды sn извлекают из пары открытый ключ и преобразуют его
в шестнадцатеричную форму.
4. Во второй половине показанного исходного кода содержится фрагмент
защищаемого метода. Пользовательский атрибут (custom attribute) определяет
строгое имя и в шестнадцатеричном формате вставляет открытый ключ, полученный
командой sn, в атрибут PublicKey.
5. В период выполнения Appl имеет необходимую подпись со строгим именем и
может использовать MyClass. В этом примере для защиты API-элемента применяется
атрибут LinkDemand.
В примере, показанном ниже, частично доверенному коду запрещается обращаться
к классам и методам (а также к свойствам и событиям). Когда такие объявления
применяются к классу, защищаются все методы, свойства и события этого класса.
Однако декларативная защита не влияет на доступ к полям. Кроме того, учти, что
требования к связи (link demands) защищают только от непосредственно вызывающего
кода — возможность атак с подменой сохраняется.
[System.Security.Permissions. PermissionSetAttribute(System.Security.
Permissions.SecurityAction.InheritanceDemand, Name="FullTrust")]
[System.Security.Permissions. PermissionSetAttribute(System.Security.
Permissions.SecurityAction.LinkDemand, Name="FullTrust")]
public class YourClass{...}
Утилита ручной настройки прав доступа к коду
Приемы безопасного кодинга
Запрос разрешений — отличный способ обеспечить поддержку защиты в
разрабатываемом коде. Он позволяет запрашивать минимальные разрешения,
необходимые для выполнения кода, и гарантировать, что код не получит разрешений
больше, чем нужно. Например:
[assemblyiFilelOPermissionAttribute (SecurityAction.RequestMinimum,
Wrlte="C:\\test.tmp")]
[assembly:РеrmissionSet(SecurityAction.RequestOptional. Unrestricted=false)]
... SecurityAction.RequestRefused ...
В этом примере системе сообщается, что код не должен запускаться, пока не
получит разрешение на запись в C:\test.tmp. Если одна из политик безопасности не
предоставляет такое разрешение, генерируется исключение PolicyException, и код
не запускается. Ты должен убедиться в том, что коду выдается нужное разрешение,
и тогда тебе не придется беспокоиться об ошибках из-за нехватки разрешений.
Кроме того, здесь система уведомляется о том, что дополнительные разрешения
нежелательны. Иначе код получит все разрешения, предусмотренные политикой
безопасности. Лишние разрешения не принесут вреда, но, если в системе
безопасности есть какая-то ошибка, уменьшение числа разрешений, выдаваемых коду,
может прикрыть брешь в защите. Таким образом, если код обладает разрешениями,
которые ему не нужны, возможны проблемы с безопасностью. Еще один способ
ограничить количество привилегий, предоставляемых коду — явно перечислить
разрешения, от которых следует отказаться. Отказ от разрешений осуществляется
объявлением необязательности разрешений и исключением конкретных разрешений из
запроса.
Заключение
Уфф! На тему обеспечения безопасности твоего кода можно говорить бесконечно.
В рамках этой статьи я постарался упомянуть только самое важное и, на мой
взгляд, интересное. Иными словами, то, что должно помочь тебе сделать свои
приложения непробиваемыми. В общем, да пребудет с тобой Сила!
WWW
blogs.msdn.com,
www.eggheadcafe.com,
bytes.com — на этих
сайтах ты сможешь найти россыпи интересной информации о технологиях .NET и популярных
языках.