2 puntos por GN⁺ 2025-02-10 | 1 comentarios | Compartir por WhatsApp
  • 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

 
GN⁺ 2025-02-10
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.

    • Windows no es el único sistema operativo que usa el sistema de archivos del usuario como si fuera un basurero.
      Apple sigue esparciendo archivos como .DS_Store, .fseventsd, ._xxxx por 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.
    • Me pareció raro que mi carpeta Documents estuviera vacía, hasta que vi que era Shortcut to Documents (OneDrive - Personal).
      Ahora esas porquerías hasta se sincronizan entre dispositivos, qué maravilla.
    • Esto también se puede hacer en modo usuario de varias formas.
      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é.
    • Ignoro por completo ese depósito de residuos tóxicos llamado 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.
    • Microsoft/Windows mismo también mete un montón de porquerías en la carpeta home, y peor aún, con nombres excesivamente largos.
      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...

  • 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 unsafe y todos los recursos se gestionan manualmente.
    Perdón por ser directo, pero así no entiendo cuál es el sentido de usar Rust.

    • Y no sé cómo se les pasó, pero intentar usar el UNICODE_STRING devuelto por string_to_ustring es un uso después de liberar garantizado.
      Si quieres escribir código de kernel para Windows, no empieces por aquí.
    • Lamentablemente, los ejemplos simples son prácticamente todo código de plomería, y esas partes terminan tocando interfaces nativas inseguras.
      En un driver que haga algo realmente interesante, podrías dejar las interfaces unsafe solo en los límites y escribir el interior con un estilo de Rust más típico.
    • También hay muchísimos nombres de tipos en mayúsculas.
      ¿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.

    • Me pasó lo mismo. Tuve que revisar otra vez con qué tecnología estaba hecho el blog.
      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.