mirror of
https://github.com/Nekrolm/ubbook.git
synced 2026-06-09 13:14:18 +03:00
101 lines
7.1 KiB
Markdown
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/
|
|
|