Firma ciega: qué estás aprobando en realidad cuando la pantalla no dice nada útil

·6 min de lectura·Por SSP Editorial Team
Portada de SSP Academy: firma ciega y aprobaciones de transacción ilegibles

Firma ciega: qué estás aprobando en realidad cuando la pantalla no dice nada útil

Hay un momento concreto en el que ocurren la mayoría de las grandes pérdidas cripto. No es cuando roban una clave. Es cuando alguien mira una petición de firma que no entiende, decide que probablemente no pasa nada, y pulsa aprobar.

El sector tiene un nombre para esto: firma ciega. Significa poner tu firma sobre datos que no puedes leer. Y durante años los monederos lo trataron como algo normal.

Qué se supone que hace una pantalla de firma

La promesa de seguridad de la autocustodia descansa sobre un supuesto: que nada se mueve sin tu aprobación. El multifirma 2 de 2, los monederos de hardware, los firmantes aislados: todos ellos son máquinas para convertir tu intención en una firma.

Esa maquinaria vale exactamente lo que vale tu comprensión de aquello que aprobaste.

Si la pantalla dice «Envía 0,5 ETH a 0x8f3C…», tu aprobación significa algo. La comparaste con lo que pretendías hacer. Si la pantalla dice «Interacción con contrato — datos: 0xa22cb465000000000000000000000000d8dA6…0001», tu aprobación no significa absolutamente nada. No estás consintiendo una transacción; estás consintiendo un rectángulo de hexadecimal.

Los atacantes saben cuál de esas dos pantallas necesitan que veas.

Qué se esconde en los bytes

La carga útil de una transacción EVM son los calldata: un selector de función de cuatro bytes seguido de argumentos codificados en ABI. Es perfectamente legible para una máquina y perfectamente opaco para una persona. Un puñado de esos selectores es por donde se va el dinero.

approve(address,uint256) — selector 095ea7b3. Concede a un contrato permiso para gastar tus tokens. No es una transferencia; es una autorización permanente. No mueve nada cuando la firmas, y por eso justamente se cuela. El vaciado ocurre después, en el calendario del atacante.

setApprovalForAll(address,bool) — selector a22cb465. El equivalente para NFT, y peor: una sola firma otorga a un operador autoridad sobre todos los tokens de esa colección que tengas, ahora y en el futuro. No hay ningún campo de cantidad que te tranquilice.

increaseAllowance(address,uint256) — selector 39509351. Recarga una asignación existente. A menudo pasa desapercibido porque no es el selector que a todo el mundo le dijeron que vigilara.

transferFrom(address,address,uint256) — selector 23b872dd. Mueve tokens desde una dirección que ya concedió una asignación. Es la firma que finalmente gasta lo que un approve anterior autorizó.

El patrón que conviene interiorizar: la transacción que te roba el dinero casi nunca es la transacción que firmaste. Firmaste un permiso. El robo es una transacción distinta, enviada después, que tú no llegas a ver.

Por qué «ilimitado» es la palabra que hay que buscar

Casi todos los abusos de asignaciones comparten un rasgo: la cantidad es infinita en la práctica.

Las dapps piden aprobaciones ilimitadas porque resulta cómodo: apruebas una vez y no vuelve a preguntarte. El resultado es que los usuarios quedan entrenados para conceder derechos de gasto sin límite de forma rutinaria, y una interfaz que muestra 115792089237316195423570985008687907853269984665640564039457584007913129639935 no le está diciendo nada a nadie.

Hay un matiz que pilla a las implementaciones ingenuas. El valor canónico de «aprobación infinita» es exactamente 2²⁵⁶−1, así que la comprobación obvia es compararlo con esa constante. Pero las dapps usan habitualmente otros números astronómicamente grandes —la mitad del máximo, 0xff…f0, 10¹⁸ × 10³⁸— que son ilimitados en cualquier sentido práctico y sin embargo fallan una prueba de igualdad exacta. Un aviso que solo salta con el valor centinela exacto es un aviso que un atacante puede esquivar restando uno.

El umbral correcto está bien por debajo del centinela y bien por encima de cualquier cosa real. SSP marca cualquier asignación igual o superior a 2²⁵⁵ —unos 5,8 × 10⁷⁶, que supera cualquier suministro ERC-20 posible por decenas de órdenes de magnitud—. Nada legítimo queda mal marcado nunca, y afinar un valor justo por debajo del máximo no esquiva el aviso.

