安全性是我们编写智能合约时要考量的最重要的因素之一。在智能合约编程领域,错误总是很轻易而又很昂贵地在发生。与其他程序一样,智能合约程序当然也会严格按照其程序逻辑来执行,虽然结果并不总是像程序员们所设想的那样。此外,所有智能合约都是公开可见的,任何人都可以简单地构造一个交易来与它们进行交互。
- 重入
以太坊智能合约的特性之一就是能够调用和使用其他外部合约的代码。合约通常也会用来处理以太币,因而也会经常将以太币发送到不同的外部用户地址。这些处理都需要合约提交外部调用。这些外部调用有可能会被攻击者劫持,迫使合约(通过回退函数)执行额外的代码,包括那些返回到合约自己代码的调用。 臭名昭著的DAO攻击中使用的就是这种方式
- 漏洞细节
以太坊智能合约的特点之一是合约之间可以进行相互间的外部调用。同时,以太坊的转账不仅局限于外部账户,合约账户同样可以拥有Ether,并进行转账等操作。
向以太坊合约账户进行转账,发送Ether的时候,会执行合约账户对应合约代码的回退函数(fallback)。一旦向攻击者合约地址发起转账操作,迫使执行攻击合约的回退函数,回退函数中包含回调被攻击者自身代码,将会导致代码执行“重新进入”合约。这种合约漏洞,被称为重入漏攻击Re-Entrancy。
以下面两个合约为例进行说明:
银行合约(Bank)吸纳储户存款,同时也响应储户的提款要求。逻辑很简单,但是有两处致命缺陷。
- 使用call函数,给不法分子进行重入攻击的机会。
Call函数有两个特性:
1.call函数会将所有剩余gas用于外部函数调用,为多次回调创造条件。
2.call函数如果调用失败只会返回false,而不回滚交易。
- 代码逻辑结构上考虑不周,用户余额信息在重入攻击过程中始终不会改变,为重入攻击提供了准入条件
攻击者合约(Hack)利用以太坊合约公开可见的特性,通过构造函数获取银行合约的实例,向银行存钱创造攻击的前提条件,最后通过fallback函数递归调用银行合约的提款函数,实现多次反复提款的目的。此外,攻击者合约还设置了一个记录调用栈层级的变量,防止因为调用栈层次过深导致交易异常造成回滚。文档中说明以太坊调用栈不能超过1024,否则出现异常,造成交易回滚,贪心不足蛇吞象,竹篮打水一场空。(此处有待细究)
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{ |