Light mode

Машинное обучение против уязвимостей в смарт-контрактах

  • #Смарт-контракты
  • #LLM

О чем материал

Рассмотрим исследование гибридного подхода для детекции уязвимостей в смарт-контрактах, сочетающего классические алгоритмы, LLM и графовый анализ.

Развитие блокчейн-технологий стремительно ускорило распространение смарт-контрактов, однако обеспечение их безопасности остается острой проблемой для бизнеса. Мы провели исследование и разработали архитектуру решения для выявления уязвимостей в смарт-контрактах с использованием LLM и статического анализа кода. 

Цель исследования — создать автоматизированную систему анализа кода и генерации детекторов уязвимостей Solidity-контрактов на основе данных из открытых источников. 

Сбор и подготовка датасета

Для построения эффективной системы анализа необходим качественный и разнообразный датасет. Отметим основные сложности при его сборе:

  1. Релевантность данных. Смарт-контракты из открытых источников могут быть устаревшими или неприменимыми на практике.
  2. Выявление уязвимостей. Разнообразие атак усложняет обнаружение всех типов уязвимостей, особенно новых или же скрытых ошибок в коде.
  3. Ложноположительные результаты. Ошибки в разметке могут привести к обучению модели на некорректных примерах.

Подготовленный датасет включает 27 073 Solidity-контракта для анализа безопасности и выявления уязвимостей:

  • Источники данных — GitHub (tintinweb.github.io) и Etherscan — позволяют включить как реальные, так и аннотированные уязвимые контракты. В выборку вошло 10 000 контрактов из сети Ethereum.
  • Состав датасета — 21 360 уязвимых и 5713 безопасных контрактов.
  • Отбор: исключены прокси-контракты и контракты без исходного кода. 
  • Основные атрибуты: адрес контракта, исходный код, байт-код, список функций, импортируемые библиотеки и метки уязвимостей. 
  • Структура контрактов: большинство содержат небольшое количество функций, но встречаются и сложные структуры более чем со 100 функциями. 

При разработке алгоритмов обнаружения уязвимых Solidity-контрактов ML-методами был учтен дисбаланс данных (преобладание уязвимых контрактов), чтобы избежать сильного смещения модели и сохранить точность классификации.

Архитектура решения

Анализатор включает несколько ключевых компонентов.

  1. Построение Data Flow Graph (DFG) с использованием инструментов slither и aderyn. Это граф зависимостей, который визуализирует связи между переменными, функциями и операциями внутри контракта.
  2. Дообучение модели GraphCodeBERT-Base-Solidity-Vulnerability на полученном датасете.

Эта модель использует подходы графового анализа кода для более точно выявления уязвимостей. Например:

contract Vulnerable {

uint public balance;

function withdraw(uint amount) public {

require(amount <= balance);

msg.sender.transfer(amount);

balance -= amount;

}

}

Рисунок 1. Граф потока данных.svg
Рисунок 1. Граф потока данных
  1. Узел balance — переменная, отражающая текущее финансовое состоянием контракта.
  2. Узел amount — переменная, которую вводит пользователь для выполнения операции (количество средств, которые он хочет вывести).
  3. Узел msg.sender.transfer(amount) — операция перевода средств, потенциально уязвима для Reentrancy-атак.
  4. Связь require(amount ← balance) — это проверка условий, которая должна предотвратить вывод средств, превышающих баланс. Однако она не защищает от повторного вызова функции перед обновлением balance.

В примере графа потока данных (см. рис. 1) связь между переменными balance, amount и операцией msg.sender.transfer(amount) помогает модели заметить потенциальную уязвимость типа Reentrancy. Рассмотрим, как Data Flow Graph (DFG) может это сделать:

  • DFG выявляет неправильный порядок операций: msg.sender.transfer(amount) выполняется до изменения переменной balance. Это нарушение паттерна Check Effect Iteraction (CEI), который используется для предотвращения Reentrancy-атак. 
  • Если атакующий контракт вызывает withdraw() рекурсивно до обновления balance, он сможет вывести больше средств, чем положено. 
  • Нарушение паттерна CEI создает уязвимость, т. к. контракт не обновляет состояние (снижение баланса) до того, как выполняются внешние взаимодействия (перевод средств).
  1. Статический анализ

Помогает выявлять потенциальные уязвимости в смарт-контрактах, оценивая их структуру и признаки использования опасных операций. Разберем основные методы анализа:

  • Проверка на опасные операции: tx.origin (уязвимость, приводящая к фишингу), delegatecall (может привести к захвату управления и модификации хранилища контракта), call и send (небезопасные методы отправки ETH; могут не завершиться успешно, но при этом не откатывают всю транзакцию). 
  • Анализ дополнительных фич: извлечение из кода контрактов признаков, которые могут быть связаны с уязвимостями, для глубокого анализа. 

Выделим ключевые категории признаков:

Анализ модификаторов и контроля доступов:

  • has_onlyOwner — проверяет, используется ли модификатор onlyOwner (контроль доступа);
  • has_access_modifier — проверка на наличие модификаторов onlyOwner, onlyAdmin (модификаторы ролей);
  • num_modifiers — количество модификаторов.

Анализ вызовов и внешних взаимодействий:

  • num_external_calls — вызовы call, delegatecall, send, transfer. Также возможны неявные call-вызовы к функциям контрактов;
  • num_external_contract_calls — вызовы других контрактов (contractInstance.method()).
  • num_delegatecall — количество delegatecall (опасный вызов внешних контрактов);
  • num_msg_sender — количество обращений к msg.sender;
  • num_msg_value — количество обращений к msg.value;
  • num_payable_functions — количество функций с payable.

Общая структура кода:

  • num_functions — количество объявленных функций;
  • num_public_functions — количество публичных функций;
  • num_if_statements — количество if-условий;
  • num_loops — количество циклов (for, while);
  • avg_function_length — средняя длина функций;
  • code_length — длина кода контракта;
  • total_contract_length — полная длина контракта в строках;
  • num_lines — общее количество строк кода;
  • num_contracts — количество контрактов в файле.

Использование проверок и событий:

  • num_require — количество проверок require (условий);
  • num_assert — количество жестких проверок assert;
  • num_revert — количество revert (откатов транзакций);
  • num_events — количество событий;
  • has_event — есть ли в коде хотя бы одно событие.

Использование опасных конструкций: 

  • num_selfdestruct — количество вызовов selfdestruct (удаление контракта);
  • num_decimals — количество объявлений переменной decimals.

Лингвистический анализ кода:

  • w2v_avg_scalar (Word2Vec Average Scalar) — усредненное значение всех компонент векторов слов в коде контракта. В нашем случае применяем как обобщенное представление важности слов, встречающихся в контракте;
  • fasttext_avg_scalar (FastText Average Scalar) — аналогичный показатель для FastText-модели, которая, в отличие от Word2Vec, учитывает морфологию и подсловные (n-gram) структуры, что улучшает обработку редких слов и морфологически сложных языков.

Корреляция признаков

После выделения признаков переходим к анализу их взаимодействий. Для этого построим тепловую карту корреляции признаков (см. рис. 2).

Рисунок 2. Тепловая карта корреляции признаков

Разберем полученные значения: 

  • Как видим из рисунка, сильная отрицательная корреляция с целевой переменной (Label) наблюдается у признаков num_functions (−0,43), num_lines (−0,37), code_length (−0,37), num_contracts (−0,34). То есть более сложные контракты (по количеству строк, функций и длине кода) реже содержат уязвимости. 
  • Умеренная отрицательная корреляция у w2v_avg_scalar (−0,28), num_events (−0,28), num_msg_sender (−0,19): контракты с большим количеством событий и семантически сложные контракты (Word2Vec) менее подверженных уязвимостям. 
  • Слабая корреляция с уязвимостями у num_require (−0,11), num_revert (−0,13) говорит о том, что наличие проверок require() и revert() не является определяющим фактором безопасности. 
  • Сильная внутренняя корреляция между структурными характеристиками кода у code_length и num_lines (0,97), num_functions и num_lines (0,78): чем больше строк в контракте, тем больше в нем функций и кода. 
  • Относительно высокая корреляция у num_require и num_msg_sender (0,48): контракты с большим количеством проверок require() чаще взаимодействуют с msg.sender. 
  • Корреляция у num_events и num_functions (0,37): больше функций в контракте означает больше событий. 
  • Незначительная корреляция у num_delegatecall, num_selfdestruct, num_assert. Эти признаки не имеют значимой связи с другими переменными. Это может означать, что они встречаются редко и не оказывают сильного влияния на модель. 

Резюмируем: более сложные контракты реже содержат уязвимости, а частое использование msg.sender и require() не является гарантией их безопасности. Эти результаты необходимо учитывать при обучении моделей, особенно при выборе признаков для классификации.

Машинное обучение

Пайплайн машинного обучения

  1. Предобработка данных
    • Отбор значимых признаков на основе анализа корреляции.
    • Преобразование текстовых данных (w2v_avg_scalar, fasttext_avg_scalar).
    • Нормализация числовых признаков.
  2. Обучение моделей
    • Тестирование традиционных алгоритмов (Logistic Regression, Random Forest, Gradient Boosting, Support Vector Machine).
    • Дообучение GraphCodeBERT-Base-Solidity-Vulnerability.
  3. Ансамблирование
    • Hard Voting (жесткое голосование) — выбор класса большинством моделей.
    • Soft Voting (взвешенное голосование) — усреднение вероятностей.
  4. Оценка качества
    • Метрики: Accuracy, False Positives, False Negatives, ROC-AUC.
    • Анализ ошибок классификации (FP, FN).

Дообучение модели GraphCodeBERT-Base-Solidity-Vulnerability 

Рассмотрим, как Solidity-код трансформируется в разные графовые представления для дообучения GraphCodeBERT-Base-Solidity-Vulnerability. 

Возьмем контракт, который позволяет владельцу (owner) вывести средства с баланса контракта. Однако если управление над owner получит злоумышленник, станет возможен непреднамеренный вывод средств.

pragma solidity ^0.8.0;

contract Vulnerable {

address owner;

constructor() {

owner = msg.sender;

}

function withdraw() public {

require(msg.sender == owner, "Not the owner");

payable(msg.sender).transfer(address(this).balance);

}

}

  1. Абстрактное синтаксическое дерево (AST)

AST представляет код как иерархическое дерево, где каждый узел соответствует конструкции языка. В нашем случае дерево будет выглядеть так:

FunctionDefinition(withdraw)

├── RequireStatement

│ ├── Identifier(msg.sender)

│ ├── BinaryOperation(==)

│ ├── Identifier(owner)

├── ExpressionStatement

├── FunctionCall(transfer)

├── MemberAccess(payable(msg.sender))

├── MemberAccess(address(this).balance)

Вершина FunctionDefinition(withdraw) содержит оператор require(), который проверяет условие на равенство между msg.sender и owen. Затем, если проверка проходит, выполняется перевод средств через transfer().

  1. Граф потока управления (CFG)

Control Flow Graph показывает, как выполняется код в зависимости от условий. Он позволяет проанализировать, как данные проходят через различные участки кода. 

START → check (msg.sender == owner?) → YES → transfer() → END

↘ NO → REVERT

Анализ CFG:

  • Если msg.sender == owner, то средства переводятся.
  • Если нет, выполнение прерывается (REVERT).

Этот граф используется для анализа ветвлений и потенциальных уязвимостей, например пропущенных проверок.

  1. Граф потока данных

