Дао безопасности

из серии «не надо думать как хакер»


/media/images/yin_yan.svg

Из жизни: ОБЫЧНО в одной персоне не сочетаются созидатель и разрушитель. Это разный образ мыслей.

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

—Николай Григорьевич Левицкий, Советский Криптограф


Хотите разработать безопасную программу — пригласите хакера.

Согласны с этим утверждением?

«Да! Конечно, да!» — скажет подавляющее большинство читающих эти строки — «Эти парни умеют взламывать программы, и, следовательно, лучше всех знают, как их надо защищать. Именно хакеров надо приглашать, когда надо модифицировать процесс разработки с учетом требований безопасности; именно хакеров надо приглашать, когда надо научить программистов создавать безопасные приложения».

«Нет! Ни в коем случае!» — скажу я. Хакеры не могут создать безопасную программу. Да, возможно, они смогут взломать ее, но защитить…

Об опыте автора можно почитать на специальной странице .

На основании своего опыта берусь утверждать:

Я предлагаю прочитать статью следующим читателям:


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

Статья состоит из нескольких вводных разделов, четырех основных частей и приложения.

Вводные разделы коротки и их стоит почитать. Не буду на них здесь останавливаться.

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

Во второй части я описываю особенности разработки безопасных программ. Эта часть называется «Как обеспечить безопасность», в ней я показываю изменения, которые необходимо внести в продукт и в процесс его разработки, если к программе предъявляются требования безопасности.

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

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

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

Статьи серии

Заявленная тема сложна. Подробно обсудить ее в одной статье, даже очень большой, у меня не получится. Поэтому я начал публиковать целую серию, посвященную этой теме.

Существуют фундаментальные причины, почему хакинг не работает. Но об этом в отдельной статье: сейчас меня больше интересует не вопрос «почему?», а вопрос «что делать?».

Я уже опубликовал первую статью на SecurityLab.ru. Это была мотивационная статья, статья в которой идея демонстрируется как можно более ярко, эмоционально.

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

Пока планируется, что серия будет состоять из трех статей.

  1. Не надо думать как хакер (опубликована на SecurityLab.ru)
  2. Дао безопасности (эта статья)
  3. Почему не работает хакинг (находится в разработке)

Сколько и какие статьи реально будут составлять серию, покажет будущее.

Хакинг провалился

/media/images2/Pastafariano.jpg

Фотография поклонника культа летающего макаронного монстра. Зачем здесь эта картинка? Не знаю, просто понравилось ее настроение. Изображение взято из виккипедии

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

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

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

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

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

Казалось бы, всё подтверждает этот подход. Действительно, ошибки находятся, исправляются…

Но их не становится меньше. Разработчики исправляют уязвимости, хакеры анализируют программу, находят новые уязвимости. Причем хорошо еще, если это «свои» хакеры, но ведь уязвимости находят и сторонние «специалисты».

У меня есть основания верить, российские хакеры — одни из самых квалифицированных, если не самые квалифицированные в мире.

В чем проблема? Ответ, лежащий на поверхности: уязвимостей много и хакеры просто не справляются с обнаружением их всех («у нас нет столько „специалистов по безопасности“»). Решение проблемы так же лежит на поверхности: надо расширять круг вовлеченных.

Наступает второй этап «хакерства».

Хакеры говорят: пусть ошибки ищут сами программисты. Действительно, они как никто знают устройство своих программ; это хакерам надо изучать программу, тратить свое драгоценное время (ведь хакер — высококвалифицированный, высокооплачиваемый специалист, не то, что какой-то там необразованный программистишка); а так все будет гораздо быстрее, эффективнее. Значит, надо чтобы мы, Хакеры научили программистов искать ошибки в их программах, надо модифицировать процесс разработки, ввести соответствующие метрики, надо давить на программистов, чтобы они делали эту работу.

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

Но уязвимости все же остаются. Процесс есть — результата нет.

Наступает следующий этап. Все мы сейчас являемся свидетелями его наступления; думаю, вы сами уже догадались о чем я.

Хакеры вовлекают сторонних специалистов. Да-да, я сейчас говорю о программе «баунти» — программе, когда компании платят сторонним специалистам за поиск уязвимостей в их продуктах.

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

Решение найдено?

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

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

Что пишут хакеры? Они пишут: «Ура!!! Мы продаем SDL!!!», еще смешнее, когда хакеры начинают продавать «SDLC» (лучшие из них уже некоторое время продают «SSDLC», что, если задуматься, тоже не слишком серьезно).

Что они пишут о результатах? Хоть кто-то из хакеров написал: «мы научили разработчиков делать безопасные программы»? Нет, мне даже довелось читать статью, в которой хакеры жалуются: вот, мы научили клиентов способам обеспечения качества программы, они попробовали, но у них ничего не получилось.

Хакеры — идиоты?

Нет, проблема не в них. Среди хакеров есть много очень квалифицированных специалистов, иногда очень-очень-очень квалифицированных. Как Специалисты все они достойны огромного уважения, и я готов снять перед многими из них шляпу. Более того, у меня есть основания верить, российские хакеры — одни из самых квалифицированных, если не самые квалифицированные в мире.

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

Но каковы цели хакеров? Ответ на этот вопрос очевиден, и все читающие эти строки могут его дать: хакеры ищут ошибки.

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

Разница видна?

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

Нет, разница целей здесь очень значима. Можно показать, что ища и исправляя уязвимости, мы никогда не сможем сделать безопасную программу: существуют фундаментальные причины, почему хакинг не работает. Если совсем коротко, то все упирается в теорему, утверждающую: не существует алгоритма, который бы для произвольной программы определил, существует ли в этой программе уязвимость (см., например, [Engle2008]). Почему это простое утверждение так сильно влияет на процесс разработки, надо писать в отдельной статье; а сейчас меня больше интересует не вопрос «почему?», а вопрос «что делать?».

И эти проблемы не уникальны для безопасности программ. Уже более 40 лет, примерно с 70-х годов прошлого столетия хорошо известно, что подход Code-and-Fix в программировании не работает — исправление одной ошибки приводит к появлению нескольких других.

Значит ли это что мы обречены использовать «дырявый» софт?

Хорошо забытое решение?

Code-and-Fix — самый первый и самый примитивный метод разработки программного обеспечения. Сейчас им пользуется только крайне низкоквалифицированные группы, или когда необходимо быстро, «на коленках» «сделать костыль», и забыть про эту разработку. Но по сути, именно эту методику предлагают хакеры для обеспечения безопасности создаваемых программных продуктов; поэтому неудивительно, что у них ничего не получается.

Но есть и другие решения.

За многие годы мы научились создавать программы. Мы научились создавать программы со свойствами, которые необходимы клиенту, например:

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

И это только часть успешно решаемых задач.

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

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

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

Более того, разработчики и тестировщики — сильно разные люди. Замечено, специалисты редко меняют свое амплуа, оставаясь либо разработчиками, либо тестировщиками; хотя, конечно, исключения есть везде.

Самое интересное, разделение ролей — не новость и для безопасности.

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

Я получил много интересных отзывов.

Но самое интересное письмо я получил от Николая Григорьевича Левицкого. Его отзыв особенно ценен для меня тем, что, по моему мнению, Николая Григорьевич — один из лучших российских специалистов в области безопасности. Он криптограф с более чем 30-летним опытом, сейчас уже более 15 лет занимается проверкой соответствия программного обеспечения требованиям безопасности.

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

Из жизни: ОБЫЧНО в одной персоне не сочетаются созидатель и разрушитель. Это разный образ мыслей.

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

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

Как разработать программу

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

Программный продукт — результат применения иерархии шаблонных решений.

Единой теории разработки программного обеспечения пока на существует ([Johnson2012]). Есть множество частных теорий, как общих для всех когнитивных процессов, так и специализированных, ориентированных на описание мышления именно разработчиков; с некоторыми из этих теорий вы можете познакомиться, например в неплохой статье Поля Ральфа ([Ralph2013]) (стоит только обратить внимание, Ральф продвигает свою теорию, поэтому статья не совсем объективна).

Часто мышление программиста сопоставляют с мышлением архитектора. Так очень авторитетный в нашей области специалист Филипп Крачтен показал применимость в нашей области модели FBS ([Kruchten2005]). Для тех, кто не знает или забыл, кто такой Крачтен, напомню: он разработал модель описания архитектуры «4+1» и играл важную роль в разработке и развитии процесса RUP.

/media/images2/The_Function-Behaviour-Structure_Framework.png

Схема модели Function-Behaviour-Structure. Рисунок взят из Википедии. Здесь схема несколько отличается от рисунка из статьи Gero, изображена несколько измененная модель; сможете найти это отличие?

Модель Function-Behaviour-Structure (FBS) описывает мышления архитекторов. Она была предложена еще в начале 90-х годов ([Gero1990]), но сейчас модель развита и существенно модифицирована ее автором ([Gero2000], [Gero2002]). Думаю, FBS является сейчас самой популярной моделью: практически все авторы, разрабатывающие свои модели, сравнивают их с FBS. Поэтому показанная Крачтеном возможность описания мышления разработчика с помощью этой модели — это хорошее доказательство сходства мышления специалистов из двух, казалось бы, разных областей.

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

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

Шаблоны, шаблоны, шаблоны

Концепция использования шаблонов берет свое начало с работ математика и архитектора Кристофера Александера. В своих работах он исследует методику конструирования зданий, и его книга «A Pattern Language: Towns, Buildings, Construction» ([Alexander1977]) стала классикой, на которую ссылаются многие авторы, в том числе, занимающиеся вопросами разработки программного обеспечения. Видимо из-за этих корней, метафора здания очень популярна сейчас и широко используется при обсуждении современных способов разработки сложных программных систем (см., например [Bass2012]), и практически каждый разработчик слышал о шаблонах архитектуры, дизайна.

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

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

А мы пока займемся примерами.

Элементарная задача

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

Тогда мы должны просто реализовать известное решение. Здесь нет никаких сложностей, требования к знаниям программирования минимальны.

/media/images/Elementary_Problem.svg

Приведу простой пример.

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

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

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

Физик может оставаться физиком.

Итак, что мы имеем. Элементарные задачи решаются с применением известного, шаблонного способа. Есть соответствие задача — метод (шаблон) ее решения, причем в элементарном случае это соответствие однозначно. Думать не надо: как только мы распознали задачу, у нас есть информация, как ее решать.

Но не все всегда так просто.

Иерархия целей

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

Комплексные задачи мы решаем, разбивая их на подзадачи. Затем эти подзадачи могут быть снова разбиты на новые подзадачи, и так далее: мы создаем иерархию промежуточных результатов. Такой алгоритм достижения «сложной» цели, во-многом, определяется устройством нашего мозга (см., например, [Botvinick2008]), поэтому создание иерархии подцелей является очень общей стратегией. Разработка программного обеспечения не является исключением: мы последовательно используем несколько шаблонных решений для пошагового приближения к нашей цели.

/media/images/Complex_Problem.svg

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

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

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

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

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

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

С этой точки зрения, разработка приложений является последовательностью уточнений. Мы начинаем с самой абстрактной идеи, например, «нам нужен классный текстовой редактор». Затем эта идея проходит серию детализации, становясь, в конце концов, набором инструкций для компьютера, пользовательской документаций, методиками обучения пользователя.

Последовательность уточнений — тоже в определенной степени шаблон. Думаю, все эту последовательность легко назовут: разработка требований, разработка архитектуры, разработка локальной архитектуры и, наконец, разработка кода. Не все так просто: о многих сложностях создания программ и как их решают — шаблонных методах разработки — я говорил в другой своей статье «Когда „agile“ (не) к месту». В современных процессах эти этапы могут перекрываться, идти параллельно, могут происходить возвраты к уже сделанному; тем не менее логика связи идей, артефактов, создающихся на разных этапах, остается неизменной.

Из этих этапов несколько выделяется разработка требований. Она сама по себе состоит из нескольких подэтапов (см., например, [Pohl2010]); их, как минимум, два: выявление требований, и собственно разработку, создание идеи решения, которое бы удовлетворило бы запросы клиента.

«Забудем» сейчас про выявление требований. Логика этого этапа несколько отличается от всех остальных, и я немного скажу о нем в Приложении, в разделе «Исследование». То есть, на этапе «разработки» требований мы будем рассматривать только собственно их конструирование, считая, что мы уже понимаем запросы пользователя.

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

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

