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

image

Меня зовут Сергей Талантов, я — архитектор и Security Champion в команде KasperskyOS «Лаборатории Касперского» и занимаюсь разработкой продуктов, к которым предъявляются самые жесткие требования в плане безопасности. В этой статье расскажу про подход Secure by design: от теории (что это такое и какие виды этого подхода существуют, а также как и почему мы его применяем) к практике (паттерны безопасного дизайна и примеры их использования на С++).

Операционная и конструктивная безопасность ПО


Сперва немного теории, чтобы унифицировать термины, которые будут использоваться в статье.

Уязвимость (vulnerability) — недостаток в системе, используя который можно намеренно нарушить ее работоспособность и вызвать неправильную работу.

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

Эксплойт (exploit) — вредоносный код или приложение, которые используют уязвимости в ПО.

Эксплуатация уязвимостей происходит через так называемую поверхность атаки.

Поверхность атаки (attack surface) — общее количество возможных уязвимых мест в ПО.

Если уязвимостей много, принято говорить, что поверхность атаки большая. Такая ситуация наталкивает на мысль, что кодовая база в принципе большая и/или код дырявый.

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

Операционная безопасность ПО


Классический цикл работы над релизом можно изобразить следующим образом:

image

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

Дальше обязательно выполняется ИБ-ревью для обеспечения безопасности разработки и безопасности продукта на выходе: выявление уязвимостей ПО, проверка явного нарушения целостности, доступности и конфиденциальности и так далее. Все эти действия выполняются явно, через код-ревью и тесты. На выходе мы получаем некоторое количество багов с особым приоритетом и которые, соответственно, обязательны для устранения.

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

После ревью безопасников мы выходим на пентесты — так называемые тесты на проникновение (penetration tests), которые проводятся с имитацией реального взлома системы. На этом этапе история с переделыванием архитектуры может повториться. В этом заключаются очевидные минусы, которые поможет устранить подход Secure by design.

Конструктивная безопасность, или Secure by design


C Secure by design тот самый комплекс мер из предыдущего подхода проводить не нужно — мы неявно выполняем эти требования за счет использования правильного дизайна и архитектуры системы. Дальнейший разговор будет как раз о том, что значит «неявно».

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

Цикл разработки немного меняется.

image

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

Secure by design как подход


Понятие Secure by design довольно широкое. Wikipedia дает такое короткое определение:

Secure by design — проектирование с учетом заложенной безопасности.

В данном случае краткость немного вводит в заблуждение, поскольку в реальности подходов к проблеме безопасного дизайна существует огромное количество. Я попытался их систематизировать, чтобы дать комплексное представление об этой области. За основу взял классификацию «Подходы к проектированию систем» (The Art of Systems Architecting, Mark W.Maier), которая выделяет всего четыре подхода.
  • Эвристический. Проектируем снизу вверх, используем лучшие практики, рекомендации, эвристики и с их помощью выстраиваем архитектуру системы. Этот вариант не сильно формализован и не очень удобен для применения.
  • Методический. Это более формальный подход. Архитектуру проектируем сверху вниз. Выбираем модели и шаблоны и, начиная с них, спускаемся по реализации вниз. В этой статье мы в большей степени будем говорить про этот подход.
  • Нормативный. Нам он не очень интересен, поскольку заключается в том, что мы просто выполняем требования регулятора. Подход имеет довольно узкую сферу применения, поэтому о нем подробнее говорить не будем.
  • Кооперационный. Этот вариант немного интереснее. Он подразумевает коллаборационные действия разных сторон для определения лучшего решения. Но сегодня мы его также не рассматриваем.

Ортогонально этой классификации есть еще одна — по уровням представления системы. Их существует много, но в этой статье достаточно поговорить о двух:
  • Архитектурный уровень — затрагивает взаимодействие высокоуровневых сущностей (высокоуровневое проектирование).
  • Уровень реализации — затрагивает отдельные аспекты реализации (низкоуровневое проектирование — код).

