模糊測試是一種自動化軟體測試技術,藉由提供非預期的輸入來檢測程式是否存在安全漏洞。本文將探討模糊測試的原理、實作方式,以及在 Rust 語言中的應用。同時,文章也涵蓋了漏洞利用開發的相關技術,並探討如何使用 Rust 開發更安全且效能更高的漏洞利用工具。文章中將會介紹如何使用 cargo-fuzz
工具進行模糊測試,以及如何分析測試結果並修復程式碼中的漏洞。此外,文章也將探討如何使用 Rust 編寫 Shellcode,並利用 Rust 的記憶體安全特性來避免常見的記憶體錯誤。最後,文章將探討 Rust 在漏洞利用開發領域的未來發展趨勢,以及如何更好地利用 Rust 的特性來提升漏洞利用開發的效率和安全性。
模糊測試(Fuzzing)技術詳解與實務應用
模糊測試是一種自動化的軟體測試技術,透過提供非預期的輸入來測試程式的穩定性和安全性。本文將探討模糊測試的基本原理、實務應用以及在Rust語言中的具體實作。
模糊測試的基本原理
模糊測試的核心思想是透過隨機或半隨機的方式產生測試資料,輸入到被測系統中,觀察系統的反應。這個過程可以發現潛在的安全漏洞、記憶體錯誤或其他隱藏的程式缺陷。
模糊測試的關鍵要素:
- 測試資料生成:產生有效的測試資料是模糊測試的基礎。可以採用完全隨機或根據特定規則的生成方式。
- 程式監控:需要監控程式在執行測試資料時的行為,包括當機、異常離開等情況。
- 錯誤分析:當程式出現異常時,需要分析導致錯誤的輸入資料。
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
錯誤資訊解讀:
- 錯誤型別:索引越界錯誤(index out of bounds)
- 發生位置:
lib.rs
檔案的第5行第19列 - 導致錯誤的輸入:
MemcopyInput
結構體的具體內容
內容解密:
fuzz_target!
巨集的使用:這個巨集用於定義模糊測試的目標函式,接收一個閉包作為引數。MemcopyInput
結構體的作用:作為模糊測試的輸入資料結構,包含目標緩衝區、來源緩衝區和複製長度等資訊。vulnerable_memcopy
函式的風險:這個函式存在潛在的安全風險,可能導致記憶體錯誤。
模糊測試的最佳實踐
- 持續執行:模糊測試應該持續執行,以發現更多潛在的問題。
- 覆寫率引導:使用覆寫率引導的模糊測試可以更有效地發現程式碼中的問題。
- 結果分析:對模糊測試發現的問題進行深入分析,修復根本原因。
利用開發(Exploit Development)技術詳解
在發現漏洞之後,下一步就是開發利用(exploit)來觸發這些漏洞。本文將探討利用開發的基本概念、Rust在其中的應用,以及相關的最佳實踐。
利用開發的基本概念
利用開發是指建立特定的程式或程式碼,用於觸發已發現的漏洞,從而達到特定的攻擊目的。傳統上,利用程式通常使用Python或C語言開發。
利用開發面臨的挑戰:
- 語言選擇:不同的語言有不同的優缺點,需要根據具體場景選擇合適的語言。
- 可重用性:如何提高利用程式碼的可重用性是一個重要的課題。
- 跨平台相容性:如何在不同平台上執行利用程式是一個挑戰。
Rust在利用開發中的優勢
Rust語言因其獨特的特性,在利用開發中展現出明顯的優勢:
- 記憶體安全:Rust的記憶體安全特性可以減少利用程式本身的安全風險。
- 效能:Rust編譯出的程式具有與C/C++相當的效能。
- 跨平台: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(())
}
內容解密:
Cargo.toml
組態說明:透過特定的組態,可以建立同時支援函式庫和二進位制輸出的crate。exploit
函式的設計:這個函式代表了利用的核心邏輯,可以根據具體需求進行擴充套件。- 函式庫和二進位制的分離設計:這種設計模式提高了程式碼的可重用性。
利用開發的最佳實踐
- 模組化設計:將利用邏輯模組化,便於重用和維護。
- 持續測試:對利用程式進行持續的測試,確保其可靠性和有效性。
- 檔案記錄:詳細記錄利用程式的設計思路和使用方法。
使用 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 編譯過程大致分為四個階段:
- 語法解析和巨集展開:將原始碼轉換為 Token 流,然後生成 AST(抽象語法樹),並展開巨集。
- 分析:進行型別推斷、特性解析和型別檢查,然後將 AST 轉換為 MIR(中級中間表示)進行借用檢查。
- 最佳化和程式碼生成:將 MIR 轉換為 LLVM IR,然後進行最佳化和生成機器碼。
- 連結:將多個目標檔案組合成最終的可執行檔。
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 中使用組合語言的方法。這些知識對於開發低階系統程式和安全相關工具非常重要。下一章節將繼續探討相關主題。