Введение: почему безопасность C++ кода критична?
C++ — один из самых мощных и производительных языков, но его гибкость требует от разработчика высокой ответственности. В отличие от многих современных языков с автоматическим управлением памятью (Java, Python), C++ предоставляет прямой доступ к памяти и системным ресурсам. Это открывает широкие возможности для атак: от переполнения буфера до утечек данных.
Для начинающих C++ разработчиков понимание типовых уязвимостей — это не просто теория, а необходимость. Ошибка в пару строк может привести к краху сервера, утечке паролей или выполнению произвольного кода злоумышленником. В этой статье мы разберем топ-10 самых распространенных уязвимостей C++ с примерами и способами защиты.
Топ-10 уязвимостей C++ с примерами и защитой
1. Переполнение буфера (Buffer Overflow)
Проблема: Запись данных за границы выделенного массива. Это классика C/C++, которая позволяет перезаписать соседние переменные, адрес возврата функции или данные в куче.
// Пример уязвимости
#include <cstring>
#include <iostream>
void vulnerable() {
char buffer[10];
std::cout << "Введите строку: ";
std::cin >> buffer; // Опасность! Нет проверки длины
}
int main() {
vulnerable();
return 0;
}
Последствия: Выполнение произвольного кода (RCE), крах программы.
Защита: Используйте std::string, std::vector, безопасные функции (например, strncpy с указанием размера).
// Безопасный вариант
#include <string>
#include <iostream>
void safe() {
std::string input;
std::getline(std::cin, input);
std::cout << "Вы ввели: " << input << std::endl;
}
2. Утечка памяти (Memory Leak)
Проблема: Память выделена через new/malloc, но не освобождена через delete/free. Со временем приложение «съедает» всю оперативную память.
void leaky() {
int* data = new int[1000];
// Забыли delete[] data;
}
Последствия: Отказ в обслуживании (DoS), замедление системы.
Защита: Используйте умные указатели (std::unique_ptr, std::shared_ptr), RAII (Resource Acquisition Is Initialization).
#include <memory>
void safe() {
std::unique_ptr<int[]> data = std::make_unique<int[]>(1000);
// Память освободится автоматически при выходе из функции
}
3. Разыменование нулевого указателя (Null Pointer Dereference)
Проблема: Попытка обратиться к памяти по адресу nullptr.
void process(int* ptr) {
*ptr = 42; // Если ptr == nullptr, программа упадет
}
Последствия: Немедленный крах (segmentation fault), DoS.
Защита: Всегда проверяйте указатели перед использованием или используйте ссылки.
void safe(int* ptr) {
if (ptr) {
*ptr = 42;
} else {
// Обработка ошибки
}
}
4. Целочисленное переполнение (Integer Overflow)
Проблема: Результат арифметической операции выходит за пределы типа данных. Это может привести к непредсказуемому поведению, особенно при работе с индексами массивов.
#include <cstdint>
void vulnerable() {
uint16_t a = 65535;
uint16_t b = 1;
uint16_t sum = a + b; // Переполнение: 65536 -> 0
}
Последствия: Логические ошибки, переполнение буфера (если используется для вычисления размера).
Защита: Используйте типы с фиксированной размерностью (int32_t, int64_t), проверяйте границы. В C++17 есть std::clamp и std::numeric_limits.
#include <limits>
bool safe_add(uint16_t a, uint16_t b, uint16_t& result) {
if (a > std::numeric_limits<uint16_t>::max() - b) {
return false; // Переполнение
}
result = a + b;
return true;
}
5. Форматная строка (Format String Vulnerability)
Проблема: Передача пользовательского ввода в качестве строки формата для printf и подобных функций.
void print_user_input(const char* user_input) {
printf(user_input); // Огромная уязвимость!
}
Последствия: Чтение/запись произвольной памяти, утечка данных, выполнение кода.
Защита: Никогда не передавайте пользовательский ввод в первый аргумент printf.
void safe(const char* user_input) {
printf("%s", user_input); // Правильно
}
6. Использование после освобождения (Use-After-Free)
Проблема: Обращение к памяти, которая уже была освобождена. Указатель становится «висячим».
int* ptr = new int(10);
delete ptr;
*ptr = 20; // Ошибка! Память уже не наша
Последствия: Повреждение данных, RCE, крах.
Защита: Обнуляйте указатели после delete (ptr = nullptr;), используйте умные указатели.
std::unique_ptr<int> ptr = std::make_unique<int>(10);
// Память будет освобождена, когда ptr выйдет из области видимости
// Доступ к ptr-> будет невозможен после освобождения
7. Гонка данных (Data Race / Race Condition)
Проблема: Два потока одновременно читают и пишут в одну переменную без синхронизации.
<