臺北時間 2021 年 8 月 27 日 20 點 50 分左右(區塊高度 13107518),以太坊突然出現分叉。我們通過分析 Geth 的代碼版本修改和這筆造成分叉的交易(0x1cb6fb36633d270edefc04d048145b4298e67b8aa82a9e5ec4aa1435dd770ce4)厘清了以太坊分叉的根本原因:Geth 舊版本在處理預編譯合約調用時,并未考慮特殊情況(corner case)下參數值的處理,從而引發重疊拷貝(overlapping copy),導致返回值異常,該漏洞(CVE-2021-39137)已提交 Geth 官方,目前尚未披露細節,但攻擊者已經利用漏洞實施了攻擊,我們認為及時的分析和披露是必要的,也希望我們的分析能夠為社區提供必要的理解和幫助,
攻擊分析
運用我們的 在線分析工具,可以看出:
圖一
這筆交易執行了一個精心構造的 STATICCALL, 攻擊者將 addr 設為 0x04 (是預編譯合約 dataCopy), inOffset 為 0, inSize 為 32, retOffset 為 7, retSize 為 32。
圖二
由于 STATICCALL 的目標地址是預編譯合約,所以會執行圖二中的 RunPrecompiledContract。
圖三
圖四
根據圖三和圖四的代碼,可以看到預編譯合約 0x04 真正執行的邏輯只是簡單地把 in (指針)返回。
圖五
圖六
圖五是 STATICCALL 的執行過程,753 行是執行預編譯合約的入口,751 行的 args 指向 EVM 的 Memory 中 inOffset ~ inOffset + inSize 這篇區域的指針,也就是說 args 指向 Mem[0:32]。
根據圖六以及前文對預編譯合約 0x04 (dataCopy)的分析,我們可以知道 753 行的返回值 ret 是與 args 完全相同的指針,也指向 Mem[0:32],
- 在 1.10.7 版本的 Geth 中(有 Bug): 762 行將 ret 指向的值賦給 EVM 的 Memory 中 retOffset ~ retOffset + retOffset 這篇區域 , 也就是將 Mem[0:32] 的值賦給 Mem[7:7+32],而由于 ret 是一個指向 Mem[0:32] 的指針,這次 Memory.Set 修改了 Mem[7:32] 的值,也就修改了 ret 所指的值,所以在第 771 行返回的 ret 已經不是預編譯合約執行結束時的 ret 了。
- 在 1.10.8 版本的 Geth 中(無 Bug): 增加了 766 行:ret = common.CopyBytes (ret), 將 Mem[0:32] 中的值做了一次深拷貝賦給 ret,那么在 767 行執行的 Memory.Set 只會修改 Memory 而不會修改 ret, 在 771 行返回的 ret 就是正確的 ret。
總結
通過對整個攻擊流程的梳理和 Geth 源代碼的分析,我們認為根本原因在于 Geth 舊版本在處理預編譯合約的調用時并未考慮異常值的處理,導致攻擊者利用該漏洞實施了重疊拷貝,影響了返回值,最終導致分叉的出現。由于 Geth 是 BSC、HECO、Polygon 等公鏈的基礎,因此該漏洞影響范圍甚廣,目前各公鏈也先后推出了升級和補丁,我們也呼吁各相關節點盡早升級打上補丁,以確保基礎設施的安全。