Pentesting Smart Contract – Introducción

En esta entrada aprenderemos hacer pentesting a Smart Contract. Un «contrato inteligente» de Ethereum es un programa informático que se ha desplegado en la blockchain de Ethereum, donde existirá para siempre. Los contratos inteligentes desplegados representan muchas capacidades como ejecutar código para realizar «transacciones» que se le envían a través de una de las funciones públicas del contrato.

Como estas entradas tendrán un carácter introductorio, nos veremos inmersos en la tecnología Ethereum y crearemos Smart Contract para luego poder saber encontrar vulnerabilidades. Usaremos múltiples recursos tal como https://capturetheether.com/challenges/. Para empezar con el primero nos pide instalar la extensión en Firefox Metamask. Nos generará una Wallet nueva y nos conectaremos a Ropsten test network. Usaremos esta página web para poder comprar ETH (La unidad monetaria de Ethereum se llama ether) en la red de prueba https://faucet.dimensions.network/.

Una vez hemos comprado ETH en nuestra «Wallet» (monedero) podremos visualizarla. El término «wallet» ha llegado a significar muchas cosas, aunque todas están relacionadas y en el día a día se reducen prácticamente a lo mismo. Utilizaremos el término «wallet» para referirnos a una aplicación de software que te ayuda a gestionar nuestra cuenta de Ethereum.

En resumen, un monedero de Ethereum es su puerta de entrada al sistema Ethereum. Guarda tus claves y puede crear y emitir transacciones. Elegir un monedero de Ethereum puede ser difícil porque hay muchas opciones diferentes con distintas características y diseños. Algunos son más adecuados para los principiantes y otros para los expertos. MetaMask es un monedero que forma parte de una extensión que se ejecuta en su navegador (Chrome, Firefox, Opera). Es fácil de usar y conveniente para tests, ya que es capaz de conectarse a una variedad de nodos Ethereum y blockchains. MetaMask es un monedero basado en web, a diferencia de ser una wallet de movil o escritorio.

Las blockchain como Ethereum funcionan como un sistema descentralizado. Eso significa muchas cosas, pero un aspecto crucial es que cada usuario de Ethereum puede/debe controlar sus propias claves privadas, que son las que dan el acceso a los fondos y a los contratos inteligentes (smart contract). Algunos usuarios optan por ceder el control de sus claves privadas recurriendo a una tercera persona, como un Exchange. El control conlleva una gran responsabilidad. Si pierdes tus claves privadas, pierdes el acceso a tus fondos y contratos. Nadie puede ayudarte a recuperar el acceso, tus fondos quedarán bloqueados para siempre.

Ethereum tiene muchos lenguajes de alto nivel diferentes, todos los cuales pueden ser utilizados para escribir un contrato y producir bytecode EVM. Un lenguaje de alto nivel como Solidity es, la opción dominante para la programación de smart contract. Solidity fue creado por el Dr. Gavin Wood, y se ha convertido en el lenguaje más utilizado en Ethereum. Usaremos Solidity para resolver en primera instancia los retos propuestos.

pragma solidity ^0.4.21;

contract DeployChallenge {
    // This tells the CaptureTheFlag contract that the challenge is complete.
    function isComplete() public pure returns (bool) {
        return true;
    }
}

Podemos observar como se declara el contrato como DeployChallenge, muy similar como si se declarase una clase en otros lenguajes como Java. Seguidamente tenemos un método publico que significa que puede ser llamado por otros contratos, y retorna un booleano (True o False). Devolverá True cuando el contrato haya sido lanzado o «deployed». Ahora que tenemos nuestro primer contrato de ejemplo, necesitamos utilizar un compilador de Solidity para convertir el código de Solidity en bytecode de EVM para que pueda ser ejecutado por el EVM en la propia blockchain. El compilador de Solidity viene como un ejecutable independiente en entornos de desarrollo integrados (IDE). Para simplificar las cosas, utilizaremos uno de los IDE más populares, llamado Remix.

Copiamos el código y lo compilamos. Ya tenemos un contrato!. Lo hemos compilado en bytecode. Ahora, tenemos que «registrar» el contrato en la cadena de bloques (blockchain) de Ethereum. Utilizaremos la Ropsten test network para probar nuestro contrato. El registro de un contrato en la blockchain implica la creación de una transacción especial cuyo address es 0x0000000000000000000000000000000000000000 conocida como la zero address. Es una dirección especial que indica a la blockchain de Ethereum que quieres registrar un contrato. Afortunadamente, el IDE Remix se encargará de todo eso y enviará la transacción a MetaMask. Para ello en Run tenemos que seleccionar Injected Web3.

Le damos a Deploy y Confirmamos. Ahora solo hay que esperar. El contrato tardará entre 15 y 30 segundos en ser minado en Ropsten.

Y aqui tendriamos el contrato desplegado! Solo tenemos que dar al botón isComplete que es el método implementado y devolverá True ya que lógicamente ya esta desplegado el contrato.

Os podéis dar cuenta que la dirección del Smart Contract es una aleatoria, distinta lógicamente a nuestra dirección de la Wallet. Cada contrato desplegado tiene una dirección, que es un número entero único de 160 bits que se utilizará posteriormente para todas las referencias a ese contrato.

