Attention! S in Ethereum stands for Security. Part 1. Blockchain things


    С этой статьи мы начинаем цикл, посвященный типичным уязвимостям, атакам и проблемным местам, присущим смарт-контрактам на языке 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 и будут майнеры.


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

    Digital Security
    188.04
    Безопасность как искусство
    Share post

    Similar posts

    Comments 21

      +2

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


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

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

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

        Похоже на то, хорошего решения нет в одну транзакцию вообще нет. Я думаю, максимум что можно достигнуть — в один блок.
          0
          А вот хорошая идея защиты от front running: hackingdistributed.com/2017/08/28/submarine-sends
            0
            От front-running защититься очень просто. Достаточно запускать свои транзакции через контракт-прокси.
              +1
              А поподробнее? Не забываем, что я могу увидеть конечный эффект любой незамайненой транзакции, применив ее к текущему состоянию БЧ, как бы глубоко обфусцирована она ни была.
                0
                Согласен. Если прогнать в симуляторе все транзакции из txpool, то да, можно найти искомый CALL и его «опередить».
          0
          Вместо curl раз в 10 быстрее и безопаснее использовать чтение и запись в пайпы geth.ipc или parity.ipc
            0
            Это же пример просто :)

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

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

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

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

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

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

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

                вот это вообще не понял
                  0
                  Зачем тясячи то? десятка за глаза

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

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

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

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

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

                    Хорошая мысль, согласен.
                0
                p4lex а как вы поймете от какого IP пришел блок?
                  0
                  В ванильной сборке такой функциональности нет. Можно подхачить немного в месте где валидируется блок и выводить на консоль не только его номер но и ip с коротого пришел…
                  0
                  ethereum.stackexchange.com/questions/34988/honest-lottery-winner-generation-pseudo-random-number-obtaining

                  Вот тут предлагал вариант для розыгрыша в лотерею. Относительно безопасного.

                  Комбинация использования blockhash с идеями commit-reveal.
                  1. На старте организатор задает некое число — ключ которое будет использоваться при розыгрыше. И выдает его хэш (само число до розагрыша закрыто).
                  2. Для всех покупателей билетов берется blockhash и сохраняется
                  3. При розыгрыше число выдается организатором и складывается с числами покупателей билетов.
                  4. результат используется при определении blockhash по которому вычисляется победитель.

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

                  Либо просто купить билет.

                  Те кто закидывал свое число выдают депозит. При открытии числа депозит возвращается. Билет для них стоит чуть дешевле, чтобы стимулировать выдачу. Если проверяльщик не открывает свое число депозит присоединятеся к розыгрышу из оставшихся. Если организатор не открывает число всем возвращаются деньги.
                    0
                    Там же в ответе указан способ как организатор может всех обмануть
                      0
                      Нет. После указания способа было добавлено в описание алгоритма — возможность каждого покупателя билета заложить key number. Это не даст организатору читить т.к. он не знает этого числа.

                  Only users with full accounts can post comments. Log in, please.