作者:極光@知道創宇404區塊鏈安全研究團隊
時間:2020年8月27日
前言
隨著區塊鏈技術的發展,越來越多的個人及企業也開始關注區塊鏈,而和區塊鏈聯系最為緊密的,恐怕就是金融行業了。 然而雖然比特幣區塊鏈大受熱捧,但畢竟比特幣區塊鏈是屬于公有區塊鏈,公有區塊鏈有著其不可編輯,不可篡改的特點,這就使得公有鏈并不適合企業使用,畢竟如果某金融企業開發出一個區塊鏈,無法受其主觀控制,那對于它的意義就不大。因此私有鏈就應運而生,但私有鏈雖然能夠解決以上的問題,如果僅僅只是各個企業自己單獨建立,那么還將是一個個孤島。如果能夠聯合起來開發私有區塊鏈,最好不過,聯盟鏈應運而生。
目前已經有了很多的聯盟鏈,比較知名的有Hyperledger。超級賬本(Hyperledger)是Linux基金會于2015年發起的推進區塊鏈數字技術和交易驗證的開源項目,加入成員包括:IBM、Digital Asset、荷蘭銀行(ABN AMRO)、埃森哲(Accenture)等十幾個不同利益體,目標是讓成員共同合作,共建開放平臺,滿足來自多個不同行業各種用戶案例,并簡化業務流程。
為了提升效率,支持更加友好的設計,各聯盟鏈在智能合約上出現了不同的發展方向。其中,Fabric聯盟鏈平臺智能合約具有很好的代表性,本文主要分析其智能合約安全性,其他聯盟鏈平臺合約亦如此,除了代碼語言本身的問題,也存在系統機制安全,運行時安全,業務邏輯安全等問題。
Fabric智能合約
Fabric的智能合約稱為鏈碼(chaincode),分為系統鏈碼和用戶鏈碼。系統鏈碼用來實現系統層面的功能,用戶鏈碼實現用戶的應用功能。鏈碼被編譯成一個獨立的應用程序,運行于隔離的Docker容器中。 和以太坊相比,Fabric鏈碼和底層賬本是分開的,升級鏈碼時并不需要遷移賬本數據到新鏈碼當中,真正實現了邏輯與數據的分離,同時,鏈碼采用Go、Java、Nodejs語言編寫。
數據流向
Fabric鏈碼通過gprc與peer節點交互
(1)當peer節點收到客戶端請求的輸入(propsal)后,會通過發送一個鏈碼消息對象(帶輸入信息,調用者信息)給對應的鏈碼。
(2)鏈碼調用ChaincodeBase里面的invoke方法,通過發送獲取數據(getState)和寫入數據(putState)消息,向peer節點獲取賬本狀態信息和發送預提交狀態。
(3)鏈碼發送最終輸出結果給peer節點,節點對輸入(propsal)和 輸出(propsalreponse)進行背書簽名,完成第一段簽名提交。
(4)之后客戶端收集所有peer節點的第一段提交信息,組裝事務(transaction)并簽名,發送事務到orderer節點排隊,最終orderer產生區塊,并發送到各個peer節點,把輸入和輸出落到賬本上,完成第二段提交過程。
鏈碼類型
- 用戶鏈碼
由應用開發人員使用Go(Java/JS)語言編寫基于區塊鏈分布式賬本的狀態及處理邏輯,運行在鏈碼容器中, 通過Fabric提供的接口與賬本平臺進行交互
- 系統鏈碼
負責Fabric節點自身的處理邏輯, 包括系統配置、背書、校驗等工作。系統鏈碼僅支持Go語言, 在Peer節點啟動時會自動完成注冊和部署。
部署
可以通過官方 Fabric-samples 部署test-network,需要注意的是國內網絡環境對于Go編譯下載第三方依賴可能出現網絡超時,可以參考 goproxy.cn 解決,成功部署后如下圖:

語言特性問題
不管使用什么語言對智能合約進行編程,都存在其對應的語言以及相關合約標準的安全性問題。Fabric 智能合約是以通用編程語言為基礎,指定對應的智能合約模塊(如:Go/Java/Node.js)
- 不安全的隨機數
隨機數應用廣泛,最為熟知的是在密碼學中的應用,隨機數產生的方式多種多樣,例如在Go程序中可以使用 math/rand 獲得一個隨機數,此種隨機數來源于偽隨機數生成器,其輸出的隨機數值可以輕松預測。而在對安全性要求高的環境中,如 UUID 的生成,Token 生成,生成密鑰、密文加鹽處理。使用一個能產生可能預測數值的函數作為隨機數據源,這種可以預測的數值會降低系統安全性。

偽隨機數是用確定性的算法計算出來自[0,1]均勻分布的隨機數序列。 并不真正的隨機,但具有類似于隨機數的統計特征,如均勻性、獨立性等。 在計算偽隨機數時,若使用的初值(種子)不變,這里的“初值”就是隨機種子,那么偽隨機數的數序也不變。在上述代碼中,通過對比兩次執行結果都相同。

通過分析rand.Intn()的源碼,可見,在”math/rand” 包中,如果沒有設置隨機種子, Int() 函數自己初始化了一個 lockedSource 后產生偽隨機數,并且初始化時隨機種子被設置為1。因此不管重復執行多少次代碼,每次隨機種子都是固定值,輸出的偽隨機數數列也就固定了。所以如果能猜測到程序使用的初值(種子),那么就可以生成同一數序的偽隨機數。
fmt.Println(rand.Intn(100)) //
fmt.Println(rand.Intn(100)) //
fmt.Println(rand.Float64()) // 產生0.0-1.0的隨機浮點數
fmt.Println(rand.Float64()) // 產生0.0-1.0的隨機浮點數
jiguang@example$ go run unsafe_rand.go
81
87
0.6645600532184904
0.4377141871869802
jiguang@example$ go run unsafe_rand.go
81
87
0.6645600532184904
0.4377141871869802
jiguang@example$
- 不當的函數地址使用
錯誤的將函數地址當作函數、條件表達式、運算操作對象使用,甚至參與邏輯運算,將導致各種非預期的程序行為發生。比如在如下if語句,其中func()為程序中定義的一個函數:
if (func == nil) {
...
}
由于使用func而不是func(),也就是使用的是func的地址而不是函數的返回值,而函數的地址不等于nil,如果用函數地址與nil作比較時,將使其條件判斷恒為false。
- 資源重釋放
defer 關鍵字可以幫助開發者準確的釋放資源,但是僅限于一個函數中。 如果一個全局對象中存儲了大量需要手動釋放的資源,那么編寫釋放函數時就很容易漏掉一些釋放函數,也有可能造成開發者在某些條件語句中提前進行資源釋放。

- 線程安全
很多時候,編譯器會做一些神奇的優化,導致意想不到的數據沖突,所以,只要滿足“同時有多個線程訪問同一段內存,且其中至少有一個線程的操作是寫操作”這一條件,就需要作并發安全方面的處理。

- 內存分配
對于每一個開發者,內存是都需要小心使用的資源,內存管理不慎極容易出現的OOM(OutOfMemoryError),內存泄露最終會導致內存溢出,由于系統中的內存是有限的,如果過度占用資源而不及時釋放,最后會導致內存不足,從而無法給所需要存儲的數據提供足夠的內存,從而導致內存溢出。導致內存溢出也可能是由于在給數據分配大小時沒有根據實際要求分配,最后導致分配的內存無法滿足數據的需求,從而導致內存溢出。
var detailsID int = len(assetTransferInput.ID)
assetAsBytes := make([]int, detailsID)
如上代碼,assetTransferInput.ID為用戶可控參數,如果傳入該參數的值過大,則make內存分配可能導致內存溢出。
- 冗余代碼
有時候一段代碼從功能上、甚至效率上來講都沒有問題,但從可讀性和可維護性來講,可優化的地方顯而易見。特別是在需要消耗gas執行代碼邏輯的合約中。
if len(assetTransferInput.ID) < 0 {
return fmt.Errorf("assetID field must be a non-empty")
}
if len(assetTransferInput.ID) == 0 {
return fmt.Errorf("assetID field must be a non-empty")
}
運行時安全
- 整數溢出
不管使用的何種虛擬機執行合約,各類整數類型都存在對應的存儲寬度,當試圖保存超過該范圍的數據時,有符號數就會發生整數溢出。
涉及無符號整數的計算不會產生溢出,而是當數值超過無符號整數的取值范圍時會發生回繞。如:無符號整數的最大值加1會返回0,而無符號整數最小值減1則會返回該類型的最大值。當無符號整數回繞產生一個最大值時,如果數據用于如 []byte(string),string([]byte) 類的內存拷貝函數,則會復制一個巨大的數據,可能導致錯誤或者破壞堆棧。除此之外,無符號整數回繞最可能被利用的情況之一是用于內存的分配,如使用 make() 函數進行內存分配時,當 make() 函數的參數產生回繞時,可能為0或者是一個最大值,從而導致0長度的內存分配或者內存分配失敗。

智能合約中GetAssetPrice函數用于返回當前計算的差價,第228可知,gas + rebate可能發生溢出,uint16表示的最大整數為65535,即大于這個數將發生無符號回繞問題:
var gas uint16 = uint16(65535)
var rebate uint16 = uint16(1)
fmt.Println(gas + rebate) // 0
var gas1 uint16 = uint16(65535)
var rebate2 uint16 = uint16(2)
fmt.Println(gas1 + rebate2) // 1

- 除數為零
代碼基本算數運算過程中,當出現除數為零的錯誤時,通常會導致程序崩潰和拒絕服務漏洞。

在CreateTypeAsset函數的第64行,通過傳入參數appraisedValue來計算接收資產類型值,實際上,當傳入參數appraisedValue等于17時,將發生除零風險問題。

- 忽略返回值
一些函數具有返回值且返回值用于判斷函數執行的行為,如判斷函數是否執行成功,因此需要對函數的返回值進行相應的判斷,以 strconv.Atoi 函數為例,其原型為:
func Atoi(s string) (int, error)如果函數執行成功,則返回第一個參數 int;如果發生錯誤,則返回 error,如果沒有對函數返回值進行檢測,那么當讀取發生錯誤時,則可能因為忽略異常和錯誤情況導致允許攻擊者引入意料之外的行為。

- 空指針引用
指針在使用前需要進行健壯性檢查,從而避免對空指針進行解引用操作。試圖通過空指針對數據進行訪問,會導致運行時錯誤。當程序試圖解引用一個期望非空但是實際為空的指針時,會發生空指針解引用錯誤。對空指針的解引用會導致未定義的行為。在很多平臺上,解引用空指針可能會導致程序異常終止或拒絕服務。如:在 Linux 系統中訪問空指針會產生 Segmentation fault 的錯誤。
func (s *AssetPrivateDetails) verifyAgreement(ctx contractapi.TransactionContextInterface, assetID string, owner string, buyerMSP string) *Asset {
....
err = ctx.GetStub().PutPrivateData(assetCollection, transferAgreeKey, []byte(clientID))
if err != nil {
fmt.Printf("failed to put asset bid: %v\n", err)
return nil
}
}
// Verify transfer details and transfer owner
asset := s.verifyAgreement(
ctx, assetTransferInput.ID, asset.Owner, assetTransferInput.BuyerMSP)
var detailsID int = len(asset.ID)
- 越界訪問
越界訪問是代碼語言中常見的缺陷,它并不一定會造成編譯錯誤,在編譯階段很難發現這類問題,導致的后果也不確定。當出現越界時,由于無法得知被訪問空間存儲的內容,所以會產生不確定的行為,可能是程序崩潰、運算結果非預期。

系統機制問題
- 全局變量唯一性
全局變量不會保存在數據庫中,而是存儲于單個節點,如果此類節點發生故障或重啟時,可能會導致該全局變量值不再與其他節點保持一致,影響節點交易。因此,從數據庫讀取、寫入或從合約返回的數據不應依賴于全局狀態變量。

- 不確定性因素
合約變量的生成如果依賴于不確定因素(如:本節點時間戳)或者某個未在賬本中持久化的變量,那么可能會因為各節點該變量的讀寫集不一樣,導致交易驗證不通過。

- 訪問外部資源
合約訪問外部資源時,如第三方庫,這些第三方庫代碼本身可能存在一些安全隱患。引入第三方庫代碼可能會暴露合約未預期的安全隱患,影響鏈碼業務邏輯。
業務邏輯安全
- 輸入參數檢查不到位
在編寫智能合約時,開發者需要對每個函數參數進行合法性,預期性檢查,即需要保證每個參數符合合約的實際應用場景,對輸入參數檢查不到位往往會導致非預期的結果。如近期爆出的Filecoin測試網代碼中的嚴重漏洞,原因是 transfer 函數中對轉賬雙方 from, to 地址檢查不到位,導致了FIL無限增發。
### Before
func (vm *VM) transfer(from, to address.Address, amt types.BigInt) aerrors.ActorError {
if from == to {
return nil
}
...
}
### After
func (vm *VM) transfer(from, to address.Address, amt types.BigInt) aerrors.ActorError {
if from == to {
return nil
}
fromID, err := vm.cstate.LookupID(from)
if err != nil {
return aerrors.Fatalf("transfer failed when resolving sender address: %s", err)
}
toID, err := vm.cstate.LookupID(to)
if err != nil {
return aerrors.Fatalf("transfer failed when resolving receiver address: %s", err)
}
if fromID == toID {
return nil
}
...
}
- 函數權限失配
Fabrci智能合約go代碼實現中是根據首字母的大小寫來確定可以訪問的權限。如果方法名首字母大寫,則可以被其他的包訪問;如果首字母小寫,則只能在本包中使用。因此,對于一些敏感操作的內部函數,應盡量保證方法名采用首字母小寫開頭,防止被外部惡意調用。
- 異常處理問題
通常每個函數調用結束后會返回相應的返回參數,錯誤碼,如果未認真檢查錯誤碼值而直接使用其返回參數,可能導致越界訪問,空指針引用等安全隱患。
- 外部合約調用引入安全隱患
在某些業務場景中,智能合約代碼可能引入其他智能合約,這些未經安全檢查的合約代碼可能存在一些未預期的安全隱患,進而影響鏈碼業務本身的邏輯。
總結
聯盟鏈的發展目前還處于項目落地初期階段,對于聯盟鏈平臺上的智能合約開發,項目方應該強化對智能合約開發者的安全培訓,簡化智能合約的設計,做到功能與安全的平衡,嚴格執行智能合約代碼安全審計(自評/項目組review/三方審計)
在聯盟鏈應用落地上,需要逐步推進,從簡單到復雜,在項目開始階段,需要設置適當的權限以防發生黑天鵝事件。
REF
[1] Hyperledger Fabric 鏈碼
https://blog.51cto.com/clovemfong/2149953
[2] fabric-samples
https://github.com/hyperledger/fabric-samples
[3] Fabric2.0,使用test-network
https://blog.csdn.net/zekdot/article/details/106977734
[4] 使用V8和Go實現的安全TypeScript運行時
https://php.ctolib.com/ry-deno.html
[5] Hyperledger fabric
https://github.com/hyperledger/fabric
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1317/
暫無評論