Скрещивая эти две классификации, мы получаем матрицу, с помощью которой можно систематизировать варианты подхода Secure by design.

image

Далее рассмотрим примеры подходов Secure by design.

MILS — Multiple Independent Levels of Safety and Security


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

image

MILS — это высоконадежный архитектурный подход к безопасности на основе концепции разделения и управления потоками данных (https://zenodo.org/record/45164).

image

Эта сложная схема отражает простую концепцию. У нас есть некие разделенные контейнеры — MILS partitions — в которых выполняется код бизнес-логики (application). Помимо этого кода, в них находится middleware — сервисы для взаимодействия друг с другом или с ядром системы.

Ядро (Separation kernel) — важный элемент платформы, который, помимо общих для ядер ОС действий по скидулингу ресурсов и памяти, выполняет разделение контейнеров. На схеме это отмечено с помощью separation barrier. При этом разделение происходит как на аппаратном, так и на программном уровне. В первом случае оно выполняется процессором (аппаратной частью платформы с поддержкой привилегий, MMU, IOMMU и так далее), во втором — за счет контроля взаимодействия между контейнерами с помощью заданных политик безопасности.

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

Cyber Immunity


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

image

Эта методология разработана нами в Kaspersky.

image

Схема работы похожа на то, что мы видели в MILS. Приложения разделены, они общаются через какие-то каналы связи — через микроядро и модуль безопасности, которые в MILS назывались separation kernel.

Cyber Immunity добавляет массу готовых решений. В частности, это наша собственная микроядерная операционная система KasperskyOS с готовыми механизмами политик, IPC и всем, что необходимо для взаимодействия приложений друг с другом, — паттернами безопасности, готовыми примерами реализации и тому подобное. Фактически бандл содержит все, что нужно для разработки на MILS-подобной архитектуре.

Кибериммунная система — это решение, которое остается безопасным даже при компрометации части своих компонент.

SDL


Следующий пример подхода Secure by design — SDL (security development lifecycle, https://www.microsoft.com/en-us/securityengineering/sdl/), разработанный в Microsoft. Он довольно всеобъемлющий, поскольку покрывает все уровни — от архитектурного до реализации. А кроме того, он покрывает все стадии разработки.

image

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



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

SDL не ограничивается именно проектированием. Подход актуален на всех стадиях и на всем цикле разработки. Эта методология тяжелая, дорогая и доступная большим проектам и компаниям.

Принципы безопасности


Есть варианты проще. Самый простой способ пойти по пути Secure by design — использовать принципы безопасности. В соответствии с нашей классификацией это эвристический подход, который актуален на всех уровнях — от архитектурного до реализации.

image

В компьютерной безопасности выделяют восемь основных эвристик. Они были сформулированы почти 50 лет назад Зельцером и Шредером, но до сих пор не утратили актуальность. Полный список принципов можно найти в оригинальной статье. Здесь же мы рассмотрим только некоторые из них.
  1. Принцип простоты (Economy of mechanism). Чем проще, тем безопаснее.
  2. Принцип безопасных умолчаний (Fail-safe defaults). По умолчанию наша система должна быть безопасной. Все разрешения должны быть явные. Одним из вариантов этого принципа является default deny, когда мы по умолчанию запрещаем все взаимодействия, разрешая что-то только явно.
  3. Принцип полноты перекрытия (complete mediation). Мы должны контролировать все взаимодействия в системе. Не должно быть темных углов, бэкдоров и так далее.
  4. Принцип открытого дизайна (open design). Архитектура не должна быть секретной. Вариация этого принципа в криптографии — алгоритм шифрования не должен быть секретным.

В чистом виде (в формате методологии) эти принципы не формализованы и применяются редко. Чаще их используют просто в виде правил. Определенную степень формализации обеспечивает следующий подход — паттерны безопасности.

Паттерны безопасности


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

image

В этой статье я буду использовать его для демонстрации подхода Secure by design на практике.

Secure by design на практике


Для демонстрации я подобрал утрированный упрощенный пример, который к жизни и продакшену совершенно неприменим. Но для демонстрации Secure by design вполне подойдет.

image

Здесь разработчик создал простой класс, представляющий аутентифицированного пользователя в системе (сама аутентификация пользователя приходит с фронта — скорее всего это web UI). В конструкторе на бэкенде мы получаем внешние данные — имя пользователя и пароль — и пытаемся провести аутентификацию через запрос в базу данных. Если аутентификация не прошла, кидаем исключение и объект не создается. Если аутентификация успешна, созданный объект сохраняется в контексте системы, откуда его можно периодически запрашивать. В частности, у объекта можно запрашивать атрибуты, например имя пользователя, чтобы отобразить его в web UI в личном кабинете.

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

image

Первая уязвимость заключается в том, что с пользовательского ввода мы можем получить не только логин, но и JS-ный скрипт. Мы сохраним его в базе данных, потом отобразим на UI и получим странное поведение. Это один из частных случаев XSS — cross site scripting.

Вторая уязвимость, которую мы здесь можем потенциально получить, это SQL Injection. Она тоже связана с пользовательским вводом. В имени пользователя могут содержаться фрагменты SQL-скрипта, из-за которого при выполнении запроса к базе данных мы можем ее повредить.

Напомню, что я использовал утрированный пример. В реальных системах, как правило, есть многочисленные проверки на Injection. Но как потенциальную проблему ее нельзя не отметить.

Третья проблема — утечка пароля, который мы храним в классе (и, судя по всему, в открытом виде). Объект аутентифицированного пользователя хранится в контексте достаточно долгое время, то есть пароль можно из памяти считать.

Четвертая уязвимость не столь явная. Она заключается в том, что использование пароля для аутентификации в принципе не самый безопасный механизм. В серьезных системах он сейчас используется только как часть двухфакторной аутентификации. Да и в целом класс User здесь имеет большую ответственность, которая на него возлагаться не должна. Но, повторюсь, к продакшену этот код не имеет никакого отношения. Это лишь пример для данной статьи. Так что эти уязвимости в дальнейшем обсуждении трогать не будем.

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

image

Здесь разработчик добавил валидацию имени пользователя и пароля на присутствие специальных символов, которые вводятся в JS- или SQL-скриптах, отказался от хранения пароля в классе и добавил чистку памяти после использования пароля при аутентификации. Такой код уже пройдет ревью безопасности.

Однако если бы мы сразу воспользовались правильными паттернами безопасности (подходом Secure by design), мы сделали бы код безопаснее, даже не выявляя эти уязвимости.

Применение паттерна read-once object


Возьмем первый из паттернов — read-once object или одноразовый объект (другие названия — clear after use, clear sensitive information). Названий у паттерна много, но суть одна — он решает проблему хранения в памяти чувствительных данных — паролей, ключей и тому подобного. Согласно этому паттерну, все, что хранится в памяти и может быть украдено, должно удаляться как можно быстрее, в идеале сразу после использования.

Реализуем очистку пароля.

Подход в лоб — SecureClearMemory


Подход в лоб подразумевает явный вызов функции SecureClearMemory после использования пароля.

image

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

Реализация RAII


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

image

В этом коде мы вместе с паролем создаем гвард, помещаем туда функцию очистки. Гвард срабатывает при выходе из scope — память очищается.

Этим подходом мы решаем проблему возможного возникновения исключения. Однако минус в том, что гвард нужно создавать явно. Когда пароль куда-либо передается, надо не забыть передать его вместе с гвардом — нет полной инкапсуляции. Идеально было бы найти способ создать общий объект, который объединит все вместе.

Аллокатор


Существует элегантный способ решения этой проблемы через кастомный аллокатор. Вся кастомизация этого аллокатора состоит в том, что в функции деаллокации на одиннадцатой строке мы явно вызываем очистку памяти. А на шестнадцатой строке создаем собственный тип этого вектора с кастомным аллокатором — secure::data.

image

В secure::data вполне инкапсулированный объект с данными и функцией очистки. Тем самым мы решаем и проблему исключения, и проблему инкапсуляции.

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

Чтобы этого избежать, нужно воспользоваться одним из способов безопасной очистки памяти:
  • В Windows: SecureZeroMemory.
  • В Linux и BSD: explicit_bzero, explicit_memset — они не стандартизированы, в разных дистрибутивах могут как присутствовать, так и отсутствовать. Нужно проверять.
  • В стандарте C11: memset_s (_s означает secure).

Если ничего из этого списка не подходит, можно применить собственную реализацию с использованием volatile:

image

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

В целом паттерн read-once object можно реализовать по-разному. Здесь я привел только самые основные варианты реализации.

Применение паттерна value object


Следующий паттерн, который можно использовать в нашем примере, называется Value object или доменный примитив (domain primitive). Паттерн решает проблему передачи параметров с использованием примитивных типов. Такая передача ведет к ошибкам в логике и выходам за пределы возможных значений. Чтобы эти пределы проверялись и действительно ограничивали доступные значения, мы вводим доменные примитивы и в них инкапсулируем все необходимые проверки.

В примере мы передавали username в виде std::string — обычного примитивного типа. Применяя паттерн, переделываем его в класс UserName, в котором в конструкторе создаем string, там же выполняем валидацию. Причем здесь мы валидируем именно доменные правила (здесь под доменными правилами понимаются заданные нами ограничения, инкапсулированные в value object, например, что username должен состоять ровно из 10 символов, содержать только большие и малые латинские буквы, не содержать специальных символов). Таким образом мы избавляемся от многих проблем, связанных с контролем граничных значений.

image

Применение паттерна входной валидации


Еще один паттерн — валидация, входная валидация или порядок валидации (его тоже называют по-разному, в том числе input validation, validation order). Как известно, весь внешний ввод данных чреват ошибками. Все, что приходит извне, может содержать злонамеренные данные. Паттерн обеспечивает валидацию всех внешних данных.

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

Порядок валидации имеет значение — валидация должна происходить от простых проверок к сложным (https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html):
  1. Проверка источника данных — доверенные источники не требуют валидации.
  2. Проверка размера. Обычно это простая проверка, которая не требует CPU или других ресурсов. Мы сразу отсекаем массу невалидных данных.
  3. Лексическая проверка (проверка набора доступных символов и значений по белому списку). Эта проверка чуть сложнее — мы проверяем набор доступных символов. Важный момент заключается в том, что выполняется проверка по белому списку (то есть проверяем только то, что мы явно разрешили). Это согласуется с принципом, что система должна быть по дефолту безопасной.
  4. Синтаксическая проверка (проверка формата данных, например, JSON, XML…).
  5. Семантическая проверка (проверка содержимого). Напоследок делаем самую сложную проверку. Проверяем, что эти данные в принципе нас устраивают по своей сути. Например, если это email, то проверяем его наличие в базе. Можем даже проверить валидность email — послать на него какое-то сообщение и дождаться ответа.

Теперь попробуем все эти паттерны применить к исходному примеру.

image

Здесь мы использовали паттерн value objects и добавили соответствующий объект — пароль и имя пользователя. Добавили валидацию в правильной последовательности (тоже в соответствии с паттерном). По паттерну read-once object храним пароль только в secure-контейнере и гарантируем его удаление сразу после использования.

Применяя паттерны, мы добились того, что все уязвимости, о которых говорили ранее (XSS, SQL injection, утечка пароля), пофиксились. При этом мы могли понятия не иметь, что такие уязвимости существуют и эксплуатируются. Мы просто использовали паттерны правильного дизайна — создания и хранения объектов, валидации — и система в целом получилась безопасной. В этом и заключается суть подхода Secure by design.

Данный пример демонстрирует работу подхода на уровне микродизайна. Но точно так же он работает и на уровне архитектуры.

Итоги: плюсы и минусы


У подхода Secure by design есть несомненные плюсы, и их довольно много:
  • Применение подхода Secure by design неявно вносит в систему атрибуты безопасности. Они вносятся в систему на этапе проектирования с теми самыми паттернами или подходом или методологией, которую мы выбираем в качестве основной.
  • Применение данного подхода не требует от разработчиков экспертных знаний в области безопасности. Это следствие из предыдущего правила. Никто не должен мониторить CVE-шки, участвовать в соревнованиях по взлому систем и тому подобное. Требуется просто представление о той методологии, которую мы хотим использовать, и иногда ревью корректности использования этой методологии.
  • Цикл разработки сокращается за счет уменьшения изменений, вносимых в систему на поздних этапах тестирования, приемки и эксплуатации. Скорее всего на этих этапах мы уже не получим серьезных недочетов, которые потребуют переработки всей архитектуры. Максимум усилий мы вложим на этапе проектирования, а по итогу получим продукт, который в целом будет иметь минимальное количество уязвимостей, и их будет легко починить.
  • Продукт становится устойчив как к известным уязвимостям, так и к потенциальным, еще не выявленным. Скорее всего новыми методиками взлома, которыми можно будет воспользоваться на других продуктах, на проектах Secure by design воспользоваться не получится (а если и получится, то с минимальными потерями для продукта).

Но к сожалению, не все так радужно. В этом подходе есть и отрицательные моменты, их тоже немало.
  • Существует много разновидностей Secure by design, отличающихся степенью формальности и стоимостью внедрения. Стоит выбирать подходящий, в зависимости от целей проекта. Выбор при этом нужно делать осознанно. Возможно, для кого-то этот момент будет не отрицательным, а положительным. Наличие выбора означает возможность внедрять подход для проекта любой стоимости и уровня сложности.
  • Системное внедрение методологии Secure by design (любой ее вариации) стоит дорого (а несистемное внедрение не даст масштабных преимуществ). Нужно понимать, что эта дороговизна на начальных этапах окупится последующими и в целом скорее всего будет выигрыш. Но серебряных пуль никто не обещает — подход не гарантирует абсолютно безопасного решения, лишь повышает шансы.
  • Релизные процедуры (тестирование, ревью, пентесты) все равно придется выполнять, несмотря на серьезные вложения в процессы, соблюдение методологии и тому подобное. Эти процедуры иногда занимают большую часть проекта, но от них никуда не деться. Хорошая новость здесь в том, что фидбэк от этих процедур скорее всего будет минимальный.

Если вам будет интересно попробовать этот подход на практике (наряду с самыми последними достижениями в сфере безопасности), приходите в «Лабораторию Касперского» — в команды C++-разработки. Процесс найма у нас максимально упрощен — так что уже через пару дней сможете увидеть, как подход реализован в реальном коде ОС :)

А здесь можно проверить свои знания C++ в нашей игре про умный город.

Список литературы


Под конец я подготовил список литературы. Особенно рекомендую первую книжку списка — Secure by design. В ней есть хорошие вводные по этой теме. Она раскрывает далеко не все аспекты данного подхода, но дает хорошо его прочувствовать на примере реальных историй из жизни.
  1. Secure by design, Daniel Deogun, Dan Bergh Johnsson, Daniel Sawano
  2. Technical Guide Security Design Patterns by Bob Blakley, Craig Heath, and members of The Open Group Security Forum The Open Group April 2004 pubs.opengroup.org/onlinepubs/9299969899/toc.pdf
  3. The Art of Systems Architecting, Mark W.Maier
  4. Input Validation Cheat Sheet, https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html
  5. Microsoft SDL https://www.microsoft.com/en-us/securityengineering/sdl/
  6. KasperskyOS, https://os.kaspersky.ru/
  7. MILS Architecture, https://zenodo.org/record/45164
  8. Designing Secure Software: A Guide for Developers, Loren Kohnfelder
  9. BASIC PRINCIPLES OF INFORMATION PROTECTION, web.mit.edu/Saltzer/www/publications/protection/Basic.html

Комментарии (1)


  1. dmitrykaplun
    06.04.2023 13:05

    И всё равно без ревью никуда, особенно лексическая проверка.