2 pontos por GN⁺ 2025-02-10 | 1 comentários | Compartilhar no WhatsApp
  • Para mostrar que drivers de kernel do Windows também podem ser escritos em Rust, foi implementado o driver WDM Booster, que altera a prioridade de uma thread arbitrária
  • O ambiente de build precisa alinhar WDK ou EWDK, LLVM/Clang, crates WDK baseados em windows-drivers-rs e as configurações de build.rs e Cargo.toml
  • No espaço de kernel não é possível usar a biblioteca padrão, então é preciso combinar #![no_std], WDK allocator, panic handler e chamadas FFI unsafe
  • O driver cria \\Device\\Booster e \\??\\Booster e, a partir de ThreadData recebido em IRP_MJ_WRITE, altera a prioridade da thread no intervalo de 1 a 31
  • Escrever drivers de kernel do Windows em Rust é viável, mas como os crates WDK ainda estão na versão 0.3, são necessários wrappers mais seguros e menos código unsafe

Driver WDM Booster implementado em Rust

  • O exemplo é uma versão em Rust do driver “Booster” usado em programação de kernel do Windows
    • O objetivo é mudar a prioridade de uma thread arbitrária para o valor desejado
    • O modelo de driver é WDM
  • O ecossistema Rust oferece segurança de memória em tempo de compilação, segurança de concorrência, o sistema de build cargo e um ecossistema de crates
  • Trabalhar diretamente com tipos de C em Rust pode deixar o código verboso
    • Com wrappers e macros adequados, essa carga pode ser reduzida

Preparação do build e configuração do Cargo

  • Para preparar o build do driver, consulte Windows Drivers-rs e instale o WDK ou o EWDK
  • Também é necessário instalar o LLVM, usado para acessar o compilador Clang
  • Cria-se um novo projeto de biblioteca Rust, já que um driver de kernel é, tecnicamente, uma DLL carregada no espaço de kernel
cargo new --lib booster
  • O build.rs configura o cargo para fazer linkagem estática com a CRT e montar o build binário do WDK
fn main() -> Result<(), wdk_build::ConfigError> {
    std::env::set_var("CARGO_CFG_TARGET_FEATURE", "crt-static");
    wdk_build::configure_wdk_binary_build()
}
  • Em Cargo.toml, entram o modelo de driver WDM, o tipo de crate cdylib e as dependências relacionadas ao WDK
    • Os principais crates são wdk, wdk-macros, wdk-alloc, wdk-panic, wdk-sys
    • Nos perfis dev/release, define-se panic = "abort" e lto = true
    • wdk e wdk-sys têm opcionalmente a feature nightly

Escrevendo código de kernel sem biblioteca padrão

  • Como não há biblioteca padrão Rust no kernel, usa-se #![no_std]
  • wdk_sys cuida da interoperabilidade com funções de kernel de baixo nível, enquanto wdk fornece wrappers de nível mais alto
  • Vec e String parecem parte da biblioteca padrão, mas na prática é possível usar os tipos do módulo alloc
    • Para isso, é necessário um alocador global
    • Define-se o WdkAllocator fornecido pelos crates WDK com #[global_allocator]
  • O WdkAllocator gerencia alocação com ExAllocatePool2 e ExFreePool
  • Como não existe biblioteca padrão, adicionam-se os crates externos wdk_panic e alloc para suporte ao allocator e ao panic handler

DriverEntry e inicialização do dispositivo

  • O ponto de entrada de um driver de kernel do Windows é DriverEntry
    • O nome da função Rust fica como driver_entry por convenção, e #[export_name = "DriverEntry"] define o nome que o linker procura
  • A macro println! é reimplementada como chamada a DbgPrint, então pode ser usada na saída de debug do kernel como em C/C++
  • UNICODE_STRING não é suportado diretamente em println!, então ele é convertido para String Rust com a função unicode_to_string
  • O objeto de dispositivo é criado em \\Device\\Booster com IoCreateDevice
    • Em caso de falha, o status de erro é exibido e esse NTSTATUS é retornado
    • Para verificar sucesso, usa-se nt_success, similar à macro NT_SUCCESS do WDK
  • Para permitir abrir o dispositivo com a chamada padrão CreateFile, cria-se um link simbólico \\??\\Booster com IoCreateSymbolicLink
    • Se a criação do link simbólico falhar, o objeto de dispositivo é removido e o status de falha é retornado
  • O objeto de dispositivo é configurado para usar Buffered I/O
    • DriverUnload é definido como boost_unload
    • IRP_MJ_CREATE e IRP_MJ_CLOSE são tratados por boost_create_close
    • IRP_MJ_WRITE é tratado por boost_write
  • A existência ou não de callbacks é representada pelo tipo Option<> do Rust

Tratamento de requisições e mudança de prioridade da thread

  • A rotina de unload chama IoDeleteSymbolicLink e IoDeleteDevice para limpar o link simbólico e o objeto de dispositivo
  • O tratamento de IRP_MJ_CREATE e IRP_MJ_CLOSE é simples
    • Define IoStatus.Status como STATUS_SUCCESS
    • Define IoStatus.Information como 0
    • Finaliza a requisição com IofCompleteRequest
  • IoStatus é um IO_STATUS_BLOCK, e ao acessar Status é preciso passar pelo membro de union gerado automaticamente, o que deixa o código pouco elegante
    • Essa definição ainda ficou como ponto a investigar
  • A mudança real de prioridade é feita no handler IRP_MJ_WRITE
  • A struct enviada pelo cliente ao driver usa #[repr(C)] para ter o mesmo layout de memória de C/C++
#[repr(C)]
struct ThreadData {
    pub thread_id: u32,
    pub priority: i32,
}
  • boost_write interpreta o SystemBuffer entregue via Buffered I/O como um ponteiro para ThreadData
  • As validações de erro incluem as seguintes condições
    • Se o ponteiro de dados for null, STATUS_INVALID_PARAMETER
    • Se a prioridade for menor que 1 ou maior que 31, STATUS_INVALID_PARAMETER
  • PsLookupThreadByThreadId localiza o objeto de thread
    • Se falhar, é possível que aquele ID de thread não exista, e o loop de tratamento é interrompido
  • Quando a thread é encontrada, KeSetPriorityThread define a prioridade e ObfDereferenceObject libera a referência
  • Ao concluir a requisição, os campos de status e informação do IRP são definidos e então IofCompleteRequest é chamado

Assinatura, instalação e testes

  • Se houver um arquivo INF ou INX, os crates aparentemente dão suporte à assinatura do driver, mas como este exemplo não usa INF, é necessário assinar manualmente
  • Na raiz do projeto, é possível assinar o artefato de build com o seguinte comando
signtool sign /n wdk /fd sha256 target\debug\booster.dll
  • /n wdk usa o certificado de teste WDK que o Visual Studio normalmente gera automaticamente ao compilar drivers
  • A extensão do artefato gerado é DLL
    • No momento, não há uma forma no processo cargo build de trocar automaticamente a extensão
    • Com INF/INX, a extensão passa a ser SYS
    • Você pode renomear manualmente a extensão ou deixar como DLL mesmo
  • Em uma máquina com assinatura de teste ativada, é possível instalar como um driver de software usando sc.exe em um Prompt de Comando com privilégios de administrador
sc.exe sc create booster type= kernel binPath= c:\path_to_driver_file
sc.exe start booster
  • O cliente de teste usa a aplicação C++ existente
    • Abre o dispositivo com CreateFile(L"\\\\.\\Booster", GENERIC_WRITE, ...)
    • Preenche ThreadData com o ID da thread e a prioridade e envia com WriteFile
    • O exemplo testa a mudança da prioridade da thread de ID 9408 para 26

Pendências e materiais de referência

  • Escrever drivers de kernel em Rust é possível
  • Os crates WDK ainda estão na versão 0.3, então ainda há espaço para melhorias
  • Para aproveitar de fato as vantagens do Rust, são necessários mais wrappers seguros
    • O código precisa ser menos verboso
    • Os blocos unsafe precisam diminuir
    • As vantagens de segurança oferecidas pelo Rust precisam ser melhor aproveitadas
  • Há um exemplo de driver KMDF em Rust em Windows-rust-driver-samples
  • O código de exemplo pode ser visto no repositório Booster
  • Materiais de aprendizado de Rust estão em https://trainsec.net

1 comentários

 
GN⁺ 2025-02-10
Comentários no Hacker News
  • Eu tinha pensado em criar um driver de filtro de sistema de arquivos que permitisse configurar regras de remapeamento de caminhos por aplicativo
    Por exemplo, algo como %userprofile%\.vscode -> %appdata%\vscode, %CSIDL_MYDOCUMENTS%\Call of Duty -> %userprofile%\Saved Games\Call of Duty
    Fiquei tão irritado com a pasta Documentos e o diretório home ficando cheios de tralhas em locais já pré-definidos que criei um esqueleto de projeto de driver de filtro em Rust e li a documentação de minifiltros, mas desisti ao ver a quantidade de trabalho
    Acabei aceitando o fato de que sistemas Windows inevitavelmente acabam cheios de lixo

    • O Windows não é o único sistema operacional que trata o sistema de arquivos do usuário como um depósito de lixo
      A Apple continua espalhando arquivos como .DS_Store, .fseventsd, ._xxxx por todo lado, não importa quantas vezes eu os apague
      Ainda assim, no macOS geralmente há um local para instalar aplicativos e outro para documentos do usuário, e a maioria dos apps respeita isso em alguma medida
      Em compensação, há um lixão designado como ~/Library, cheio de tralhas que não sei se preciso ou não
    • Minha pasta Documents estava estranhamente vazia; fui ver e era Shortcut to Documents (OneDrive - Personal)
      Agora essas porcarias estão até sincronizando entre dispositivos, o que é ótimo
    • Isso também é possível em modo de usuário de várias formas
      A biblioteca Detours permite anexar uma DLL do usuário quando um processo é iniciado para fazer hook das chamadas Win32 às APIs de sistema de arquivos
      Os “shims de compatibilidade” embutidos também funcionam de forma parecida, embora sejam quase nada documentados, permitindo ativar flags de compatibilidade e redirecionar caminhos de arquivos e do Registro
      Também é conveniente o fato de terem sido projetados para serem acionados com base em heurísticas apenas para EXEs específicos
      Para dar suporte a tecnologias como App-V e, depois, contêineres Docker, o Windows tem uma superfície de APIs para virtualizar o sistema de arquivos, o Registro e outros namespaces do kernel NT
      Além disso, há o User Mode Filesystem, e provavelmente existem outras abordagens que não conheço ou esqueci
    • Eu ignoro completamente aquele depósito de resíduos tóxicos chamado My Documents e coloco os arquivos com que realmente me importo em outro lugar, com caminhos mais curtos
      Talvez eu tenha redirecionado um ou dois programas que tinham o local dos dados hardcoded usando junções de diretório
    • A própria Microsoft/Windows também coloca um monte de tralha na pasta home e, pior, com nomes excessivamente longos
      Coisas 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
  • Meio relacionado: alguém tem informações recentes sobre o estado do uso de Rust no kernel do Windows?
    Quase dois anos atrás falaram em “36.000 linhas de código, incluindo chamadas de sistema” [1], e eu queria saber como esse projeto evoluiu
    [1] https://www.thurrott.com/windows/282471/microsoft-is-rewriti...

  • Interessante. Parece bem diferente dos drivers embarcados que fiz bastante em Rust
    Nesse mundo, o foco é mais ler e escrever registradores, deslocamento de bits, DMA e consultar datasheets

  • O código aqui parece basicamente C com outra sintaxe. Todas as funções estão marcadas como unsafe, e todos os recursos são gerenciados manualmente
    Desculpe ser direto, mas assim não entendo qual é o sentido de usar Rust

    • E não sei como isso passou batido, mas a tentativa de usar o UNICODE_STRING retornado por string_to_ustring é um uso após liberação garantido
      Se você quer escrever código de kernel para Windows, não deveria começar por aqui
    • Infelizmente, exemplos simples são praticamente só código de encanamento, e essas partes acabam tocando interfaces nativas inseguras
      Em um driver que faça algo realmente interessante, daria para deixar as interfaces unsafe só nas bordas e escrever o interior em Rust mais típico
    • Também há uma quantidade enorme de nomes de tipos em caixa alta aqui
      Será que vão mesmo abandonar todas as convenções padrão de nomenclatura do Rust e adotar a velha prática do Windows de “expressar o tipo pelo nome”?
  • Uns 25 anos atrás precisei escrever um driver específico para Windows
    Na época eu já tinha migrado totalmente para Linux e não queria usar Windows para escrever e compilar, então me esforcei bastante para fazê-lo compilar com MSYS
    No fim deu certo, e o driver funcionou bem
    Acho que, para ele ser carregado de fato, eu precisava usar um patcher no arquivo PE resultante (.sys)
    Tempos divertidos

  • O texto é bom, mas o design do blog me impressionou ainda mais
    É limpo, intuitivo, confortável para os olhos e carrega instantaneamente

    • Comigo foi igual. Tive que conferir de novo qual tecnologia o blog usava
      Pelo visto, parece ser WordPress.com
      Parece depender bastante de cache e CDN
      Dá até arrependimento de eu não ter mantido meu blog WordPress de 10 anos atrás