Ese umbral se aplica solo a las llamadas que conceden asignaciones. En una transferencia normal, «ilimitado» no es una idea con sentido —estás moviendo una cantidad concreta—, así que allí se deja en paz el centinela del máximo exacto.

Qué hace SSP

SSP descodifica los calldata a lenguaje llano en la pantalla de aprobación. Los selectores reconocidos se representan por lo que realmente son: quién es la contraparte, cuál es la cantidad, si los derechos que se conceden son ilimitados. El hexadecimal en bruto ya no es el contenido principal: vive detrás de una sección Avanzado, para quien lo quiera.

Tres decisiones de diseño importan más que la descodificación en sí.

El descodificador es solo presentación. Nunca cambia lo que se firma. La aprobación siempre firma la carga original exacta; el ayudante únicamente vuelve a presentar bytes que antes se mostraban como hexadecimal en bruto. Un descodificador capaz de alterar la carga sería una nueva superficie de ataque en lugar de una defensa: lo que lees y lo que firmas tienen que ser los mismos bytes, siempre.

Falla cerrado. Selector desconocido, longitud errónea, hexadecimal malformado, relleno de dirección no estándar: cualquier cosa inesperada no devuelve nada, y la interfaz recae en una acción genérica con el hexadecimal en bruto en Avanzado. Nunca adivina. Un descodificador que adivina es peor que ninguno, porque un resumen equivocado y seguro de sí mismo es más peligroso que el hexadecimal visible: el hexadecimal al menos te dice honestamente que no lo entiendes.

Ese instinto de fallar cerrado va más hondo que las funciones desconocidas. Una dirección codificada en ABI son doce bytes a cero seguidos de veinte bytes de dirección; una palabra con cualquier otra cosa en esos bytes iniciales no está codificada canónicamente, y SSP la trata como sospechosa en vez de intentar interpretarla. Lo mismo con los booleanos: solo se aceptan las codificaciones canónicas de todo ceros (falso) y 31 ceros seguidos de 0x01 (verdadero). Las codificaciones no canónicas en una pantalla que concede derechos de gasto son una señal de alarma, no un reto de análisis sintáctico.

Los símbolos y decimales de token nunca se adivinan. Solo se muestra una cantidad legible cuando el token se conoce con seguridad desde el registro que hay en el dispositivo. En caso contrario, obtienes el número en bruto en unidades base. Esto es deliberadamente menos bonito: mostrar «5,0 USDC» para un contrato que simplemente se hace llamar USDC convertiría el descodificador en una máquina de mentir, que es exactamente el resultado que quiere un atacante.

Y luego está la parte que es estructural, no cosmética. En SSP la transacción se construye en un sitio y se aprueba en otro: se compone en la extensión del navegador, y se descodifica y se muestra de forma independiente en tu teléfono, donde SSP Key vuelve a calcular el hash de la transacción en el propio dispositivo y se niega a firmar si no coincide con lo que se enseñó. La firma ciega en un único dispositivo significa que basta con una pantalla comprometida. Aquí, la pantalla que te muestra la acción descodificada y el dispositivo que guarda la segunda clave son el mismo dispositivo, y ese dispositivo verifica en lugar de confiar.

Qué hacer con tus propias aprobaciones

Trata approve y setApprovalForAll como las peligrosas. Son las firmas que le cuestan dinero a la gente, y parecen inofensivas precisamente porque no mueven nada.

Rechaza las aprobaciones ilimitadas cuando puedas. Muchas dapps aceptan una cantidad acotada si la fijas tú. Es fricción, y limita tu exposición a lo que de verdad pretendías gastar.

Audita lo que ya has concedido. Las aprobaciones viejas no caducan. Un permiso que le diste a un protocolo hace dos años sigue vivo, y si ese contrato se ve comprometido más adelante, es un camino abierto hacia tus tokens. Revocar aprobaciones es rápido, y es la hora de mantenimiento más rentable de la autocustodia.

Cuando la pantalla no te dice nada, esa es la señal. Si tu monedero no puede decir qué hace una transacción, eso es información, no un inconveniente que atravesar a clics. La respuesta correcta ante una petición ilegible es parar, no entrecerrar los ojos.

El objetivo nunca fue que leyeras hexadecimal. Es asegurarse de que, cuando apruebas algo, tú y tu monedero coincidís en qué era.

Comparte este artículo

Artículos relacionados