Сегодня мы поговорим о причинах появления ошибок в коде. Ниже я попытаюсь объяснить, что видит и думает реверс-инженер, когда анализирует чужую программу. Спойлер: вы смиритесь, что ошибки неизбежны.
Неоправданные ожидания
Ошибки в коде — прямое следствие гонки за эффективностью. В коммерческой разработке код пишут ради денег. Само собой, чем «эффективнее» программисты это делают, тем больше зарабатывает компания. При этом 95% кода, который создается сейчас, уже был кем-то и где-то написан. Отсюда возникает еще одна проблема — копипаст. На Stack Overflow можно найти массу кода и использовать его в своих проектах. Да, это удобно и быстро, но вы не же станете брать материал для научной работы из Википедии... Кроме того, разработчики зачастую копируют куски своего же кода из одной части программы в другую, после чего в ней возникают всевозможные сайд-эффекты.
На все эти нюансы накладываются неоправданные ожидания бизнеса:
- Разработчик должен быть недорогим.
- Он должен быстро писать код.
- Код должен быть легким в плане чтения и тюнинга. Все должно работать правильно и требовать минимума ресурсов.
Прежде чем вы начнете смеяться и позовете посмеяться коллег, поясню: само собой, все работает не совсем так. В большинстве случае приходится выбирать — либо качественно, либо быстро, либо дешево. В первую очередь бизнес готов поступиться ресурсами, потому что, если у пользователя все тормозит, это, как известно, проблема пользователя :) Идем дальше: писать юнит-тесты — дорого и медленно, поэтому многие стремятся снизить их количество. Также код можно сделать не очень модифицируемым: «Почему бы и нет, все равно ведь работает». Наконец, все мы понимаем, что недорогой разработчик, способный писать хороший код, — это миф. Если он не допускает ошибок, скорее всего, вы просто их не нашли.
От трех ожиданий обычно остается одно: программист должен быстро писать код, который решает поставленную задачу. В нем есть ошибки, его неудобно читать и модифицировать? Это вторично, главное — скорость. Как ни странно, в этом есть логика: выводя на рынок новое решение, компания в первую очередь стремится занять нишу, а уже потом начинает думать о качестве и безопасности продукта.
Как бороться с ошибками
Mitigation, или снижение возможности ошибки. Возьмем, к примеру, метод stack canaries. Суть в том, чтобы обнаружить разрушение стека и не дать неправильно написанной программе выполнить недопустимые действия. «Канарейка», конечно, не спасет от падений ПО, но помешает атакующему сделать что-то критичное . Или другой пример — аппаратные решения Data Execution Prevention и Control-Flow Enforcement.
С одной стороны, эти подходы не сокращают количество ошибок в коде. С другой — они реализуются в компиляторе или на аппаратном уровне, поэтому с точки зрения разработчиков идут for free. Даже если люди будут допускать столько же ошибок, как и раньше, ПО станет безопаснее.
Автоматизированный поиск ошибок. Разработчики давно поняли, что поиск ошибок в коде нужно автоматизировать, и начали создавать подобные инструменты. Уже в 1978 г. появился Lint — статический анализатор для С, который сообщал о подозрительных или не переносимых на другие платформы выражениях. Сегодня на рынке полно технологий, которые помогают выявлять дыры в коде на ранних этапах разработки. Например, SAST (static application security testing), DAST (dynamic application security testing) или IAST (interactive application Security Testing).
Правильный выбор языка. На мой взгляд, не бывает хороших и плохих языков. Главное — правильно подбирать решение под конкретную задачу. Можно начать небольшой проект на Haskell, но, если бизнес захочет быстро нарастить объемы и нанять еще десять разработчиков, пишущих на этом языке, вы их просто не найдете.
Другой пример: С заточен на быстродействие, а Java фокусируется на устойчивости и безопасности. Первый позволяет не писать свой обработчик исключения, а для второго это обязательно. Java подразумевает, что разработчик должен прописывать весь возможный контроль, — это заложено в саму концепцию языка, поэтому он хорошо подходит для разработки устойчивого корпоративного софта.
Изучение ООП до изучения ООП. Звучит странно, пока не расшифруем аббревиатуры. ООП — это не только объектно-ориентированное программирование, но и объектно-ориентированное проектирование. Разработчикам дали мощный инструмент в виде объектно-ориентированного программирования, но не научили правильно строить структуры классов. Некоторых посещают светлые мысли: «Все функции, у которых разные наборы аргументов, я назову одинаково! У меня всегда будет вызываться функция с одним и тем же именем, и программа будет работать». Идея полиморфизма в том, что имплементация скрыта в момент использования, но две функции с идентичным названием должны делать одно и то же. Если при этом первая принимает один аргумент, а вторая два, это странное решение. Люди часто допускают ошибки, потому что не знают или забывают, как нужно использовать логику языка.
Перестраховка. Не буду подробно останавливаться на defensive programming и secure coding (подходы, в которых вы пытаетесь предусмотреть вообще все), лучше расскажу про secure execution. В своей практике я встречал два вида его реализации. Первый достаточно прост: программа одновременно выполняется на трех вычислителях, и результаты их работы сравниваются между собой. Если совпадут хотя бы два, считается, что мы получили решение на очередном шаге.
Второй подход мне нравится больше — я встречал его в проектах, связанных с безопасностью железнодорожной автоматики. Существует специализированный язык Sternol, заточенный на проверку отсутствия конфликтов при взаимодействии группы дискретных устройств. В частности, на нем описывают топологию железнодорожных элементов: путей, семафоров, стрелок и др. Компилятор Sternol генерирует два набора правил, которые проверяют, не возникнет ли вероятность столкновения поездов после изменения состояния автоматики. Далее берутся две разные аппаратные платформы, например PowerPC и ARM. Разные компиляторы используются для сборки кода от препроцессора Sternol под эти платформы. Получившиеся программы независимо проверяют состояния и выдают ответ: безопасно решение о переключении автоматики или нет. Если да, стрелка или семафор переключаются. В противном случае ничего не происходит — поезда могут остановиться (но точно не столкнутся). В привычной нам корпоративной среде настолько параноидальные подходы используются редко, потому что это слишком дорого.
SSDLC (secure software development lifecycle). Это концепция разработки, еще один набор правил и техник, следуя которым можно снизить вероятность попадания ошибок в релиз.
«Клиент никуда не денется»
Почему совсем избавиться от ошибок невозможно? Приведу простой пример. Еще в 1996 г. вышла книга на 368 страниц, посвященная одной операционной системе и одной задаче — разработке многопоточных приложений. Сегодня их пишут все, но, чтобы правильно сделать это хотя бы на одной ОС, нужно изучить почти 400 страниц текста. Само собой, проверить статическим анализом факт неправильного использования потока невозможно, поскольку это динамика. Но и динамический анализ не гарантирует, что программа не содержит логических ошибок, а все потоки правильно синхронизированы. Подобные проверки нельзя автоматизировать: пока программист не будет на 146% понимать, что он делает, ошибок не избежать.
Аналогичные сложности возникают и с криптографией. Чем они отличаются от других ошибок в коде? Тем, что криптографию не видно. Если отпустить неправильно работающую программу, она упадет. Если плохо написать криптографию, зашифрованные данные кто-то сможет расшифровать, но программа при этом будет работать.
Еще одна причина связана со спецификой крупного бизнеса, а точнее, с реакцией больших компаний на обнаруженные уязвимости. На одной из зарубежных конференций ИБ-эксперт Siemens сказал мне: «Ваша компания присылает много репортов. Они классные, и мы все понимаем. Но и вы ведь понимаете, что исправления выходят только через год…» Я знаю только две компании, которые быстро латают дыры: Microsoft и Apple. В остальных случаях схема выглядит примерно так:
- Компания получает репорт об ошибке, разбирает его и сообщает в разработку, что нужно исправить.
- Примерно через месяц разработка отвечает, что сейчас они загружены релизом.
- Когда заканчивается очередной цикл, разработка говорит, что в приоритете составленный бизнесом бэклог, поэтому другие задачи на следующий цикл они не возьмут.
- Спустя еще один цикл специалисты наконец-то берут кейс в разработку, выпускают исправленную версию, и вендор публикует патч. Как правило, примерно через год после первого сообщения об уязвимости.
Более того, однажды я услышал от вендора следующее: «Ну да, уязвимость, и что? Клиенты не сбегут, потому что перестройка бизнеса на другой продукт обойдется дороже, чем ущерб от уязвимости. Клиент посчитает деньги и никуда не денется, а значит, причин волноваться нет».
Также отмечу, что в большинстве лицензионных соглашений прописано что-то из серии: «Если хотите сделать реверс-инжиниринг нашего продукта, вам нельзя». Особенно хитрые вендоры пользуются нашей политикой responsible disclosure, согласно которой мы не раскрываем информацию об уязвимостях до их устранения. Если мы найдем дыру в коде и в тот же день опубликуем детали, злоумышленники сразу начнут ее эксплуатировать. В итоге пострадают клиенты, поэтому мы всегда ждем патчей. Но с точки зрения производителя ситуация выглядит примерно так: «Пока мы не выпустим фикс, об уязвимости никто не узнает, значит, мы защищены». В итоге патчи просто не выходят.
В заключение, так уж заведено, нужно ответить на вопрос «Как жить дальше?». Скажу честно: меня как реверсера все устраивает — чем больше ошибок, тем интереснее моя работа :) А если серьезно, чтобы получить хорошее ПО, менеджерам нужно научиться жертвовать эффективностью. Одними технологиями проблему ошибок в коде не решить, нужно инвестировать в обучение специалистов и культуру разработки в целом. Программисты же, в свою очередь, должны понимать, зачем им писать хороший код и как это делать. И наконец, рекомендую присоединиться к нашему сообществу — это существенно облегчит вам жизнь.