WEB3DEV

Cover image for Uma análise aprofundada da vulnerabilidade de entradas Aliasing do zk-SNARK
Diogo Jorge
Diogo Jorge

Posted on

Uma análise aprofundada da vulnerabilidade de entradas Aliasing do zk-SNARK

Image description

1. Contexto

Em 2019, descobriu-se que um projeto de prova de conhecimento zero, Semaphore, tinha uma vulnerabilidade de entrada aliasing (criando pseudônimos) que poderia levar a gastos duplos. O repórter de vulnerabilidade poma forneceu dois exemplos de transações bem-sucedidas:

Image description

https://github.com/semaphore-protocol/semaphore/issues/16)

O escopo dessa vulnerabilidade é muito amplo, não apenas envolvendo muitas bibliotecas de terceiros conhecidas na zk-SNARKs, mas também afetando vários projetos DApp. No final deste artigo, listaremos os códigos de vulnerabilidade específicos e a correção para cada projeto. Primeiro, vamos fornecer uma introdução detalhada à vulnerabilidade de entradas aliasing.

2. Princípio da Vulnerabilidade

O Semaphore permite que os usuários do Ethereum realizem ações como votar como membro da equipe sem revelar sua identidade original. Todos os membros da equipe formam uma árvore Merkle, sendo cada membro um nó folha. O contrato exige que os membros da equipe forneçam uma prova de conhecimento zero para demonstrar a legitimidade de sua identidade. Para evitar a falsificação de identidade, cada prova só pode ser usada uma vez. Portanto, o contrato armazena uma lista de provas verificadas. Se um usuário fornecer uma prova usada, o programa lançará um erro. O código de implementação específico é o seguinte:

Image description
https://github.com/semaphore-protocol/semaphore/blob/602dd57abb43e48f490e92d7091695d717a63915/semaphorejs/contracts/Semaphore.sol#L83)

Como podemos ver, o código acima primeiro chama a função ‘verifyProof’ para verificar a validade da prova de conhecimento zero e, em seguida, verifica se a prova está sendo usada pela primeira vez por meio do parâmetro nullifiers_hash. No entanto, devido à falta de uma verificação de legalidade abrangente em nullifiers_hash, um invasor pode forjar várias provas que passam na verificação e executam um ataque de gasto duplo. Especificamente, o tipo de variável de contrato uint256 pode representar um intervalo muito maior de valores do que o circuito de prova de conhecimento zero. O código considera apenas se o próprio nullifiers_hash já foi usado antes e não restringe o intervalo de valores de nullifiers_hash no contrato, permitindo que o invasor forje várias provas que passam na verificação do contrato explorando a aritmética modular na criptografia.

Como a faixa de valores dos parâmetros envolve algum conhecimento matemático relacionado a provas de conhecimento zero, e, diferentes algoritmos de prova de conhecimento zero correspondem a diferentes faixas de valores, os detalhes serão introduzidos posteriormente no texto.

Primeiro, se você deseja gerar e verificar provas zk-SNARK na Ethereum, você precisa usar circuitos de curva F_p-aritmética, onde a equação geral da curva elíptica definida sobre um campo finito é a seguinte:

Image description

Pode-se observar que os pontos da curva a módulo p, portanto a faixa de valores dos parâmetros de prova gerados pelo circuito é [0, 1, …, p-1]. No entanto, o intervalo de uint256 em Solidity é [0, 115792089237316195423570985008687907853269984665640564039457584007913129639935]. Quando o intervalo de variáveis ​​de contrato é maior que o intervalo de valores de circuito, existem vários valores de parâmetro de prova com a mesma saída:

Image description

Em resumo, desde que um parâmetro de prova válido s seja conhecido, s + np (n = 1, 2, …, n) dentro do intervalo uint256 pode satisfazer o cálculo de verificação. Portanto, uma vez que um invasor obtém qualquer s que passa na verificação, eles podem construir provas max(uint256)/p onde cada s passa na verificação. O processo de ataque específico é o seguinte:

