Безопасность C++ приложений: топ-10 уязвимостей и способы защиты
C++ — мощный язык системного программирования, но его гибкость и прямой доступ к памяти делают приложения уязвимыми для серьёзных атак. По данным OWASP и CWE, многие критические уязвимости в промышленном ПО (от игр до авионики) возникают именно из-за ошибок в C++ коде. В этой статье мы разберём топ-10 самых опасных уязвимостей, приведём примеры кода и покажем, как их избежать.
1. Переполнение буфера (Buffer Overflow)
Классическая уязвимость, позволяющая злоумышленнику перезаписать соседние участки памяти. Возникает при копировании данных без проверки размера.
// Уязвимый код
#include <cstring>
void vulnerable(char* input) {
char buffer[10];
strcpy(buffer, input); // Опасность: если input длиннее 9 символов
}
// Безопасный вариант
void secure(char* input, size_t len) {
char buffer[10];
if (len < sizeof(buffer)) {
strncpy(buffer, input, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0';
}
}
Защита: используйте std::string, std::vector или функции с ограничением длины (strncpy, snprintf). Включите флаги компилятора -fstack-protector-strong и -D_FORTIFY_SOURCE=2.
2. Использование после освобождения (Use-After-Free)
Обращение к памяти, которая уже была освобождена. Приводит к чтению/записи в чужую область или к выполнению произвольного кода.
// Уязвимый код
int* ptr = new int(42);
delete ptr;
*ptr = 100; // Ошибка! ptr уже невалиден
// Безопасный вариант
int* ptr = new int(42);
delete ptr;
ptr = nullptr; // Обнуляем указатель
if (ptr) *ptr = 100; // Проверка перед использованием
Защита: используйте умные указатели (std::unique_ptr, std::shared_ptr), обнуляйте сырые указатели после delete, применяйте санитайзеры (-fsanitize=address).
3. Целочисленное переполнение (Integer Overflow)
Выход за границы допустимого диапазона при арифметических операциях. Часто используется для обхода проверок размера буфера.
// Уязвимый код
size_t size = 0;
size_t user_input = 4294967295; // 2^32 - 1
size_t total = size + user_input + 1; // Переполнение!
// Безопасный вариант с проверкой
if (user_input > SIZE_MAX - size) {
throw std::overflow_error("Размер слишком велик");
}
size_t total = size + user_input + 1;
Защита: используйте типы с фиксированной разрядностью (uint32_t), проверяйте переполнение перед операцией, применяйте __builtin_add_overflow (GCC/Clang).
4. Разыменование нулевого указателя (Null Pointer Dereference)
Попытка обратиться к памяти по адресу NULL. Приводит к аварийному завершению программы (segfault) или к уязвимости DoS.
// Уязвимый код
void process(int* ptr) {
*ptr = 10; // Если ptr == nullptr — краш
}
// Безопасный вариант
void process(int* ptr) {
if (!ptr) {
throw std::invalid_argument("Null pointer received");
}
*ptr = 10;
}
Защита: всегда проверяйте указатели на nullptr перед использованием, возвращайте std::optional вместо сырых указателей.
5. Утечки памяти (Memory Leaks)
Потеря доступа к выделенной памяти без её освобождения. Со временем истощает ресурсы системы.
// Уязвимый код
void leak() {
int* data = new int[100];
// Забыли delete[] data — утечка
}
// Безопасный вариант
void no_leak() {
std::unique_ptr<int[]> data = std::make_unique<int[]>(100);
// Память освободится автоматически
}
Защита: используйте RAII (умные указатели, контейнеры STL), применяйте valgrind или санитайзеры для обнаружения утечек.
6. Гонки данных (Data Races / Race Conditions)
Неконтролируемый одновременный доступ к общим данным из разных потоков. Приводит к повреждению данных и непредсказуемому поведению.
// Уязвимый код (многопоточный)
int counter = 0;
void increment() { ++counter; } // Не атомарно
// Безопасный вариант
std::atomic<int> counter{0};
void increment() { counter.fetch_add(1); }
Защита: используйте std::atomic, мьютексы (std::mutex), lock-free структуры данных, избегайте глобальных переменных.
7. Уязвимости форматной строки (Format String Vulnerabilities)
Передача пользовательского ввода в качестве аргумента форматной строки. Позволяет читать/записывать стек.
// Уязвимый код
printf(user_input); // Если user_input = "%x %x %x" — утечка стека
// Безопасный вариант
printf("%s", user_input); // Фиксированный формат
Защита: никогда не передавайте пользовательский ввод как первый аргумент printf/sprintf. Используйте std::format (C++20) или std::string.
8. Переполнение стека (Stack Overflow)
Глубокая или бесконечная рекурсия, превышающая лимит стека. Приводит к аварийному завершению.
// Уязвимый код
int factorial(int n) {
return n * factorial(n - 1); // Нет базового случая
}
// Безопасный вариант (итеративный)
int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) result *= i;
return result;
}
<