閒聊加密貨幣最普遍的攻擊手法:重入攻擊(Re-Entrancy Attack)

James Shieh
技術保鮮盒
Published in
Dec 24, 2021

本文聊聊智能合約(Smart Contract)中經典的攻擊手法之一Re-Entrancy Attack,並嘗試用比較淺顯易懂的方式去解說攻擊原理,希望讓非工程師背景的人也能聽明白。

首先,項目方撰寫的智能合約,其中關於出金的部分,其實就是一種ATM與提款卡(DebitCard)的概念,所以我們就直接把項目方負責提款與存款的合約代換成ATM,而負責代理提款相關功能的合約則代換成DebitCard,合約大致會是這樣的邏輯:

如果我想要領1000元出來,就是調用ATM.withdraw(1000)這個function。這個function會先檢查餘額夠不夠,然後才用transfer()方法將1000元轉帳至我的帳戶裡,接著還會調用一個叫做DebitCard.receiveMoney()的方法,處理相關的扣款業務(我們就簡單理解為銀行處理帳務的SOP吧),最後才把1000元從使用者的帳戶餘額中扣除。

看起來非常正常,對吧?

但攻擊者可以用這樣的方式無限提款:

為什麼呢?因為攻擊者製造了一個循環——會一直不斷重複withdraw()與receiveMoney():

```
備註:
為了簡化圖解,我們在ATM Contract中用
checkBalance()代替原本程式碼中的require( checkIfBalanceIsEnough() ),同理:
transfer()代替transfer(amount);
updateBalance()代替balance[user_id] = balance[user_id] — amount;
而Attracker Contract中用
reachTarget()代替 stolen_money < withdraw_target
withdraw()代替ATM(ATM_address).withdraw(amount);
```

攻擊者在Step.1時調用ATM的withdraw() function,並且製造了一個不正確的function入口(Step.2),導致這個transaction在還沒updateBalance()之前就不斷調用攻擊者合約中的receiveMoney() function,也就導致Step.1與Step2不斷重複。直到滿足結束條件(step.n) 也就是提款1000萬,才結束這個function,然後最後在step.n+1時才更新攻擊者的餘額。如此便可以領出超額的存款。當然這個攻擊有個前提,就是攻擊者必須先存入100萬,讓第一次進入ATM時可以通過餘額檢查。

以上就是整個攻擊手法的原理。

避免這個攻擊的方法就是用交易鎖(transaction lock),這個方法在SQL中很普遍:

攻擊者進入withdraw()提款時的條件是它的lock要等於false,一旦開始提款交易,就會把使用者的lock設成true。但直到updateBalance()完成前,lock都會是true的狀態。這樣攻擊者就沒辦法re-entrance the function了。

或者我們可以直接使用OpenZeppelin的ReentracyGuard,基本上它實現了交易鎖的機制,可以簡單地避免這個漏洞產生:

其中withdraw()變成了:

function withdraw(unit256 amount) nonReentrant() external{ ...

也許對於非智能合約工程師的投資人,想了解項目方是否有漏洞,其中一個簡單明瞭的方法就是看項目方的關鍵函數有沒有加上nonReentrant()這個Modifier:

--

--

James Shieh
技術保鮮盒

Find something more important than you are and dedicate your life to it.