智能合約 Storage 注意事項
前言
前幾天在逛 Medium 的文章,意外看到這篇在討論如何利用 Ethereum Smart Contract Storage 實際行為跟預想的落差來攻擊。由於這個漏洞相較溢位等其他漏洞比較不會被注意到,決定記錄起來,如果大家未來在使用 Storage 時可以多注意一下。
Code
先來看看以下這個 contract:
這是一個幫助使用者 hodl Ether 的 smart contract。當使用者將 Ether 放進這個 contract 中,會被強制鎖定一段時間之後才能取出,在這段時間中使用者毋須擔心自己會受不了誘惑把錢拿去亂投資 ICO 導致血本無歸 (?)。
攻擊
問題出在 PayIn()
這個函式。當使用者想要存一些 Ether 進去,他會送出一筆存款的交易給這份 smart contract,但這筆存款交易其實不會記錄在使用者的 balance,而是會記錄到這份 smart contract owner 的 balance 中。
為什麼會這樣呢?
讓我們仔細看看 payIn()
在幹嘛,以下是他裡面在做的事:
HoldRecord newRecord;
newRecord.amount += msg.value;
newRecord.unlockTime = now + holdTime;
balance[msg.sender] = newRecord;
問題出在第一行,在 EVM (Ethereum Virtual Machine) 中,當沒有指定 storage/memory 時,預設會使用 storage,所以 newRecord
會是 storage 的 pointer。而 newRecord
又沒有給定初始值,那麼 newRecord
就會指向 address(0),也就是這份 contract 最一開始的地方。詳細行為可以參照 Solidity 的文件。
那麼本來 newRecord 是希望指到 HoldRecord 的 struct,藉此存取
uint amount;
uint unlockTime;
其實指到的是 contract 一開始的
uint ownerAmount;
uint numberOfPayouts;
所以在 payIn()
的newRecord.amount += msg.value;
其實是加到 ownerAmount
中,也當然 numberOfPayouts
也被覆蓋掉了。然後 owner 就可以用 ownerWithdrawal()
來將剛剛使用者存入的錢偷走。
反攻擊
但這邊其實有個反攻擊的方法。在 payIn()
的第四行
balance[msg.sender] = newRecord;
如前面所說,newRecord
是指到 ownerAmount
,所以這行其實是把 ownerAmount
指定給 msg.sender
,使用者可以用一個很短的鎖定時間存一筆很小的 Ether,然後在到期後馬上使用 withdraw()
將 owner 的所有錢提領出來。
結論
這個例子是要舉出在 smart contract 中 storage 預設行為的危險性。其實只要維持一個原則就可以避免這個問題。
養成明確定義使用 storage 還是 memory 的好習慣
一般來說,指定 storage 時就直接給初始值;而在 function 裡面需要用到的暫存器都用 memory,除非想要直接修改鏈上的值。現在 compiler 都會很聰明的提醒開發者要定義 storage 還是 memory,而當 storage pointer 沒有初始值時也會提醒開發者。
然而如果不是開發者只是使用者,其實在看 code 的時候很容易會忽略掉這部分的漏洞,所以在有更安全的選項出來前,就要多加注意了。
在此附上我的更改版,一批很純的Hodl (?)
如果覺得內容哪裡有誤,歡迎留言討論交流,然後別忘了分享給你所有很愛用預設參數的朋友 (?
如果覺得這篇文章不錯,也歡迎使用 LikeCoin 丟我,多多支持 LikeCoin!