文章來源:廖雪峯
作者:廖雪峯

最近爆發的層出不窮的區塊鏈安全問題,從以太坊 RPC 攻擊轉移用戶資產,到 ERC20 代幣頻繁爆出溢出漏洞,這些問題其實大部分應該在區塊鏈系統的設計階段解決,而不是留給開發者來自己關注安全問題。

我們先來看一下以太坊的設計。

首先值得肯定的是,有別於比特幣 UTXO 模型,以太坊設計了賬戶模型。賬戶模型對於實現智能合約是十分合理而且非常必要的,直接在 UTXO 模型上嫁接智能合約的做法顯得不倫不類。

然而,以太坊的設計仍然存在不少問題,有許多設計問題實際上是引發安全漏洞的源頭。如果在設計時避免了這些問題,至少不會大面積爆發各種安全問題。

RPC 安全

以太坊的一個重大安全缺陷是由 RPC 遠程調用引起的。去年以來,黑客利用遠程掃描工具或者植入木馬,來嘗試連接暴露在公網的以太坊節點,然後,不斷嘗試通過 sendTransaction 這個 RPC 把該節點的所有以太幣轉移到黑客的地址。

這個操作雖然需要私鑰,然而黑客卻可以繞開私鑰,原因就在於用戶正常發送交易時,無論是在錢包輸入口令,還是通過 web3 的 JS,都間接調用了 geth 提供的 web3 的 unlockAccount 命令,這個命令一旦被用戶觸發,在接下來的一段時間內,黑客的 sendTransaction 無需私鑰就能成功。並且,由於失敗的 sendTransaction 不會被寫入日誌,用戶幾乎無法發現自己的全節點被黑客盯上了。如果黑客利用木馬監聽全節點的網絡通信,完全可以通過 unlockAccount 獲取用戶口令,而以太坊的 RPC 是沒有任何加密的。

這個安全漏洞實際上完全可以從設計避免。以太坊的錢包仿照了比特幣錢包的設計,它實際上把全節點功能和錢包合二爲一,用戶私鑰以加密形式保存在全節點中。中本聰最早設計的這種內置錢包的比特幣全節點實際上是有嚴重安全問題的,但是,以太坊和大部分公鏈的開發者都照抄了這個設計。當用戶進行正常轉賬時,錢包實際上和全節點的交互如下:

區塊鏈安全,要從設計抓起

用戶正常創建交易之前,需要調用 unlockAccount 來解鎖私鑰,然而,這個極其危險而重要的操作本質上是一個 RPC 調用。黑客能利用 sendTransaction 原因就在於,全節點不應該提供任何錢包的功能。全節點工作在 P2P 和共識層,而錢包工作在應用層,私鑰理應由錢包管理,而不是全節點管理。

區塊鏈安全,要從設計抓起

由錢包管理的加密私鑰就不需要暴露在網絡上,並且,從用戶輸入口令,創建交易,簽名交易這一過程,根本不需要 RPC 調用,只有最後一步 sendRawTransaction 才需要 RPC 調用。sendRawTransaction 發送的是待廣播的已簽名交易,因此,這個數據被黑客截獲是沒有用的。

雖然比特幣和以太坊的全節點提供了 disableWallet 這個選項來禁用私鑰存儲,但是問題在於,全節點根本就不應該提供存儲私鑰的功能。全節點也不應該提供 sendTransaction,全節點只能提供 sendRawTransaction。有個別公鏈居然只提供 sendTransaction 而不提供 sendRawTransaction,可見其工程實現的安全之差。

合約調用的容錯性

以太坊的另一個重大漏洞允許任何用戶進行 ERC20 的超額轉賬。該漏洞利用原理如下:

