Как усложнить взлом вашего приложения.

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

Будьте осторожны при работе с памятью

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

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

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

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

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

Вот пример фрагмента кода, уязвимого для атаки переполнения буфера:

char text[256];

scanf("%s", text);

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

Это очевидный пример уязвимости, однако он не всегда так очевиден.

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

Всегда дезинфицируйте пользовательский ввод

Это само собой разумеется. Никогда не доверяйте пользователю. Период.

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

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

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

Не сообщайте об ошибках пользователю

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

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

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

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

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

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

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

Не храните конфиденциальные данные в виде обычного текста

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

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

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

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

Не храните конфиденциальные значения в системе контроля версий

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

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

Ограничение использования отражения

Что такое отражение? Рефлексия — это способность программы получать доступ и/или изменять свою внутреннюю структуру или поведение.

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

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

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

Не доверяйте строковым форматам

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

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

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

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

Почему безопасное программирование имеет значение

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

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

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

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

Пример того, как складываются уязвимости

В качестве примера возьмем ситуацию, с которой я столкнулся в прошлом.

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

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

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

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

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

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

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

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

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

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

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

Повышение уровня кодирования

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу