Files
ubbook/runtime/array_overrun.md
T
Sergei Shulman 5c64f74cb2 Fix typos
2022-06-07 12:56:47 +02:00

101 lines
7.1 KiB
Markdown

# Переполнение буфера
Переполнение буфера и выход за границы массива — злобные ошибки и причины не только лишь простых падений программ, но дыр в безопасности, позволяющих получать доступ куда не следует или даже исполнять произвольный код.
В стандартной библиотеке 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 <algorithm>`
- Не использовать `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/