DFG отображает, как данные передаются между переменными и функциями.

msg.sender ---> owner (constructor)

|

├──> require(msg.sender == owner)

|

├──> payable(msg.sender)

| |

| ├──> transfer()

| |

address(this).balance ------┘

Анализ DFG:

  • msg.sender передается в owner в конструкторе.
  • В функции withdraw() проходит проверка msg.sender перед выполнением transfer().
  • balance контракта используется в transfer() — это может быть уязвимостью в Reentrancy-сценариях.
  1. Гибридное представление: граф + токены

GraphCodeBERT представляет графы в виде списка ребер:

[

{"source": "msg.sender", "target": "transfer", "edge_type": "flows_to"},

{"source": "require", "target": "transfer", "edge_type": "controls"}

]

Кроме графовой структуры, модель использует токены:

[CLS] function withdraw() public { require ( msg . sender == owner , " Not the owner " ) ; 

payable ( msg . sender ) . transfer ( address ( this ) . balance ) ; } [SEP]

Гибридное представление (граф + токены) позволяет модели учитывать как структуру кода, так и его семантическое значение.

Оценка качества

 Рисунок 3. Диаграммы средней точности и стандартного отклонения моделей

На рис. 3 представлены диаграммы для традиционных алгоритмов и их ансамблей, демонстрирующие среднюю точность моделей (слева) и их стандартное отклонение (справа). Расчеты проведены с использованием K-Fold и Shuffle Split кросс-валидации:

  • Все модели показали высокую точность (около 0,85–0,93).
  • Ансамблевые методы (Hard Voting и Soft Voting) достигают лучших результатов, улучшая предсказания за счет комбинирования нескольких моделей.
  • Разница между методами K-Fold и Shuffle Split минимальна, что подтверждает стабильность моделей.
  • Gradient Boosting имеет наибольший разброс (самый высокий Std), что указывает на нестабильность предсказаний модели на разных разбиениях данных.
  • Logistic Regression, Hard Voting и Soft Voting демонстрируют наименьшее отклонение, что свидетельствует о большей надежности их предсказаний.
  • Разница между K-Fold и Shuffle Split заметна, но Shuffle Split дает более стабильные результаты (меньший разброс).
Рисунок4. Сравнение моделей по всей метрике
 Accuracy (Test)False Positives (FP)False Negatives (FN)ROC-AUC ScoreConfusion Matrix
Logistic Regression0.8797785201310.931623TN=619, FP=520, FN=131, TP=4145
Random Forest0.9364731761680.9741TN=963, FP=176, FN=168, TP=4108
Gradient Boosting0.9078492742250.955142TN=865, FP=274, FN=225, TP=4051
Support Vector Machine0.8976923721820.945679TN=767, FP=372, FN=182, TP=4094
angusleung100/GraphCodeBERT-Base-Solidity-Vulnerability 
(дообученная)
 
0.71561148392NoneTN=30, FP=1148, FN=392, TP=3845 
Ensemble (Hard Voting)0.914866296165NoneTN=843, FP=296, FN=165, TP=4111
Ensemble (Soft Voting)0.9163432971560.96663TN=842, FP=297, FN=156, TP=4120
GraphCodeBert Solidity0.7156051148392 TN=30, FP=1147, FN=392, TP=3845
Ensemble (All Models + GraphCodeBERT)0.910619290194 TN=888, FP=290, FN=194, TP=4043
Таблица 1. Оценка качества по метрикам Accuracy, False Positives, False Negatives, ROC-AUC

