- 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
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 DutyFiquei 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
A Apple continua espalhando arquivos como
.DS_Store,.fseventsd,._xxxxpor todo lado, não importa quantas vezes eu os apagueAinda 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ãoShortcut to Documents (OneDrive - Personal)Agora essas porcarias estão até sincronizando entre dispositivos, o que é ótimo
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
My Documentse coloco os arquivos com que realmente me importo em outro lugar, com caminhos mais curtosTalvez eu tenha redirecionado um ou dois programas que tinham o local dos dados hardcoded usando junções de diretório
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-msMeio 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...
O exemplo mais recente é o firmware do enclave de segurança
https://techcommunity.microsoft.com/blog/windows-itpro-blog/...
win32k.sysforam portadas para RustPesquisando por
win32*.sys, dá para ver como isso foi separadoInteressante. 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 manualmenteDesculpe ser direto, mas assim não entendo qual é o sentido de usar Rust
UNICODE_STRINGretornado porstring_to_ustringé um uso após liberação garantidoSe você quer escrever código de kernel para Windows, não deveria começar por aqui
Em um driver que faça algo realmente interessante, daria para deixar as interfaces
unsafesó nas bordas e escrever o interior em Rust mais típicoSerá 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
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