Команда CertiK Skyfall недавно обнаружила несколько уязвимостей в RPC-узлах на основе Rust в нескольких блокчейнах, включая Aptos, StarCoin и Sui. Поскольку узлы RPC являются критически важными компонентами инфраструктуры, соединяющими dApps и базовый блокчейн, их надежность имеет решающее значение для бесперебойной работы. Разработчики блокчейна знают о важности стабильных сервисов RPC, поэтому они используют безопасные для памяти языки, такие как Rust, чтобы избежать распространенных уязвимостей, которые могут разрушить узлы RPC.
Принятие безопасного для памяти языка, такого как Rust, помогает узлам RPC избежать многих атак, основанных на уязвимостях повреждения памяти. Однако в ходе недавнего аудита мы обнаружили, что даже безопасные для памяти реализации Rust, если они не будут тщательно спроектированы и проверены, могут быть уязвимы для определенных угроз безопасности, которые могут нарушить доступность служб RPC.
В этой статье мы расскажем о нашем обнаружении ряда уязвимостей на практических примерах.
Роль узла RPC в блокчейне
Служба удаленного вызова процедур (RPC) блокчейна является основным компонентом инфраструктуры блокчейна уровня 1. Он предоставляет пользователям важный интерфейс API и выступает в качестве шлюза к серверной сети блокчейна. Однако служба блокчейна RPC отличается от традиционной службы RPC тем, что облегчает взаимодействие с пользователем без аутентификации. Непрерывная доступность сервиса имеет решающее значение, и любой сбой в обслуживании может серьезно повлиять на доступность базовой цепочки блоков.
Точка зрения аудита: традиционный RPC-сервер VS блокчейн RPC-сервер
Аудит традиционных серверов RPC в основном фокусируется на проверке входных данных, авторизации/аутентификации, подделке межсайтовых запросов/подделке запросов на стороне сервера (CSRF/SSRF), уязвимостях внедрения (таких как внедрение SQL, внедрение команд) и утечке информации.
Однако для серверов RPC с блокчейном ситуация иная. Пока транзакция подписана, нет необходимости аутентифицировать запрашивающего клиента на уровне RPC. Одной из основных целей службы RPC, являющейся интерфейсом блокчейна, является обеспечение ее доступности. В случае сбоя пользователи не могут взаимодействовать с блокчейном, что не позволяет им запрашивать данные в цепочке, отправлять транзакции или выполнять функции контракта.
Следовательно, наиболее уязвимым аспектом RPC-сервера блокчейна является «доступность». Если сервер выходит из строя, пользователи теряют возможность взаимодействовать с блокчейном. Что более серьезно, так это то, что некоторые атаки будут распространяться по цепочке, затрагивать большое количество узлов и даже приводить к параличу всей сети.
Почему новый блокчейн будет использовать безопасный для памяти RPC
Некоторые известные блокчейны уровня 1, такие как Aptos и Sui, используют безопасный для памяти язык программирования Rust для реализации своих сервисов RPC. Благодаря высокой безопасности и строгим проверкам во время компиляции, Rust делает программы практически невосприимчивыми к уязвимостям повреждения памяти, таким как переполнение стека, разыменование нулевого указателя и уязвимости повторной ссылки после освобождения.
Чтобы еще больше обезопасить кодовую базу, разработчики строго следуют рекомендациям, например не добавляют небезопасный код. Используйте #![forbid(unsafe_code)] в исходном коде, чтобы обеспечить блокировку и фильтрацию небезопасного кода.
Примеры разработчиков блокчейнов, реализующих методы программирования на Rust
Чтобы предотвратить целочисленное переполнение, разработчики обычно используют функции типа checked_add, checked_sub, saturating_add, saturating_sub и т. д. вместо простого сложения и вычитания (+, -). Смягчите исчерпание ресурсов, установив соответствующие тайм-ауты, ограничения размера запроса и ограничения элемента запроса.
Угрозы RPC безопасности памяти в блокчейне уровня 1
Несмотря на то, что узлы RPC не уязвимы к незащищенности памяти в традиционном смысле, злоумышленники могут легко манипулировать входными данными. В реализации безопасного для памяти RPC существует несколько ситуаций, которые могут привести к отказу в обслуживании. Например, расширение памяти может исчерпать память службы, а логические проблемы могут привести к бесконечным циклам. Кроме того, условия гонки могут представлять угрозу, из-за которой параллельные операции могут иметь неожиданную последовательность событий, оставляя систему в неопределенном состоянии. Кроме того, неправильно управляемые зависимости и сторонние библиотеки могут создавать в системе неизвестные уязвимости.
В этом посте наша цель — привлечь внимание к более непосредственным способам срабатывания средств защиты среды выполнения Rust, вызывающих самопроизвольное прерывание служб.
Явная паника в Rust: способ напрямую прекратить службы RPC
Разработчики могут преднамеренно или непреднамеренно ввести явный код паники. Эти коды в основном используются для обработки непредвиденных или исключительных ситуаций. Некоторые распространенные примеры включают в себя:
assert!(): Используйте этот макрос, когда должно быть выполнено условие. Если заявленное условие не выполняется, программа запаникует, указывая на серьезную ошибку в коде.
panic!(): эта функция вызывается, когда программа сталкивается с ошибкой, после которой она не может восстановиться и продолжить работу.
unreachable!(): Используйте этот макрос, когда часть кода не должна выполняться. Если этот макрос вызывается, это указывает на серьезную логическую ошибку.
unimplemented!() и todo!(): эти макросы замещают нереализованные функции. Если это значение будет достигнуто, программа рухнет.
unwrap(): этот метод используется для типов Option или Result.Когда встречается переменная Err или None, программа аварийно завершает работу.
Уязвимость 1: инициировать утверждение в Move Verifier!
Блокчейн Aptos использует верификатор байт-кода Move для выполнения эталонного анализа безопасности посредством абстрактной интерпретации байт-кода. Функция ute() является частью реализации трейта TransferFunctions и имитирует выполнение инструкций байт-кода в базовых блоках.
Задача функции ute_inner() состоит в том, чтобы интерпретировать текущую инструкцию байт-кода и соответствующим образом обновить состояние. Если мы выполнили последнюю инструкцию в базовом блоке, на что указывает index == last_index, функция вызовет assert!(self.stack.is_empty()), чтобы гарантировать, что стек пуст. Смысл этого поведения в том, чтобы гарантировать, что все операции сбалансированы, что также означает, что у каждого нажатия есть соответствующее всплывающее окно.
В обычном потоке выполнения стек всегда сбалансирован во время абстрактной интерпретации. Это гарантирует средство проверки баланса стека, которое проверяет байт-код перед его интерпретацией. Однако, как только мы расширим нашу перспективу до области абстрактных интерпретаторов, мы увидим, что предположение о балансе стека не всегда верно.
Патч для уязвимости analysis_function в AbstractInterpreter
По своей сути абстрактный интерпретатор эмулирует байт-код на уровне базового блока. В его первоначальной реализации обнаружение ошибки во время ute_block побуждало процесс анализа регистрировать ошибку и продолжать выполнение к следующему блоку в графе потока управления. Это может создать ситуацию, когда ошибка в исполнительном блоке может привести к несбалансированности стека. Если в этом случае выполнение продолжится, будет выполнена проверка assert!, если стек не пуст, что вызовет панику.
Это дает злоумышленникам возможность использовать их. Злоумышленник может вызвать ошибку, создав специальный байт-код в ute_block(), а затем ute() может выполнить утверждение, если стек не пуст, что приведет к сбою проверки утверждения. Это еще больше вызовет панику и прекратит работу службы RPC, что повлияет на ее доступность.
Чтобы предотвратить это, реализованное исправление обеспечивает остановку всего процесса анализа, когда функция ute_block впервые сталкивается с ошибкой, тем самым избегая риска последующих сбоев, которые могут возникнуть при продолжении анализа из-за дисбаланса стека из-за ошибок. Эта модификация устраняет условия, которые могли вызвать панику, и помогает повысить надежность и безопасность абстрактного интерпретатора.
** Уязвимость 2: вызвать панику в StarCoin! **
Блокчейн Starcoin имеет собственный форк реализации Move. В этом репозитории Move есть паника в конструкторе типа Struct!Если предоставленное StructDefinition имеет информацию о собственном поле, паника будет вызвана явно! .
Явные паники для инициализированных структур в процедурах нормализации
Этот потенциальный риск существует в процессе распространения модулей. Если опубликованный модуль уже существует в хранилище данных, нормализация модуля требуется как для существующего модуля, так и для входного модуля, контролируемого злоумышленником. Во время этого процесса функция «normalized::Module::new» строит структуру модуля из входных модулей, контролируемых злоумышленником, вызывая «панику!».
Предпосылки для процедуры нормализации
Эта паника может быть вызвана отправкой специально созданной полезной нагрузки от клиента. Таким образом, злоумышленники могут нарушить доступность служб RPC.
Патч паники при инициализации структуры
Патч Starcoin вводит новое поведение для обработки случая Native. Теперь вместо паники возвращается пустой ec. Это снижает вероятность того, что пользователи отправят данные, вызывающие панику.
Скрытая паника в Rust: легко упускаемый из виду способ прекращения работы RPC-сервисов
Явные паники легко распознаются в исходном коде, в то время как неявные паники, скорее всего, будут игнорироваться разработчиками. Неявные паники обычно возникают при использовании API, предоставляемых стандартными или сторонними библиотеками. Разработчики должны внимательно прочитать и понять документацию по API, иначе их программы на Rust могут неожиданно остановиться.
Неявная паника в BTreeMap
В качестве примера возьмем BTreeMap из Rust STD. BTreeMap — это широко используемая структура данных, которая организует пары ключ-значение в отсортированное двоичное дерево. BTreeMap предоставляет два метода получения значений по ключу: get(&self, key: &Q) и index(&self, key: &Q).
Метод get(&self, key: &Q) извлекает значение с помощью ключа и возвращает параметр. Опция может быть Some(&V), если ключ существует, вернуть ссылку значения, если ключ не найден в BTreeMap, вернуть None.
С другой стороны, index(&self, key: &Q) напрямую возвращает ссылку на значение, соответствующее ключу. Однако у него есть большой риск: он вызовет неявную панику, если ключ не существует в BTreeMap. При неправильном обращении программа может неожиданно выйти из строя, что делает ее потенциальной уязвимостью.
Фактически, метод index(&self, key: &Q) является базовой реализацией трейта std::ops::Index. Этот трейт представляет собой индексную операцию в неизменяемом контексте (т. е. в контейнере). [index] ) предоставляет удобный синтаксический сахар. Разработчики могут напрямую использовать btree_map [key] , вызовите метод index(&self, key: &Q). Однако они могут игнорировать тот факт, что такое использование может привести к панике, если ключ не будет найден, что создает неявную угрозу стабильности программы.
Уязвимость 3: вызвать неявную панику в Sui RPC
Процедура выпуска модуля Sui позволяет пользователям отправлять полезные данные модуля через RPC. Обработчик RPC использует функцию SuiCommand::Publish для непосредственной дизассемблирования полученного модуля перед пересылкой запроса во внутреннюю сеть проверки для проверки байт-кода.
Во время этой разборки раздел code_unit представленного модуля используется для построения VMControlFlowGraph. Процесс сборки состоит из создания базовых блоков, которые хранятся в BTreeMap под названием «блоки». Процесс включает в себя создание карты и управление ею, где при определенных условиях срабатывает неявная паника.
Вот упрощенный код:
Неявная паника при создании VMControlFlowGraph
В этом коде новый VMControlFlowGraph создается путем обхода кода и создания нового базового блока для каждой единицы кода. Базовые блоки хранятся в именованном блоке BTreeMap.
Карта блоков индексируется с помощью block[&block] в цикле, который перебирает стек, который был инициализирован с помощью ENTRY_BLOCK_ID. Здесь предполагается, что в карте блоков есть хотя бы один ENTRY_BLOCK_ID.
Однако это предположение выполняется не всегда. Например, если зафиксированный код пуст, «карта блоков» останется пустой после процесса «создания базового блока». Когда позже код попытается пройти карту блоков, используя for succ в &blocks[&block].successors , может возникнуть неявная паника, если ключ не найден. Это связано с тем, что выражение blocks[&block] по сути является вызовом метода index(), который, как упоминалось ранее, вызовет панику, если ключ не существует в BTreeMap.
Злоумышленник с удаленным доступом может воспользоваться уязвимостью в этой функции, отправив неверно сформированную полезную нагрузку модуля с пустым полем code_unit. Этот простой запрос RPC приводит к сбою всего процесса JSON-RPC. Если злоумышленник продолжит отправлять такие искаженные полезные данные с минимальными усилиями, это приведет к устойчивому прерыванию обслуживания. В сети блокчейна это означает, что сеть может быть не в состоянии подтвердить новые транзакции, что приводит к ситуации отказа в обслуживании (DoS). Функциональность сети и доверие пользователей к системе будут серьезно затронуты.
Исправление Sui: удалить дизассемблирование из процедуры обработки RPC.
Стоит отметить, что CodeUnitVerifier в Move Bytecode Verifier отвечает за то, чтобы раздел code_unit никогда не был пустым. Однако порядок операций подвергает обработчики RPC потенциальным уязвимостям. Это связано с тем, что процесс проверки происходит на узле Validator, который является этапом после того, как RPC обрабатывает входные модули.
В ответ на эту проблему Sui устранил уязвимость, удалив функцию дизассемблирования в процедуре RPC выпуска модуля. Это эффективный способ предотвратить обработку службами RPC потенциально опасного непроверенного байт-кода.
Кроме того, стоит отметить, что другие методы RPC, связанные с поиском объектов, также содержат возможности дизассемблирования, но они не уязвимы для использования пустых ячеек кода. Это потому, что они всегда запрашивают и дизассемблируют существующие опубликованные модули. Опубликованные модули должны быть проверены, поэтому при построении VMControlFlowGraph всегда выполняется предположение о непустых ячейках кода.
Предложения для разработчиков
Поняв угрозы стабильности сервисов RPC в блокчейнах от явных и неявных паник, разработчики должны освоить стратегии для предотвращения или снижения этих рисков. Эти стратегии могут снизить вероятность незапланированных отключений услуг и повысить отказоустойчивость системы. Поэтому команда экспертов CertiK выдвигает следующие предложения и перечисляет их в качестве лучших практик программирования на Rust.
Абстракция паники в Rust: по возможности рассмотрите возможность использования функции catch_unwind Rust для перехвата паники и преобразования ее в сообщения об ошибках. Это предотвращает сбой всей программы и позволяет разработчикам контролируемо обрабатывать ошибки.
Используйте API с осторожностью: неявные паники обычно возникают из-за неправильного использования API, предоставляемых стандартными или сторонними библиотеками. Поэтому крайне важно полностью понять API и научиться правильно обрабатывать потенциальные ошибки. Разработчики всегда должны предполагать, что API могут дать сбой, и готовиться к таким ситуациям.
Правильная обработка ошибок: используйте типы Result и Option для обработки ошибок вместо того, чтобы прибегать к панике. Первый обеспечивает более контролируемый способ обработки ошибок и особых случаев.
Добавьте документацию и комментарии: убедитесь, что ваш код хорошо документирован, и добавьте комментарии к критическим разделам (включая те, где может возникнуть паника). Это поможет другим разработчикам понять потенциальные риски и эффективно с ними справиться.
Подведем итог
Узлы RPC на основе Rust играют важную роль в таких блокчейн-системах, как Aptos, StarCoin и Sui. Поскольку они используются для соединения DApps и базовой цепочки блоков, их надежность имеет решающее значение для бесперебойной работы системы цепочки блоков. Хотя в этих системах используется безопасный для памяти язык Rust, все же существует риск некачественного дизайна. Исследовательская группа CertiK изучила эти риски на реальных примерах, которые демонстрируют необходимость тщательного и тщательного проектирования безопасного для памяти программирования.
Посмотреть Оригинал
This page may contain third-party content, which is provided for information purposes only (not representations/warranties) and should not be considered as an endorsement of its views by Gate, nor as financial or professional advice. See Disclaimer for details.
Сбой RPC: анализ уязвимости нового типа в безопасных для памяти RPC-узлах блокчейна
Команда CertiK Skyfall недавно обнаружила несколько уязвимостей в RPC-узлах на основе Rust в нескольких блокчейнах, включая Aptos, StarCoin и Sui. Поскольку узлы RPC являются критически важными компонентами инфраструктуры, соединяющими dApps и базовый блокчейн, их надежность имеет решающее значение для бесперебойной работы. Разработчики блокчейна знают о важности стабильных сервисов RPC, поэтому они используют безопасные для памяти языки, такие как Rust, чтобы избежать распространенных уязвимостей, которые могут разрушить узлы RPC.
Принятие безопасного для памяти языка, такого как Rust, помогает узлам RPC избежать многих атак, основанных на уязвимостях повреждения памяти. Однако в ходе недавнего аудита мы обнаружили, что даже безопасные для памяти реализации Rust, если они не будут тщательно спроектированы и проверены, могут быть уязвимы для определенных угроз безопасности, которые могут нарушить доступность служб RPC.
В этой статье мы расскажем о нашем обнаружении ряда уязвимостей на практических примерах.
Роль узла RPC в блокчейне
Служба удаленного вызова процедур (RPC) блокчейна является основным компонентом инфраструктуры блокчейна уровня 1. Он предоставляет пользователям важный интерфейс API и выступает в качестве шлюза к серверной сети блокчейна. Однако служба блокчейна RPC отличается от традиционной службы RPC тем, что облегчает взаимодействие с пользователем без аутентификации. Непрерывная доступность сервиса имеет решающее значение, и любой сбой в обслуживании может серьезно повлиять на доступность базовой цепочки блоков.
Точка зрения аудита: традиционный RPC-сервер VS блокчейн RPC-сервер
Аудит традиционных серверов RPC в основном фокусируется на проверке входных данных, авторизации/аутентификации, подделке межсайтовых запросов/подделке запросов на стороне сервера (CSRF/SSRF), уязвимостях внедрения (таких как внедрение SQL, внедрение команд) и утечке информации.
Однако для серверов RPC с блокчейном ситуация иная. Пока транзакция подписана, нет необходимости аутентифицировать запрашивающего клиента на уровне RPC. Одной из основных целей службы RPC, являющейся интерфейсом блокчейна, является обеспечение ее доступности. В случае сбоя пользователи не могут взаимодействовать с блокчейном, что не позволяет им запрашивать данные в цепочке, отправлять транзакции или выполнять функции контракта.
Следовательно, наиболее уязвимым аспектом RPC-сервера блокчейна является «доступность». Если сервер выходит из строя, пользователи теряют возможность взаимодействовать с блокчейном. Что более серьезно, так это то, что некоторые атаки будут распространяться по цепочке, затрагивать большое количество узлов и даже приводить к параличу всей сети.
Почему новый блокчейн будет использовать безопасный для памяти RPC
Некоторые известные блокчейны уровня 1, такие как Aptos и Sui, используют безопасный для памяти язык программирования Rust для реализации своих сервисов RPC. Благодаря высокой безопасности и строгим проверкам во время компиляции, Rust делает программы практически невосприимчивыми к уязвимостям повреждения памяти, таким как переполнение стека, разыменование нулевого указателя и уязвимости повторной ссылки после освобождения.
Чтобы еще больше обезопасить кодовую базу, разработчики строго следуют рекомендациям, например не добавляют небезопасный код. Используйте #![forbid(unsafe_code)] в исходном коде, чтобы обеспечить блокировку и фильтрацию небезопасного кода.
Примеры разработчиков блокчейнов, реализующих методы программирования на Rust
Чтобы предотвратить целочисленное переполнение, разработчики обычно используют функции типа checked_add, checked_sub, saturating_add, saturating_sub и т. д. вместо простого сложения и вычитания (+, -). Смягчите исчерпание ресурсов, установив соответствующие тайм-ауты, ограничения размера запроса и ограничения элемента запроса.
Угрозы RPC безопасности памяти в блокчейне уровня 1
Несмотря на то, что узлы RPC не уязвимы к незащищенности памяти в традиционном смысле, злоумышленники могут легко манипулировать входными данными. В реализации безопасного для памяти RPC существует несколько ситуаций, которые могут привести к отказу в обслуживании. Например, расширение памяти может исчерпать память службы, а логические проблемы могут привести к бесконечным циклам. Кроме того, условия гонки могут представлять угрозу, из-за которой параллельные операции могут иметь неожиданную последовательность событий, оставляя систему в неопределенном состоянии. Кроме того, неправильно управляемые зависимости и сторонние библиотеки могут создавать в системе неизвестные уязвимости.
В этом посте наша цель — привлечь внимание к более непосредственным способам срабатывания средств защиты среды выполнения Rust, вызывающих самопроизвольное прерывание служб.
Явная паника в Rust: способ напрямую прекратить службы RPC
Разработчики могут преднамеренно или непреднамеренно ввести явный код паники. Эти коды в основном используются для обработки непредвиденных или исключительных ситуаций. Некоторые распространенные примеры включают в себя:
assert!(): Используйте этот макрос, когда должно быть выполнено условие. Если заявленное условие не выполняется, программа запаникует, указывая на серьезную ошибку в коде.
panic!(): эта функция вызывается, когда программа сталкивается с ошибкой, после которой она не может восстановиться и продолжить работу.
unreachable!(): Используйте этот макрос, когда часть кода не должна выполняться. Если этот макрос вызывается, это указывает на серьезную логическую ошибку.
unimplemented!() и todo!(): эти макросы замещают нереализованные функции. Если это значение будет достигнуто, программа рухнет.
unwrap(): этот метод используется для типов Option или Result.Когда встречается переменная Err или None, программа аварийно завершает работу.
Уязвимость 1: инициировать утверждение в Move Verifier!
Блокчейн Aptos использует верификатор байт-кода Move для выполнения эталонного анализа безопасности посредством абстрактной интерпретации байт-кода. Функция ute() является частью реализации трейта TransferFunctions и имитирует выполнение инструкций байт-кода в базовых блоках.
Задача функции ute_inner() состоит в том, чтобы интерпретировать текущую инструкцию байт-кода и соответствующим образом обновить состояние. Если мы выполнили последнюю инструкцию в базовом блоке, на что указывает index == last_index, функция вызовет assert!(self.stack.is_empty()), чтобы гарантировать, что стек пуст. Смысл этого поведения в том, чтобы гарантировать, что все операции сбалансированы, что также означает, что у каждого нажатия есть соответствующее всплывающее окно.
В обычном потоке выполнения стек всегда сбалансирован во время абстрактной интерпретации. Это гарантирует средство проверки баланса стека, которое проверяет байт-код перед его интерпретацией. Однако, как только мы расширим нашу перспективу до области абстрактных интерпретаторов, мы увидим, что предположение о балансе стека не всегда верно.
Патч для уязвимости analysis_function в AbstractInterpreter
По своей сути абстрактный интерпретатор эмулирует байт-код на уровне базового блока. В его первоначальной реализации обнаружение ошибки во время ute_block побуждало процесс анализа регистрировать ошибку и продолжать выполнение к следующему блоку в графе потока управления. Это может создать ситуацию, когда ошибка в исполнительном блоке может привести к несбалансированности стека. Если в этом случае выполнение продолжится, будет выполнена проверка assert!, если стек не пуст, что вызовет панику.
Это дает злоумышленникам возможность использовать их. Злоумышленник может вызвать ошибку, создав специальный байт-код в ute_block(), а затем ute() может выполнить утверждение, если стек не пуст, что приведет к сбою проверки утверждения. Это еще больше вызовет панику и прекратит работу службы RPC, что повлияет на ее доступность.
Чтобы предотвратить это, реализованное исправление обеспечивает остановку всего процесса анализа, когда функция ute_block впервые сталкивается с ошибкой, тем самым избегая риска последующих сбоев, которые могут возникнуть при продолжении анализа из-за дисбаланса стека из-за ошибок. Эта модификация устраняет условия, которые могли вызвать панику, и помогает повысить надежность и безопасность абстрактного интерпретатора.
** Уязвимость 2: вызвать панику в StarCoin! **
Блокчейн Starcoin имеет собственный форк реализации Move. В этом репозитории Move есть паника в конструкторе типа Struct!Если предоставленное StructDefinition имеет информацию о собственном поле, паника будет вызвана явно! .
Явные паники для инициализированных структур в процедурах нормализации
Этот потенциальный риск существует в процессе распространения модулей. Если опубликованный модуль уже существует в хранилище данных, нормализация модуля требуется как для существующего модуля, так и для входного модуля, контролируемого злоумышленником. Во время этого процесса функция «normalized::Module::new» строит структуру модуля из входных модулей, контролируемых злоумышленником, вызывая «панику!».
Предпосылки для процедуры нормализации
Эта паника может быть вызвана отправкой специально созданной полезной нагрузки от клиента. Таким образом, злоумышленники могут нарушить доступность служб RPC.
Патч паники при инициализации структуры
Патч Starcoin вводит новое поведение для обработки случая Native. Теперь вместо паники возвращается пустой ec. Это снижает вероятность того, что пользователи отправят данные, вызывающие панику.
Скрытая паника в Rust: легко упускаемый из виду способ прекращения работы RPC-сервисов
Явные паники легко распознаются в исходном коде, в то время как неявные паники, скорее всего, будут игнорироваться разработчиками. Неявные паники обычно возникают при использовании API, предоставляемых стандартными или сторонними библиотеками. Разработчики должны внимательно прочитать и понять документацию по API, иначе их программы на Rust могут неожиданно остановиться.
Неявная паника в BTreeMap
В качестве примера возьмем BTreeMap из Rust STD. BTreeMap — это широко используемая структура данных, которая организует пары ключ-значение в отсортированное двоичное дерево. BTreeMap предоставляет два метода получения значений по ключу: get(&self, key: &Q) и index(&self, key: &Q).
Метод get(&self, key: &Q) извлекает значение с помощью ключа и возвращает параметр. Опция может быть Some(&V), если ключ существует, вернуть ссылку значения, если ключ не найден в BTreeMap, вернуть None.
С другой стороны, index(&self, key: &Q) напрямую возвращает ссылку на значение, соответствующее ключу. Однако у него есть большой риск: он вызовет неявную панику, если ключ не существует в BTreeMap. При неправильном обращении программа может неожиданно выйти из строя, что делает ее потенциальной уязвимостью.
Фактически, метод index(&self, key: &Q) является базовой реализацией трейта std::ops::Index. Этот трейт представляет собой индексную операцию в неизменяемом контексте (т. е. в контейнере). [index] ) предоставляет удобный синтаксический сахар. Разработчики могут напрямую использовать btree_map [key] , вызовите метод index(&self, key: &Q). Однако они могут игнорировать тот факт, что такое использование может привести к панике, если ключ не будет найден, что создает неявную угрозу стабильности программы.
Уязвимость 3: вызвать неявную панику в Sui RPC
Процедура выпуска модуля Sui позволяет пользователям отправлять полезные данные модуля через RPC. Обработчик RPC использует функцию SuiCommand::Publish для непосредственной дизассемблирования полученного модуля перед пересылкой запроса во внутреннюю сеть проверки для проверки байт-кода.
Во время этой разборки раздел code_unit представленного модуля используется для построения VMControlFlowGraph. Процесс сборки состоит из создания базовых блоков, которые хранятся в BTreeMap под названием «блоки». Процесс включает в себя создание карты и управление ею, где при определенных условиях срабатывает неявная паника.
Вот упрощенный код:
Неявная паника при создании VMControlFlowGraph
В этом коде новый VMControlFlowGraph создается путем обхода кода и создания нового базового блока для каждой единицы кода. Базовые блоки хранятся в именованном блоке BTreeMap.
Карта блоков индексируется с помощью block[&block] в цикле, который перебирает стек, который был инициализирован с помощью ENTRY_BLOCK_ID. Здесь предполагается, что в карте блоков есть хотя бы один ENTRY_BLOCK_ID.
Однако это предположение выполняется не всегда. Например, если зафиксированный код пуст, «карта блоков» останется пустой после процесса «создания базового блока». Когда позже код попытается пройти карту блоков, используя for succ в &blocks[&block].successors , может возникнуть неявная паника, если ключ не найден. Это связано с тем, что выражение blocks[&block] по сути является вызовом метода index(), который, как упоминалось ранее, вызовет панику, если ключ не существует в BTreeMap.
Злоумышленник с удаленным доступом может воспользоваться уязвимостью в этой функции, отправив неверно сформированную полезную нагрузку модуля с пустым полем code_unit. Этот простой запрос RPC приводит к сбою всего процесса JSON-RPC. Если злоумышленник продолжит отправлять такие искаженные полезные данные с минимальными усилиями, это приведет к устойчивому прерыванию обслуживания. В сети блокчейна это означает, что сеть может быть не в состоянии подтвердить новые транзакции, что приводит к ситуации отказа в обслуживании (DoS). Функциональность сети и доверие пользователей к системе будут серьезно затронуты.
Исправление Sui: удалить дизассемблирование из процедуры обработки RPC.
Стоит отметить, что CodeUnitVerifier в Move Bytecode Verifier отвечает за то, чтобы раздел code_unit никогда не был пустым. Однако порядок операций подвергает обработчики RPC потенциальным уязвимостям. Это связано с тем, что процесс проверки происходит на узле Validator, который является этапом после того, как RPC обрабатывает входные модули.
В ответ на эту проблему Sui устранил уязвимость, удалив функцию дизассемблирования в процедуре RPC выпуска модуля. Это эффективный способ предотвратить обработку службами RPC потенциально опасного непроверенного байт-кода.
Кроме того, стоит отметить, что другие методы RPC, связанные с поиском объектов, также содержат возможности дизассемблирования, но они не уязвимы для использования пустых ячеек кода. Это потому, что они всегда запрашивают и дизассемблируют существующие опубликованные модули. Опубликованные модули должны быть проверены, поэтому при построении VMControlFlowGraph всегда выполняется предположение о непустых ячейках кода.
Предложения для разработчиков
Поняв угрозы стабильности сервисов RPC в блокчейнах от явных и неявных паник, разработчики должны освоить стратегии для предотвращения или снижения этих рисков. Эти стратегии могут снизить вероятность незапланированных отключений услуг и повысить отказоустойчивость системы. Поэтому команда экспертов CertiK выдвигает следующие предложения и перечисляет их в качестве лучших практик программирования на Rust.
Абстракция паники в Rust: по возможности рассмотрите возможность использования функции catch_unwind Rust для перехвата паники и преобразования ее в сообщения об ошибках. Это предотвращает сбой всей программы и позволяет разработчикам контролируемо обрабатывать ошибки.
Используйте API с осторожностью: неявные паники обычно возникают из-за неправильного использования API, предоставляемых стандартными или сторонними библиотеками. Поэтому крайне важно полностью понять API и научиться правильно обрабатывать потенциальные ошибки. Разработчики всегда должны предполагать, что API могут дать сбой, и готовиться к таким ситуациям.
Правильная обработка ошибок: используйте типы Result и Option для обработки ошибок вместо того, чтобы прибегать к панике. Первый обеспечивает более контролируемый способ обработки ошибок и особых случаев.
Добавьте документацию и комментарии: убедитесь, что ваш код хорошо документирован, и добавьте комментарии к критическим разделам (включая те, где может возникнуть паника). Это поможет другим разработчикам понять потенциальные риски и эффективно с ними справиться.
Подведем итог
Узлы RPC на основе Rust играют важную роль в таких блокчейн-системах, как Aptos, StarCoin и Sui. Поскольку они используются для соединения DApps и базовой цепочки блоков, их надежность имеет решающее значение для бесперебойной работы системы цепочки блоков. Хотя в этих системах используется безопасный для памяти язык Rust, все же существует риск некачественного дизайна. Исследовательская группа CertiK изучила эти риски на реальных примерах, которые демонстрируют необходимость тщательного и тщательного проектирования безопасного для памяти программирования.