Back to Blog

#10 Incidente Panóptico: La Linealidad XOR Rompe el Esquema de Huella Digital de Posición

Code Auditing
February 13, 2026
9 min read

El 25 de agosto de 2025, con la asistencia de Cantina y Seal911, Panoptic llevó a cabo una operación de rescate de sombrero blanco, asegurando aproximadamente $400K en fondos en riesgo [1]. La causa raíz fue una falla en la construcción de s_positionsHash: el protocolo utilizó XOR para agregar hashes Keccak256 de IDs de posición en una única huella digital. Si bien los hashes Keccak256 individuales siguen siendo resistentes a colisiones, la linealidad matemática del XOR hace que la huella digital compuesta sea insegura. Un atacante puede generar un conjunto de IDs de posición falsificados cuyo hash agregado por XOR coincida con cualquier huella digital objetivo, eludiendo la verificación de posiciones del protocolo y retirando colateral sin reembolsar la deuda.

Antecedentes

Panoptic es un protocolo descentralizado de trading de opciones perpetuas construido sobre Ethereum que permite a los usuarios operar con opciones de compra y venta.

La función withdraw() tiene un parámetro de entrada positionList, que consiste en un conjunto de tokenIds. Cada tokenId representa una posición. La función withdraw() verifica el estado de deuda de cada posición basándose en s_positionsHash y luego recupera el colateral del usuario.

Para ahorrar gas, el protocolo no almacena cada posición (es decir, tokenId) del usuario. En cambio, calcula una huella digital basada en todos los tokenIds del usuario y la registra en s_positionsHash. Los 8 bits más significativos de cada s_positionsHash representan numLegs, mientras que los 248 bits inferiores representan el user position hash.

Para cada tokenId recibido, el protocolo calcula su hash, toma los 248 bits inferiores y actualiza el user position hash realizando un XOR bit a bit con los 248 bits inferiores del s_positionsHash actual.

Simultáneamente, countLegs se ajusta según el rango numérico del tokenId: permanece sin cambios cuando el tokenId es inferior a 2642^{64}, y aumenta en 1, 2 o 3 cuando está en los rangos (264,2112)(2^{64}, 2^{112}), (2112,2168)(2^{112}, 2^{168}) y (2168,2208)(2^{168}, 2^{208}) respectivamente. Esto finalmente se escribe en los 8 bits superiores de s_positionsHash para actualizar numLegs.

Análisis de Vulnerabilidad

La causa raíz es una falla en el algoritmo del contrato para construir s_positionsHash, específicamente el uso de la operación XOR para agregar resultados de hashes Keccak256. Si bien una función hash individual sigue siendo segura, la linealidad matemática de la operación XOR hace que el algoritmo general de huella digital (es decir, la suma XOR de hashes) sea inseguro [2].

La linealidad implica que un atacante no necesita romper la función hash en sí misma (es decir, revertir el tokenId desde el hash). En cambio, puede emplear una estrategia combinatoria: generar una gran cantidad de tokenIds aleatorios, calcular su Keccak256(tokenId), y a partir de estos valores de hash, seleccionar un subconjunto específico tal que la suma XOR de estos hashes de tokenId coincida exactamente con la huella digital objetivo de la víctima.

Esto permite a un atacante pasar un conjunto de tokenIds falsificados al llamar a withdraw() para superar la verificación de salud y recuperar todo el colateral correspondiente a s_positionsHash.

Teoría

Supongamos que la huella digital de posición del usuario s_positionsHash tiene un user position hash de TT y numLegs de kk, con tokenIds de usuario {t1,t2,,tn}\{t_1, t_2, \dots, t_n\}. Por lo tanto:

i=1k[Hash(ti)(mod2248)]=T\bigoplus_{i=1}^{k} [\text{Hash}(t_i) \pmod{2^{248}}] = T

Aquí, cada valor hash de 248 bits puede verse como un vector de 248 dimensiones.

Hash(ti)(mod2248)=[b0,b1,,b247]T,where bi{0,1}\text{Hash}(t_i) \pmod{2^{248}} = [b_0, b_1, \dots, b_{247}]^T, where\ b_i \in \{0, 1\}

Por lo tanto, TT reside en un espacio de 248 dimensiones compuesto por 0s y 1s (F2248\mathbb{F}_2^{248}). En este espacio, la operación XOR es equivalente a la suma vectorial (Apéndice I).

El objetivo del atacante es encontrar nn vectores de 248 dimensiones {v1,v2,,vn}\{v_1, v_2, \dots, v_n\} del conjunto de todos los vectores disponibles, tal que los 248 bits inferiores de su suma XOR sean iguales a TT. Por lo tanto, podemos formular el objetivo del atacante como un sistema de ecuaciones lineales:

x1v1+x2v2++xnvn=Tx_1 v_1 + x_2 v_2 + \dots + x_n v_n = T

Específicamente, no necesitamos intentar construir TT directamente. En cambio, seleccionamos nn vectores hash linealmente independientes {v1,v2,,vn}\{v_1, v_2, \dots, v_n\} (donde nn es igual a la dimensión 248) y los usamos como vectores columna para construir una matriz n×nn \times n AA:

A=[v1,v2,,vn]A = [v_1, v_2, \dots, v_n]

El problema se transforma entonces en encontrar un vector de coeficientes xx tal que:

Ax=TA \cdot x = T

Donde x=[x1,x2,,xn]Tx = [x_1, x_2, \dots, x_n]^T, y xi{0,1}x_i \in \{0, 1\}.

Según la teoría del álgebra lineal, siempre que la matriz AA tenga rango completo, abarca todo el espacio de nn dimensiones. Esto significa que para cualquier objetivo TT, el sistema de ecuaciones tiene una solución única. Después de resolver xx, simplemente conservamos aquellos vectores viv_i para los cuales xi=1x_i=1; su suma XOR es TT.

Ejemplo

Para facilitar la comprensión, demostramos este proceso de construcción con un caso de estudio. Supongamos que los vectores son de 3 dimensiones, con cada dimensión compuesta únicamente por 0 o 1, y el valor hash objetivo es 101, es decir, T=[1,0,1]TT = [1, 0, 1]^T.

A continuación, generamos aleatoriamente 3 valores hash:

  • v1=[1,1,0]Tv_1 = [1, 1, 0]^T
  • v2=[0,1,0]Tv_2 = [0, 1, 0]^T
  • v3=[0,1,1]Tv_3 = [0, 1, 1]^T

Construimos la matriz AA usando estos tres vectores como columnas y planteamos la ecuación Ax=TAx=T:

[100111001][x1x2x3]=[101]\begin{bmatrix} 1 & 0 & 0 \\ 1 & 1 & 1 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x_1 \\ x_2 \\ x_3 \end{bmatrix} = \begin{bmatrix} 1 \\ 0 \\ 1 \end{bmatrix}

Resolviendo mediante eliminación gaussiana, obtenemos x=[1,0,1]Tx = [1, 0, 1]^T. Esto significa que debemos seleccionar v1v_1 y v3v_3 (correspondientes a x1=1,x3=1x_1=1, x_3=1), y su suma XOR es exactamente igual al objetivo TT.

Selección de n Vectores Linealmente Independientes

Como se mencionó anteriormente, para resolver Ax=TAx=T, necesitamos construir una matriz AA de rango completo. Dado que estamos trabajando con un espacio vectorial extremadamente grande (m=2248m = 2^{248}), la pregunta central es: ¿Cómo seleccionamos rápidamente nn vectores linealmente independientes de este vasto espacio?

Consideremos el método más sencillo: generar aleatoriamente nn tokenIds y seleccionar sus resultados de hash como vectores.

Sobre F2\mathbb{F}_2, la probabilidad PP de que nn vectores de nn dimensiones seleccionados aleatoriamente formen una matriz de rango completo es:

P(n)=k=0n1(12k2n)P(n) = \prod_{k=0}^{n-1} \left(1 - \frac{2^k}{2^n}\right)

Cuando nn es grande (en este ejemplo, n=248n=248), esta probabilidad converge a una constante (Apéndice II):

limnP(n)0.28879\lim_{n \to \infty} P(n) \approx 0.28879

Esto significa que cada intento aleatorio tiene aproximadamente un 28,9% de probabilidad de éxito. En promedio, un atacante solo necesita aproximadamente 3,5 intentos para encontrar un conjunto de vectores linealmente independientes. Por lo tanto, el costo computacional es extremadamente bajo, y el atacante puede construir rápidamente una matriz AA que satisfaga las condiciones.

Para controlar el countLegs en los 8 bits superiores, solo necesitamos ajustar el rango de los valores tokenId generados aleatoriamente según el countLegs objetivo.

Por ejemplo, si queremos que el numLegs en la huella digital falsificada sea 0, simplemente nos aseguramos de que los nn tokenIds generados aleatoriamente sean todos menores que 2642^{64}. Dado que el incremento de legs para tokenIds en este rango es 0, independientemente de qué vectores seleccione la solución xx de la eliminación gaussiana, el numLegs acumulado final será inevitablemente 0.

Análisis del Ataque

El rescatador de sombrero blanco inició múltiples transacciones de rescate. Por simplicidad, la siguiente discusión se basa en solo una de estas transacciones [3].

La lógica central consta de los siguientes 5 pasos:

  1. Tomar prestados 0,23e8 WBTC y 28e18 WETH mediante préstamo flash de Aave.
  2. Depositar 0,23e8 WBTC y 28e18 WETH en el contrato poWBTC y el contrato 0x1f8d_poWETH.
  3. Llamar a mintOptions() del contrato PanopticPool con un positionIdList normal para tomar fondos prestados y abrir una posición apalancada.
  4. Llamar a withdraw(), pasando los tokenIds falsificados. Debido a que estas posiciones falsificadas tienen un positionSize de 0, la función devuelve tokenRequired como 0, lo que significa que el colateral total requerido para todas las posiciones se calcula erróneamente como cero. Mientras tanto, el s_positionsHash generado por estos tokenIds es exactamente igual al generado en el paso 3, lo que permite al rescatador recuperar todo el colateral sin reembolsar ninguna deuda.
  5. Reembolsar el préstamo flash y ejecutar la siguiente ronda.

Resumen

Este incidente pone de relieve dos fallas combinadas que juntas permitieron el retiro no autorizado de colateral.

  • XOR no es una función de agregación segura para hashes. Keccak256 es resistente a colisiones, pero la linealidad del XOR significa que la suma XOR de múltiples hashes no hereda esa propiedad. Construir un conjunto de entradas cuyos hashes hagan XOR a cualquier valor objetivo se reduce a resolver un sistema de ecuaciones lineales sobre F2\mathbb{F}_2, lo cual es computacionalmente trivial. La composición de hashes requiere operaciones que preserven la resistencia a colisiones, como la concatenación seguida de un nuevo hash.
  • Falta de validación de IDs de posición. El protocolo no verificó si los positionIds pasados correspondían a posiciones de opciones válidas. Los valores por debajo de 2642^{64} tienen un countLegs de 0 y un positionSize de 0, lo que significa que las posiciones falsificadas no incurren en deuda. Esto permitió al atacante superar la verificación de salud con requisito de colateral cero mientras coincidía con la huella digital objetivo.

Referencias

  1. https://x.com/Panoptic_xyz/status/1961187739866644524

  2. https://cseweb.ucsd.edu/~mihir/papers/inchash.pdf

  3. https://app.blocksec.com/explorer/tx/eth/0x67a45dfe5ff4b190058674d7c791bbdc48e889f319f937c24fa13a5f9093f088

Apéndice

Los siguientes dos apéndices proporcionan una explicación matemática y demostración de las afirmaciones del texto principal, a saber, que la operación XOR es equivalente a la suma vectorial (Apéndice I) y la probabilidad de que una matriz aleatoria tenga rango completo (Apéndice II).

Apéndice I

En el campo finito F2={0,1}\mathbb{F}_2 = \{0, 1\}, la adición se define como la adición módulo 2:

0+0=00+1=11+0=11+1=0(mod2)\begin{aligned} 0 + 0 &= 0 \\ 0 + 1 &= 1 \\ 1 + 0 &= 1 \\ 1 + 1 &= 0 \pmod 2 \end{aligned}

Se puede observar que esto es idéntico a la operación lógica OR Exclusivo (XOR, símbolo \oplus).

Para el espacio vectorial de nn dimensiones F2n\mathbb{F}_2^n (en este caso n=248n=248), la adición de dos vectores u=[u1,,un]Tu = [u_1, \dots, u_n]^T y v=[v1,,vn]Tv = [v_1, \dots, v_n]^T se define como la adición módulo 2 componente a componente:

u+v=[u1+v1(mod2)un+vn(mod2)]=[u1v1unvn]=uvu + v = \begin{bmatrix} u_1 + v_1 \pmod 2 \\ \vdots \\ u_n + v_n \pmod 2 \end{bmatrix} = \begin{bmatrix} u_1 \oplus v_1 \\ \vdots \\ u_n \oplus v_n \end{bmatrix} = u \oplus v

Por lo tanto, en el espacio vectorial F2n\mathbb{F}_2^{n}, la suma vectorial es equivalente a la operación XOR bit a bit sobre los componentes del vector.

Apéndice II

Para que la matriz tenga rango completo, estos nn vectores deben ser linealmente independientes.

El primer vector v1v_1 puede ser cualquier vector en F2n\mathbb{F}_2^n excepto el vector cero, lo que proporciona 2n12^n - 1 opciones; el segundo vector v2v_2 no debe residir en el subespacio generado por {v1}\{v_1\}, que contiene 212^1 vectores, dejando 2n22^n - 2 opciones; siguiendo esta lógica, el kk-ésimo vector vkv_k no puede estar en el subespacio generado por los k1k-1 vectores anteriores, resultando en 2n2k12^n - 2^{k-1} opciones posibles.

El número total de tales matrices (es decir, el orden de GL(n,F2)GL(n, \mathbb{F}_2)) es:

N=k=0n1(2n2k)=(2n1)(2n2)(2n4)(2n2n1)N = \prod_{k=0}^{n-1} (2^n - 2^k) = (2^n - 1)(2^n - 2)(2^n - 4)\cdots(2^n - 2^{n-1})

Y el número total de todas las posibles matrices n×nn \times n es (2n)n=2n2(2^n)^n = 2^{n^2}.

Por lo tanto, la probabilidad PP es:

P=k=0n1(2n2k)2n2=k=0n1(2n2k2n)=k=0n1(12k2n)P = \frac{\prod_{k=0}^{n-1} (2^n - 2^k)}{2^{n^2}} = \prod_{k=0}^{n-1} \left( \frac{2^n - 2^k}{2^n} \right) = \prod_{k=0}^{n-1} (1 - \frac{2^k}{2^n})

Haciendo j=nkj = n - k, podemos reescribir esta expresión como:

P=j=1n(112j)P = \prod_{j=1}^{n} \left(1 - \frac{1}{2^j}\right)

Cuando nn \to \infty, este producto converge a una constante:

P0.288788P \approx 0.288788

Esto significa que para nn grande, la probabilidad de que una matriz aleatoria tenga rango completo es aproximadamente 28,9%.


Acerca de BlockSec

BlockSec es un proveedor integral de seguridad blockchain y cumplimiento de criptomonedas. Desarrollamos productos y servicios que ayudan a los clientes a realizar auditorías de código (incluyendo contratos inteligentes, blockchain y billeteras), interceptar ataques en tiempo real, analizar incidentes, rastrear fondos ilícitos y cumplir con las obligaciones AML/CFT, a lo largo de todo el ciclo de vida de los protocolos y plataformas.

BlockSec ha publicado múltiples artículos de seguridad blockchain en conferencias prestigiosas, reportado varios ataques de día cero en aplicaciones DeFi, bloqueado múltiples hackeos para rescatar más de 20 millones de dólares, y asegurado miles de millones en criptomonedas.

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit