Windowsカーネル開発におけるRustの使用についての紹介 (パート1)
はじめに
こんにちは、恐れを知らない読者の皆さん!強力なRustプログラミング言語によって支えられたドライバー開発の領域への刺激的な旅に備えてください!
このスリリングな冒険に出発するにあたり、目に見えないプロセスをレンダリングする強力なドライバーを作成する今後の探検を期待してください。この記事を読まずにはいけないでしょう、なぜならここで提示された質問に対する答えがここに含まれているからです。
Rustが未知の領域である場合でも心配無用です。私があなたの案内人となり、C++という馴染みのある領域にも踏み込んでいきます。必要なのは少しの自由な時間、C++プログラミングの熟練度、そしてカーネルレベルの魔法についての基本的な理解だけです。
しかし、なぜRustかと疑問に思うかもしれませんね。答えは明らかです:Rustはスタイル、革新、そして揺るぎない力を融合させています。それはC++に匹敵するパフォーマンスを提供し、突破不可能な安全対策によって強化されています。Microsoft、Google、Mozilla、そしてLinux Foundationなどの技術大手がその可能性を認めています。"C forever"の提唱者であるLinus Torvaldsも、公式にLinuxカーネルへのRustの統合を支持しました[1]。MicrosoftはWindowsのコアをRustで書き換えています[2]が、まだWindowsカーネル内でRustを使う特権を開発者に提供してはいません。問題ありません;私たちは自ら手綱を握り、彼らの努力を超えていきます。
2015年に言語の最初のバージョンがリリースされ、それ以来、6週間ごとに更新されています。リリースチャネルは、安定版、ベータ版、ナイトリー版の3つがあります。(ちなみに、ドライバーの開発にはナイトリーツールチェーンが必要になります)。さらに、Rustにはエディション(C++の標準のようなもの)があり、具体的にはRust 2015、Rust 2018、そして執筆時点のRust 2021があります。そのため、Rustには新しい標準のために互換性のない箇所を部分的に修正するのを助けるcargo fix --editionコマンドもあります。
依存関係
私たちのクエストの舞台を設定するために、必要なツールを集めて、これからの課題に備えて環境を整えなければなりません。必要なもののチェックリストは次のとおりです:
必要なツール
1. Rust のインストール:まず初めに、こちらから Rust をインストールし、そのパワーを活用しましょう。
2. SDK のインストール:私たちの冒険には Windows SDK が必要です。それはこちらで入手できます。
3. WDK のインストール:Windows Driver Kit (WDK) は私たちの信頼できるドライバ開発のパートナーです。それはこちらから入手できます。
4. Sysinternals のインストール:Sysinternals Suite は私たちの非常に価値あるツールキットになります。こちらから手に入れましょう。
注意: ターミナルは管理者権限で実行する必要があります。
開発
構成
プロジェクトを作成した後、「hello-win-kernel」ディレクトリに移動します。次の構造が見つかります:
cargo new hello-win-kernel --lib
プロジェクトを作成した後、hello-win-kernelディレクトリに移動します。次の構造が見つかります:
# hello-win-kernel folder
srclib.rs
.gitignore
Cargo.toml
Rustでは、通常アプリケーションのエントリーポイントを含むファイルをmain.rsと名付け、ライブラリにはlib.rsが使われます。ファイルCargo.tomlにはプロジェクトの依存関係と設定が含まれており、.gitignoreは賢くもtargetディレクトリを除外しています。このディレクトリにはバイナリが保存されます。
さて、Cargo.tomlファイルを編集してプロジェクト設定を強化しましょう。まず、[lib]セクションを追加し、エントリーポイントファイルのパスを指定して「動的ライブラリ」として指定します。
[package]
name = "hello-win-kernel"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/lib.rs"
crate-type = ["cdylib"]
また、[profile.dev] および[profile.release] セクションを作成し、より良いエラー処理のためpanicタイプを"abort"に設定します。
次に、[build-dependencies]セクションを含め、Windowsレジストリ操作のためのwinregクレートを導入します:
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
[build-dependencies]
winreg = "0.50.0"
プロジェクトのコンパイルをスムーズに進めるために、hello-win-kernelディレクトリにrust-toolchainファイルを作成し、nightly toolchainの使用を指定し、特別なフラグへのアクセスを可能にします:
rust-toolchain
nightly
ドライバーをコンパイルするには、WDKへのパスを追加する必要があります。これは、build.rs スクリプトを使用して実現できます。このbuild.rs スクリプトは、パッケージをビルドする前にcargo によって実行されます。インストールされたライブラリを見つけ出し、その情報をCargoに含めることができます。コード中で#[link(name = "libname")] 構造を使用した後、ビルドプロセスは追加されたパスを検索し、CやC++と同様にライブラリをリンクします。 hello-win-kernelディレクトリにbuild.rsファイルを作成します。このファイルにWDKをフェッチするコードを記入します。このGitHubリポジトリにあるスクリプトを参考にしてください。
build.rs
// A.k.a #include in the C++
extern crate winreg;
// Imports the need features. A.k.a using namespace std in the C++
use std::env::var;
use std::error::Error;
use std::fmt;
use std::path::{Path, PathBuf};
use winreg::enums::*;
use winreg::RegKey;
// Windows Kits registry key
const WIN_KITS_KEY: &str = r"SOFTWAREMicrosoftWindows KitsInstalled Roots";
// Custom error type
#[derive(Debug)]
struct BuildError {
msg: String,
}
impl Error for BuildError {}
impl fmt::Display for BuildError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "BuildError: {}", self.msg)
}
}
// Get the Windows Kits directory from the registry
fn get_windows_kits_dir() -> Result {
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
match hklm.open_subkey(WIN_KITS_KEY) {
Ok(key) => match key.get_value::("KitsRoot10") {
Ok(dir) => Ok(dir.into()),
Err(_) => Err(BuildError {
msg: format!("Can not get value: {}", WIN_KITS_KEY),
}),
},
Err(_) => Err(BuildError {
msg: format!("Can not open sub_key: {}", WIN_KITS_KEY),
}),
}
}
// Get the latest km directory from the Windows Kits directory
fn get_km_dir(windows_kits_dir: &PathBuf) -> Result {
match Path::new(windows_kits_dir).join("lib").read_dir() {
Ok(read_dir) => {
match read_dir
.filter_map(|dir| dir.ok())
.map(|dir| dir.path())
.filter(|dir| {
dir.components()
.last()
.and_then(|c| c.as_os_str().to_str())
.map(|c| c.starts_with("10.") && dir.join("km").is_dir())
.unwrap_or(false)
})
.max()
.ok_or_else(|| format!("Can not find a valid km dir in `{:?}`", windows_kits_dir))
{
Ok(max_lib_dir) => Ok(max_lib_dir.join("km")),
Err(msg) => Err(BuildError { msg }),
}
}
Err(_) => Err(BuildError {
msg: format!("Can not read dir: {:?}", windows_kits_dir),
}),
}
}
fn main() {
let windows_kits_dir = get_windows_kits_dir().unwrap();
let km_dir = get_km_dir(&windows_kits_dir).unwrap();
let target = var("TARGET").unwrap();
let arch = if target.contains("x86_64") {
"x64"
} else if target.contains("i686") {
"x86"
} else {
panic!("Only support x64 and x86!");
};
let wdk_lib_dir = km_dir.join(arch);
// link
println!(
"cargo:rustc-link-search=native={}",
wdk_lib_dir.to_str().unwrap()
);
}
私たちのドライバーを導入するためには、まず設定ファイルを作成する必要があります。恐れることはありません、私がこの道を勝利へと導きます。
1. 現在のドメインに.cargoという名前のディレクトリを作成します—可能性の領域。
2. この新たに見つけたドメイン内に、configという名前のファイルを刻み込んでください。ここに私たちの指示を刻みます。
config ファイルにこれらのフラグを記入してください。それぞれがドライバーのコンパイルの迷宮を通り抜けるための灯台となります。これらのフラグについてさらに深く理解するためには、MSDN [5] にある知恵の年代記に目を通すことができます。
.cargo/config
[build]
target = "x86_64-pc-windows-msvc"
rustflags = [
# Pre Link Args
"-Z", "pre-link-arg=/NOLOGO",
"-Z", "pre-link-arg=/NXCOMPAT",
"-Z", "pre-link-arg=/NODEFAULTLIB",
"-Z", "pre-link-arg=/SUBSYSTEM:NATIVE",
"-Z", "pre-link-arg=/DRIVER",
"-Z", "pre-link-arg=/DYNAMICBASE",
"-Z", "pre-link-arg=/INCREMENTAL:NO",
"-Z", "pre-link-arg=/MANIFEST:NO",
"-Z", "pre-link-arg=/PDBALTPATH:none",
# Post Link Args
"-C", "link-arg=/OPT:REF,ICF",
"-C", "link-arg=/ENTRY:driver_entry",
"-C", "link-arg=/MERGE:.edata=.rdata",
"-C", "link-arg=/MERGE:.rustc=.data",
"-C", "link-arg=/INTEGRITYCHECK"
]
実装
ふう!やった、成功だ!信じられる?コーディングを始める時間だよ。そうだね、ドライバーの開発に飛び込むことができる。C++のように、Rustもユーザーモードとカーネルモードの違いのために標準ライブラリをカーネルモードではサポートしていない。そのため、#![no_std] 属性を使って無効にする必要があり、これは lib.rs ファイルの始めに指定すべきだ。
次に、リンケージエラーを解決するために、__CxxFrameHandler3 関数と_fltused変数を宣言する必要があります。さらに、標準ライブラリを無効にしたため、独自のpanic_handler も実装する必要があります。
これから、ドライバーのエントリーポイントとなる driver_entry 関数を作成しましょう。この関数の名前は変更可能ですが、.cargo/config ファイル内で指定する必要があります。例えば、フィールド "-C", "link-arg=/ENTRY:driver_entry" を "-C", "link-arg=/ENTRY:entry" に変更します。
Windows APIはCで実装されているため、FFI (Foreign Function Interface) を使用する必要があります。簡単に言うと、これは他の言語からAPIを呼び出すことを可能にするメカニズムです。
始めるには、#[no_mangle] 属性を使用して名前マングリングを無効にし、関数呼び出しにはシステム (stdcall) 呼び出し規約が使用されることを指定する必要があります。extern "system"。便宜上、いくつかの型を作成しましょう。PDRIVER_OBJECT と PUNICODE_STRING の代わりに、現時点ではこれらのデータ構造が必要ないため、PVOID を使用します。
それでは、以下のコードをsrc/lib.rsに追加しましょう。
src/lib.rs
// Disables the standard library
#![no_std]
// A.k.a #include in C++
mod dbg;
mod ntstatus;
mod types;
// Imports the need features. A.k.a using namespace std in the C++
use dbg::*;
use ntstatus::*;
use types::*;
// Need for fix linker error.
#[no_mangle]
pub extern "system" fn __CxxFrameHandler3(_: *mut u8, _: *mut u8, _: *mut u8, _: *mut u8) -> i32 {
unimplemented!()
}
// Need for fix linker error. Floating point calculations aren`t allowed when running in the Windows Kernel.
#[export_name = "_fltused"]
static _FLTUSED: i32 = 0;
// Panic hanlder
#[cfg(not(test))]
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
// The purpose of the loop construct in this context is to create an infinite loop.
// When a panic occurs, the panic function is invoked, and instead of unwinding
// the stack or aborting the program, it enters an infinite loop.
// This behavior is intentional to prevent the program from terminating or propagating
// the panic further. By using an infinite loop in the panic handler, the program effectively
// hangs or stalls at the point of panic, allowing for potential diagnostics or
// debugging to be performed.
loop {}
}
// The entry point of the driver, set as global (pub).
#[no_mangle]
pub extern "system" fn driver_entry(_driver: PVOID, _path: PVOID) -> NTSTATUS {
kd_print!("Hello, world!
");
STATUS_SUCCESS
}
次に、src/types.rsを作成し、エイリアスを使用してC言語のような型(Cではtypedefとして知られています)を追加します。さらに、src/ntstatus.rsファイルには、STATUS_SUCCESSのようなステータスを追加します。
src/types.rs
// Aliases for Windows types
pub type NTSTATUS = u32;
pub type PCSTR = *const u8;
pub type PVOID = *mut core::ffi::c_void;
src/ntstatus.rs
// Imports the need features. A.k.a using namespace std in the C++
use crate::types::*;
pub const STATUS_SUCCESS: NTSTATUS = 0x00000000;
素晴らしい、それではデバッグ情報の記録に使われる DbgPrint APIを追加しましょう。これを行うために、src/dbg.rsというファイルを作成し、ntoskrnlにリンクをして、DbgPrint APIを追加します。さらに便利に作業を進めるために、C言語の特定のプログラミングでは文の最後に \O 文字を常に追加する必要があるので、kd_print マクロも作成します(C言語では、文字列は \O 文字で終了します)。
We`ll wrap DbgPrintをunsafeブロックで囲むことにします。それは他の言語のAPIであり、Rustはそれに対して保証を提供できないためです。
src/dbg.rs
// Imports the need features. A.k.a using namespace std in the C++
use crate::types::*;
// Imports the `DbgPrint` function from `ntoskrnl.exe`, and set as a global function (pub),
// that can be called from anywhere module in the crate.
#[link(name = "ntoskrnl")]
extern "C" {
#[allow(dead_code)]
pub fn DbgPrint(format: PCSTR, ...) -> NTSTATUS;
}
// Macro that calls the `DbgPrint` function, and prepends the string with the name of the driver,
// and a null terminator. The `unsafe` is used because the `DbgPrint` function is written in a different language,
// and Rust cannot guarantee safety.
#[macro_export]
#[cfg(debug_assertions)]
macro_rules! kd_print {
($string: expr) => {
unsafe {
$crate::DbgPrint(concat!("[hello_win_kernel.sys] ", $string, "�").as_ptr())
}
};
($string: expr, $($x:tt)*) => {
unsafe {
$crate::DbgPrint(concat!("[hello_win_kernel.sys] ", $string, "�").as_ptr(), $($x)*)
}
};
}
// Empty macro for release
#[macro_export]
#[cfg(not(debug_assertions))]
macro_rules! kd_print {
($string: expr) => {};
($string: expr, $($x:tt)*) => {};
}
これでうまくドライバーの構築ができました。しかし、これで終わりではありません。次に、ドライバーの署名と展開メカニズムを実装する必要があります。
C++での以下の例のドライバは、CまたはC++でドライバを書く方がRustと比較して簡単であることを示しています。しかし、Microsoftからの公式サポートがあるまで、皆が熱望しているのを待っている間(実際には、C++の場合と同様に、自分たちで対処して実装するまで)、私たちはRustの保証を開発レベルで得ることができ、これにより、より安全なドライバを構築することができます。また、フルインラインASM、パターンマッチング、Rustが提供する他の多くのクールな機能と共にです。
いくつかのスタートアップは、サイバーセキュリティの文脈で仮想デバイスのドライバを開発するために既にRustを使用しています。
"hello-win-kernel-cpp" プロジェクトからの C++ によるサンプルドライバー:
#include
extern "C"
{
DRIVER_INITIALIZE DriverEntry;
}
#ifdef ALLOC_PRAGMA
#pragma alloc_text(INIT, DriverEntry)
#endif
// The entry point of the driver
_Use_decl_annotations_
NTSTATUS DriverEntry([[maybe_unused]] PDRIVER_OBJECT DriverObject,
[[maybe_unused]] PUNICODE_STRING RegistryPath)
{
DbgPrint("[hello-win-kernel-cpp.sys] Hello, World!
");
return STATUS_SUCCESS;
}
デプロイメントとテスト
ドライバーをテストするために、まず.dllから.sysに拡張子を変更し、テスト証明書で署名する必要があります。これをすべて自動化するために、cargo-makeを使用します。それを行うには、ターミナルで以下のコマンドを実行してください。
cargo install cargo-make
次に、Windows SDK ツールに関連する可能性のある WIN_SDK_TOOLS というミドルウェアを作成します。以下の手順について、誰が署名しますか:
1. View advanced system settings を検索して起動することで開きます。
2. System Propertiesウィンドウで、Advancedタブを選択します。
3. Environment Variables ボタンをクリックしてください。
4. Environment Variables ウィンドウで、System variables セクションまでスクロールダウンし、New ボタンをクリックします。
5. Variable name フィールドに、変数名をWIN_SDK_TOOLSとして入力します。
6. Variable value フィールドには、Windows SDK ツールが含まれているフォルダへのパスを入力してください。
7. 変更を保存するにはOKをクリックしてください。
次に、ビルドルールを定義するためにmakefile.tomlを作成する必要があります。
Cargo-makeは通常のmakeに似ていますが、ルールがやや異なり、よりシンプルな形式で定義されています。具体的には、このスクリプトではドライバーのビルドタスクを作成し、拡張子を.sysに変更し、makecer.exeとsigntool.exeユーティリティを使用してドライバーに署名し、ドライバー(ドライバーと証明書を別のbinフォルダに移動)をデプロイし、ビルドをクリーンアップするタスクを作成します。
Makefile.toml
[env.development]
TARGET_PATH = "target/x86_64-pc-windows-msvc/debug"
[env.release]
TARGET_PATH = "target/x86_64-pc-windows-msvc/release"
BUILD_FLAGS = "--release"
[tasks.build-driver]
script = [
"cargo build %BUILD_FLAGS%"
]
[tasks.rename-driver]
dependencies = ["build-driver"]
ignore_errors = true
script = [
"cd %TARGET_PATH%",
"rename hello_win_kernel.dll hello_win_kernel.sys",
]
[tasks.sign-driver]
dependencies = ["build-driver", "rename-driver"]
script = [
""%WIN_SDK_TOOLS%\makecert.exe" -r -pe -ss PrivateCertStore -n "CN=hello_win_kernel_cert" hello_win_kernel_cert.cer",
""%WIN_SDK_TOOLS%\signtool.exe" sign /fd sha256 /v /s PrivateCertStore /n "hello_win_kernel_cert" /t http://timestamp.digicert.com /a %TARGET_PATH%/hello_win_kernel.sys",
]
[tasks.deploy]
dependencies = ["sign-driver"]
script = [
"mkdir bin",
"move hello_win_kernel_cert.cer bin\hello_win_kernel_cert.cer",
"move %TARGET_PATH%\hello_win_kernel.sys bin\hello_win_kernel.sys"
]
[tasks.cleanup]
script = [
"rmdir /s /q bin",
"cargo clean",
]
プロジェクトを構築し、拡張子を変更し、ドライバーに署名するためには、次のコマンドを実行する必要があります(ドライバー署名中に問題が発生する可能性があるため、組み込みのVSCodeターミナルは使用しないでください。Windows Terminalの使用を推奨します):
cargo make deploy
プロジェクトをクリーンするために、次のコマンドを実行してください:
cargo make cleanup
個々のステップを別々に実行することもできます。例えば、ドライバーをサインするだけが必要な場合は、次のコマンドを実行してください:
cargo make sign-driver
次に、テストに進むことができます。これを行うには、Windows 10 x64またはWindows 11が搭載された仮想マシンをテストモードおよびデバッグモードで起動します。これらのモードが無効になっている場合は、以下のコマンドを実行してOSを再起動する必要があります:
注意: ターミナルは管理者権限で実行する必要があります
# Enable Test mode
bcdedit -set TESTSIGNING ON
# Enable Debug kernel
bcdedit /debug on
**sysinternals**パッケージから**DebugView** ユーティリティを、Microsoftサーバーからダウンロードしたものを、仮想マシンにコピーしてください。デスクトップ上にテストフォルダを作成し、ホストマシンからこのフォルダにドライバーと証明書を転送してください。
次に、**DebugView**を実行し、**Capture**をクリックし、**Capture Kernel**とEnable Verbose Kernel Outputの横にあるボックスをチェックします。これにより、カーネルのデバッグ出力を見ることができます。
これらの操作を行った後、次のコマンドをターミナルで実行して登録を進め、ドライバーを起動することができます:
注記:Windowsでドライバを登録および起動する際は、ターミナルを管理者権限で実行する必要があります
ドライバーを登録するには:
sc create hello_win_kernel binPath= "FullPathhello_win_kernel.sys" type= kernel start= demand
ドライバーを起動
sc start hello_win_kernel
結果
素晴らしいです!登録が成功し、ドライバーを起動したら、DebugViewでドライバーからの「Hello, world!」メッセージを見ることができるはずです。
概要
それでおしまいです! Rustプログラミング言語を使用して自分のドライバーを構築する方法を学びました。見ての通り、Rustでドライバーを書くことは可能ですが、いくらかの努力が必要です。実際、すべてを正しく設定すれば、その結果は非常に印象的になるでしょう。では、第2の記事でお待ちしております。そこでは実際のドライバーの記述に進んでいきます。
これ以上の質問がある場合や将来的にさらなる支援が必要な場合は、お気軽にお問い合わせください。ドライバー開発での成功をお祈りしています!
参考文献
1. Microsoftはメモリ安全なRustでコアWindowsコードの書き換えに忙しいです
https://www.theregister.com/2023/04/27/microsoft_windows_rust/
2. リーナス・トーバルズ:RustはLinux 6.1に導入されます
https://www.zdnet.com/article/linus-torvalds-rust-will-go-into-linux-6-1/
3. ビルドスクリプト
https://doc.rust-lang.org/cargo/reference/build-scripts.html
4. ドライバー用のビルドスクリプト
https://github.com/memN0ps/rootkit-rs/blob/master/driver/build.rs
5. MSVC フラグ
https://learn.microsoft.com/en-us/cpp/build/reference/linker-options?view=msvc-170