С этой статьи мы начинаем цикл, посвященный типичным уязвимостям, атакам и проблемным местам, присущим смарт-контрактам на языке Solidity, и платформе Ethereum в целом. В первой части мы поговорим вот о чем:


  • почему сложно реализовать децентрализованную биржу на смарт-контрактах
  • как сгенерировать случайное число
  • как вывести из строя всю Proof-of-Authority сеть

Почему сложно реализовать децентрализованную биржу на смарт-контрактах


Disclaimer: О front-running attack мы уже рассказывали в разборе конкурса для ZeroNights 2017. Поэтому те, кто читал, могут сразу переходить к следующему пункту.


Термин front-running появился уже давно, и означает возможность манипуляции на рынке за счет обладания закрытой информацией о транзакциях в состоянии ожидания (pending). Если мошенник в курсе того, что грядет большая закупка, он может быстро скупить предмет торга по дешевке, таким образом гарантируя себе выгоду.


В криптовалютах, и в Ethereum в частности, все транзакции сначала помещаются в пул неподтвержденных (pending pool или mempool или backlog), где ожидают, пока майнер возьмет их оттуда и добавит в блок. Однако, в отличие от классических бирж, где такая информация доступна очень узкому кругу лиц, pending pool в Ethereum могут видеть все участники сети. И, поскольку среднее время, через которое новая транзакция попадет в блок, составляет ~14 секунд, у атакующего достаточно времени, чтобы проанализировать поведение рынка и послать собственную транзакцию, учитывающую это поведение. Последнее — как атакующий может гарантировать, что его транзакция будет обработана в первую очередь? Ответ кроется в размере комиссии за транзакцию — чем она больше, тем быстрее транзакция попадет в блок. Однако, в Ethereum понятие комиссии немного сложнее, чем обычно, и высчитывается следующим образом:

$fee = gasPrice * gas$


где gas — это некая единица топлива для EVM, которая расходуется при исполнении смарт-контракта и сохранении данных в блокчейн. Таким образом, на самом деле атакующий может, манипулируя ценой за единцу газа (gasPrice), добиться того, чтобы для биржи его транзакция была первой. Тут и тут есть пример проведения такой атаки против смарт-контракта.

В качестве mitigation от этой атаки смарт-контракт может анализировать свойство транзакции tx.gasprice. Однако, это не будет полным решением проблемы, поскольку майнер на самом деле не обязан сортировать транзакции по убыванию gasPrice — это лишь экономический стимул. Если вдруг появится вариант более значительного стабильного выигрыша, кто знает, чем займется майнер :)


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


Как сгенерировать случайное число


Ethereum по своему дизайну — детерминированный механизм, поэтому внутри него очень сложно где-либо черпать энтропию. Однако не все разработчики на Solidity являются искушенными во внутреннем устройстве, а видят перед собой лишь описание синтаксиса языка, и пытаются применить его так, как они это делали в других языках программирования.
Итак, откуда нельзя брать энтропию:



Исключение составляет функция block.blockhash(uint blockNumber), которая возвращает хеш блока по его номеру. Однако, применять ее нужно, держа в уме две вещи:


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

Примеры неправильного использования blockhash и эксплоиты к ним можно посмотреть тут и тут.


Другой вариант — схема commit-reveal, которую использует RANDAO. В первой фазе в течение M блоков N участников загадывают случайное число и отправляют смарт-контракту хеш от него с некоторым депозитом. Во второй фазе участники посылают свои загаданные числа смарт-контракту, а тот проверяет число, взяв от него хеш. После того, как все отправили числа, контракт использует их как seed для PRNG. Если участник в заданное время не присылает свое число, он лишается того депозита, который внес, а раунд отменяется (остальные получают свой депозит назад). Недостаток схемы очевиден — она подвержена DOS, поэтому если случайные числа нужны постоянно и незамедлительно, такая схема в чистом виде вряд ли подойдет. Стоит присмотреться к дополнительным правилам для этой схемы, как предлагают сами RANDAO или придумать свою на ее основе, как Виталик. А можно объединить идею с хешом блока и схемой commit-reveal.


Еще один вариант, заслуживающий внимания — Signidice. Схема хороша, когда участников немного, поэтому мы рассмотрим ее на примере игры в рулетку. Итак, есть два участника — казино и игрок, а так же смарт-контракт, который реализует логику игры. На подготовительном этапе казино генерирует пару приватный-открытый ключ и отправляет публичный смарт-контракт. На этом подготовка окончена, можно играть. Поехали:


  • игрок делает новую ставку — присылает загаданное число и некий депозит
  • казино берет загаданное число у смарт-контракта, подписывает с помощью приватного ключа, сгенерированного на подготавительном этапе, и присылает подпись назад
  • смарт-контракт проверяет, что подпись валидна — подписывалось именно то число и именно с помощью того приватного ключа, публичная пара которого известна смарт-контракту
  • если все проверки пройдены, сама подпись используется в качестве seed для PRNG. А он, в свою очередь, дает ту самую цифру, "на которой остановился шарик".

Самый большой недостаток, который останавливает от того, чтобы брать и применять схему прямо сейчас, — это то, что алгоритм подписи у Ethereum — ECDSA. И если использовать его, то у казино всегда будет возможность читерить. Согласну алгоритму, на третьем шаге выбирается случайное k. Этот параметр напрямую влияет на итоговую подпись, причем нельзя использовать одно и то же k, иначе, имея две подписи, можно будет восстановить приватный ключ казино (раскрывать k нельзя по той же причине). Поэтому казино может менять k до тех пор, пока не получит такую подпись, с которой оно выиграет. Вот тут есть пример такого казино.
К тому же (отбросим предыдущую проблему), необходимо решить вопрос с тем, как быть, если участник загадает то же самое число. При условии, что казино использует те же параметры для подписи (в том числе, k), она будет идентична предыдущей, а значит, не случайна. Поэтому нужен запрет на переиспользование загадываемого числа или генерация новой пары ключей каждый раунд.


"Светом в конце тоннеля" для Signidice является EIP-198 о добавлении операции взятие по модулю. Это делает возможным реализацию проверки подписи для RSA. При использовании RSA читерить казино не удастся.


На самом деле, подходов к решению задачи получения случайных чисел много, за бортом остались варианты с получением энтропии off-chain (Oraclize) и эксперименты с whisper.


Как вывести из строя Proof-of-Authority сеть


В этом блоке мы не будем касаться смарт-контрактов, однако рассмотрим одну особенность permissioned blockchain — валидаторы всегда известны. Как пример, сеть с Proof-of-Authority консенсусом. Proof-of-Authority — это частный случай сети с Proof-of-work консенсусом, только майнить могут лишь избранные ноды (валидаторы). Яркий пример таких сетей — это тестовые сети для разработчиков Kovan и Rinkbey. Придуман такой консенсус главным образом для того, чтобы избежать спам атак. Когда злонамеренный майнер, имея преимущество в мощности (за счет использования GPU), добывает новые блоки быстрее остальной сети, он консолидирует в своих руках большое количество ether. В то время как обычные участники сети не могут больше добывать ether самостоятельно — они осушают "краны", которые ранее пополнялись добросовестными майнерами. Все это приводит к тому, что новые разработчкики не могут пользоваться сетью из-за отсутсвия ether. Для тех, у кого эфир все же есть, нормальная работа в такой сети тоже не представляется возможной. Майнер способен замусорить сеть бессмысленными, но дорогими транзакциями, что, в свою очередь, сделает добавление в блок транзакций нормальных участников сети затруднительным и долгим.


Так вот, Proof-of-Authority подход с избранными валидаторами может применяться не только для тестовых сетей, но так же для permissioned-сетей. Например PoA network — публично доступная сеть, в которой валидаторами являются юридически закрепленные участники. Избавляет ли PoA эти сети от возможности проведения DOS атак? И да, и нет.


На уровне сети интернет все еще есть возможность сгенерировать большой объем трафика на порт 30303, на котором слушает майнер. И тем самым сделать его недоступным для остальной сети. Далее очевидно — нет валидатора, никто не добывает блоки, транзакции копятся, сеть стоит. Но как же вычислить майнера в сети? Ведь он использует точно такой же клиент для сети, как и остальные.


Алгоритм на самом деле прост:


  • подключаемся к сети тем же самым клиентом (в данном примере Parity)
  • получаем подключенных к нам участников сети через RPC интерфейс:

curl --data '{"method":"parity_netPeers","params":[],"id":1,"jsonrpc":"2.0"}' -H "Content-Type: application/json" -X POST localhost:8545 -s | jq '.result.peers[]' | jq '.network.remoteAddress' | cut -d "\"" -f 2 | cut -d ":" -f 1

Если их более 25, то придется принудительно рвать соединения до известных IP, например, с помощью iptables, и так собрать их все.


Далее все, что нужно, — это отслеживать, с каких IP первыми приходят новые блоки. Эти IP и будут майнеры.


