模糊測試是一種自動化軟體測試技術,藉由提供非預期的輸入來檢測程式是否存在安全漏洞。本文將探討模糊測試的原理、實作方式,以及在 Rust 語言中的應用。同時,文章也涵蓋了漏洞利用開發的相關技術,並探討如何使用 Rust 開發更安全且效能更高的漏洞利用工具。文章中將會介紹如何使用 cargo-fuzz 工具進行模糊測試,以及如何分析測試結果並修復程式碼中的漏洞。此外,文章也將探討如何使用 Rust 編寫 Shellcode,並利用 Rust 的記憶體安全特性來避免常見的記憶體錯誤。最後,文章將探討 Rust 在漏洞利用開發領域的未來發展趨勢,以及如何更好地利用 Rust 的特性來提升漏洞利用開發的效率和安全性。

模糊測試(Fuzzing)技術詳解與實務應用

模糊測試是一種自動化的軟體測試技術,透過提供非預期的輸入來測試程式的穩定性和安全性。本文將探討模糊測試的基本原理、實務應用以及在Rust語言中的具體實作。

模糊測試的基本原理

模糊測試的核心思想是透過隨機或半隨機的方式產生測試資料,輸入到被測系統中,觀察系統的反應。這個過程可以發現潛在的安全漏洞、記憶體錯誤或其他隱藏的程式缺陷。

模糊測試的關鍵要素:

  1. 測試資料生成:產生有效的測試資料是模糊測試的基礎。可以採用完全隨機或根據特定規則的生成方式。
  2. 程式監控:需要監控程式在執行測試資料時的行為,包括當機、異常離開等情況。
  3. 錯誤分析:當程式出現異常時,需要分析導致錯誤的輸入資料。

Rust中的模糊測試實作

Rust語言提供了強大的工具支援模糊測試,特別是透過cargo-fuzz工具。以下是一個具體的例項:

建立模糊測試目標:

fuzz_target!(|data: MemcopyInput| {
    let mut data = data.clone();
    fuzzing::vulnerable_memcopy(&mut data.dest, &data.src, data.n);
});

執行模糊測試:

$ cargo +nightly fuzz run fuzz_target_1

這個命令會啟動模糊測試引擎,開始對目標函式進行測試。

模糊測試結果分析

當模糊測試發現程式當機時,會輸出詳細的錯誤資訊,包括導致當機的輸入資料。例如:

thread '<unnamed>' panicked at 'index out of bounds: the len is 0 but the index is 0', black-hat-rust/ch_06/fuzzing/src/lib.rs:5:19

錯誤資訊解讀:

  1. 錯誤型別:索引越界錯誤(index out of bounds)
  2. 發生位置lib.rs檔案的第5行第19列
  3. 導致錯誤的輸入MemcopyInput結構體的具體內容

內容解密:

  1. fuzz_target!巨集的使用:這個巨集用於定義模糊測試的目標函式,接收一個閉包作為引數。
  2. MemcopyInput結構體的作用:作為模糊測試的輸入資料結構,包含目標緩衝區、來源緩衝區和複製長度等資訊。
  3. vulnerable_memcopy函式的風險:這個函式存在潛在的安全風險,可能導致記憶體錯誤。

模糊測試的最佳實踐

  1. 持續執行:模糊測試應該持續執行,以發現更多潛在的問題。
  2. 覆寫率引導:使用覆寫率引導的模糊測試可以更有效地發現程式碼中的問題。
  3. 結果分析:對模糊測試發現的問題進行深入分析,修復根本原因。

利用開發(Exploit Development)技術詳解

在發現漏洞之後,下一步就是開發利用(exploit)來觸發這些漏洞。本文將探討利用開發的基本概念、Rust在其中的應用,以及相關的最佳實踐。

利用開發的基本概念

利用開發是指建立特定的程式或程式碼,用於觸發已發現的漏洞,從而達到特定的攻擊目的。傳統上,利用程式通常使用Python或C語言開發。

利用開發面臨的挑戰:

  1. 語言選擇:不同的語言有不同的優缺點,需要根據具體場景選擇合適的語言。
  2. 可重用性:如何提高利用程式碼的可重用性是一個重要的課題。
  3. 跨平台相容性:如何在不同平台上執行利用程式是一個挑戰。

Rust在利用開發中的優勢

Rust語言因其獨特的特性,在利用開發中展現出明顯的優勢:

  1. 記憶體安全:Rust的記憶體安全特性可以減少利用程式本身的安全風險。
  2. 效能:Rust編譯出的程式具有與C/C++相當的效能。
  3. 跨平台:Rust支援跨平台編譯,可以輕鬆地在不同作業系統上執行。

建立同時支援函式庫和二進位制的可執行crate:

Cargo.toml中組態:

[lib]
name = "binlib"
path = "src/lib.rs"

[[bin]]
name = "binlib"
path = "src/bin.rs"

程式碼實作:

// src/lib.rs
pub fn exploit(target: &str) -> Result<(), String> {
    println!("exploiting {}", target);
    Ok(())
}

// src/bin.rs
use binlib::exploit;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    exploit("example_target")?;
    Ok(())
}

內容解密:

  1. Cargo.toml組態說明:透過特定的組態,可以建立同時支援函式庫和二進位制輸出的crate。
  2. exploit函式的設計:這個函式代表了利用的核心邏輯,可以根據具體需求進行擴充套件。
  3. 函式庫和二進位制的分離設計:這種設計模式提高了程式碼的可重用性。

利用開發的最佳實踐

  1. 模組化設計:將利用邏輯模組化,便於重用和維護。
  2. 持續測試:對利用程式進行持續的測試,確保其可靠性和有效性。
  3. 檔案記錄:詳細記錄利用程式的設計思路和使用方法。

使用 Rust 進行漏洞利用開發

前言

隨著資安領域的發展,利用 Rust 語言開發漏洞利用(exploit)已成為一個重要的研究方向。Rust 語言因其記憶體安全特性和高效能,逐漸在資安社群中受到重視。本文將探討如何使用 Rust 進行漏洞利用開發,並介紹相關的工具和技術。

建立漏洞利用工具包

在 Python 社群中,pwntools 是一個著名的漏洞利用開發框架,提供了許多方便的功能和輔助工具來加速漏洞的發現和利用。然而,Rust 社群更傾向於使用小型 crate(Rust 的套件管理單位)並組合這些小型套件來實作功能。以下是一些可以用於漏洞利用開發的 Rust crate:

  • reqwest:用於傳送 HTTP 請求
  • hyper:用於建立低階 HTTP 伺服器或客戶端
  • tokio:用於與 TCP 或 UDP 服務互動
  • goblin:用於讀取或修改可執行檔(PE、ELF、Mach-O)
  • rustls:用於處理 TLS 服務
  • flate2:用於壓縮/解壓縮

程式碼範例:使用 reqwest 傳送 HTTP 請求

use reqwest;

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let res = reqwest::get("https://kerkour.com").await?;
    println!("Status: {}", res.status());
    Ok(())
}

內容解密:

此範例展示瞭如何使用 reqwest crate 傳送 HTTP GET 請求至指定的 URL,並列印出回應的狀態碼。首先,我們引入 reqwest crate,並使用 #[tokio::main] 屬性來標記主函式為非同步函式。然後,在主函式中,我們呼叫 reqwest::get 函式來傳送請求,並使用 await? 等待回應。最後,我們列印出回應的狀態碼。

CVE-2019-11229 和 CVE-2019-89242

本文作者將 CVE-2019-11229 和 CVE-2019-89242 的漏洞利用程式碼從 Python 移植到 Rust。這些程式碼可以在附帶的 GitHub 儲存函式庫中找到。雖然作者認為評論這些程式碼沒有太大的教育價值,但仍鼓勵讀者閱讀這些程式碼,以瞭解在 Rust 中進行漏洞利用開發時可以使用哪些 crate。

CVE-2021-3156:sudo 中的堆積疊緩衝區溢位漏洞

CVE-2021-3156 是 sudo 中的一個堆積疊緩衝區溢位漏洞。作者將這個漏洞的利用程式碼從 C 語言移植到 Rust。在這個過程中,作者遇到了一些挑戰,例如需要建立一個動態 C 函式庫,並在載入函式庫時執行特定的函式。

建立動態 C 函式庫

要建立一個動態 C 函式庫,需要在 Cargo.toml 中進行相應的設定:

[lib]
name = "x"
crate_type = ["dylib"]

程式碼範例:建立動態 C 函式庫

#![no_std]

use core::arch::asm;

// ...

#[link_section = ".init_array"]
pub static INIT: unsafe extern "C" fn() = rust_init;

#[no_mangle]
pub unsafe extern "C" fn rust_init() {
    // 實際的 payload 程式碼
}

內容解密:

此範例展示瞭如何建立一個動態 C 函式庫,並在載入函式庫時執行特定的函式。首先,我們使用 #![no_std] 屬性來告訴 Rust 編譯器不要連結標準函式庫。然後,我們定義了一個名為 rust_init 的函式,並使用 #[link_section = ".init_array"] 屬性將其指標放在 .init_array 區段中。當函式庫被載入時,rust_init 函式將被呼叫。最後,我們在 rust_init 函式中實作了實際的 payload 程式碼。

使用 libc 進行系統呼叫

在 Rust 中,可以使用 libc crate 來進行系統呼叫。例如,可以使用 libc::execve 來執行外部程式。

程式碼範例:使用 libc::execve 執行外部程式

use std::ffi::CString;
use std::os::raw::c_char;

fn main() {
    let args = ["sudoedit", "-A", "-s", "AA..."];
    let args: Vec<*mut c_char> = args
        .iter()
        .map(|e| CString::new(*e).expect("building CString").into_raw())
        .collect();
    // ...
    unsafe {
        libc::execve(
            args[0],
            args.as_ptr(),
            std::ptr::null(),
        );
    }
}

內容解密:

此範例展示瞭如何使用 libc crate 中的 execve 函式來執行外部程式。首先,我們定義了一個字串陣列 args,其中包含了要執行的程式名稱和引數。然後,我們將 args 中的字串轉換為 C 風格的字串指標,並儲存在一個向量中。最後,我們呼叫 libc::execve 函式來執行外部程式。

隨著 Rust 語言在資安領域的應用越來越廣泛,未來可能會出現更多根據 Rust 的漏洞利用開發工具和技術。同時,Rust 社群也需要繼續改進和完善相關的工具和函式庫,以滿足資安人員的需求。

使用 Rust 編寫 Shellcode

8.1 什麼是 Shellcode?

Shellcode 是在被攻擊的機器上執行的原始程式碼。傳統上,編寫 Shellcode 通常直接使用組合語言,以獲得絕對的控制權,但這需要大量的知識,並且難以除錯、移植和維護。

範例 Shellcode

以下是一個 Shellcode 的範例:

488d35140000006a01586a0c5a4889c70f056a3c5831ff0f05ebfe68656c6c6f20776f726c640a

這個十六進製表示看起來難以理解,但可以透過將其寫入檔案並反彙編來檢視其實際內容:

$ echo '488d35140000006a01586a0c5a4889c70f056a3c5831ff0f05ebfe68656c6c6f20776f726c640a' | xxd -r -p > shellcode.bin
$ objdump -D -b binary -mi386 -Mx86-64 -Mintel shellcode.bin

反彙編結果顯示,這段 Shellcode 實際上執行了以下操作:

write(STDOUT, "hello world\n", 12);
exit(0);

程式碼解析

; 將 "hello world\n" 的位址載入 rsi 暫存器
lea rsi, [rip + 0x14]

; 設定 write 系統呼叫引數
push 0x1
pop rax
push 0xc
pop rdx
mov rdi, rax

; 執行 write 系統呼叫
syscall

; 設定 exit 系統呼叫引數
push 0x3c
pop rax
xor edi, edi

; 執行 exit 系統呼叫
syscall

; 跳轉到自身,形成無限迴圈
jmp 0x19

; "hello world\n" 字串
push 0x6f6c6c65

8.2 可執行檔的區段

可執行檔(程式)被分成多個區段,用於儲存不同的中繼資料、程式碼和資料。例如,.text 區段包含編譯後的程式碼,而 .data 區段包含字串等資料。

ELF 檔案格式

ELF(Executable and Linkable Format)是一種常見的可執行檔格式,其結構如下圖所示:

  graph LR;
    A[ELF Header] --> B[.text];
    A --> C[.data];
    A --> D[.bss];
    B --> E[程式碼];
    C --> F[已初始化資料];
    D --> G[未初始化資料];

圖表翻譯: 此圖示展示了 ELF 檔案的基本結構,包括 ELF Header 和多個區段。

8.3 Rust 編譯過程

Rust 編譯過程大致分為四個階段:

  1. 語法解析和巨集展開:將原始碼轉換為 Token 流,然後生成 AST(抽象語法樹),並展開巨集。
  2. 分析:進行型別推斷、特性解析和型別檢查,然後將 AST 轉換為 MIR(中級中間表示)進行借用檢查。
  3. 最佳化和程式碼生成:將 MIR 轉換為 LLVM IR,然後進行最佳化和生成機器碼。
  4. 連結:將多個目標檔案組合成最終的可執行檔。

Rust 編譯階段示意圖

  graph LR;
    A[原始碼] -->|詞法分析|> B[Token 流];
    B -->|語法分析|> C[AST];
    C -->|巨集展開|> D[MIR];
    D -->|最佳化|> E[LLVM IR];
    E -->|程式碼生成|> F[機器碼];
    F -->|連結|> G[可執行檔];

圖表翻譯: 此圖示展示了 Rust 編譯過程的各個階段。

8.4 no_std 環境

在某些情況下,我們不需要標準函式庫提供的功能,例如在嵌入式系統或作業系統核心開發中。這時可以使用 no_std 屬性來停用標準函式庫。

最小 no_std 程式範例

Cargo.toml:

[package]
name = "nostd"
version = "0.1.0"
edition = "2021"

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"
opt-level = "z"
lto = true
codegen-units = 1

.cargo/config.toml:

[build]
rustflags = ["-C", "link-arg=-nostdlib", "-C", "link-arg=-static"]

main.rs:

#![no_std]
#![no_main]
#![feature(start)]

#[start]
fn start(_argc: isize, _argv: *const *const u8) -> isize {
    0
}

#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
    loop {}
}

程式碼解析

  • #![no_std]:停用標準函式庫。
  • #![no_main]:停用預設的 main 函式入口點。
  • #![feature(start)]:啟用自定義入口點功能。
  • #[start]:標記自定義的入口點函式。
  • #[panic_handler]:標記 panic 處理函式。

8.5 在 Rust 中使用組合語言

從 Rust 1.59 版本開始,穩定版支援內聯組合語言(asm!)和自由形式的組合語言(global_asm!)。

最小內聯組合語言範例

use std::arch::asm;

fn main() {
    let x: u64;
    unsafe {
        asm!("mov {}, 5", out(reg) x);
    }
    println!("x = {}", x);
}

程式碼解析

  • asm! 巨集用於內聯組合語言。
  • out(reg) x 指定輸出暫存器,並將結果儲存在 x 中。

本章節介紹了 Shellcode 的基本概念、Rust 編譯過程、no_std 環境以及在 Rust 中使用組合語言的方法。這些知識對於開發低階系統程式和安全相關工具非常重要。下一章節將繼續探討相關主題。