WEB3DEV

Cover image for Como resolver o Ethernaut com Yul
Fatima Lima
Fatima Lima

Posted on

Como resolver o Ethernaut com Yul

Brincando com Yul (Série de 2 Partes)

1Brincando com Yul (Assembly)

Após aprender os fundamentos do Assembly em Solidity na Part 1: Brincando com Yul, vamos agora mergulhar mais fundo! 💪

Vamos resolver os desafios do Ethernaut , escritos pelo OpenZeppelin, inteiramente em Assembly. Usaremos Assembly mais avançado e também aprenderemos sobre exploits de segurança da Ethereum. Além disso, usaremos o Foundry, criado pelo Paradigm, para executar nossos scripts, testes e implantar na testnet (rede de teste) Goerli.

Espero que você aprenda muito ao ler este artigo!

A ideia é que você mesmo experimente os desafios, um por um, e depois de cada desafio, verifique meu repositório para entender a versão Yul. Você verá que, ao longo dos desafios, as mesmas técnicas de Assembly são usadas repetidas vezes, por isso pode ser um pouco monótono depois de algum tempo.

Este artigo não deve ser lido sozinho. Eu apenas escrevi aqui algumas explicações do código para algumas partes difíceis. Portanto, seu primeiro passo deve ser ir para o meu repositório Ethernaut-yul .

Não vou dar explicações detalhadas sobre como resolver cada desafio. A maioria dos desafios é bastante básica e você encontrará muitas explicações se pesquisar on-line. Este tutorial é focado principalmente em Assembly.

O evm.codes é um ótimo site se você não entende o que alguns opcodes fazem.

Assim como na Parte 1, se você tiver alguma dúvida ou problemas para entender o que eu escrevi ou se você quiser apenas conversar → me envie uma mensagem no Twitter 0xteddav.

Primeiro: configure o repositório

Este repo é um mix de Foundry e Hardhat, portanto, também pode ser um bom boilerplate (padrão) para seus projetos futuros (você é bem-vindo 😁).

Instale o Foundry:

https://github.com/foundry-rs/foundry/#installation

Então, execute yarn para instalar as dependências no package.json.

Copie .env.tmpl para .env e digite os valores corretos de sua chave privada e o RPC (Remote Procedure Call, Chamada de Procedimento Remoto) que você vai usar.

Níveis

Cada nível é resolvido pelo forge script. Você encontrará todos os scripts em script/foundry.

Você encontrará um template em Level.s.sol, o qual poderá copiar/colar. Você só precisa de:

  • especificar o endereço do nível;
  • a rede em que você está trabalhando (seja "local" ou "Goerli");
  • mudar a interface do nível em que você está trabalhando.

Se você estiver executando os níveis localmente, precisa executar anvil (ou um nó Hardhat) como fork da Goerli.

$ anvil -f https://rpc.ankr.com/eth_goerli
Enter fullscreen mode Exit fullscreen mode

Então você pode executar o script com:

$ forge script ./script/foundry/XX_LevelName.s.sol
Enter fullscreen mode Exit fullscreen mode

Quando você estiver pronto para realmente enviar as transações on-chain, você "executa" a transmissão adicionando —-broadcast

$ forge script ./script/foundry/XX_LevelName.s.sol --broadcast
Enter fullscreen mode Exit fullscreen mode

Cada script possui uma baseVersion() onde o nível é resolvido com Solidity e o mesmo código é reescrito em Yul com a função yulVersion().

Quando um contrato precisava ser implantado, eu geralmente escrevia a versão Solidity, comentava e reescrevia a versão Yul por baixo.

No diretório de test você encontrará alguns testes forge que escrevi enquanto trabalhava em alguns níveis, para que algumas vezes, você possa ver qual foi a minha linha de raciocínio. Você pode executar esses testes com:

$ forge test -vvvvv --mt testNameOfTheTest
Enter fullscreen mode Exit fullscreen mode

Vamos agora detalhar como eu resolvi alguns dos níveis!

HelloEthernaut

Não usei Yul para este nível, pois ele foi usado apenas para configurar tudo. Eu poderia voltar a ele se alguém estivesse interessado. Poderia ser divertido analisar a resposta da string no Assembly.

Fallback

Vamos começar com o básico. Precisamos chamar o contribute() com um valor de 1.

let dest := sload(instance.slot)
Enter fullscreen mode Exit fullscreen mode

Nós carregamos o endereço da instância que está na variável instance. Poderíamos descobrir nós mesmos o slot de armazenamento da instance (você terá que seguir pela cadeia dos contratos-pai dos quais estamos herdando, o que é chato...), mas em Yul podemos facilmente obter esse valor com .slot.

Vamos detalhar como chamamos uma função externa com Yul. Usaremos muito esse mesmo padrão durante os desafios.

mstore(0, "contribute()")

mstore(0, keccak256(0, 12))
Enter fullscreen mode Exit fullscreen mode

Para chamar uma função, precisamos de seu "selector" (seletor). Você pode ler mais sobre ele em Solidity doc. No Solidity faríamos:

bytes4(keccak256(abi.encodePacked("contribute()")))

Primeiro armazenamos, na memória, em 0 a assinatura da função que queremos chamar: contribute() (observe que o comprimento é 12). Nossa memória vai ficar assim

0x00 0x636f6e7472696275746528290000000000000000000000000000000000000000
0x20 0x0000000000000000000000000000000000000000000000000000000000000000
0x40 0x0000000000000000000000000000000000000000000000000000000000000080

(Se você não entende o que é “636f6e747269627574652829”, busque “string para hexadecimal” no Google 😁).

Em seguida, criamos um hash para a assinatura com keccak256(0, 12) e armazenamos o resultado em 0 na memória. Isso sobrescreverá o valor anterior, mas não importa porque não precisaremos dele mais. Agora nossa memória é

0x00 0xd7bb99ba2c5adddd21e5297f8f4a22a22e4de232bc63ec1e2ec542e79805202e
0x20 0x0000000000000000000000000000000000000000000000000000000000000000
0x40 0x0000000000000000000000000000000000000000000000000000000000000080

O seletor de função contribute() será os 4 primeiros bytes de: 0xd7bb99ba

Então, executaremos nossa call. Consulte evm.codes para detalhes sobre os parâmetros do opcode CALL.


let success := call(gas(), dest, 1, 0, 4, 0, 0)

if iszero(success) {

    revert(0, 0)

}
Enter fullscreen mode Exit fullscreen mode

dest é o endereço do contrato que estamos chamando. Passamos para ele uma valor 1, então dizemos a ele para obter os dados da memória começando em 0 até 4 (nosso seletor de função) e, então, não esperamos um valor de retorno e passamos 0 e 0. success será ou 0 ou, 1 dependendo do resultado da chamada. Então, checamos com iszero e, se a chamada falhar, revertemos.

Acabamos de fazer nossa primeira chamada externa. Isso foi fácil! 🎉

Vamos fazer outro exemplo: uma chamada view. Mais abaixo no código, você encontrará:

mstore(0, "owner()")

mstore(0, keccak256(0, 7))

success := staticcall(gas(), dest, 0, 4, 0, 0x20)

if iszero(success) {

    revert(0, 0)

}
Enter fullscreen mode Exit fullscreen mode

Aqui, chamamos owner() na instância, mas, desta vez, esperamos um resultado. O resultado ficará armazenado na memória em 0 e terá 32 bytes de comprimento (0x20). Usamos staticcall porque esta é uma função view e não modificará o estado. Mais detalhes… no doc.

Depois, carregamos o valor retornado e verificamos se ele corresponde ao nosso player. Caso contrário, revertemos:

let owner := mload(0)

if iszero(eq(owner, sload(player.slot))) {

    revert(0, 0)

}
Enter fullscreen mode Exit fullscreen mode

CoinFlip (Cara ou coroa)

Este nível não poderia ser resolvido com um script Foundry porque cada chamada para exploit() precisa ser enviada em uma transação separada. Então, você encontrará o solucionador (solver) em um script Hardhat em script/hardhat/3_CoinFlip.ts.

Telephone (Telefone)

Este nível introduz um novo padrão: a implantação de um contrato.

Você precisa entender a diferença entre "create code” (código de criação) (ou "código init") e "runtime code” (código de tempo de execução). Você pode encontrar explicações no doc ou neste artigo ou no Stackoverflow.

Queremos implantar nosso contrato TelephoneExploit. O constructor leva 1 argumento address _telephone. Os passos são:

  • armazenar o código init na memória;
  • adicionar o parâmetro constructor;
  • chamar o opcode CREATE.

Só podemos acessar o código creation no Solidity. Portanto, teremos:

bytes memory creationCode = type(TelephoneExploit).creationCode;
Enter fullscreen mode Exit fullscreen mode

Isto facilita tudo para nós porque armazena automaticamente o código na memória. Você deve lembrar-se de como os bytes são armazenados na memória. Vamos supor que não haja mais nada na memória (o que deveria ser o caso, já que não há outra instrução), então nossa memória deveria começar em 0x80. Eis como isso deve ficar:

0x80 size of the code
0xa0 the code of TelephoneExploit…
0xc0 the code of TelephoneExploit…
0xe0

Como o creationCode é o endereço na memória onde começam os dados; como assumimos que os dados são armazenados em 0x80, deveremos ter creationCode == 0x80.

Se fizermos mload(creationCode) (que é igual a mload(0x80)), isso retornará o tamanho do contrato TelephoneExploit. Logo, o código real começa 32 bytes mais tarde. Então fazemos add(creationCode, 0x20):

let contractSize := mload(creationCode)

let contractOffset := add(creationCode, 0x20)
Enter fullscreen mode Exit fullscreen mode

Só precisamos armazenar o argumento do constructor. Isso é armazenado no final do código do contrato. Como conhecemos o tamanho do contrato, basta adicioná-lo ao início do código do contrato. O endereço para _telephone deve ser o endereço da instance e, então, usamos sload(instance.slot).

let offsetConstructorArg := add(contractOffset, contractSize)

mstore(offsetConstructorArg, sload(instance.slot))
Enter fullscreen mode Exit fullscreen mode

E então, apenas precisamos usar CREATE e nosso contrato está implantado! 🎉

let telephoneExploit := create(0, contractOffset, mload(creationCode))
Enter fullscreen mode Exit fullscreen mode

Você também observou a função getOwner(). Nossa primeira função no Yul. Muito legal!

function getOwner(_contract) -> _owner {

    mstore(0, "owner()")

    mstore(0, keccak256(0, 7))

    let success := staticcall(gas(), _contract, 0, 4, 0, 0x20)

    if iszero(success) {

        revert(0, 0)

    }

    _owner := mload(0)

}
Enter fullscreen mode Exit fullscreen mode

Infelizmente, as funções Yul só podem ser utilizadas no mesmo bloco assembly em que foram definidas. Portanto, não as usaremos em demasia porque teremos que reescrevê-las de qualquer forma.

Token

Vamos ver como podemos chamar uma função com um parâmetro e obter um resultado de volta.

mstore(0, "balanceOf(address)")

mstore(0, keccak256(0, 18))

mstore(0x4, sload(player.slot))

pop(staticcall(gas(), token, 0, 0x24, 0, 0x20))

let startBalance := mload(0)
Enter fullscreen mode Exit fullscreen mode

Como você já viu antes: temos o seletor para balanceOf(address), mas, desta vez, vamos acrescentar um argumento. Nós fazemos mstore(0x4, sload(player.slot)). Armazenamos o endereço do player no offset 4. Portanto, os 4 primeiros bytes serão o seletor de função e os 32 bytes seguintes representarão o endereço. Por exemplo, digamos que o endereço seja 0x7c019b7834722f69771cd4e821afc8e717baaab5.

Os dados serão:

0x70a082310000000000000000000000007c019b7834722f69771cd4e821afc8e717baaab5
Enter fullscreen mode Exit fullscreen mode

E seu comprimento será 36 bytes (0x24).

Observe que usamos pop porque não queremos checar se a chamada foi bem sucedida ou não. Se não fosse bem sucedida, a transação seria revertida de qualquer forma em algum momento e fracassaríamos no desafio. Mas, na produção, você deve sempre verificar se a chamada foi bem sucedida ou não!

King (Rei)

Vamos tentar algo novo: reverter com uma string. Verifique contracts/9_King.sol.

Você vai encontrar aqui e aqui explicações sobre como funcionam os erros em Solidity. Assim como as funções, os erros também têm seletores. O seletor para uma string erro é Error(string). Portanto, precisamos tê-lo, armazená-lo na memória e depois armazenar nossa string. Fácil!

Armazena o seletor:

mstore(ptr, "Error(string)")

mstore(ptr, keccak256(ptr, 13))

Armazena a string:

mstore(add(ptr, 4), 0x20)

mstore(add(add(ptr, 4), 0x20), 9)

mstore(add(add(ptr, 4), 0x40), "not owner")
Enter fullscreen mode Exit fullscreen mode

Lembre-se de como as strings são tratadas pela EVM (assim como bytes): primeiro o offset, depois o comprimento da string e, finalmente, a própria string. E então, revertemos com os dados que acabamos de armazenar: revert(ptr, 0x64).

Reentrancy (Reentrância)

Não vou fazer muitas explicações, pois é o mesmo processo de antes, mas aqui basta observar que temos que armazenar mais de 1 parâmetro para a função exploit(). Se tentássemos armazená-los na memória 0, sobrescreveríamos o ponteiro de memória livre em 0x40, o que levaria a um comportamento perigoso e provavelmente nossa transação falharia. Em vez disso, armazenamos nossos dados na memória onde temos espaço disponível → para onde o ponteiro de memória livre nos aponta.

Observe que, no nível anterior (King), quando armazenamos nossa string erro, nós sobrescrevemos o ponteiro de memória livre. Mas não nos importamos, já que paramos a execução e revertemos logo em seguida.

Privacy (Privacidade)

Neste nível, precisamos de um bytes16, mas você sabe que os valores são armazenados em 32 bytes na EVM, então precisamos de uma bitmask para apagar alguns dos bytes. Os bytes são armazenados em bytes higher-order (alinhados à esquerda). Portanto, se quisermos os 16 primeiros bytes, precisamos criar uma mask (máscara) que se pareça com esta:

0xffffffffffffffffffffffffffffffff00000000000000000000000000000000

let mask := shl(128, sub(exp(2, 128), 1))
Enter fullscreen mode Exit fullscreen mode

Que é 2**128 - 1 &lt;< 128.

Então, só precisamos aplicar nossa máscara: let key := and(data2, mask).

Preservation (Preservação)

Em contracts/16_Preservation.sol temos uma bitmask (máscara de bits) para um endereço. Um endereço na Ethereum tem 20 bytes (160 bits). Assim, nossa máscara será 2 ** 160 - 1 → sub(exp(2, 160), 1).

Recovery (Recuperação)

Computamos o endereço onde o contrato será implantado.

address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xd6), bytes1(0x94), address(instance), nonce)))))
Enter fullscreen mode Exit fullscreen mode

Você encontrará algumas explicações sobre isso aqui e aqui.

No Foundry há um cheatcode (código de trapaça) conveniente para isso: computeCreateAddress.

MagicNumber

Aqui você pode ver claramente a diferença entre código creation (de criação) e código de tempo de execução sobre o qual falamos anteriormente. O código de tempo de execução é o código que será retornado pelo constructor do contrato.

constructor(bytes memory bytecode) {

    assembly {

        return(add(bytecode, 0x20), mload(bytecode))

    }

}
Enter fullscreen mode Exit fullscreen mode