На этом пока что все. В следующей части перейдем непосредственно к проблемам, которые могут встретиться в смарт-контрактах.

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


  1. Raz0r
    09.01.2018 14:02

    Спасибо за статью, ждем последующие части. К теме front-running в биржах можно добавить хороший пример такой проблемы в Bancor: https://hackernoon.com/front-running-bancor-in-150-lines-of-python-with-ethereum-api-d5e2bfd0d798.


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


    1. p4lex Автор
      09.01.2018 16:08

      Да, про bancor статья хорошая. Я указал ее в статье :) Видимо стоит более явно давать ссылки. Учту)

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

      Похоже на то, хорошего решения нет в одну транзакцию вообще нет. Я думаю, максимум что можно достигнуть — в один блок.


    1. therealal
      10.01.2018 13:05

      А вот хорошая идея защиты от front running: hackingdistributed.com/2017/08/28/submarine-sends


      1. Tsvetik
        10.01.2018 13:18

        От front-running защититься очень просто. Достаточно запускать свои транзакции через контракт-прокси.


        1. therealal
          10.01.2018 13:31

          А поподробнее? Не забываем, что я могу увидеть конечный эффект любой незамайненой транзакции, применив ее к текущему состоянию БЧ, как бы глубоко обфусцирована она ни была.


          1. Tsvetik
            10.01.2018 13:46

            Согласен. Если прогнать в симуляторе все транзакции из txpool, то да, можно найти искомый CALL и его «опередить».


  1. Tsvetik
    09.01.2018 17:10

    Вместо curl раз в 10 быстрее и безопаснее использовать чтение и запись в пайпы geth.ipc или parity.ipc


    1. p4lex Автор
      09.01.2018 18:30

      Это же пример просто :)

      Но все же. Чем ipc безопасне rpc в этих условиях?


      1. Tsvetik
        09.01.2018 18:59

        К ipc возможен доступ только с локальной машины, а rpc открыт для всех. Если на rpc включены api для отправки транзакций, то ими может воспользоваться любой желающий.
        Либо можно заддосить ноду тяжелыми запросами по RPC, например, из разряда debug_*, либо eth_call, eth_compile


        1. p4lex Автор
          09.01.2018 19:20

          а rpc открыт для всех

          Это же как запустишь, можно на localhost. Я так и делалаю всегда…


          1. fzzr
            10.01.2018 16:35

            Пример для Geth: geth --rpc --rpcaddr "0.0.0.0". Не дефолтное поведение, но при желании можно выстрелить себе куда-нибудь. Удобно и необходимо для shared dev-nodes.


  1. batyrmastyr
    10.01.2018 09:23

    «Front-running» (к слову сказать, он всё же без дефиса) уже давно переводится и как «опережающая сделка», и как «сделка на опережение», и даже как «игра на опережение». Выбирайте любой.


  1. therealal
    10.01.2018 13:03

    Если их более 25, то придется принудительно рвать соединения до известных IP, например, с помощью iptables, и так собрать их все.

    А сколько их? Например, у меня постоянно запущена нода rinkeby. Есть подозрение, что порядок величины — десятки тысяч и более. Трудоемкая задача получается.
    Как вариант защиты — майнеру отправлять блоки с write-only ноды. Тогда ддосать придется ip целиком.


    1. p4lex Автор
      10.01.2018 16:33

      Есть подозрение, что порядок величины — десятки тысяч и более

      Зачем тясячи то? десятка за глаза

      майнеру отправлять блоки с write-only ноды. Тогда ддосать придется ip целиком

      вот это вообще не понял


      1. therealal
        10.01.2018 17:52

        Зачем тясячи то? десятка за глаза

        Насколько понимаю, Вами предлагается найти всех майнеров, перебрав участников сети. Подозреваю, участников сети десятки тысяч и более — трудно будет искать майнеров. Или можно определить их ip, не перебирая всех участников?

        майнеру отправлять блоки с write-only ноды

        Уточню: майнер может «читать» сеть с одного ip, а посылать новый блок с другого, на котором он вообще не слушает никакие порты.


        1. p4lex Автор
          10.01.2018 22:32

          Подозреваю, участников сети десятки тысяч и более — трудно будет искать майнеров

          Да, это не так понял сначала. Действительно участников может быть много. Но тут 2 фактора:
          • 25 это софтварное ограничение клиентов сети (geth, parity). Его можно естественно повысить до тех что позволяет ОС и железо (как раз порядки тысяч)
          • В сетях чуть более закрытых чем rinkbey (типо PoA network) учасников будет еще меньше (может и меньше 25)

          Уточню: майнер может «читать» сеть с одного ip, а посылать новый блок с другого, на котором он вообще не слушает никакие порты.

          Хорошая мысль, согласен.


  1. rstormsf
    10.01.2018 16:33

    p4lex а как вы поймете от какого IP пришел блок?


    1. p4lex Автор
      10.01.2018 16:34

      В ванильной сборке такой функциональности нет. Можно подхачить немного в месте где валидируется блок и выводить на консоль не только его номер но и ip с коротого пришел…