Saltar al contenido
0%

Vulnerabilidad de empaquetado de UserOperation en ERC-4337

Autor: Adam Egyed

Última actualización: 21 de marzo de 20237 min de lectura

Colaboradores adicionales: fangting@alchemy.com, @drortirosh, @Gooong, @taylorjdawson, @leekt, @livingrockrises

Resumen

El 7 de marzo de 2023, Alchemy y otros miembros de la comunidad de desarrollo de código abierto, incluyendo @Gooong, @taylorjdawson, @leekt y @livingrockrises, identificaron problemas de decodificación de calldata en el contrato EntryPoint de ERC-4337 y en el contrato de ejemplo VerifyingPaymaster.

Estos contratos están actualmente desplegados en varias chains y generan hashes sobre las user operations. La implementación producía hashes inconsistentes según el método de firma usado, lo que puede derivar en varios efectos de segundo orden, como hashes divergentes para las mismas UserOperations y hashes colisionantes para UserOperations distintas.

La discusión fue facilitada por @drortirosh y está documentada en este issue de Github.

Análisis detallado

A continuación se presenta un análisis del código afectado, explicaciones de la vulnerabilidad de empaquetado del EntryPoint, la vulnerabilidad de empaquetado del VerifyingPaymaster y su respectivo impacto.

Código afectado

El segmento de código en cuestión es el siguiente:

‍UserOperation.sol:61-75

VerifyingPaymaster.sol:35-49

Como contexto, el struct UserOperation se define así:

Ambos segmentos de código usan assembly para copiar una porción grande del calldata a memoria, con la intención de capturar parte de una user operation para hashearla.

El método pack en UserOperationLib pretende capturar todos los campos de la user operation desde sender hasta maxPriorityFeePerGas, incluyendo los campos de tamaño variable (llamados campos dinámicos en la codificación ABI) initCode, callData y paymasterAndData.

El método pack en VerifyingPaymaster incluye todos esos campos excepto el campo paymasterAndData, ya que este aún no está definido.

Para implementar esto, ambos métodos usan un campo de conveniencia en Yul provisto a los tipos dinámicos en calldata, llamado .offset. Esto se refiere al valor provisto en la codificación ABI de un struct, definido aquí en la especificación de Solidity. (En realidad se refiere a la palabra de memoria posterior al offset, pero eso es solo por conveniencia al momento de cargarlo).

Un codificador ABI estándar codificará los valores de los campos dinámicos (llamados su "tail" en el codificador ABI) en el orden en que aparecen.

Consideremos la siguiente codificación de una user operation en calldata que podría generarse:

💡 Nota: Este ejemplo muestra una user operation donde todos los campos dinámicos tienen una longitud menor a una palabra, por brevedad.

💡‍ *‍*Nota: El espacio de direcciones de memoria aquí es interno al struct de la user operation. En calldata real, estará ubicado en otro lugar debido al espacio ocupado por el selector de método y la tupla de argumentos.

En este ejemplo, seguir pnd.offset para generar un empaquetado de la user operation dará como resultado este "slice" de calldata:

¡Esto contiene exactamente lo que queremos!

Sin embargo, los contratos que usan argumentos codificados en ABI no validan en qué orden se definen los campos, ni siquiera que los offsets sean válidos.

Usar signature.offset o pnd.offset leerá el valor de "offset" correspondiente directamente desde calldata.

Usando ese valor como límite, es posible construir representaciones válidas de user operations en calldata que tienen propiedades de hash inusuales.

Exploremos cómo esto afecta al EntryPoint y al VerifyingPaymaster de forma independiente.

Vulnerabilidad de empaquetado del EntryPoint

Para demostrar esta vulnerabilidad, debemos considerar un contrato de wallet distinto del SimpleAccount.sol provisto, ya que ese ejemplo reutiliza el código vulnerable de EntryPoint.

La divergencia de hashes se vuelve relevante cuando se usa un esquema de hasheo diferente entre el EntryPoint y el contrato de wallet, o si la wallet firma una codificación de user operation no estándar.

El riesgo que esto introduce a EntryPoint es que una sola user operation puede estar representada por múltiples "user op hashes" y que un mismo "user op hash" puede representar múltiples user operations.

Consideremos esta cuenta, llamada ExampleAccount, que tiene su propio ExampleAccountFactory. La cuenta de ejemplo usa un único firmante para validar user operations.

Para otorgar permiso de ejecutar una user operation, se genera y firma un hash sobre todos los campos de la user operation, excepto la firma misma.

El método validateUserOp se define así:

Esta es una implementación relativamente simple de validación de firmas, ya que verifica todos los campos de userOp, junto con la dirección del entrypoint y el chain id.

Como uno de los objetivos de la account abstraction, el método validateUserOp puede contener lógica arbitraria (aunque limitada por restricciones de acceso a storage), ya que este método representa las condiciones bajo las cuales puede originarse una user operation.

Para esta cuenta de ejemplo, las user operations parten de una firma del owner. Más en general, sin embargo, las user operations pueden originarse a partir de condiciones arbitrarias: estado on-chain, múltiples firmas o firmas específicas de la app; es una característica de la account abstraction.

Para demostrar esta vulnerabilidad, construyamos calldata malicioso hacia EntryPoint.handleOps de modo que el UserOperationEvent emitido por EntryPoint tenga un valor inesperado.

Tras definir un struct de ejemplo UserOperation memory uo, así podemos construir el calldata:

rightPadBytes es una función de ayuda escrita para alinear los tipos bytes a la longitud de palabra completa más cercana.

Se define así:

Ahora, al llamar a handleOps, el evento emitido y el resultado de EntryPoint.getUserOpHash\(\) serán diferentes.

Impacto

Bundlers maliciosos, o EOAs sin rol de bundler que llamen a EntryPoint.handleOps, pueden modificar su representación de un UserOp en calldata para cambiar el hash de UO en los eventos emitidos. Esto puede romper sistemas off-chain que integran con los eventos emitidos, ya que ahora queda expuesto que los eventos no son determinísticos para una UO dada.

Adicionalmente, el bundler deberá lidiar con no-determinismo al leer los userOpHashes emitidos desde el contrato EntryPoint. Para ver si un UserOperationEvent emitido desde el EntryPoint corresponde a una user operation en el mempool local del bundler, ya no basta con comparar el valor de hash, ya que el calldata hacia handleOps puede modificarse para cambiar los valores de hash.

En su lugar, los bundlers tendrán que buscar recibos de transacción, obtener el calldata enviado a handleOps, decodificar el calldata y luego obtener los hashes "canónicos" recodificando mediante el codificador ABI estándar y llamando a EntryPoint.getUserOpHash\(...\). Esto es necesario para determinar si las user operations en el mempool local ya fueron minadas o no. Además, dado que las llamadas a EntryPoint.handleOps pueden ocurrir desde dentro de otras llamadas a contratos, la decodificación puede estar profunda en la pila de llamadas.

Esta divergencia también afectará la implementación de los métodos RPC del bundler, ya que se usa un user op hash para identificación en eth\_getUserOperationByHash y eth\_getUserOperationReceipt.

Los bundlers necesitarán realizar búsquedas, parseo y decodificación costosos del calldata hacia EntryPoint.handleOps\(...\) para traducir los hashes emitidos por los eventos en hashes "canónicos" desde EntryPoint.getUserOpHash\(...\).

💡 Nota: Esta vulnerabilidad es distinta del hecho de que SCWs maliciosas puedan reutilizar user operations. Las user operations reutilizadas, y en general todas las user operations, deberían tener un hash determinístico. Otras aplicaciones y servicios construidos sobre ERC-4337 tendrán que implementar su propia mitigación a menos que esto se resuelva.

Dado que ERC-4337 se encuentra en etapas tempranas de adopción en general, es difícil describir el impacto potencial de esta vulnerabilidad en el ecosistema más amplio. El alcance del impacto depende de las implementaciones de bundlers, exploradores de user operations, indexadores y otros servicios off-chain.

Como mínimo, causaría una experiencia de usuario confusa, ya que el user operation hash (similar al hash de transacción) puede cambiar entre el momento de envío y el de inclusión, por lo que algunas wallets podrían no contemplar esa diferencia y fallar al mostrar actualizaciones a sus usuarios.

En un caso de riesgo medio, las wallets pueden diseñarse de modo que eviten intencionalmente la indexación configurando todos sus user op hashes para que sean iguales (ver el ejemplo provisto por @leekt).

En un caso de riesgo alto, un servicio off-chain que monitorea la inclusión de user operations podría no detectar la inclusión de una determinada user operation e intentar reenviarla o manejar mal datos y claves.

Prueba de concepto

Vea la prueba de concepto completa en este repositorio.

Vulnerabilidad de empaquetado del VerifyingPaymaster

Los riesgos que esto introduce a VerifyingPaymaster son que una user operation puede contener un contenido diferente entre el momento de la firma y su inclusión on-chain. Esto puede ocurrir cuando dos user operations distintas devuelven el mismo hash desde VerifyingPaymaster.getHash\(UserOperation userOp, uint48 validUntil, uint48 validAfter\).

Construyamos calldata para esta función para mostrar cómo puede darse este caso:

Calldata 1

Note cómo esta codificación cambia el orden de los campos dinámicos, pero por lo demás sigue siendo válida: los offsets apuntan a las ubicaciones correctas y las longitudes son todas válidas. Pero, dado que el offset de paymasterAndData tiene un valor inesperado, el slice que obtenemos de pack\(\) será el siguiente:

¡Note cómo initCode y callData quedan excluidos del slice!

Construyamos un segundo calldata de entrada, esta vez modificando maliciosamente ambos campos:

  • initCode pasará de 1517c0de a 1517c0de02
  • callData pasará de ca11dada1 a ca11data02

Esto nos da lo siguiente:

Calldata 2

Si realizamos la misma operación pack sobre este calldata diferente, ¡se obtendrá el mismo slice que antes! Y podemos verificar que los hashes son iguales con lo siguiente:

Al ejecutar esto en un entorno de pruebas en foundry se revela que ambas user ops tienen el mismo hash: 0x736f86d224bab46a95ae119947e172efa694379d9ac682d4ca780b7640a89b06

Vea este archivo de pruebas para la POC completa.

Impacto

En esta vulnerabilidad, el hash puede modificarse para cubrir menos elementos de los esperados, permitiendo que initCode, callData y posiblemente otros campos estáticos queden excluidos del hash, y por lo tanto varíen entre la firma y el uso. Esto puede resultar en que firmas de sponsorship del paymaster se usen para propósitos distintos de los previstos.

Por ejemplo, el factory de despliegue del contrato de la wallet y la llamada a la función execute de una wallet pueden ser sustituidos. Si un paymaster previamente quería patrocinar solo a usuarios de su wallet, y solo patrocinar cuando acuñaran un NFT específico, esas reglas podrían ser eludidas.

Para eludir las reglas, el sender cambiaría userOp.initCode y userOp.callData después de obtener una firma. Luego, el token nativo del paymaster (ETH u otro) se usaría para un propósito distinto al de su intención original de un mint de NFT sin gas.

Los firmantes off-chain que reciben user operations para firmar en un formato codificado en ABI, o firmantes que tienen integraciones con contratos para preparar datos para firma, son vulnerables. Este es un alcance limitado, ya que esencialmente "se explotan a sí mismos", pero representa un riesgo para operar un servicio de paymaster.

Las medidas defensivas contra esto incluyen desplegar una versión actualizada de VerifyingPaymaster, o manejar ellos mismos el proceso de codificación ABI a partir de la entrada del usuario.

Conclusión

Tras varias conversaciones excelentes con miembros del ecosistema, @drortirosh fusionó un parche optimizado y legible para el contrato Entrypoint para resolver esta vulnerabilidad. Una vez redesplegados, los contratos Entrypoint ya no presentarán el comportamiento documentado arriba.

Adicionalmente, existe una propuesta para abstraer el soporte de nonce en el Entrypoint que también refuerza este sistema. Dado que el riesgo para los paymasters es limitado, no se ha realizado un parche oficial upstream, y los operadores de paymasters pueden decidir cómo manejar esto según lo necesiten para sus implementaciones.

Queremos agradecer a la comunidad de 4337 aquí, incluyendo a @drortirosh,** @Gooong**,** @taylorjdawson**,** @leekt** y** @livingrockrises** por trabajar en esta vulnerabilidad junto con nosotros.

¿Tiene alguna pregunta o algún tema que quiera discutir?

Contáctenos en account-abstraction@alchemy.com.

Boletín de Alchemy

Entérate primero de los lanzamientos

Suscríbete a nuestro boletín

Recibe las últimas actualizaciones de productos y recursos de Alchemy

A
O
D
+
Más de 80,000 suscriptores

Al ingresar tu correo electrónico, aceptas recibir nuestras comunicaciones de marketing y actualizaciones de productos. Reconoces que Alchemy procesa la información que recibimos de acuerdo con nuestro Aviso de Privacidad. Puedes cancelar tu suscripción en cualquier momento.