Разные этапы — разные шаблонные решения. Мы используем известные алгоритмы (причем, для каждой области деятельности есть свои собственные алгоритмы), шаблоны архитектуры (см., например, [Taylor2009], [Bass2012]), шаблоны дизайна программы (здесь нельзя не упомянуть классическую книгу «банды четырех» [Gamma1995]). Даже при кодировании мы используем идиомы, свойственные тому или иному языку программирования.

Например, пусть нам надо организовать цикл в программе на языке C. Думаю, мало программистов будут долго задумываться, они сразу напишут:

for ( int i=0; i<n; i++ )

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

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

Обсуждение стратегии

Здесь я хочу сделать маленькое отступление и обратить особое внимание на когнитивную стратегию проектирования программного продукта.

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

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

Вернемся к примерам. Вы думаете, решив комплексную задачу, мы достигли максимума сложности? Это не совсем так.

Компромиссные решения

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

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

Такая ситуация далеко не уникальна для разработки приложений. Нам часто приходится иметь дело с необходимостью принятия решений: мы выбираем себе дома, машины, телефоны. И каждый раз мы сталкиваемся с тем, что нет единственного «правильного» выбора; при любом решении мы что-то приобретаем, но что-то и теряем, в общем: «Если бы губы Никанора Ивановича да приставить к носу Ивана Кузьмича…».

Безопасность — компромисс на всех этапах разработки.

В разработке приложений ситуация выбора — скорее закономерность, чем исключение. Необходимость принятия решений возникает на всех этапах жизненного цикла, однако, наиболее ярко эта необходимость проявляется при проектировании, когда решается, какими свойствами будет обладать программа. Так нам приходится решать, какие функции должна реализовывать программа, решать возникающие при этом конфликты, приоритезировать требования (см., например, [Pohl2010]). Кроме того, нам приходится решать, как наше приложение будет выполнять свои функции, какими качественными характеристиками оно будет обладать; например, насколько оно будет безопасным, насколько оно будет надежным, быстрым… И, конечно, многие качества противоречат друг другу, нам приходится делать выбор (см., например, [Bass2012]).

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

Однако, в основе большинства методов разрешения конфликтов лежит одна и та же схема.

/media/images/Resolving_Conflict.svg

В этой схеме можно выделить пять этапов.

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

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

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

Но и на объективной оценке все не заканчивается. Получив такую характеристику, мы должны оценить полезность полученного качества. А эта полезность связана с объективным измерением очень нелинейно, и, иногда, не очень очевидно (см., например, [Bass2012]).

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

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

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

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

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

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

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

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

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

Во-первых, разные участники проекта могут иметь разные интересы. Так непосредственный пользователь продукта и представитель отдела безопасности заказчика, очевидно, будут спорить о необходимой функциональности. Пожалуй, разница интересов — одна из самых часто встречающихся причин возникновения конфликтов, и поэтому методам нахождения компромисса в этой ситуации посвящено множество исследований и опубликовано немало литературы (см., например, [Masters2002]).

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

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

Поэтому не случайно выбор при наличии множества критериев привлекает пристальное внимание исследователей. Способов разрешить противоречия разработано множество, но, по моим наблюдениям, для этого очень часто используется метод «анализа иерархий», разработанный Томасом Саати (см., [Saaty1989]); возможно даже, он не просто «часто используется», а является самым популярным.

И наконец, в-третьих, усложняет принятие решений и отсутствие полной информации. С точки зрения математики, это задача оценки рисков, и решается она с использованием соответствующего математического аппарата; так одной из популярных моделей, позволяющей оценить вероятности того или иного события являются баесовские сети (см., например, [Fenton2012]), в нашей области их очень хорошо комбинировать с известной моделью «деревьев атак».

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

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

Заинтересовавшихся отправляю к специальной литературе. Для начала можно посмотреть, например, две книги, о которых я уже упомянул в начале этой части ([Pohl2010] и [Bass2012]).

Творческое решение

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

Вспомним, как мы искали компромиссное решение.

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

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

Причин может быть, как минимум, две.

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

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

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

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

А сейчас вернемся к творчеству.

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

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

Психологические состояния

Считается очевидным, что творчество проходит ряд этапов (см., например, [Solso1996], стр. 475).

/media/images/Creative_Solution.svg

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

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

Часто, периоды анализа и инкубации приходится повторять несколько раз.

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

В результате мы находим новое решение.

Озарение не возникает «ниоткуда»: изобретение, открытие часто связывается с двумя находками (см., например [Ormerod2005]). Во-первых, может обнаружиться искусственность некоторых ограничений, связанных с предыдущим опытом решения подобных задач («а что и так можно?!»); кстати, возможно, поэтому дети и молодые специалисты часто решают творческие задачи лучше, чем опытные. И, во-вторых, находится некоторая аналогия, возможно, из совсем других областей, которая служит подсказкой для решения задачи (здесь как раз опытный специалист имеет преимущество). Кстати, обратите внимание, аналогия — тоже схема, шаблон решения, хотя и существенно менее конкретный, чем при решении инженерной задачи.

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

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

Как легко увидеть, все эти этапы требуют различного психологического настроя. Многие авторы считают, что если у нас получится провести человека через эти состояния в правильной последовательности, то мы сможем помочь ему быть более творческим. Эти идеи очень широко представлены в популярных книгах: «Как создать идею» Тома Вуджека ([Wujec1997]), «Шесть шляп мышления» Эдварда де Боно ([Bono1997]) и «Стратегии гениев» Роберта Дилтса ([Dilts1998]).

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

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

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

Сейчас же я хочу подробнее обсудить стадию анализа.

Анализ

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

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

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

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

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

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

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

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

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

Развитие шаблонных решений подробно изучал наш соотечественник Генрих Саулович Альтшуллер. Всем интересующимся этой темой я рекомендую почитать его книгу «Найти идею: Введение в ТРИЗ — теорию решения изобретательских задач» ([Altshuller2007]); мне кажется, именно в этой работе его идея изложена наиболее последовательно (хотя, конечно, вы можете с этим не согласиться, почитайте и другие его книги).

Идея Альтшуллера проста.

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

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

Интересно, логика анализа здесь отличается от логики инженерных работ.

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

Здесь можно обнаружить параллель с математикой.

Математика — строго дедуктивная наука. Мы формулируем аксиомы, затем на их основании, использую точные правила, выводим достоверные результаты — формулируем теоремы. Вся математика построена именно таким образом, именно так мы ее изучаем в школе, в университете…

Но, оказывается, и здесь есть место для наблюдений и аналогий. Часто теорема изначально формулируется, как гипотеза, и только потом либо находится ее доказательство, либо находится контрпример, показывающий ее несостоятельность. Этот процесс очень подробно обсуждает в своей двухтомной книге «Математика и правдоподобные рассуждения» ([Polya1975]) известный американский математик венгерского происхождения Джордж Пойа (Pólya Györg, на русском существует огромное количество различных написаний его имени). Я думаю, человеку, интересующемуся разработкой новых решений в программировании, будет очень интересно почитать эту книгу, поскольку математика — самая близкая к программированию наука (на мой взгляд).

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

Интересующихся темой развития технологий программирования отошлю к литературе. Думаю, вам будет интересно почитать введение в сборник «Software architectures - advances and applications» ([Barroca2000]) или статью «The Golden Age of Software Architecture:A Comprehensive Survey» ([Shaw2006]).

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

Как обеспечить корректность

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

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

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

Развитость профилактических мер — критерий зрелости процессов в компании

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

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

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

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

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

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

Поэтому формулировка нашей задачи проста. Мы «просто» хотим уменьшить вероятность возникновения дефектов и/или уменьшить их влияние на работоспособность (и, конечно, на безопасность) программы.

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

Комплексность

Обеспечение корректности — сложная и комплексная задача. В ее решении можно выделить три взаимодополняющих направления (см., например, [Tian2005]):

  • профилактика дефектов;
  • обнаружение дефектов;
  • изоляция дефектов.

Профилактика и обнаружение — технологии времени разработки. Говоря о безопасности, методы, применяемые на этом этапе, согласно iso 15408 необходимо описывать в разделе «Гарантии разработки».

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

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

Профилактика

Профилактика позволяет нам вообще избежать появления дефектов.

Наиболее популярная методики профилактики — обучение программистов. Хорошее понимание, что нужно делать, чего делать совсем не стоит, позволяет не допускать ошибок, и, соответственно, изначально избегать появления дефектов.

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

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

Один из самых эффективных способов профилактики — применение формальных методик. Возможность использовать математические преобразования для построения корректных программ обсуждается уже давно. Так Дейкстра еще в 1968 году предложил ([Dijkstra1968]) метод конструирования корректных программ, использующий математические трансформации. Проблема здесь в том, что спецификация является более общей, чем сама программа, поэтому у задачи есть множество «правильных» решений, множество программ могут удовлетворять данной спецификации, и необходимо применять некоторые трюки.

Использовать формальные методы непросто. И одна из причин этого — отсутствие квалифицированных специалистов.

Но они все же могут быть использованы (см., например, [Monin2003]). Применение формальных методов может быть оправдано в случаях, когда требуется достичь высоких показателей корректности. Их можно применять хотя бы при разработке наиболее критичных частей системы, одновременно используя изоляцию дефектов в менее критичных областях.

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

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

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

The results of the TSP and cleanroom projects confirm the General Principle of Software Quality: It’s cheaper to build high-quality software than it is to build and fix low-quality software. Productivity for a fully checked-out, 80,000-line clean-room project was 740 lines of code per work-month. The industry average rate for fully checked out code, is closer to 250-300 lines per work-month, including all non-coding overhead (Cusumano et al 2003). The cost savings and productivity come from the fact that virtually no time is devoted to debugging on TSP or cleanroom projects. No time spent on debugging? That is truly a worthy goal!

—McConnell. Code Complete ([McConnell2004]) стр. 563

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

Таким образом, развитость профилактических мер — критерий зрелости процессов в компании.

Изоляция

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

Мы не умеем (пока?) делать программы бездефектными. Даже если бы мы могли предпринять очень-очень строгие профилактические меры, даже если бы мы могли очень долго тестировать программу и устранять найденные ошибки, гарантии, что мы не пропустим один или несколько дефектов у нас все равно нет.

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

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

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

А теперь перейдем к более подробному разговору об обнаружении дефектов и работе над ошибками.

Проверка решения

Обнаруживаются дефекты в ходе проверки решения.

Проверка решения при разработке — задача сама по себе комплексная. Чтобы убедиться в правильности полученных нами результатов, мы должны ответить на два вопроса ([Braude2000]): «Мы делаем правильную программу?» и «Мы делаем программу правильно?». Ответ на первый вопрос мы ищем в ходе валидации, на второй — в ходе верификации.

Мы ответили отрицательно на любой из этих вопросов? Значит мы нашли в нашей программе дефект.

Верификация

В ходе верификации мы проверяем корректность нашего решения.

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

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

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

С формальной точки зрения верификация — доказательство принадлежности.

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

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

Наиболее сильная форма верификации — использование формальных методов (см., например, [Monin2003]). В начале каждого этапа мы ставим задачи, описываем цели, которые мы хотим достичь. И делаем мы это на формальном языке — языке, который не допускает неоднозначной интерпретации. По окончании этапа мы математически доказываем, что полученный результат удовлетворяет всем заданным нами критериям.

Строгая верификация использует дедуктивную логику. Мы строим модель, доказываем, что эта модель решает нашу задачу; затем мы доказываем, что наша программа соответствует построенной модели.

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

Валидация

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

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

Поэтому валидация почти всегда требует привлечения заказчика. Например, разработка исследовательского прототипа и обсуждение его с клиентом — один из методов валидации требований к программе, предоставление бета версии продукта заказчику для его пробной эксплуатации — один из методов валидации готового решения. Современные «гибкие» методики разработки включают в себя очень большой элемент именно валидации.

Таким образом, с точки зрения бизнеса, в ходе валидации делается решающая проверка качества нашего решения.

Работа над ошибками

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

К сожалению, практика несколько расстраивает. Кто занимался программированием, знает шутливую (или не очень шутливую?) примету: «если программа компилируется с первого раза, значит она написана принципиально неправильно»?

Сейчас практически в любой новой программе находятся дефекты.

Но обнаружение дефекта — не самоцель. Очевидно, необходима работа над ошибками (хотя многие компании стремятся этого не делать).

Исправление

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

По-моему, здесь все понятно. Именно цикл обнаружение/исправление и предлагается большинством «специалистов» по безопасности программного обеспечения.

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

Во-первых, мы не можем устранить все дефекты. Традиционной стала оценка, что только обнаружение — без учета затрат на исправление — более чем 95% дефектов является крайне экономически сложной задачей: поиск каждого следующего дефекта делается в разы более дорогим (см., например, [SEI2008]).

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

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

Отбраковка

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

Выход есть: при высоких требованиях корректности устранение дефектов почти не производится. Так в методе разработки «Clean Room» (см., например, [Linger1996]) предусмотрено, что если найдено слишком много дефектов, или найденные дефекты слишком серьезны, дальнейшая работа с данным модулем прекращается, все полученные результаты удаляются, разработка модуля начинается с начала.

Обратите внимание! Если спроецировать этот подход на методы обеспечения безопасности, то, как легко заметить, он существенно отличается от принятого сейчас «хакерского». Да, мы ищем уязвимости; да, это делают хакеры. Но мы используем полученную информацию для отбраковки отдельных элементов (и даже продуктов целиком), а не для их исправления.

Конечно, мелкие проблемы исправляются. И главный вопрос здесь — какие проблемы считать «мелкими»; ответ на него зависит от конкретного проекта.

Обратная связь

Устранить дефект или забраковать модуль — еще не окончание работы над ошибками. Если вы думаете, что после этого про найденную проблему можно забыть, вы очень глубоко заблуждаетесь.

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

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

Мы хотим предотвратить дальнейшее возникновении подобных уязвимостей. Для этого нам необходимо понять причины, которые привели к низкому качеству созданного нами продукта (см., например, [McDonald2007]). После этого мы можем изменить наши меры по профилактике дефектов, о которых я уже говорил чуть раньше.

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

Как обеспечить безопасность

/media/images/problem-solution.svg

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

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

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

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

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

Вот обо всем этом мы очень-очень кратко поговорим в этой части.

Шаблоны безопасности

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

Функциональность

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

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

Функциональность безопасности — тема очень обширная. Например, к вопросам функций следует отнести и модели безопасности ([Grusho1996], [Devaytin2005]), и методы аутентификации ([Smith2002]), и криптографические методы ([Schneier2002], [Grusho2000]), и методы анализа журналов событий ([Babbin2006]), и методики обнаружения сетевых атак ([Bejtlich2004]), и многое другое, что я сейчас даже не вспомню. Вообще, как мне кажется, эта сторона является самой изученной во всей тематике безопасности программного обеспечения, и литературы здесь действительно более чем достаточно. Заинтересованным читателям могу порекомендовать хороший обзор этой темы, мой любимый учебник по безопасности ([Bishop2003]).

Большое внимание функциям безопасности уделяется и в современных стандартах. Так в iso 15408 примерно половина всего документа посвящена именно функциональным свойствам «продукта информационных технологий».

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

Пользовательский интерфейс

У безопасности есть важный, но часто забываемый аспект. Чтобы наше приложение можно было безопасно использовать, оно должно иметь понятный и приемлемый для пользователя интерфейс (см., например, [Cranor2005]).

Некоторые специалисты относят пользовательский интерфейс к архитектуре программы (см., например [Bass2001]). Действительно, архитектура является носителем нефункциональных свойств приложения, и пользовательский интерфейс, очевидным образом попадает в эту категорию.

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

Приемлемость

Очевидно, пользователь не должен возражать против применения средств защиты.

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

Неприемлемые механизмы защиты пользователь будет, как минимум, игнорировать. Так мне лично приходилось наблюдать, как работники одной из компаний блокировали замки на входных дверях их кабинетов, предотвращая автоматическое захлопывание. Причем эти замки были поставлены по требованию специалистов по безопасности для получения некого сертификата. Пока проверяющие находились на объекте, механизм использовался; как только проверяющие удалялись — замки блокировались, чтобы не мешать «нормальной работе».

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

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

Понятность

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

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

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

Таким образом, сложность защиты играет против безопасности.

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

  • какие знания должен иметь пользователь;
  • какую информацию он должен запомнить (например, пароли);
  • какова когнитивная сложность решаемой задачи (о когнитивной сложности см., например, [Oosterhof2008]) .

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

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

Архитектура

Безопасность — нефункциональное свойство программного обеспечения. Это заявление, конечно же, звучит несколько странно после того, как мы только что обсудили ее функциональные аспекты, но именно так рассматривают безопасность многие весьма уважаемые специалисты (см., например, [Bass2012] и [Taylor2009]). И если подумать, то можно прийти к выводу, что здесь есть своя логики: функции безопасности не делают ничего «полезного», они, скорее, предназначены для предотвращения незаконного использования другой функциональности программы; подробнее об этом вы можете почитать в другой моей статье.

Считается, что носителем нефункциональных свойств является архитектура. Упрощенно, под «архитектурой» программы можно понимать ее структуру; так, в частности, архитектура определяется частями, из которых состоит программа, свойствами этих частицей, и взаимодействием отдельных частей между собой. В частности, написана программа на C++ или на Jave — это описывается вместе с архитектурой приложения.

Безопасность должна быть отделена от бизнес логики.

Я уже писал о влиянии архитектуры на безопасность. Заинтересованному читателю порекомендую почитать посвященный этой теме раздел другой моей статьи (Качество и архитектура программы).

Приведу только небольшой пример оттуда.

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

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

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

Архитектурный аспект безопасности проработан относительно мало. Пока не существует стандартов безопасной архитектуры; тем не менее, уже давно — по меркам нашей индустрии — можно найти работы, которые описывают успешный опыт в этой области (см., например, [Kienzle2003], [Blakley2004], [Dougherty2009]).

Консерватизм

Безопасность является крайне консервативной областью. Действительно, прежде чем начать использовать то или иное шаблонное решение, специалисты долго его изучают, анализируют его уязвимости. Даже если новый шаблон на первый взгляд выглядит чрезвычайно защищенным, применять его на практике не спешат (отличается от поведения «программистов», особенно молодых?).

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

Такому консерватизму есть объяснение: обнаружение ошибок занимает много времени. Иногда уязвимости в алгоритме, шаблонном архитектурном решении, конкретной библиотеке обнаруживаются только через несколько лет, иногда совершенно (ну или почти) случайно.

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

Компромиссность

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

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

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

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

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

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

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

Еще раз. Безопасность — компромисс на всех этапах разработки приложения.

Процесс разработки

О процессах разработки я достаточно подробно писал в другой статье — «Когда „agile“ (не) к месту». Думаю, заинтересованный читатель может найти там интересные для себя моменты.

Здесь я хочу поговорить об отдельном шаге любого из этих процессов. Что значит «шаг», зависит от конкретного процесса: это и любой из этапов водопадной модели; это и один из «спринтов» в методологии скрам; это любой «оборот» в спиральной модели — все они могут быть построены по более или менее схожей схеме. Мое описание этих элементов во-многом опирается на информацию из раздела «Как разработать программу» этой статьи, если вы ее еще почему-то не прочитали, сейчас как раз самое время это сделать.

Я очень долго думал, куда поместить этот подраздел. Действительно, легко заметить, что бо́льшая часть сказанного здесь применима к разработке программных продуктов в целом; с этой точки зрения надо было поместить этот материал в раздел «Как разработать программу». С другой стороны, я обращаю особое внимание на разработку программ, для которых важна безопасность; тогда получается, место этого продраздела здесь.

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

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

/media/images/Security_Engineering.svg

Постановка цели

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

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

Более того, наши «безопасники» часто вообще этот этап забывают. Внимательно почитайте любую статью наших «аналитиков», послушайте, что они говорят на вебминарах. Вы видели, чтобы кто-то из них говорил о постановке цели? В лучшем случае они приводят определение уязвимости, это, конечно, имеет некоторое отношение к нашим целям, но все же совсем не то. Например, попробуйте как-нибудь задать «аналитику» вопрос, какого объема анализа нашей программы достаточно, на каком этапе можно остановить поиск уязвимостей; как вы думаете, сможет он обоснованно на него ответить? Но этот вопрос — только один из множества, на которые необходимо найти ответ, чтобы получить четкое понимание целей разработки.

Корректное описание цели безопасности — тема отдельной большой статьи, или даже книги. Оставлю это на будущее.

Решения-кандидаты

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

В зависимости от новизны ситуации, здесь возможны несколько вариантов:

  • для решения нашей задачи существуют «лучшие практики» и/или стандарты;
  • для решения нашей задачи существует несколько возможных решений, но пока нет общепринятого лучшего решения, и мы должны сами сделать выбор;
  • ни одно из существующих решений не удовлетворяет нас в полной мере, и мы должны разработать новое решение, возможно, новую технологию.

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

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

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

Важно заметить: пока мы не выходили за рамки известных шаблонов.

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

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

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

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

Уникальное решение

Здесь я не буду говорить много. Я уже подробно описал, как создаются новые решения в разделе «Творческие решения».

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

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

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

Анализ уникального решения

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

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

Характеристики продукта сильно различаются по возможности их проверки.

Есть относительно легко наблюдаемые свойства. Например, мы можем более или менее легко оценить быстродействие полученной программы, померив ее производительность при выполнении ключевых операций; мы можем оценить удобство использования интерфейса, дав пользователю «поиграть» с ним и спросив его о впечатлениях.

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

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

К сложно проверяемым свойствам относится и безопасность. Когда мы пытаемся разработать свои средства безопасности, свои алгоритмы, архитектуру приложения, мы должны очень хорошо представлять себе, на чем основана наша уверенность, что они действительно безопасны. Аргумент «никто из моих друзей не смог сломать, и Я не смог», конечно, здесь не принимается.

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

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

Если нет, держитесь подальше от изобретательства в безопасности.

Оценка и выбор решения

К этому моменту уже мы знаем свойства всех решений-кандидатов. Нам осталось только «взвесить» эти решения и выбрать наилучшее.

Решение этой задачи я уже описывал в разделе «Компромиссные решения». Если вы забыли его содержание, лучше просмотреть его еще раз.

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

Реализация шаблона

Что значит «реализовать» решение, зависит от фазы разработки. Это может быть и создание документа, описывающего использование конкретного алгоритма в нашей программе; это может быть и документ с определением архитектуры программы; это может быть и код программы.

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

Проверка соответствия

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

Теперь мы должны проверить соответствие нашей реализации шаблону.

Здесь надо обратить внимание, у большинства шаблонов есть два аспекта. С одной стороны, шаблон — описание того, что и/или как должен делать программист (разработчик вообще) чтобы достичь своей цели; именно так мы чаще всего и воспринимаем наш шаблон. Но у большинства шаблонов есть и другая сторона: перечисление ошибок, которые мы можем сделать, следуя данному рецепту. Эта сторона очень часто скрыта от нас, часто бывает неявным, неформальным дополнением к используемому рецепту.

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

Легко заметить, знание о возможных ошибках может быть переформулировано в шаблон. Так, например, мы исключим переполнение буфера, если в описание шаблона будет будет включена обязательная проверка размера буфера. Для языков типа С, С++, Fortran и подобных эту проверку должен делать программист явно; для языков типа Java или C# такую проверку будет делать среда исполнения.

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

Первый этап — проверка самим разработчиком. Практически любой программист в большей или меньшей степени проверяет свою работу. Однако, очень редко формальное описание процесса явно требует выполнять это действие; я знаю только TSP и, практически являющийся его частью, PSP (см. [Humphrey2006], [Humphrey2007]), которые делают это.

Второй этап — peer review. Я не стал переводить этот термин, поскольку его знает сейчас практически каждый разработчик. Peer review предполагает проверку работы таким же специалистом, как и тот, что делал основную работу (подробности можно найти в книге [Wiegers2010]). На этом этапе «отлавливается» гораздо больше проблем, чем на первом, поскольку проверку делает человек со свежим взглядом. Другой большой плюс peer review — возможность неявного обучения неопытного специалиста, передача ему опыта от эксперта.

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

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

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

Завершение этапа

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

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

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

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

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

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

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

Думать как хакер?

Должен ли разработчик думать как хакер? Действительно, следую наивной логике, можно предположить, что защиту программы надо начинать из позиции хакера. Можно предположить, что мы сначала должны поставить себя на место атакующего, представить все возможные его действия. Затем, на основании полученной информации, мы сможем создать хорошо защищенное приложение.

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

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

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

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

Не надо думать как хакер!

Когда хакер не нужен

Как мы видели, мы (иногда) привлекаем хакера для проверки безопасности нашей разработки. Возникает вопрос: всегда ли необходимо это делать?

Нет, не всегда! Представим себе две крайних ситуации.

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

Вторая ситуация — хорошо известное шаблонное решение.

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

/media/images/creative-proven.svg

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

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

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

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

Хакера не обязательно привлекать, если:

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

Хакера-тестировщика надо привлекать, если шаблон известен, но у нас нет большого опыта работы с ним; хакера-исследователя надо привлекать, если шаблон мало известен (о хакерах-исследователях и хакерах-тестировщиках я буду более подробно говорить в части «Люди, роли, опыт»).

Когда хакинг работает

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

В идеальном мире не должно быть хакинга!

Наращивание технического долга можно сравнить с варкой лягушки на медленном огне

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

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

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

Как мы должны реагировать на такое событие?

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

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

Стоит обратить особое внимание: проблемы имеют свойство умножаться. Действительно, переработать модуль в хорошо продуманной программе относительно легко, быстро и дешево. По мере накопления «костылей» стоимость переработки растет и привлекательность использования «быстрых», но «грязных» решений возрастает; и это влечет за собой дальнейшее ухудшение качества. Возникает замкнутый круг.

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

Но есть и другая сторона. Да, наш модуль дефектен, в нем есть неизвестные пока уязвимости, возможно, их много; да, мы, возможно, внесем еще больше уязвимостей, создавая «заплатку». Но все эти недостатки пока неизвестны, возможно, они будут открыты завтра. А сегодня мы имеем вполне конкретную известную уязвимость, и мы знаем, злоумышленники готовы ее использовать, если до сих пор не используют.

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

Но как же с ухудшением качества программы?

Оптимальным решением в такой ситуации является сделать двойную работу. Сначала мы должны быстро решить текущую проблему, «залатать» программу, затем, при малейшей возможности, сделать работу по переработке проблемной части. И переработку желательно провести до того, как в программе начнут открываться новые недостатки.

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

Технический долг

Термин «технический долг» был предложен в начале 90-х годов. Это была аналогия, придуманная для объяснения нетехническим специалистам, почему время от времени приходится возвращаться к уже готовым частям программы. Проводя аналогию с финансовым долгом, легко было объяснить, что мы должны «отдать технический долг» за быстрое решение в прошлом. Конечно, как всякая аналогия, технический долг имеет только некоторые общие черты с финансовым, тем не менее аналогия оказалась очень успешной и сейчас активно используется.

Постепенно аналогия стала популярна и в среде специалистов. Так Филипп Крачтен считает, что понятие «технического долга» давно переросло простую аналогию и уже в качестве точного технического термина активно используется при планировании реальной разработки (см., например [Kruchten2012]). Более того, он считает, что это понятие желательно включить в программу обучения студентов, специализирующихся на разработке программного обеспечения ([Falessii2015]).

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

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

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

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

С другой стороны, заемные деньги действительно решают множество проблем. Так мы знаем, иногда взять ипотечный кредит оказывается выгоднее, чем платить за съемную квартиру; компании, которые берут взаймы оборотные средства во многих случаях (в большинстве?) развиваются быстрее, чем те, которые используют только свои.

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

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

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

Конечно, я затронул концепцию технического долга очень поверхностно. Заинтересованного читателя отошлю к литературе. Начать можно с упомянутых здесь двух статей Крачтена; также очень рекомендую почитать соответствующие страницы в блогах других двух известных специалистов: Steve McConnell о техническом долге и Martin Fowler о техническом долге.

Есть возможность и более глубоко познакомится с обсуждаемой концепцией. Читателям, которым необходимо принимать решение, в частности, об использовании «хакинга», и возвращении возникшего долга, очень рекомендую прочитать книгу «Managing Software Debt: Building for Inevitable Change» ([Sterling2010]). Кстати , пятая глава этой книги напрямую описывает подход, который я называю «хакингом», как одну из причин возникновения и роста технического долга; впрочем, компании, увлекающиеся использованием «хакинга», часто создают неподъемный технический долг и другими способами, описанными в этой книге.

Читайте, думайте, применяйте.

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

Как найти уязвимость

Вам кажется, что поиск уязвимостей — магия? Вы думаете, что для выполнения этой работы необходимо обладать каким-то особым талантом, быть «компьютерным гением»?

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

Поиск уязвимости — индуктивная стратегия: изучение, выдвижение и проверка гипотез, обобщение.

Признанной общей методологией поиска уязвимостей является «Flaw Hypothesis». Она широко используется при поиске уязвимостей в программном обеспечении; также можно найти работы, описывающие применения этого подхода к решению других задач, возникающих при разработке безопасных программ (см., например, [Srivatanakul2004] и [Srivatanakul2005]). Обсуждение этой методологии можно найти в почти каждом солидном учебнике по безопасности, включая мой любимый ([Bishop2003]).

Заинтересованных читателей отошлю к первоисточнику. Методология была изначально была предложена еще в 1973 году ([Weissman1973]); мне не удалось разыскать в интернете оригинальной статьи, но более поздние работы автора по этой теме легко доступны ([Weissman1995a], [Weissman1995b]).

«Flaw Hypothesis» — очень авторитетная методология. Чтобы продемонстрировать это, сошлюсь на стандарты сертификации безопасности программного обеспечения.

На необходимость использования именно этого подхода указывалось в «Оранжевой книге». Процитирую:

Team members shall be able to follow test plans prepared by the system developer and suggest additions, shall be familiar with the "flaw hypothesis" or equivalent security testing methodology, and shall have assembly level programming experience

—Оранжевая книга ([TCSEC1985]), пункт 10.1.1

Современные «Общие критерии» также не обходятся без нее. Так в руководстве по проверке соответствия продукта требованиям безопасности написано:

A flaw hypothesis methodology needs to be used whereby specifications and development and guidance evidence are analysed and then potential vulnerabilities in the TOE are hypothesised, or speculated

—Общие критерии ([CEMv3.1]), пункт 15.2.3.6

Интересное наблюдение. Обратите внимание, что в «оранжевой книге», когда говориться о данной методологии, есть добавление «или ее аналог»; в современных «общих критериях» такой оговорки нет; то есть, рекомендуется применять «Flaw Hypothesis» и только ее.

Обсудим эту методику подробнее.

Flaw hypothesis

Процесс анализа, согласно «flaw hypothesis», состоит из четырех этапов ([Bishop2003]).

/media/images/Flaw_Hypothesis.svg

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

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

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

Четвертый этап — обобщение полученного опыта. Логика этого этапа основывается на том, что люди «любят» делать одинаковые ошибки в разных ситуациях. Поэтому, если мы нашли в их продукте какую-либо уязвимость, очень вероятно, что в нем присутствуют и другие уязвимости подобного типа. Таким образом, у нас появляются новые основания для выдвижения гипотез и мы снова переходим на этап их генерации.

Легко видеть, поиск уязвимости — индуктивная стратегия: изучение, выдвижение и проверка гипотез, обобщение.

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

Это особенность «flaw hypothesis». Методология определяет не конкретный процесс, а общий подход, фреймворк; по этой схеме могут быть построены различные реальные методы анализа.

Далее я вам предложу описание одного из процессов анализа. Этот процесс построен, естественно, по схеме «flaw hypothesis». Мой пример, возможно, немного искусственен, я сделал его таким специально, чтобы более явно продемонстрировать схему рассуждений.

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

Но на начальном этапе, все же, лучше следовать «искусственному», учебному рассуждению.

Изучение системы

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

Этап сбора информации очень хорошо описал Сем Канер (Cem Kaner). Заинтересовавшимся читателям я предложу почитать его работу «A Tutorial in Exploratory Testing» ([Kaner2008]), кстати, термин «exploratory testing» был предложен именно Семом Канером. Конечно, Канер в этой работе говорит не о тестировании безопасности, а о тестировании программного обеспечения вообще, но все его идеи из этой работы полностью применимы и при поиске уязвимостей.

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

Выдвижение гипотез

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

Здесь мы можем использовать разные информационные источники.

Так многим нравится использовать «шаблоны атак». Большой справочник по известным шаблонам собран на сайте Common Attack Pattern Enumeration and Classification. На этом сайте представлена информация о способах атаки на уязвимости, возникающие в различных частях информационных систем. Подробнее об использовании «Шаблонов атак» вы можете прочитать в книге «Exploiting Software: How to Break Code» ([Hoglund2004]). Похожие идеи вы можете найти и в книге «How to Break Software Security» ([Whittaker2003]).

Некоторые специалисты больше любят говорить о «слабостях системы». Этот подход является дополнительным к «шаблонам атак»: каждая атака использует одну или несколько слабостей наших разработок. Справочник по слабостям различных программ и технологий представлен на сайте Common Weakness Enumeration.

Вернемся к нашему примеру. Вспомним, на первом этапе мы обнаружили, что программа написана на C++. Сейчас, на этапе генерации гипотез, мы можем вспомнить факт, что программы на этом языке часто содержат уязвимости «переполнение буфера». Таким образом, одной из наших гипотез будет предположение, что изучаемая программа содержит подобные уязвимости.

Итак, на втором этапе мы сформировали одну гипотезу: изучаемая программа содержит уязвимости «переполнение буфера».

Проверка гипотез

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

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

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

Вернемся к нашему примеру.

Для проверки возможности переполнения буфера мы можем использовать «fuzzing». При использовании этой техники на вход программы подается большое количество случайных данных, и наблюдаются реакции программы на это воздействие. По практике, техника «fuzzing» эффективна для обнаружения ошибок, связанных с переполнением буфера (см., например, [Howard2009]); если в тестируемом приложении есть ошибки, связанные с отсутствием контроля входных данных, очень вероятно, что приложение «упадет». Преимуществом метода является ее простота и высокая автоматизируемость — закодировать такой тест может более или менее разбирающийся студент, а дальше проверка программы происходит вообще без участия человека. Недостаток метода — большое время работы и негарантированность результата.

Предположим, наша гипотеза подтвердилась — приложение позволяет осуществить переполнение буфера и, возможно, оно уязвимо.

Мы знаем, наличие предпосылок — не всегда наличие уязвимости. Переполнение буфера — это только предпосылка, мы можем сказать, что это действительно проблема безопасности, только предложив способ использовать («эксплуатировать») эту ошибку для обхода защиты.

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

Поэтому в нашем примере мы закончим этап на обнаружении возможности переполнения буфера.

Обобщение

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

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

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

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

Окончание цикла проверки

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

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

Иерархия анализа

Любой программный продукт — сложный объект. Мы видели, разработчик вынужден при его создании выстраивать сложную иерархию целей и подцелей. Я так же упоминал, построение иерархии целей и решения является очень общей стратегией, определяющейся, в том числе, строением нашего мозга (см., например [Botvinick2008]).

Именно отличие взглядя хакера от взгляда разработчика делает его поиск уязвимостей более эффективным.

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

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

Начнем декомпозицию нашей системы.

Система состоит из двух частей. Эти две части — клиент и сервер; взаимодействие между ними происходит по http протоколу. Кажется, тривиальная находка, но, во-первых, это только первый этап; во-вторых, уже сейчас мы должны начать выдвигать гипотезы.

Мы знаем типичные уязвимости такой системы. Действительно, исходя из нашего опыта мы можем предположить, что сервер будет содержать типовые серверные уязвимости; в частности, мы знаем, что одна из самых частых уязвимостей здесь — SQL Enjection (для приверженцев точной классификации — недостаточность проверки входных данных, приводящая…). Аналогично, на клиенте мы можем поискать возможность переполнения буфера при обработке pdf файлов, приводящее к исполнению произвольного кода.

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

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

И, конечно, глубина подобного анализа зависит от сложности объекта.

Разработка новых атак

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

Конечно, «впихивать» творческий процесс в общую схему достаточно сложно. Более того, поиск уязвимостей не всегда происходит целенаправленно, и многие из них на самом деле были открыты случайно: кто-то отлаживал программу, обнаружил ее странное поведение, исследовал, понял, что эту странность можно использовать для атаки, обобщил.

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

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

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

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

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

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

Вернемся к переполнению буфера.

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

Затем было открыто, что эту ошибку можно использовать для атаки. Искусные специалисты нашли, как при переполнении буфера в стеке мы можем переписать важные служебные области и заставить программу выполнить необходимые нам действия. Очень подробно этот метод атаки был описан в классической статье Smashing The Stack For Fun And Profit ([Aleph1996]).

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

Теперь сделаем еще один уровень обобщения. Обратим внимание, что наша уязвимость связана с использованием необработанных данных, введенных пользователем. Мы можем заинтересоваться, где еще в программах часто используют необработанные входные данные. Вполне возможно, что где-то в таких местах мы сможем обнаружить новые уязвимости.

Например, так могла бы быть открыта уязвимость «uncontrolled format string». Из того, что мне доводилось читать про эту уязвимость, открыта она была случайно при отладке одного из новых демонов для Un*x. Но есть основания считать, что она была известна и раньше в узких кругах хакеров ([Hoglund2004], стр. 317). Как они обнаружили эту уязвимость, мне не известно, вполне возможно, именно следуя нашей логике или какой-то подобной.

Отсутствие контроля входных данных приводит и к другим уязвимостям. Например, это и популярный сейчас XSS, и CSRF — все они связаны именно с излишним доверием информации, переданной пользователем.

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

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

Интересно отметить пару фактов.

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

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

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

Результаты анализа

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

«Flaw hypotesis» — методика системного поиска уязвимостей. Очень осторожно я бы назвал ее «анализом уязвимостей», но ни в коем случае я не сказал бы, что это «оценка уязвимости».

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

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

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

И еще раз: хакер ищет уязвимости. Результат его деятельности — открытая уязвимость или слабость продукта или технологии. Никаких других выводов, скорее всего, мы на основании его анализа сделать не можем.

Проверка результата

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

В методике Flaw Hypothesis такая проверка уже встроена. Этап «проверка гипотез», по определению, решает именно эту подзадачу. Конечно, глубина контроля зависит от поставленной цели. Например, мы можем просто найти в программе возможность переполнить буфер, и этого будет достаточно; но от нас также могут потребовать продемонстрировать, что мы можем «эксплуатировать» эту ошибку — написать «эксплойт».

Здесь надо сделать пару замечания.

Во-первых, проверку делает сам хакер. По крайней мере, так происходит чаще всего; иногда, конечно, экслойты пишутся не им самим, а его напарником, который умеет делать это лучше, но все же хакер даже до написания такого эксплойта уже понимает, что достиг цели.

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

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

Думать как разработчик?

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

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

Должны ли совпадать эти иерархии? Другими словами, должен ли хакер стремится понять мотивы разработчика, чтобы углубить свой анализ; или он должен построить свою, оригинальную модель объекта?

Ответ на этот вопрос неоднозначен.

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

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

Как же своя модель может нам помочь?

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

Мы должны найти «малозначительное». Анализирую систему, программный продукт, мы строим свою модель этого объекта, модель, учитывающая те взаимодействия, которые, возможно, упустил разработчик. И это дает возможность обнаружить неожиданные — по крайней мере для разработчика — свойства этого объекта. И именно возможность построить свою модель, именно отличие взгляда хакера от взгляда разработчика делает его поиск уязвимостей более эффективным.

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

Люди, роли, опыт

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

Правильно построенная команда — ключ к успеху проекта. Даже самые лучшие специалисты не могут проявить свои качества, если в группе не налажено правильное разделение по ролям и правильное их взаимодействие. Иногда плохое построение команды приводит к катастрофическим последствиям, что показал, в частности, опыт NASA (см., например, [Pellerin2009]); конечно, в менее критичных области провал команды может быть не так заметен, и на этот аспект не часто обращают внимание, но это не уменьшает его реальное влияние.

Построение и поддержание команды — тема очень многогранная. Создавая группу, мы должны понимать, специалисты с какими навыками и знаниями нам нужны; мы должны обеспечить их «гладкое» взаимодействие, сплотить группу; с другой стороны, сплочение группы не должно привести к ситуации, когда люди «заметают под ковер» возникающие проблемы. Поддерживая группу, мы должны обеспечивать обучение и рост наших специалистов, замену в случае их ухода, отпуска, болезни. Так согласно уже упомянутой здесь книге ([Pellerin2009]), NASA оценивает культуру коллектива по достаточно сложной «4D-системе», и, думаю, это еще далеко не полная оценка.

Построение команды в разработке ПО имеет свои особенности. Заинтересованным читателям я могу порекомендовать очень хороший обзор различных вариантов решения этой задачи в классической книге Стива Макконела ([McConnell1996]).

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

Роли

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

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

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

Инженер и изобретатель в области безопасности должны работать в разных отделах.

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

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

Хакер-тестировщик. Задача хакера-тестировщика — тестирование и анализ готовых продуктов, отдельных их частей, промежуточных артефактов; в ходе свой работы он ищет в них уязвимости уже известного типа. Это человек, который:

  • отслеживает современное положение дел с уязвимостями различных технологий;
  • умеет анализировать продукт, находить в нем признаки уязвимостей;
  • умеет объяснить разработчику причину появления проблемы.

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

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

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

Задачи и роли
  Постановка задачи Формулировка решений-кандидатов Создание технологии Анализ технологии Оценка решений Выбор решения Реализация Самопроверка и peer review «Тестирование» Решений о завершении этапа
Руководитель группы              
Разработчик-инженер            
Разработчик-изобретатель                
Хакер-тестировщик         Частично        
Хакер-исследователь                  

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

Стратегии

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

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

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

Характеристики решаемых задач
  Руководитель группы Разработчик-инженер Разработчик-изобретатель Хакер-тестировщие Хакер-исследователь
Цель Создание продукта, соответствующего ожиданиям клиента Создать новый продукт с заданными свойствами Создать новую технологию, решающую наши задачи Найти уязвимости в существующум продукте Найти слабости существующей технологии
Исходные материалы Пожелания пользователей, угрозы безопасности Цели разработки, угрозы безопасности Угрозы безопасности Существующая система Существующая технология
Стратегия достижения цели Синтез Синтез Синтез+Анализ Анализ Анализ+Синтез
Логика планирования Дедукция Дедукция Дедукция + Индукция Индукция Индукция + Дедукция
Результат Продукт удовлетворяющий заказчика, безопасный продукт Продукт с набором свойств, безопасный продукт Безопасная технология Найденная в продукте уязвимость Найденная в технологии слабость
Методика проверки Анализ Анализ и тестирование Анализ Прямой эксперимент Прямой эксперимент
Время проверки Нескольких дней - несколько месяцев Нескольких дней - несколько месяцев Несколько месяцев - несколько лет «Мгновенно» «Мгновенно»
Выводы проверки Вероятностные Вероятностные Вероятностные Точные Точные

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

Навыки

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

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

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

Знания и навыки, необходимые для исполнения роли
  Руководитель Разработчик - инженер Разработчик - изобретатель Хакер - тестировщик Хакер - исследователь
Шаблоны атак широко      
Шаблон атаки глубоко        
Тенденции развития атак        
Шаблон защиты широко      
Шаблон защиты глубоко        
Тенденции развития защиты        
Анализ, приоритезация исследования      
Синтез, критерии достижения цели    
Принятие компромиссных решений        
Наблюдение и обобщение    
Доказательство соответствия    

Сравнение

Теперь мы можем сопоставить информацию из полученных таблиц.

Хорошо видно разделение ролей на две группы.

В первую группу входят роли, занимающиеся «созиданием». Они синтезируют новые продукты из уже известных элементов или создают новые элементы с новыми свойствами; они пользуются знанием свойств отдельных элементов и правил их комбинирования для предсказания свойств результата. Легко заметить, людей, играющих «созидательные» роли, я в этой статье называю «разработчиками».

Во вторую группу входят «аналитики». Они исследуют, анализируют уже существующие продукты и технологии, изучают их свойства. Они используют похожесть изучаемого объекта на другие, уже изученные и исходя из этой похожести прогнозируют поведение нового объекта в разных ситуациях и проверяют его действительное поведение. И опять легко заметить, людей, играющих роль аналитиков, я в этой статье называю «хакерами».

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

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

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

Но здесь есть несколько тонких различий.

/media/images2/Faces_or_Vase.png

Известнейший пример двойственности восприятия. Что вы видите: вазу или два лица? Рисунок взят из Википедии

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

Эксперт отличается от новичка именно простроенным пространством задачи (см., например, [Anderson2002]). Как только задача уходит из этого пространства, его преимущества перед новичком сразу полностью теряются. Так, например, известно, что шахматист легко запоминает позицию на доске, но произвольный набор фигур запоминает ничем не лучше, чем обычный человек, поскольку задача не вписывается в известные ему шаблоны.

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

Другими словами: пересечение есть, но оно очень и очень слабое.

Между «инженерными» ролями вообще нет никаких пересечений.

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

И опыт, сын ошибок трудных…

Очень интересен вопрос об опыте, формирующем наши знания и навыки. Как быстро можно получить новый опыт, как быстро можно «сменить амплуа»? Я в этой части буду приводить примеры получения опыта разработчика, но практически те же самые слова можно сказать и о хакере, да и вообще о любом Специалисте.

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

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

Опыт приобретается годами. В его накоплении мы можем выделить четыре фазы.

Первая фаза — неосознанное незнание. Мы не умеем чего-то делать или чего-то не знаем, и при этом наше незнание настолько глубоко, что мы даже не осознаем его, не замечаем своих ошибок, неправильных решений и прочего подобного. В Интернете подобная ситуация известна как эффект Даннинга-Крюгера, названный по фамилиям людей, экспериментально его продемонстрировавших ([Kruger1999]). Это исследование получило Шнобелевскую премию в 2000 году, а сейчас можно найти не одну подобную работу — стадия незнания может быть интересна исследователям.

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

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

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

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

Длительность нахождения во второй фазе зависит от объема изучаемого материала. Это может быть пара часов, как в случае посещения простейшей лекции или семинара. Но этот период может длиться и 5-6 лет, как в традиционном высшем образовании.

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

Третья фаза — осознанное знание. Это фаза, в которой мы используем уже полученную нами информацию на практике, сопоставляем ее с реальностью, набиваем шишки, формируем собственное «кладбище», это фаза, в которой мы приобретаем опыт.

Длительность нахождения в третьей фазе сильно зависит от сложности изучаемых навыков. Она может составлять от несколько месяцев до 5-8 лет.

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

Очевидно, разные знания у нас находятся в разных фазах. Как я уже отмечал, мы очень-очень-очень-очень много не знаем и даже не осознаем этого незнания. С другой стороны, есть области, в которых мы являемся экспертами: как минимум, большинство из нас не задумывается, как переставлять ноги при ходьбе, мы делаем это автоматически, на экспертном уровне. Многие другие знания находятся у нас на промежуточном уровне.

Путь от новичка к эксперту в сложной области займет у нас от 8 до 12 лет (см., например, [Anderson2002]). За это время бо́льшая часть знаний в этой области пройдет вторую и третью фазу и перейдет в четвертую. Обратите внимание: бо́льшая часть, не все, совершенствоваться в любой области мы можем практически всю жизнь.

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

Таким образом хакеру понадобится лет 8-12, чтобы стать разработчиком. Конечно, это время надо отсчитывать от момента начала обучения, когда он поймет, что учиться надо. И конечно, как всегда при учебе, хакеру необходимо иметь высокую мотивацию, готовность испытывать негативные эмоции от неудач; надо ли это человеку, который уже имеет высокую квалификацию в одной из областей?

Обратное тоже верно, разработчику надо очень много работать, чтобы стать хакером.

Более того, изучить любую область заново гораздо легче, чем сменить амплуа. При смене амплуа в нашем случае может сыграть свою отрицательную роль негативный перенос.

Негативный перенос

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

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

В этом мнении есть рациональное зерно. Да, действительно, человек, добившийся успеха в одной области, обычно легче развивается и в другой. Опыт решения различных задач накапливается, при решении новой задачи иногда можно действовать по аналогии, поэтому в психологии существуют теории, которые рассматривают накопленный опыт в качестве «базы», кристализовавшегося интеллекта (см., например, [Druginin1999]).

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

Здесь дело в особенностях нашей психологии. В большинстве случаев нам не надо находить оптимальное решение, мы должны найти приемлемый способ действия, используя минимум времени и усилий. Поэтому при решении задач мы используем различные упрощения; и, действуя таким образом, мы ведем себя в обычных условиях в достаточной степени рационально. В частности, мы очень любим «повторяться» ([Anderson2002]):

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

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

У меня была возможность поэкспериментировать с влиянием установок. Ближе к концу 90-х годов я преподавал Unix студентам на факультете ВМиК МГУ и на коммерческих курсах. В это время Unix в России был практически неизвестен, и поэтому у меня была возможность посмотреть, как студенты решают задачи, а не вспоминают известные шаблонные решения той или иной проблемы.

В числе прочего, я преподавал программирование на языке Shell. Это интерпретируемый язык, который очень активно используется и администраторами, и пользователями, и программистами; этот же язык используется во многих (сейчас, вероятно, в большинстве) командных интерпретаторов в Unix.

