Аналіз вразливостей компілятора Solidity та стратегії реагування
Компілятор є однією з основних складових сучасних комп'ютерних систем. Це спеціальна комп'ютерна програма, яка відповідає за перетворення вихідного коду високорівневої мовлення програмування, зрозумілого та простого для людини, в інструкційний код, який може виконуватися процесором або байт-кодом віртуальної машини.
Хоча більшість розробників і експертів з безпеки зазвичай більше зосереджені на безпеці коду додатків, безпеку самого компілятора також не слід ігнорувати. Як комп'ютерна програма, компілятор може мати вразливості безпеки, які в певних випадках можуть призвести до серйозних ризиків безпеки. Наприклад, під час компіляції та розбору виконуваного коду Javascript на стороні клієнта браузер може бути підданий атакам через вразливості в розбірнику Javascript, що може призвести до віддаленого виконання коду зловмисниками, коли користувач відвідує шкідливу веб-сторінку, в результаті чого зловмисники отримують контроль над браузером жертви або навіть над усією операційною системою.
Компілятор Solidity не є винятком, у різних версіях існують вразливості безпеки.
Уразливість компілятора Solidity
Основна функція компілятора Solidity полягає в перетворенні коду смарт-контракту, написаного розробниками, на інструкційний код, який може виконуватися в Ethereum Virtual Machine (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() публічний перегляд повертає (uint) {
повернути b;
}
}
Змінна storage b не зазнала жодних змін, тому функція run() повинна повертати значення за замовчуванням 0. Але в коді, згенерованому уразливим компілятором, run() насправді повертає 1.
Звичайним розробникам важко виявити проблеми в наведеному коді за допомогою простого огляду коду. Хоча цей приклад відносно простий і, можливо, не призведе до особливо серйозних наслідків, якщо змінна b використовується для перевірки прав, обліку активів та інших критичних цілей, така невідповідність очікуванням може призвести до серйозних загроз безпеці.
Ця проблема виникає через те, що EVM використовує стекову віртуальну машину, де кожен елемент стека має розмір 32 байти (тобто розмір змінної uint256). Кожен слот у базовому сховищі storage також має розмір 32 байти. Мова Solidity підтримує такі типи даних, як uint32 та інші, менші за 32 байти, і компілятор, обробляючи ці змінні, повинен належним чином очистити їх старші біти (clean up), щоб забезпечити правильність даних. У наведеному випадку, коли відбувається переповнення цілого числа в результаті додавання, компілятор неправильно очистив старші біти результату, внаслідок чого старший біт, що переповнився, був записаний у storage, в результаті чого було перезаписано b змінну, що призвело до зміни значення b на 1.
SOL-2022-4 InlineAssemblyMemoryПобічні ефекти
Ця вразливість існує в компіляторах версій >=0.8.13 <0.8.15. Розгляньте наступний код:
солідність
контракт C {
функція f() публічна чиста повертає (uint) {
збірка {
mstore(0, 0x42)
}
uint x;
збіговисько {
x := mload(0)
}
повернути x;
}
}
Компілятор Solidity під час перетворення мови Solidity на код EVM не просто виконує простий переклад. Він також проводить глибокий аналіз контролю потоку та даних, реалізуючи різні процеси компіляційної оптимізації, щоб зменшити обсяг згенерованого коду та оптимізувати витрати газу під час виконання. Такі оптимізаційні операції є досить поширеними в компіляторах різних мов високого рівня, але через складність випадків, які потрібно враховувати, також можуть виникати помилки або вразливості безпеки.
Вразливість наведеного коду походить від такого роду оптимізаційних дій. Компілятор вважає, що якщо в певній функції є код, який змінює дані за адресою нульового зсуву пам'яті, але в подальшому це значення не використовується більше ніде, то можна безпосередньо видалити код, що змінює пам'ять за адресою 0, заощаджуючи газ і не впливаючи на подальшу логіку програми.
Ця стратегія оптимізації сама по собі не є проблемою, але в конкретній реалізації коду компілятора Solidity така оптимізація застосовується лише до одного блоку assembly. У вищезгаданому PoC коді запис та доступ до пам'яті 0 знаходяться в двох різних блоках assembly, тоді як компілятор аналізує та оптимізує лише окремий блок assembly. Оскільки в першому блоці assembly після запису в пам'ять 0 немає жодних операцій читання, то команда запису вважається зайвою і буде видалена, що призводить до помилки. У вразливій версії функція f() поверне значення 0, тоді як насправді правильне значення, яке повинна повернути вищезгадана частина коду, - це 0x42.
Ця вразливість впливає на компілятори версій >= 0.5.8 < 0.8.16. Розгляньте наступний код:
солідність
контракт C {
функція f(string[1] calldata a) зовнішні чисті повертає (string memory) {
повернути abi.decode(abi.encode(a), (рядок[1]))[0];
}
}
У нормальних умовах змінна a, яку повертає вищезазначений код, повинна бути "aaaa". Але в уразливій версії вона поверне порожній рядок "".
Причиною цієї вразливості є те, що Solidity неправильно обробляв масиви типу calldata під час операції abi.encode, помилково очищаючи певні дані, що призвело до зміни сусідніх даних і, як наслідок, до несумісності закодованих і декодованих даних.
Слід звернути увагу на те, що при виконанні зовнішнього виклику та випуску подій Solidity неявно кодує параметри за допомогою abi.encode, тому ймовірність виникнення вказаного уразливого коду буде вищою, ніж може здаватися на перший погляд.
Рекомендації з безпеки
Після аналізу моделі загроз уразливостей компілятора Solidity та систематизації історичних уразливостей, ми надаємо наступні рекомендації для розробників та спеціалістів з безпеки.
Для розробників:
Використовуйте новішу версію компілятора Solidity. Хоча нові версії також можуть впроваджувати нові проблеми безпеки, відомих проблем безпеки зазвичай менше, ніж у старих версіях.
Удосконалення тестових випадків одиниць. Більшість помилок на рівні компілятора призводять до того, що результати виконання коду не відповідають очікуванням. Ці проблеми важко виявити під час рецензування коду, але їх легко виявити на етапі тестування. Тому підвищення покриття коду може максимально зменшити ймовірність виникнення таких проблем.
Намагайтеся уникати використання вбудованого асемблера, складних операцій з багатовимірними масивами та складними структурами при кодуванні та декодуванні ABI, якщо немає чіткої потреби. Уникайте сліпого використання нових особливостей мови та експериментальних функцій з метою демонстрації майстерності. За аналізом історичних вразливостей, більшість з них пов'язані з вбудованим асемблером, ABI-кодувальниками та іншими подібними операціями. Компілятори частіше зазнають помилок при обробці складних мовних особливостей. З іншого боку, розробники також можуть легко потрапити в пастку неправильного використання нових функцій, що призводить до проблем безпеки.
Для співробітників безпеки:
Під час проведення безпекового аудиту коду Solidity не ігноруйте потенційні ризики безпеки, які можуть бути внесені компілятором Solidity. Відповідний пункт перевірки у Smart Contract Weakness Classification (SWC) - SWC-102: Застаріла версія компілятора.
У внутрішньому процесі розробки SDL закликаємо команду розробників оновити версію компілятора Solidity та розглянути можливість впровадження автоматичної перевірки версії компілятора в процесі CI/CD.
Але не слід панікувати через вразливості компілятора, більшість вразливостей компілятора активуються лише в певних кодових патернах, і не кожен контракт, скомпільований за допомогою вразливої версії компілятора, обов'язково має безпекові ризики; фактичний вплив на безпеку необхідно оцінювати конкретно залежно від ситуації в проекті.
Корисні ресурси
Статті з безпеки, які регулярно публікує команда Solidity
Список помилок, що регулярно оновлюється в офіційному репозиторії Solidity
Список помилок компілятора різних версій. На основі цього можна автоматично перевіряти версії компілятора під час процесу CI/CD, щоб повідомляти про наявні у поточній версії безпекові вразливості.
Code трикутний знак оклику у верхньому правому куті може вказувати на проблеми безпеки, пов'язані з поточною версією компілятора.
Підсумок
У цій статті розглядаються основні концепції компілятора, описуються вразливості компілятора Solidity та аналізується безпековий ризик, який вони можуть спричинити в реальному середовищі розробки Ethereum. Нарешті, для розробників та фахівців з безпеки надається кілька практичних рекомендацій щодо безпеки. Завдяки розумінню цих вразливостей і вжиттю відповідних запобіжних заходів, ми можемо краще захистити безпеку смарт-контрактів і зменшити ризик потенційних втрат активів.
Ця сторінка може містити контент третіх осіб, який надається виключно в інформаційних цілях (не в якості запевнень/гарантій) і не повинен розглядатися як схвалення його поглядів компанією Gate, а також як фінансова або професійна консультація. Див. Застереження для отримання детальної інформації.
12 лайків
Нагородити
12
6
Поділіться
Прокоментувати
0/400
0xLuckbox
· 21год тому
Логічні вразливості дійсно шкодять, я пішов, пішов.
Переглянути оригіналвідповісти на0
LidoStakeAddict
· 07-30 09:56
Переповнення, код потрібно змінити.
Переглянути оригіналвідповісти на0
StablecoinArbitrageur
· 07-30 09:26
*коригує окуляри* хмм... з статистичної точки зору, ризики компілятора серйозно недооцінюються в розрахунках TVL в DeFi
Переглянути оригіналвідповісти на0
BoredStaker
· 07-30 09:24
Коли можна буде говорити по-людськи!
Переглянути оригіналвідповісти на0
ArbitrageBot
· 07-30 09:24
Знову потрібно займатися ямою компілятора?
Переглянути оригіналвідповісти на0
APY追逐者
· 07-30 09:15
Тільки коли я заплатив за газ при полюванні, я згадав про вразливість компілятора.
Аналіз вразливостей компілятора Solidity та практика запобігання безпеці
Аналіз вразливостей компілятора Solidity та стратегії реагування
Компілятор є однією з основних складових сучасних комп'ютерних систем. Це спеціальна комп'ютерна програма, яка відповідає за перетворення вихідного коду високорівневої мовлення програмування, зрозумілого та простого для людини, в інструкційний код, який може виконуватися процесором або байт-кодом віртуальної машини.
Хоча більшість розробників і експертів з безпеки зазвичай більше зосереджені на безпеці коду додатків, безпеку самого компілятора також не слід ігнорувати. Як комп'ютерна програма, компілятор може мати вразливості безпеки, які в певних випадках можуть призвести до серйозних ризиків безпеки. Наприклад, під час компіляції та розбору виконуваного коду Javascript на стороні клієнта браузер може бути підданий атакам через вразливості в розбірнику Javascript, що може призвести до віддаленого виконання коду зловмисниками, коли користувач відвідує шкідливу веб-сторінку, в результаті чого зловмисники отримують контроль над браузером жертви або навіть над усією операційною системою.
Компілятор Solidity не є винятком, у різних версіях існують вразливості безпеки.
Уразливість компілятора Solidity
Основна функція компілятора Solidity полягає в перетворенні коду смарт-контракту, написаного розробниками, на інструкційний код, який може виконуватися в Ethereum Virtual Machine (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() публічний перегляд повертає (uint) { повернути b; } }
Змінна storage b не зазнала жодних змін, тому функція run() повинна повертати значення за замовчуванням 0. Але в коді, згенерованому уразливим компілятором, run() насправді повертає 1.
Звичайним розробникам важко виявити проблеми в наведеному коді за допомогою простого огляду коду. Хоча цей приклад відносно простий і, можливо, не призведе до особливо серйозних наслідків, якщо змінна b використовується для перевірки прав, обліку активів та інших критичних цілей, така невідповідність очікуванням може призвести до серйозних загроз безпеці.
Ця проблема виникає через те, що EVM використовує стекову віртуальну машину, де кожен елемент стека має розмір 32 байти (тобто розмір змінної uint256). Кожен слот у базовому сховищі storage також має розмір 32 байти. Мова Solidity підтримує такі типи даних, як uint32 та інші, менші за 32 байти, і компілятор, обробляючи ці змінні, повинен належним чином очистити їх старші біти (clean up), щоб забезпечити правильність даних. У наведеному випадку, коли відбувається переповнення цілого числа в результаті додавання, компілятор неправильно очистив старші біти результату, внаслідок чого старший біт, що переповнився, був записаний у storage, в результаті чого було перезаписано b змінну, що призвело до зміни значення b на 1.
SOL-2022-4 InlineAssemblyMemoryПобічні ефекти
Ця вразливість існує в компіляторах версій >=0.8.13 <0.8.15. Розгляньте наступний код:
солідність контракт C { функція f() публічна чиста повертає (uint) { збірка { mstore(0, 0x42) } uint x; збіговисько { x := mload(0) } повернути x; } }
Компілятор Solidity під час перетворення мови Solidity на код EVM не просто виконує простий переклад. Він також проводить глибокий аналіз контролю потоку та даних, реалізуючи різні процеси компіляційної оптимізації, щоб зменшити обсяг згенерованого коду та оптимізувати витрати газу під час виконання. Такі оптимізаційні операції є досить поширеними в компіляторах різних мов високого рівня, але через складність випадків, які потрібно враховувати, також можуть виникати помилки або вразливості безпеки.
Вразливість наведеного коду походить від такого роду оптимізаційних дій. Компілятор вважає, що якщо в певній функції є код, який змінює дані за адресою нульового зсуву пам'яті, але в подальшому це значення не використовується більше ніде, то можна безпосередньо видалити код, що змінює пам'ять за адресою 0, заощаджуючи газ і не впливаючи на подальшу логіку програми.
Ця стратегія оптимізації сама по собі не є проблемою, але в конкретній реалізації коду компілятора Solidity така оптимізація застосовується лише до одного блоку assembly. У вищезгаданому PoC коді запис та доступ до пам'яті 0 знаходяться в двох різних блоках assembly, тоді як компілятор аналізує та оптимізує лише окремий блок assembly. Оскільки в першому блоці assembly після запису в пам'ять 0 немає жодних операцій читання, то команда запису вважається зайвою і буде видалена, що призводить до помилки. У вразливій версії функція f() поверне значення 0, тоді як насправді правильне значення, яке повинна повернути вищезгадана частина коду, - це 0x42.
SOL-2022-6 AbiReencodingHeadOverflowWithStaticArrayCleanup
Ця вразливість впливає на компілятори версій >= 0.5.8 < 0.8.16. Розгляньте наступний код:
солідність контракт C { функція f(string[1] calldata a) зовнішні чисті повертає (string memory) { повернути abi.decode(abi.encode(a), (рядок[1]))[0]; } }
У нормальних умовах змінна a, яку повертає вищезазначений код, повинна бути "aaaa". Але в уразливій версії вона поверне порожній рядок "".
Причиною цієї вразливості є те, що Solidity неправильно обробляв масиви типу calldata під час операції abi.encode, помилково очищаючи певні дані, що призвело до зміни сусідніх даних і, як наслідок, до несумісності закодованих і декодованих даних.
Слід звернути увагу на те, що при виконанні зовнішнього виклику та випуску подій Solidity неявно кодує параметри за допомогою abi.encode, тому ймовірність виникнення вказаного уразливого коду буде вищою, ніж може здаватися на перший погляд.
Рекомендації з безпеки
Після аналізу моделі загроз уразливостей компілятора Solidity та систематизації історичних уразливостей, ми надаємо наступні рекомендації для розробників та спеціалістів з безпеки.
Для розробників:
Використовуйте новішу версію компілятора Solidity. Хоча нові версії також можуть впроваджувати нові проблеми безпеки, відомих проблем безпеки зазвичай менше, ніж у старих версіях.
Удосконалення тестових випадків одиниць. Більшість помилок на рівні компілятора призводять до того, що результати виконання коду не відповідають очікуванням. Ці проблеми важко виявити під час рецензування коду, але їх легко виявити на етапі тестування. Тому підвищення покриття коду може максимально зменшити ймовірність виникнення таких проблем.
Намагайтеся уникати використання вбудованого асемблера, складних операцій з багатовимірними масивами та складними структурами при кодуванні та декодуванні ABI, якщо немає чіткої потреби. Уникайте сліпого використання нових особливостей мови та експериментальних функцій з метою демонстрації майстерності. За аналізом історичних вразливостей, більшість з них пов'язані з вбудованим асемблером, ABI-кодувальниками та іншими подібними операціями. Компілятори частіше зазнають помилок при обробці складних мовних особливостей. З іншого боку, розробники також можуть легко потрапити в пастку неправильного використання нових функцій, що призводить до проблем безпеки.
Для співробітників безпеки:
Під час проведення безпекового аудиту коду Solidity не ігноруйте потенційні ризики безпеки, які можуть бути внесені компілятором Solidity. Відповідний пункт перевірки у Smart Contract Weakness Classification (SWC) - SWC-102: Застаріла версія компілятора.
У внутрішньому процесі розробки SDL закликаємо команду розробників оновити версію компілятора Solidity та розглянути можливість впровадження автоматичної перевірки версії компілятора в процесі CI/CD.
Але не слід панікувати через вразливості компілятора, більшість вразливостей компілятора активуються лише в певних кодових патернах, і не кожен контракт, скомпільований за допомогою вразливої версії компілятора, обов'язково має безпекові ризики; фактичний вплив на безпеку необхідно оцінювати конкретно залежно від ситуації в проекті.
Корисні ресурси
Підсумок
У цій статті розглядаються основні концепції компілятора, описуються вразливості компілятора Solidity та аналізується безпековий ризик, який вони можуть спричинити в реальному середовищі розробки Ethereum. Нарешті, для розробників та фахівців з безпеки надається кілька практичних рекомендацій щодо безпеки. Завдяки розумінню цих вразливостей і вжиттю відповідних запобіжних заходів, ми можемо краще захистити безпеку смарт-контрактів і зменшити ризик потенційних втрат активів.