Аналіз вразливостей компілятора Solidity та стратегії реагування
Компилятор є однією з основних складових сучасних комп'ютерних систем. Це комп'ютерна програма, основна функція якої полягає в перетворенні вихідного коду на мовах високого рівня, зрозумілому та зручному для написання людиною, на інструкції, виконувані процесором або віртуальною машиною байт-коду.
Більшість розробників і експертів з безпеки зазвичай більше зосереджуються на безпеці коду програмних додатків, але можуть ігнорувати проблеми безпеки самого компілятора. Насправді компілятор, як комп'ютерна програма, також має вразливості, які в певних умовах можуть призвести до серйозних ризиків безпеки. Наприклад, під час компіляції та аналізу виконання коду JavaScript на фронтенді, браузер може стати жертвою вразливостей JavaScript-інтерпретатора, які можуть бути використані зловмисниками для виконання віддаленого коду під час відвідування користувачем шкідливих веб-сторінок, що в кінцевому підсумку може призвести до контролю над браузером жертви або навіть операційною системою.
Компілер Solidity не є винятком, згідно з попередженнями команди розробників Solidity, у кількох різних версіях компілятора Solidity існують вразливості безпеки.
Уразливість компілятора Solidity
Роль компілятора Solidity полягає в перетворенні коду смарт-контрактів, написаного розробниками, у байтовий код інструкцій (EVM) для віртуальної машини Ethereum. Ці EVM інструкції упаковуються в транзакції та завантажуються в Ethereum, а в кінцевому підсумку виконуються EVM.
Необхідно відокремити вразливості компілятора Solidity від вразливостей самого EVM. Вразливості EVM стосуються проблем безпеки, які виникають під час виконання інструкцій віртуальною машиною. Оскільки зловмисники можуть завантажувати будь-який код в Ethereum, цей код врешті-решт буде виконуватись у кожній програмі клієнта Ethereum P2P. Якщо у 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 підтримуються такі типи даних, як uint32 та інші, які менші за 32 байти, і компілятор, обробляючи такі змінні, повинен виконувати відповідні дії для очищення високих розрядів (clean up), щоб забезпечити правильність даних. У вищезгаданому випадку, коли додавання призводить до переповнення цілих чисел, компілятор не правильно очистив високі розряди результату, що призвело до того, що 1 біт високого розряду після переповнення був записаний в storage, в результаті чого було перезаписано значення змінної a, а значення змінної b було змінено на 1.
SOL-2022-4 InlineAssemblyMemoryПобічні ефекти
Розгляньте наступний код:
солідність
контракт C {
функція f() публічна чиста повертає (uint) {
збірка {
mstore(0, 0x42)
}
uint x;
збірка {
x := mload(0)
}
повернути x;
}
}
Ця вразливість існує в компіляторах версій >=0.8.13 <0.8.15. Компілятор Solidity не лише просто перекладає мову Solidity у код EVM, а й проводить глибокий аналіз контролю потоку та даних, реалізуючи різні процеси компіляційної оптимізації для зменшення обсягу згенерованого коду та оптимізації споживання газу під час виконання. Такі оптимізаційні операції є звичними в компіляторах різних високорівневих мов, але через велику кількість ситуацій, які потрібно враховувати, легко можуть виникнути помилки або вразливості безпеки.
Вразливість наведеного коду виникає внаслідок таких оптимізаційних дій. Припустимо, що в певній функції є код, що змінює дані за адресою пам'яті 0, але в подальшому ця інформація не використовується, тоді фактично можна безпосередньо видалити код зміни пам'яті 0, щоб заощадити газ і не вплинути на подальшу логіку програми.
Ця стратегія оптимізації сама по собі не є проблемою, але в конкретній реалізації коду компілятора Solidity така оптимізація застосовується лише до одного блоку assembly. У наведеному PoC коді запис і доступ до пам'яті 0 знаходяться в двох різних блоках assembly, тоді як компілятор аналізував і оптимізував лише окремий блок assembly. Оскільки в першому блоці assembly після запису в пам'ять 0 не було жодної операції читання, то ця команда запису вважається зайвою і буде видалена, що призведе до помилки. У вразливій версії функція f() поверне значення 0, тоді як фактично правильне значення, яке повинно повертатися в наведеному коді, - це 0x42.
солідність
контракт C {
функція f(string[1] calldata a) зовнішні повернення (string пам'яті) {
return abi.decode(abi.encode(a), (string));
}
}
Ця вразливість впливає на компілятори версій >= 0.5.8 < 0.8.16. В нормальних умовах, змінна a, що повертається вищезазначеним кодом, повинна бути "aaaa". Але в уразливій версії вона повертає порожній рядок "".
Причиною цієї уразливості є те, що Solidity неправильно обробляє масиви типу calldata під час операції abi.encode, помилково очищаючи деякі дані, що призводить до зміни сусідніх даних і в результаті до несумісності кодування та декодування.
Слід зазначити, що Solidity під час виконання зовнішніх викликів та емісії подій неявно кодує параметри за допомогою abi.encode, тому ймовірність виникнення вищезгаданого вразливого коду буде вищою, ніж можна було б очікувати.
Рекомендації з безпеки
Команда з безпеки блокчейну Cobo, після аналізу моделі загроз уразливостей компілятора Solidity та узагальнення історичних уразливостей, пропонує наступні рекомендації для розробників і спеціалістів з безпеки.
Для розробників:
Використовуйте новішу версію компілятора Solidity. Хоча нові версії також можуть впроваджувати нові проблеми безпеки, відомих проблем безпеки зазвичай менше, ніж у старих версіях.
Удосконалення тестових випадків для модулів. Більшість помилок на рівні компілятора призводять до того, що результати виконання коду не відповідають очікуваним. Ці проблеми важко виявити під час перевірки коду, але їх легко виявити на етапі тестування. Тому, підвищуючи покриття коду, можна максимально уникнути таких проблем.
Намагайтеся уникати використання вбудованого асемблера, складних операцій кодування та декодування ABI для багатодомінних масивів і складних структур, якщо немає чіткої потреби, уникайте бездумного використання нових можливостей мови та експериментальних функцій заради демонстрації навичок. Згідно з аналізом історичних вразливостей Solidity командою безпеки Cobo, більшість вразливостей пов'язані з вбудованим асемблером, операціями кодування ABI тощо. Компілятор дійсно легше стикається з помилками при обробці складних мовних особливостей. З іншого боку, розробники також можуть зіштовхнутися з помилками під час використання нових можливостей, що призводить до проблем безпеки.
для безпекових працівників:
Під час проведення безпекового аудиту коду Solidity не ігноруйте потенційні ризики безпеки, які можуть виникнути через компілятор Solidity. Відповідний пункт перевірки у Smart Contract Weakness Classification(SWC) – SWC-102: Застаріла версія компілятора.
У внутрішньому процесі розробки SDL, закликаємо команду розробників оновити версію компілятора Solidity та розглянути можливість впровадження автоматичної перевірки версії компілятора у процесі CI/CD.
Але не слід панікувати щодо вразливостей компілятора, більшість вразливостей компілятора активуються лише в певних кодових шаблонах, і використання вразливих версій компілятора для компіляції контрактів не завжди несе в собі безпекові ризики; фактичний вплив на безпеку потрібно оцінювати конкретно в залежності від ситуації проекту.
Практичні ресурси:
Регулярні публікації команди Solidity щодо Security Alerts:
Офіційний репозиторій Solidity регулярно оновлює список помилок:
Список багів компілятора різних версій:
Code трикутний значок з оклику у верхньому правому куті може вказувати на наявність вразливостей у компіляторі поточної версії.
Підсумок
Ця стаття починається з основних понять компіляторів, представляє вразливості компілятора Solidity та аналізує можливі ризики безпеки, які вони можуть викликати в реальному середовищі розробки Ethereum, а наприкінці надає ряд практичних порад для розробників і фахівців з безпеки.
Ця сторінка може містити контент третіх осіб, який надається виключно в інформаційних цілях (не в якості запевнень/гарантій) і не повинен розглядатися як схвалення його поглядів компанією Gate, а також як фінансова або професійна консультація. Див. Застереження для отримання детальної інформації.
11 лайків
Нагородити
11
5
Поділіться
Прокоментувати
0/400
staking_gramps
· 07-25 15:28
Двері з дірками багато, хто ще наважиться розробляти?
Переглянути оригіналвідповісти на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 полягає в перетворенні коду смарт-контрактів, написаного розробниками, у байтовий код інструкцій (EVM) для віртуальної машини Ethereum. Ці EVM інструкції упаковуються в транзакції та завантажуються в Ethereum, а в кінцевому підсумку виконуються EVM.
Необхідно відокремити вразливості компілятора Solidity від вразливостей самого EVM. Вразливості EVM стосуються проблем безпеки, які виникають під час виконання інструкцій віртуальною машиною. Оскільки зловмисники можуть завантажувати будь-який код в Ethereum, цей код врешті-решт буде виконуватись у кожній програмі клієнта Ethereum P2P. Якщо у 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 підтримуються такі типи даних, як uint32 та інші, які менші за 32 байти, і компілятор, обробляючи такі змінні, повинен виконувати відповідні дії для очищення високих розрядів (clean up), щоб забезпечити правильність даних. У вищезгаданому випадку, коли додавання призводить до переповнення цілих чисел, компілятор не правильно очистив високі розряди результату, що призвело до того, що 1 біт високого розряду після переповнення був записаний в storage, в результаті чого було перезаписано значення змінної a, а значення змінної b було змінено на 1.
SOL-2022-4 InlineAssemblyMemoryПобічні ефекти
Розгляньте наступний код:
солідність контракт C { функція f() публічна чиста повертає (uint) { збірка { mstore(0, 0x42) } uint x; збірка { x := mload(0) } повернути x; } }
Ця вразливість існує в компіляторах версій >=0.8.13 <0.8.15. Компілятор Solidity не лише просто перекладає мову Solidity у код EVM, а й проводить глибокий аналіз контролю потоку та даних, реалізуючи різні процеси компіляційної оптимізації для зменшення обсягу згенерованого коду та оптимізації споживання газу під час виконання. Такі оптимізаційні операції є звичними в компіляторах різних високорівневих мов, але через велику кількість ситуацій, які потрібно враховувати, легко можуть виникнути помилки або вразливості безпеки.
Вразливість наведеного коду виникає внаслідок таких оптимізаційних дій. Припустимо, що в певній функції є код, що змінює дані за адресою пам'яті 0, але в подальшому ця інформація не використовується, тоді фактично можна безпосередньо видалити код зміни пам'яті 0, щоб заощадити газ і не вплинути на подальшу логіку програми.
Ця стратегія оптимізації сама по собі не є проблемою, але в конкретній реалізації коду компілятора Solidity така оптимізація застосовується лише до одного блоку assembly. У наведеному PoC коді запис і доступ до пам'яті 0 знаходяться в двох різних блоках assembly, тоді як компілятор аналізував і оптимізував лише окремий блок assembly. Оскільки в першому блоці assembly після запису в пам'ять 0 не було жодної операції читання, то ця команда запису вважається зайвою і буде видалена, що призведе до помилки. У вразливій версії функція f() поверне значення 0, тоді як фактично правильне значення, яке повинно повертатися в наведеному коді, - це 0x42.
SOL-2022-6 AbiReencodingHeadOverflowWithStaticArrayCleanup
Розгляньте наступний код:
солідність контракт C { функція f(string[1] calldata a) зовнішні повернення (string пам'яті) { return abi.decode(abi.encode(a), (string)); } }
Ця вразливість впливає на компілятори версій >= 0.5.8 < 0.8.16. В нормальних умовах, змінна a, що повертається вищезазначеним кодом, повинна бути "aaaa". Але в уразливій версії вона повертає порожній рядок "".
Причиною цієї уразливості є те, що Solidity неправильно обробляє масиви типу calldata під час операції abi.encode, помилково очищаючи деякі дані, що призводить до зміни сусідніх даних і в результаті до несумісності кодування та декодування.
Слід зазначити, що Solidity під час виконання зовнішніх викликів та емісії подій неявно кодує параметри за допомогою abi.encode, тому ймовірність виникнення вищезгаданого вразливого коду буде вищою, ніж можна було б очікувати.
Рекомендації з безпеки
Команда з безпеки блокчейну Cobo, після аналізу моделі загроз уразливостей компілятора Solidity та узагальнення історичних уразливостей, пропонує наступні рекомендації для розробників і спеціалістів з безпеки.
Для розробників:
Використовуйте новішу версію компілятора Solidity. Хоча нові версії також можуть впроваджувати нові проблеми безпеки, відомих проблем безпеки зазвичай менше, ніж у старих версіях.
Удосконалення тестових випадків для модулів. Більшість помилок на рівні компілятора призводять до того, що результати виконання коду не відповідають очікуваним. Ці проблеми важко виявити під час перевірки коду, але їх легко виявити на етапі тестування. Тому, підвищуючи покриття коду, можна максимально уникнути таких проблем.
Намагайтеся уникати використання вбудованого асемблера, складних операцій кодування та декодування ABI для багатодомінних масивів і складних структур, якщо немає чіткої потреби, уникайте бездумного використання нових можливостей мови та експериментальних функцій заради демонстрації навичок. Згідно з аналізом історичних вразливостей Solidity командою безпеки Cobo, більшість вразливостей пов'язані з вбудованим асемблером, операціями кодування ABI тощо. Компілятор дійсно легше стикається з помилками при обробці складних мовних особливостей. З іншого боку, розробники також можуть зіштовхнутися з помилками під час використання нових можливостей, що призводить до проблем безпеки.
для безпекових працівників:
Під час проведення безпекового аудиту коду Solidity не ігноруйте потенційні ризики безпеки, які можуть виникнути через компілятор Solidity. Відповідний пункт перевірки у Smart Contract Weakness Classification(SWC) – SWC-102: Застаріла версія компілятора.
У внутрішньому процесі розробки SDL, закликаємо команду розробників оновити версію компілятора Solidity та розглянути можливість впровадження автоматичної перевірки версії компілятора у процесі CI/CD.
Але не слід панікувати щодо вразливостей компілятора, більшість вразливостей компілятора активуються лише в певних кодових шаблонах, і використання вразливих версій компілятора для компіляції контрактів не завжди несе в собі безпекові ризики; фактичний вплив на безпеку потрібно оцінювати конкретно в залежності від ситуації проекту.
Практичні ресурси:
Регулярні публікації команди Solidity щодо Security Alerts:
Офіційний репозиторій Solidity регулярно оновлює список помилок:
Список багів компілятора різних версій:
Code трикутний значок з оклику у верхньому правому куті може вказувати на наявність вразливостей у компіляторі поточної версії.
Підсумок
Ця стаття починається з основних понять компіляторів, представляє вразливості компілятора Solidity та аналізує можливі ризики безпеки, які вони можуть викликати в реальному середовищі розробки Ethereum, а наприкінці надає ряд практичних порад для розробників і фахівців з безпеки.