Одна из сложных тем здесь — специальные символы. Как и во всех языках, в Shell есть символы, которые интерпретируются специальным образом, например, конец строки и точка с запятой являются признаками конца команды. Иногда эти символы необходимо использовать в самой команде — необходимо иметь возможность отменять их специальное значение. Для этого существуют целых три символа: кавычки ('), двойные кавычки (") и обратный слэш (\). Эти три символа сами по себе являются специальными и по-разному отменяют значение всех специальных символов, включая самих себя, и хитрым образом взаимодействуют между собой.

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

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

Создав установку, я проверял влияние полученного опыта. Я задавал задачу, похожую на только что решенные — просил удалить файл, название которого начинается с символа дефиса, например "-u". Хитрость задачи в данном контексте в том, что дефис — тоже специальный символ, но не для shell, с которым мы только что работали, а для самой команды "rm". Команда «отказывалась» удалять файл; и все методы отмены специального значения символа для shell здесь, очевидно, не срабатывают.

Задача, конечно, имеет решение. Но это решение лежит за пределами только что изученных способов.

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

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

Таким образом, совет «забудьте все, чему вас учили раньше» часто имеет огромный смысл. К сожалению, ему не так легко следовать!

Творчество и безопасность

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

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

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

Стратегии «инженера» и «изобретателя» сильно различаются. Инженер должен очень строго следовать «рецептам», шаблонам безопасной разработки; изобретение, творческое решение — практически всегда выход за границы известных ограничений.

Можно ли разрешить это противоречие? Можно ли разрабатывать «инновационные» программы, при этом сохранять их безопасность?

Я верю, можно! Для этого надо только следовать нескольким простым правилам.

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

Может ли это быть один человек?

Есть основания говорить «нет». Действительно, практические психологи говорят (см., например, [Hall2007]), что у человека есть «любимые» стратегии решения задач, в том числе есть люди «рецептов» («процедур» в терминологии книги из ссылки выше) и есть люди «возможностей».

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

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

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

Таким образом, безопасность, вообще-то не так и сильно противоречит творчеству.

Укреплять «слабое звено»

И, традиционно, несколько слов в заключение.

Хорошо известно правило «слабого звена». Если мы хотим эффективно повысить качество нашей работы, мы должны искать ограничения и устранять их; причем в каждый момент надо сосредоточится на небольшом их количестве, в идеале — на одном. Собственно эта идея и была одной из основ знаменитой «теории ограничений», представленной Голдраттом (см., например [Dettmer2008]).

Мы знаем, что это же правило применимо и к безопасности. В нашем случае оно гласит, что любая система безопасна настолько, насколько безопасна ее самая слабая ее часть. Поэтому бесполезно укреплять и без того уже сильные части, ничего, кроме увеличения затрат на защиту мы получить не сможем.

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

Связаться с автором этой статьи очень просто!

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

Сейчас «хакерское» обеспечение разработки крайне велико. Поэтому дальнейшие вложения в него уже малоэффективны; думаю, это сейчас заметно очень многим и скоро станет очевидным всем. Более того, уменьшение вложений в эту область, даже весьма существенное, не приведет к более или менее заметному ухудшению безопасности. Зато отпадет необходимость привлекать «квалифицированных», «дорогих» специалистов, спрос на них снизится, их цена придет в норму, и появится возможность сотрудничать с действительно квалифицированными хакерами.

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

По моим наблюдениям, разработчики очень быстро осваивают «безопасность». Причем — опять же, по моим наблюдениям — осваивают они ее гораздо быстрее, чем хакеры осваивают разработку; если вы внимательно прочитали предложенную мною аналитику, у вас это наблюдение не должно вызвать удивления, все так и должно быть.

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

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

В противном случае все наши усилия будут бесполезными тратами денег, времени…


Приложения. Об интеллекте

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

Науки об интеллекте

Я думаю, интеллект — самая изучаемая тема в науке.

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

По теме интеллектуальных агентов есть немало литературы. Интересующимся строгими математическими результатами я могу порекомендовать книгу на английском языке «Artificial Intelligence: A Modern Approach» ([Russel2003]). Более популярное описание вы можете найти в (еще) советских книгах: «Оркестр играет без дирижера. Размышления об эволюции некоторых технических систем и управления ими» ( [Varshavsky1984]) и «От амебы до робота: модели поведения» ([Gaaze1987]). Обе книги написаны в соавторстве с нашим ведущим ученым Дмитрием Александровичем Поспеловым и посвящены моделированию поведения как отдельного индивида, так и целых организаций.

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

Конечно, человек — очень специальный «интеллектуальный агент». На данный момент именно он считается самой сложной интеллектуальной системой, и мы до сих пор не можем сказать, что в достаточное мере его изучили. Более того, есть предположения (см., например, [Penrose2003]), что человеческое мышление выходит за рамки модели интеллектуального агента.

Но пока модель интеллектуального агента — лучшая математическая модель.

Интеллектом человека занимаются психологи. Психологи стараются понять устройство нашего мышления по его результатам, по внешнему его проявлению. Мы пока еще многого не понимаем, но результаты этих исследований уже приносят практическую пользу, в частности, существенно улучшая эффективность создаваемых тренингов (см., например, [Foshay2003]). Интересующимся этой темой могу порекомендовать две переводные книги разных авторов, но с одинаковым названием «Когнитивная психология» ([Solso1996] и [Anderson2002]). Кстати, и содержание этих книг очень сильно пересекается, но каждая из них все-же рассматривает тему несколько по-разному. Андерсон обсуждает когнитивную психологию, во-многом, с точки зрения преподавателя; Солсо — с точки зрения специалиста по искусственному интеллекту.

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

Интеллект организации

Не только человек может считаться интеллектуальным агентом.

Очень интересна аналогия группы и отдельного человека. Оказывается, внедрение новой технологии, новой методики работы, нового процесса очень удобно рассматривать, как обучение группы, организации. Здесь можно проследить все те же самые этапы, столкнуться с аналогичными трудностями. Более того, у организации даже можно выделить некоторую аналогию нашего сознания и подсознания (см., например, [Polanyi1958]).

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

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

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

Типы знаний

Очевидно, каждая задача уникальна. Если бы это было не так, мы могли бы найти универсальное решение и пользоваться им все оставшееся время; более того, нам не нужен был бы наш сложный мозг — все реакции, решения можно было бы запрограммировать в «железе».

Тем не менее, задачи можно и нужно классифицировать. В частности, от типа задачи зависит способ обучения методам ее решения.

В настоящее время выделяют два типа интеллектуальных навыков (см., например, [Foshay2003]):

  • декларативные знания;
  • процедурные знания.

Сейчас принято рассматривать эти категории как «почти независимые». Так при разработке тестов или проверочных заданий рекомендуется различать задачи, требующие разных типов навыков и проводить сравнение сложности разных заданий исключительно в рамках одного типа ([Oosterhof2008]). Более того, есть основания считать, что эти знания вообще связаны с использованием различных участков головного мозга (см., например [Berg1999]).

Именно из-за фундаментальности такой классификации имеет смысл подробнее обсудить каждую из этих категорий.

Что мы знаем

Декларативные знания я бы по-другому назвал теоретическими. К этой категории относится следующая информация ([Foshay2003]):

  • факты (2×2=4);
  • концепции, категории предметов (чем табуретки отличаются от стульев);
  • правила (если замерзнешь, то можно заболеть);
  • ментальные модели, объединяющие все эти знания в единый комплекс.

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

В западной традиции обучение начинают с декларативных знаний (см., например, [Anderson2002], [Foshay2003]). Ученикам, слушателям сначала передается информация — декларативные знания, — необходимые им для решения задачи, а уже потом они приобретают опыт ее решения, у них формируются процедурные навыки.

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

Что мы умеем

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

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

Наше поведение при реализации таких действий хорошо описываются моделью ТOTE. Эта модель поведения была предложена еще в 60-е годы прошлого столетия и описана в ставшей сейчас классикой книге «Plans and the Structure of Behavior» ([Miller1960]). Сейчас разработаны более подробные и более точные модели, но TOTE хороша своей простотой и наглядностью, именно за это ее любят практики. Читателей, заинтересовавшихся этой темой, отошлю к отличному сборнику статей «The Cognitive Psychology of Planning» ([Moris2005]), в особенности к вводной его статье ([Ward2005]).

Модель TOTE описывает практически рефлекторную последовательность:

  • наши действия запускаются конкретной текущей ситуацией, вполне определенными внешними или внутренними стимулами;
  • все действия точно запрограммированы;
  • каждое действие заканчивается после достижение предопределенного результата;
  • последовательность действий точно запрограммирована, каждое следующее действие запускается окончанием предыдущего;
  • последовательность заканчивается достижением цели последнего действия.

Все что мы делаем хорошо — мы делаем автоматически. Именно на достижение автоматизма операций направлены тренировки и тренинги. Экспертные умения автоматичны и неплохо описываются уже упомянутой моделью TOTE; поэтому моделировать знания/умения эксперта также можно в форме описания последовательно проверок-действий (см., например, [Dilts1998]).

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

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

Задачи и решения

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

Решение задачи — интеллектуальный навык. Все операции проводятся внутри нашей головы; «операндами» являются наши знания, именно они выступают исходными данными и они же являются результатами решения задачи. Поэтому для моделирования процесса решения задачи удобно использовать результаты исследований в области искусственного интеллекта.

Я не буду подробно описывать все результаты исследований в этой области. Более или менее подробно я затрону только один аспект — полноту определения задачи.

С точки зрения математики задача полностью определяется четырьмя элементами ([Russel2003]):

  • текущим состоянием;
  • критерием достижения цели;
  • возможными действиями;
  • стоимостью действий.

Психологи часто говорят о наличии ограничений на возможные действия (см., например, [Goel1992]). Вероятно, такой подход к определению задачи связан с тем, что именно снятием искусственных ограничений частично объясняют обнаружение «творческих» решений.

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

Я тоже внесу в список небольшие изменений. Будем говорить, что ограничения на возможные действия отражаются в списке этих самых действий, мы просто не упоминаем «запрещенные» элементы; будем также считать, что стоимость действий отражена в их описании.

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

Не всегда все условия задачи определены и хорошо понятны. В связи с этим принято различать хорошо определенные и нечетко поставленные задачи.

Полнота описания и сложность — ортогональные характеристики задачи. Нечетко поставленная задача может оказаться «простой», в то время как хорошо определенная — очень «сложной» (см., например, [Oosterhof2008]).

Полнота описания — очень важная характеристика задачи. Во-многом именно она определяет стратегию ее решения.

Хорошо определенная задача

В хорошо определенной задаче все элементы известны.

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

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

Стратегии решения хорошо определенной задачи удобно изучать на примере планирования маршрута.

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

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

Существует несколько общих стратегий решения этой задачи.

Заинтересовавшихся отошлю к специальной литературе. Кроме уже неоднократно упоминавшихся здесь книг по когнитивной психологии и искусственному интеллекту, могу порекомендовать статью «Planning and problem solving in well-defined domains» ([Davies2005]).

Здесь же мы только обсудим общий подход к решению.

Уменьшение различий

Задачу можно решить простым перебором. Действительно, если мы будем без особых размышлений перебирать все возможные маршруты, мы рано или поздно наткнемся на нужный нам. Есть только одно «но» — количество вариантов может сделать даже самую простую задачу планирования нерешаемой.

Но есть способ сократить пространство перебора. Мы можем последовательно выбирать шаги, которые сокращают дистанцию до нашей цели; если в какой-то момент возможности сделать такой шаг нет, мы должны вернуться на предыдущий этап и произвести новый выбор, уже без учета проверенного варианта. Интересно, что этот метод решения задачи «встроен» в живые существа очень давно, даже птицы демонстрируют подобный способ планирования (см., например, [Anderson2002]).

Иногда этот алгоритм приводит к проблемам. Так известно (опять же, [Anderson2002]), что люди плохо справляются с задачами, в которых надо делать шаги, которые, как кажется, отдаляют нас от цели; к таким задачам относится, например, хорошо известная головоломка о перевозе через реку волка, овцы и капусты — проблемы с планированием последовательности перевозки в первый раз испытывают почти все (ну, может быть, кроме читающих эти строки?).

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

Иерархия целей и подцелей

Взглянем на задачу построение маршрута еще раз. У нас есть хорошо известное текущее состояние — наша точка отправления; у нас есть хорошо определенная цель — наш пункт назначения. И есть несколько городов, которые мы должны последовательно проехать, чтобы в конце концов попасть в пункт назначения.

Искомый маршрут — последовательность промежуточных точек.

Но чтобы достичь каждую из них, нам надо решить отдельную задачу. Так нам нужно решить, каким путем мы будем выбираться из города, в котором находимся сейчас; в каждом промежуточном пункте, возможно, мы должны будем решать, как перебраться с одного шоссе на другое; в конечной «точке», пункте назначения нам надо будет найти путь по этому городу, поскольку наша задача, скорее всего, не город вообще, а конкретное место в нем.

Получается: задача распалась на несколько подзадач. Задача самого высокого уровня — попасть в пункт назначения; подзадачи — последовательный переезд из одного промежуточного пункта в другой.

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

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

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

Похоже, стратегия декомпозиции запрограммирована на уровне «железа» нашего мозга. Оказывается, кора головного мозга человека, которая и «отвечает» за наш интеллект, имеет иерархическое строение, и это строение отражается на нашем мышлении (хороший обзор иерархических моделей решения задач можно найти в [Botvinick2008]).

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

Нечетко поставленная задача

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

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

Конечно, нам кое-что уже понятно. Заинтересовавшихся читателей отошлю к специальной литературе, в частности к хорошей статье «Planning and ill-defined problems» ([Ormerod2005]). Мы же здесь обсудим только один аспект проблемы — две различные стратегии решения таких задач.

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

Исследование

В задаче исследования нечетко определено текущее состояние.

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

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

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

Для решения задач исследования мы используем анализ и индуктивные умозаключения. На основании наших знаний устройства системы и ее наблюдаемого поведения мы формируем гипотезу о причине «неисправности»; затем мы проводим эксперимент, который опровергает или подтверждает нашу гипотезу. Если эксперимент гипотезу подтверждает — задача решена; если нет — мы переходим к формированию новых гипотез.

Конструирование

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

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

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

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

Напомню стратегию решения. Мы берем известные шаблонные решения и «примеряем» их к нашей задаче; если получается — задача решена. Если среди известных шаблонов решение не найдено, мы начинаем их модифицировать, создавать новые шаблоны, надеясь достичь таким образом новых их качеств.

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

Предпочтительная стратегия

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

Конечно, есть комплексные задачи. Для их решения нам необходимо применять как анализ, так и синтез. Но в этом случае мы можем разложить задачу на подзадачи и решать каждую из них по отдельности, используя «чистую» стратегию.

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

Неспециалист использует привычную ему стратегию.

В 1979 году Лоусон (Lawson) провел очень интересный эксперимент. Я читал про этот эксперимент в книге Кросса «Designerly Ways of Knowing» ([Cross2006]); в ней, конечно, есть ссылка на первоисточник ([Lawson1979]).

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

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

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

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

Внимательный читатель, вероятно, заметил, эти выводы во многом перекликаются с идеями из раздела «Негативный перенос».

Мыслительные операции

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

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

Тем не менее, есть операции, которые включают в свой список практически все исследователи. Вот эти операции (см., например [Maklakov2008]):

  • сравнение;
  • абстракция и конкретизация;
  • анализ и синтез;
  • индукция и дедукция.

Я хочу обратить внимание только на последние две пары.

Анализ и синтез

Анализ и синтез — взаимно обратные операции.

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

В ходе синтеза мы «собираем» объект из частей. Мы комбинируем отдельные части с известными нам свойствами, организуем их взаимодействие; таким образом мы создаем новый объект, возможно, обладающий новыми качествами. Подобно анализу, синтез может быть как реальным, так и мысленным.

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

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

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

Дедукция и индукция

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

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

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

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

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

Вся математика основывается на дедуктивных рассуждениях. Мы начинаем с фактов, которые признаем бесспорно истинными — аксиом; затем мы применяем правила преобразования и получаем остальные факты — теоремы. Откуда берутся аксиомы — «бесспорные истины» — остается большой загадкой математики.

Еще раз. При дедуктивном рассуждении мы работаем с достоверными фактами, мы имеет достоверные факты на входе — мы имеем достоверные факты на выходе.

Теперь перейдем к индукции.

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

Пример задачи на индукцию из теста на IQ. Вам предлагается продолжить следующий ряд чисел: 2,4,6… (ну это очень простой тест, для школьников). Мы можем предположить, что эти числа — начала ряда, состоящего из четных чисел; тогда его продолжение будет: …8, 10, 12…

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

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

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

Дедуктивная и индуктивная стратегия очень сильно различаются психологически. Каждая из них формирует свое «ощущение», интуитивное понимание «истины»; и это понятно, ведь дедуктивная «истина» абсолютна, а индуктивная — вероятностна. Конечно, мы так или иначе применяем обе этих стратегии, более того, есть люди, которые легко переключаются между ними и легко используют наиболее подходящую из них. Тем не менее, очень часто люди предпочитают использовать одну из этих стратегий (см., например, [Hall2007]).

Похоже, индукция более естественна для человека.

Дедукция основана на применении абстрактных правил. В отличие от индукции, которая основана на манипуляциях с относительно конкретными «предметами», при дедуктивных операциях мы имеем дело с «категориями».

Необходимость работать с абстрактными понятиями усложняет задачу. Люди хорошо оперируют с чем-то конкретным, и не очень хорошо с обобщениями. Поэтому, например, школьников учат сложению сначала на примере простых предметов — яблок, счетных палочек и подобных, — и только потом рассказывают про правила сложения, вычитания; не прошедшие обучение дети хорошо оперируют количеством предметов, но плохо — абстрактными числами ([Anderson2002], [Solso1996]).

Для взрослого человека абстракции также представляют сложностью. Так при разработке тестовых заданий необходимость оперировать абстрактными понятиями считается усложнением задачи ([Oosterhof2008]).

Частая ошибка в логики — замена дедукции индукцией. Даже достаточно образованные люди решая задачи, где надо применить правило — воспользоваться дедуктивной логикой, — стараются применить индуктивные выводы ([Anderson2002]).

Думаю, именно дедуктивность делает математику такой трудной дисциплиной.

Неявные знания и обучение

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

Но это не единственная возможность.

Сейчас мы знаем о существовании неявных знаний. Одним из первых на западе об этом стал говорить Полани ([Polanyi1958]).

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

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

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

Вспомним понятия «научная школа». Я думаю, его существование связано именно с передачей неявных знаний. Ученый (впрочем, как и инженер, хакер, медик…) за годы своей работы приобретает огромный опыт; часть этого опыта он хорошо — или не очень хорошо — осознает и может явно передать, например, написав книгу; другая, больша́я (я думаю, даже, бо́льшая) часть этого опыта остается неосознанной. Молодой сотрудник, работая с Мастером, перенимает у него весь его опыт, в том числе и неосознанный; при этом он и сам не осознает полученные знания.

У этого процесса есть и обратная сторона. Если мы не осознаем наши знания, не осознаем методы, которые применяем, мы не можем оценить их эффективность в контексте решаемой нами сейчас задачи. Ситуация могла сильно измениться, а мы этого могли не заметить; в результате мы стараемся со все большим упорством применять эффективные «когда-то» методы. Говорят, научная школа умирает вместе с последним ее представителем.

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

Кстати, один из способов сделать это — прочитать спецкурс для студентов.

Литература

[Aleph1996]Aleph One «Smashing The Stack For Fun And Profit» Phrack Magazine, Vol. 7, No. 49., 1996
[Alexander1977]Christopher Alexander, Sara Ishikawa, Murray Silverstein «A Pattern Language: Towns, Buildings, Construction», Oxford University Press, 1977
[Altshuller2007]Генрих Альтшуллер «Найти идею: Введение в ТРИЗ — теорию решения изобретательских задач», Альпина Бизнес Букс, 2007, ISBN 978-5-9614-0534-7
[Anderson2002](1, 2, 3, 4, 5, 6, 7, 8, 9) Джон Р. Андерсон «Когнитивная психология», Издательство «Питер», 2002, ISBN 5-272-00216-4
[Babbin2006]Jacob Babbin, Dave Kleiman, Everett F. Carter Jr. «Security Log Management: Identifying Patterns in the Chaos», Syngress, 2006, ISBN 978-1597490429
[Bejtlich2004]Richard Bejtlich «The Tao of Network Security Monitoring: Beyond Intrusion Detection», Addison-Wesley Professional, 2006, ISBN 978-0321246776
[Barroca2000]Leonor Barroca, Jon Hall, Patrick Hall «Software architectures - advances and applications», Springer, 2000
[Bass2001]Len Bass, Bonnie E. John, Jesse Kates «Achieving Usability Through Software Architecture» Technical Report, Software Engineering Institute, CMU/SEI-2001-TR-005 ESC-TR-2001-005, 2001
[Bass2012](1, 2, 3, 4, 5, 6) Len Bass, Paul Clements, Rick Kazman «Software Architecture in Practice», Addison-Wesley Professional, SEI Series in Software Engineering, 3 edition, 2012
[Berg1999](1, 2) Timon Ten Berge, Rene Van Hezewijk, «Procedural and declarative knowledge; An evolutionary perspective.» Theory and Psychology, 1999, Vol. 9(5)
[Bishop2003](1, 2, 3) Matt Bishop "Computer Security: Art and Science", Addison-Wesley Professional, 2003, ISBN 0-201-44099-7
[Blakley2004]Bob Blakley, Craig Heath and others «Security Design Patterns», Technical Guide, The Open Group, 2004, ISBN 1-931624-27-5
[Bono1997]Эдвард де Боно «Шесть шляп мышления», Питер Пресс, 2006, ISBN 5-88782-227-9
[Botvinick2008](1, 2, 3) Matthew M. Botvinick «Hierarchical models of behavior and prefrontal function» Trends in Cognitive Sciences, Volume 12, Issue 5, p171-208, May 2008
[Braude2000]Eric J. Braude «Software Engineering: An Object-Oriented Perspective» Wiley Publishing, 2000, ISBN 978-0471322085
[CEMv3.1]«Common Methodology for Information Technology Security Evaluation. Evaluation methodology», Common Criteria, 2012, version 3.1 revision 4
[Cranor2005]Lorrie Faith Cranor, Simson Garfinkel «Security and Usability Designing Secure Systems that People Can Use», O'Reilly Media, 2005, ISBN 978-0-596-00827-7
[Cross2006]Nigel Cross «Designerly Ways of Knowing», Springer-Verlag London, 2006, ISBN 978-1-84628-300-0
[Davies2005]Simon P. Davies «Planning and problem solving in well-defined domains» в сборнике Robin Morris, Geoff Ward «The Cognitive Psychology of Planning», Psychology Press, New York, 2005
[Dettmer2008]Уильям Детмер «Теория ограничений Голдратта. Системный подход к непрерывному совершенствованию» Альпина Паблишер, 2008, ISBN 978-5-9614-0889-8
[Devaytin2005]Девятин П.Н. «Модели безопасности компьютерных систем», Издательский центр «Академия», 2005, ISBN 5-7695-2053-1
[Dijkstra1968]E.W. Dijkstra «A Constructive Approach to the Problem of Program Correctness», Kluwer Academic Publishers Volume 8, Number 3, 1968
[Dilts1998](1, 2) Роберт Дилтс «Стратегии гениев», Издательство «Класс», 1998, три тома ISBN 5-86375-071-5, ISBN 5-86375-072-3, ISBN 5-86375-073-1
[Dougherty2009]Chad Dougherty, Kirk Sayre, Robert C. Seacord, David Svoboda, Kazuya Togashi «Secure Design Patterns», Technical Report, Software Engineering Institute, CMU/SEI-2009-TR-010, 2009
[Druginin1999](1, 2) В.Н. Дружинин «Психология общих способностей», Издательство «Питер», Санкт-Петербург, 1999, ISBN 5-314-00121-7
[Engle2008]Sophie Engle, Sean Whalen, Matt Bishop «Modeling Computer Insecurity», Technical Report, Department of Computer Science, University of California, Davis, 2008
[Falessii2015]Davide Falessi, Philippe Kruchten «Five Reasons for Including Technical Debt in the Software Engineering Curriculum», Proceedings of the 2015 European Conference on Software Architecture Workshops
[Fenton2012]Norman Fenton, Martin Neil «Risk Assessment and Decision Analysis with Bayesian Networks» CRC Press, 2012, ISBN 978-1439809105
[Foshay2003](1, 2, 3, 4) Wellesley R. Foshay, Kenneth H. Silber, Michael Stelnicki «Writing Training Materials That Work: How to Train Anyone to Do Anything», Pfeiffer, 2003, ISBN 0-7879-6411-5
[Gaaze1987]М.Г. Гаазе-Рапопорт,Д.А. Поспелов «От амебы до робота: модели поведения», Главная редакция физико-математической литературы, Издательство «Наука», Москва, 1987
[Gamma1995]Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides «Design Patterns: Elements of Reusable Object-Oriented Software» Addison-Wesley Longman Publishing Co., Inc., Boston, MA, USA, 1995
[Gero1990]John S. Gero «Design Prototypes: a knowledge representation schema for design», American Association for Artificial Intelligence, AI Magazine Volume 11 Issue 4 Pages 26-36, 1990
[Gero2000]John S Gero, Udo Kannengiesser «Towards a Situated Function-Behaviour-Structure Framework as the Basis of a Theory of Designing» в сборнике T. Smithers (ed.), Workshop on Development and Application of Design Theories in AI in Design Research, Artificial Intelligence in Design’00, 2000
[Gero2002]John S Gero, Udo Kannengiesser «The Situated Function – Behaviour – Structure Framework» Artificial Intelligence in Design ’02, Springer Netherlands, 2002
[Goel1992]Vinod Goel «Comparison of Well-Structured & Ill-Structured Task Environments and Problem Spaces» Proceedings of the Fourteenth Annual Conference of the Cognitive Science Society, Indiana University, 1992
[Grusho1996]Грушо А.А., Тимонина Е.Е. «Теоретические основы защиты информации», издательство «Яхтсмен», 1996, ISBN 5-86071-064-X
[Grusho2000]Грушо А.А., Тимонина Е.Е., Применко Э.А. «Анализ и синтез криптоалгоритмов», Изд-во Марийского филиала Московского открытого социального университета, 2000, ISBN 5-7590-0871-4
[Haken2001](1, 2) Герман Хакен «Принципы работы головного мозга. Синергетический подход к активности мозга, поведению и когнитивной деятельности», издательство «ПЕР СЭ», 2001, ISBN 5-9292-0047-5
[Hall2007](1, 2) Майкл Холл «51 метапрограмма НЛП. Прогнозирование поведения», издательство Прайи-ЕВРОЗНАК, 2007, ISBN 5-93878-198-1
[Hoglund2004](1, 2) Greg Hoglund, Gary McGraw «Exploiting Software: How to Break Code», Addison-Wesley Professional, 2004, ISBN 0-201-78695-8
[Howard2009]Michael Howard, David LeBlanc, Jonh Viega, «24 Deadly Sins of Software Security. Programming Flaws and How to Fix Them», McGraw-Hill/Osborne, 2009
[Humphrey2006]Watts S. Humphrey «Introduction to the Team Software Process», Addison Wesley, 2006, ISBN 978-0-201-47719-1
[Humphrey2007]Watts S. Humphrey «PSP. A Self-Improvement Process fo Software Engineers», Addison Wesley, 2007, ISBN 978-0-321-30549-7
[Izard2000]Каррол Э. Изард «Психология эмоций» Питер, 2000, ISBN 5-314-00067-9
[Johnson2012]Pontus Johnson, Mathias Ekstedt, and Ivar Jacobson «Where’s the Theory for Software Engineering?» IEEE Software, vol.29, no. 5, pp. 96, Sept.-Oct. 2012
[Jorgensen2008]Paul C. Jorgensen «Software Testing: A Craftsman's Approach», Auerbach Publications; 3 edition, 2008, ISBN 0-8493-7475-8
[Kahneman2005]Дэниел Канеман, Пол Словик, Амос Тверский «Принятие решений в неопределенности: Правила и предубеждения» Гуманитарный центр, Харьков, 2005, ISBN 966-8324-14-5
[Kaner2008]Cem Kaner «A Tutorial in Exploratory Testing», QAI QUEST Conference, Chicago, April 2008
[Kholodnay2004](1, 2) М. А. Холодная «Когнитивные стили. О природе индивидуального ума», Питер, 2004, ISBN 5-469-00128-8
[Kienzle2003]Darrell M. Kienzle, Matthew C. Elder, David Tyree, James Edwards-Hewitt, «Security Patterns Repository Version 1.0», 2003(?)
[Kruchten2005]Philippe Kruchten «Casting Software Design in the Function-Behavior-Structure Framework» IEEE Software Volume:22, Issue: 2, 2005
[Kruchten2012]Philippe Kruchten, Robert L. Nord, Ipek Ozkaya «Technical Debt: From Metaphor to Theory and Practice» IEEE Software, vol.29, no. 6, pp. 18-21, Nov.-Dec
[Kruger1999]Justin Kruger, David Dunning «Unskilled and unaware of it: How difficulties in recognizing one's own incompetence lead to inflated self-assessments» Journal of Personality and Social Psychology, Vol 77(6), Dec 1999
[Libin2000]А.В. Либин «Дифференциальная психология. На пересечении европейских, российских и американских традиций», Издательство «Смысл», Москва, 2000, ISBN 5-89357-068-5
[Linger1996]Richard C. Linger «Cleanroom Software Engineering Reference Model» Technical Report CMU/SEI-96-TR-022, Software Engineering Institute, 1996
[Lawson1979]Bryan R. Lawson «Cognitive Strategies in Architectural Design» Ergonomics, Volume 22, Issue 1, 1979
[Maklakov2008]А.Г. Маклаков «Общая психология», Питер, 2008, ISBN 978-5-272-00062-0
[Masters2002]Marick F. Masters, Robert R. Albright «The Complete Guide to Conflict Resolution in the Workplace», AMACOM, 2002, ISBN 978-0814417188
[McConnell1996]Steve McConnell «Rapid Development: Taming Wild Software Schedules», Microsoft Press, 1996, ISBN 978-1556159008
[McConnell2004]Steve McConnell «Code Complete: A Practical Handbook of Software Construction», Microsoft Press; 2nd edition 2004, ISBN 978-0735619678
[McDonald2007]Marc McDonald, Robert Musson, Ross Smith «The Practical Guide to Defect Prevention (Best Practices)», Microsoft Press, 2007. ISBN 0735622531
[Miller1960]George a. Miller, Eugene Galanter, Karl H. Pribram «Plans and the Structure of Behavior» Martino Fine Books, Reprint 2013, ISBN  978-1614275206
[Monin2003](1, 2) Jean-Francois Monin «Understanding Formal Methods», Springer, 2003, ISBN 978-1-85233-247-1
[Moris2005]Robin Morris, Geoff Ward «The Cognitive Psychology of Planning», Psychology Press, New York, 2005
[Oosterhof2008](1, 2, 3, 4) Albert Oosterhof, Faranak Rohani, Carol Sanfilippo, Peggy Stillwell, Karen Hawkins «The Capabilities-Complexity Model» Center for Advancement of Learning and Assessment Florida State University, 2008
[Ormerod2005](1, 2) Thomas C. Ormerod «Planning and ill-defined problems» в сборнике Robin Morris, Geoff Ward «The Cognitive Psychology of Planning», Psychology Press, New York, 2005
[Patriotta2003]Gerardo Patriotta «Organizational Knowledge in the Making: How Firms Create, Use, and Institutionalize Knowledge», Oxford University Press, 2003 ISBN 0199256780
[Pellerin2009](1, 2) Charles J. Pellerin «How NASA Builds Teams: Mission Critical Soft Skills for Scientists, Engineers, and Project Teams», John Wiley&Sons, 2009, ISBN 9780470456484
[Penrose2003]Роджер Пенроуз «Новый ум короля. О компьютерах, мышлении и законах физики», УРСС, Москва, 2003, ISBN 978-5-354-00005-0
[Pohl2010](1, 2, 3) Klaus Pohl «Requirements Engineering: Fundamentals, Principles, and Techniques», Springer; 2010 edition, 2010
[Polanyi1958](1, 2) М. Полани «Личностное знание. На пути к посткритической философии» Перевод с английского, БГК Им. И. А. Бодуэна Де Куртенэ, 1958. ISBN 5-8015-7123-X
[Polya1975]Джордж Пойа, «Математика и правдоподобные рассуждения», Пер. с англ., 2-е изд.испр., Глав. ред. физ-мат. лит., 1975
[Ralph2010]Paul Ralph «Fundamentals of Software Design Science», Ph.D. Thesis, The University of British Columbia, Vancouver, 2010
[Ralph2013]Paul Ralph «Possible core theories for software engineering» 2nd SEMAT Workshop on a General Theory of Software Engineering (GTSE), IEEE, 2013
[Russel2003](1, 2) Stuart Russell, Peter Norvig «Artificial Intelligence: A Modern Approach» Prentice Hall, 2nd edition, 2003 ISBN 0-13-080302-2
[Saaty1989]Саати Т. Л. «Принятие решений. Метод анализа иерархий» Радио и связь, 1989
[Saltzer1975]Jerome H. Saltzer, Michael D. Schroeder «The Protection of Information in Computer Systems», Proceedings of the IEEE, 1975
[SEI2008]Julia H. Allen, Sean Barnum, Robert J. Ellison, Gary McGraw, Nancy R. Mead «Software Security Engineering: A Guide for Project Manager», Addison-Wesley Professional, 2008, ISBN 978-0321509178
[Shaw2006]Mary Shaw, Paul Clements «The Golden Age of Software Architecture:A Comprehensive Survey», IEEE Software Volume 23 Issue 2, March 2006 Page 31-39
[Smith2002]Ричард Э. Смит «Аутентификация. От паролей до открытых ключей», Вильямс, 2002, ISBN 5-8459-0341-6
[Schneier2002]Брюс Шнайер «Прикладная криптография. Протоколы, алгоритмы, исходные тексты на языке Си», Триумф, 2002, ISBN 5-89392-055-4
[Solso1996](1, 2, 3) Роберт Л. Солсо «Когнитивная психология» Издательство «Тривола» 1996, ISBN 5-88415-024-5
[Srivatanakul2004]Thitima Srivatanakul, John A. Clark, Fiona Polack «Making Penetration Testing Work: Adding Rigour to Flaw Hypothesis Method» Unpublished, 2004
[Srivatanakul2005]Thitima Srivatanakul,John A. Clark, Fiona Polack «Stressing Security Requirements: Exploiting the Flaw Hypothesis Method with Deviational Techniques» Symposium on Requirements Engineering for Information Security in conjunction with RE 05 - 13th IEEE International Requirements Engineering Conference, Paris, August 2005
[Sterling2010]Chris Sterling «Managing Software Debt: Building for Inevitable Change», Addison-Wesley Professional, Agile Software Development Series, 2010, ISBN 978-0321554130
[Taylor2009](1, 2) R. N. Taylor, N. Medvidovic, and E. M. Dashofy. «Software Architecture: Foundations, Theory, and Practice». Wiley Publishing, 2009.
[TCSEC1985]«Trusted Computer System Evaluation Criteria» DoDD 5200.28-STD
[Tian2005]Jeff Tian «Software Quality Engineering: Testing, Quality Assurance, and Quantifiable Improvement», Wiley-IEEE Computer Society Press, 2005, ISBN 978-0-471-71345-6
[Varshavsky1984]В.И. Варшавский, Д.А. Поспелов «Оркестр играет без дирижера. Размышления об эволюции некоторых технических систем и управления ими», Главная редакция физико-математической литературы, Издательство «Наука», Москва, 1984
[Vedenov1988]А.А. Веденов «Моделирование элементов мышления», Издательство «Наука», Москва, 1988
[Ward2005]Geoff Ward, Robin Morris «Introduction to the psychology of planning», в сборнике Robin Morris, Geoff Ward «The Cognitive Psychology of Planning», Psychology Press, New York, 2005
[Weissman1973]C. Weissman. «System Security Analysis/Certification Methodology and Results.» Technical report, SDC SP- 3728, 1973.
[Weissman1995a]Clark Weissman «Penetration Testing», Chapter 10 in «Handbook for the Computer Security Certification of Trusted Systems», NRL Technical Memorandum 5540:082A, Naval Research Laboratory, 1995
[Weissman1995b]Clark Weissman «PenetrationTesting», Essay 11 in «Information Security: An Integrated Collection of Essays», IEEE Computer Society Press, Los Alamitos, CA, USA, 1st edition, 1995
[Whittaker2003]James A. Whittaker, Hugh Thompson «How to Break Software Security», Addison Wesley, 2003, ISBN 978-0321194336
[Wiegers2010]Kar E. Wiegers «Peer Reviews in Software. A Practical Guide» Addison Wesley, 2010, ISBN 978-0-321-49266-1
[Wujec1997]Том Вуджек «Как создать идею», Питер Пресс, 1997 ISBN 5-88782-190-6, 0-385-41462-5

Модифицировано:

Вопросы, мнения?

— адрес электронной почты не проверяется и никому, кроме меня, не показывается
— для форматирования комментариев можно использовать markdown
— для отслеживания комментариев на этой странице можно подписаться на atom feed

Совет

Есть вопросы по системе комментирования?

Вам сюда!