Image description

Como mencionado anteriormente, o intervalo de valores dos parâmetros é determinado por p, e diversos tipos de _F_p _correspondem a diferentes valores de p, que precisam ser determinados conforme o algoritmo de conhecimento zero específico que está sendo usado. Por exemplo:

· No EIP-196, a curva BN254 (também conhecida como curva ALT_BN128) tem p = 21888242871839275222246405745257275088548364400416034343698204186575808495617.

· Circom2 introduziu dois novos números primos para a curva BLS12–381, com p = 52435875175126190479447740508185965837690552500527637822603658699938581184513.

Tomando a curva ALT_BN128 como exemplo, um total de 5 parâmetros de prova diferentes podem passar na verificação e o processo de cálculo é o seguinte:

Image description

3. Reprodução de Vulnerabilidade

Como o código do projeto Semaphore já foi modificado e a reimplantação de todo o projeto é complicada, usaremos o compilador de prova de conhecimento zero amplamente utilizado circom para escrever um PoC (Proof of Concept) q reproduza todo o processo de ataque. Para ajudar todos a entender melhor o processo de reprodução, primeiro usaremos o circom como exemplo e apresentaremos o processo de geração e verificação das provas de conhecimento zero Groth16.

Image description
https://docs.circom.io/

  1. A equipe do projeto precisa projetar um circuito aritmético e usar a sintaxe circom para escrevê-lo em um arquivo de descrição de circuito *.circom.

  2. Compile o arquivo de circuito e converta-o em um arquivo de descrição de circuito R1CS.

  3. Use a biblioteca snarkjs para calcular a testemunha correspondente com base no arquivo de entrada input.json.

  4. Em seguida, gere uma chave de comprovação e uma chave de validação por meio de uma configuração confiável. A chave Proving é usada para gerar a Prova, e a chave Validação é usada para verificar a Prova. Finalmente, o usuário gera a prova de conhecimento zero correspondente (Proof) usando as chaves.

  5. Verifique a prova do usuário.

Vamos agora apresentar cada etapa do processo em detalhes.

3.1 Compilar multiplier2.circom

Para facilitar o entendimento de todos, usaremos diretamente o demo oficial do circom. O código é o seguinte:

pragma circom 2.0.0;
template Multiplier2() {
signal input a;
signal input b;
signal output c;
c <== a*b;
}

component main = Multiplier2();
Enter fullscreen mode Exit fullscreen mode

Neste circuito, existem dois sinais de entrada a e b e um sinal de saída c. O valor de c é o resultado da multiplicação de a e b.

3.2 Circuito de compilação

Use a seguinte linha de comando para compilar multiplier2.circom e convertê-lo em R1CS:

circom multiplier2.circom — r1cs — wasm — sym —c
Enter fullscreen mode Exit fullscreen mode

Após a compilação, 4 arquivos serão gerados, incluindo:

· '— r1cs': O circuit.r1cs gerado é um arquivo de restrição de circuito de formato binário.

· '- wasm: A pasta multiplier2_js gerada contém o código assembly wasm e o diretório de outros arquivos necessários para gerar a testemunha (generate_witness.js, multiplier2.wasm).

· '-sym': A pasta multiplier2.sym gerada é um arquivo de símbolo, usado para depurar ou imprimir o sistema de restrição em um modo anotado.

· ‘— c’: a pasta multiplier2_cpp gerada contém arquivos de código C++ necessários para gerar uma testemunha.

Observação: há duas maneiras de gerar testemunhas, uma usando wasm e a outra usando o código C++ recém-gerado. Se for um circuito grande, usar o código C++ é mais eficiente do que o wasm.

3.3 Calcular a ‘testemunha’

Crie um arquivo 'input.json' na pasta 'multiplier2_js'. Este arquivo contém a entrada escrita no formato JSON padrão, usando strings em vez de números porque o JavaScript não pode lidar com números maiores que $2^{53}$ com precisão. Gere a testemunha correspondente para o 'input.json' especificado:

node generate_witness.js multiplier2.wasm input.json witness.wtns
Enter fullscreen mode Exit fullscreen mode

3.4 Configurações confiáveis

Esta etapa envolve principalmente selecionar o tipo de curva elíptica necessária para a prova de conhecimento zero e gerar uma série de arquivos-chave originais .key. O arquivo multiplier2_0000.zkey contém a chave de prova e a chave de validação, enquanto o arquivo multiplier2_0001.zkey arquivo contém a chave de validação. O arquivo de chave de validação final exportado é verification_key.json.

snarkjs powersoftau new bn128 12 pot12_0000.ptau -v
snarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau — name="First contribution" -v
snarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau -v
snarkjs groth16 setup multiplier2.r1cs pot12_final.ptau multiplier2_0000.zkey
snarkjs zkey contribute multiplier2_0000.zkey multiplier2_0001.zkey — name="1st Contributor Name" -v
snarkjs zkey export verificationkey multiplier2_0001.zkey verification_key.json
Enter fullscreen mode Exit fullscreen mode

3.5 Gerar Provas

Existem duas maneiras de gerar provas usando snarkjs: um é por meio da linha de comando e o outro é por meio da geração de script. Como precisamos construir vetores de ataque, usamos principalmente a geração de scripts nesse caso.

3.5.1 Gerar publicSignal normal

snarkjs groth16 prove multiplier2_0001.zkey witness.wtns proof.json public.json
Enter fullscreen mode Exit fullscreen mode

Este comando produzirá dois arquivos: proof.json é o arquivo de prova gerado e public.json é o valor de entrada público.

3.5.2 Gerar ataque publicSignal

async function getProof() {
let inputA = "7"
let inputB = "11"
const { proof, publicSignals } = await snarkjs.groth16.fullProve({ a: inputA, b: inputB }, "Multiplier2.wasm", "multiplier2_0001.zkey")
console.log("Proof: ")
console.log(JSON.stringify(proof, null, 1));
let q = BigInt("21888242871839275222246405745257275088548364400416034343698204186575808495617")
let originalHash = publicSignals
let attackHash = BigInt(originalHash) + q
console.log("originalHash: " + publicSignals)
console.log("attackHash: " + attackHash)
}
Enter fullscreen mode Exit fullscreen mode

Prova gerada (Proof), parâmetro de validação original (originalHash) e parâmetro de ataque (attackHash) são mostrados no diagrama a seguir:

Image description

3.6 Provas de Validação

Há também duas maneiras de verificar provas, uma é usar a biblioteca snarkjs para verificação e a outra é usar a verificação de contrato. Aqui, usamos principalmente a verificação de contrato on-chain para verificar o parâmetro de validação original (originalHash) e o parâmetro de validação de ataque (attackHash).

Usamos snarkjs para gerar automaticamente um contrato de verificação chamado ‘verifier.sol'. Observe que a última versão 0.6.10 do snarkjs já corrigiu esse problema, então usamos uma versão mais antiga para gerar o contrato:

snarkjs zkey export solidityverifier multiplier2_0001.zkey verifier.sol
Enter fullscreen mode Exit fullscreen mode

Os códigos-chave do contrato são os seguintes:

function verify(uint[] memory input, Proof memory proof) internal view returns (uint) {
VerifyingKey memory vk = verifyingKey();
require(input.length + 1 == vk.IC.length,"verifier-bad-input");
// Calcula a combinação linear vk_x
Pairing.G1Point memory vk_x = Pairing.G1Point(0, 0);
for (uint i = 0; i < input.length; i++)
vk_x = Pairing.addition(vk_x, Pairing.scalar_mul(vk.IC[i + 1], input[i]));
vk_x = Pairing.addition(vk_x, vk.IC[0]);
if (!Pairing.pairingProd4(
Pairing.negate(proof.A), proof.B,
vk.alfa1, vk.beta2,
vk_x, vk.gamma2,
proof.C, vk.delta2
)) return 1;
return 0;
}
Enter fullscreen mode Exit fullscreen mode

Neste ponto, a verificação passa usando o originalHash:

Image description

Por fim, usando o attackHash que acabou de ser forjado:

21888242871839275222246405745257275088548364400416034343698204186575808495694

Mais uma vez, a validação da prova é aprovada! Ou seja, a mesma prova pode ser verificada várias vezes para causar um ataque de gasto duplo.

Além disso, como a curva ALT_BN128 é usada para reprodução neste artigo, um total de 5 parâmetros diferentes podem ser gerados para passar na verificação:

Image description

Além disso, como a curva ALT_BN128 é usada para reprodução neste artigo, um total de 5 parâmetros diferentes podem ser gerados para passar na verificação:

Image description

4. Remediação

O Semaphore corrigiu a vulnerabilidade com o seguinte código de correção:

Image description
https://github.com/semaphore-protocol/semaphore/blob/0cb0ef3514bc35890331379fd16c7be071ada4f6/packages/contracts/contracts/base/SemaphoreVerifier.sol#L42)

Image description
https://github.com/semaphore-protocol/semaphore/blob/0cb0ef3514bc35890331379fd16c7be071ada4f6/packages/contracts/contracts/base/Pairing.sol#L94)

No entanto, esta vulnerabilidade é uma vulnerabilidade de implementação geral. Por meio da pesquisa da equipe de segurança da Beosin, descobrimos que muitos componentes conhecidos do algoritmo de prova de conhecimento zero e projetos DApp são afetados por essa vulnerabilidade. A maioria deles foi prontamente corrigida. Seguem alguns exemplos de soluções adotadas por diversas equipes de projeto:

ethsnarks:

Image description
(https://github.com/HarryR/ethsnarks/commit/34a3bfb1b0869e1063cc5976728180409cf7ee96)

snarkjs:

Image description
(https://github.com/iden3/snarkjs/commit/25dc1fc6e311f47ba5fa5378bfcc383f15ec74f4)

heiswap-dapp:

Image description

Image description
https://github.com/kendricktan/heiswap-dapp/commit/de022ffc9ffdfa4e6d9a7b51dc555728e25e9ca5#diff-a818b8dfd8f87dea043ed78d2e7c97ed0cda1ca9aed69f9267e520041a03 7bd5)

EY Blockchain:

Image description
https://github.com/EYBlockchain/nightfall/pull/96/files)

No entanto, alguns projetos ainda não corrigiram esse problema. A equipe de segurança da Beosin já entrou em contato com os projetos e está auxiliando ativamente no processo de remediação.

Em resposta a esta vulnerabilidade, a equipe de segurança da Beosin aconselha os projetos do ZK-SNARK a considerar totalmente os riscos de segurança decorrentes dos atributos da linguagem de código ao implementar a validação de prova no design do algoritmo. Também recomendamos enfaticamente que os projetos procurem auditores de segurança profissionais para auditorias de segurança completas antes de iniciar o projeto.

Sobre Beosin

Beosin é uma empresa global líder em segurança de blockchain co-fundada por vários professores de universidades de renome mundial e há mais de 40 PhDs na equipe. Possui escritórios em Cingapura, Coreia, Japão e outros 10 países. Com a missão de “Proteger o Ecossistema Blockchain”, a Beosin fornece uma solução de segurança blockchain “All-in-one” que abrange Auditoria de Contrato Inteligente, Monitoramento e Alerta de Risco, KYT/AML e Rastreamento de Criptomoedas. A Beosin já auditou mais de 3.000 contratos inteligentes e protegeu mais de US$ 500 bilhões em fundos de nossos clientes.

Este artigo foi escrito por Beosin e traduzido por Diogo Jorge. O artigo original pode ser encontrado aqui.

Top comments (0)