Збій RPC: аналіз нового типу вразливості у вузлах RPC Blockchain, безпечних для пам’яті

Команда Skyfall із CertiK нещодавно виявила численні вразливості у вузлах RPC на основі Rust у кількох блокчейнах, включаючи Aptos, StarCoin і Sui. Оскільки вузли RPC є критично важливими компонентами інфраструктури, що з’єднують dApps і базовий блокчейн, їх надійність має вирішальне значення для безперебійної роботи. Розробники блокчейнів знають важливість стабільних служб RPC, тому вони використовують мови, безпечні для пам’яті, такі як Rust, щоб уникнути поширених уразливостей, які можуть зруйнувати вузли RPC.

Застосування безпечної для пам’яті мови, такої як Rust, допомагає вузлам RPC уникати багатьох атак, заснованих на вразливості пам’яті. Однак під час нещодавнього аудиту ми виявили, що навіть безпечні для пам’яті реалізації Rust, якщо вони не були ретельно розроблені та перевірені, можуть бути вразливими до певних загроз безпеці, які можуть порушити доступність служб RPC.

У цій статті ми представимо наше відкриття ряду вразливостей на практичних прикладах.

Роль вузла Blockchain RPC

Сервіс віддаленого виклику процедур (RPC) блокчейна є основним компонентом інфраструктури блокчейну рівня 1. Він надає користувачам важливий зовнішній API і діє як шлюз до внутрішньої мережі блокчейн. Однак служба RPC блокчейну відрізняється від традиційної служби RPC тим, що вона полегшує взаємодію користувача без автентифікації. Безперервна доступність сервісу має вирішальне значення, і будь-який збій у роботі сервісу може серйозно вплинути на доступність основного блокчейну.

Перспектива аудиту: традиційний сервер RPC проти блокчейн-сервера 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 під час виконання, що спричиняє самовимкнення служб.

Явна паніка іржі: спосіб прямого припинення служб 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()), щоб переконатися, що стек порожній. Намір такої поведінки полягає в тому, щоб гарантувати, що всі операції збалансовані, що також означає, що кожне натискання має відповідне вискакування.

У звичайному потоці виконання стек завжди збалансований під час абстрактної інтерпретації. Це гарантує перевірка балансу стека, яка перевіряє байт-код перед його інтерпретацією. Однак, коли ми розширюємо нашу перспективу до сфери абстрактних інтерпретаторів, ми бачимо, що припущення про баланс стека не завжди вірне.

Патч для аналізу_функції вразливості в AbstractInterpreter

За своєю суттю абстрактний інтерпретатор емулює байт-код на рівні базового блоку. У початковій реалізації виявлення помилки під час ute_block спонукало б процес аналізу зареєструвати помилку та продовжити виконання до наступного блоку в графі потоку керування. Це може створити ситуацію, коли помилка в блоці виконання може призвести до того, що стек стане незбалансованим. Якщо в цьому випадку виконання продовжується, буде виконано перевірку assert!, якщо стек не порожній, що спричинить паніку.

Це дає зловмисникам можливість використовувати. Зловмисник може викликати помилку, створивши певний байт-код у ute_block(), а потім ute() може виконати твердження, якщо стек не порожній, що призведе до помилки перевірки твердження. Це призведе до подальшої паніки та припинення служби RPC, що вплине на її доступність.

Щоб запобігти цьому, реалізоване виправлення гарантує, що весь процес аналізу буде зупинено, коли функція ute_block вперше стикається з помилкою, таким чином уникаючи ризику подальших збоїв, які можуть виникнути під час продовження аналізу через дисбаланс стека через помилки. Ця модифікація усуває умови, які можуть викликати паніку, і допомагає підвищити надійність і безпеку абстрактного інтерпретатора.

** Уразливість 2: викликайте паніку в StarCoin! **

Блокчейн Starcoin має власний форк реалізації Move. У цьому репозиторії Move існує паніка в конструкторі типу Struct! Якщо надане StructDefinition має інформацію про рідне поле, паніку буде явно запущено! .

Явна паніка для ініціалізованих структур у процедурах нормалізації

Цей потенційний ризик існує в процесі перерозподілу модулів. Якщо опублікований модуль уже існує в сховищі даних, нормалізація модуля потрібна як для наявного модуля, так і для модуля введення, керованого зловмисником. Під час цього процесу функція «normalized::Module::new» створює структуру модуля з контрольованих зловмисником модулів введення, викликаючи «паніку!».

Передумови для процедури нормалізації

Ця паніка може бути викликана надсиланням спеціально створеного корисного навантаження від клієнта. Тому зловмисники можуть порушити доступність служб RPC.

Патч ініціалізації структури

Патч Starcoin представляє нову поведінку для обробки рідного регістру. Тепер, замість паніки, він повертає порожній ec. Це зменшує ймовірність того, що користувачі, які подають дані, викликають паніку.

Implicit Rust Panic: легко проігнорований спосіб припинення служб RPC

Явну паніку легко визначити у вихідному коді, тоді як неявну паніку розробники, швидше за все, проігнорують. Неявна паніка зазвичай виникає під час використання API, наданих стандартними або сторонніми бібліотеками. Розробники повинні уважно прочитати та зрозуміти документацію API, інакше їхні програми Rust можуть несподівано зупинитися.

Неявна паніка в BTreeMap

Візьмемо для прикладу BTreeMap із Rust STD. BTreeMap — це широко використовувана структура даних, яка організовує пари ключ-значення у відсортованому бінарному дереві. BTreeMap надає два методи для отримання значень за ключем: get(&self, key: &Q) і index(&self, key: &Q).

Метод get(&self, key: &Q) отримує значення за допомогою ключа та повертає Option. Параметр може бути 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 Panic: за можливості використовуйте функцію 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.
  • Нагородити
  • Прокоментувати
  • Поділіться
Прокоментувати
0/400
Немає коментарів
  • Закріпити