介绍以太坊账户,合约部署,如何绕过EOA账户检查,以及给开发者的安全建议。
最近我在对 ArmorFi 协议的漏洞进行分析,该漏洞通过绕过了 isContract()
的检查,结合闪贷进行攻击,会造成 60,000 ETH的损失,借此,我写了这篇文档供大家参考,后续也会更新对 ArmorFi 协议的漏洞分析,请大家关注。on my Github
以太坊中存在着两种账户类型,分别是EOA账户,以及合约账户。
以太坊白皮书介绍了以太坊账户,可以看到以太坊账户包含四个字段:
只要是合约账户,就一定会包含合约代码,EOA账户则不会,合约代码长度一定为0,我们是否能据此来进行区分呢?
Address.isContract(address)
根据这一特性,早期判断账户类型是通过读取账户地址,判断其 excodesize 是否大于 0。包括openzepplin早期3.4版本实现的isContract(address)也是通过这一原理。
function isContract(address account) internal 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.
uint256 size;
// solhint-disable-next-line no-inline-assembly
assembly { size := extcodesize(account) }
return size > 0;
}
我们不经疑问,这真的是准确的吗?在 2021年,ArmorFi被发现存在漏洞,该漏洞主要是通过 闪电贷价格操纵 来实现攻击,该协议试图通过Address.isContract
和nonContract
修饰符来防止用户在调用allocate()
时对价格进行操纵,此漏洞证明这种保护措施没有用。攻击者在构造合约时调用了allocate()
,绕过了 Contract 检查。
contract Allocator {
constructor(IBondingCurve bondingCurve) public {
// We run this call from a constructor
// to bypass the non-contract check of `allocate()`
bondingCurve.allocate();
}
}
在现在openzepplin doc有关 Address.isContract
的描述中,它明确指出,当返回为 0 时,存在这4种情况:
被销毁的合约
这表明,在某些情况下,extcodesize 指令可能无法检测到正在构建中的合约或者已经被销毁的合约,接下来我们对合约部署和销毁的流程进行探究,以便了解后面三中的情况。
首先让我们理解 State,State 是指存储在区块链上的所有账户的当前状态,包括它们的余额、合约代码、合约数据等信息。
总的来说,部署合约就是 EVM 通过调用 Create() 函数会创建一个新的合约地址,并且将合约代码存储在合约地址中;
size := extcodesize(account)
为0;constructor
函数,则执行constructor
函数对合约进行初始化;size := extcodesize(account)
变为非0,表示合约地址上存储了合约代码。合约的销毁通常是通过 Solidity 中的 selfdestruct(address payable recipient) 函数来触发的。当执行 selfdestruct 函数时,合约账户上剩余的以太币会被发送到指定的目标地址(recipient),同时合约的存储和代码会从状态中被移除。
这意味着合约的状态数据(包括存储的数据和代码)会被清除,合约地址上的 extcodesize 将会变为 0,因为该地址上已经不再存储任何代码。
综上,我们可以很清楚地了解到,在合约部署时,我们可以通过在construct()
函数里调用其他函数,绕过 isContract() 检查,大家可以结合Ethernaut-GateKeeperTwo进行学习,试着写写PoC来加深理解。
function isContract(address account) public view returns (bool) {
return (tx.origin != msg.sender);
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!