Анализатор: Slither
Описание: Open-source static analysis framework for Solidity
githib: https://github.com/trailofbits/slither


Это статический анализатор кода, написанный на python. Он умеет следить за переменными, вызовами, и детектирует вот такой список уязвимостей: https://github.com/trailofbits/slither#detectors. У каждой уязвимости есть ссылка с описанием, и, если вы новичок в Solidity, вам имеет смысл ознакомиться со всеми.


Slither может работать, как модуль python и предоставлять программисту интерфес, для аудита по собственному плану. Простой и показательный пример того, что умеет делать slither можно увидеть тут: https://github.com/trailofbits/slither/blob/master/examples/scripts/functions_writing.py


Мы еще вернемся к сценариям анализа в конце статьи, а пока запустим Slither:


git clone https://github.com/trailofbits/slither.git
cd slither
docker build -t slither .

и попробуем проанализировать наш контракт.


Заходим в каталог с constructor-eth-booking и пытаемся запустить slither из докера:


$ docker run -v $(pwd)/contracts:/slither/contracts slither contracts/flattened.sol

получаем ошибку “Source file requires different compiler version”, и теперь нам надо засунуть версию solc=0.4.20 в докер slither-а. Для этого правим Dockerfile самого Slither, как было указано в п.2 во введении, т.е. где нибудь в конце Dockerfile (https://github.com/trailofbits/slither/blob/master/Dockerfile) добавляем строку:


COPY --from=ethereum/solc:0.4.20 /usr/bin/solc /usr/bin

, пересобираем образ, запускаем, ура, все компилируется.


Видим вывод, warning-и про разные “pragma” и про неверные названия переменных типа Parameter '_price' of Booking.Booking (flattened.sol#73) is not in mixedCase. Анализаторы выдают много предупреждений, но мы ищем реальные баги, и не будем обращать внимание на мелочь. Отфильтруем все сообщения про mixedCase, сейчас не до стиля:


$ docker run -v $(pwd)/contracts:/slither/contracts slither contracts/flattened.sol 2>&1 | fgrep -v 'mixedCase'

Истинные программисты пропускают все зелененькое, смотрят все красненькое, и вот что помимо false-positives нашел в контракте Slither:


Booking.refundWithoutCancellationFee (flattened.sol#243-250) sends eth to arbirary user
        Dangerous calls:
        - client.transfer(address(this).balance) (flattened.sol#249)
Reference: https://github.com/trailofbits/slither/wiki/Vulnerabilities-Description#functions-that-send-ether-to-arbitrary-destinations
INFO:Detectors:
Booking.refundWithCancellationFee (flattened.sol#252-259) sends eth to arbirary user
        Dangerous calls:
        - owner.transfer(m_cancellationFee) (flattened.sol#257)
        - client.transfer(address(this).balance) (flattened.sol#258)
Reference: https://github.com/trailofbits/slither/wiki/Vulnerabilities-Description#functions-that-send-ether-to-arbitrary-destinations

теперь смотрим что в контракте не так с этим функциями:


    /************************** PRIVATE **********************/

    function refundWithoutCancellationFee() private  {
        address client = m_client;
        m_client = address(0);
        changeState(State.OFFER);

        client.transfer(address(this).balance);
    }

    function refundWithCancellationFee() private {
        address client = m_client;
        m_client = address(0);
        changeState(State.CANCELED);

        owner.transfer(m_cancellationFee);
        client.transfer(address(this).balance);
    }

при этом, например, функция refundWithoutCancellationFee() вызывается вот так:


    function rejectPayment() external onlyOwner onlyState(State.PAID) {
        refundWithoutCancellationFee();
    }

    function refund() external onlyClient onlyState(State.PAID) {
        refundWithoutCancellationFee();
    }

Хм, формально ошибок нет: вызовы защищены всякими onlyOwner, но Slither ругается, что, мол, внутри refundWithoutCancellationFee() без всяких проверок шлется эфир. И он прав, сама функция действительно не имеет почти никаких ограничений. Пускай она private, и вызывается из wrapper-ов “rejectPayment()” “refund()” c нужными ограничениями, но в таком виде, если дорабатывать контракт — велик риск забыть про ограничения и воткнуть вызов refundWithoutCancellationFee() в какое нибудь другое место, доступное атакующему. Так, что, пусть формально уязвимости нет, информация оказалась полезной — это как минимум “warning” level, если по заданию планируется развивать код контракта дальше. В данном случае, две функции от разных участников используют один код, и, такое решение было принято для экономии газа — контракт является одноразовым, и стоимость его выкладки является важным фактором.


Я перепроверил, не просто ли так Slither ругается на любую отправку эфира, и перенес тело функции прямо в вышеуказанные “rejectPayment()” и “refund()”, предупреждение исчезло, т.е. Slither понял, что теперь эфир не высылается без проверок адресов. Отличное начало!


Теперь проверим как Slither следит за инициализацией переменных, для этого закомментируем две инициализации:


- m_fileHash = _fileHash;
+ // m_fileHash = _fileHash;
- m_price = _price;
+ // m_price = _price;

первая — не сильно важная, в плане дыр, кроме траты ресурсов, ибо m_fileHash не используется нигде, он просто сохраняется в блокчейне при создании контракта. А вот m_price используется, и Slither правильно ругается на то, что m_price нигде не инициализируется, хотя и используется:


Booking.m_price (flattened.sol#128) is never initialized. It is used in:
        - fallback (flattened.sol#144-156)

Ну это простой трюк, как и ожидалось, всё сработало нормально.


Теперь внесём в контракт столь полюбившуюся всем reentrancу: будем изменять state контракта после внешнего вызова. Внесем такие изменения:


    function refundWithoutCancellationFee() private  {
        address client = m_client;
-         m_client = address(0);
-         changeState(State.OFFER);
-         client.transfer(address(this).balance);
+        client.call.value(address(this).balance)();
+        m_client = address(0);
+        changeState(State.OFFER);
    }

Пришлось заменить transfer на call, т.к. на вариант с transfer Slither не ругается из за того, что transfer отправляет вызов с минимумом газа, и обратный вызов невозможен (хотя при переходе на форк Constantinople в Ethereum, была изменена цена газа, и это заново “включило” reentrancy атаку с использованием transfer (https://github.com/ChainSecurity/constantinople-reentrancy).


Результат поиска reentrancy:


Reentrancy in Booking.refundWithoutCancellationFee (flattened.sol#243-253):
        External calls:
        - client.call.value(address(this).balance)() (flattened.sol#245)
        State variables written after the call(s):
        - m_client (flattened.sol#246)

Прекрасно, как минимум изменять state variables после внешних вызовов он не даст, и это очень хорошо.


Если двигаться по списку, то остальные уязвимости в списке либо представляют собой просто поиск конкретных методов в коде, либо известные паттерны, которые при наличии доступа к разметке кода, выполненной python-ом, конечно же работают, и достаточно надежно. Т.е. well-knows паттерны Slither не пропустит.


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


-         client.transfer(address(this).balance
+         for (uint i=0; i < 1; i++) {                                                                                                                                     
+             client.transfer(address(this).balance - 999999999999999999);                                                                                                 
+         }  

и результат:


Booking.refundWithoutCancellationFee has external calls inside a loop:
        - client.transfer(address(this).balance - 999999999999999999) (flattened.sol#252)
Reference: https://github.com/trailofbits/slither/wiki/Vulnerabilities-Description/_edit#calls-inside-a-loop

Цикл выполняется единожды, и является вырожденным — поэтому выданное предупреждение — false positive, а отсутствие предупреждения об опасной арифметике — false negative. Анализ типов, результатов операций, подсчет вызовов — задачи не для статических анализаторов. Поэтому чётко понимайте, какие ошибки найдет Slither, а какие необходимо искать с помощью других инструментов.


Мы обещали упомянуть о возможности написания собственных сценариев для тестирования, и вывода всякой интересной информации о контракте с помощью ключа --print. С этой точки зрения, Slither представляет собой отличный инcтрумент для CI. Разработчики большой системы контрактов знают названия критических для безопасности переменных: балансов, размеров комиссий, флагов, и могут написать сценарий тестирования, который будет блокировать любые изменения в коде, которые, к примеру, перезаписывают важную переменную, или меняют state переменные после внешнего вызова, и его отлично предсказуемый анализ является отличным инструментом для использования в hook-ах.
Задача Slither — избавить вас от глупых багов, найти хорошо знакомые опасные паттерны и предупредить разработчика. В этом варианте он хорош и как инструмент начинающего разработчика на Solidity, сразу подсказывая как правильно писать код на Solidity.


Итоги


В моем личном тестировании я бы поставил Slither четверку, за универсальность, простоту и удобство использования, а также за простые и понятные сценарии тестирования и приспособленность к CI.
Slither уверенно нашел реальный WARNING, связанный с использованием функции, отправляющей эфир, обнаружил все внесенные баги. Он не справился лишь с динамическим анализом, который формально и не должен делать, иначе придется пожертвовать универсальностью, предсказуемостью и простотой использования.


В следующей статье мы займемся анализатором Mythril, а вот все оглавление статей, которые готовы или планируются к написанию:
Часть 1. Введение. Компиляция, flattening, версии Solidity
Часть 2. Slither (эта статья)
Часть 3. Mythril (в процессе написания)
Часть 4. Manticore (в процессе написания)
Часть 5. Echidna (в процессе написания)

Комментарии (0)