Passamos no bytecode o que eventualmente queremos que o código de tempo de execução seja e só precisamos retorná-lo do constructor. Nosso código de tempo de execução será 602a60005260206000f3, que é traduzido para:


PUSH1 42

PUSH1 0

MSTORE

PUSH1 32

PUSH1 0

RETURN
Enter fullscreen mode Exit fullscreen mode

Puzzle Wallet

Uau! Este aqui é um pouco mais avançado. Mais chamadas externas do que o normal e muitos bytes sendo codificados 😱. Então, vou tentar lhe dar algumas explicações.

Vamos pensar sobre o que queremos codificar:

→ chame multicall(bytes[]) com:

  • uma chamada para deposit();
  • outra chamada para multicall(bytes[]);
    • uma subchamada para deposit().

Então, como nossos dados de chamada vão ficar?

Em Solidity, os bytes são codificados em três partes em calldata:

  1. Um offset onde o comprimento dos bytes pode ser encontrado.
  2. No offset especificado no passo 1, o comprimento dos bytes é armazenado.
  3. Em seguida, ao offset do passo 1, adicionamos 0x20 e é onde os bytes reais são armazenados.

Esta codificação é usada para permitir que a EVM leia eficientemente o comprimento dos bytes que estão sendo passados como argumentos em uma chamada de função, sem a necessidade de analisar os dados completos.

Um exemplo rápido como um lembrete?

function myFunction(uint256 myUint, bytes memory myBytes, uint256 myOtherUint) public {}
Enter fullscreen mode Exit fullscreen mode

Digamos que queremos chamar esta função com os seguintes valores:

  • myUint: 123
  • myBytes: 0xabcdef
  • myOtherUint: 456

Eis como os dados de chamada seriam codificados:

  1. O seletor de função: bytes4(keccak256(abi.encodePacked("myFunction(uint256,bytes,uint256)"))) → 0xe329087e
  2. O primeiro parâmetro (myUint): 000000000000000000000000000000000000000000000000000000000000007b
  3. O segundo parâmetro (myBytes):
    1. O offset onde encontraremos o comprimento: 000000000000000000000000000000000000000000000000000000000000001c
    2. Neste offset, o comprimento de bytes: 0000000000000000000000000000000000000000000000000000000000000003
    3. No offset + 0x20, os bytes reais: abcdef0000000000000000000000000000000000000000000000000000000000
  4. O terceiro parâmetro (myOtherUint): 00000000000000000000000000000000000000000000000000000000000001c8

Considerando tudo isso, os dados de chamada resultantes seriam:

0xe329087e

000000000000000000000000000000000000000000000000000000000000007b

0000000000000000000000000000000000000000000000000000000000000060

00000000000000000000000000000000000000000000000000000000000001c8

0000000000000000000000000000000000000000000000000000000000000003

abcdef0000000000000000000000000000000000000000000000000000000000
Enter fullscreen mode Exit fullscreen mode

Agora, vamos às coisas mais complexas: nosso multicall(bytes[]).

Vamos começar com a primeira chamada deposit().

function multicall(bytes[] calldata data) external {}

function deposit() external {}
Enter fullscreen mode Exit fullscreen mode

O seletor de função para multicall(bytes[]): bytes4(keccak256(abi.encodePacked("multicall(bytes[])"))) → 0xac9650d8.

O seletor de função deposit(): bytes4(keccak256(abi.encodePacked("deposit()"))) → 0xd0e30db0.

Para arrays, a codificação dos dados de chamada é semelhante aos bytes, exceto que primeiro precisamos armazenar o comprimento do array. Assim, nossos dados de chamada ficarão assim:

  • seletor de função
  • offset onde vamos encontrar o comprimento do array
  • comprimento do array
  • lista de offsets onde os dados serão encontrados para cada elemento
  • dados

Nosso multicall(bytes[]) com deposit():

0xac9650d8

0000000000000000000000000000000000000000000000000000000000000020 offset

0000000000000000000000000000000000000000000000000000000000000001 length of array

0000000000000000000000000000000000000000000000000000000000000020 offset of first element of array

