Само слово «переполнение» вполне описывает уязвимость, которую мы собираемся обсудить в этом посте. Рассмотрим стакан, в который наливают воду. Если общий объем налитой воды меньше или равен объему стакана, все в розовом цвете. Когда объем воды превышает объем стакана, он «переливается через край», и у вас остается мокрый пол, который нужно мыть.
Спецификаторы и модификаторы типов переменных
В C (и многих других языках) значения хранятся в переменных определенного типа. (Конечно, есть такие языки, как Python, где программисту не нужно указывать типы переменных.) Существует четыре основных типа переменных:
- уголь
- инт
- плавать
- двойной
В дополнение к указанным выше типам (также называемым спецификаторами типов) есть четыре модификатора типов:
- подписал
- неподписанный
- короткая
- длинная
Тип переменной вместе с (необязательным) модификатором определяет максимальное и минимальное значения, которые может хранить переменная. Например, на 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 модификатор типа по умолчанию для int — signed. У нас есть тип переменной 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.
Переполнение происходит, когда два числа с:
- добавляются одинаковые знаки, и результат отрицательный,
- разные знаки вычитаются, и результат положительный.
Сложение можно рассматривать как движение по кругу целых чисел по часовой стрелке, а вычитание можно рассматривать как движение против часовой стрелки. Теперь, когда мы кое-что знаем об условиях переполнения, давайте рассмотрим два примера.
Целочисленное переполнение — сложение
Рассмотрим следующую программу:
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
Что за …? Вот что произошло:
- При добавлении 0x1 к 0x7ffffffff результат стал 0x80000000. Итак, почему это вызвало переполнение? Ответ все еще может быть представлен в 32 битах. Вообще-то, нет. Для типа signed int количество битов, доступных для значения, равно 31 биту, поскольку 32-й бит является битом знака. В этом случае для представления суммы также требовался 32-й бит, что вызывало переполнение.
- В добавлении 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.
Спасибо за чтение, и если у вас есть какие-либо вопросы, пожалуйста, дайте мне знать в разделе комментариев ниже, и я свяжусь с вами, как только смогу.