Unmasking EVM Attack: Analyzing the TornadoCash Governance Takeover

Deploying of Multiple Contracts at a Single Address ?

·

10 min read

Unmasking EVM Attack: Analyzing the TornadoCash Governance Takeover

In recent years, the world of decentralized finance (DeFi) has become a hotbed for cyberattacks. One notable case study is the TornadoCash Governance Takeover. In this blog, we break down the complexities of this attack, making it easier for you to grasp and appreciate the full picture.

So, what is TORN by the way?

TornadoCash is a DeFi protocol that enables anonymous Ethereum transactions. The platform utilizes zero-knowledge proofs, an advanced cryptographic technique, it ensures untraceable and unlinkable transactions, safeguarding user privacy. Users can deposit ETH into a mixer, which mixes and anonymizes the funds. They can withdraw anonymized ETH or convert it to zETH for anonymous transactions on other DeFi protocols. With TornadoCash, users can enjoy enhanced privacy and control over their financial transactions in the DeFi landscape.

It is built on top of Ethereum which allows users to send in cryptocurrency, and then anonymously withdraw it. Everyone can see the money go in, but after that, it’s impossible to see where the money goes next. The trail is dead. using zero-knowledge proofs, allows that user to withdraw their specific tokens to another address without leaving a public record on-chain that could link the user’s sending and receiving address.

Cracking the TornadoCash Governance Takeover?

An anonymous hacker seized control of this decentralized finance (DeFi) protocol and took full control of Tornado Cash by granting themselves 1.2 million votes through a malicious proposal.

The attacker simply used the "emergency-stop function to update the proposal logic to grant themselves the fake votes” which helps customers conceal transactions, Tornado Cash, by exploiting a malicious governance proposal. Reportedly, the hacker has stolen over $1 million during the week he has control over the protocol.

Goveranance protocol huh ??

A goveranance protocol in the DeFi world is like a way for people to vote and decide on the rules and features of a platform, just like how you and your friends decide on the rules of a game before you play. It helps make sure everyone is treated fairly and has a say in how things are done.

Surprisingly, out of 1.2 million votes 700,000 votes were legitimate, thereby granting the attacker control of the DAO.

Procedural Breakdown of the Attack: How ItUnfolded

Two key accounts played essential roles in the attack,

Attacker 2, took charge of managing the life cycle of the proposal. This involved tasks such as deploying the proposal, submitting it, destroying it, redeploying it, and executing it.

Attacker 1, had control over the drainer contracts, which were instrumental in unlocking the TORN tokens from Tornado Cash.

The attackers overwrote the code at the same address with a new proposal implementation in the alteration of the Governance storage, followed by the unauthorized removal of TORN tokens.

Stage 0: Initial Transactions [Proposal Handler]

Attacker 2 executed a transaction where they withdrew 10 ETH from the TORN (Tornado Cash) system. They then proceeded to swap this amount using the 1Inch platform.

wofffff!!

A new platform, so what is 1inch ??

1Inch is a decentralized exchange aggregator that compares token prices across various decentralized exchanges to find the best prices for users. By leveraging 1Inch's functionality, Attacker 2 aimed to optimize the swap and maximize their gains, and he locked that 1017TORN in TORN cash governance enabling proposal contract submission.

Stage 2: Creating Subsidiary Accounts [Drainer Controller]

Meanwhile, Attacker 1 cleverly devised a plan by creating 100 subsidiary accounts, also known as minions. Surprisingly, these accounts were set up with zero TORN balance locked in the Tornado system for each one. It's quite intriguing to note that even without any approved and locked TORN from these 100 subsidiary accounts, the attack could still have been successful. This adds an element of curiosity and interest to the overall scheme.

Stage 3: Account Creation [Drainer Controller]

After locking the TORN tokens, Attacker 2 submitted a proposal to the Tornado Governance system and had a hidden self-destruct command inside an emergency stop function.

Attacker 2 then activated the emergency stop .....

