Анализ уязвимостей компилятора Solidity и стратегии реагирования
Компилятор является одной из основных составляющих современных компьютерных систем. Это программа, основная функция которой заключается в преобразовании исходного кода на высокоуровневом языке программирования, который легко понимается и пишется человеком, в исполняемый код инструкций для низкоуровневого процессора или виртуальной машины байт-кода.
Большинство разработчиков и специалистов по безопасности обычно уделяют больше внимания безопасности исходного кода программных приложений, но могут игнорировать проблемы безопасности самого компилятора. На самом деле, компилятор как компьютерная программа также имеет уязвимости, которые в определенных обстоятельствах могут представлять серьезный риск для безопасности. Например, в процессе компиляции и анализа выполнения JavaScript кода на стороне клиента браузер может быть атакован злоумышленниками из-за уязвимости в движке JavaScript, что может привести к выполнению удаленного кода при посещении пользователем вредоносных веб-страниц, в конечном итоге контролируя браузер жертвы или даже операционную систему.
Компиляторы Solidity не являются исключением. Согласно предупреждениям о безопасности от команды разработчиков Solidity, в нескольких различных версиях компиляторов Solidity существуют уязвимости.
Уязвимость компилятора Solidity
Роль компилятора Solidity заключается в преобразовании кода смарт-контрактов, написанного разработчиками, в код инструкций для виртуальной машины Ethereum (EVM). Эти инструкции EVM упаковываются в транзакции и загружаются в Ethereum, а затем в конечном итоге интерпретируются и выполняются EVM.
Необходимо различать уязвимости компилятора Solidity и уязвимости самого EVM. Уязвимости EVM относятся к проблемам безопасности, возникающим при выполнении инструкций виртуальной машины. Поскольку злоумышленники могут загружать произвольный код в Ethereum, этот код в конечном итоге будет выполняться в каждой программе клиентского P2P Ethereum, если в EVM есть уязвимость безопасности, это повлияет на всю сеть Ethereum, что может привести к отказу в обслуживании всего сети (DoS) или даже к полному контролю злоумышленников. Однако, поскольку EVM имеет относительно простую архитектуру и основной код не обновляется часто, вероятность возникновения вышеупомянутых проблем невелика.
Уязвимость компилятора Solidity относится к проблемам, возникающим при преобразовании Solidity в код EVM. В отличие от сценариев, когда браузеры компилируют и выполняют JavaScript на компьютере пользователя, процесс компиляции Solidity происходит только на компьютере разработчика смарт-контрактов и не выполняется в сети Ethereum. Поэтому уязвимости компилятора Solidity не влияют на саму сеть Ethereum.
Основной опасностью уязвимости компилятора Solidity является то, что она может привести к несоответствию сгенерированного EVM-кода ожиданиям разработчиков смарт-контрактов. Поскольку смарт-контракты на Ethereum обычно связаны с криптоактивами пользователей, любые ошибки смарт-контракта, вызванные компилятором, могут привести к потере активов пользователей и серьезным последствиям.
Разработчики и аудиторы контрактов могут сосредоточиться на проблемах реализации логики кода контракта, а также на вопросах безопасности на уровне Solidity, таких как повторные входы и переполнение целых чисел. Однако для обнаружения уязвимостей компилятора Solidity просто провести аудит логики исходного кода контракта будет недостаточно. Необходимо совместное анализирование конкретных версий компилятора и определенных кодовых паттернов, чтобы определить, затрагивает ли уязвимость компилятора смарт-контракт.
Пример уязвимости компилятора Solidity
Следующие реальные примеры уязвимостей компиляторов Solidity демонстрируют их конкретные формы, причины и последствия.
SOL-2016-9 HighOrderByteCleanStorage
Уязвимость существует в ранних версиях компилятора Solidity (>=0.1.6 <0.4.4).
Рассмотрите следующий код:
солидность
контракт C {
uint32 a = 0x1234;
uint32 b = 0;
функция f() публичная {
a += 1;
}
функция run() публичный просмотр возвращает (uint32) {
вернуть b;
}
}
Переменная хранения b не была изменена, поэтому функция run() должна возвращать значение по умолчанию 0. Но в коде, сгенерированном уязвимой версией компилятора, функция run() вернет 1.
Если не понимать уязвимость компилятора, обычным разработчикам будет трудно обнаружить ошибки в приведенном выше коде простым обзором кода. Это всего лишь простой пример, который не приведет к особенно серьезным последствиям. Но если переменная b используется для проверки прав, учета активов и т. д., такое несоответствие ожиданиям может привести к очень серьезным проблемам.
Причина этого странного явления заключается в том, что EVM использует стековую виртуальную машину, где каждый элемент стека имеет размер 32 байта (, что соответствует размеру переменной uint256 ). С другой стороны, каждый слот в базовом хранилище (storage) также имеет размер 32 байта. На уровне языка Solidity поддерживаются различные типы данных, меньшие 32 байт, такие как uint32, и компилятор, обрабатывая такие переменные, должен выполнять соответствующие операции очистки высоких битов (clean up) для обеспечения корректности данных. В приведенной ситуации, когда сложение приводит к целочисленному переполнению, компилятор не выполнил правильную очистку высоких битов результата, что привело к тому, что высокие биты 1 были записаны в хранилище, в конечном итоге перезаписав переменную a, изменив значение переменной b на 1.
солидность
контракт C {
функция f() публичная чистая возвращает (uint) {
сборка {
mstore(0, 0x42)
}
uint x;
сборка {
x := mload(0)
}
вернуть x;
}
}
Уязвимость существует в компиляторах версий >=0.8.13 <0.8.15. Компилятор Solidity в процессе преобразования языка Solidity в код EVM не просто выполняет простую трансляцию. Он также проводит глубокий анализ управления потоком и данных, реализует различные процессы оптимизации компиляции для уменьшения объема генерируемого кода и оптимизации расхода газа в процессе выполнения. Такие операции оптимизации встречаются во многих компиляторах высокоуровневых языков, но из-за сложности рассматриваемых ситуаций они также могут привести к появлению ошибок или уязвимостей безопасности.
Уязвимость кода выше возникает из-за таких оптимизационных действий. Предположим, что в некоторой функции есть код, изменяющий данные по нулевому смещению в памяти, но в дальнейшем эти данные нигде не используются, тогда на самом деле можно напрямую удалить код изменения памяти 0, что позволит сэкономить газ и не повлияет на последующую логику программы.
Эта стратегия оптимизации сама по себе не имеет проблем, но в конкретной реализации кода компилятора Solidity такая оптимизация применяется только к отдельному блоку assembly. В приведенном выше коде PoC запись и доступ к памяти 0 находятся в двух разных блоках assembly, тогда как компилятор анализирует и оптимизирует только отдельный блок assembly. Поскольку в первом блоке assembly после записи в память 0 нет никаких операций чтения, это приводит к тому, что данная команда записи считается избыточной и будет удалена, что вызовет ошибку. В уязвимой версии функция f() вернет значение 0, в то время как на самом деле данный код должен вернуть правильное значение 0x42.
солидность
контракт C {
function f(string[1] calldata a) внешних возвращает (string memory) {
вернуть abi.decode(abi.encode(a), (строка));
}
}
Уязвимость затрагивает компиляторы версий >= 0.5.8 < 0.8.16. Обычно переменная a, возвращаемая вышеуказанным кодом, должна быть "aaaa". Однако в уязвимой версии она возвращает пустую строку "".
Причиной уязвимости является ошибка в Solidity, которая при выполнении операции abi.encode над массивами типа calldata неправильно очищает некоторые данные, что приводит к изменению соседних данных и вызывает несоответствие между закодированными и декодированными данными.
Стоит отметить, что при выполнении внешних вызовов и эмитировании событий в Solidity параметры неявно кодируются с помощью abi.encode, поэтому вероятность появления указанной уязвимости выше, чем может показаться на первый взгляд.
Рекомендации по безопасности
Команда по безопасности блокчейна Cobo, проанализировав модель угроз уязвимостей компилятора Solidity и изучив исторические уязвимости, предлагает следующие рекомендации для разработчиков и специалистов по безопасности.
Для разработчиков:
Используйте более новую версию компилятора Solidity. Хотя новая версия может также вводить новые проблемы безопасности, но известных проблем безопасности обычно меньше, чем в старой версии.
Улучшите тестовые случаи модулей. Большинство багов на уровне компилятора приводят к тому, что результаты выполнения кода не соответствуют ожиданиям. Эти проблемы трудно выявить при код-ревью, но их легко обнаружить на этапе тестирования. Поэтому, увеличивая покрытие кода, можно в максимальной степени избежать таких проблем.
Старайтесь избегать использования встроенного ассемблера, сложных операций с многомерными массивами и структурой ABI, если нет явной необходимости; избегайте слепого использования новых языковых функций и экспериментальных возможностей ради демонстрации мастерства. Согласно анализу команды безопасности Cobo по историческим уязвимостям Solidity, большинство уязвимостей связано с встроенным ассемблером и операциями с кодировщиком ABI. Компилятор действительно может чаще выдавать ошибки при обработке сложных языковых особенностей. С другой стороны, разработчики также могут легко допустить ошибки при использовании новых функций, что может привести к проблемам с безопасностью.
Для охранных служб:
При проведении безопасности аудита кода Solidity не игнорируйте потенциальные риски безопасности, которые могут быть связаны с компилятором Solidity. Соответствующий пункт проверки в Smart Contract Weakness Classification(SWC) - это SWC-102: Устаревшая версия компилятора.
В процессе внутренней разработки SDL настоятельно призываю команду разработчиков обновить версию компилятора Solidity и рассмотреть возможность введения автоматической проверки версии компилятора в процесс CI/CD.
Но не стоит слишком паниковать по поводу уязвимостей компилятора; большинство уязвимостей компилятора возникают только в определенных моделях кода, и использование уязвимой версии компилятора не обязательно означает наличие рисков безопасности для смарт-контрактов. Фактическое воздействие на безопасность необходимо оценивать в зависимости от конкретного проекта.
Практические ресурсы:
Публикации о Security Alerts от команды Solidity, выпускаемые регулярно:
Официальный репозиторий Solidity регулярно обновляет список ошибок:
Список ошибок компилятора для всех версий:
На странице Code контракта на Etherscan в правом верхнем углу треугольный восклицательный знак может указать на существующие уязвимости в компиляторе текущей версии.
Итог
В этой статье рассматриваются основные концепции компилятора, представляются уязвимости компилятора Solidity и анализируются возможные риски безопасности, которые они могут создать в реальной среде разработки Ethereum. В конце статьи предоставляются несколько практических рекомендаций для разработчиков и специалистов по безопасности.
На этой странице может содержаться сторонний контент, который предоставляется исключительно в информационных целях (не в качестве заявлений/гарантий) и не должен рассматриваться как поддержка взглядов компании Gate или как финансовый или профессиональный совет. Подробности смотрите в разделе «Отказ от ответственности» .
11 Лайков
Награда
11
5
Поделиться
комментарий
0/400
staking_gramps
· 16ч назад
Куда больше уязвимостей, кто теперь осмелится разрабатывать?
Посмотреть ОригиналОтветить0
JustHereForMemes
· 07-23 05:15
У компилятора тоже большие проблемы, я ухожу, ухожу.
Посмотреть ОригиналОтветить0
BakedCatFanboy
· 07-22 21:56
Уязвимости только подбирать, да?
Посмотреть ОригиналОтветить0
OldLeekNewSickle
· 07-22 21:56
Высококлассные неудачники ищут дыры, низшие неудачники следят за Книгой ордеров
Посмотреть ОригиналОтветить0
RektCoaster
· 07-22 21:52
Компилятор тоже выдает ошибки? Это действительно заставляет меня вспыхнуть и взорваться.
Анализ уязвимостей компилятора Solidity и стратегии безопасности для разработчиков
Анализ уязвимостей компилятора Solidity и стратегии реагирования
Компилятор является одной из основных составляющих современных компьютерных систем. Это программа, основная функция которой заключается в преобразовании исходного кода на высокоуровневом языке программирования, который легко понимается и пишется человеком, в исполняемый код инструкций для низкоуровневого процессора или виртуальной машины байт-кода.
Большинство разработчиков и специалистов по безопасности обычно уделяют больше внимания безопасности исходного кода программных приложений, но могут игнорировать проблемы безопасности самого компилятора. На самом деле, компилятор как компьютерная программа также имеет уязвимости, которые в определенных обстоятельствах могут представлять серьезный риск для безопасности. Например, в процессе компиляции и анализа выполнения JavaScript кода на стороне клиента браузер может быть атакован злоумышленниками из-за уязвимости в движке JavaScript, что может привести к выполнению удаленного кода при посещении пользователем вредоносных веб-страниц, в конечном итоге контролируя браузер жертвы или даже операционную систему.
Компиляторы Solidity не являются исключением. Согласно предупреждениям о безопасности от команды разработчиков Solidity, в нескольких различных версиях компиляторов Solidity существуют уязвимости.
Уязвимость компилятора Solidity
Роль компилятора Solidity заключается в преобразовании кода смарт-контрактов, написанного разработчиками, в код инструкций для виртуальной машины Ethereum (EVM). Эти инструкции EVM упаковываются в транзакции и загружаются в Ethereum, а затем в конечном итоге интерпретируются и выполняются EVM.
Необходимо различать уязвимости компилятора Solidity и уязвимости самого EVM. Уязвимости EVM относятся к проблемам безопасности, возникающим при выполнении инструкций виртуальной машины. Поскольку злоумышленники могут загружать произвольный код в Ethereum, этот код в конечном итоге будет выполняться в каждой программе клиентского P2P Ethereum, если в EVM есть уязвимость безопасности, это повлияет на всю сеть Ethereum, что может привести к отказу в обслуживании всего сети (DoS) или даже к полному контролю злоумышленников. Однако, поскольку EVM имеет относительно простую архитектуру и основной код не обновляется часто, вероятность возникновения вышеупомянутых проблем невелика.
Уязвимость компилятора Solidity относится к проблемам, возникающим при преобразовании Solidity в код EVM. В отличие от сценариев, когда браузеры компилируют и выполняют JavaScript на компьютере пользователя, процесс компиляции Solidity происходит только на компьютере разработчика смарт-контрактов и не выполняется в сети Ethereum. Поэтому уязвимости компилятора Solidity не влияют на саму сеть Ethereum.
Основной опасностью уязвимости компилятора Solidity является то, что она может привести к несоответствию сгенерированного EVM-кода ожиданиям разработчиков смарт-контрактов. Поскольку смарт-контракты на Ethereum обычно связаны с криптоактивами пользователей, любые ошибки смарт-контракта, вызванные компилятором, могут привести к потере активов пользователей и серьезным последствиям.
Разработчики и аудиторы контрактов могут сосредоточиться на проблемах реализации логики кода контракта, а также на вопросах безопасности на уровне Solidity, таких как повторные входы и переполнение целых чисел. Однако для обнаружения уязвимостей компилятора Solidity просто провести аудит логики исходного кода контракта будет недостаточно. Необходимо совместное анализирование конкретных версий компилятора и определенных кодовых паттернов, чтобы определить, затрагивает ли уязвимость компилятора смарт-контракт.
Пример уязвимости компилятора Solidity
Следующие реальные примеры уязвимостей компиляторов Solidity демонстрируют их конкретные формы, причины и последствия.
SOL-2016-9 HighOrderByteCleanStorage
Уязвимость существует в ранних версиях компилятора Solidity (>=0.1.6 <0.4.4).
Рассмотрите следующий код:
солидность контракт C { uint32 a = 0x1234; uint32 b = 0; функция f() публичная { a += 1; } функция run() публичный просмотр возвращает (uint32) { вернуть b; } }
Переменная хранения b не была изменена, поэтому функция run() должна возвращать значение по умолчанию 0. Но в коде, сгенерированном уязвимой версией компилятора, функция run() вернет 1.
Если не понимать уязвимость компилятора, обычным разработчикам будет трудно обнаружить ошибки в приведенном выше коде простым обзором кода. Это всего лишь простой пример, который не приведет к особенно серьезным последствиям. Но если переменная b используется для проверки прав, учета активов и т. д., такое несоответствие ожиданиям может привести к очень серьезным проблемам.
Причина этого странного явления заключается в том, что EVM использует стековую виртуальную машину, где каждый элемент стека имеет размер 32 байта (, что соответствует размеру переменной uint256 ). С другой стороны, каждый слот в базовом хранилище (storage) также имеет размер 32 байта. На уровне языка Solidity поддерживаются различные типы данных, меньшие 32 байт, такие как uint32, и компилятор, обрабатывая такие переменные, должен выполнять соответствующие операции очистки высоких битов (clean up) для обеспечения корректности данных. В приведенной ситуации, когда сложение приводит к целочисленному переполнению, компилятор не выполнил правильную очистку высоких битов результата, что привело к тому, что высокие биты 1 были записаны в хранилище, в конечном итоге перезаписав переменную a, изменив значение переменной b на 1.
SOL-2022-4 ВстроенныеАссемблерныеПобочныеЭффектыПамяти
Рассмотрим следующий код:
солидность контракт C { функция f() публичная чистая возвращает (uint) { сборка { mstore(0, 0x42) } uint x; сборка { x := mload(0) } вернуть x; } }
Уязвимость существует в компиляторах версий >=0.8.13 <0.8.15. Компилятор Solidity в процессе преобразования языка Solidity в код EVM не просто выполняет простую трансляцию. Он также проводит глубокий анализ управления потоком и данных, реализует различные процессы оптимизации компиляции для уменьшения объема генерируемого кода и оптимизации расхода газа в процессе выполнения. Такие операции оптимизации встречаются во многих компиляторах высокоуровневых языков, но из-за сложности рассматриваемых ситуаций они также могут привести к появлению ошибок или уязвимостей безопасности.
Уязвимость кода выше возникает из-за таких оптимизационных действий. Предположим, что в некоторой функции есть код, изменяющий данные по нулевому смещению в памяти, но в дальнейшем эти данные нигде не используются, тогда на самом деле можно напрямую удалить код изменения памяти 0, что позволит сэкономить газ и не повлияет на последующую логику программы.
Эта стратегия оптимизации сама по себе не имеет проблем, но в конкретной реализации кода компилятора Solidity такая оптимизация применяется только к отдельному блоку assembly. В приведенном выше коде PoC запись и доступ к памяти 0 находятся в двух разных блоках assembly, тогда как компилятор анализирует и оптимизирует только отдельный блок assembly. Поскольку в первом блоке assembly после записи в память 0 нет никаких операций чтения, это приводит к тому, что данная команда записи считается избыточной и будет удалена, что вызовет ошибку. В уязвимой версии функция f() вернет значение 0, в то время как на самом деле данный код должен вернуть правильное значение 0x42.
SOL-2022-6 AbiReencodingHeadOverflowWithStaticArrayCleanup
Рассмотрите следующий код:
солидность контракт C { function f(string[1] calldata a) внешних возвращает (string memory) { вернуть abi.decode(abi.encode(a), (строка)); } }
Уязвимость затрагивает компиляторы версий >= 0.5.8 < 0.8.16. Обычно переменная a, возвращаемая вышеуказанным кодом, должна быть "aaaa". Однако в уязвимой версии она возвращает пустую строку "".
Причиной уязвимости является ошибка в Solidity, которая при выполнении операции abi.encode над массивами типа calldata неправильно очищает некоторые данные, что приводит к изменению соседних данных и вызывает несоответствие между закодированными и декодированными данными.
Стоит отметить, что при выполнении внешних вызовов и эмитировании событий в Solidity параметры неявно кодируются с помощью abi.encode, поэтому вероятность появления указанной уязвимости выше, чем может показаться на первый взгляд.
Рекомендации по безопасности
Команда по безопасности блокчейна Cobo, проанализировав модель угроз уязвимостей компилятора Solidity и изучив исторические уязвимости, предлагает следующие рекомендации для разработчиков и специалистов по безопасности.
Для разработчиков:
Используйте более новую версию компилятора Solidity. Хотя новая версия может также вводить новые проблемы безопасности, но известных проблем безопасности обычно меньше, чем в старой версии.
Улучшите тестовые случаи модулей. Большинство багов на уровне компилятора приводят к тому, что результаты выполнения кода не соответствуют ожиданиям. Эти проблемы трудно выявить при код-ревью, но их легко обнаружить на этапе тестирования. Поэтому, увеличивая покрытие кода, можно в максимальной степени избежать таких проблем.
Старайтесь избегать использования встроенного ассемблера, сложных операций с многомерными массивами и структурой ABI, если нет явной необходимости; избегайте слепого использования новых языковых функций и экспериментальных возможностей ради демонстрации мастерства. Согласно анализу команды безопасности Cobo по историческим уязвимостям Solidity, большинство уязвимостей связано с встроенным ассемблером и операциями с кодировщиком ABI. Компилятор действительно может чаще выдавать ошибки при обработке сложных языковых особенностей. С другой стороны, разработчики также могут легко допустить ошибки при использовании новых функций, что может привести к проблемам с безопасностью.
Для охранных служб:
При проведении безопасности аудита кода Solidity не игнорируйте потенциальные риски безопасности, которые могут быть связаны с компилятором Solidity. Соответствующий пункт проверки в Smart Contract Weakness Classification(SWC) - это SWC-102: Устаревшая версия компилятора.
В процессе внутренней разработки SDL настоятельно призываю команду разработчиков обновить версию компилятора Solidity и рассмотреть возможность введения автоматической проверки версии компилятора в процесс CI/CD.
Но не стоит слишком паниковать по поводу уязвимостей компилятора; большинство уязвимостей компилятора возникают только в определенных моделях кода, и использование уязвимой версии компилятора не обязательно означает наличие рисков безопасности для смарт-контрактов. Фактическое воздействие на безопасность необходимо оценивать в зависимости от конкретного проекта.
Практические ресурсы:
Публикации о Security Alerts от команды Solidity, выпускаемые регулярно:
Официальный репозиторий Solidity регулярно обновляет список ошибок:
Список ошибок компилятора для всех версий:
На странице Code контракта на Etherscan в правом верхнем углу треугольный восклицательный знак может указать на существующие уязвимости в компиляторе текущей версии.
Итог
В этой статье рассматриваются основные концепции компилятора, представляются уязвимости компилятора Solidity и анализируются возможные риски безопасности, которые они могут создать в реальной среде разработки Ethereum. В конце статьи предоставляются несколько практических рекомендаций для разработчиков и специалистов по безопасности.