Skip to content

Latest commit

 

History

History
100 lines (71 loc) · 7.14 KB

array_overrun.md

File metadata and controls

100 lines (71 loc) · 7.14 KB

Переполнение буфера

Переполнение буфера и выход за границы массива — злобные ошибки и причины не только лишь простых падений программ, но дыр в безопасности, позволяющих получать доступ куда не следует или даже исполнять произвольный код.

В стандартной библиотеке C, доставшейся C++ по наследству, великое множество дырявых функций, позволяющих добиться переполнения буфера, если программист не удосужился проверить все возможные и невозможные варианты.

  • scanf("%s", buf) — нет проверки размера буфера
  • strcpy(dst, src) — нет проверки размера буфера
  • strcat(dst, src) — нет проверки размера буфера
  • gets(str) — нет проверки размера буфера
  • memcpy(dst, src, n) — проверку размера dst нужно делать вручную.

И еще многие другие, преимущественно работающие со строками, функции.

Эти функции доставляли и продолжают доставлять проблемы. Некоторые компиляторы (msvc) по умолчанию откажутся собирать ваш код, если увидят одну из них. Другие будут менее заботливыми и, возможно, выдадут предупреждение. По крайней мере про функцию gets уж точно. Если с другими функциями у программиста есть возможность уберечься (проверка до вызова; у scanf можно указать размер в ограничение строке), то с gets — без вариантов.

Для большинства старых небезопасных сишных функций сейчас есть «безопасные» аналоги с размерами буферов. Часть из них не стандартизирована, часть стандартизирована. Все это породило огромное количество костылей с макроподстановками для работы со всем этим зоопарком. Но сейчас не об этом.


Проверки размеров — дополнительная работа. Генерировать под них инструкции — замедлять программу. Тем более программист мог все проверить сам. Так что в C/С++ обращение за границы массива, хоть на запись, хоть на чтение — влечет неопределенное поведение. И дыры в безопасности могут зарастать различными спецэффектами.

В большинстве случаев, если нарушение размеров происходит не всегда, попытка почитать за границами массива проявится либо получением мусорных результатов, либо простой и так всеми любимой ошибкой сегментации (SIGSEGV).

Но иногда начинается веселье.

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 оптимизациями, всегда «найдет» пятерку в массиве. Независимо от того какие числа будут введены. Причем никаких предупреждений ни clang, ни gcc не производят.

Происходит такой спецэффект из следующих соображений:

  1. Компиляторы вольны считать, что UB в программах не бывает
  2.  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. Оптимизировано!

Или вот конечный цикл становится бесконечным:

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/