All Articles

Rust で NTAPI を使用する

今回は 前回までの記事 に引き続き、Rust で Windows Crate を使用して GetModuleHandleW などの API を使用して DLL をロードし、NtQuerySystemInformation のような公式の Windows Crate に含まれていない NTAPI を使用するプログラムを作成してみました。

参考:NtQuerySystemInformation 関数 (winternl.h) - Win32 apps | Microsoft Learn

もくじ

作成したコード

今回作成したコードは以下の通りです。

このコードを実行すると NtQuerySystemInformation API を使用して取得した情報から、システム内で稼働するプロセスやハンドルの情報などを列挙することができます。

image-20250413192404220

// cargo run --bin CallNtApi

use windows::{
    // core::{ HSTRING, PCSTR },
    core:: { s, w },
    Wdk::System::SystemServices::VM_COUNTERS,
    Win32::{Foundation::CloseHandle, System::{
        LibraryLoader::{ GetModuleHandleW, GetProcAddress },
        Threading::IO_COUNTERS
    }}
};

use std::{
    // ffi::CString,
    io::stdin,
    mem::transmute,
    os::raw::{ c_void, c_ulong, c_ushort },
    ptr::null_mut,
    slice::from_raw_parts
};

type HANDLE = *mut c_void;
type ULONG = c_ulong;
type USHORT = c_ushort;
type WCHAR = u16;

#[repr(C)]
struct UNICODE_STRING {
    Length: USHORT,
    MaximumLength: USHORT,
    Buffer: *const WCHAR,
}

#[repr(C)]
struct SYSTEM_PROCESS_INFORMATION {
    NextEntryOffset: ULONG,
    NumberOfThreads: ULONG,
    Reserved: [u8; 48],
    ImageName: UNICODE_STRING,
    BasePriority: ULONG,
    UniqueProcessId: HANDLE,
    InheritedFromUniqueProcessId: HANDLE,
    HandleCount: ULONG,
    Reserved2: ULONG,
    PrivatePageCount: ULONG,
    VirtualMemoryCounters: VM_COUNTERS,
    IoCounters: IO_COUNTERS
    // 省略 Threads: SYSTEM_THREAD
}

type NtQuerySystemInformationFn = unsafe extern "system" fn(
    SystemInformationClass: ULONG,
    SystemInformation: *mut c_void,
    SystemInformationLength: ULONG,
    ReturnLength: *mut ULONG,
) -> i32;


fn get_input(input_string: &str) {
    println!("{}", input_string);
    let mut input = String::new();
    stdin().read_line(&mut input).expect("Failed to read input.");
    return;
}