Из табл. 1 можно сделать следующие выводы:

  • Logistic Regression показала самую низкую точность (0,8798) среди традиционных методов, допустив 520 FP и 131 FN.
  • Random Forest достиг наивысшей точности (0,9365) среди простых моделей, однако число FP (176) и FN (168) остается значительным.
  • Gradient Boosting продемонстрировал сбалансированные результаты с точностью 0,9078, но увеличил FN (225 случаев).
  • Support Vector Machine показал результаты хуже, чем Random Forest, но лучше, чем Logistic Regression (точность 0.8977).
  • GraphCodeBERT (дообученная модель) значительно уступает классическим моделям (точность 0,7156), имея очень высокий уровень FP (1148) и FN (392). Это говорит о переобучении на предобученных данных и сложности адаптации к реальному Solidity-коду.
  • Ансамблевые методы показали лучшие результаты: 
    • Hard Voting: точность 0.9149, 296 FP и 165 FN.
    • Soft Voting: наилучший баланс FP/FN, точность 0.9163, 297 FP и 156 FN.
    • Ансамбль с GraphCodeBERT ухудшил результаты (точность 0,9106).

Наилучшие результаты показал алгоритм Random Forest. График важности признаков (см. рис. 5) показывает, что ключевую роль в классификации уязвимостей Solidity-контрактов играют количество функций (num_functions) и сложность кода (num_contracts, num_lines, code_length). Использование require, revert и событий также влияет на предсказания модели, но в меньшей степени. Вызовы selfdestruct, delegatecall и других рискованных операций не являются доминирующими признаками, хотя могут быть полезны в отдельных случаях. Добавление семантических признаков (Word2Vec, FastText) усиливает модель, что показывает важность текстового анализа кода.

Рисунок 5. График важности признаков

Анализ ошибок классификации False Positives и False Negatives

Анализ FP и FN для каждого алгоритма и ансамблевой модели помогает определить лучшую архитектуру.

False Positives — ложные срабатывания:

  • Во многих FP-контрактах присутствуют лицензии SPDX (GPL, MIT, BSL-1.1 и др.)
  • Среди FP встречаются примеры из OpenZeppelin, которые вряд ли содержат уязвимости.
  • Часть ложных срабатываний вызвана сложными контрактами с импортами, где статический анализ ошибочно интерпретировал их как потенциально уязвимые.

False Negatives — пропущенные уязвимости:

  • В FN часто встречаются функции multicall, mint, swap, withdraw, что указывает на высокую вероятность критических багов.
  • Некоторые FN связаны с reentrance- и DoS-уязвимостями. Моделям сложно их детектировать.
  • Обнаружены контракты с проблемами безопасности, описанными в отчетах CTF и багбаунти, но оставшиеся незамеченными нашими моделями.

Выводы и дальнейшие планы

Использование ансамблевого подхода Hard Voting Ensemble вместе с GraphCodeBERT-Base-Solidity-Vulnerability позволяет детектировать уязвимости в смарт-контрактах, которые пропускает ансамбль классических моделей.

Представленный нами подход автоматизирует процесс выявления уязвимостей, снижая вероятность атак благодаря сочетанию ансамблевого и графового методов на дообученной GraphCodeBERT-Base-Solidity-Vulnerability.

В дальнейшем мы планируем доработать модель: 

  • интеграцией генеративных моделей (GAN) для адаптации новых методов атак и защитных механизмов;
  • расширением датасета для более точного файн-тюнинга LLM;
  • динамическим анализом (включая фаззинг) для обнаружения логических уязвимостей, которые сложно выявить статическим анализом;
  • прогнозированием нестандартных сценариев эксплуатации уязвимостей, которые могут быть неочевидны для традиционных статических анализаторов.

Так как большинство статических анализаторов ищут известные шаблоны уязвимостей, но неспособны выявлять новые и сложные атаки, также целесообразно:

  • применять обучение с подкреплением (RL) для обучения агентов поиску новых уязвимостей;
  • разрабатывать методы автоматического тестирования, анализирующие возможные сценарии эксплуатации.

Мы дěлаем Positive Research → для ИБ-экспертов, бизнеса и всех, кто интересуется ✽ {кибербезопасностью}