Cointime

Download App
iOS & Android

Exploring Tornado Cash In-Depth to Reveal Malleability Attacks in ZKP Projects

In the previous article, we explained the inherent malleability vulnerability in the Groth16 proof system theoretically.

https://glacier-screen-c36.notion.site/Beosin-s-Research-Transaction-Malleability-Attack-of-Groth16-Proof-c090649950804af686e276baa3ba8182

In this article, we take the Tornado.Cash project as an example, modifying parts of its circuit and code to demonstrate malleability attack flows and the corresponding mitigations in the project, hoping to raise awareness for other zkp projects. Tornado.Cash uses the snarkjs library with the following development flow, so we'll dive right in - please refer to the first article in the series if you are unfamiliar with the library.

1 Tornado.Cash Structure

There are 4 main entities in the interaction flow of Tornado.Cash:

  • User: Uses this DApp to conduct private coin mixing transactions, including deposits and withdrawals.

Source: https://docs.circom.io/

  • Web page: The frontend web page of the DApp, contains some user buttons.
  • Relayer: To prevent on-chain nodes from recording privacy-related info like IP addresses, this server replays transactions on behalf of users to further enhance privacy.
  • Contract: Contains a proxy contract Tornado.Cash Proxy, which selects the specified Tornado pool based on deposit/withdrawal amounts. Currently there are 4 pools for amounts: 0.1, 1, 10, 100.

First the user initiates deposit or withdrawal on Tornado.Cash frontend. Then the Relayer forwards the transaction request to the Tornado.Cash Proxy contract on-chain, which further forwards it to the corresponding Pool based on amount, and finally performs the deposit/withdrawal processing. The architecture is as follows:

As a coin mixer, Tornado.Cash has two main business functions:

  • deposit: When a user makes a deposit, they first select the token (BNB, ETH etc) and amount on the frontend. To better ensure privacy, only 4 preset amounts can be deposited.

Source: https://ipfs.io/ipns/tornadocash.eth/

The server then generates two 31-byte random numbers - nullifier and secret. Concatenating and hashing them generates the commitment. The nullifier + secret is returned to the user as a note, like below:

Then a deposit transaction is initiated, sending the commitment to the on-chain Tornado.Cash Proxy contract. The proxy forwards the data to the corresponding Pool based on deposit amount. Finally the Pool contract inserts the commitment as a leaf node into the merkle tree, and stores the computed root in the Pool contract.

  • withdraw: When a user makes a withdrawal, they first enter the note data returned during deposit, and recipient address on the frontend;

The server then retrieves all Tornado.Cash deposit events off-chain, withdraws the commitments to build a local merkle tree, and uses the user provided note (nullifier + secret) to generate the commitment and corresponding merkle path and root. This is input into a circuit to obtain a zero-knowledge SNARK proof. Finally, a withdraw transaction is initiated to the on-chain Tornado.Cash Proxy contract, which forwards it to the corresponding Pool to verify the proof, and sends the money to the user's specified receiving address.

The core of Tornado.Cash's withdraw is toprove that a certain commitment exists in the Merkle tree without revealing the user's nullifier and secret.

The Merkle tree structure is as follows:

2 Tornado.Cash Vulnerable Version After Modification

2.1 Tornado.Cash Modification

Based on the previous article about Groth16 malleability attack principles, we know attackers can generate multiple different Proofs using the same nullifier and secret, so if developers don't consider replay attacks leading to double-spending, it can threaten project funds. Before modifying Tornado.Cash, this article will first introduce the Pool contract code that handles withdraws in Tornado.Cash:

As shown in the image above, to prevent attackers from double spending using the same Proof, while not revealing the nullifier and secret, Tornado.Cash added a public signal called nullifierHash in the circuit, which is the Pedersen hash of the nullifier, and can be passed as a parameter on-chain. The Pool contract then uses this variable to check if a valid Proof has been used before.However, what if instead of modifying the circuit, the project simply records Proofs to prevent double spending attacks? This would reduce circuit constraints and save costs, but would it work?To test this hypothesis, this article will remove the added nullifierHash public signal from the circuit, and change the contract verification to just check the Proof.Since Tornado.Cash retrieves all deposit events to build the merkle tree on each withdraw, then verifies if the root values are within the last 30 generated, which is cumbersome, this article will also remove the merkleTree circuit, leaving just the core withdraw logic, as follows:

Note: We discovered during the experiments that the latest TornadoCash code on GitHub lacks output signals in the withdraw circuit, requiring manual fixes to run properly. (https://github.com/tornadocash/tornado-core**)**

Based on the modified circuit above, following the development process outlined earlier using snarkjs etc, a normal Proof is generated, denoted as proof1:

2.2 Experimental Verification

2.2.1 Verification with Default circom Contract

First we use the default contract generated by circom. Since it does not record any used Proof info, attackers can replay proof1 multiple times to achieve double-spending attacks. In the following experiment, the same input's proof can be replayed unlimited times and still pass verification.

The image below shows proof1 passing verification in the default contract, including the Proof parameters A, B, C from the previous article, and the final result:

The next image shows the results of calling the verifyProof function multiple times with the same proof1. The experiment finds that for the same input, no matter how many times proof1 is used by the attacker, it always passes:

Testing in the native snarkjs js library also does not defend against reused Proofs, with results as follows:

2.2.2 Verification with Basic Anti-Replay Contract

To fix the replay vulnerability in the default circom contract, this article records a value from the valid Proof(proof1) to prevent replaying already verified proofs for double-spending attacks, as shown below:

Continuing to verify with proof1, the experiment finds the transaction reverts with "The note has been already spent" when reusing the same proof, as shown:

However,although this achieves the goal of preventing basic proof replay attacks, as covered earlier Groth16 has malleability vulnerabilities that can bypass this. The following PoC constructs a forged SNARK proof for the same input based on the algorithm from previous article, and it still passes verification. The PoC code to generate forged proof2 is:

The generated forgery PROOF2 is shown below:

Again using this parameter to call verifyProof function for proof verification, the experiment found that the same input in the case of using proof2 verification has passed again, as shown below:

Although the forged proof2 can only be used once more, since there are nearly unlimited forged proofs for the same input, this could lead to contract funds being withdrawn unlimited times.

Testing in the circom js library also shows proof1 and the forged proof2 passing verification:

2.2.3 Verification with Tornado.Cash Anti-Replay Contract

After so many failed attempts, is there no way to solve this once and for all? Here, following Tornado.Cash's method of checking if the original input has been used, this article further modifies the contract code as:

It should be noted thatto demonstrate simple mitigations against Groth16 malleability attacks, this article takes the approach of directly recording original circuit inputs, which does not conform to zero knowledge principles of keeping inputs private.For example in Tornado.Cash the inputs are private, so a new public input is added to identify a proof. Since this article's circuit does not add an identifier, the privacy is poorer compared to Tornado.Cash - this is just an experimental demo. The results are as follows:

It can be seen that with the same input, only the first proof1 passes verification. After that, both proof1 and the forged proof2 cannot pass verification.

3 Summary and Recommendations

Through modifying TornadoCash's circuit and using the default contract verification generated by the commonly used Circom, this article has verified the existence and risks of replay vulnerabilities. It further proves that using common measures at the contract level can defend against replay attacks, but cannot prevent Groth16 malleability attacks. Based on this, we suggest Zero Knowledge Proof projects note the following during development:

  • Unlike traditional DApps that use unique addresses to generate node data, zkp projects typically use combined random numbers to generate Merkle tree nodes. Pay attention if business logic allows inserting duplicate node values, as the same leaf node data can lead to some user funds being locked in contracts, or the same leaf data having multiple Merkle Proofs confusing business logic.
  • zkp projects typically record used Proofs in a mapping to prevent double-spending attacks. When using Groth16, malleability attacks exist, so recording should use original node data rather than just Proof data.
  • Complex circuits can have circuit uncertainty, lack of constraints etc, leading to incomplete validation conditions and logical vulnerabilities in contracts. We strongly recommend projects seek comprehensive audits from security audit firms well-versed in circuits and contracts before launch, to ensure security.

Beosin is a leading global blockchain security company co-founded by several professors from world-renowned universities and there are 40+ PhDs in the team, and set up offices in 10+ cities including Hong Kong, Singapore, Tokyo and Miami. With the mission of "Securing Blockchain Ecosystem", Beosin provides "All-in-one" blockchain security solution covering Smart Contract Audit, Risk Monitoring & Alert, KYT/AML, and Crypto Tracing. Beosin has already audited more than 3000 smart contracts including famous Web3 projects PancakeSwap, Uniswap, DAI, OKSwap and all of them are monitored by Beosin EagleEye. The KYT AML are serving 100+ institutions including Binance.

Contact

If you need any blockchain security services, welcome to contact us:

Official WebsiteBeosin EagleEyeTwitterTelegramLinkedin

Comments

All Comments

Recommended for you

  • Another Iranian Oil Tanker Returns to Iran After Breaking US Blockade

    On April 21, according to CCTV News, maritime intelligence company 'TankerTrackers' reported that a tanker belonging to the National Iranian Tanker Company returned to Iran after unloading approximately 2 million barrels of crude oil in Indonesia, crossing the relevant maritime blockade line. The tanker is currently en route to Iran's main oil export hub, Khark Island, and is expected to arrive on April 22 local time. It is reported that the tanker set sail from Iran in late March, heading towards the Riau Islands of Indonesia.

  • White House: US and Iran on the Verge of Reaching an Agreement

    On April 21, White House Press Secretary Kayleigh McEnany stated in an interview with Fox News on the evening of the 20th that the United States and Iran are on the "verge of reaching an agreement." McEnany remarked, "The US has never been closer to achieving a truly good deal." However, she did not disclose any information regarding the current status of the negotiations. McEnany noted that even if an agreement is not reached, President Trump has multiple options and is not afraid to utilize these measures. Previous actions have demonstrated that Trump is not just "bluffing."

  • Kelp DAO Attacker Transfers 30,800 ETH to Special Address

    On April 21, news emerged that, according to monitoring by PeckShield, the Kelp DAO attacker transferred 30,800 ETH to a special address starting with 0x00000, possibly indicating a destruction action.

  • Trump: 'Midnight Hammer' Completely Dismantled Iran's Nuclear Dust Base

    On April 21, U.S. President Trump stated that the 'Midnight Hammer' operation has completely destroyed the 'nuclear dust' base within Iran. As a result, the cleanup will be a long and arduous process. The fake news media, including CNN and other corrupt media networks and platforms, have failed to give our great pilots the credit they deserve, instead always attempting to belittle and undermine them. They are losers!!! (Dongxin News Agency)

  • BTC Drops Below $76,000

    Market data shows that BTC has dropped below $76,000, currently priced at $75,999.63, with a 24-hour increase of 1.68%. The market is experiencing significant volatility, so please ensure proper risk management.

  • Japan Officially Allows Export of Lethal Weapons Through Cabinet Resolution

    On April 21, according to Kyodo News, the Japanese government officially revised the 'Three Principles on Transfer of Defense Equipment' and its operational guidelines during a cabinet meeting, which will, in principle, allow the export of lethal weapons. (Xinhua News Agency)

  • Trump Claims Iran Will Negotiate

    On April 21, during a phone interview with CNN, U.S. President Trump stated that Iran "will negotiate" and expressed confidence in potential talks set to take place in Pakistan. Trump remarked, "They will negotiate; if they don't, they will face unprecedented problems." He also expressed hope that both sides could reach a "fair agreement" and emphasized that Iran "will not have nuclear weapons." Additionally, he defended military actions against Iran by stating there was "no choice" and claimed that they would ultimately "wrap things up."

  • Amazon to Invest Additional $5 Billion in Anthropic

    On April 21, Amazon announced on Monday that it will invest an additional $5 billion in the artificial intelligence company Anthropic, bringing the total investment to as much as $20 billion. Anthropic develops the Claude chatbot and programming tools, and plans to invest over $100 billion in Amazon's cloud technology and chips over the next decade.

  • Three U.S. Carrier Strike Groups May Deploy Simultaneously in the Middle East

    On April 21, according to CCTV, the U.S. military is expected to deploy three carrier strike groups simultaneously in the Middle East in the coming days. Currently, the USS Lincoln strike group is stationed in the Gulf of Oman, near the Strait of Hormuz, participating in maritime blockade operations; the USS Ford strike group is located in the northern Red Sea; and the USS Bush strike group, which is taking a route around Africa, is heading north from the southeast of Africa and is expected to enter the Arabian Sea—this carrier may replace the USS Ford in its mission. In the short term, the U.S. military may have three aircraft carriers in the Middle East.