Pular para o conteúdo
0%

Vulnerabilidade de empacotamento de UserOperation no ERC-4337

Autor: Adam Egyed

Última atualização: 21 de março de 20237 min de leitura

Contribuidores Adicionais: fangting@alchemy.com, @drortirosh, @Gooong, @taylorjdawson, @leekt, @livingrockrises

Visão geral

Em 7 de março de 2023, a Alchemy e outros membros da comunidade open source de desenvolvedores, incluindo @Gooong, @taylorjdawson, @leekt e @livingrockrises, identificaram problemas de decodificação de calldata no contrato EntryPoint do ERC-4337 e no contrato de exemplo VerifyingPaymaster.

Esses contratos estão atualmente implantados em diversas chains e geram hashes sobre user operations. A implementação resultava em hashes inconsistentes dependendo do método de assinatura, o que pode levar a diversos efeitos secundários, como hashes divergentes para as mesmas UserOperations e hashes colidentes para UserOperations diferentes.

A discussão foi conduzida por @drortirosh e está documentada nesta issue do Github.

Detalhamento

Abaixo está um detalhamento do código afetado, explicações sobre a Vulnerabilidade de Packing do EntryPoint, a Vulnerabilidade de Packing do VerifyingPaymaster, e seus respectivos impactos.

Código afetado

O segmento de código em questão é o seguinte:

‍UserOperation.sol:61-75

VerifyingPaymaster.sol:35-49

Para contexto, a struct UserOperation é definida como:

Ambos os segmentos de código usam assembly para copiar uma grande porção do calldata para a memória, com a intenção de capturar parte de uma user operation para gerar o hash.

O método pack em UserOperationLib tem a intenção de capturar todos os campos da user operation, de sender a maxPriorityFeePerGas, incluindo os campos de tamanho variável (chamados de campos dinâmicos na codificação ABI) initCode, callData e paymasterAndData.

O método pack em VerifyingPaymaster inclui todos esses campos, exceto o campo paymasterAndData, já que este ainda não está definido.

Para implementar isso, ambos os métodos usam um campo de conveniência em Yul fornecido para tipos dinâmicos no calldata, chamado .offset. Isso se refere ao valor fornecido na codificação ABI de uma struct, definido aqui na especificação do Solidity. (Na verdade, refere-se à palavra de memória após o offset, mas isso é apenas por conveniência no carregamento).

Um codificador ABI padrão codificará os valores dos campos dinâmicos (chamados de "tail" no ABI coder) na ordem em que aparecem.

Considere a seguinte codificação de uma user operation em calldata que poderia ser gerada:

💡 Nota: Este exemplo mostra uma user operation em que todos os campos dinâmicos têm menos de uma palavra de comprimento, por questão de brevidade.

💡‍ *‍*Nota: O espaço de endereços de memória aqui está dentro da própria struct da user operation. No calldata real, ele será colocado em outro lugar devido ao espaço ocupado pelo method selector e pela tupla de argumentos.

Neste exemplo, seguir pnd.offset para gerar um packing da user operation resultará nesta "fatia" do calldata:

Isso contém exatamente o que queremos!

No entanto, contratos que usam argumentos codificados em ABI não validam em que ordem os campos são definidos, nem mesmo se os offsets são válidos.

Usar signature.offset ou pnd.offset lerá o valor de "offset" correspondente diretamente do calldata.

Usando isso como limite, é possível construir representações válidas de user operations no calldata que têm propriedades de hash incomuns.

Vamos explorar como isso afeta o EntryPoint e o VerifyingPaymaster de forma independente.

Vulnerabilidade de packing do EntryPoint

Para demonstrar essa vulnerabilidade, precisamos considerar um contrato de wallet diferente do SimpleAccount.sol fornecido, já que esse exemplo reutiliza o código vulnerável de EntryPoint.

A divergência de hash se torna relevante quando um esquema de hashing diferente é usado entre o EntryPoint e o contrato de wallet, ou se a wallet assina uma codificação de user operation não padrão.

Os riscos introduzidos em EntryPoint são que uma única user operation pode ser representada por múltiplos "user op hashes" e que o mesmo "user op hash" pode representar múltiplas user operations.

Considere esta conta, chamada ExampleAccount, que possui sua própria ExampleAccountFactory. A conta de exemplo usa um único signatário para validar user operations.

Para conceder permissão para executar uma user operation, é gerado e assinado um hash sobre todos os campos da user operation, exceto a própria assinatura.

O método validateUserOp é definido da seguinte forma:

Esta é uma implementação relativamente simples de validação de assinatura, pois verifica todos os campos de userOp, junto com o endereço do entrypoint e o chain id.

Como um dos objetivos da account abstraction, o método validateUserOp pode conter lógica arbitrária (embora limitada pelas restrições de acesso a storage), já que esse método representa as condições sob as quais uma user operation pode se originar.

Para esta conta de exemplo, as user operations partem de uma assinatura do owner. De forma mais geral, porém, user operations podem se originar de condições arbitrárias: estado on-chain, múltiplas assinaturas, ou assinaturas específicas de app – isso é um recurso da account abstraction.

Para demonstrar essa vulnerabilidade, vamos construir um calldata malicioso para EntryPoint.handleOps de forma que o UserOperationEvent emitido por EntryPoint tenha um valor inesperado.

Depois de definir uma struct UserOperation memory uo de exemplo, veja como podemos construir o calldata:

rightPadBytes é uma função auxiliar escrita para alinhar tipos bytes ao comprimento de palavra completo mais próximo.

Ela é definida da seguinte forma:

Agora, ao chamar handleOps, o evento emitido e o resultado de EntryPoint.getUserOpHash\(\) serão diferentes.

Impacto

Bundlers maliciosos, ou EOAs que não são bundlers chamando EntryPoint.handleOps, podem modificar sua representação de um UserOp no calldata para alterar o hash da UO nos eventos emitidos. Isso pode quebrar sistemas off-chain que integram com os eventos emitidos, já que agora se revela que os eventos são não-determinísticos para uma dada UO.

Além disso, o bundler terá que lidar com não-determinismo ao ler userOpHashes emitidos pelo contrato EntryPoint. Para verificar se um UserOperationEvent emitido pelo EntryPoint corresponde a uma user operation no mempool local do bundler, uma comparação do valor de hash não é mais suficiente, já que o calldata para handleOps pode ser modificado para alterar os valores de hash.

Em vez disso, os bundlers terão que buscar transaction receipts, obter o calldata enviado para handleOps, decodificá-lo, e então obter os hashes "canônicos" recodificando via o ABI coder padrão e chamando EntryPoint.getUserOpHash\(...\). Isso é necessário para determinar se as user operations no mempool local foram mineradas ou não. Além disso, como chamadas para EntryPoint.handleOps podem ocorrer de dentro de outras chamadas de contrato, a decodificação pode estar em uma parte profunda da call stack.

Essa divergência também afetará a implementação dos métodos RPC do bundler, já que um user op hash é usado para identificação em eth\_getUserOperationByHash e eth\_getUserOperationReceipt.

Os bundlers precisarão realizar buscas, parsing e decodificação custosos do calldata para EntryPoint.handleOps\(...\) a fim de traduzir os hashes emitidos nos eventos para hashes "canônicos" a partir de EntryPoint.getUserOpHash\(...\).

💡 Nota: Essa vulnerabilidade é distinta do fato de que SCWs mal-intencionadas podem reutilizar user operations. User operations reutilizadas, e de forma mais geral, todas as user operations, deveriam ter um hash determinístico. Outras aplicações e serviços construídos sobre o ERC-4337 terão que implementar sua própria mitigação, a menos que isso seja resolvido.

Como o ERC-4337 está nos estágios iniciais de adoção em geral, é difícil descrever o impacto potencial dessa vulnerabilidade no ecossistema mais amplo. O escopo do impacto depende das implementações de bundlers, exploradores de user operations, indexadores e outros serviços off-chain.

No mínimo, causaria uma experiência de usuário confusa, já que o user operation hash (semelhante ao transaction hash) pode mudar entre o momento do envio e o momento da inclusão, de forma que algumas wallets podem não considerar essa diferença e deixar de exibir atualizações a seus usuários.

Em um caso de risco médio, wallets podem ser projetadas de forma a evitar intencionalmente a indexação, definindo todos os seus user op hashes como iguais (veja o exemplo disso fornecido por @leekt).

Em um caso de risco alto, um serviço off-chain que monitora a inclusão de user operations poderia deixar de detectar a inclusão de uma determinada user operation, e tentar reenviar ou lidar incorretamente com dados e chaves.

Prova de conceito

Veja a prova de conceito completa neste repositório.

Vulnerabilidade de packing do VerifyingPaymaster

Os riscos introduzidos em VerifyingPaymaster são que uma user operation pode conter conteúdo diferente entre o momento da assinatura e a inclusão on-chain. Isso pode acontecer quando duas user operations diferentes retornam o mesmo hash de VerifyingPaymaster.getHash\(UserOperation userOp, uint48 validUntil, uint48 validAfter\).

Vamos construir o calldata para essa função para mostrar como isso pode ocorrer:

Calldata 1

Observe como essa codificação muda a ordem dos campos dinâmicos, mas continua válida de outra forma - os offsets apontam para os locais corretos e os comprimentos são todos válidos. Porém, como o offset de paymasterAndData tem um valor inesperado, a fatia que obtemos de pack\(\) será a seguinte:

Veja como initCode e callData são excluídos da fatia!

Vamos construir um segundo calldata de entrada, desta vez modificando maliciosamente ambos os campos:

  • initCode mudará de 1517c0de para 1517c0de02
  • callData mudará de ca11dada1 para ca11data02

Isso nos dá o seguinte:

Calldata 2

Se realizarmos a mesma operação pack nesse calldata diferente, o resultado será a mesma fatia de antes! E podemos verificar que os hashes são iguais com o seguinte:

Executando isso em um ambiente de teste no foundry revela que ambas as user ops têm o mesmo hash: 0x736f86d224bab46a95ae119947e172efa694379d9ac682d4ca780b7640a89b06

Veja este arquivo de teste para o POC completo.

Impacto

Nessa vulnerabilidade, o hash pode ser modificado para cobrir menos elementos do que o esperado, permitindo que initCode, callData, e possivelmente outros campos estáticos sejam excluídos do hash, e assim variar entre a assinatura e o uso. Isso pode resultar em assinaturas de sponsorship do paymaster sendo usadas para finalidades diferentes das pretendidas.

Por exemplo, a factory de deploy do contrato de wallet e a chamada para a função execute de uma wallet podem ser substituídas. Se um paymaster anteriormente queria patrocinar apenas usuários de sua wallet, e apenas quando eles mintassem um NFT específico, essas regras poderiam ser contornadas.

Para contornar as regras, o sender mudaria userOp.initCode e userOp.callData depois de obter uma assinatura. Então, o token nativo do paymaster (ETH ou outro) seria usado para um propósito diferente da intenção original de um mint de NFT sem custo de gas.

Signatários off-chain que recebem user operations para assinar em um formato codificado em ABI, ou signatários que têm integrações de contrato para preparar dados para assinatura, são vulneráveis. Esse é um escopo limitado, já que essencialmente estão "explorando a si mesmos", mas isso apresenta um risco para a operação de um serviço de paymaster.

Medidas defensivas contra isso incluem implantar uma versão atualizada de VerifyingPaymaster, ou lidar com o processo de codificação ABI eles mesmos a partir da entrada do usuário.

Conclusão

Após diversas conversas excelentes com membros do ecossistema, @drortirosh fez o merge de um patch otimizado e legível para o contrato Entrypoint para resolver essa vulnerabilidade. Uma vez reimplantados, os contratos Entrypoint não apresentarão mais o comportamento documentado acima.

Além disso, há uma proposta para abstrair o suporte a nonce no Entrypoint, que também reforça esse sistema. Dado que o risco para os paymasters é limitado, nenhum patch oficial upstream foi feito, e os operadores de paymaster podem decidir como lidar com isso conforme necessário para suas implementações.

Queremos agradecer à comunidade do 4337 aqui, incluindo @drortirosh, @Gooong, @taylorjdawson, @leekt, e @livingrockrises, por trabalharem conosco nessa vulnerabilidade.

Tem alguma dúvida ou tópico que gostaria de discutir?

Entre em contato com a gente em account-abstraction@alchemy.com.

Newsletter da Alchemy

Seja o primeiro a saber sobre lançamentos

Assine nossa newsletter

Receba as últimas atualizações de produtos e recursos da Alchemy

A
O
D
+
Mais de 80.000 inscritos

Ao inserir seu endereço de e-mail, você concorda em receber nossas comunicações de marketing e atualizações de produtos. Você reconhece que a Alchemy processa as informações que recebemos de acordo com nosso Aviso de Privacidade. Você pode cancelar a inscrição a qualquer momento.