- Para mostrar que también se pueden escribir controladores del kernel de Windows en Rust, se implementó el controlador WDM Booster, que cambia la prioridad de un hilo arbitrario
- El entorno de compilación requiere que WDK o EWDK, LLVM/Clang, los crates de WDK basados en
windows-drivers-rs, y la configuración de build.rs y Cargo.toml encajen correctamente
- En el espacio del kernel no se puede usar la biblioteca estándar, por lo que hay que combinar
#![no_std], el asignador de WDK, un manejador de panic y llamadas FFI unsafe
- El controlador crea
\Device\Booster y \??\Booster, y cambia la prioridad del hilo en el rango de 1 a 31 usando el ThreadData recibido en IRP_MJ_WRITE
- Es posible escribir controladores del kernel de Windows en Rust, pero como los crates de WDK están en la etapa 0.3, hacen falta wrappers más seguros y menos código
unsafe
Controlador WDM Booster implementado en Rust
- El ejemplo traslada a Rust el controlador “Booster” usado en programación del kernel de Windows
- El objetivo es cambiar la prioridad de un hilo arbitrario al valor deseado
- El modelo de controlador es WDM
- El ecosistema de Rust ofrece seguridad de memoria en tiempo de compilación, seguridad de concurrencia, el sistema de compilación
cargo y un ecosistema de crates
- Manejar tipos de C directamente en Rust puede hacer que el código sea verboso
- Usar wrappers y macros adecuados puede reducir esa carga
Preparación de compilación y configuración de Cargo
- Para preparar la compilación del controlador, hay que consultar Windows Drivers-rs e instalar WDK o EWDK
- También se necesita instalar LLVM, que se usa para acceder al compilador Clang
- Se crea un nuevo proyecto de biblioteca en Rust; técnicamente, un controlador de kernel es una DLL que se carga en el espacio del kernel
cargo new --lib booster
build.rs configura cargo para hacer enlace estático con el CRT y configura la compilación del binario WDK
fn main() -> Result<(), wdk_build::ConfigError> {
std::env::set_var("CARGO_CFG_TARGET_FEATURE", "crt-static");
wdk_build::configure_wdk_binary_build()
}
- En
Cargo.toml se incluye el modelo de controlador WDM, el tipo de crate cdylib y las dependencias relacionadas con WDK
- Los crates principales son
wdk, wdk-macros, wdk-alloc, wdk-panic y wdk-sys
- En los perfiles dev/release se configura
panic = "abort" y lto = true
wdk y wdk-sys tienen opcionalmente la feature nightly
Escribir código de kernel sin la biblioteca estándar
- Como en el kernel no existe la biblioteca estándar de Rust, se usa
#![no_std]
wdk_sys se encarga de la interoperabilidad con funciones de bajo nivel del kernel, y wdk ofrece wrappers de más alto nivel
Vec y String parecen formar parte de la biblioteca estándar, pero en realidad se pueden usar los tipos del módulo alloc
- Para usarlos se necesita un asignador global
- Se designa el
WdkAllocator que proveen los crates de WDK como #[global_allocator]
WdkAllocator administra las asignaciones usando ExAllocatePool2 y ExFreePool
- Como no hay biblioteca estándar, se agregan los crates externos
wdk_panic y alloc para soporte de asignador y manejador de panic
DriverEntry e inicialización del dispositivo
- El punto de entrada de un controlador del kernel de Windows es
DriverEntry
- La función de Rust se nombra
driver_entry siguiendo la convención, y con #[export_name = "DriverEntry"] se especifica el nombre que busca el linker
- La macro
println! está reimplementada como llamadas a DbgPrint, por lo que puede usarse para salida de depuración del kernel tal como se usa DbgPrint en C/C++
UNICODE_STRING no está soportado directamente en println!, así que se convierte a un String de Rust mediante la función unicode_to_string
- El objeto de dispositivo se crea en
\Device\Booster con IoCreateDevice
- Si falla, se imprime el estado de error y se devuelve el
NTSTATUS correspondiente
- Para determinar si tuvo éxito, se usa
nt_success, similar a la macro NT_SUCCESS de WDK
- Para poder abrir el dispositivo con una llamada estándar a
CreateFile, se crea el enlace simbólico \??\Booster con IoCreateSymbolicLink
- Si falla la creación del enlace simbólico, se elimina el objeto de dispositivo y se devuelve el estado de fallo
- El objeto de dispositivo se configura para usar Buffered I/O
DriverUnload se configura como boost_unload
IRP_MJ_CREATE e IRP_MJ_CLOSE se manejan con boost_create_close
IRP_MJ_WRITE se maneja con boost_write
- La existencia de callbacks se representa con el tipo
Option<> de Rust
Manejo de solicitudes y cambio de prioridad de hilos
- La rutina de descarga llama a
IoDeleteSymbolicLink y IoDeleteDevice para limpiar el enlace simbólico y el objeto de dispositivo
- El manejo de
IRP_MJ_CREATE e IRP_MJ_CLOSE es simple
- Establece
IoStatus.Status del IRP en STATUS_SUCCESS
- Establece
IoStatus.Information en 0
- Completa la solicitud con
IofCompleteRequest
IoStatus es IO_STATUS_BLOCK, y al acceder a Status hay que pasar por un miembro de union generado automáticamente, lo que hace que el código no se vea bien
- Esa definición queda como un punto por revisar más adelante
- El cambio real de prioridad se realiza en el handler de
IRP_MJ_WRITE
- La estructura que el cliente pasa al controlador usa
#[repr(C)] para tener la misma disposición de memoria que en C/C++
#[repr(C)]
struct ThreadData {
pub thread_id: u32,
pub priority: i32,
}
boost_write interpreta el SystemBuffer entregado por Buffered I/O como un puntero a ThreadData
- La validación de errores incluye las siguientes condiciones
- Si el puntero de datos es null,
STATUS_INVALID_PARAMETER
- Si la prioridad es menor que 1 o mayor que 31,
STATUS_INVALID_PARAMETER
- Busca el objeto de hilo con
PsLookupThreadByThreadId
- Si falla, es posible que no exista ese ID de hilo, y se sale del ciclo de procesamiento
- Si encuentra el hilo, establece la prioridad con
KeSetPriorityThread y libera la referencia con ObfDereferenceObject
- Al completar la solicitud, establece los campos de estado e información del IRP y luego llama a
IofCompleteRequest
Firma, instalación y pruebas
- Si existe un archivo INF o INX, parece que los crates soportan la firma del controlador, pero este ejemplo no usa INF, por lo que se requiere firma manual
- Desde la raíz del proyecto, se puede firmar el artefacto de compilación con el siguiente comando
signtool sign /n wdk /fd sha256 target\debug\booster.dll
/n wdk usa el certificado de prueba de WDK que Visual Studio normalmente genera automáticamente al compilar controladores
- La extensión del artefacto de compilación es DLL
- Actualmente no hay forma de cambiar automáticamente la extensión durante el proceso de
cargo build
- Si se usa INF/INX, la extensión cambia a SYS
- La extensión puede cambiarse manualmente o dejarse como DLL
- En una máquina con firma de prueba habilitada, se puede instalar como controlador de software usando
sc.exe desde un símbolo del sistema con privilegios de administrador
sc.exe sc create booster type= kernel binPath= c:\path_to_driver_file
sc.exe start booster
- El cliente de prueba usa una aplicación C++ existente
- Abre el dispositivo con
CreateFile(L"\\\\.\\Booster", GENERIC_WRITE, ...)
- Coloca el ID de hilo y la prioridad en
ThreadData y los pasa con WriteFile
- El ejemplo es una prueba que cambia a 26 la prioridad del hilo con ID 9408
Pendientes y referencias
- Es posible escribir controladores de kernel en Rust
- Los crates de WDK están en la versión 0.3, así que todavía hay margen de mejora
- Para aprovechar bien las ventajas de Rust, hacen falta más wrappers seguros
- El código debería ser menos verboso
- Deberían reducirse los bloques
unsafe
- Se deberían aprovechar mejor las ventajas de seguridad que ofrece Rust
- Hay muestras de controladores KMDF en Rust en Windows-rust-driver-samples
- El código de ejemplo puede consultarse en el repositorio Booster
- Hay materiales para aprender Rust en https://trainsec.net
1 comentarios
Opiniones en Hacker News
Había pensado en crear un driver de filtro de sistema de archivos que permitiera configurar reglas de reasignación de rutas por aplicación.
Por ejemplo, algo como
%userprofile%\.vscode -> %appdata%\vscode,%CSIDL_MYDOCUMENTS%\Call of Duty -> %userprofile%\Saved Games\Call of Duty.Me enojaba mucho que la carpeta Documentos y el directorio home se llenaran de porquerías que ya tienen ubicaciones definidas, así que armé el esqueleto de un proyecto de driver de filtro en Rust y leí la documentación de minifiltros, pero al ver la cantidad de trabajo me rendí.
Terminé aceptando que los sistemas Windows inevitablemente se llenan de basura.
Apple sigue esparciendo archivos como
.DS_Store,.fseventsd,._xxxxpor todos lados, sin importar cuántas veces los borre.Aun así, en macOS suele haber una ubicación para instalar aplicaciones y otra para los documentos del usuario, y la mayoría de las apps más o menos lo respetan.
En cambio, hay un basurero designado como
~/Library, lleno de porquerías que no sé si necesito.Shortcut to Documents (OneDrive - Personal).Ahora esas porquerías hasta se sincronizan entre dispositivos, qué maravilla.
La biblioteca Detours permite adjuntar una DLL de usuario al ejecutar un proceso y enganchar llamadas Win32 a las API del sistema de archivos.
Los “shims de compatibilidad” integrados también funcionan de forma parecida, aunque casi no están documentados, y permiten activar flags de compatibilidad y redirigir rutas de archivos y del registro.
También es cómodo que estén diseñados para activarse con heurísticas solo para un EXE específico.
Para dar soporte a tecnologías como App-V y, más tarde, contenedores Docker, Windows tiene una superficie de API para virtualizar el sistema de archivos, el registro y otros espacios de nombres del kernel NT.
Además existe User Mode Filesystem, y probablemente haya otros enfoques que no conozco o que olvidé.
My Documents, y pongo los archivos que realmente me importan en otra ubicación con una ruta más corta.Puede que haya redirigido con junctions de directorio uno o dos programas que tenían hardcodeada la ubicación de los datos.
Cosas como
ntuser.ini,ntuser.dat.LOG1,ntuser.dat.LOG2,NTUSER.DAT,NTUSER.DAT{b2352f18-cdbf-1122-8680-002248483d79}.TM.blf,NTUSER.DAT{b2352f18-cdbf-1122-8680-002248483d79}.TMContainer00000000000000000001.regtrans-ms,NTUSER.DAT{b2352f18-cdbf-1122-8680-002248483d79}.TMContainer00000000000000000002.regtrans-ms.Medio relacionado: me pregunto si hay información reciente sobre el estado del uso de Rust en el kernel de Windows.
Hace casi 2 años dijeron que eran “36,000 líneas de código, incluidas llamadas al sistema” [1], y me gustaría saber cómo avanzó ese proyecto.
[1] https://www.thurrott.com/windows/282471/microsoft-is-rewriti...
Lo más reciente es el firmware del enclave de seguridad.
https://techcommunity.microsoft.com/blog/windows-itpro-blog/...
win32k.sysse portaron a Rust.Si buscas
win32*.sys, puedes ver cómo lo separaron.Interesante. Se ve bastante distinto de los drivers embebidos con los que he trabajado mucho en Rust.
En ese ámbito casi todo gira alrededor de leer y escribir registros, desplazamientos de bits, DMA y consultar datasheets.
El código aquí parece básicamente C con otra sintaxis. Todas las funciones están marcadas como
unsafey todos los recursos se gestionan manualmente.Perdón por ser directo, pero así no entiendo cuál es el sentido de usar Rust.
UNICODE_STRINGdevuelto porstring_to_ustringes un uso después de liberar garantizado.Si quieres escribir código de kernel para Windows, no empieces por aquí.
En un driver que haga algo realmente interesante, podrías dejar las interfaces
unsafesolo en los límites y escribir el interior con un estilo de Rust más típico.¿De verdad van a tirar por la borda las convenciones estándar de nombres de Rust y adoptar la vieja costumbre de Windows de “expresar el tipo en el nombre”?
Hace unos 25 años tuve que escribir un driver específico para Windows.
Para entonces ya me había pasado por completo a Linux, así que no quería usar Windows para escribirlo y compilarlo, y me esforcé bastante para que se pudiera compilar con MSYS.
Al final lo logré y el driver funcionó bien.
Creo que para que realmente cargara había que usar un parcheador sobre el archivo PE resultante (
.sys).Eran tiempos divertidos.
El artículo está bueno, pero el diseño del blog me impresionó aún más.
Es limpio, intuitivo, agradable a la vista y carga de inmediato.
Por lo que vi, parece ser WordPress.com.
Parece depender mucho de caché y CDN.
Me dio pena no haber mantenido mi blog de WordPress de hace 10 años.