Само слово «переполнение» вполне описывает уязвимость, которую мы собираемся обсудить в этом посте. Рассмотрим стакан, в который наливают воду. Если общий объем налитой воды меньше или равен объему стакана, все в розовом цвете. Когда объем воды превышает объем стакана, он «переливается через край», и у вас остается мокрый пол, который нужно мыть.

Спецификаторы и модификаторы типов переменных

В C (и многих других языках) значения хранятся в переменных определенного типа. (Конечно, есть такие языки, как Python, где программисту не нужно указывать типы переменных.) Существует четыре основных типа переменных:

  1. уголь
  2. инт
  3. плавать
  4. двойной

В дополнение к указанным выше типам (также называемым спецификаторами типов) есть четыре модификатора типов:

  1. подписал
  2. неподписанный
  3. короткая
  4. длинная

Тип переменной вместе с (необязательным) модификатором определяет максимальное и минимальное значения, которые может хранить переменная. Например, на 32-разрядных машинах тип переменной signed int может хранить значения от -2 147 483 648 до 2 147 483 647, или, другими словами, signed int имеет размер 4 байта. Дополнительные сведения о размерах типов данных см. в разделе Типы данных C.

Примечание

Система, которую я использую для всех примеров:

kr@k3n@ubuntu:~$ uname -a
Linux ubuntu 4.15.0–36-generic #39~16.04.1-Ubuntu SMP Вт, 25 сентября, 09:00:45 UTC 2018 i686 i686 i686 GNU /Линукс

Крушение ракеты Ariane 5

Если у нас есть переменная signed int с именем, например x, и мы пытаемся ввести значение, превышающее максимальное или минимальное допустимое значение для signed int , он переполнится. В слабо типизированных языках, таких как C, ситуации переполнения обычно не помечаются как ошибки во время компиляции. gcc, скорее всего, выдаст предупреждение в таких случаях, но его можно игнорировать (что является проблемой). Рассмотрим следующую программу:

