以太坊鏈審計報告之go-ethereum安全審計
近期,以太坊go-ethereum
公開了兩份審計報告,玄貓安全團隊第一時間對其進行了翻譯工作。此為第一篇《Go Ethereum Security Review》即2017-04-25_Geth-audit_Truesec
,此審計報告完成時間為2017年4月25日。如果您正在使用的是較舊版本的go-ethereum
,強烈建議升級至最新版本,以免遭受不必要的損失。
1.1. 概述
TrueSec
在2017年4月對以太坊的GO語言
實現進行了程式碼審計。審計結果表明程式碼質量是比較高的,且開發者具備一定的安全意識。在審計過程中沒有發現嚴重的安全漏洞。最嚴重的一個漏洞是當客戶端的RPC HTTP開啟時,web瀏覽器同源策略的繞過。其他發現的問題並沒有直接的攻擊向量可供利用,報告的其他部分為通用的評論和建議。
1.2. 目錄
1.2.1. P2P和網路
已知問題
記憶體分配過大
1.2.2. 交易和區塊處理
零除風險
程式碼複雜性
1.2.3. IPC和RPC介面
CORS:在HTTP RPC中預設允許所有域
1.2.4. Javascript引擎和API
偽隨機數生成器的弱隨機種子
1.2.5. EVM實現
濫用intPool
導致廉價的記憶體消耗
在挖礦區塊中脆弱的負值保護
1.2.6. 雜項
在挖礦程式碼中的條件競爭
許多第三方依賴
1.3. 結果細節
1.3.1. P2P和網路
TrueSec
對p2p
和網路部分的程式碼進行了審計,主要關注:
安全的通道實現 – 握手和共享secrets
的實現
安全的通道屬性 – 保密性和完整性
訊息的序列化
節點發現
對於DOS的防範:超時和訊息大小限制
TrueSec
還通過go-fuzz
對RLP解碼進行fuzz,沒有發現節點崩潰的現象。
1.3.1.1. 已知問題
雖然共享secrets
在encryption handshake
中實現得比較好,但是由於在對稱加密演算法中的two-time-pad
缺陷,使得通道缺乏保密性。這個是已知的問題。(詳情參考https://github.com/ethereum/devp2p/issues/32
和https://github.com/ethereum/go-ethereum/issues/1315
)。由於現在通道只傳輸公開的區塊鏈資料,這個問題暫時不必解決。
另外一個一直存在的問題是在安全的通道等級(在以太坊開發者討論中提到過一個預設的基於時間的重放保護機制)中缺乏重放保護
。TrueSec
建議協議的下一個版本通過控制訊息數量來實現重放保護
。
1.3.1.2. 記憶體分配過大
在rlpx.go
,TrueSec
發現兩個使用者可控的,過大的記憶體分配。TrueSec
沒有發現可以利用的DOS情景,但是建議恰當地對其進行驗證。
當讀取協議訊息時,16.8MB
大小的記憶體可以被分配:
func (rw *rlpxFrameRW) ReadMsg() (msg Msg, err error) { ... fsize := readInt24(headbuf) // ignore protocol type for now // read the frame content var rsize = fsize // frame size rounded up to 16 byte boundary if padding := fsize % 16; padding > 0 { rsize += 16 - padding } // TRUESEC: user-controlled allocation of 16.8MB: framebuf := make([]byte, rsize) ... }
由於以太坊協議中,對訊息大小的最大值定義為10MB
,TrueSec
推薦記憶體分配也定義為相同大小。
在encryption handshake
過程中,可以給握手資訊分配65KB
大小記憶體。
func readHandshakeMsg(msg plainDecoder, plainSize int, prv *ecdsa.PrivateKey, r io.Reader) ([]byte, error) { ... // Could be EIP-8 format, try that. prefix := buf[:2] size := binary.BigEndian.Uint16(prefix) if size < uint16(plainSize) { return buf, fmt.Errorf("size underflow, need at least ...") } // TRUESEC: user-controlled allocation of 65KB: buf = append(buf, make([]byte, size-uint16(plainSize)+2)...) ... }
除非握手訊息確實包含65KB
大小的資料,TrueSec
建議對握手訊息的大小作限制。
1.3.2. 交易和區塊處理
TrueSec
對交易和區塊下載,區塊處理的部分進行了程式碼審計,主要關注:
由記憶體分配,gorountine洩露
和IO操作
導致的拒絕服務
同步問題
1.3.2.1. 零除風險
在Go中,除以零會導致一個panic
。在downloader.go
的qosReduceConfidence
方法中,是否出現零除取決於呼叫者正確呼叫:
func (d *Downloader) qosReduceConfidence() { peers := uint64(d.peers.Len()) ... // TRUESEC: no zero-check of peers here conf := atomic.LoadUint64(&d.rttConfidence) * (peers - 1) / peers ... }
TrueSec
沒有發現可以導致節點崩潰的利用方式,但是僅僅依賴呼叫者來保證d.peers.Len()
不為零是不安全的。TrueSec
建議所有非常數的被除數應該在進行除法之前進行檢查。
1.3.2.2. 程式碼複雜性
TrueSec
發現交易和區塊處理的程式碼部分相對其他部分程式碼來說更加複雜,更難閱讀和審計。這部分的方法相對更大,在fetcher.go
,downloader.go
和blockchain.go
中有超過200行的程式碼。同步的實現有時候會結合互斥鎖和通道訊息。比如說,結構體Downloader
定義需要60行程式碼,包含3個互斥鎖和11個通道。
難以閱讀和理解的程式碼是滋生安全問題的肥沃土壤。特別是eth
包中存在一些程式碼量大的方法,結構體,介面與擴充套件的互斥鎖和通道。TrueSec
建議花一些功夫重構和簡化程式碼,來防止未來安全問題的發生。
1.3.3. IPC和RPC介面
TrueSec
對IPC和RPC(HTTP和Websocket)介面進行了審計,關注於潛在的訪問控制問題,從公共API提權到私有API(admin, debug等)的問題。
1.3.3.1. CORS:在預設的HTTP RPC裡允許所有域
HTTP RPC
介面可以通過geth
的--rpc
引數開啟。這會啟動一個web伺服器,用於監聽8545埠的HTTP請求,且任何人都可以對其進行訪問。由於潛在暴露埠的可能性(比如連線到不可信的網路),預設只有公共API允許HTTP RPC介面。
同源策略和預設的跨域資源共享(CORS)配置限制了web瀏覽器的訪問,並且限制通過XSS攻擊RPC API的可能性。allowed origins
能夠通過--rpccorsdomain "domain"
來配置,也可以通過逗號分隔來配置多個域名-- rpccorsdomain "domain1,domain2"
,或者配置為--rpccorsdomain "*"
,使得所有的域都可以通過標準web瀏覽器訪問。如果沒有進行配置,CORS頭將不會被設定——並且瀏覽器不會允許跨域請求:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:8545/. (Reason: CORS header 'Access-Control-Allow-Origin missing').
由於缺少CORS頭,Firefox
禁止跨域請求。
但是,在commit 5e29f4b 中(從2017年4月12日開始)——同源策略可以被繞過,RPC可以從web瀏覽器被訪問。
HTTP RPC的CORS配置被改變為處理allowed origins
的字元陣列——而不是在內部作為一個單引號分隔的字串傳輸。
在此之前,逗號分隔的字串被分成一個數組,在例項化cors
中介軟體之前(請見Listing 1)。with預設值(防使用者沒有顯性配置任何設定時,如使用–rpccorsdomain)空字串,這會導致一個字元陣列包含一個空字串。
在commit 5e29f4b
之後,預設值是一個空的陣列,這個陣列傳遞給位於newCorsHandler
的中介軟體cors
(請見 Listing 2)。
cors
中介軟體隨後檢查allowed origins
陣列的長度(請見 Listing 3)。如果長度為0,在這裡即代表空陣列,cors中介軟體
將會變成預設值並且允許所有域。
這個問題可以通過執行geth -rpc
來複現,不需要指定任何allowed origins
,並檢查commit 5e29f4b
前後帶有OPTION
請求的CORS頭。第二個輸出的Access-Control-Allow-Origin
值得注意。
注意即使是改變之前,這裡也是這樣。如果不是因為字串分割導致在cors
沒有解釋輸入值(一個數組包含一個空字串)為空。
這個問題可以通過下面的JavaScript
程式碼來利用,從任意域來執行(甚至可以是本地檔案系統,即無效或者null origin)
var xhr = new XMLHttpRequest(); xhr.open("POST", "http://localhost:8545", true); xhr.setRequestHeader("Content-Type", "application/json"); xhr.onreadystatechange = function() { if (xhr.readyState == XMLHttpRequest.DONE && xhr.status == 200) { console.log("Modules: " + xhr.responseText); } } xhr.send('{"jsonrpc":"2.0","method":"rpc_modules","params":[],"id":67}')
TrueSec
建議將CORS的預設配置進行顯性的限制,(如將allowed origin
設定為localhost
,或根本不設定CORS頭),而不是依賴外界來選擇一個正常(安全)的預設設定
165 func newCorsHandler(srv *Server, corsString string) http.Handler { 166var allowedOrigins []string 167for _, domain := range strings.Split(corsString, ",") { 168allowedOrigins = append(allowedOrigins, strings.TrimSpace(domain)) 169} 170c := cors.New(cors.Options{ 171AllowedOrigins: allowedOrigins, 172AllowedMethods: []string{"POST", "GET"}, 173MaxAge: 600, 174AllowedHeaders: []string{"*"}, 175}) 176return c.Handler(srv) 177 }
Listing 1: rpc/http.go, before commit 5e29f4be935ff227bbf07a0c6e80e8809f5e0202
164 func newCorsHandler(srv *Server, allowedOrigins []string) http.Handler { 165c := cors.New(cors.Options{ 166AllowedOrigins: allowedOrigins, 167AllowedMethods: []string{"POST", "GET"}, 168MaxAge: 600, 169AllowedHeaders: []string{"*"}, 170}) 171return c.Handler(srv) 172 }
Listing 2: rpc/http.go, after commit 5e29f4be935ff227bbf07a0c6e80e8809f5e0202
113// Allowed Origins 114if len(options.AllowedOrigins) == 0 { 115// Default is all origins 116c.allowedOriginsAll = true 117}
Listing 3: vendor/github.com/rs/cors/cors.go
$ curl -i -X OPTIONS -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: content-type" -H "Origin: foobar" http://localhost:8545 HTTP/1.1 200 OK Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Date: Tue, 25 Apr 2017 08:49:10 GMT Content-Length: 0 Content-Type: text/plain; charset=utf-8
Listing 4: CORS headers before commit 5e29f4b
$ curl -i -X OPTIONS -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: content-type" -H "Origin: foobar" http://localhost:8545 HTTP/1.1 200 OK Access-Control-Allow-Headers: Content-Type Access-Control-Allow-Methods: POST Access-Control-Allow-Origin: foobar Access-Control-Max-Age: 600 Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Date: Tue, 25 Apr 2017 08:47:24 GMT Content-Length: 0 Content-Type: text/plain; charset=utf-8
Listing 5: CORS headers after commit 5e29f4b
1.3.4. JavaScript引擎和API
JavaScript引擎otto
是Go Ethereum中的CLI指令碼介面,一個IPC/RPC介面的終端互動直譯器,也是私有debug API
的一部分。考慮到其程式碼有限,在審計中優先順序比較低。
1.3.4.1. 偽隨機數生成的弱隨機數種子
在jsre
中對偽隨機數生成器進行初始化的時候,如果crypto/rand
(crypto/rand
返回密碼學安全地偽隨機數)方法失敗,隨機數種子將會依賴於當時的UNIX時間。在listing 6中,這個弱隨機數種子將會被用於初始化math/rand
的例項。
這個PRNG
沒有被用於任何敏感資訊,而且顯然也不應該被用作於密碼學安全的RNG
,但是由於使用者可以通過命令列執行指令碼來使用PRNG
,使其失敗而不是製造出弱隨機數種子顯然是更安全的。從crypto/rand
中得到錯誤意味著其他地方可能也存在問題。即使是得到了安全的隨機數種子,在文件中也應該指出PRNG
並不是密碼學安全的。
84 // randomSource returns a pseudo random value generator. 85 func randomSource() *rand.Rand { 86bytes := make([]byte, 8) 87seed := time.Now().UnixNano() // 不是完全隨機 88if _, err := crand.Read(bytes); err == nil { 89seed = int64(binary.LittleEndian.Uint64(bytes)) 90} 91 92src := rand.NewSource(seed) 93return rand.New(src) 94 }
Listing 6: internal/jsre/jsre.go
1.3.5. 以太坊虛擬機器(EVM)的實現
TrueSec
對以太坊虛擬機器(EVM)部分的程式碼進行了審計,主要關注由濫用記憶體分配和IO操作而引起的拒絕服務。EVM直譯器(runtime/fuzz.go)存在一個go-fuzz
的入口點,這個入口點成功地被使用。TrueSec
確認了其功能性,但是在fuzzing過程中沒有發現有影響的漏洞。
1.3.5.1. 濫用intPool導致的廉價的記憶體消耗
由於效能的原因,在EVM的執行過程中,使用大整數會進入整數池intPool
(intpool.go)。由於沒有對整數池大小進行限制,使用特定的opcode
組合,將導致意外出現廉價使用記憶體的情況。
0 JUMPDEST// 1 gas 1 COINBASE// 2 gas 2 ORIGIN// 2 gas 3 EQ// 3 gas, puts 20 + 20 bytes on the intpool 4 JUMP// 8 gas, puts 4-8 bytes on the intpool
比如說,合約程式碼將會消耗3.33e9單位的gas(在當時大約價值3300USD),分配10G記憶體給intPool
。以太坊虛擬機器中分配10GB記憶體的預期gas
成本是1.95e14(大約195,000,000USD)
當intPool
產生out of memory panic
時,會導致拒絕服務攻擊。但是共識演算法對gaslimit
進行了限制,能夠阻止該拒絕服務攻擊的發生。但是考慮到攻擊者可能發現一種更有效的填充intPool
的方式,或者gaslimit target
增長過於迅速等,TrueSec
仍然推薦對intPool
的大小進行限制。
1.3.5.2. 在挖礦區塊中脆弱的負值保護
賬戶之間以太坊的轉賬是通過core/evm.go
裡的Transfer
方法進行的。
func Transfer(db vm.StateDB, sender, recipient common.Address, amount *big.Int) { db.SubBalance(sender, amount) db.AddBalance(recipient, amount) }
輸入amount
是一個指向有符號型別的指標,可能存在負的引用值。一個負的amount
將會把以太坊從收款方轉移到轉賬方,使得轉賬方可以從收款方那裡盜竊以太坊。
當接收到一個沒有被打包的交易時,將會驗證交易的值是否為正。如tx_pool.go, validateTx()
:
if tx.Value().Sign() < 0 { return ErrNegativeValue }
但是在區塊處理過程中卻不存在這樣顯性的驗證;存在負值的交易只是隱性地被p2p
序列化格式(RLP)阻止,而RLP不能解碼負值。假設一個邪惡的礦工為了非法獲取以太坊,釋出了具有負值交易的區塊,這時依賴於特定的序列化格式來提供保護,似乎有些脆弱。TrueSec
推薦在區塊處理過程中也顯性地檢查交易的值。或者使用無符號型別來強制指定交易的值為正。
1.3.6. 雜項
1.3.6.1. 在挖礦程式碼中的條件競爭
TrueSec
使用”-race”來構建標誌位,並通過Go
語言內建的條件競爭探測特性來尋找條件競爭。在ethash/ethash.go
中發現了一個與在挖礦時使用的ethash datasets
時間戳相關的條件競爭。
func (ethash *Ethash) dataset(block uint64) []uint32 { epoch := block / epochLength // If we have a PoW for that epoch, use that ethash.lock.Lock() ... current.used = time.Now() // TRUESEC: race ethash.lock.Unlock() // Wait for generation finish, bump the timestamp and finalize the cache current.generate(ethash.dagdir, ethash.dagsondisk, ethash.tester) current.lock.Lock() current.used = time.Now() current.lock.Unlock() ... }
為了去除條件競爭,通過使用current.lock
互斥鎖可以保護第一個current.used
的設定。
TrueSec
沒有研究條件競爭是否會對節點的挖礦造成影響。
1.3.6.2. 過多第三方依賴
Go Ethereum
依賴於71個第三方包(通過govendor list +vend
列舉)
由於每個依賴都可能引入新的攻擊向量,並且需要時間和精力來監控安全漏洞,TrueSec
總是建議將第三方包的數量控制到最小。
71個依賴對任何一個專案來說都是比較多的。TrueSec
推薦以太坊開發者調研是否所有的依賴都是真正需要的,或者說其中一些是否可以用程式碼來替代。
1.4. 附錄
1.4.1. 宣告
我們努力提供準確的翻譯,可能有些部分不太準確,部分內容不太重要並沒有進行翻譯,如有需要請參見原文。
1.4.2. 原文地址
https://github.com/ethereum/go-ethereum/blob/master/docs/audits/2017-04-25_Geth-audit_Truesec.pdf
1.4.3. 參考連結
參考專案 | URL地址 |
---|---|
go ethereum | https://ethereum.github.io/go-ethereum/ |
go fuzz | https://github.com/dvyukov/go-fuzz/ |
commit 5e29f4 | https://github.com/ethereum/go-ethereum/commit/5e29f4be935ff227bbf07a0c6e80e8809f5e0202 |
cors中介軟體 | https://github.com/rs/cors |
otto | https://github.com/robertkrimen/otto |
*參考來源:github ,Javierlev@玄貓安全團隊編譯整理,轉載請註明來自 FreeBuf.COM。