赞
踩
上一篇博客中的babybank就是重入攻击的一道题,做之前学习了一下重入攻击,现在再来总结一下。
任何从合约 A 到合约 B 的交互以及任何从合约 A 到合约 B 的 以太币Ether 的转移,都会将控制权交给合约 B。 这使得合约 B 能够在交互结束前回调 A 中的代码。
官方文档里讲的就比较难懂(对于我这样的小白来说),简单来说就是,就是因为:
向以太坊合约账户进行转账,发送Ether的时候,会执行合约账户对应合约代码的回调函数(fallback)。
在以太坊智能合约中,进行转账操作,一旦向被攻击者劫持的合约地址发起转账操作,迫使执行攻击合约的回调函数,回调函数中包含回调自身代码,将会导致代码执行“重新进入”合约。这种合约漏洞,被称为“重入漏洞”。
引用一下ctfwiki的一个银行取款的例子:
contract Bank {
mapping(address => uint256) public balanceOf;
...
function withdraw(uint256 amount) public {
require(balanceOf[msg.sender] >= amount);
msg.sender.call.value(amount)();
balanceOf[msg.sender] -= amount;
}
}
首先要求账号的余额要大于等于需要取的钱。然后把这些代币转给调用者,然后银行的账本上把存储的钱减掉取的钱。
这里就出现了重入攻击。了解一下fallback函数:
官方文档的解释还是很难懂,而且这个fallback函数是新产生的,之前的是这样:
类似这样定义:
function() external payable{
require(flag==false);
flag=true;
bank.withdraw(2);
}
因此,如果银行取款的那个例子中,msg.sender是一个智能合约的话,msg.sender.call.value(amount)
调用call,会执行这个智能合约中的Fallback函数。这时候,如果这个智能合约是我们恶意构造的,它的fallback里又调用了这个withdraw方法的话,就相当于 ”重新进入“ 了 银行的那个合约中,因此叫”重入攻击“。
重新进入有什么危害呢?危害就在于,有些这样取款的代码,就如同这个例子中的那样:
msg.sender.call.value(amount)();
balanceOf[msg.sender] -= amount;
它是先取的款,然后再修改银行的合约中的那个记录了每个账号存了多少钱的状态变量balanceOf。
如果先call,然后进入恶意的fallback,fallback又调用了withdraw,这时候重新进入的withdraw里的balanceof[msg.sender]是还没有修改的,因此仍然可以再一次取款,成功盗取了更多的以太币。
构造的合约如下(摘录自ctfwiki):
contract Hacker { bool status = false; Bank b; constructor(address addr) public { b = Bank(addr); } function hack() public { b.withdraw(1 ether); } function() public payable { if (!status) { status = true; b.withdraw(1 ether); } } }
对于第一点:
//如果异常会转账失败,抛出异常(等价于requi(send()))(合约地址转账)
// 有gas限制,最大2300
<address payable>.transfer(uint256 amount)
//如果异常会转账失败,仅会返回false,不会终止执行(合约地址转账)
// 有gas限制,最大2300
<address payable>.send(uint256 amount) returns (bool)
如果异常会转账失败,仅会返回false,不会终止执行(调用合约的方法并转账)
// 没有gas限制
<address>.call(bytes memory) returns (bool, bytes memory)
因此call没有gas的限制,因此可以实现重入攻击。
对于第二点,上一篇文章中强网杯那题也正好是利用了这一点,利用合约的自销毁来强制转账。
此外ctfwiki也提到了:
重入漏洞与整数下溢出漏洞关联密切。在上述攻击后,攻击合约的存款由 1 ether 变为 -1 ether。但注意到存款由 uint256 保存,负数实际上保存为一个极大的正数,后续攻击合约可以继续使用这个大数额的存款。
官方文档是采用了”检查-生效-交互“的模式:
pragma solidity >=0.6.0 <0.9.0;
contract Fund {
/// 合约中 |ether| 分成的映射。
mapping(address => uint) shares;
/// 提取你的分成。
function withdraw() public {
var share = shares[msg.sender];
shares[msg.sender] = 0;
payable(msg.sender).transfer(share);
}
}
当然还有其他的方法。比如:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。