Un breve resumen de lo que hemos aprendido. Los Smart Contract son programas que controlan transacciones de dinero que se ejecutan en una maquina virtual denominada EVM. Son creados por una transacción especial que suben su bytecode a la blockchain. Una vez son creados en la blockchain, tiene una dirección Ethereum pública tal como por ejemplo un monedero. Cada vez que alguien envía una transacción a una dirección del contrato, hace que el contrato se ejecute en el EVM con la transacción como input. En este caso no tenemos transacción de ningún tipo, solo una llamada a un método. Las transacciones que se envían a una dirección del contrato, se envía como input ether o datos. Si es ether entonces será una transacción para realizar un deposito a la dirección del contrato. Si contiene datos, será entonces en este ejemplo una llamada a una función o método.

Ahora, navegamos por el navegador hasta ropsten.etherscan.io y pegamos la dirección en el search. Deberíamos ver el historial de direcciones Ethereum del contrato.

Vamos a ver el siguiente ejemplo más complejo que el anterior pero muy sencillo al mismo tiempo.

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Instance {

  string public password;
  uint8 public infoNum = 42;
  string public theMethodName = 'The method name is method7123949.';
  bool private cleared = false;

  // constructor
  constructor(string memory _password) public {
    password = _password;
  }

  function info() public pure returns (string memory) {
    return 'You will find what you need in info1().';
  }

  function info1() public pure returns (string memory) {
    return 'Try info2(), but with "hello" as a parameter.';
  }

  function info2(string memory param) public pure returns (string memory) {
    if(keccak256(abi.encodePacked(param)) == keccak256(abi.encodePacked('hello'))) {
      return 'The property infoNum holds the number of the next info method to call.';
    }
    return 'Wrong parameter.';
  }

  function info42() public pure returns (string memory) {
    return 'theMethodName is the name of the next method.';
  }

  function method7123949() public pure returns (string memory) {
    return 'If you know the password, submit it to authenticate().';
  }

  function authenticate(string memory passkey) public {
    if(keccak256(abi.encodePacked(passkey)) == keccak256(abi.encodePacked(password))) {
      cleared = true;
    }
  }

  function getCleared() public view returns (bool) {
    return cleared;
  }
}

En el contrato tenemos una serie de atributos, un constructor y unos métodos. El constructor nos sirve para cuando hagamos deploy del contrato insertemos una password. Las strings pueden almacenarse tanto en la memoria como en el Storage, en este caso es en memoria de la instancia del contrato. Los siguientes métodos realmente devuelven valores strings que nos da información de los métodos existentes. El objetivo en este contrato realmente es autenticarse usando el método correcto con la password de parámetro. Compara lo que pasamos por argumento en el método auth con la password que se usa en el Deploy del contrato, que recordamos es una password que solo sabe el Owner, ya que lo lanza con una password tal como indica el constructor.

Todas las funciones y variables almacenadas en la cadena de bloques son visibles declaradas como public. Lo cual usar password como public es inseguro. Nunca almacenar password dentro de un contrato.

Desde MetaMask copiamos la dirección del contrato y enviamos 0.0001 ether, para que así podáis observar que no es solo usar los métodos del contrato, podemos hacer una transacción de dinero.

Llamar a funciones/métodos

Para resolver este smart contract necesitamos llamar a la función callme en el contrato inteligente que se ha desplegado en la dirección mostrada en la pantalla:

Usaremos Remix para ello, ya que es muy sencillo. Como tenemos el código fuente simplemente lo copiamos en un fichero nuevo y en vez de hacer deploy tenemos que elegir At address.

Ahora, ya tenemos los métodos disponibles para poder interactuar con el contrato. También es importante en environment elegir Injected Web3, seleccionando automáticamente nuestra wallet desde MetaMask encargada de realizar la transacción a la dirección del contrato.

En este ejemplo, tenemos que pasar un nickname a una función.

pragma solidity ^0.4.21;

// Relevant part of the CaptureTheEther contract.
contract CaptureTheEther {
    mapping (address => bytes32) public nicknameOf;

    function setNickname(bytes32 nickname) public {
        nicknameOf[msg.sender] = nickname;
    }
}

// Challenge contract. You don't need to do anything with this; it just verifies
// that you set a nickname for yourself.
contract NicknameChallenge {
    CaptureTheEther cte = CaptureTheEther(msg.sender);
    address player;

    // Your address gets passed in as a constructor parameter.
    function NicknameChallenge(address _player) public {
        player = _player;
    }

    // Check that the first character is not null.
    function isComplete() public view returns (bool) {
        return cte.nicknameOf(player)[0] != 0;
    }
}

La parte relevante del contrato es la función setNickname. Esta función recibe un argumento de tipo bytes32 (Byte-Array fixed hasta 32 bytes) como nickname, por tanto buscando un poco en Google simplemente le tenemos que pasar a la función 32 bytes en hexadecimal del nickname que queremos poner:

0x6e61697665414141414141414141414141414141414141414141414141414141

En este caso sera naiveAAAAA… Más información sobre el tipo de dato bytes32.

Para seguir escribiendo entradas sobre pentesting smart contract, una pequeña aportación os dejo mi address! (por la mainnet) 0x18AF27f51A49e304Abc64B1B9dd3a002FaE264B4