Грехи C++

Kate

Administrator
Команда форума
image


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

Давайте рассмотрим некоторые распространенные угрозы безопасности, которые могут таиться в нашем коде C/C++.

Эта статья представляет собой адаптированную версию презентации Мэри Келли при поддержке Embarcadero.

Мэри — опытный разработчик приложений с подтвержденным опытом работы в индустрии компьютерного программного обеспечения. Имеет опыт в C++, Delphi, базах данных, предпродажной подготовке и техническом писании. Сильный инженер со степенью бакалавра физики Университета штата Айова. Ее профиль на Linkedin и в других блогах на Embarcadero.

Что такое безопасность программного обеспечения​


Чтобы заложить основу для нашего сегодняшнего обсуждения, давайте взглянем на определение безопасности:

Согласно Techopedia:

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

Важность безопасности программного обеспечения​


  • Меньше вероятность взлома данных
  • Безопасность клиентов
  • Репутация
  • Вопросы соответствия/Нормативно-правовая база/Закон
  • Возможная потеря дохода
  • Легче поддерживать


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

Переполнение буфера​


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

Кратко:

  • у вас есть буфер размером N
  • вы получаете некоторые входные данные размера M
  • вы записываете данные в буфер, не проверяя размер, если M < N.

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

helloworldthisisfirst28charsrundll


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

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

Ниже приведен фрагмент типичного «старого» переполнения буфера:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main() {
char password[28];
char otherImportantBuffer[100] = { 'a'};
printf("Enter your password: ");
scanf("%s", password);
printf("your secret: %s\n", password);
}


Попробуйте передать более 28 символов.

В лучшем случае вы получите серьезный сбой или необработанную исключительную ситуацию. Но есть вероятность, что буфер «съест» часть памяти.

К счастью, такой код сложно скомпилировать даже на современных компиляторах! Это связано с тем, что различные «безопасные» альтернативы таким функциям, как scanf, gets или strcpy, требуют, чтобы вы передавали длину.

Вот несколько исправлений при переполнении буфера:

  • Используйте новейшие компиляторы и библиотеки — они предлагают обновленные исправления безопасности и наиболее безопасную версию используемых вами функций.
  • Используйте стандартную библиотеку C++ и STL
  • Используйте библиотеки, которые проверяют границы
  • Для переполнения буфера существует популярный метод, называемый нечетким тестированием. Fuzz-тестирование, или фаззинг, как его называют во многих кругах, — это метод тестирования, с помощью которого вы проверяете свои входные данные с помощью сгенерированных полурандомизированных значений, помогающих обеспечить стабильность и производительность ваших приложений. Я упомянул одну библиотеку фаззинга, которую я использую, под названием libFuzzer.


А вот отличное объяснение Heartbleed — страшная ошибка в OpenSSL, затронувшая миллионы пользователей:
.

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

Проблемы с форматированием строк​


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

void vulnerable() {
char buffer[60];
if (fgets(buffer, sizeof (buffer), stdin) == NULL)
return;
printf(buffer);
}
void notVulnerable () {
char buffer[60];
if (fgets(buffer, sizeof (buffer), stdin) == NULL)
return;
printf ("%s", buffer);
}


Какая функция безопаснее?

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

Рекомендуемые правки:

  • Не передавайте вводимые пользователем данные непосредственно в качестве строки формата в функции форматирования.
  • Используйте строки фиксированного формата или строки формата из надежного источника
  • Следите за предупреждениями и ошибками компилятора
  • Если необходимо использовать строки формата, используйте: printf («%s», user_input)
  • Еще лучше не использовать семейство функций printf. Используйте потоковые операции, такие как std::cout или std::format (C++ 20) — они безопасны для типов.


Целочисленные переполнения​


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

Вы можете сделать несколько простых правок:

  • Изучите и поймите свой код. Поработайте немного с математикой!
  • Проверьте все используемые вычисления, чтобы убедиться, что выделенная память и индексы массивов не могут переполниться.
  • Используйте беззнаковые переменные для смещений массива и размеров для выделения памяти
  • Обратите внимание на предупреждения вашего компилятора
  • Проверьте наличие усечения и проблем с подписью при работе с size_t
  • Опять же, C++ 20 улучшает функциональность с помощью функций безопасного интегрального сравнения в C++ 20.


Массив new и delete​


Когда вы пишете что-то новое в своих приложениях, вы создаете неуправляемые объекты, а затем вам необходимо вызывать удаление, если вы не хотите рисковать утечками. Так что вообще не используйте new и delete, так как это считается плохой практикой в C++. Более того, работа в современном C++ позволяет использовать интеллектуальные указатели и классы контейнеров стандартной библиотеки, которые упрощают сопоставление каждого new с delete.

См. Рекомендации C++ Core — R.11: Избегайте явного вызова new и delete.

Плохая обработка ресурсов​


В C ++ конструктор копирования вызывается, когда из объекта создается новая переменная. Если вы не создаете конструктор копирования, ваш компилятор сгенерирует конструктор копирования. Звучит здорово! Но если вы неправильно настроите конструктор, ошибки будут повторяться.

class PrtHolder {
public:
PtrHolder(void* p) : m_ptr(p) { }
~PtrHolder() {
delete m_ptr;
}
private:
void* m_ptr;
};

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

Инициализация указателя​


Foo* pFoo;
if (GetFooPtr ( &pFoo ) )
{
// some code
}
// If pFoo is uninitialized, this is exploitable
pFoo->Release();

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

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


Недостаток знаний STL​


Знайте стандарты C++.

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

Вся презентация​


Посмотреть всю презентацию Марии можно здесь:



Источник статьи: https://habr.com/ru/post/559584/
 
Сверху