Аналіз вразливостей компілятора Solidity та стратегії реагування
Компилятор є однією з основних складових сучасних комп'ютерних систем. Це комп'ютерна програма, основна функція якої полягає в перетворенні вихідного коду мов високого рівня, зрозумілого та зручного для написання людиною, в інструкційний код, що може виконуватись на базовому ЦП або байтовій машині.
Більшість розробників та фахівців з безпеки зазвичай більше уваги приділяють безпеці коду програмних додатків, але можуть ігнорувати безпеку самого компілятора. Насправді, компілятор, як комп'ютерна програма, також має вразливості в безпеці, які в певних випадках можуть призвести до серйозних ризиків безпеки. Наприклад, під час компіляції та розбору виконання коду Javascript на фронтенді браузера, через вразливість у рушії розбору Javascript, користувачі можуть бути піддані атаці зловмисників, які використовують цю вразливість для віддаленого виконання коду, в кінцевому підсумку отримуючи контроль над браузером жертви або навіть операційною системою.
Компілер Solidity не є винятком, він має вразливості безпеки в кількох різних версіях.
Вразливість компілятора Solidity
Роль компілятора Solidity полягає в перетворенні коду смарт-контрактів, написаного розробниками, у код інструкцій Ethereum Virtual Machine (EVM). Ці інструкції 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() публічного перегляду повертає (uint) {
повернути b;
}
}
Змінна storage b не була змінена, тому функція run() повинна повертати значення за замовчуванням 0. Але насправді, у згенерованому коді з вразливих версій компілятора, run() поверне 1.
Без розуміння цього вразливості компілятора, звичайним розробникам буде важко виявити вищезазначену помилку в коді простим переглядом. Цей приклад коду досить простий і, можливо, не завдасть особливих серйозних шкод. Але якщо змінна b використовується для перевірки прав, обліку активів та інших цілей, така невідповідність з очікуваннями може призвести до дуже серйозних наслідків.
Причина виникнення цього аномального явища полягає в тому, що EVM використовує стекову віртуальну машину, де кожен елемент стека має розмір 32 байти (, тобто розмір змінної uint256 ). З іншого боку, кожен слот у нижньому зберіганні також має розмір 32 байти. На рівні мови Solidity підтримуються різні типи даних менше 32 байтів, такі як uint32, і компілятор, обробляючи такі змінні, повинен відповідним чином очищати їх старші біти ( clean up ), щоб забезпечити правильність даних. У наведеному випадку, коли під час додавання виникає переповнення цілого числа, компілятор не очистив правильно старші біти результату, що призвело до запису старшого біта 1 у storage, в кінцевому підсумку перекривши змінну a змінною b, в результаті чого значення змінної b було змінено на 1.
SOL-2022-4 InlineAssemblyMemoryПобічні ефекти
Ця уразливість існує в компіляторах версій >=0.8.13 <0.8.15. Розгляньте наступний код:
солідність
контракт C {
функція f() public pure повертає (uint) {
збірка {
mstore(0, 0x42)
}
uint x;
збірка {
x := mload(0)
}
повернути x;
}
}
Компілятор Solidity під час перетворення мови Solidity в код EVM не просто здійснює простий переклад. Він також проводить глибокий аналіз потоку управління та даних, реалізуючи різні процеси оптимізації компіляції, щоб зменшити обсяг згенерованого коду і оптимізувати споживання газу під час виконання. Такі оптимізаційні операції є звичними для компіляторів різних мов високого рівня, але через велику кількість складних випадків, які потрібно враховувати, також легко можуть виникнути помилки або вразливості безпеки.
Вразливість вищезазначеного коду виникає через такі оптимізаційні операції. Розглянемо таку ситуацію: якщо в якійсь функції є код, що змінює дані за адресою пам'яті 0, але в подальшому це значення не використовується, то насправді можна безпосередньо видалити код, що змінює пам'ять 0, економлячи газ і не впливаючи на подальшу логіку програми.
Ця стратегія оптимізації сама по собі не є проблемою, але в конкретній реалізації коду компілятора Solidity такі оптимізації застосовуються лише в межах одного блоку assembly. У наведеному прикладі коду запис і доступ до пам'яті 0 знаходяться в двох різних блоках assembly, в той час як компілятор проводить аналіз і оптимізацію лише окремого блоку assembly. Оскільки в першому блоці assembly після запису в пам'ять 0 немає жодної операції читання, то ця команда запису вважається зайвою, і вона буде видалена, що призведе до помилки. У версії з уразливістю функція f( поверне значення 0, тоді як насправді наведений код має повертати правильне значення 0x42.
Вразливість впливає на компілятори версій >= 0.5.8 < 0.8.16. Розгляньте наступний код:
солідність
контракт C {
функція f###uint8( calldata a[4] публічна чиста повертає )string memory( {
повернути string)abi.encode(a();
}
}
У нормальних умовах змінна a, що повертається вищезгаданим кодом, повинна дорівнювати "aaaa". Але в версії з уразливістю вона повертає порожній рядок "".
Причиною цієї вразливості є помилка в Solidity, коли операція abi.encode над масивами типу calldata неправильно очищає деякі дані, що призводить до зміни сусідніх інших даних і викликає несумісність даних після кодування та декодування.
Слід зазначити, що Solidity під час виконання зовнішнього виклику та емісії події неявно кодує параметри за допомогою abi.encode, тому ймовірність виникнення вищезгаданої вразливості буде вищою, ніж можна було б очікувати.
![Аналіз вразливостей компілятора Solidity та заходи реагування])https://img-cdn.gateio.im/webp-social/moments-c97428f89ed62d5ad8551cdb2ba30867.webp(
Рекомендації з безпеки
На основі аналізу моделей загроз вразливостей компілятора Solidity та історичного огляду вразливостей, пропонуємо наступні рекомендації для розробників та спеціалістів з безпеки.
Для розробників:
Використовуйте новішу версію компілятора Solidity. Хоча нові версії також можуть вводити нові проблеми з безпекою, відомих проблем з безпекою зазвичай менше, ніж у старих версіях.
Удосконалити юніт-тести. Більшість помилок на рівні компілятора призводять до того, що результати виконання коду не відповідають очікуванням. Такі проблеми важко виявити під час перевірки коду, але їх легко виявити на етапі тестування. Тому підвищення покриття коду може максимально зменшити ймовірність виникнення таких проблем.
Слід уникати використання внутрішньої асемблерної мови, складних операцій з багатовимірними масивами та складними структурами при кодуванні та декодуванні ABI, якщо немає чітких вимог, а також не слід бездумно використовувати нові можливості мови та експериментальні функції заради ефектності. Згідно з аналізом історичних вразливостей, більшість з них пов'язані з внутрішньою асемблерною мовою, кодерами ABI та подібними операціями. Компілятори дійсно частіше зазнають помилок при обробці складних мовних характеристик. З іншого боку, розробники також можуть припуститися помилок при використанні нових можливостей, що призводить до проблем безпеки.
Для охоронців:
Під час безпечного аудиту коду Solidity не слід ігнорувати безпекові ризики, які можуть бути внесені компілятором Solidity. Відповідний елемент перевірки в Smart Contract Weakness Classification)SWC( - SWC-102: Застаріла версія компілятора.
У внутрішньому процесі розробки SDL, закликайте команду розробників оновити версію компілятора Solidity та розгляньте можливість впровадження автоматичної перевірки версії компілятора в процес CI/CD.
Але не слід надмірно панікувати через вразливості компілятора, більшість вразливостей компілятора спрацьовують лише в певних кодових моделях, а це не означає, що контракти, зкомпільовані за допомогою вразливої версії компілятора, завжди мають ризик безпеки. Реальний вплив на безпеку потрібно оцінювати залежно від конкретної ситуації проекту.
Декілька корисних ресурсів:
Команда Solidity регулярно публікує сповіщення про безпеку:
Список помилок, що регулярно оновлюється в офіційному репозиторії Solidity:
Список помилок компілятора різних версій:
Code в правому верхньому куті трикутний знак оклику вказує на наявність вразливостей безпеки в поточній версії компілятора.
Підсумок
У цій статті розглядаються основні концепції компілятора, представлені вразливості компілятора Solidity і проаналізовані потенційні ризики безпеки, які можуть виникнути в реальному середовищі розробки Ethereum. На завершення надано кілька практичних порад для розробників та фахівців з безпеки.
![Аналіз вразливостей компілятора Solidity та заходи реагування])https://img-cdn.gateio.im/webp-social/moments-84f5083d8748f2aab71fd92671d999a7.webp(
Ця сторінка може містити контент третіх осіб, який надається виключно в інформаційних цілях (не в якості запевнень/гарантій) і не повинен розглядатися як схвалення його поглядів компанією Gate, а також як фінансова або професійна консультація. Див. Застереження для отримання детальної інформації.
Доклад про вразливості компілятора Solidity та стратегії їх запобігання
Аналіз вразливостей компілятора Solidity та стратегії реагування
Компилятор є однією з основних складових сучасних комп'ютерних систем. Це комп'ютерна програма, основна функція якої полягає в перетворенні вихідного коду мов високого рівня, зрозумілого та зручного для написання людиною, в інструкційний код, що може виконуватись на базовому ЦП або байтовій машині.
Більшість розробників та фахівців з безпеки зазвичай більше уваги приділяють безпеці коду програмних додатків, але можуть ігнорувати безпеку самого компілятора. Насправді, компілятор, як комп'ютерна програма, також має вразливості в безпеці, які в певних випадках можуть призвести до серйозних ризиків безпеки. Наприклад, під час компіляції та розбору виконання коду Javascript на фронтенді браузера, через вразливість у рушії розбору Javascript, користувачі можуть бути піддані атаці зловмисників, які використовують цю вразливість для віддаленого виконання коду, в кінцевому підсумку отримуючи контроль над браузером жертви або навіть операційною системою.
Компілер Solidity не є винятком, він має вразливості безпеки в кількох різних версіях.
Вразливість компілятора Solidity
Роль компілятора Solidity полягає в перетворенні коду смарт-контрактів, написаного розробниками, у код інструкцій Ethereum Virtual Machine (EVM). Ці інструкції 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() публічного перегляду повертає (uint) { повернути b; } }
Змінна storage b не була змінена, тому функція run() повинна повертати значення за замовчуванням 0. Але насправді, у згенерованому коді з вразливих версій компілятора, run() поверне 1.
Без розуміння цього вразливості компілятора, звичайним розробникам буде важко виявити вищезазначену помилку в коді простим переглядом. Цей приклад коду досить простий і, можливо, не завдасть особливих серйозних шкод. Але якщо змінна b використовується для перевірки прав, обліку активів та інших цілей, така невідповідність з очікуваннями може призвести до дуже серйозних наслідків.
Причина виникнення цього аномального явища полягає в тому, що EVM використовує стекову віртуальну машину, де кожен елемент стека має розмір 32 байти (, тобто розмір змінної uint256 ). З іншого боку, кожен слот у нижньому зберіганні також має розмір 32 байти. На рівні мови Solidity підтримуються різні типи даних менше 32 байтів, такі як uint32, і компілятор, обробляючи такі змінні, повинен відповідним чином очищати їх старші біти ( clean up ), щоб забезпечити правильність даних. У наведеному випадку, коли під час додавання виникає переповнення цілого числа, компілятор не очистив правильно старші біти результату, що призвело до запису старшого біта 1 у storage, в кінцевому підсумку перекривши змінну a змінною b, в результаті чого значення змінної b було змінено на 1.
SOL-2022-4 InlineAssemblyMemoryПобічні ефекти
Ця уразливість існує в компіляторах версій >=0.8.13 <0.8.15. Розгляньте наступний код:
солідність контракт C { функція f() public pure повертає (uint) { збірка { mstore(0, 0x42) } uint x; збірка { x := mload(0) } повернути x; } }
Компілятор Solidity під час перетворення мови Solidity в код EVM не просто здійснює простий переклад. Він також проводить глибокий аналіз потоку управління та даних, реалізуючи різні процеси оптимізації компіляції, щоб зменшити обсяг згенерованого коду і оптимізувати споживання газу під час виконання. Такі оптимізаційні операції є звичними для компіляторів різних мов високого рівня, але через велику кількість складних випадків, які потрібно враховувати, також легко можуть виникнути помилки або вразливості безпеки.
Вразливість вищезазначеного коду виникає через такі оптимізаційні операції. Розглянемо таку ситуацію: якщо в якійсь функції є код, що змінює дані за адресою пам'яті 0, але в подальшому це значення не використовується, то насправді можна безпосередньо видалити код, що змінює пам'ять 0, економлячи газ і не впливаючи на подальшу логіку програми.
Ця стратегія оптимізації сама по собі не є проблемою, але в конкретній реалізації коду компілятора Solidity такі оптимізації застосовуються лише в межах одного блоку assembly. У наведеному прикладі коду запис і доступ до пам'яті 0 знаходяться в двох різних блоках assembly, в той час як компілятор проводить аналіз і оптимізацію лише окремого блоку assembly. Оскільки в першому блоці assembly після запису в пам'ять 0 немає жодної операції читання, то ця команда запису вважається зайвою, і вона буде видалена, що призведе до помилки. У версії з уразливістю функція f( поверне значення 0, тоді як насправді наведений код має повертати правильне значення 0x42.
) SOL-2022-6 AbiReencodingHeadOverflowWithStaticArrayCleanup
Вразливість впливає на компілятори версій >= 0.5.8 < 0.8.16. Розгляньте наступний код:
солідність контракт C { функція f###uint8( calldata a[4] публічна чиста повертає )string memory( { повернути string)abi.encode(a(); } }
У нормальних умовах змінна a, що повертається вищезгаданим кодом, повинна дорівнювати "aaaa". Але в версії з уразливістю вона повертає порожній рядок "".
Причиною цієї вразливості є помилка в Solidity, коли операція abi.encode над масивами типу calldata неправильно очищає деякі дані, що призводить до зміни сусідніх інших даних і викликає несумісність даних після кодування та декодування.
Слід зазначити, що Solidity під час виконання зовнішнього виклику та емісії події неявно кодує параметри за допомогою abi.encode, тому ймовірність виникнення вищезгаданої вразливості буде вищою, ніж можна було б очікувати.
![Аналіз вразливостей компілятора Solidity та заходи реагування])https://img-cdn.gateio.im/webp-social/moments-c97428f89ed62d5ad8551cdb2ba30867.webp(
Рекомендації з безпеки
На основі аналізу моделей загроз вразливостей компілятора Solidity та історичного огляду вразливостей, пропонуємо наступні рекомендації для розробників та спеціалістів з безпеки.
Для розробників:
Використовуйте новішу версію компілятора Solidity. Хоча нові версії також можуть вводити нові проблеми з безпекою, відомих проблем з безпекою зазвичай менше, ніж у старих версіях.
Удосконалити юніт-тести. Більшість помилок на рівні компілятора призводять до того, що результати виконання коду не відповідають очікуванням. Такі проблеми важко виявити під час перевірки коду, але їх легко виявити на етапі тестування. Тому підвищення покриття коду може максимально зменшити ймовірність виникнення таких проблем.
Слід уникати використання внутрішньої асемблерної мови, складних операцій з багатовимірними масивами та складними структурами при кодуванні та декодуванні ABI, якщо немає чітких вимог, а також не слід бездумно використовувати нові можливості мови та експериментальні функції заради ефектності. Згідно з аналізом історичних вразливостей, більшість з них пов'язані з внутрішньою асемблерною мовою, кодерами ABI та подібними операціями. Компілятори дійсно частіше зазнають помилок при обробці складних мовних характеристик. З іншого боку, розробники також можуть припуститися помилок при використанні нових можливостей, що призводить до проблем безпеки.
Для охоронців:
Під час безпечного аудиту коду Solidity не слід ігнорувати безпекові ризики, які можуть бути внесені компілятором Solidity. Відповідний елемент перевірки в Smart Contract Weakness Classification)SWC( - SWC-102: Застаріла версія компілятора.
У внутрішньому процесі розробки SDL, закликайте команду розробників оновити версію компілятора Solidity та розгляньте можливість впровадження автоматичної перевірки версії компілятора в процес CI/CD.
Але не слід надмірно панікувати через вразливості компілятора, більшість вразливостей компілятора спрацьовують лише в певних кодових моделях, а це не означає, що контракти, зкомпільовані за допомогою вразливої версії компілятора, завжди мають ризик безпеки. Реальний вплив на безпеку потрібно оцінювати залежно від конкретної ситуації проекту.
Декілька корисних ресурсів:
Команда Solidity регулярно публікує сповіщення про безпеку:
Список помилок, що регулярно оновлюється в офіційному репозиторії Solidity:
Список помилок компілятора різних версій:
Code в правому верхньому куті трикутний знак оклику вказує на наявність вразливостей безпеки в поточній версії компілятора.
Підсумок
У цій статті розглядаються основні концепції компілятора, представлені вразливості компілятора Solidity і проаналізовані потенційні ризики безпеки, які можуть виникнути в реальному середовищі розробки Ethereum. На завершення надано кілька практичних порад для розробників та фахівців з безпеки.
![Аналіз вразливостей компілятора Solidity та заходи реагування])https://img-cdn.gateio.im/webp-social/moments-84f5083d8748f2aab71fd92671d999a7.webp(