Целочисленное переполнение
Целочи́сленное переполне́ние (англ. integer overflow) — ситуация в компьютерной арифметике, при которой вычисленное в результате операции значение не может быть помещено в n-битный целочисленный тип данных. Различают переполнение через верхнюю границу представления и через нижнюю (англ. Underflow).
Пример: сложение двух переменных размером 8 бит с записью результата в переменную того же размера:
возникает переполнение.
При этом в результат записывается не ожидаемое , а . Стоит отметить, что вычисление здесь произошло по модулю 2n, а арифметика по модулю циклическая, то есть 255+1=0 (при n = 8). Данная ситуация переполнения фиксируется вычислительной машиной установкой специальных битов регистра флагов Overflow и Carry (пункт 3.4.3.1 Combined Volume: Volume 1[1]). При программировании на языке ассемблера такую ситуацию можно напрямую установить, например, вручную проверив состояние регистра флагов после выполнения операции (пункт 7.3.13.2 Combined Volume: Volume 1[1]).
Происхождение проблемы
Битность регистра определяет диапазон данных, представимый в нём. Диапазоны представления для целых типов в бинарных вычислительных машинах:
Битность | 8 бит | 16 бит | 32 бит | 64 бит | |
Беззнаковый | Диапазон | 0..28−1 | 0..216−1 | 0..232−1 | 0..264−1 |
Диапазон (десятич.) | 0..255 | 0..65535 | 0..4294967295 | 0.. 18446744073709551615 | |
Знаковый | Диапазон | -27.. 27−1 | -215.. 215−1 | -231.. 231−1 | -263.. 263−1 |
Диапазон (десятич.) | -128..127 | -32768..32767 | -2147483648.. 2147483647 | -9223372036854775808.. 9223372036854775807 |
Переполнение может возникнуть в исходном коде вследствие ошибки программиста или его недостаточной бдительности к входным данным[2].
- Несоответствие знакового и беззнакового. Если числа представляются на вычислителе в дополнительном коде, то одному потоку бит соответствуют различные числа. В 32-битной арифметике знаковому −1 соответствует беззнаковое 4294967295 (верхняя граница представления). То есть приведение одного типа к другому может привести к значительной разнице в значении. Этот тип ошибки часто является последствием signedness error ( and), то есть неправильного приведения типов между типами разной знаковости
- Проблема срезки. Возникает, если число интерпретируется как целое меньшей длины. В таком случае только младшие биты останутся в числе. Старшие отбросятся, что приведет к изменению численного значения
- Знаковое расширение. Стоит помнить, что при приведении знакового числа к типу большей длины происходит копирование старшего бита, что в случае интерпретации как беззнаковое приведет к получению очень большого числа[3]
Риски для безопасности
Возможность переполнения широко используется программистами, например, для хеширования и криптографии, генерирования случайных чисел и нахождения границ представления типа[4]. В то же время, например, по стандарту языков C и C++, беззнаковые вычисления выполняются по модулю 2, в то время как знаковое переполнение является классическим примером[5] неопределённого поведения[6].
Такой вид некорректности в коде ведёт к следующим последствиям[4]:
- Компиляция может пойти неожиданным образом. Из-за наличия неопределённого поведения в программе, оптимизации компилятора могут изменить поведение программы.
- Бомба замедленного действия. На текущей версии ОС, компилятора, опций компиляции, структурной организации программы и т. д. всё может работать, а при любом изменении, например, появлении более агрессивных оптимизаций, сломаться.
- Иллюзия предсказуемости. Конкретная конфигурация компилятора может иметь вполне определённое поведение, например компиляторы языков C и C++ обычно реализуют операции по модулю 2n и для знаковых типов (только интерпретированных в дополнительном коде), если отключены агрессивные оптимизации. Однако, надеяться на такое поведение нельзя, иначе есть риск эффекта «бомбы замедленного действия»
- Образование диалектов. Некоторые компиляторы предоставляют дополнительные опции для того, чтобы доопределить неопределённое поведение. Например, и GCC, и Clang поддерживают опцию -fwrapv, обеспечивающую выше описанное (в пункте 3) поведение.
Изменение стандарта может привести к новым проблемам с переполнением. К примеру, 1<<31 было зависимым от реализации в стандартах ANSI C и C++98, в то время как стали неопределённым в C99 и C11 (для 32-битных целых).[4]
Также, последствиями такой ошибки могут быть и другие, например переполнение буфера.
Эксплуатация и последствия
Основные последствия для безопасности[7]:
- Для доступности. Возможный отказ системы (окно для DoS-атак)
- Для целостности. Неавторизированный доступ к данным
- Для конфиденциальности. Исполнение стороннего кода, обход защитного механизма
Классически переполнение может быть эксплуатировано через переполнение буфера.
img_t table_ptr; /*struct containing img data, 10kB each*/
int num_imgs;
...
num_imgs = get_num_imgs();
table_ptr = (img_t*)malloc(sizeof(img_t)*num_imgs);
...
Данный пример[7] иллюстрирует сразу несколько уязвимостей. Во-первых, слишком большой num_imgs приведёт к выделению огромного буфера, из-за чего программа может потребить все ресурсы системы или вызвать её крах.
Другая уязвимость в том, что если num_imgs ещё больше, это приведёт к переполнению аргумента malloc. Тогда выделится лишь небольшой буфер. При записи в него произойдёт переполнение буфера, последствиями чего могут стать: перехват контроля над исполнением, исполнение кода злоумышленника, доступ к важной информации.[8]
Предотвращение проблемы
Защита от подобного поведения должна проводиться на нескольких уровнях[7]:
- Планирования и требований к программе:
- Убедитесь, что все протоколы взаимодействия между компонентами строго определены. В том числе, что все вычисления вне границ представления будут обнаружены. И требуйте строгого соответствия этим протоколам
- Используйте язык программирования и компилятор, которые не позволяет этой уязвимости воплотиться, либо позволяют легче её обнаружить, либо выполняют авто-проверку границ. Инструменты, которые предоставляются компилятором, включают санитайзеры (например, Address Sanitizer или Undefined Behavior Sanitizer).
- Архитектуры программы:
- Используйте проверенные библиотеки или фреймворки, помогающие проводить вычисления без риска непредсказуемых последствий. Примеры включают такие библиотеки, как SafeInt (C++) или IntegerLib (C или C++).
- Любые проверки безопасности на стороне клиента должны быть продублированы на стороне сервера, чтобы не допустить CWE-602. Злоумышленник может миновать проверку на стороне клиента, изменив сами значения непосредственно после прохождения проверки или модифицируя клиента, чтобы полностью убрать проверки.
- Реализации:
- Проводите валидацию любых поступивших извне числовых данных, проверяя что они находятся внутри ожидаемого диапазона. Проверяйте обязательно как минимальный порог, так и максимальный. Используйте беззнаковые числа, где это возможно. Это упростит проверки на переполнение.
- Исследуйте все необходимые нюансы языка программирования, связанные с численными вычислениями (CWE-681). Как они представляются, какие различия между знаковыми и беззнаковыми, 32-битными и 64-битными, проблемы при кастовании (обрезка, знаково-беззнаковое приведения типов — выше) и как обрабатываются числа, слишком маленькие или, наоборот, большие для их машинного представления. Также убедитесь, что используемый вами тип (например, int или long) покрывают необходимый диапазон представления
- Подробно изучите полученные от компилятора предупреждения и устраните возможные проблемы безопасности, такие как несоответствия знаковости операндов при операциях с памятью или использование неинициализированных переменных. Даже если уязвимость совсем небольшая, это может привести к опасности для всей системы.
Другие правила, позволяющие избежать этих уязвимостей, опубликованы в стандарте CERT C Secure Coding Standard в 2008 году, включают[9]:
- Не пишите и не используйте функции обработки строкового ввода, если они обрабатывают не все случаи
- Не используйте битовые операции над знаковыми типами
- Вычисляйте выражения на типе большего размера перед сравнением или присваиванием в меньший
- Будьте внимательны перед приведением типа между числом и указателем
- Убедитесь, что вычисления по модулю или результаты деления не приводят к последующему делению на ноль
- Используйте intmax_t или uintmax_t для форматированного ввода-вывода пользовательских числовых типов
Примеры из жизни
Исследование SPECCINT
В статье[4] в качестве предмета исследования программ на языках C и C++ на целочисленное переполнение подробно исследуется один из самых широко применимых и известных тестовых пакетов SPEC, используемый для измерений производительности. Состоит он из фрагментов наиболее распространённых задач, как то: тестов вычислительной математики, компиляции, работы с базами данных, диском, сетью и прочее.
Результаты анализа SPECCINT2000 показывают наличие 219 статических источников переполнения в 8 из 12 бенчмарков, из которых 148 использовали беззнаковое переполнение и 71 — знаковое (снова неопределённое поведение). В то же время, беззнаковое переполнение тоже не всегда намеренное и может являться ошибкой и источником уязвимости (например, Listing 2 той же статьи[4]).
Также был проведён тест на «бомбы замедленного действия» в SPECCINT2006. Его идеей является в каждом месте неопределённого поведения вернуть случайное число и посмотреть, к каким последствиям это может привести. Если оценивать неопределённое поведение с точки зрения стандарта C99/C++11, то тест не пройдут целых 6 бенчмарков из 9.
Примеры из других программных пакетов
int addsi (int lhs, int rhs) {
errno = 0;
if (((( lhs+rhs)^lhs)&(( lhs+rhs)^rhs)) >> (sizeof(int)*CHAR_BIT -1)) {
error_handler("OVERFLOW ERROR", NULL, EOVERFLOW);
errno = EINVAL;
}
return lhs + rhs;
}
Данный фрагмент кода[4] из пакета IntegerLib проверяет, могут ли быть lhs и rhs сложены без переполнения. И ровно в строчке 3 это переполнение может возникнуть (при сложении lhs+rhs). Это UB, так как lhs и rhs — знакового типа. Кроме этого, в данной библиотеке найдено ещё 19 UB-переполнений.
Также авторы сообщили разработчикам о 13 переполнениях в SQLite, 43 в SafeInt, 6 в GNU MPC library, 30 в PHP, 18 в Firefox, 71 в GCC, 29 в PostgreSQL, 5 в LLVM и 28 в Python. Большинство из ошибок были вскоре исправлены.
Другие примеры
Известный пример целочисленного переполнения происходит в игре Pac-Man, так же, как и в других играх серии: Ms. Pac-Man, Jr. Pac-Man. Также этот глюк появляется в Pac-Man Google Doodle в качестве так называемого «пасхального яйца».[10] Здесь же на уровне 256 можно наблюдать «экран смерти», а сам уровень называют «уровнем разделенного экрана». Энтузиасты дизассемблировали исходный код, пытаясь исправить ошибку модифицированием игры.
Такая же проблема якобы была в игре Sid Meier's Civilization и известна как Ядерный Ганди[11]. Согласно легенде, на определённом этапе игры с очень миролюбивым Ганди, происходит переполнение через 0 уровня враждебности, результатом чего может стать ядерная война с Ганди. На самом деле, такой миф появился лишь с выходом Civilization V, где параметр его искусственного интеллекта, регулирующий создание и использование ядерного вооружения, имеет наивысшее значение 12, что не противоречило тому, что Ганди является одним из самых миролюбивых лидеров в игре[12].
Ещё одним примером является глюк в SimCity 2000[13]. Здесь дело в том, что бюджет игрока стал очень большим, а после перехода через 231 внезапно стал отрицательным. Игра оканчивается поражением.
Этот глюк из Diablo III. Из-за одного из изменений патча 1.0.8, игровая экономика сломалась. Максимальную сумму для сделок повысили с 1 млн до 10 млн. Стоимость покупки переполнялась через 32-битный тип, а при отмене операции возвращалась полная сумма. То есть игрок оставался с прибылью в 232 игровой валюты[14]
См. также
Примечания
- Intel® 64 and IA-32 Architectures Software Developer Manuals | Intel® Software (англ.). software.intel.com. Дата обращения: 22 декабря 2017.
- x86 Exploitation 101: “Integer overflow” – adding one more… aaaaaaaaaaand it’s gone (англ.), gb_master's /dev/null (12 August 2015). Дата обращения 20 декабря 2017.
- The Web Application Security Consortium / Integer Overflows . projects.webappsec.org. Дата обращения: 8 декабря 2017.
- W. Dietz, P. Li, J. Regehr, V. Adve. Understanding integer overflow in C/C #x002B; #x002B; // 2012 34th International Conference on Software Engineering (ICSE). — June 2012. — С. 760—770. — doi:10.1109/icse.2012.6227142.
- CWE - 2011 CWE/SANS Top 25 Most Dangerous Software Errors (англ.). cwe.mitre.org. Дата обращения: 21 декабря 2017.
- ISO/IEC 9899:2011 - Information technology -- Programming languages -- C (англ.). www.iso.org. Дата обращения: 21 декабря 2017.
- CWE-190: Integer Overflow or Wraparound (3.0) (англ.). cwe.mitre.org. Дата обращения: 12 декабря 2017.
- CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer (3.0) (англ.). cwe.mitre.org. Дата обращения: 12 декабря 2017.
- CWE-738: CERT C Secure Coding (2008 Version) Section 04 - Integers (INT) (3.0) (англ.). cwe.mitre.org. Дата обращения: 15 декабря 2017.
- Map 256 Glitch (англ.), Pac-Man Wiki. Дата обращения 12 декабря 2017.
- Nuclear Gandhi, Know Your Meme. Дата обращения 15 декабря 2017.
- Артемий Леонов. Почему история о баге с «ядерным Ганди» в Civilization, скорее всего, выдумана . DTF (5 сентября 2019). Дата обращения: 24 октября 2020.
- Sim City 2000 Integer Overflow . Blake O\'Hare. Дата обращения: 12 декабря 2017.
- Diablo III Economy Broken by an Integer Overflow Bug (англ.), minimaxir | Max Woolf's Blog. Дата обращения 12 декабря 2017.