sherlock-audit / 2024-09-symmio-v0-8-4-update-contest-judging

0 stars 0 forks source link

safdie - `ecrecover` and Pre-EIP-155 replay attack could be reused signatures across different chains by attacker in `LibMuonV04ClientBase.sol` #25

Open sherlock-admin3 opened 1 week ago

sherlock-admin3 commented 1 week ago

safdie

Medium

ecrecover and Pre-EIP-155 replay attack could be reused signatures across different chains by attacker in LibMuonV04ClientBase.sol

Summary

The ecrecover function may be susceptible to replay attacks before EIP-155 (where v could be 27 or 28). Malicious actors could reuse a valid signature on one network to replay a transaction on another network (before EIP-155). This is not a concern for most modern contracts deployed on mainnet after EIP-155, but it can be problematic in cross-chain contexts or forks.

Root Cause

Before the introduction of EIP-155, Ethereum signatures were vulnerable to replay attacks because the transaction structure did not include the chain ID. This allowed an attacker to broadcast a signed transaction on one network (e.g., Ethereum Mainnet) and replay it on another network (e.g., a testnet or a forked network) where the same transaction could still be valid. When using the ecrecover function for signature verification, contracts can be exposed to replay attacks if they do not account for chain IDs in signatures, especially when dealing with older transactions or compatibility with pre-EIP-155 systems.

The ecrecover function extracts the Ethereum address that corresponds to the public key used to sign a message, based on the signature. However, without incorporating the chain ID (as EIP-155 does), the same signature can be replayed across different Ethereum networks or forks. https://github.com/sherlock-audit/2024-09-symmio-v0-8-4-update-contest/blob/main/protocol-core/contracts/libraries/LibMuonV04ClientBase.sol#L135

Internal pre-conditions

No response

External pre-conditions

  1. The attacker deposits ETH on Ethereum Mainnet into this contract.
  2. The attacker signs a transfer transaction using their private key. This creates a valid signature v, r, s, and the message hash hash. The signature and message hash are verified using the ecrecover function, and the transaction executes successfully on Mainnet.

Attack Path

Since the chain ID is not included in the message (i.e., it's pre-EIP-155 logic), the attacker takes the exact same signed transaction and replays it on a different chain, such as a testnet or a fork of the Ethereum network (PoC below). On this new network, the signature remains valid, and the ecrecover function will recover the attacker's address. Since the contract does not distinguish between transactions on different chains, it executes the same transfer again on the second chain. The attacker has now transferred the same amount of tokens/ETH twice: once on Ethereum Mainnet and once on another network. If this replay involves real assets, the attacker can double-spend, resulting in financial loss for the contract owner or the users of the contract.

Impact

  1. Replay attacks allow an attacker to double-spend their funds across different networks, which can lead to severe financial losses, especially in cases where real assets or tokens are involved.
  2. If a chain fork occurs (e.g., during hard forks like the Ethereum-Classic split), transactions that are valid on one chain may be replayed on the other chain. Without incorporating EIP-155 or chain ID validation, contracts could process these replayed transactions unintentionally.
  3. In cases where a contract operates on multiple Ethereum-compatible networks (e.g., Ethereum and Binance Smart Chain), an attacker could replay valid transactions from one chain to another. This can lead to unintended token transfers, manipulation of balances, or other financial misbehavior.

PoC

pragma solidity ^0.8.18;

contract VulnerableReplay {
    mapping(address => uint256) public balances;

    // Simulating a simple transfer function
    function transfer(uint256 amount, bytes32 hash, uint8 v, bytes32 r, bytes32 s) public {
        // Recover the signer of the message
        address signer = ecrecover(hash, v, r, s);

        // Check if the signature is valid and the signer has sufficient balance
        require(balances[signer] >= amount, "Insufficient balance");

        // Reduce the balance of the signer
        balances[signer] -= amount;

        // Transfer to the caller (for simplicity)
        balances[msg.sender] += amount;
    }

    // Function to deposit funds (for testing)
    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }
}

Mitigation

  1. Always ensure that the chain ID is incorporated into the transaction signature, as specified in EIP-155. This ensures that signatures are only valid on the chain where they were created, preventing cross-chain replay attacks.
    uint256 chainId;
    assembly {
    chainId := chainid()
    }
  2. Opt for a signature verification scheme that includes the chain ID in the signature message, such as EIP-712, which is designed to mitigate replay attacks by structuring the signature format to include domain-specific data, including the chain ID.
    struct EIP712Domain {
    string name;
    string version;
    uint256 chainId;
    address verifyingContract;
    }