Устранение уязвимости целочисленного переполнения/недополнения в смарт-контрактах
12 февраля 2023 г.Почти все мы использовали Google Таблицы или Microsoft Excel для ввода данных для некоторых вычислений. Допустим, вы хотите ввести имена сотрудников, их номера телефонов, должности и заработную плату, которую они получают.
В простейшей форме запись или обращение будут выглядеть в Таблицах или Excel следующим образом:
Как видите, имя и должность сотрудника состоят из текста, а номер телефона и зарплата – из последовательности чисел.
Таким образом, с семантической точки зрения мы, люди, понимаем, что эти поля означают в реальном мире, и можем различать их.
Понятно, что хотя вам и не нужна степень в области компьютерных наук, чтобы заметить разницу, как компилятор или интерпретатор обрабатывает эти данные?
Типы данных
Именно здесь вступают в действие типы данных, на определение которых программисты либо тратят время, либо нет, в зависимости от языка программирования, на котором они пишут код.
Другими словами, точки данных под именем сотрудника и должностью называются строками. Конечно, зарплата явно является целым числом в силу отсутствия десятичных знаков. Проще говоря, это типы данных, которые должны быть объявлены как таковые при написании кода, чтобы выполнялись только правильные операции, связанные с этим типом данных.
Вот как мы объявляем целочисленный тип данных в Solidity:
Тем не менее, поле "Номер телефона" в приведенной выше электронной таблице содержит точку данных, которая будет использоваться как уникальная строка, но это обсуждение в другой раз. На данный момент наше внимание будет сосредоточено на примитивном типе данных, с которым мы все выполняли базовые арифметические действия.
Да, мы говорим о целочисленном типе данных, который, хотя и важен для ключевых арифметических операций, имеет ограниченный диапазон для любых вычислений.
Почему происходит целочисленное переполнение/недополнение?
Возможно, самый популярный пример целочисленного переполнения в реальном мире происходит на транспортных средствах. Эти устройства, также известные как одометры, обычно отслеживают, сколько миль проехал автомобиль.
Итак, что происходит, когда значение пройденных миль достигает целочисленного значения без знака 999999 на шестизначном одометре?
В идеале, после добавления еще одной мили это значение должно достигать 1000000, верно? Но этого не происходит, так как предусмотрена седьмая цифра.
Вместо этого значение пройденных миль сбрасывается до 000000, как показано ниже:
По определению, поскольку седьмая цифра недоступна, это приводит к «переполнению», поскольку точное значение не представлено.
Вы поняли картину, верно?
И наоборот, может произойти и обратное, даже если это не так часто. Другими словами, когда записанное значение меньше наименьшего значения, доступного в диапазоне, что иначе называется «недополнением».
Как мы все знаем, компьютеры будут хранить целые числа в памяти как их двоичный эквивалент. Теперь для простоты предположим, что вы используете 8-битный регистр.
Итак, если вы хотите сохранить беззнаковое целое число 511, оно будет разбито на:
= 2⁸*1 + 2⁷*1 + 2⁶*1 + 2⁵*1 + 2⁴*1 + 2³*1 + 2²*1 + 2¹*1 + 2⁰*1
= 256 + 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1
= 111111111
Где каждый бит равен 1, и, как вы понимаете, вы не можете сохранить большее значение.
С другой стороны, если вы хотите сохранить число 0 в 8-битном регистре, это будет выглядеть так:
= 2⁸*0 + 2⁷*0 + 2⁶*0 + 2⁵*0 + 2⁴*0 + 2³*0 + 2²*0 + 2¹*0 + 2⁰*0
= 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
= 000000000
Где каждый бит равен 0, что должно говорить вам о том, что вы не можете сохранить меньшее значение.
Другими словами, диапазон целых чисел, разрешенных для такого 8-битного регистра, составляет 0–511. Итак, можно ли хранить в таком регистре целое число 512 или -1?
Конечно, нет. В результате вы сохраните значение, похожее на значение сброса пройденных миль в примере с одометром, но в виде двоичных значений.
Ясно, что для удобного размещения такого числа вам потребуются регистры с еще несколькими битами. В противном случае вы снова рискуете переполнением.
В случае целых чисел со знаком мы храним и отрицательные целые числа. Таким образом, когда мы пытаемся сохранить число, которое меньше допустимого диапазона или меньше нуля, как показано выше, происходит потеря значимости.
Опять же, поскольку целью любого вычисления является получение детерминированных результатов, это может в лучшем случае раздражать, а в худшем — привести к потере миллионов. В частности, когда в смарт-контрактах возникают эти целочисленные ошибки переполнения или потери значимости.
Почему уязвимость Integer Overflow/Underflow может быть настолько разрушительной?
Несмотря на то, что целочисленное переполнение и потеря значимости существуют уже несколько десятилетий, их существование в качестве ошибки в смарт-контракте подняло ставки. Когда злоумышленники используют такие ошибки, они могут лишить смарт-контракт большого количества токенов.
Вероятно, впервые такая ошибка произошла с блоком 74638, который создал миллиарды биткойнов для трех адресов. Потребуются часы, чтобы устранить эту ошибку с помощью софт-форка, который отбрасывал блок, что делало транзакцию недействительной.
Во-первых, транзакции на сумму более 21 миллиона биткойнов были отклонены. Это не отличалось от транзакций переполнения, как и та, которая отправила столько денег на три вышеупомянутых счета.
Однако смарт-контракты Ethereum также сталкивались с целочисленным переполнением и недостаточным переполнением, и BeautyChain также является ярким примером.
В этом случае смарт-контракт содержал одну ошибочную строку кода:
В результате злоумышленники теоретически могли получить неограниченное количество токенов BEC, что теоретически может составлять значение (2²⁵⁶)-1.
Теперь давайте посмотрим на другой пример смарт-контракта, в котором происходит недополнение/переполнение целого числа.
Устранение уязвимости целочисленного переполнения/недополнения в смарт-контракте
На первый взгляд, в этом примере взаимодействуют два контракта, которые демонстрируют, что происходит в случае целочисленного переполнения.
Как вы можете видеть ниже, контракт TimeLock позволяет вам вносить и снимать средства, но с разницей: вы можете выполнять последнее только через определенный период времени. В этом случае вы сможете вывести свои средства только через неделю.
Однако, как только вы вызываете функцию атаки в контракте атаки, временная блокировка больше не действует, и поэтому злоумышленник может немедленно вывести сумму баланса.
Другими словами, из-за целочисленного переполнения оператором type(uint).max+1-timeLock.locktime(address(this)) блокировка по времени устраняется.
Например, после того как вы развернули оба смарт-контракта с помощью приведенного выше кода, вы можете проверить, действует ли временная блокировка, вызвав функции депозита и снятия в контракте TimeLock, как показано ниже:
Как видите, выбрав количество 2 Эфира, мы получим баланс смарт-контракта в 2 Эфира, показанный выше:
В частности, конкретный адрес, на котором хранится баланс 2 эфиров, можно проверить, добавив адрес в поле функции баланса и нажав кнопку баланса:
Однако, как упоминалось выше, вы еще не можете вывести эти средства из-за временной блокировки. Когда вы посмотрите на консоль после нажатия функции вывода средств, вы обнаружите ошибку, обозначенную красным символом «x». Как вы можете видеть ниже, причина этой ошибки указана в контракте: «Время блокировки не истекло»:
Теперь давайте посмотрим на развернутый контракт Attack, как показано ниже:
Теперь, чтобы вызвать функцию атаки, вам необходимо внести депозит в размере 1 эфира или более. Итак, в данном случае мы выбрали 2 эфира, как показано ниже:
После этого нажмите «Атака». Вы обнаружите, что 2 Эфира, которые вы внесли, будут немедленно сняты и добавлены к контракту Атаки, о чем свидетельствует баланс 2 Эфира ниже:
Ясно, что этого не должно происходить из-за того, что длительная временная блокировка должна вступить в силу, как только вы сделаете депозит. Конечно, как мы знаем, оператор type(uint).max+1-timeLock.locktime(address(this)) уменьшает время блокировки с помощью функции увеличенияLockTime. Именно поэтому мы можем немедленно вывести остаток эфира.
Что приводит нас к очевидному вопросу: есть ли способы исправить уязвимость целочисленного переполнения и потери значимости?
2 способа обойти уязвимость целочисленного переполнения/недополнения
Понимая, что уязвимость, связанная с целочисленным переполнением/опустошением, может быть разрушительной, было выпущено несколько исправлений для этой ошибки. Давайте посмотрим на оба эти исправления и на то, как они обходятся с такой ошибкой:
Способ 1. Используйте библиотеку SafeMath от OpenZeppelin
Open Zeppelin как организация предлагает многое, когда речь идет о технологиях и услугах кибербезопасности. math/SafeMath.sol">библиотека SafeMath, являющаяся частью репозитория разработки смарт-контрактов. Этот репозиторий содержит контракты, которые можно импортировать в код вашего смарт-контракта, и библиотека SafeMath является одним из них.
Давайте посмотрим, как одна из функций SafeMath.sol проверяет целочисленное переполнение:
Теперь, когда произошло вычисление a+b, нужно проверить, имеет ли место c<a. Конечно, это верно только в случае целочисленного переполнения.
С версией компилятора Solidity, достигшей версии 0.8.0 и выше, теперь встроены проверки целочисленного переполнения и потери значимости. Таким образом, эту библиотеку все еще можно использовать для проверки этой уязвимости, как при использовании языка, так и этой библиотеки. Конечно, если для вашего смарт-контракта требуется версия компилятора ниже 0.8.+, вы должны использовать эту библиотеку, чтобы избежать переполнения или недополнения.
Способ 2. Используйте версию компилятора 0.8.0
Теперь, как упоминалось ранее, если для вашего смарт-контракта вы используете версию компилятора 0.8.0 и выше, эта версия имеет встроенную проверку на такую уязвимость.
На самом деле, просто чтобы проверить, работает ли он со смарт-контрактом выше, при изменении версии компилятора на «^0.8.0» и его повторном развертывании возникает следующая ошибка «возврата»:
Депозит 2 Эфира естественно не осуществляется, что связано с проверкой на переполнение значения блокировки времени. В результате вывод средств невозможен из-за того, что средства не были внесены.
Без сомнения, вызов функции Attack.attack() здесь не сработал, так что все в порядке!
Подводя итоги уязвимости переполнения/недополнения
Если вы и должны что-то понять из этого длинного сообщения в блоге, так это то, что игнорирование этой уязвимости, как и атаки BEC, может дорого обойтись. Как вы также можете сказать, если не проверять, легко могут возникнуть незлонамеренные ошибки. Или так же просто для хакеров, чтобы использовать эту уязвимость.
Говоря об этом и используя наше понимание того, как произошла атака BEC, распознавание этой уязвимости может иметь большое значение для предотвращения любых атак при написании ваших смарт-контрактов благодаря предлагаемым исправлениям. Даже если есть несколько других уязвимостей в смарт-контрактах, которые подстерегают вас, чтобы сбить вас с толку.
Оригинал