本篇将为大家介绍一下以太坊智能合约的 拒绝服务、可预测随机数、不安全函数返回值、构造函数拼写错误等引起的漏洞,接下来依据漏洞的类型分别为大家做介绍。
1. 拒绝服务攻击
拒绝服务顾名思义就是通过制造错误返回来拒绝为其他人提供服务,那么在以太坊的智能合约是怎么做到的呢?下面先来看一份智能合约代码:
pragma solidity ^0.4.10;
contract PresidentOfCountry {
address public president;
uint256 price;
function PresidentOfCountry(uint256 _price) {
require(_price > 0);
price = _price;
president = msg.sender;
}
function becomePresident() payable {
require(msg.value >= price);
president.transfer(price);
president = msg.sender;
price = price * 2;
}
}
上述智能合约的代码逻辑很简单,就是谁的出价高,谁就会成为新的president ,并将智能合约目前拥有的price以太币通过transfer()函数返回给前任的president的地址。
这份合约看起来貌似没有什么问题,但仔细思考,以太坊的账户有合约账户和普通账户。如果普通用户调用这份合约没有什么问题,如果是恶意攻击的合约账户呢?
我们都知道,合约账户的以太币转账都是通过 fallback() 函数进行的,如果攻击者在 fallback()代码中恶意进行了类似 revert() 这样主动跑出错误的操作,那么其他账户也就无法再正常通过 becomePresident()函数逻辑成为 president 。下面给出恶意攻击的合约代码:
pragma solidity ^0.4.10;
contract Attack {
function () { revert(); }
function Attack(address _target) payable {
_target.call.value(msg.value)(bytes4(keccak256("becomePresident()")));
}
}
2. 可预测随机数攻击
可预测随机数顾名思义就是随机数是可以预测的即为伪随机数。伪随机问题一直都存在于现代计算机系统中,但是在开放的区块链中,像在以太坊智能合约中编写的基于随机数的处理逻辑感觉就有点不切实际了,由于人人都能访问链上数据,合约中的存储数据都能在链上查询分析得到。如果合约代码没有严格考虑到链上数据公开的问题去使用随机数,可能会被攻击者恶意利用来进行 “作弊”。下面介绍一段示例代码:
uint256 private seed;
function play() public payable {
require(msg.value >= 1 ether);
iteration++;
uint randomNumber = uint(keccak256(seed + iteration));
if (randomNumber % 2 == 0) {
msg.sender.transfer(this.balance);
}
}
在上述代码中 seed 变量被标记为了私有变量,前面有说过链上的数据都是公开的,seed 的值可以通过扫描与该合约相关的 TX 来获得。获取 seed 值后,同样的 iteration 值也是可以得到的,那么整个 uint(keccak256(seed + iteration)) 的值就是可预测了。攻击者可以利用可以预见的随机数在逻辑上做对应的攻击来获取利益。
3. 不安全函数返回值攻击
由于合约代码调用底层的函数,但没有对返回值进行判断,函数执行错误后续的代码还继续执行,从而形成漏洞。下面来介绍一下常用的底层函数及其用法:
call()函数:call() 用于 Solidity 进行外部调用,例如调用外部合约函数 address.call(bytes4(keccak(“somefunc(params)”), params)),外部调用 call() 返回一个 bool 值来表明外部调用成功与否。
delegatecall():除了 delegatecall() 会将外部代码作直接作用于合约上下文以外,其他与 call() 一致,同样也是只能获取一个 bool 值来表示调用成功或者失败(发生异常)。
callcode()函数:callcode() 其实是 delegatecall() 之前的一个版本,两者都是将外部代码加载到当前上下文中进行执行,但是在 msg.sender 和 msg.value 的指向上却有差异。
call.value()()函数:在合约中直接发起 TX 的函数之一。
send()函数:通过 send() 函数发送 Ether 失败时直接返回 false;这里需要注意的一点就是,send() 的目标如果是合约账户,则会尝试调用它的 fallbcak() 函数,fallback() 函数中执行失败,send() 同样也只会返回 false。但由于只会提供 2300 Gas 给 fallback() 函数,所以可以防重入漏洞(恶意递归调用)。
transfer()函数:transfer() 也可以发起 Ether 交易,但与 send() 不同的时,transfer() 是一个较为安全的转币操作,当发送失败时会自动回滚状态,该函数调用没有返回值。同样的,如果 transfer() 的目标是合约账户,也会调用合约的 fallback() 函数,并且只会传递 2300 Gas 用于 fallback() 函数执行,可以防止重入漏洞(恶意递归调用)。
下面来给出一个示例代码来说明未判断函数的返回值操作的漏洞:
function _send(uint256 _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] -= _amount;
etherLeft -= _amount;
msg.sender.send(_amount); // 未验证 send() 返回值,若 msg.sender 为合约账户 fallback() 调用失败,则 send() 返回 false
}
4. 构造函数拼写错误攻击
构造函数拼写错误顾名思义就是智能合约的构造函数在拼写时,函数名拼写错误。大家都知道,一个智能合约只能拥有一个构造函数,在部署智能合约的时候执行一次,之后不再执行。在某些时候,由于开发人员的疏忽,将智能合约的构造函数拼写错误造成灾难性的后果,下面给出一段示例代码:
pragma solidity ^0.4.16;
//基类合约
contract Ownable {
address public owner;
function ownable() public {
owner = msg.sender;
//构造函数,对owner进行赋值,部署合约的发起方
}
...
上述代码中,由于构造函数 function ownable() public的拼写错误(第一次字母应该是大写O),本来应该是构造函数变成一个对外可调用的普通函数,任何一个账户都可以调用这段代码,将自己的账户地址设置为owner。
在后续的Soildity-Remix代码编辑器,针对智能合约的构造函数增加了 construct 关键字进行修饰,具体使用请大家自行查阅资料。