before the proposal could be executed, causing the destruction of both the proposal and the temporary contract. By doing this, the attacker reset the transient contract's nonce, which enabled them to alter the implementation of the proposal.

Attacker2 managed to deploy, destruct and redeploy the proposal on the same address by using a series of deployment techniques to carry out their plan. Here's how it worked:

  • Deploying a Factory: Attacker 2 set up a factory that had the ability to store and retrieve the creation code for both the original proposal and the malicious one. This factory was an essential tool for their attack.

  • Transient Contract Deployment with create2: Attacker 2 utilized a method called create2 to deploy a transient contract. This type of deployment involved specifying a unique value called a salt, which acted as an additional input during contract creation. By using create2, the attacker ensured that the same salt would always result in the same contract address, even after self-destruction.

  • Destruct and Redeploy: After deploying the transient contract, Attacker 2 triggered the destruction of both the transient contract and the proposal. This step allowed them to reset the nonce of the transient contract, which was necessary for their next move.

  • Redeployment of Proposal: By leveraging the factory's capabilities, Attacker 2 redeployed the proposal to the same address as the previous one. Since the transient contract's address remained the same due to the create2 deployment, the attacker was able to deploy two different implementations of the proposal using the factory.

So, how can we calculate contract addresses using create2 vs create ???

In Ethereum, contract addresses can be computed using two different opcodes:

  1. create2: This opcode was introduced in Ethereum Improvement Proposal (EIP) 1014 by Vitalik Buterin. It takes four stack arguments: endowment, memory_start, memory_length, and salt. The create2 opcode behaves similarly to create but uses a different formula to calculate the contract address.

The address computation using create2 follows this formula:

address = keccak256(0xff + sender_address + salt + keccak256(init_code))[12:]

In this formula:

  • sender_address is the address of the sender of the transaction.

  • salt is an additional value that can be provided to change the resulting address.

  • init_code refers to the initialization code of the contract that will be deployed.

  • Keccak256 is a cryptographic hash function that takes an input and produces a fixed-size (256-bit) hash value.

By using create2 the same salt and init_code, the resulting contract address will always be the same. This allows for deterministic deployment of contracts, meaning that the same code and initialization will always produce the same address.

create: This is the legacy opcode for contract deployment. It calculates the contract address based on the rightmost 20 bytes (160 bits) of the Keccak-256 hash of the RLP-encoded sender address concatenated with its nonce

The address computation using create follows this formula:

address = keccak256(rlp([sender_address, sender_nonce]))[12:]

In this formula:

  • sender_address is the address of the sender of the transaction.

  • sender_nonce is the nonce of the sender's account at the time of contract deployment.

  • RLP is a binary encoding format used in Ethereum to represent structured data, including lists and nested data structures. It provides a compact and efficient way to encode and decode data, enabling the smooth operation of various Ethereum processes such as transaction processing, block validation, and contract execution.

By incrementing the nonce with each deployment, a new contract address can be generated using create. Each new deployment will result in a different address.

It's worth noting that the attacker in the given scenario utilized create2 to deploy the transient contracts, taking advantage of the deterministic address computation to manipulate the proposal implementations.

Stage 5: Locked Balance Modification [Proposal Handler]

During the execution of the proposal within the Tornado Cash system, Attacker 2 employed several techniques to manipulate the lockedBalance mapping of the subsidiary accounts created by Attacker 1. To understand how this manipulation occurred, let's break down the key elements involved:

  1. Delegatecall: Delegatecall is a low-level function in Ethereum smart contracts that allows one contract to call another contract while maintaining the context of the calling contract.

    In this attack scenario, Attacker 2 utilized delegatecall to execute the new proposal's implementation code within the context of the Tornado Cash system.

  2. Sstore Instructions: Sstore is an opcode in Ethereum that is responsible for storing a value in the contract's storage. Attacker 2 strategically added sstore instructions to the new proposal's implementation. These instructions were specifically designed to modify the lockedBalance mapping, which is a crucial data structure in Tornado Cash that keeps track of the locked TORN balances associated with each account.

  3. Locked Balance: In the context of Tornado Cash, the lockedBalance mapping is responsible for storing the amount of TORN tokens locked for each account. Initially, the subsidiary accounts created by Attacker 1 had a zero balance of locked TORN. However, through the manipulation of the lockedBalance mapping using sstore instructions, Attacker 2 assigned a significantly inflated balance of 10,000 locked TORN to each subsidiary account.

By leveraging delegatecall to execute the new proposal's implementation and incorporating sstore instructions to modify the lockedBalance mapping, Attacker2 successfully manipulated the subsidiary accounts' TORN balances. This deceptive tactic created a false impression that the subsidiary accounts held a substantial amount of locked TORN, even though they had initially started with zero balances.

It is important to note that these actions were part of a malicious attack, aiming to exploit the system's vulnerabilities and deceive the governance participants. Such manipulations of critical data structures like the lockedBalance mapping could have severe consequences, compromising the integrity and proper functioning of the Tornado Cash system.

Stage 6: Token Transfer [Drainer Controller]

During this stage, Attacker 1 unlocked and transferred the TORN tokens from the manipulated subsidiary accounts. By updating the balances of the accounts and leveraging their authority over the drainer contracts, Attacker 1 unlocked the tokens and directed them to their own account, effectively stealing the TORN tokens that were initially locked by the subsidiary accounts.

Finally, Here is your sample code :

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract DAO {
        struct Proposal {
            address target;
            bool approved;
            bool executed;
        }

    address public owner = msg.sender;
    Proposal[] public proposals;

    function approve(address target) external {
        require(msg.sender == owner, "not authorized");

        proposals.push(Proposal({target: target, approved: true,                         executed: false}));
    }

    function execute(uint256 proposalId) external payable {
        Proposal storage proposal = proposals[proposalId];
        require(proposal.approved, "not approved");
        require(!proposal.executed, "executed");

        proposal.executed = true;

        (bool ok, ) = proposal.target.delegatecall(
            abi.encodeWithSignature("executeProposal()")
        );
        require(ok, "delegatecall failed");
    }
}

contract Proposal {
    event Log(string message);

    function executeProposal() external {
        emit Log("Excuted code approved by DAO");
    }


    function emergencyStop() external {
        selfdestruct(payable(address(0)));
    }
}

contract Attack {
    event Log(string message);

    address public owner;

    function executeProposal() external {
        emit Log("Excuted code not approved by DAO :)");
        // For example - set DAO's owner to attacker
        owner = msg.sender;
    }
}

contract DeployerDeployer {
    event Log(address addr);

    function deploy() external {
        bytes32 salt = keccak256(abi.encode(uint(123)));
        address addr = address(new Deployer{salt: salt}());
        emit Log(addr);
    }
}

contract Deployer {
    event Log(address addr);

    function deployProposal() external {
        address addr = address(new Proposal());
        emit Log(addr);
    }

    function deployAttack() external {
        address addr = address(new Attack());
        emit Log(addr);
    }

    function kill() external {
        selfdestruct(payable(address(0)));
    }
}

source

Tornado: The Conclusion

In conclusion, the Tornado Cash Governance system experienced a serious security breach orchestrated by malicious actors. Through a series of clever maneuvers, the attackers exploited vulnerabilities in the system, leading to unauthorized access and manipulation of locked TORN tokens. Wait! Attacker 2 pulled off a magic trick by altering the locked balance of subsidiary accounts.

The attackers utilized deceptive proposals, deployed contracts with precision, and tampered with important data structures to their advantage. This incident serves as a reminder of the importance of robust security measures and thorough auditing in decentralized finance platforms.

Remember, security is paramount in the world of decentralized finance. As users, it is crucial to exercise caution, stay informed about potential risks, and only engage with platforms that prioritize the safety of your assets.

Let us learn from this event and continue to push for innovation and security in the exciting realm of decentralized finance. Together, we can build a stronger and more trustworthy ecosystem for everyone involved.