Attention! S in Ethereum stands for Security. Part 4. Tools


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


    В первой части мы обсудили front-running attack, различные алгоритмы генерации случайных чисел и отказоустойчивость сети с Proof-of-Authority консенсусом. Во второй говорили об Integer overflow, ABI encoding/decoding, Uninitialized storage pointer, Type Confusion и о том, как сделать бэкдор. В третьей части мы затронули несколько отличительных особенностей Solidity и разобрали кое-какие логические уязвимости, встречающиеся в контрактах. В этой части мы предлагаем вашему вниманию обзор существующих инструментов для анализа смарт-контрактов.


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



    Далее мы коснёмся каждой из них. Какие-то из инструментов работают только при наличии исходного кода смарт-контракта, другие — и без него. И сразу оговоримся, что речь пойдет только о тех инструментах, которые нам известны, есть в открытом доступе, и с которыми мы работали. Если у вас есть опыт работы с какими-либо другими тулзами данной тематики, то милости просим в комментарии :)


    Отладка


    На текущий момент существует всего одна (или единственная удобная) возможность отлаживать смарт-контракт с исходным кодом — Remix IDE. А если необходимо разобраться в байткоде, то вам может помочь Radare2 или MythrilTraceExplorer.


    Remix IDE


    Если вы разработчик, чаще всего будете использовать именно Remix для отладки (скорее всего, вы так и делаете). Очень удобно отслеживать состояние локальных переменных (стек), состояние memory, storage, потребляемый gas и многое другое.


    Рисунок 1. Интерфейс Remix IDE

    Если вас пугает содержимое вкладки "Storage completely loaded", советуем разобраться с устройством storage. Из минусов можно отметить разве что периодические баги в JavaScript VM и тот факт, что если вы используете ассемблерные вставки, то на построчную отладку вы вряд ли можете надеяться.


    Radare2


    Если не нравится Remix, но обожаете консольку, то попробуйте Radare2. При помощи него можно дизассемблировать и отлаживать контракты. Для отладки используются трассы (записанные случившиеся транзакции). Радар по rpc подключается к ноде (ganache-cli удобен) и стягивает данные, необходимые для повторения транзакции.


    В графическом режиме можно отрыть тайлы с содержимым стека и памяти:


    Рисунок 2. Интерфейс Radare 2

    Можно ставить точки останова и писать в память, однако мы не нашли способа заглянуть в storage или модифицировать его.


    У ребят из Positive Technologies есть примеры использования Radare для дизассемблирования и отладки контрактов. Так как радар не самый интуитивный инструмент, советуем заглянуть и ознакомиться с его командами.


    Автоматический поиск уязвимостей


    До нас уже было проведено сравнение трёх инструментов — Mythrill, Manticore и Oyente. По результатам этого анализа лидирует Mythrill, а хуже всех справляется Oyente. Во время тестов авторы "кормили" инструменты исходными кодами контрактов, так что результаты не отражают, насколько хорошо они справляются с сырым байткодом, но в основном совпадают с нашими впечатлениями.


    Mythril


    Mythril обладает широкой функциональностью, в том числе, для анализа как исходного кода, так и байткода смарт-контрактов. Примечательно то, что он может также работать on-chain, то есть в режиме online подключиться к ноде, выкачать контракт и проанализировать его. Также он является дизассемблером и умеет строить неплохие графы (но будьте осторожны, он часто делает это некорректно, так что лучше попробуйте Ethersplay или IDA-EVM). Также Mythril может сгенерировать state для тулзы под названием MythrilTraceExplorer, которая позволит вам отлаживать байткод смарт-контракта с символьными переменными:


    Рисунок 3. Интерфейс MythrilTraceExplorer

    С полным перечнем опций Mythil и MythrilTraceExplorer можно ознакомиться в документации, а найти секретный флаг, которого в ней нет, можно тут.


    Возьмём контракт eth_tx_order_dependence_minimal из тестов Consensys


    Исходник eth_tx_order_dependence_minimal
    contract Benchmark {
        address public owner;
        bool public claimed;
        uint public reward;
    
        function Benchmark() public {
            owner = msg.sender;
        }
    
        function setReward() public payable {
            require (!claimed);
    
            require(msg.sender == owner);
            owner.transfer(reward);
            reward = msg.value;
        }
    
        function claimReward(uint256 submission) {
            require (!claimed);
            require(submission < 10);
    
            msg.sender.transfer(reward);
            claimed = true;
        }
    }

    и посмотрим, как Mythril справляется при наличии исходных кодов:


    user% myth -x eth_tx_order_dependence_minimal.sol 
    ==== Ether send ====
    Type: Warning
    Contract: Benchmark
    Function name: claimReward(uint256)
    PC address: 693
    In the function 'claimReward(uint256)' a non-zero amount of Ether is sent to msg.sender.
    Call value is storage_1.
    
    There is a check on storage index 7. This storage slot can be written to by calling the function 'claimReward(uint256)'.
    --------------------

    а теперь без них:


    user% myth -x -c 60806040523...5f9dc0029
    The analysis was completed successfully. No issues were detected.

    Как видим, сырой байткод Mythril проанализировать не удалось.


    Oyente


    Инструмент с собственным движком символьного выполнения для обнаружения уязвимостей в смарт-контрактах, написан на Python. Не так давно разработчики наконец-то добавили вывод информации о входных данных, которые приведут к срабатыванию проблемы. Инструмент выдаёт более-менее адекватные результаты только при анализе контрактов с исходными кодами. Если натравить его на байткод, то он покроет только небольшую его часть и потому, скорее всего, не найдёт баги.


    По аналогии с Mythril поставим опыт и над Oyente и посмотрим, как хорошо он ищет уязвимости с исходными кодами и без них:


    user% oyente -s eth_tx_order_dependence_minimal.sol 
    INFO:root:contract eth_tx_order_dependence_minimal.sol:Benchmark:
    INFO:oyente.symExec:    ============ Results ===========
    INFO:oyente.symExec:      EVM Code Coverage:             98.3%
    INFO:oyente.symExec:      Integer Underflow:             False
    INFO:oyente.symExec:      Integer Overflow:              False
    INFO:oyente.symExec:      Parity Multisig Bug 2:         False
    INFO:oyente.symExec:      Callstack Depth Attack Vulnerability:  False
    INFO:oyente.symExec:      Transaction-Ordering Dependence (TOD): True
    INFO:oyente.symExec:      Timestamp Dependency:          False
    INFO:oyente.symExec:      Re-Entrancy Vulnerability:         False
    INFO:oyente.symExec:Flow1
    eth_tx_order_dependence_minimal.sol:14:9: Warning: Transaction-Ordering Dependency.
            owner.transfer(reward)
    Flow2
    eth_tx_order_dependence_minimal.sol:22:9: Warning: Transaction-Ordering Dependency.
            msg.sender.transfer(reward)
    INFO:oyente.symExec:    ====== Analysis Completed ======
    

    Теперь натравим на байткод этого контракта:


    user% oyente -b -s eth_tx_order_dependence_minimal.bin
    INFO:oyente.symExec:    ============ Results ===========
    INFO:oyente.symExec:      EVM Code Coverage:             18.0%
    INFO:oyente.symExec:      Callstack Depth Attack Vulnerability:  False
    INFO:oyente.symExec:      Transaction-Ordering Dependence (TOD): False
    INFO:oyente.symExec:      Timestamp Dependency:          False
    INFO:oyente.symExec:      Re-Entrancy Vulnerability:         False
    INFO:oyente.symExec:    ====== Analysis Completed ======

    Как видим, сильно уменьшилось покрытие (с 98 до 18 процентов) и инструмент не находит проблем с контрактом.


    Развивать Oyente непросто из-за (ну очень) плохого кода. Например, параметры из модуля в модуль передаются через глобальные переменные. Скорее всего потому, что над проектом работает не более полутора человек.


    На всякий случай стоит держать Oyente в своём ящичке с инструментами, но неизвестно, будет ли он развиваться и совершенствоваться.


    Porosity


    Пожалуй, самый распиаренный декомпилятор. Его главной особенностью является то, что он не работает.


    Для демонстрации этой фичи давайте декомпилируем игрушечный контракт, который для чистоты эксперимента соберём относительно старой версией solc'а 0.4.12 от 1 июля 2017-го. Отметим, что в последний раз Porosity обновлялся в январе 2018-го, так что можно расчитывать, что инструмент справится с нашим контрактом.


    Оригинал:


    contract sample {
        uint y;
    
        function foobar (uint x) {
            if (x == 0) {
                y = 0x1337;
            } else {
                y = 0xb33f;
            }
        }
    }

    Неповторимый вывод декомпилятора:


    .\porosity.exe --code $binRuntime --decompile
    
    function func_14ba3f12 {
          if (!msg.value) {
          }
          if (arg_4 != 0x0) {
             store[var_xrpMC] = 0xB33F;
          }
          store[var_zdGc8] = 0x1337;
          return;
          return;
    }

    При попытке передать ещё и abi, чтобы упростить инструменту работу, Porosity упадёт.


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


    Manticore


    Инструмент для символьного выполнения программ. Некоторое время назад в него добавили поддержку EVM и сейчас развивают (а если появится постоянный разработчик, то дело пойдёт ещё быстрее). В нём много фишечек, но они не оттестированы и чаще всего не работают. Также у проекта довольно плохо с документацией, но мы надеемся, что в скором времени сообщество исправит этот недостаток.


    Перед тем, как мы рассмотрим примеры использования Manticore, стоит "на пальцах" объяснить, что такое символьное выполнение, и в этом нам поможет слайд из презентации "Symbolic Execution for finding bugs" авторства Michael Hicks:


    image
    Рисунок 4. Symbolic Execution for finding bugs. Michael Hicks.

    Символьное выполнение позволяет понять, какой вход нужно подать функции/программе, чтобы оказаться в заданной точке. Грубо говоря, во время символьного выполнения собираются все ограничения (условия) на пути исполнения в заданную точку, из этих ограничений составляется формула, решив которую, мы получим нужный нам ввод.


    Для демонстрации решим задачу "The Lock" конкурса EtherHack, проходившего на конференции Positive Hack Days 8. Участникам не был дан исходный код, а только адрес контракта. Для решения задания нужно было вызвать контракт с верным пин-кодом (его проверка реализована в fallback). Решение предполагало проведение обратной разработки для понимания алгоритма проверки пин-кода.


    Давайте автоматизируем решение этой задачи. Однако, нам всё же придётся взглянуть на дизассемблированный код контракта, чтобы выяснить, что произойдет (или не произойдёт), если участник отправит валидный пин-код. А именно: стоит подметить, что в программе всего одна инструкция sstore, и мы можем смело предположить, что она будет выполнена, если ключ от ларчика подобран верно.


    К сожалению, честно решить задачу не дают баги в Manticore (мы отправили баг-репорты разработчикам, надеемся, в скором времени ошибки исправят), так что придётся выкручиваться — мы переписали контракт в исходный код на Solidity и заменили в нём код возведения в степень последовательными умножениями (из-за баги Мантикора на текущий момент не может эмулировать возведение в степень с символьной базой). Конечно, решать задание во время конкурса таким извращённым способом бессмысленно, но не забывайте, что мы просто демонстрируем инструмент! А наличие исходного кода не упрощает работу, ведь символьное выполнение работает не с ним, а с байткодом.


    Достаточно оправданий и выгораживаний, давайте взглянем на скрипт:


    the lock solver
    from manticore.ethereum import ManticoreEVM
    from manticore.core.smtlib import Operators
    from struct import pack
    
    m = ManticoreEVM()
    m.verbosity(0)
    
    contract_source_code = '''
    contract lock {
    
      uint public unlocked;
    
      function unlock(bytes4 cPincode) payable {
        uint digitPowers = 0;
        uint iPincode = 0;
    
        for (uint i = 0; i < 4; i++) {
            if (cPincode[i] >= 0x30 && cPincode[i] <= 0x39) {
                // manticore can't handle pow with symbolic input at the moment
                uint digit = (uint(cPincode[i]) - 0x30);
                digit *= digit;
                digit *= digit;
                digitPowers +=  digit;
                iPincode += (uint(cPincode[i]) - 0x30) * 10**(3-i);
            } else {
                revert();
            }
        }
    
        if (uint(iPincode) == 0 || uint(iPincode) == 1) {
          revert();
        }
    
        if (digitPowers == uint(iPincode)) {
            unlocked = 0x31337;
        }
      }
    }
    '''
    
    user_account = m.create_account(balance=10**18)
    m.world.set_balance(user_account, 10**18)
    
    contract_account = m.solidity_create_contract(contract_source_code, owner=user_account)
    
    print "[+] Created a contract account 0x%x" % contract_account
    
    print '[+] Sending transaction with symbolic input'
    
    symbolic_data = m.make_symbolic_buffer(36)
    m.transaction(caller=user_account,
                    address=contract_account,
                    data=symbolic_data,
                    value=10**18)
    
    pincodes = None
    
    for state in m.all_states:
        world = state.platform
        for where, what in world.get_storage_items(int(contract_account)):
            _input = Operators.CONCAT(8*4, *world.human_transactions[-1].data[4:8]) # getting 4 bytes after function signature
            print '[+] Storage write 0x%x' % state.solve_one(what)
            pincodes = state.solve_n(_input, 10)
            print '[+] Found %i pincodes:' % len(pincodes)
            for pincode in sorted(pincodes):
                print '    "%s"' % pack('>I', pincode)
            break
    
        if pincodes is not None:
          break

    Запустив его, получим такой вывод:


    [+] Created a contract account 0x1bfa530d5d685155e98cd7d9dd23f7b6a801cfef
    [+] Sending transaction with symbolic input
    [+] Storage write 0x31337
    [+] Found 3 pincodes:
      "1634"
      "8208"
      "9474"

    Разберёмся с происходящим. Для начала взглянем на код контракта. Функция unlock принимает на вход четыре байта, в которых ожидает увидеть текстовое представление четырёхзначного десятичного числа. Подойдут только те пин-коды, сумма разрядов в четвёртой степени которых равняется самому пин-коду, исключая ноль и один. Действительно,

    $1^4 + 6^4 + 3^4 + 4^4 = 1634$


    Если пин-код удовлетворяет этим условиям, в переменную unlocked, расположенную в storage, будет записано число 0x31337.

    Теперь разберёмся со скриптом. Что происходит в первой части, легко понять: создаётся аккаунт пользователя, затем контракт. Запускается транзакция с символьными входными данными. Этим действием будет активировано символьное выполнение, которое займёт пару минут. По его завершению, мы проходимся по всем конечным состояниям, проверяя, было ли что-то записано в storage, и если было, выясняем, с какими входными данными совершалась транзакция. Мы вычленяем из них пин-код, четыре байта после сигнатуры вызываемой функции. Если бы мы решали задачу и не знали, сколько цифр в пин-коде ожидает контракт, то доставали бы не четыре, а все тридцать два байта (дефолтный размер ячейки памяти в EVM). Тогда на экране увидели бы подобные числа:


    0x3136333434343434343434343434343434343434343434343434343434343434
    0x3832303800000000000000000000000000000000000000000000000000000000

    Пользуясь результатами анализа Manticore, мы можем не только решать игрушечные задачки, но и искать игрушечные баги в игрушечных контрактах, просто задавая такие наивные вопросы вроде: "Что нужно отправить контракту, чтобы на нашем адресе оказались все эфирки, которые содержал контракт?"


    Посмотрим на пример с незащищённым кошельком с воркшопа EthCC 2018:


    UnprotectedWallet
    from manticore.ethereum import ManticoreEVM
    from manticore.core.smtlib import solver
    
    m = ManticoreEVM() # initiate the blockchain
    source_code = '''
    contract UnprotectedWallet{
        address public owner;
    
        modifier onlyowner {
            require(msg.sender==owner);
            _;
        }
    
        function UnprotectedWallet() public {
            owner = msg.sender;
        }
    
        // this function should be protected
        function changeOwner(address _newOwner) public {
           owner = _newOwner;
        }
    
        function deposit() payable public { }
    
        function withdraw() onlyowner public {
            msg.sender.transfer(this.balance);
        }
    }
    '''
    
    # Generate the accounts. Creator has 10 ethers; attacker 0 
    creator_account = m.create_account(balance=10*10**18)
    attacker_account = m.create_account(balance=0)
    contract_account = m.solidity_create_contract(source_code, owner=creator_account)
    
    print "Creator account: 0x%x" % creator_account
    print "Attacker account: 0x%x" % attacker_account
    
    # Deposit 1 ether, from the creator
    contract_account.deposit(caller=creator_account, value=10**18)
    
    # Two raw transactions from the attacker
    symbolic_data = m.make_symbolic_buffer(320)
    m.transaction(caller=attacker_account,
                  address=contract_account,
                  data=symbolic_data,
                  value=0)
    
    symbolic_data = m.make_symbolic_buffer(320)
    m.transaction(caller=attacker_account,
                  address=contract_account,
                  data=symbolic_data,
                  value=0)
    
    for state in m.running_states:
        # Check if the attacker can ends with some ether
    
        balance = state.platform.get_balance(attacker_account)
        state.constrain(balance > 1)
    
        if solver.check(state.constraints):
            print "Attacker can steal the ether! see %s"%m.workspace
            m.generate_testcase(state, 'WalletHack')

    Очевидно, разработчики забыли наградить changeOwner атрибутом onlyOwner. Из-за этого атакующий в две транзакции может заполучить средства. Как обычно, всё самое интересное — в конце скрипта. Перечисляя состояния, мы задаём вопрос: "В каком из них у атакующего появились монетки?" А Manticore на него в состоянии ответить. Ответы ищем в папке mcore_XXX в файле WalletHack_00000000.tx:


    image
    Рисунок 5. Результат работы Manticore.

    Если Manticore не забросят, она вырастет в неплохой инструмент для гибкого анализа умных контрактов, но сейчас она ещё не готова к встрече с реальным миром и реальными задачами.


    Echidna (фазинг)


    Echidna — это экспериментальный инструмент, предназначеный для фаззинга смартконтрактов. Однако это не тот фаззинг, о котором вы могли подумать. Для того, чтобы Echidna могла фаззить функции смарт-контракта, вам необходимо специальным образом подготовить свой контракт.


    // До
    contract EchidnaTest {
        function sensitiveFunc(uint num, bool stop) {
            if (num > 5 && !stop) {
                selfdestruct(msg.sender);
            }
        }
    }

    после
    contract EchidnaTest {
        uint num;
        bool stop;
    
        function num_setter(uint _num) public {
            num = _num;
        }
    
        function stop_setter(bool _stop) public {
            stop = _stop;
        }
    
        function echidna_sensitiveFunc() public returns (bool) {
            if (num > 5 && stop) {
                selfdestruct(msg.sender);
            }
            return true;
        }
    }
    

    Другими словами, вы должны "вывернуть" функцию, которую хотите профазить. Echidna будет делать разные последовательности вызовов num_setter и stop_setter, а затем echidna_sensitiveFunc. Также заметьте, что Echidna ожидает (но явно вам не скажет), что ваша функция будет возвращать true, когда все идет хорошо. Таким образом, "неправильное" поведение вы можете пометить возвратом false, revert(), selfdestruct(0) и др.


    p4lex@ubuntu:~/tools/echidna$ echidna-test echid.sol 
    ━━━ echid.sol ━━━
      ✗ "echidna_sensitiveFunc" failed after 23 tests and 51 shrinks.
    
          │ Call sequence: num_setter(6);
          │                stop_setter(false);
    
      ✗ 1 failed.

    Также стоит отметить, что вы не можете задать некие входные данные, которые Echidna будет изменять, поэтому для сложных задач эффективность такого фаззинга оставляет желать лучшего. А вот то, что придется переписывать каждую функцию, которую нужно профаззить, вы наверняка заметили. Справедиливости ради стоит отметить, что если вы умеете писать на Haskell, вы все же можете задавать генераторы входных значений так, чтобы они давали определенный домен из множества возможных значений (например, вы знаете что num должен быть больше пяти).


    В целом, фаззинг может дать свои плоды. Если уязвимость проявляется только после длинной цепочки действий, символьное исполнение потребует больших затрат CPU и памяти в отличие от фаззинга (который, впрочем, обнаружит эту уязвимость только с некоторой вероятностью). Подробнее о подходе, который используется в Echidna, можно почитать в тут.


    SmartCheck


    Весьма эксперементальный инструмент с закрытым исходным кодом, которому повсюду мерещатся reentrancy. Из приятного: можно просто скопипастить свой контракт в форму отправки или указать ссылку на гитхаб. И поскольку используется статический анализ кода, то ваш контракт даже не обязан компилироваться. Выглядит это так:


    Рисунок 6. Интерфейс SmartCheck

    В данном примере обнуление баланса происходит после передачи Ether, что и является сигнатурой для SmartCheck. Однако, использование функций send и transfer само по себе предотвращает reentrancy (мы разбирали это в третьей части), так что на скриншоте выше мы видим false positive.


    Обратная разработка


    Поскольку на данный момент в открытом доступе нет работающих декомпиляторов, то для того, чтобы понять, какую логику реализует тот или иной контракт, придется пореверсить. В целом, надежда на то, что декомпилятор будет, есть. Первые попытки были предприняты в Porosity, а скорее всего решение работающее, но, к сожалению, закрытое есть у ребят из RET2. А пока давайте взглянем, что мы можем сделать, оставшись наедине с байткодом.


    EVMdis


    Старый и самый удобный дизассемблер. Он не может похвастаться красивыми интерфейсами, зато у него есть фишечки, которых нет в других инструментах: во-первых, он сворачивает инструкции, вместо последовательности показывая одно, более читабельное выражение, и во-вторых, для каждого базового блока EVMdis показывает стек на входе в блок, в том числе символизированные переменные. Это значительно ускоряет процесс понимания логики кода, поскольку без этих данных состояние стека пришлось бы восстанавливать с начала функции, ведь EVM — стековая машина.


    Например:


    # Stack: []
    0x33    PUSH(0xFFFFFFFF & CALLDATALOAD(0x0) / 0x100000000000000000000000000000000000000000000000000000000)
    0x34    DUP1
    0x3E    JUMPI(:label5, 0x2F54BF6E == POP())
    ...
    :label36
    # Stack: [@0x420 @0x408 0x0 @0x2A1 @0x282 :label22 @0x33]
    0x43F   POP()
    0x440   POP()
    0x443   PUSH(MLOAD(0x40))
    ...

    По 0x33 достаётся значение из calldata и кладётся на стек. Поскольку это значение неизвестно, оно символизируется. Далее, мы можем наблюдать его в стеке базового блока label36 среди других символьных и конкретных значений (чисел и адресов переходов).


    На наш взгляд, это самый удобный инструмент для обратной разработки контрактов.


    Ethersplay


    Плагин к Binary Ninja, добавляющий поддержку архитектуры EVM. Промежуточные представления (LLIL, и MLIL) ниндзи не будут работать, ведь EVM — это стековая машина, так что читать придётся голые ассемблерные инструкции. Из приятного: у плагина есть словарь с парами { хеш: прототип функции }, так что можно сразу определить многие известные функции (например, реализующие разные интерфейсы стандартов ERC):


    image
    Рисунок 7. Интерфейс Binary Ninjia с плагином Ethersplay

    Также в плагине есть фича, показывающая состояние стека для каждый инструкции:


    image
    Рисунок 8. Состояние стека показываемое Ethersplay

    Но она заменяет все символьные переменные словом Unknown, тогда как EVMdis вместо него пишет адрес, где эта символьная переменная появилась в стеке.


    Ethersplay не прижился в нашем арсенале, но, возможно, его разовьют до стадии, когда он превратится в полезный инструмент.


    Ida-evm


    Процессорный модуль для IDA Pro, позволяющий ей дизассемблировать контракты. Ещё в нём есть словарь с прототипами функций, как в Ethersplay.


    image
    Рисунок 9. Интерфейс IDA с процессорным модулем IDA-EVM

    Однако, этого недостаточно для удобной обратной разработки контрактов, ведь состояние стека всей программы придётся прорабатывать руками. На наш взгляд, внимания этот инструмент не заслуживает, поскольку скорее всего развития не получит, а в текущем виде он бесполезен.


    Тестировaние


    В этом разделе хотелось бы поговорить о том, что реально имеет тенденцию сделать из вашего контракта защищенный контракт, а именно о тестирование. Как показывает практика, в смарт-контракте, который хотя бы на 95% покрыт тестами, вероятность серьезной уязвимости небольшая. В процессе написания тестов, скорее всего, будет дописана масса необходимых проверок, но не стоит обольщаться, еще останутся вещи, которые могут подсказать только опытные аудиторы безопасности или разработчик. Например, найти 0day уязвимости в предметной области, которую реализует контракт, или какие-то другие логические ошибки, либо же подсказать, как сделать определенные механизмы контракта (DApp) эффективнее.


    Truffle


    Удобнее всего проводить тестирование "не отходя от кассы" с помощью Truffle (c ним же и разрабатывается большинство смарт-контрактов). В папке test (появляется после truffle init) вы можете писать тесты и запускать их с помощью команды truffle test. Впрочем, вряд ли кто-то может рассказать лучше про тестирование с truffle, чем официальная документация. Также не забудьте померить покрытие тестами с помощью инструмента solidity-coverage.


    web3.js


    Если по каким-то причинам (а их может быть немало) вас не устраивает Truffle, можно писать тесты, используя web3 библиотеки (они есть под разные языки). Удобнее всего применять web3.js, поскольку написанный код тестов можно будет легко перенести в будущую DApp как часть логики. Единственное предостережение — используйте web3.js версии ^1.0.0. Будущее за ней, и с версией 0.14.0, которая будет у вас первой строкой в гугле, она не совместима. Рассмотрим пример:


    исходник
    pragma solidity ^0.4.23;
    
    contract Testable {
        address public human;
        uint private counter;
    
        constructor() {
            human = tx.origin;
        }
    
        event CallmeLog();
        function callme(uint times) public {
            counter++;
            if (times > 1) {
                callme(--times);
            } else {
                emit CallmeLog();
            }
        }
    }

    Прежде чем перейти к самим тестам, придется написать функцию, которая будет заливать свежий контракт в сеть, благо это нужно сделать только раз :)


    тесты
    const solc = require('solc');
    const fs = require('fs');
    const Web3 = require('web3');
    const web3 = new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:8545'));
    const code = fs.readFileSync('./testable.sol');
    
    async function main() {
        // when use unlocked account from node
        let defaultAccs = await web3.eth.getAccounts();
        web3.eth.defaultAccount = defaultAccs[0];
    
        // this is for known private key
        // web3.eth.accounts.wallet.add(privKey.key);
    
        let Testable = await deploy();
    
        human_test(Testable);
        callme_tx_status_test(Testable, 3);
        callme_event_test(Testable, 3);
    
        // let's got new instance
        Testable = await deploy();
        callme_storage_test(Testable, 5);
    
    }
    
    async function deploy() {
        let output = solc.compile(code.toString(), 1);
        let TestableABI = output.contracts[Object.keys(output.contracts)[0]].interface;
        let TestableBytecode = output.contracts[Object.keys(output.contracts)[0]].bytecode;
    
        let Testable = new web3.eth.Contract(JSON.parse(TestableABI),{
            from: web3.eth.defaultAccount
        });
    
        let gasCount = await Testable.deploy({
            data: TestableBytecode
        }).estimateGas();
    
        Testable = await Testable.deploy({
            data: TestableBytecode
        }).send({
            gas: gasCount
        });
    
        return Testable;
    }
    
    async function human_test(instance) {
        // let's test that contract has owner (human)
        // use "call" for public variables and constant methods
        let humanAddress = await instance.methods.human().call();
        if (humanAddress !== web3.eth.defaultAccount) {
            throw "✕ human address is wrong"
        } else {
            console.log(" human_test passed");
        }
    }
    
    ...

    Под спойлером показан лишь один тест, поскольку тесты (на чем бы они ни писались) занимают обычно много места. С полным примером предлагаем самостоятельно ознакомиться в web3_utilz/Testing.


    Подытожим


    Инструменты, позволяющие искать баги автоматически, могут выявить сейчас лишь мейнстримные уязвимости, которые и так у всех на слуху, но, скорее всего, будут полезны на CTF.


    Что касается инструментов из категории отладки и обратной разработки (reverse engineering), они определенно могут быть полезны в ситуации, когда исходного кода нет. Так, например, они могут помочь в процессе разбора эксплоита для очередной громкой уязвимости или при заимствовании ноу-хау у конкурентов (вспоминается пример с cryptoKittes). Ну и не забываем, что для работы с этими инструментами нужны люди с соотвествующими умениями и квалификациями по reverse engineering. Главными же друзьями разработчика, как всегда, остаются изучение чужих ошибок, тестирование и аудирование своих проектов.


    Большое спасибо, что докрутили ползунок до конца статьи, для вас старались p4lex и Igor1024.

    Digital Security
    136,00
    Безопасность как искусство
    Поделиться публикацией

    Комментарии 1

      +3

      Классно, что ты попробовал решить задание EtherHack не реверс с помощью manticore, символическое исполнение позволяет найти пинкоды без какого-либо ручного анализа. Я пробовал с Mythril, но с ним не получилось.


      Еще добавлю:


      • MAIAN — еще одна реализация символического исполнения EVM, которая позволяет в отличие от Mythril искать уязвимости, для которых требуется больше одной транзакции, а также способная подтверждать уязвимости на приватном блокчейне с целью снижения фолзов.
      • ethereum-graph-debugger — графический отладчик Solidity
      • KEVM — формальная верификация ERC20-контрактов
      • sol-function-profiler — простая утилита для профилирования контрактов

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое