С 12.08.2024 в нашем сервисе доступны все торговые пары на спот рынке ByBit. Если вы заметили ошибку пожалуйста свяжитесь с нами.

Атака на theDAO. Как злоумышленник это сделал?

Хакер атаковавший TheDAO получил около 3.5 млн ether, что в долларах составляет ~ $45.000.000. Что касается самой атаки, то уязвимость состояла в том, что «рекурсивный вызов» был недостаточно защищён.

Начало

Стоило бы начать с того, что механизм splitDAO был с самого начала уязвим. Злоумышленник легко воссоздал дочерние DAO, дело в том, что данная функция отвечает за вывод средств.

Давайте разберем функцию чуть подробнее. Сам смысл функции состоит в том, что держатели токенов TheDAO могут пожелать отделиться из за нежелания мириться с невыгодным предложением.Что же касается этой эпопеи с theDAO, то здесь сыграло роль желание вывести деньги.

Реализовывается это путём создания предложения под названием «разделение». Для этого предложения требуется около недели для голосования и подготовки. Те участники, которые голосовали за разделение, имеют возможность воспользоваться функцией splitDAO.

Далее, создается контракт (дочерний) DAO, при условии его отсутствия, после чего ether пересылаются на childDAO, выводя все накопившиеся средства пользователям.

Путаница

Касательно обзора, к сожалению последняя представленная версия кода на GitHub не имеет ничего общего с реальным кодом theDAO.

О коде

splitDAO имеет довольно длинный код, но рассмотрим интересный момент:

function splitDAO(

uint _proposalID,

address _newCurator

) noEther onlyTokenholders returns (bool _success) {

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

[snip]…

Тут можно отметить строку «передать ether»

// Передать ether и выпустить новые жетоны

uint fundsToBeMoved =

(balances[msg.sender] * p.splitData[0].splitBalance) /

p.splitData[0].totalSupply;

if (p.splitData[0].newDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender) == false)

throw;

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

[snip]

// Burn DAO Tokens

Transfer(msg.sender, 0, balances[msg.sender]);

withdrawRewardFor(msg.sender); // получите свое вознаграждение

totalSupply -= balances[msg.sender];

balances[msg.sender] = 0;

paidOut[msg.sender] = 0;

return true;

}

С самого начала, идея была плохая. Сначала вызывается функция withdrawRewardFor, а потом переменные totalSupply, balances и paidOut определяются.

То есть, мы хотим сказать, что так делать не стоит. В том случае, если на withdrawRewardFor будет произведена атака посредством Race To Empty, то есть имеется возможность вызова функции до обновления данных paidOut.

Взглянем подробнее на withdrawRewardFor.