int main(void) {
int x = 0x180000000;
printf ("%d\n", x);
return 0;

В C модификатор типа по умолчанию для intsigned. У нас есть тип переменной signed int, в которую мы помещаем значение (по какой-либо причине) 0x180000000, которое является шестнадцатеричным для 6 442 450 944, что, очевидно, превышает максимально допустимое значение 2 147 483 647. Давайте посмотрим, что произойдет, когда мы скомпилируем с помощью gcc и запустим:

kr@k3n@ubuntu:~$ gcc -o overflow overflow.c
overflow.c: В функции 'main':
overflow.c:5:13: предупреждение: переполнение в неявной константе преобразование [-Woverflow]
int x = 0x180000000;
^
kr@k3n@ubuntu:~$ ./overflow
-2147483648

C просто посмотрел на 32 бита, отбросил значение 33-го бита и выше. Это случай усечения, вызванного переполнением. Представьте себе ситуацию, когда значение x требуется на более позднем этапе программы, и ожидается, что x будет содержать 0x180000000. В худшем случае это приведет к катастрофе. Рассмотрим случай с ракетой Ariane 5, которая разбилась из-за небольшой компьютерной программы, пытавшейся вставить 64-битное число в 16-битное пространство.

Целочисленное переполнение — примеры

Как правило, не все уязвимости могут быть использованы злоумышленниками. Однако уязвимости переполнения особенно опасны, потому что они могут привести к нестабильности программы даже при нормальной работе и во многих случаях также могут быть использованы. Прежде чем мы рассмотрим другие примеры, вот что нужно иметь в виду:

Круг целых чисел

Когда я изучал целочисленные переполнения, я обнаружил, что Круг целых чисел (полностью выдуманное название) помогает развить интуитивное понимание этого предмета.

Для signed int старший значащий бит (MSB) также отмечает знак значения — бит знака. Например: 0x7ffffffff равно 2147483647, но 0x80000000 равно -2147483648, что совпадает с 2³¹, но с отрицательным знаком, так как старший бит равен 1. Для unsigned int такого бита знака не существует. Следовательно, 0x80000000 равно 2147483648.

Переполнение происходит, когда два числа с:

  1. добавляются одинаковые знаки, и результат отрицательный,
  2. разные знаки вычитаются, и результат положительный.

Сложение можно рассматривать как движение по кругу целых чисел по часовой стрелке, а вычитание можно рассматривать как движение против часовой стрелки. Теперь, когда мы кое-что знаем об условиях переполнения, давайте рассмотрим два примера.

Целочисленное переполнение — сложение

Рассмотрим следующую программу:

int main(void) {
int num1 = 0x7ffffffff;
int num2 = 0x1;
int addation_result = num1 + num2;
printf («%d\n» , результат_дополнения);

unsigned int num3 = 0xffffffff;
unsigned int num4 = 0x1;
unsigned int uaddition_result = num3 + num4;
printf («%u\n», uaaddition_result);

вернуть 0;

Для неподготовленного глаза ожидается, что значение в addition_result будет 0x80000000 (или 2 147 483 648), а uaddition_result ожидается 0x100000000 (или 4 294 967 296). Посмотрим реальные значения:

kr@k3n@ubuntu:~$ gcc -o переполнение overflow.c
kr@k3n@ubuntu:~$ ./overflow
-2147483648
0

Что за …? Вот что произошло:

  1. При добавлении 0x1 к 0x7ffffffff результат стал 0x80000000. Итак, почему это вызвало переполнение? Ответ все еще может быть представлен в 32 битах. Вообще-то, нет. Для типа signed int количество битов, доступных для значения, равно 31 биту, поскольку 32-й бит является битом знака. В этом случае для представления суммы также требовался 32-й бит, что вызывало переполнение.
  2. В добавлении unsigned int для представления суммы требовался 33-й бит, что приводило к переполнению.

Опять же, представьте, если бы значения этих переменных требовались на более поздних этапах программы. Какая катастрофа будет, если операция, ожидающая 0x100000000, вместо этого получит 0x0.

Целочисленное переполнение — умножение

У меня есть идеальный пример для этого. Следующий код представляет собой фрагмент одной из задач в picoctf 2018:

if(number_flags › 0){
int total_cost = 0;
total_cost = 1000*number_flags;
printf("\nВаша общая стоимость: %d\n", total_cost) ;
if(total_cost ‹= account_balance){
account_balance = account_balance — total_cost;
printf("\nВаш новый баланс: %d\n\n", account_balance);
}
else{
printf("Недостаточно средств\n");
}

Вот как можно использовать этот код:

nikel@pico-2018-shell-1:~$ nc 2018shell1.picoctf.com 5795

Добро пожаловать в приложение Store V1.0

Самое безопасное приложение для покупок в мире

[1] Проверка баланса счета

[2] Купить вещи

[3] Выйти

Введите пункт меню

1

Баланс: 1100

Добро пожаловать в приложение Store V1.0

Самое безопасное приложение для покупок в мире

[1] Проверка баланса счета

[2] Купить вещи

[3] Выйти

Введите пункт меню

2

Текущие аукционы

[1] Не могу поверить, что это не флаг!

[2] Настоящий флаг

1

Имитационные флаги стоят 1000 каждый, сколько вы хотите?

1000000000

Ваша общая стоимость: -727379968

Ваш новый баланс: 727381068

И это целочисленное переполнение из-за умножения! Обратите внимание на этот оператор в исходном коде:

total_cost = 1000*number_flags

Что делать, если значение продукта превышает диапазон положительных значений, возможных для переменной signed int, total_cost? Он станет отрицательным, и вот что случилось! Представьте, если бы реальное производственное программное обеспечение имело такой код!

Сделанный!

Это все! В этой статье мы специально рассмотрели целочисленные переполнения, но переполнение может произойти в переменной любого типа данных.

Уязвимости переполнения легко пробираются в код и обнаружить их достаточно сложно. Самый простой способ смягчить их — не вводить их в первую очередь. Однако фаззинг — хороший метод для обнаружения переполнения. Проверка кода также может помочь, но для этого нужен опытный глаз. Уязвимости переполнения опасны, как вы уже видели на примере Ariane 5 и picoctf.

Спасибо за чтение, и если у вас есть какие-либо вопросы, пожалуйста, дайте мне знать в разделе комментариев ниже, и я свяжусь с вами, как только смогу.