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

      bengalaQ

    • Más
    • Email
    • Twitter
    • Github

Capture The Ether - Parte 1

15 May 2022

Tiempo de lectura ~7 minutos


Antes de comenzar, quería aprovechar este breve párrafo para aclarar un poco la filosofía de este blog. Pese a estar indefectiblemente enseñando la solución a los problemas, NO ES LO IDEAL EN ABSOLUTO recurrir a esta explicación sin antes intentarlo por cuenta propia. No importa cuánto demores, muchos de estos desafíos enseñan, más allá del conocimiento, a ser pacientes con uno mismo y adquirir esa habilidad de levantarse y seguir intentando. Por ello, la metodología de Lectura-Ideas-Test-Explicación-Conclusión se enseñará solo dando click a los botones de "Revelar", con lo que vas a poder obtener pistas de una forma más atómica. Dicho esto, si no entendiste el problema del todo o querés ver cómo lo resolvió otra persona, ¡Bienvenide! Estás en el lugar que soñabas 😎.

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

1. Guess the number
2. Guess the secret number
3. Guess the random number

1. Guess the number

Problema:

"Estoy pensando en un número. Todo lo que tienes que hacer es adivinarlo."

El contrato que atacaremos será el siguiente

pragma solidity ^0.4.21;

contract GuessTheNumberChallenge {
    uint8 answer = 42;

    function GuessTheNumberChallenge() 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);

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

Datos de lectura:

  • El número 42 aparece a simple vista.
  • La función guess compara un número que le pasemos con el que posea la variable answer.

Ideas para soluciones:

  • Llamar a la función guess con el valor 42, el cual es el almacenado en la variable answer.

Explicación:

El número que debemos adivinar está practicamente frente a nuestras narices. No deberían subestimarnos tanto, ¿No creés?

Test a ejecutar:

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

let lotteryContract: Contract;

beforeEach(async () => {
  const lotteryFactory = await ethers.getContractFactory(
    "GuessTheNumberChallenge"
  );
  lotteryContract = lotteryFactory.attach(
    "DIRECCION_DEL_CHALLENGE"
  );
});

describe("Guess The Number", async () => {
  it("Resuelve el Lottery challenge - Guess The Number", async () => {
    const tx = await lotteryContract.guess(42, {
      value: ethers.utils.parseEther("1"),
      gasLimit: 1e5,
    });
    const txHash = await tx.hash;
    console.log(`El Hash de la transaccion es ${txHash}`);

    expect(txHash).not.to.be.undefined;
  });
});

Conclusión:

Leer código es SÚPER importante. Así, muchas veces encontraremos cosas que los desarrolladores olvidaron borrar o simplemente pensaron que no habría problema alguno en dejarlo ahí, a nuestro alcance. Conoce a tu enemigo y esas cosas, vos me entendés…


2. Guess the secret number

Problema:

"Esta vez solo guardé el hash del número. Buena suerte reverseando el hash criptográfico!"

El contrato que atacaremos será el siguiente

pragma solidity ^0.4.21;

contract GuessTheSecretNumberChallenge {
    bytes32 answerHash = 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365;

    function GuessTheSecretNumberChallenge() 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);

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

Datos de lectura:

  • El hash criptográfico (answerHash) es 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365.
  • La función guess en esta ocasión compara el keccak256 (vaya uno a saber qué es eso) de un número que le pasemos, con el answerHash.

Ideas para soluciones:

  • Averiguar qué es keccak256.
  • Encontrar un "n" que pasado por parámetro a ese keccak256 raro, logre generar algo igual al answerHash.

Explicación:

Keccak256

Keccak256 es una función hash. Las funciones hash, son funciones que toman una entrada, y generan un resultado de tal forma que la probabilidad de poder crear ese mismo resultado con otra entrada distinta, sea muy (muy, muy, muy, extremadamente muy) baja. En caso que ésta función hash pueda predefinir su conjunto de entrada, la misma es llamada "función hash perfecta", o lo que matemáticamente se le conoce como función inyectiva (al valor 1 solo le pertenece el valor D, tal y como muestra la imagen).
Con esto nos va a alcanzar. Quedan muchas interrogantes sobre esta función (obviamente, no esperabas que semejante belleza terminara de entenderse en 5 renglones, ¿O sí?), pero las veremos más a futuro. De momento, estamos sobrados.

Encontrar "n"

Sabemos que es un número por el tipo de dato que debe recibir "guess" (uint8), un número entero positivo que se pueda formar con 8 bits.

Test a ejecutar:

import { expect } from "chai";
import { ethers } from "hardhat";
import { Contract } from "ethers";
import internal from "stream";

let guessTheSecretNumberContract: Contract;
let secretNumber;
let coincidencia: boolean;
let answerHash = "0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365";

function averiguarValorPorKeccak() {
  for (let numero = 0; !coincidencia; numero++) {
    let numeroKeccakeado = ethers.utils.keccak256([numero]);
    if (numeroKeccakeado == answerHash) {
      coincidencia = true;
      console.log(`MATCH!! [✔] ---------------------> Numero: ${numero}`);
      return numero;
    } else {
      console.log(`Intento fallido [X] ---------------------> Numero: ${numero}`);
    }
  }
}

beforeEach(async () => {
  coincidencia = false;
  const guessTheSecretNumberFactory = await ethers.getContractFactory(
    "GuessTheSecretNumberChallenge"
  );
  guessTheSecretNumberContract = guessTheSecretNumberFactory.attach(
    "DIRECCION_DEL_CHALLENGE"
  );
});

describe("Guess The Secret Number", async () => {
  it("Resuelve el Lottery challenge - Guess The Secret Number", async () => {
    secretNumber = averiguarValorPorKeccak();
    console.log(`El numero encontrado por la función es: ${secretNumber}`);
    const tx = await guessTheSecretNumberContract.guess(secretNumber, {
      value: ethers.utils.parseEther("1"),
      gasLimit: 1e5,
    });
    expect(tx.hash).not.to.be.undefined;
  });
});

Conclusión:

Siempre es interesante recordar que un atacante dispone de 2 cosas: tiempo y recursos infinitos. Pretender que una entrada “n” es imposible de hallar, cuando se comparte públicamente en la blockchain la lógica que aplicamos, es subestimar esos 2 elementos mencionados anteriormente.


3. Guess the random number

Problema:

"Esta vez el número es generado basándose en un par de fuentes bastante aleatorias."

El contrato que atacaremos será el siguiente

pragma solidity ^0.4.21;

contract GuessTheRandomNumberChallenge {
    uint8 answer;

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

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

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

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

Datos de lectura:

  • La variable answer puede valer entre 0 y 255 (por ser de tipo uint8).
  • Se utiliza nuevamente la función keccak.

Ideas para soluciones:

  • Si solo existen 256 valores posibles para la variable answer, ¿Podríamos probar una por una?.
  • Si todo código y estado de un contrato es público en la blockchain, debe existir alguna forma de verlo, ¿No?.

Explicación:

Intentos uno por uno de los 256 valores

Si revisamos la función guess veremos que inicialmente se necesita 1 ether para invocarla, lo cual resulta un inconveniente (en el peor de los casos gastaremos 256 ether). Además, pueden existir herramientas de monitoreo que detecten nuestros numerosos intentos.

Buscar valor público de answer

Toda la información en la blockchain, aún las variables privadas de un Smart Contract, son públicas. Tal vez no son posibles de leer desde un contrato, pero con librerías como ethers js o Web3 sí es posible, tal y como se demuestra en este ejercicio. Lo importante a entender acá es que el llamado storage, lugar donde se almacena el estado del contrato, es público y podemos acceder a él tanto mediante código, como a través de un exploradores de bloques como etherscan.

Test a ejecutar:

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

let randomNumberContract: Contract;
const contractAddress: string = "DIRECCION_DEL_CHALLENGE";

beforeEach(async()=>{
  const randomNumberFactory = await ethers.getContractFactory("GuessTheRandomNumberChallenge");
  randomNumberContract = randomNumberFactory.attach(contractAddress);
});

describe("Guess The Random Number", async ()=>{
  it("Resuelve el Lottery challenge - Guess The Random Number", async ()=>{
    const randomNumber:BigNumber = BigNumber.from(await randomNumberContract.provider.getStorageAt(contractAddress,0));
    console.log(`El numero random buscado es: ${randomNumber}`);

    const tx = await randomNumberContract.guess(randomNumber,{
      value: ethers.utils.parseEther("1"),
      gasLimit: 1e5
    });
    expect(tx.hash).not.to.be.undefined;
    const isComplete:boolean = await randomNumberContract.isComplete();
    console.log(`El valor de isComplete es: ${isComplete}`);
    
    expect(isComplete).to.be.true;
  })
})
Sin embargo, no quiero dejar de remarcar la posibilidad de encontrar el answer simplemente observando la transacción en etherscan:

Imagen 1) Buscamos el address donde se deployó el contrato -> Internal Txns -> Elegimos la transacción con la que se creó.

Imagen 2) State -> Buscamos el cambio de estado de 0 ETH a 1 ETH, que sería cuando se creó el contrato y se modificó el state (variable answer) -> Observamos el storage y al hexadecimal del campo "After" le indicamos desde el desplegable que queremos verlo en formato "Number" -> Observamos el valor oculto y realizamos la llamada guess con el parámetro correspondiente.

Conclusión:

Existen 3 tipos de visibilidad para las variables: public, internal y private. Sin embargo, cuando hablamos de esta visibilidad, nos estamos refiriendo a si otro contrato puede o no ver su contenido. Fuera de los contratos, cualquier persona puede analizar el estado del contrato, desde etherscan por ejemplo, y encontrar el valor actual de cierta variable. Todo es público y transparente, por eso amamos la web 3.0 😎



ctechallengeresolución Tweet +1