fn main() {
    unsafe {

        // Load ntdll
        // let ntdll = &HSTRING::from("ntdll.dll");
        let ntdll = w!("ntdll.dll");
        let h_ntdll_module = GetModuleHandleW(ntdll).unwrap();
        if h_ntdll_module.is_invalid() {
            eprintln!("Failed to get handle to ntdll.dll");
            return;
        } else {
            println!("Created h_ntdll: {:?}", h_ntdll_module);
        }

        // Get proc address
        // let s_func_name = CString::new("NtQuerySystemInformation").unwrap();
        let s_func_name = s!("NtQuerySystemInformation");
        let func_addr = GetProcAddress(h_ntdll_module, s_func_name);
        if func_addr.is_none() {
            eprintln!("Failed to get proc address.");
            return;
        } else {
            println!("Get proc address: {:?}", func_addr);
        }
        let nt_query_system_information: NtQuerySystemInformationFn = transmute(func_addr);

        // First call func
        let system_process_information = 5; // SystemProcessInformation
        let mut u_return_length: ULONG = 0;
        let status = nt_query_system_information(
            system_process_information,
            null_mut(),
            0,
            &mut u_return_length,
        );
        // println!("First NtQuerySystemInformation call status: {:#x}", status);
        assert!(status as u32 == 0xc0000004);
        println!("NtQuerySystemInformation return length: {:?}", u_return_length);

        // Second call func
        let buffer = vec![0u8; u_return_length as usize];
        let status = nt_query_system_information(
            system_process_information,
            buffer.as_ptr() as *mut c_void,
            u_return_length,
            &mut u_return_length,
        );
        if status != 0 {
            eprintln!("NtQuerySystemInformation failed: {:?}", status);
            return;
        }

        // Analyze buffer
        let mut offset = 0;
        while offset < u_return_length {
            let spi = &*(buffer.as_ptr().add(offset as usize) as *const SYSTEM_PROCESS_INFORMATION);

            let name = if !spi.ImageName.Buffer.is_null() && spi.ImageName.Length > 0 {
                let slice = from_raw_parts(spi.ImageName.Buffer, (spi.ImageName.Length / 2) as usize);
                String::from_utf16_lossy(slice)
            } else {
                String::from("System Idle Process")
            };

            println!(
                "Name: {},\tPID: {:?},\tPPID: {:?},\tThreads: {:?},\tHandles: {:?}",
                name,
                spi.UniqueProcessId,
                spi.InheritedFromUniqueProcessId,
                spi.NumberOfThreads,
                spi.HandleCount
            );

            if spi.NextEntryOffset == 0 {
                break;
            }

            offset += spi.NextEntryOffset;

        }

    }

    get_input("Finish program.");
    return;
}

使用したモジュール

今回は以下のモジュールをロードしています。

コメントアウトしているモジュールは Windows API の引数として使用する Param<PCWSTR>Param<PCSTR> を用意するために使用していたものですが、windows::cores!w! といった便利なマクロを使用する方が便利なため使用をやめたものです。

参考:w in windows_sys::core - Rust

参考:s in windows_sys::core - Rust

use windows::{
    // core::{ HSTRING, PCSTR },
    core:: { s, w },
    Wdk::System::SystemServices::VM_COUNTERS,
    Win32::{Foundation::CloseHandle, System::{
        LibraryLoader::{ GetModuleHandleW, GetProcAddress },
        Threading::IO_COUNTERS
    }}
};

use std::{
    // ffi::CString,
    io::stdin,
    mem::transmute,
    os::raw::{ c_void, c_ulong, c_ushort },
    ptr::null_mut,
    slice::from_raw_parts
};

GetModuleHandleW の使用

今回はまず始めに GetModuleHandleW API により NtQuerySystemInformation がエクスポートされている ntdll.dll のモジュールハンドルを取得します。

GetModuleHandleW は呼び出し元プロセスにロードされているモジュールのモジュールハンドルを取得できる API です。

参考:GetModuleHandleW 関数 (libloaderapi.h) - Win32 apps | Microsoft Learn

GetModuleHandleW は引数として Param<PCWSTR> を要求するので w!("ntdll.dll") を使用して文字列リテラルの変換を行っています。

参考:GetModuleHandleW in windows::Win32::System::LibraryLoader - Rust

参考:HMODULE in windows::Win32::Foundation - Rust

// Load ntdll
// let ntdll = &HSTRING::from("ntdll.dll");
let ntdll = w!("ntdll.dll");
let h_ntdll_module = GetModuleHandleW(ntdll).unwrap();
if h_ntdll_module.is_invalid() {
    eprintln!("Failed to get handle to ntdll.dll");
    return;
} else {
    println!("Created h_ntdll: {:?}", h_ntdll_module);
}

また、Windows Crate の GetModuleHandleW は Result<HMODULE> を返すため unwrap で HMODULE を取り出しています。

NtQuerySystemInformation のアドレス取得

続いて、取得したモジュールハンドルを引数として GetProcAddress により NtQuerySystemInformation の関数アドレスを取得します。

参考:GetProcAddress 関数 (libloaderapi.h) - Win32 apps | Microsoft Learn

こちらは引数として Param<PCWSTR> ではなく Param<PCSTR> を要求するので、s!("NtQuerySystemInformation") で文字列リテラルを変換したものを引数としています。

参考:GetProcAddress in windows::Win32::System::LibraryLoader - Rust

// Get proc address
// let s_func_name = CString::new("NtQuerySystemInformation").unwrap();
let s_func_name = s!("NtQuerySystemInformation");
let func_addr = GetProcAddress(h_ntdll_module, s_func_name);
if func_addr.is_none() {
    eprintln!("Failed to get proc address.");
    return;
} else {
    println!("Get proc address: {:?}", func_addr);
}
let nt_query_system_information: NtQuerySystemInformationFn = transmute(func_addr);

NtQuerySystemInformation 関数を使用する

続いて、取得した関数アドレスを使用して NtQuerySystemInformation によるプロセス情報の列挙を行っていきます。

// First call func
let system_process_information = 5; // SystemProcessInformation
let mut u_return_length: ULONG = 0;
let status = nt_query_system_information(
    system_process_information,
    null_mut(),
    0,
    &mut u_return_length,
);
// println!("First NtQuerySystemInformation call status: {:#x}", status);
assert!(status as u32 == 0xc0000004);
println!("NtQuerySystemInformation return length: {:?}", u_return_length);

// Second call func
let buffer = vec![0u8; u_return_length as usize];
let status = nt_query_system_information(
    system_process_information,
    buffer.as_ptr() as *mut c_void,
    u_return_length,
    &mut u_return_length,
);
if status != 0 {
    eprintln!("NtQuerySystemInformation failed: {:?}", status);
    return;
}

取得する配列サイズの確認

今回の目的はプロセスの列挙であるため、SystemProcessInformation フラグを引数として NtQuerySystemInformation 関数を呼び出します。

参考:SystemProcessInformation

その際、返却される SYSTEMPROCESSINFORMATION 構造体の配列のサイズが不明であるため、まず 1 回目の NtQuerySystemInformation 関数の呼び出しでサイズを取得します。

この呼び出しは第 2 引数と第 3 引数に不適切な値を設定しているため 0xc0000004 のエラーで失敗しますが、u_return_length には返却される構造体配列のサイズが書き込まれます。

// First call func
let system_process_information = 5; // SystemProcessInformation
let mut u_return_length: ULONG = 0;
let status = nt_query_system_information(
    system_process_information,
    null_mut(),
    0,
    &mut u_return_length,
);
// println!("First NtQuerySystemInformation call status: {:#x}", status);
assert!(status as u32 == 0xc0000004);
println!("NtQuerySystemInformation return length: {:?}", u_return_length);

プロセスの情報を含む構造体配列の取得

1 回目の関数呼び出しで返される構造体配列のサイズを取得できたら、次にそのサイズを第 3 引数の SystemInformationLength として使用して再度 NtQuerySystemInformation 関数を呼び出します。

返却される構造体配列は vec! マクロによって確保した u_return_length のサイズのベクタに保存されます。

// Second call func
let buffer = vec![0u8; u_return_length as usize];
let status = nt_query_system_information(
    system_process_information,
    buffer.as_ptr() as *mut c_void,
    u_return_length,
    &mut u_return_length,
);
if status != 0 {
    eprintln!("NtQuerySystemInformation failed: {:?}", status);
    return;
}

これでプロセスの情報を含む SYSTEMPROCESSINFORMATION 構造体の情報を取得することができました。

構造体の解析を行う

最後に、取得した SYSTEMPROCESSINFORMATION 構造体の配列から各プロセスの情報を解析します。

Rust から SYSTEMPROCESSINFORMATION 構造体のような C で定義されている構造体を扱うのは少々面倒でしたが、C のヘッダファイルで実装されているような構造体情報を独自に定義すれば OK でした。

type HANDLE = *mut c_void;
type ULONG = c_ulong;
type USHORT = c_ushort;
type WCHAR = u16;

#[repr(C)]
struct UNICODE_STRING {
    Length: USHORT,
    MaximumLength: USHORT,
    Buffer: *const WCHAR,
}

#[repr(C)]
struct SYSTEM_PROCESS_INFORMATION {
    NextEntryOffset: ULONG,
    NumberOfThreads: ULONG,
    Reserved: [u8; 48],
    ImageName: UNICODE_STRING,
    BasePriority: ULONG,
    UniqueProcessId: HANDLE,
    InheritedFromUniqueProcessId: HANDLE,
    HandleCount: ULONG,
    Reserved2: ULONG,
    PrivatePageCount: ULONG,
    VirtualMemoryCounters: VM_COUNTERS,
    IoCounters: IO_COUNTERS
    // 省略 Threads: SYSTEM_THREAD
}

構造体情報を定義するため、Rust の #[repr(C)] を使用します。

これは C 言語のデータレイアウトを使用することを指示する記法で、構造体のメモリレイアウトの最適化を制御して C 構造体と同じメモリレイアウトの構造体を定義することができるようになるようです。

参考:Rustの構造体メモリレイアウト - ryochack.blog

参考:Other reprs - The Rustonomicon

今回は UNICODE_STRINGSYSTEM_PROCESS_INFORMATION の 2 つの構造体情報を定義しました。

ただし、SYSTEM_THREAD 構造体については定義が見つからなかったのと特に必要がなかったので今回は実装を省略しています。

参考:NTAPI Undocumented Functions

参考:UNICODESTRING (ntdef.h) - Win32 apps | Microsoft Learn

構造体の定義ができたので、最後に NtQuerySystemInformation 関数で取得した SYSTEMPROCESSINFORMATION 構造体配列の解析を行います。

SYSTEMPROCESSINFORMATION 構造体には便利なことに NextEntryOffset という次の配列の要素までのオフセットを含むメンバが存在するため、これをループ処理内で加算していく方法で次の要素にアクセスしています。

// Analyze buffer
let mut offset = 0;
while offset < u_return_length {
    let spi = &*(buffer.as_ptr().add(offset as usize) as *const SYSTEM_PROCESS_INFORMATION);

    let name = if !spi.ImageName.Buffer.is_null() && spi.ImageName.Length > 0 {
        let slice = from_raw_parts(spi.ImageName.Buffer, (spi.ImageName.Length / 2) as usize);
        String::from_utf16_lossy(slice)
    } else {
        String::from("System Idle Process")
    };

    println!(
        "Name: {},\tPID: {:?},\tPPID: {:?},\tThreads: {:?},\tHandles: {:?}",
        name,
        spi.UniqueProcessId,
        spi.InheritedFromUniqueProcessId,
        spi.NumberOfThreads,
        spi.HandleCount
    );

    if spi.NextEntryOffset == 0 {
        break;
    }

    offset += spi.NextEntryOffset;

}

また、&*(buffer.as_ptr().add(offset as usize) as *const SYSTEM_PROCESS_INFORMATION); では buffer.as_ptr().add(offset as usize) で配列のポインタアドレスに解析対象の構造体までのオフセットを加算したアドレスを SYSTEM_PROCESS_INFORMATION 構造体として解釈した変数を宣言しています。

さらに、UNICODESTRING 構造体で保持されている ImageName からプロセス名を取り出すために、fromrawparts によってマルチバイトごとに区切られたスライスを作成し、fromutf16_lossy でデコードしています。

let name = if !spi.ImageName.Buffer.is_null() && spi.ImageName.Length > 0 {
    let slice = from_raw_parts(spi.ImageName.Buffer, (spi.ImageName.Length / 2) as usize);
    String::from_utf16_lossy(slice)
} else {
    String::from("System Idle Process")
};

参考:fromrawparts in std::slice - Rust

参考:String in std::string - Rust

これで、プロセス名を含む各種プロセスの情報を取得できました。

まとめ

これで NTAPI についても Rust から使用することができるようになりました。

ただ、あえて Rust から使用するメリットがあるのかについてはいまいちよくわかっていません。