Недавно, просматривая опубликованные смарт контракты в сети Эфириум, наткнулся на один интересный контракт с уязвимостью внутри. С первого взгляда, разработчик ошибся в коде и вы можете получить деньги контракта, но если внимательно проанализировать логику контракта, все выглядит совершенно иначе.

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

pragma solidity ^0.4.19;

contract NEW_YEARS_GIFT {
    string message;
    bool passHasBeenSet = false;
    address sender;
    bytes32 public hashPass;

    function () public payable {}

    function GetHash(bytes pass) public constant returns(bytes32) {
        return sha3(pass);
    }

    function SetPass(bytes32 hash)  public payable {
        if ((!passHasBeenSet && (msg.value > 1 ether)) || hashPass == 0x0) {
            hashPass = hash;
            sender = msg.sender;
        }
    }

    function SetMessage(string _message) public {
        if (msg.sender == sender) {
            message = _message;
        }
    }

    function GetGift(bytes pass) external payable returns(string) {
        if (hashPass == sha3(pass)) {
            msg.sender.transfer(this.balance);
            return message;
        }
    }

    function Revoce() public payable {
        if (msg.sender == sender) {
            sender.transfer(this.balance);
            message = "";
        }
    }

    function PassHasBeenSet(bytes32 hash) public {
        if (msg.sender == sender && hash == hashPass) {
            passHasBeenSet = true;
        }
    }
}

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

  1. Вы кладете деньги на контракт, с помощью метода SetPass, при этом задавая хэш SHA-3 вашего пароля, который доступен получателю (как романтично).
  2. Вы отправляете сообщение получателю с помощью метода SetMessage
  3. Также можете отказаться от подарка методом Revoce
  4. А получатель получает деньги и сообщением методом GetGift

Ну не красота ли? Плюс эту картину дополняют три транзакции:



Первые две из них, это:

  1. Публикация контракта
  2. Вызов функции SetPass с неким хэшем и пополнением баланса контракта на 1 Эфир.

Заметим, что только одна функция была вызвана.

Третья транзакция это «неудачная» попытка взломать контракт с вызовом метода GetGift и случайным набором данных.

А теперь собственно ловушка:

Давайте внимательно рассмотрим проверку в методе SetPass:

function SetPass(bytes32 hash)  public payable {
        if ((!passHasBeenSet && (msg.value > 1 ether)) || hashPass == 0x0) {
            hashPass = hash;
            sender = msg.sender;
        }
    }

Как видите, она основана на переменной passHasBeenSet, которую необходимо устанавливать отдельным одноименным методом PassHasBeenSet. Этот метод вызван не был, следовательно переменная до сих пор имеет значение false. Причем, данный метод может вызвать только тот, кто сначала вызвал метод SetPass.

То есть, теоретически, любой кто вызовет метод SetPass, с пополнением баланса больше чем 1 Эфир, станет sender'ом. Причем, чтобы уже никто другой не стал им, нужно просто сразу вызвать метод PassHasBeenSet или просто один из методов Revoce/GetGift для вывода денег.

И вроде бы все логично — просто вызываешь эти два метода и 1 Эфир твой. Но, как мы знаем, в Эфириуме есть атака опережением (front-running). Смысл ее в следующем: злоумышленник наблюдает пул ожидающих транзакций и ждёт вашу транзакцию. Как только транзакция, связанная с контрактом, появляется в пуле транзакций, злоумышленник выполняет транзакцию с большей ценой газа. Транзакция злоумышленника пришла последней в текущем раунде, но благодаря наивысшей цене газа она в реальности будет исполнена раньше, чем ваша транзакция.

Как думаете, какой метод исполнит злоумышленник? Конечно же PassHasBeenSet.
Это даст ему возможность избежать смены sender'а и кроме того, все ваши деньги, отосланные методом SetPass, благополучно осядут в контракте. Ну а дальше он просто их выведет.

Будьте внимательны при работе с блокчейнами!

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


  1. TrllServ
    15.02.2018 17:19

    Правильно ли я понимаю, что для этой атаки он(а) теперь сидит онлайн 24/7 и ждет жертву?
    Просто кажется тут какая то незавершенность.


    1. AgentRX Автор
      15.02.2018 17:21

      Достаточно для этих целей написать робота и спать спокойно.
      PS. Более подробно данная атака разбирается здесь: blog.positive.com/zeronights-ico-hacking-contest-writeup-63afb996f1e3


  1. Tsvetik
    15.02.2018 21:36
    +11

    Тут все намного проще.
    Никто не сидит 24/7.
    Владелец просто уже вызвал PassHasBeenSet через контракт-прокси. Эта транзакция на etherscan не отображается.
    Значение переменной passHasBeenSet легко проверяется через web3.eth_getStorageAt()

    Пару недель назад на etherscan просмотрщик исходного кода был без wordwrap, и поэтому было множество контрактов-ловушек, где часть кода пряталась за горизонтальной полосой прокрутки.
    После настойчивых обращений разработчики включили wordwrap.

    У меня есть большая коллекция ловушек. Некоторые — просто шедевры.


    1. catanfa
      15.02.2018 23:20
      +13

      ловушки в студию, пожалуйста


      1. Tsvetik
        16.02.2018 12:35

        Возможно, я когда-нибудь о них расскажу, но сегодня мне лень.


    1. Psychosynthesis
      16.02.2018 03:54
      +5

      А запилите-ка нам статейку.


  1. decomeron
    16.02.2018 01:54

    Думаю, прежде чем что то кому то начать, нужно все проверить на хабре, а так ли я делаю. И только потом уже в дело пускать


  1. ktod
    16.02.2018 11:00

    Интересно, а если после появления транзы злоумышленника докинуть газа на свою транзу? Предусмотрел ли он такой вариант? =)


    1. vibornoff
      16.02.2018 12:07

      Есть еще вариант — самому смайнить блок с транзакцией окешивания ловушки, не принимая в него транзакцию злоумышленника. Для этого транзакцию окешивания даже в сеть передавать не надо.


      1. ktod
        16.02.2018 13:53

        Да да да. Я тоже об этом подумал.