Tx.Origin создает уязвимость в Ethereum

Это была тяжелая неделя для Ethereum. Пока драма с TheDAO продолжает развиваться, несколько дней назад команда Ethereum сообщила об уязвимости кошелька.

В принципе текст сообщения был корректным. Но разработчикам приложений оказалось не так просто понять направление атаки. Давайте разберемся в ситуации.

Tx.Origin создает уязвимость в Ethereum

Определение вызывающего: внутренняя редакция

В Solidity есть стандартный способ определения того, кто вызывает функцию — msg.sender, который используется повсеместно. msg.sender гарантированно возвращает адрес контракта или открытого ключа вызвавшего первую функцию в области действия текущего контракта.

Это звучит несколько сложнее, чем есть на самом деле; давайте рассмотрим ситуацию на примере небольшой цепочки вызовов.

function checkOwner() internal {
if (!(msg.sender == owner)) { throw; }
}
function spendMoney(address destination, uint amount) {
  checkOwner();
  if (!destination.send(amount)) {
    LogFailedSend(destination, amount)
  }
}

Это достаточно защищенный код — единственной открытой точкой доступа является spendMoney. В первую очередь здесь проверяется, что вызывающим является «владелец» контракта. После подтверждения того, что функцию действительно вызывает владелец, она пытается выполнить передачу и в случае неудачи регистрирует ошибку в журнале.

Обратите внимание, что checkOwner вызывается функцией spendMoney. Тем не менее, для нее в качестве msg.sender устанавливается адрес того, кто первоначально взывал функцию, а не адрес контракта функции spendMoney.

Определение вызывающего: внешние вызовы

Рассмотрим другую схему вызова: попробуем обратиться к кошельку пользователя от его имени. Контракт UserWallet — это безопасный кошелек, и наш контракт пытается инициировать передачу.

contract UserWallet {
  function checkOwner() internal {
    if (!(msg.sender == owner)) { throw; }
  }
  function doSpend(address destination, uint amount) {
    checkOwner();
    if (!destination.send(amount)) {
      LogFailedSend(destination, amount)
    }
}
contract OurContract {
   function spendMoney(UserWallet w, address destination, uint amount) {
      w.doSpend(destination, amount);
   }
}

Какое значение имеет msg.sender в вызове UserWallet.checkOwner()? Это адрес контракта OurContract: данный адрес становится msg.sender, заменяя адрес первоначального вызвавшего, как только совершается внешний вызов.

Это достаточно разумно. Иначе можно было бы представить любое количество атакующих кошельков злоумышленников, которые претендовали бы на право получить деньги. Как только кто-нибудь достаточно беспечный передал бы им деньги, они могли бы запустить удаленную атаку на передавшего. Фактически, если бы msg.sender работал как-то иначе, это была бы серьезная дыра в системе безопасности.

Представим, что msg.sender сообщал бы контрактам, кто является первоначальным вызывающим, то есть кто запустил весь процесс. Тогда этот атакующий контракт мог бы вытянуть все деньги из каждого кошелька отправляющего (с учетом ограничений на газ — этот вопрос мы рассмотрим в другой раз).

contract UserWallet {
  function send(address dest, uint amount) {
   if (msg.sender != owner) { throw; }
   dest.send(amount);
  }
}
contract AttackWallet {
  function() {
     UserWallet w = UserWallet(userWalletAddress);
     w.send(thiefStorageAddress, msg.sender.balance);
  }
}

Примечание. В моем первоначальном коде была ошибка, которую любезно исправил redditor crypto-jesus. Эти изменения учтены в примере выше.

Несмотря на то что UserWallet проверяет отправителя, если бы функция msg.sender работала как-то иначе, эта атака могла бы сработать. Единственным способом защиты от этой атаки является обновление msg.sender в соответствии с адресом вызывающего контракта при каждом внешнем вызове. Очевидно, что невыполнение этого правила не позволит получить жизнеспособную инфраструктуру.

Определение вызывающего: точка зрения разработчика

Существует способ узнать «первоначального вызывающего». Это tx.origin. Функция tx.origin проходит по всему стеку вызовов и сообщает, кто первоначально инициировал данный вызов.

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

Что произошло?

Что же произошло с этими старыми кошельками? Они использовали для проверки вызывающего tx.origin, а не msg.sender. Это и есть почва для атаки.

Теперь мы можем расшифровать утверждения из сообщения о нарушении безопасности:

Атака возможна, если затронутый кошелек взаимодействует с вредоносным контрактом.

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

ИЛИ если счет владельца затронутого кошелька взаимодействует с вредоносным контрактом, который знает адрес его кошелька.

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

владелец уязвимого кошелька -> привлекающий атакующий
       контракт атакующего -> отдельный контракт кошелька
          вызывается контракт кошелька, и владелец отображается в tx.origin

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

Оценка уязвимости

Если вы используете tx.origin, НЕМЕДЛЕННО обновите свои контракты. Я буду рад вам помочь — дополнительную информацию см. на странице «Контракт».

Peter Vessenes

Категория: 
Безопасность
10
Средняя: 10 (1 оценка)
0
Ваша оценка: Нет
992 / 0
Аватар пользователя admin
Публикацию добавил: admin
Дата публикации: вт, 07/05/2016 - 10:56

Что еще почитать:

Добавить комментарий