轉移代幣實際上就是調用 ERC20 合約的 transfer(address to, unit256 amount) 方法。該方法一共有兩個參數:地址和數量。兩個參數實際上都是 32 字節整數。調用該方法時,用戶創建的 68 字節交易數據如下:

  • 4 字節方法哈希,總是 a9059cbb;
  • 32 字節以太坊地址,由於以太坊地址總是 20 字節,因此高位補 0,例如:000000000000000000000000abcabcabcabcabcabcabcabcabcabcabcabcabca;
  • 32 字節代幣數量,例如:00000000000000000000000000000000000000000000000000000000000000ff。

加在一起的交易數據就是:

a9059cbb
000000000000000000000000abcabcabcabcabcabcabcabcabcabcabcabcabca
00000000000000000000000000000000000000000000000000000000000000ff

然而,以太坊的地址末尾如果是 0,用戶輸入的地址少於 20 字節時,以太坊會自動給它「補零」。利用以太坊虛擬機的這一「容錯性」機制,可以創建一個惡意轉賬數據:

a9059cbb
000000000000000000000000abcabcabcabcabcabcabcabcabcabcabcabcab
00000000000000000000000000000000000000000000000000000000000000ff

上述交易數據只有 67 字節,原因是地址末尾少了一個 0 字節。然而以太坊虛擬機並不會報錯,轉賬也會成功。更令人驚奇的是,以太坊虛擬機從後面的參數「借」了一個 0,然後,在末尾自動補充 0,所以,實際參數變成了:

a9059cbb
000000000000000000000000abcabcabcabcabcabcabcabcabcabcabcabcab00
000000000000000000000000000000000000000000000000000000000000ff00

注意到 ff 後面的 0 是以太坊虛擬機自動補上的,這樣一來,amount 從 ff 變成了 ff00,轉賬金額擴大了 256 倍。

通過計算一個末尾帶 0 的地址,黑客就可以對交易所發起攻擊。原理是交易所構造的轉賬交易是按 a9059cbb+用戶提現地址+金額拼接而成。黑客先填寫不足 20 字節的地址,如果交易所未檢查地址長度,黑客通過申請一個 ff 金額的提現,實際到賬金額是 ff00。

這個鍋由以太坊虛擬機來背一點也不冤。檢查非法輸入是任何高級語言必須提供的基本功能。

Batch Overflow

最近爆出的 ERC20 代幣的 Batch Overflow 漏洞看上去是開發者的問題:

function batchTransfer(address[] receivers, unit256 value) {
    unit256 amount = receivers.length * value
    require(value>0 && balances[msg.sender] >= amount)
    ...
}

漏洞代碼在於計算總額 amount = receivers.length * value 時,輸入一個非常大的 value 可能導致計算結果爲負數,也就是整數計算溢出,從而導致後續轉賬成功,黑客憑空爲自己轉移出天量代幣。

當然可以指責開發者沒有編寫出安全的代碼,資深 Solidity 開發者還會說凡是涉及計算都應該使用 SafeMath 這個專爲合約開發的計算庫。SafeMath 會檢查整數計算溢出,例如:

function add(uint256 a, uint256 b) {
    uint256 c = a + b;
    assert(c >= a);
    return c;
}

問題是,以太坊合約是一種非常高級的代碼,虛擬機本身理應對所有整數運算自動檢查溢出,而不是把責任推給開發者。沒有任何人喜歡編寫 c = add(a, b) 這樣的代碼。Java 虛擬機就通過禁用指針、數組索引檢查、運行時類型檢查等內置安全機制,有效提升了程序的健壯性。如果以太坊虛擬機內置了整數運算溢出檢查,這個微小的工作就足以讓 95% 的合約安全問題不復存在。

類似的問題還包括:合約沒有真正的「所有者」,造成合約代碼無法升級或者暫停。(目前的暫停機制也是合約邏輯的一部分,而不是以太坊合約機制的一部分)。新的智能合約公鏈應該在設計時儘量避免潛在的安全問題,從虛擬機上堵住惡意攻擊,而不是一味教育開發者編寫「安全」的代碼。