# Переполнение буфера Переполнение буфера и выход за границы массива — злобные ошибки и причины не только лишь простых падений программ, но дыр в безопасности, позволяющих получать доступ куда не следует или даже исполнять произвольный код. В стандартной библиотеке C, доставшейся C++ по наследству, великое множество дырявых функций, позволяющих добиться переполнения буфера, если программист не удосужился проверить все возможные и невозможные варианты. - `scanf("%s", buf)` — нет проверки размера буфера - `strcpy(dst, src)` — нет проверки размера буфера - `strcat(dst, src)` — нет проверки размера буфера - `gets(str)` — нет проверки размера буфера - `memcpy(dst, src, n)` — проверку размера `dst` нужно делать вручную. И еще многие другие, преимущественно работающие со строками, функции. Эти функции доставляли и продолжают доставлять проблемы. Некоторые компиляторы (msvc) по умолчанию откажутся собирать ваш код, если увидят одну из них. Другие будут менее заботливыми и, возможно, выдадут предупреждение. По крайней мере про функцию `gets` уж точно. Если с другими функциями у программиста есть возможность уберечься (проверка до вызова; у `scanf` можно указать размер в ограничение строке), то с `gets` — без вариантов. Для большинства старых небезопасных сишных функций сейчас есть «безопасные» аналоги с размерами буферов. Часть из них не стандартизирована, часть стандартизирована. Все это породило огромное количество костылей с макроподстановками для работы со всем этим зоопарком. Но сейчас не об этом. --- Проверки размеров — дополнительная работа. Генерировать под них инструкции — замедлять программу. Тем более программист мог все проверить сам. Так что в C/С++ обращение за границы массива, хоть на запись, хоть на чтение — влечет неопределенное поведение. И дыры в безопасности могут зарастать различными спецэффектами. В большинстве случаев, если нарушение размеров происходит не всегда, попытка почитать за границами массива проявится либо получением мусорных результатов, либо простой и так всеми любимой ошибкой сегментации (SIGSEGV). Но иногда начинается веселье. ```C++ const int N = 10; int elements[N]; bool contains(int x) { for (int i = 0; i <= N; ++i) { if (x == elements[i]) { return true; } } return false; } int main() { for (int i = 0; i < N; ++i) { std::cin >> elements[i]; } return contains(5); } ``` Эта программа, собранная gcc c оптимизациями, всегда [«найдет»](https://godbolt.org/z/949Kxc) пятерку в массиве. Независимо от того какие числа будут введены. Причем никаких предупреждений ни clang, ни gcc не производят. Происходит такой спецэффект из следующих соображений: 1. Компиляторы вольны считать, что UB в программах не бывает 2. ```C++ for (int i = 0; i <= N; ++i) { if (x == elements[i]) { return true; } } ``` В этом цикле будет обращение за границы массива, а значит UB. 3. Но, так как UB не бывает, до `N+1` итерации дело дойти не должно 4. Значит, мы выйдем из цикла по `return true` 5. А значит вся функция `contains` — это один `return true`. Оптимизировано! Или вот конечный цикл [становится бесконечным](https://godbolt.org/z/hPc1cf): ```C++ const int N = 10; int main() { int decade[N]; for (int k = 0; k <= N; ++k) { printf("k is %d\n",k); decade[k] = -1; } } ``` И фокус здесь не менее хитрый: 1. `decade[k] = -1;` Обращение к элементу массива должно быть без UB. А значит `k < N` 2. Раз `k < N`, то условие продолжения цикла `k <= N` — всегда истинно. Проверять его не надо. Оптимизировано! В этих примерах, конечно, сразу же должен броситься в глаза `<=` в заголовках циклов. Но и с более привычным `<` тоже можно изобрести себе проблемы. Константа `N`, например, может быть не связана с размером массива. И все, приехали. --- В дружелюбных и безопасных языках вы получите ошибку во время выполнения. Панику или исключение. В C++ же все надо проверять, проверять и еще раз проверять самим: - Не использовать отдельно висящие константы при проверке размеров. Лучше `std::size()` или метод `size()` - Писать меньше сырых циклов со счетчиками. Предпочтительнее range-based-for или стандартные алгоритмы из `#include ` - Не использовать `operator[]`, когда не критична производительность. Безопаснее метод `at()` контейнера, проверяющий границы. ## Полезные ссылки 1. https://blog.rapid7.com/2019/02/19/stack-based-buffer-overflow-attacks-what-you-need-to-know/ 2. https://dhavalkapil.com/blogs/Buffer-Overflow-Exploit/