Back to Blog

#10 Incidente Panóptico: A Linearidade XOR Quebra o Esquema de Impressão Digital de Posição

Code Auditing
February 13, 2026
9 min read

Em 25 de agosto de 2025, com a assistência de Cantina e Seal911, a Panoptic conduziu uma operação de resgate white hat, protegendo aproximadamente $400K em fundos em risco [1]. A causa raiz foi uma falha na construção do s_positionsHash: o protocolo usava XOR para agregar hashes Keccak256 de IDs de posição em uma única impressão digital. Embora os hashes Keccak256 individuais permaneçam resistentes a colisões, a linearidade matemática do XOR torna a impressão digital composta insegura. Um atacante pode gerar um conjunto de IDs de posição forjados cujo hash agregado por XOR corresponde a qualquer impressão digital alvo, contornando a verificação de posição do protocolo e retirando colateral sem pagar a dívida.

Contexto

A Panoptic é um protocolo descentralizado de negociação de opções perpétuas construído na Ethereum que permite aos usuários negociar opções de venda e compra.

A função withdraw() possui um parâmetro de entrada positionList, que consiste em um conjunto de tokenIds. Cada tokenId representa uma posição. A função withdraw() verifica o status de dívida de cada posição com base no s_positionsHash e então recupera o colateral do usuário.

Para economizar gas, o protocolo não armazena cada posição (ou seja, tokenId) do usuário. Em vez disso, ele calcula uma impressão digital com base em todos os tokenIds do usuário e a registra em s_positionsHash. Os 8 bits mais significativos de cada s_positionsHash representam numLegs, enquanto os 248 bits inferiores representam o user position hash.

Para cada tokenId passado, o protocolo calcula seu hash, pega os 248 bits inferiores e atualiza o user position hash realizando um XOR bit a bit com os 248 bits inferiores do s_positionsHash atual.

Simultaneamente, countLegs é ajustado com base no intervalo numérico do tokenId: permanece inalterado quando o tokenId é menor que 2642^{64}, e aumenta em 1, 2 ou 3 quando nos intervalos (264,2112)(2^{64}, 2^{112}), (2112,2168)(2^{112}, 2^{168}) e (2168,2208)(2^{168}, 2^{208}) respectivamente. Isso é finalmente escrito nos 8 bits superiores do s_positionsHash para atualizar numLegs.

Análise da Vulnerabilidade

A causa raiz é uma falha no algoritmo do contrato para construir o s_positionsHash, especificamente o uso da operação XOR para agregar resultados de hash Keccak256. Embora uma única função hash permaneça segura, a linearidade matemática da operação XOR torna o algoritmo de impressão digital geral (ou seja, a soma XOR de hashes) inseguro [2].

A linearidade implica que um atacante não precisa quebrar a própria função hash (ou seja, reverter o tokenId a partir do hash). Em vez disso, ele pode empregar uma estratégia combinatória: gerando um grande número de tokenIds aleatórios, calculando seu Keccak256(tokenId) e, a partir desses valores de hash, selecionando um subconjunto específico de modo que a soma XOR desses hashes de tokenId corresponda exatamente à impressão digital alvo da vítima.

Isso permite que um atacante passe um conjunto de tokenIds forjados ao chamar withdraw() para passar na verificação de saúde e recuperar todo o colateral correspondente ao s_positionsHash.

Teoria

Suponha que a impressão digital de posição do usuário s_positionsHash tenha um user position hash de TT e numLegs de kk, com tokenIds do usuário {t1,t2,,tn}\{t_1, t_2, \dots, t_n\}. Assim:

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

Aqui, cada valor de hash de 248 bits pode ser visto como um vetor de 248 dimensões.

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\}

Portanto, TT reside em um espaço de 248 dimensões composto de 0s e 1s (F2248\mathbb{F}_2^{248}). Neste espaço, a operação XOR é equivalente à adição vetorial (Apêndice I).

O objetivo do atacante é encontrar nn vetores de 248 dimensões {v1,v2,,vn}\{v_1, v_2, \dots, v_n\} dentre todos os vetores disponíveis, de modo que os 248 bits inferiores de sua soma XOR sejam iguais a TT. Assim, podemos formular o objetivo do atacante como um sistema de equações lineares:

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

Especificamente, não precisamos tentar construir TT diretamente. Em vez disso, selecionamos nn vetores de hash linearmente independentes {v1,v2,,vn}\{v_1, v_2, \dots, v_n\} (onde nn é igual à dimensão 248) e os usamos como vetores coluna para construir uma matriz n×nn \times n AA:

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

O problema então se transforma em encontrar um vetor de coeficientes xx tal que:

Ax=TA \cdot x = T

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

De acordo com a teoria da álgebra linear, desde que a matriz AA seja de posto completo, ela abrange todo o espaço nn-dimensional. Isso significa que para qualquer alvo TT, o sistema de equações possui uma solução única. Após resolver xx, simplesmente retemos os vetores viv_i para os quais xi=1x_i=1; sua soma XOR é TT.

Exemplo

Para facilitar a compreensão, demonstramos esse processo de construção com um estudo de caso. Suponha que os vetores sejam tridimensionais, com cada dimensão consistindo apenas de 0 ou 1, e o valor de hash alvo seja 101, ou seja, T=[1,0,1]TT = [1, 0, 1]^T.

Em seguida, geramos aleatoriamente 3 valores de 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

Construímos a matriz AA usando esses três vetores como colunas e configuramos a equação 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}

Resolvendo via eliminação de Gauss, obtemos x=[1,0,1]Tx = [1, 0, 1]^T. Isso significa que precisamos selecionar v1v_1 e v3v_3 (correspondentes a x1=1,x3=1x_1=1, x_3=1), e sua soma XOR é exatamente igual ao alvo TT.

Selecionando n Vetores Linearmente Independentes

Como mencionado anteriormente, para resolver Ax=TAx=T, precisamos construir uma matriz AA de posto completo. Dado que estamos lidando com um espaço vetorial extremamente grande (m=2248m = 2^{248}), a questão central é: Como selecionamos rapidamente nn vetores linearmente independentes nesse vasto espaço?

Considere o método mais simples: gerar nn tokenIds aleatoriamente e selecionar seus resultados de hash como vetores.

Sobre F2\mathbb{F}_2, a probabilidade PP de que nn vetores nn-dimensionais selecionados aleatoriamente formem uma matriz de posto completo é:

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

Quando nn é grande (neste exemplo, n=248n=248), essa probabilidade converge para uma constante (Apêndice II):

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

Isso significa que cada tentativa aleatória tem aproximadamente 28,9% de probabilidade de sucesso. Em média, um atacante precisa de apenas cerca de 3,5 tentativas para encontrar um conjunto de vetores linearmente independentes. Portanto, o custo computacional é extremamente baixo, e o atacante pode rapidamente construir uma matriz AA que satisfaça as condições.

Para controlar o countLegs nos 8 bits superiores, precisamos apenas ajustar o intervalo dos valores de tokenId gerados aleatoriamente com base no countLegs alvo.

Por exemplo, se quisermos que o numLegs na impressão digital forjada seja 0, simplesmente garantimos que os nn tokenIds gerados aleatoriamente sejam todos menores que 2642^{64}. Como o incremento de leg para tokenIds nesse intervalo é 0, independentemente de quais vetores a solução xx da eliminação de Gauss selecionar, o numLegs acumulado final será inevitavelmente 0.

Análise do Ataque

O resgatador white hat iniciou múltiplas transações de resgate. Por simplicidade, a discussão a seguir é baseada em apenas uma dessas transações [3].

A lógica central consiste nas seguintes 5 etapas:

  1. Tomar emprestado 0,23e8 WBTC e 28e18 WETH via flash loan da Aave.
  2. Depositar 0,23e8 WBTC e 28e18 WETH no contrato poWBTC e no contrato 0x1f8d_poWETH.
  3. Chamar mintOptions() do contrato PanopticPool com um positionIdList normal para tomar fundos emprestados e abrir uma posição alavancada.
  4. Chamar withdraw(), passando os tokenIds forjados. Como essas posições forjadas têm um positionSize de 0, a função retorna tokenRequired como 0, o que significa que o colateral total exigido para todas as posições é erroneamente calculado como zero. Enquanto isso, o s_positionsHash gerado por esses tokenIds é exatamente o mesmo que o gerado na etapa 3, permitindo que o resgatador recupere todo o colateral sem pagar nenhuma dívida.
  5. Pagar o flash loan e executar a próxima rodada.

Resumo

Este incidente destaca duas falhas combinadas que juntas possibilitaram a retirada não autorizada de colateral.

  • XOR não é uma função de agregação segura para hashes. O Keccak256 é resistente a colisões, mas a linearidade do XOR significa que a soma XOR de múltiplos hashes não herda essa propriedade. Construir um conjunto de entradas cujos hashes XOR resultem em qualquer valor alvo se reduz a resolver um sistema de equações lineares sobre F2\mathbb{F}_2, o que é computacionalmente trivial. A composição de hashes requer operações que preservem a resistência a colisões, como concatenação seguida de re-hashing.
  • Validação ausente de IDs de posição. O protocolo não verificou se os positionIds passados correspondiam a posições de opção válidas. Valores abaixo de 2642^{64} carregam um countLegs de 0 e um positionSize de 0, o que significa que as posições forjadas não geram dívida. Isso permitiu que o atacante passasse na verificação de saúde com requisito de colateral zero enquanto correspondia à impressão digital alvo.

Referência

  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

Os dois apêndices a seguir fornecem uma explicação e prova matemática das afirmações no texto principal, a saber, que a operação XOR é equivalente à adição vetorial (Apêndice I) e a probabilidade de que uma matriz aleatória seja de posto completo (Apêndice II).

Apêndice I

No corpo finito F2={0,1}\mathbb{F}_2 = \{0, 1\}, a adição é definida como adição 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}

Observa-se que isso é idêntico à operação lógica OR Exclusivo (XOR, símbolo \oplus).

Para o espaço vetorial nn-dimensional F2n\mathbb{F}_2^n (neste caso n=248n=248), a adição de dois vetores u=[u1,,un]Tu = [u_1, \dots, u_n]^T e v=[v1,,vn]Tv = [v_1, \dots, v_n]^T é definida como adição componente a componente módulo 2:

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

Portanto, no espaço vetorial F2n\mathbb{F}_2^{n}, a adição vetorial é equivalente à operação XOR bit a bit nos componentes do vetor.

Apêndice II

Para que a matriz seja de posto completo, esses nn vetores devem ser linearmente independentes.

O primeiro vetor v1v_1 pode ser qualquer vetor em F2n\mathbb{F}_2^n exceto o vetor zero, fornecendo 2n12^n - 1 escolhas; o segundo vetor v2v_2 não deve residir no subespaço gerado por {v1}\{v_1\}, que contém 212^1 vetores, deixando 2n22^n - 2 escolhas; seguindo essa lógica, o kk-ésimo vetor vkv_k não pode estar no subespaço gerado pelos k1k-1 vetores anteriores, resultando em 2n2k12^n - 2^{k-1} escolhas possíveis.

O número total de tais matrizes (ou seja, a ordem de GL(n,F2)GL(n, \mathbb{F}_2)) é:

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})

E o número total de todas as matrizes n×nn \times n possíveis é (2n)n=2n2(2^n)^n = 2^{n^2}.

Assim, a probabilidade PP é:

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})

Fazendo j=nkj = n - k, podemos reescrever essa expressão como:

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

Quando nn \to \infty, esse produto converge para uma constante:

P0.288788P \approx 0.288788

Isso significa que para nn grande, a probabilidade de que uma matriz aleatória seja de posto completo é aproximadamente 28,9%.


Sobre a BlockSec

A BlockSec é um provedor completo de segurança blockchain e conformidade cripto. Desenvolvemos produtos e serviços que ajudam os clientes a realizar auditorias de código (incluindo contratos inteligentes, blockchain e carteiras), interceptar ataques em tempo real, analisar incidentes, rastrear fundos ilícitos e cumprir obrigações AML/CFT, ao longo de todo o ciclo de vida de protocolos e plataformas.

A BlockSec publicou múltiplos artigos de segurança blockchain em conferências de prestígio, relatou vários ataques zero-day em aplicações DeFi, bloqueou múltiplos hacks para resgatar mais de 20 milhões de dólares e protegeu bilhões em criptomoedas.

Best Security Auditor for Web3

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

BlockSec Audit