0000000000000000000000000000000000000000000000000000000000000004 length of data -> 4 bytes, which is the length of the function selector

d0e30db000000000000000000000000000000000000000000000000000000000 data -> the function selector of `deposit()`
Enter fullscreen mode Exit fullscreen mode

E agora, nossos dados de chamada com tudo:

0x00 0xac9650d8
0x00 0000000000000000000000000000000000000000000000000000000000000020
0x20 0000000000000000000000000000000000000000000000000000000000000002
0x40 0000000000000000000000000000000000000000000000000000000000000040
0x60 0000000000000000000000000000000000000000000000000000000000000080
0x80 0000000000000000000000000000000000000000000000000000000000000004
0xa0 d0e30db000000000000000000000000000000000000000000000000000000000
0xc0 00000000000000000000000000000000000000000000000000000000000000a4
0xe0 ac9650d800000000000000000000000000000000000000000000000000000000
0x100 0000002000000000000000000000000000000000000000000000000000000000
0x120 0000000100000000000000000000000000000000000000000000000000000000
0x140 0000002000000000000000000000000000000000000000000000000000000000
0xac9650d8

0000000000000000000000000000000000000000000000000000000000000020 offset of array length

0000000000000000000000000000000000000000000000000000000000000002 length of array -> 2

0000000000000000000000000000000000000000000000000000000000000040 offset of 1st element of array: `deposit()` call

0000000000000000000000000000000000000000000000000000000000000080 offset of 2nd element: `multicall(bytes[])` call

0000000000000000000000000000000000000000000000000000000000000004 first element. This is the call to deposit(). We already covered it.

d0e30db000000000000000000000000000000000000000000000000000000000

00000000000000000000000000000000000000000000000000000000000000a4 second element. This is the inner multicall(). The data size is 0xa4 -> 164 bytes

ac9650d800000000000000000000000000000000000000000000000000000000

0000002000000000000000000000000000000000000000000000000000000000

0000000100000000000000000000000000000000000000000000000000000000

0000002000000000000000000000000000000000000000000000000000000000

00000004d0e30db0000000000000000000000000000000000000000000000000

0000000000000000000000000000000000000000000000000000000000000000
Enter fullscreen mode Exit fullscreen mode

Vamos separar os dados da segunda chamada. Esta é apenas mais uma chamada para multicall(bytes[])` e então é fácil de entender.
`
ac9650d8

0000000000000000000000000000000000000000000000000000000000000020 offset

0000000000000000000000000000000000000000000000000000000000000001 length

0000000000000000000000000000000000000000000000000000000000000020 offset of element 1

0000000000000000000000000000000000000000000000000000000000000004 length of data

d0e30db000000000000000000000000000000000000000000000000000000000 data -> function selector for deposit()

00000000000000000000000000000000000000000000000000000000 -> this is just some padding to make sure we have a multiple of 32 bytes
`

Motorbike (Motocicleta)

Apenas um comentário sobre este nível: quando carregamos o exploitSelector, não aplicamos uma máscara a ele. Não nos importamos muito porque, quando formos passá-lo, especificaremos que o comprimento é de 4 bytes. O resto será ignorado. Mas lembre-se de que, se você realmente enviar essa transação, ela será mais cara, porque você estará enviando mais bytes. E os bytes de calldata são caros!
`
mstore(add(fmp, 0x24), 0x40)

mstore(add(fmp, 0x44), 4) // <--- aqui especificamos: "leia apenas os próximos 4 bytes" (o seletor para exploit())

mstore(add(fmp, 0x64), exploitSelector) // <-- mas aqui armazenamos todo o hash, não apenas o seletor de 4 bytes
`

DoubleEntryPoint

Outra bitmask para obter somente os 4 bytes do seletor:
`
let selectorMask := shl(224, sub(exp(2, 32), 1))
`

Se você abrir contracts/26_DoubleEntryPoint.sol, a assinatura da função é function handleTransaction(address user, bytes calldata msgData) external.

Então msgData está em calldata. Não foi copiado para a memória, por isso, não podemos usar mload. Precisamos usar calldataload em vez disso para carregar os dados.

Podemos, nós mesmos, facilmente calcular o offset onde encontraremos os dados, mas há uma maneira mais fácil: .offset.

Primeiro obtemos o seletor de funções aplicando a bitmask selectorMask.

Então, nós precisamos obter o terceiro parâmetro: lembre-se de que a função é delegateTransfer(address to, uint256 value, address origSender) e só queremos origSender.
`
let msgSelector := and(calldataload(msgData.offset), selectorMask)

let origSender := calldataload(add(msgData.offset, 0x44))
`

GoodSamaritan (BomSamaritano)

Aqui, revertemos com um erro habitual do Solidity. Nada de especial. Funciona exatamente como os seletores de função.
`
mstore(0, "NotEnoughBalance()")

mstore(0, keccak256(0, 18))

revert(0, 4)
`

GateKeeper 3

Com esta última, já que temos muitas funções externas para chamar, vamos escrever algumas funções Yul. Você já deve ter visto, no nível Telephone, que eu já experimentei funções nos testes (verifique em test/foundry/4_Telephone.t.sol).

Precisamos fazer algumas chamadas para o contrato gatekeeper: construct0r(), createTrick(), getAllowance(uint256) e enter().

Podemos facilmente escrever uma função que apenas crie um hash da assinatura da função para nós e armazene o seletor. Mas, como você sabe, você precisa do comprimento da assinatura da função.

Portanto, vamos primeiro escrever uma função para obter o comprimento de uma string.
`
function getStrLen(_str) -> _length {

for {

    let i := 0

} lt(i, 0x20) {

    i := add(i, 1)

} {

    if iszero(byte(i, _str)) {

        break

    }

    _length := add(_length, 1)

}
Enter fullscreen mode Exit fullscreen mode

}

Fácil! Apenas passamos uma string como input, e serão feitos loops com ela enquanto houver um char (caractere). Se ele encontrar um
0,` significa que estamos no final da string, então retornamos o comprimento. Tenha cuidado: só funciona enquanto a string tiver menos de 32 bytes, mas tudo isso é bom para nós aqui, pois todas as nossas assinaturas de função têm menos de 32 caracteres.

Em seguida, podemos criar um hash para esta string e armazenar o resultado na memória:
`
function hashSelector(_sig) {

mstore(0, _sig)

mstore(0, keccak256(0, getStrLen(_sig)))
Enter fullscreen mode Exit fullscreen mode

}

De novo, bem simples. Finalmente, podemos simplesmente fazer nossa chamada:

function callExternal(_address, _sig, _param) {

hashSelector(_sig)

let calldataLen := 4

if iszero(iszero(_param)) {

    mstore(4, _param)

    calldataLen := 0x24

}

let _success := call(gas(), _address, 0, 0, calldataLen, 0, 0)

if iszero(_success) {

    revert(0, 0)

}
Enter fullscreen mode Exit fullscreen mode

}

Como
getAllowance` leva um parâmetro, acrescentamos uma declaração if para armazenar esse parâmetro e aumentar o tamanho dos dados de chamada que serão enviados.

🔥 A propósito, algo engraçado sobre esse nível. Ao resolvê-lo, descobri um pequeno bug no Foundry, para o qual eu abri uma questão e, finalmente, decidi resolvê-la eu mesmo e submeti um PR. Você pode ler mais sobre isso no meu tweet e o artigo relacionado no dev.to.

E… Terminamos !!! 😍

Conclusão

Isso é tudo! Espero que tenham se divertido resolvendo os desafios do Ethernaut e que tenha gostado desta pequena caminhada e aprendido muito sobre Assembly!

Mais uma vez, se você não tiver entendido alguma coisa ou se eu cometi um erro, pode me contatar no Twitter: 0xteddav ❤️.

Esse artigo foi escrito por teddav e traduzido por Fátima Lima. O original pode ser lido aqui.

Latest comments (0)