function withdrawRewardFor(address _account) noEther internal returns (bool _success) {

if ((balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply < paidOut[_account])

throw;

uint reward =

(balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply — paidOut[_account];

if (!rewardAccount.payOut(_account, reward))

throw;

paidOut[_account] += reward;

return true;

}

Выше небольшая функция, приведенная без изменений. Дальше интересней, после произведения вознаграждения, функция преображается. Это можно заметить после вызова функции rewardAccount.payOut. Идём дальше.

rewardAccount – функция представляет из себя контракт «ManagedAccount». После её вызова, нам предоставляется ответ:

function payOut(address _recipient, uint _amount) returns (bool) {

if (msg.sender != owner || msg.value > 0 || (payOwnerOnly && _recipient != owner))

throw;

if (_recipient.call.value(_amount)()) {

PayOut(_recipient, _amount);

return true;

} else {

return false;

}

}

Попробуем воссоздать атаку

1. Создать контракт кошелька, который будет иметь функцию вызова splitDAO, но не нужно вызывать её много раз, иначе вы опустошите DAO, что превысит допустимый лимит callstack или газа..

2. Разделить адрес получателя, который схож с адресом контракта кошелька, который вы создали. Для этого нужно создать новое предложение.

3. Должна пройти одна неделя, чтоб разделение завершилось.

4. Вызвать splitDAO.

Код будет выглядеть таким образом (предполагается, что запрос кошелька будет выполнен только дважды):

После этого код будет иметь такой вид:
(конечно же, при условии, что запрос кошелька выполнен всего 2 раза)

splitDao

withdrawRewardFor

payOut

recipient.call.value()()

splitDao

withdrawRewardFor

payOut

recipient.call.value()()

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

DAO опорочено

Вызовем пару строк функций разделения:

withdrawRewardFor(msg.sender); // получите свое вознаграждение

totalSupply -= balances[msg.sender];

balances[msg.sender] = 0;

paidOut[msg.sender] = 0;

Что мы видим после того, как завершился второй вызов splitDAO:

1. totalSupply становится равным балансу отправителя.
2. Баланс у отправителя становится равен нулю.
3. Нулевым становится баланс и отправителя paidOut .

Теперь посмотрим, что станет основного вызова splitDAO:

1. Параметр totalSupply становится равен балансу отправителя, то есть = 0.
2. Баланс отправителя ещё раз превращается в 0.
3. Параметр paidOut опять ставится в 0.

После проделанной работы, theDao думает, что у нее на 258 монет больше, чем на в действительности. Попробуем повторить запрос несколько раз, теперь, theDAO считает, что имеет на 3.5 миллиона больше моент. На самом деле это всего лишь 1 пример из многих сотен и цифры могут быть разные.

Дочернее DAO разбогатело на $45 миллионов долларов

Посмотрим, что же идёт у нас дальше после этого вызова. А происходит именно то, что дочернее DAO начинает получать деньги. Код:

// передать ether и получить новые жетоны

uint fundsToBeMoved =

(balances[msg.sender] * p.splitData[0].splitBalance) /

p.splitData[0].totalSupply;

if (p.splitData[0].newDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender) == false)

throw;

Жетоны переходят в newDAO, но это никак не отражается на соотношении ether и жетонов. Если коротко, то жетоны появляются в subDAO и ManagedAccount, который связан с msg.sender. Он пересылает ether используя функцию fundsToBeMoved. Злоумышленник сумел воспользоваться этой уязвимостью и увеличил средства в 30 раз.

30 * 258!= 3500000, ошибка

С другой стороны атака, которая была произведена 30 раз должна была принести примерно 7500 ether. Как злоумышленник сумел получить такое большое количество ?

Тим Годдард смог найти страшную ошибку в коде.

Обратимся к строке withdrawRewardFor. Код приведен без изменений, etherscan это подтверждает.

// Burn DAO Tokens

Transfer(msg.sender, 0, balances[msg.sender]);

withdrawRewardFor(msg.sender); // получите свое вознаграждение

Типичный для языка solidity контракт. Я предположил, что его задачей является передача средств. И действительно, есть такая строка:

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

event Transfer(address indexed _from, address indexed _to, uint256 _amount);

Пока всё ок и вопросов нет. Но загвоздка вот где: на каком этапе, каким образом и где сгорали жетоны DAO? Как оказалось, разработчики сделали две функции transfer: первая начинается с маленькой t, вторая с заглавной T. Рассмотрим функцию с t:

function transfer(address _to, uint256 _amount).

(bool success) {

if (balances[msg.sender] >= _amount && _amount > 0) {

balances[msg.sender] -= _amount;

balances[_to] += _amount;

Transfer(msg.sender, _to, _amount);

return true;

} else {

return false;

}

}

Функция уменьшает баланс пользователя, до того, как воспользуются уязвимостью. Вместо создания логов, получаем:

if (!transfer(0 , balances[msg.sender])) { throw; }

У пользователя число жетонов сокращается, а вот работа рекурсивного вызова становится хуже.

Каким образом злоумышленник смог вызвать функцию большое количество раз?

Чаще всего рекурсивный вызов получается выполнить только один раз; баланс в котором нуждается пользователь ставится в 0. Не смотря на это, хакер сумел повторить этот цикл раз 50, как минимум. Как же так?

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

Злоумышленник, далее, шлёт жетоны назад и запускает цикл снова.

Извлечем уроки

Результатом данного инцидента стал неудачный программинг.

Что следует сделать:

1. Нужен язык, который имеет богатую базу системы типов, как мы видим, его пока что нет.

2. Вызовы, которые пытаются связаться с непонятными адресами — обязательно должны быть заблокированы.

3. Следует проверять баланс перед отправкой средств. Это важно.

4. Желательно сохранять логи событий.

5. Обязательно нужно реализовать систему, которая будет отслеживать статус каждого разделителя. Это напрямую относится к функции splitDAO.

Источник: Coinspot

Дата публикации: 09.07.2016
Оставить комментарии

Здесь еще не было комментариев.

Pro banner