
No último post, em resposta à uma dúvida de um leitor, comentei um pouco sobre como criar e usar novas IOCTLs. Afinal de contas, o lema desse blog é “Servir bem para servir sempre”. Dúvidas de leitores são ótimas fontes de sugestões para novos posts. Creio que como em qualquer outra especialidade, o desenvolvimento de drivers é um tópico que pode ser desmembrado em muitas e muitas partes. Não é a toa que o menor livro que eu conheço sobre desenvolvimento de drivers não tenha menos de quatrocentas páginas. Por isso é bom saber quais são as dificuldades dos leitores para saber sobre qual assunto postar aqui. Sintam-se à vontade para mandar novas sugestões ou dúvidas. Já há algum tempo, recebi um e-mail de Fábio Dias (Fortaleza, CE), que sugeriu um post sobre como acessar o Registry a partir de um driver. Muito bem, vamos lá.
HKEY_LOCAL_MACHINE é coisa se User Mode
Antes de sair dizendo quais APIs você deve usar para acessar o registro, vamos antes dar uma olhada em como o registro está organizado. Creio que o primeiro passo aqui seja falar sobre como abrir uma chave do registro. Assim como em User Mode, temos que ter uma string que informe o caminho da chave a qual queremos abrir, e assim obter um handle para ela. Opa! Eu disse handle? Sendo assim, vale comentar que as chaves do registro também são recursos gerenciados pelo Object Manager. As chaves do registro estão todas armazenadas sob o namespace “\Registry”. Desta forma, enquanto usamos HKEY_LOCAL_MACHINE em User Mode como nome de uma das divisões básicas do registro, em Kernel Mode usamos “\Registry\Machine”. E de maneira análoga à HKEY_USERS temos “\Registry\Users”. Mais detalhes aqui.
Qual é o CurrentControlSet?
A rotina DriverEntry de qualquer driver para Windows NT recebe dois parâmetros de entrada, sendo um deles um ponteiro para a estrutura DRIVER_OBJECT que representa a instância do nosso driver, e o outro parâmetro é um UNICODE_STRING contendo o caminho do registro que indica onde nosso driver está cadastrado. Mas como assim? Nosso driver pode não saber como ele foi cadastrado no registro? Aqui na referência é simples:
“The registry path string pointed to by RegistryPath is of the form \Registry\Machine\System\CurrentControlSet\Services\DriverName”
Tudo bem, vamos pegar carona em um driver qualquer e dar uma olhada nisso utilizando o WinDbg. Vamos abrir um parênteses aqui para informar aos mais novos neste blog que estou utilizando uma máquina virtual para fazer os testes com o driver de exemplo, como explica neste post. Isso permite que eu possa fazer Debug de Kernel sem necessariamente ter que utilizar duas máquinas. Para facilitar a substituição do driver a cada modificação que faço, estou utilizando o mapeamento de drivers do WinDbg, como explica neste outro post. Este recurso permite que o WinDbg sempre carregue uma nova versão driver na máquina TARGET sem que eu tenha que necessariamente substituir o driver nela. Fecha parênteses.
Antes mesmo do driver ser carregado, coloquei um breakpoint na rotina DriverEntry. Opa! Como você pode colocar um breakpoint em um driver que ainda não foi carregado? Ah tá… Você pode fazer isso utilizando o comando bu como mostra abaixo. Os breakpoints são setados quando o driver for carregado. Na linha seguinte, eu apenas listo os breakpoints, só pra… Quando o driver é carregado, a execução pára em meu breakpoint e uso a extensão !ustr, que mostra UNICODE_STRINGs, para ver o valor desse meu segundo parâmetro da DriverEntry.
kd> bu KernelReg!DriverEntry
kd> bl
0 eu 0001 (0001) (KernelReg!DriverEntry)
kd> g
KD: Accessing 'Z:\Sources\DriverEntry\KernelReg\objchk_w2k_x86\i386\KernelReg.sys'
(\??\C:\WINDOWS\system32\drivers\KernelReg.sys)
File size 2K.
MmLoadSystemImage: Pulled \??\C:\WINDOWS\system32\drivers\KernelReg.sys from kd
Breakpoint 0 hit
KernelReg!DriverEntry:
f8d394a0 8bff mov edi,edi
kd> !ustr poi(pusRegistryPath)
String(114,114) at 82302000: \REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\KernelReg
Mas não era pra ser CurrentControlSet? Na verdade o CurrentControlSet é um link para um outro ControlSet. No caso observado acima, o CurrentControlSet está refletindo o ControlSet001, ou seja, as alterações feitas no ControlSet001 são vistas no CurrentControlSet e vice-versa. O CurrentControlSet pode refletir qualquer outro ControlSet. Algumas regras determinam quando eles mudam, como em casos de mudanças de configurações de sistema ou instalações de drivers. Para saber para onde o CurrentControlSet está apontando, dê uma olhada no valor Current que está na chave “\Registry\Machine\System\Select” como mostra a figura abaixo.
Mas isso é apenas para efeito de curiosidade. Quando você utiliza CurrentControlSet como parte do caminho da chave que você deseja acessar, o Object Manager faz a traducão pra você e todos vivem felizes para sempre.
Cadê \Registry\Machine\Software?
Já pode ter acontecido com alguns de vocês. Vocês escrevem um driver que deve ler uma certa chave dentro de “\Registry\Machine\Software” quando o driver é carregado. Enquanto você faz testes com o driver, que neste momento tem seu Start configurado para Manual(3), tudo funciona que é uma maravilha, mas na hora de mudar o Start do driver para Boot(0) ou System(1), não conseguimos abrir a chave recebendo STATUS_OBJECT_NAME_NOT_FOUND (0xC00000034).
Como assim nome não encontrado? Estava aqui agora mesmo! O que acontece é que a chave SOFTWARE é montada mais tarde. Assim, durante o boot do sistema ela ainda não existe. Para fazer acesso a essa chave, você terá que esperar um cadim, talvez utilizando a tática deste post. Aí tudo funciona que é uma beless.
Algo a dizer sobre HKEY_CURRENT_USER?
Então, veja bem. Quem é o Current User? Usuário que está fazendo a chamada, certo? Assim, conforme a tradição, você começa pensando que é fácil e estando logado como Paulo, você inicia seu driver manualmente. O driver, durante a chamada à DriverEntry, lê configurações referentes ao usuário logado, que de fato estão armazenadas dentro de HKEY_CURRENT_USER do Paulo. Bom, acho que está certo até aqui. Já ouviram aquela expressão “Quem muito acha acaba se perdendo”? Pois é, tá tudo errado. Para Paulo, as configurações estão lá mesmo, mas a rotina DriverEntry é chamada em contexto de sistema, que por sinal não é Paulo.
Esse é um problema enfrentado mesmo em User Mode, quando serviços, que são executados em conta de sistema, tentam abrir a chave HKEY_CURRENT_USER. Uma coisa que temos que ter em mente é que “O usuário logado” não é uma informação única do sistema. Tentem imaginar um Terminal Service, podem haver vários usuários logados ao mesmo tempo. Mesmo quando há somente um usuário logado no sistema, threads podem ser executadas em contexto de outros usuários ou mesmo em contexto de sistema. Drivers não tem contexto próprio. Algumas partes do driver são executadas em contexto de sistema, outras em contexto arbitrário. Dependendo do tipo de driver e sua posiçao dentro da pilha de dispositivos, ele pode receber as chamadas em contexto do usuário. Que é o caso dos drivers de File System por exemplo.
Ah tá! Aí sim HKEY_CURRENT_USER vai funcionar! Er… Como posso dizer isso? Não, não vai funcionar. Segundo a referência da Microsoft, não existe uma tradução simples para HKEY_CURRENT_USER, mas existem rotinas que oferecem simplificações. Depois de toda essa conversinha sobre contexto e tals, você ainda terá que acessar a chave HKEY_USERS, ou melhor dizendo, “\Registry\User”, no formato já conhecido dos programadores User Mode utilizado com HKEY_USERS. Um exemplo é: “\Registry\User\S-1-5-21-73586283-1897051121-839522115-500”. O equivalente para uma conta de sistema é a chave “\Registry\User\.DEFAULT”.
Tá tá tá! Exemplo agora
Mais uma vez, estou assumindo que você já sabe compilar e instalar um driver como explicado neste post. O código exemplo disponível para download ao final deste post também pode ser compilado pelo Visual Studio utilizando o DDKBUILD, como explica este post. Neste exemplo vou mostrar a leitura de dois valores do registro que estão em uma chave como mostra a figura abaixo. Para deixar a brincadeira mais divertida, quando o driver for executado pela primeira vez, este criará a chave e os valores nela contidos quando perceber que estes ainda não existem.

Os valores estarão contidos em uma sub-chave da chave que recebemos como parâmetro na rotina DriverEntry. Então, nosso trabalho básico na codificação da DriverEntry é justamente criar o caminho completo da chave do registro que conterá estes valores. Com esta UNICODE_STRING em mãos, a passaremos para as rotinas de leitura e escrita no registro. Segue abaixo a codificação da rotina DriverEntry. Ela não mostra nada de especial com relação ao registro. Como sempre, vale a pena dar uma lida nos comentários.
/****
*** DriverEntry
**
** Ponto de entrada do nosso driver.
** Faca nos dentes e sangue nos olhos.
*/
extern "C"
NTSTATUS
DriverEntry(__in PDRIVER_OBJECT pDriverObject,
__in PUNICODE_STRING pusRegistryPath)
{
UNICODE_STRING usParameters = RTL_CONSTANT_STRING(L"\\Parameters");
UNICODE_STRING usFullRegPath = {0, 0, NULL};
PWCHAR pwzBuffer = NULL;
NTSTATUS nts = STATUS_SUCCESS;
//-f--> Aqui recebemos como parâmetro o caminho do registro até
// a chave do nosso driver. Nossos parâmetros estão em uma
// sub-chave chamada "Parameters". Vamos montar o caminho
// completo apendando o nome da sub-chave ao caminho do
// registro que recebemos como parâmetro.
__try
{
__try
{
//-f--> Seta a rotina de callback de descarga do driver.
pDriverObject->DriverUnload = OnDriverUnload;
//-f--> Para fazer esse append, precisaremos de um buffer
// que seja grande o bastante para armazenar a string
// original mais o tamanho da sub-chave. Aqui alocamos
// este buffer.
pwzBuffer = (PWCHAR)ExAllocatePoolWithTag(PagedPool,
pusRegistryPath->Length +
usParameters.Length,
_KRN_REG_TAG);
if (pwzBuffer == NULL)
ExRaiseStatus(STATUS_INSUFFICIENT_RESOURCES);
//-f--> Agora que temos o buffer, vamos inicializar a estrutura
// UNICODE_STRING referente a string resultante deste append.
RtlInitEmptyUnicodeString(&usFullRegPath,
pwzBuffer,
pusRegistryPath->Length +
usParameters.Length);
//-f--> Copia a string recebida como parâmetro
RtlCopyUnicodeString(&usFullRegPath,
pusRegistryPath);
//-f--> Concatena a string "\Parameters"
RtlAppendUnicodeStringToString(&usFullRegPath,
&usParameters);
//-f--> Utilizando o caminho completo, tenta carregar os parâmetros
nts = LoadParams(&usFullRegPath);
if (!NT_SUCCESS(nts))
{
//-f--> Em caso de falha, verifica se a causa foi a falta da
// sub-chave no registro. Isso deve acontecer quando o
// driver for executado pela primeira vez. Mas se a falha
// for outra, então cada um com seus pobrema.
if (nts != STATUS_OBJECT_NAME_NOT_FOUND)
ExRaiseStatus(nts);
//-f--> Vamos criar a sub-chave e gravar os valores
// pré-definidos.
nts = CreateParams(&usFullRegPath);
if (!NT_SUCCESS(nts))
ExRaiseStatus(nts);
//-f--> Tenta ler os parâmetros novamente.
// Tá, eu sei que isso é burrice. Afinal, se eu
// acabei de criar os parâmetros, por quê eu iria
// lê-los novamente? Bom, pra ilustrar.
nts = LoadParams(&usFullRegPath);
if (!NT_SUCCESS(nts))
ExRaiseStatus(nts);
}
}
__finally
{
//-f--> Limpando a bagunça.
if (pwzBuffer)
ExFreePool(pwzBuffer);
}
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
//-f--> Ops!
nts = GetExceptionCode();
ASSERT(FALSE);
}
return nts;
}
Aqui é mais brincadeira de UNICODE_STRING mesmo. Vamos dar uma olhada na rotina que grava os valores no registro. Esta rotina também não oferece nada de muito diferente do que já estamos habituados a ver em User Mode. Mas é como se diz por aí: “Um exemplo vale mais que mil artigos”. Nossa, essa foi terrível! Eu preciso parar com isso. Vocês devem estar pensando agora: “Caraca! Cada coisa que a gente precisa ler pra poder ter um exemplo de driver”.
/****
*** CreateParams
**
** Esta rotina é chamada quando não for detectada a chave
** que contém os parâmetros. Ela cria a chave e os parâmetros
** com valores pré-definidos.
*/
NTSTATUS
CreateParams(__in PUNICODE_STRING pusRegistryPath)
{
HANDLE hKey = NULL;
NTSTATUS nts = STATUS_SUCCESS;
OBJECT_ATTRIBUTES ObjAttributes;
UNICODE_STRING usStringParam = RTL_CONSTANT_STRING(L"String");
UNICODE_STRING usDWordParam = RTL_CONSTANT_STRING(L"DoubleWord");
ULONG ulParam = 0x12345678;
WCHAR wzParam[] = L"DriverEntry.com.br";
__try
{
__try
{
//-f--> Aqui não tem segredo. É tudo pá pum.
InitializeObjectAttributes(&ObjAttributes,
pusRegistryPath,
OBJ_CASE_INSENSITIVE,
NULL,
NULL);
//-f--> Cria a nova chave e já solicitamos
// acesso para setar valores dentro dela.
nts = ZwCreateKey(&hKey,
KEY_SET_VALUE,
&ObjAttributes,
0,
NULL,
REG_OPTION_NON_VOLATILE,
NULL);
if (!NT_SUCCESS(nts))
ExRaiseStatus(nts);
//-f--> Seta o valor numérico
nts = ZwSetValueKey(hKey,
&usDWordParam,
0,
REG_DWORD,
&ulParam,
sizeof(ulParam));
if (!NT_SUCCESS(nts))
ExRaiseStatus(nts);
//-f--> Seta o valor string
nts = ZwSetValueKey(hKey,
&usStringParam,
0,
REG_SZ,
&wzParam,
sizeof(wzParam));
if (!NT_SUCCESS(nts))
ExRaiseStatus(nts);
}
__finally
{
//-f--> Fecha o handle da nova chave criada
if (hKey)
ZwClose(hKey);
}
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
//-f--> Ops!
nts = GetExceptionCode();
ASSERT(FALSE);
}
return nts;
}
Por fim a parte mais divertida desta história. Creio que a maior parte da diversão está concentrada na rotina ZwQueryValueKey, que realiza a leitura de valores no registro.
NTSTATUS
ZwQueryValueKey(
IN HANDLE KeyHandle,
IN PUNICODE_STRING ValueName,
IN KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass,
OUT PVOID KeyValueInformation,
IN ULONG Length,
OUT PULONG ResultLength
);
O ponto que difere da API usada em User Mode para fazer a mesma tarefa é o terceiro parâmetro KEY_VALUE_INFORMATION_CLASS. Um enum que informa quais as informações gostaríamos de obter sobre determinado valor no registro. Um deles solicita apenas o nome enquanto outro solicita todas as informações e o último informações parciais referentes ao valor.
typedef enum _KEY_VALUE_INFORMATION_CLASS {
KeyValueBasicInformation,
KeyValueFullInformation,
KeyValuePartialInformation
} KEY_VALUE_INFORMATION_CLASS;
Para cada valor do enum, uma estrutura diferente é retornada. Sempre em um único bloco, a estrutura pode trazer várias informações utilizando Offsets de onde encontrar o dado dentro daquela única alocação de memória.
No exemplo, utilizei o KeyValuePartialInformation, que imagino ser o valor mais utilizado. Mais uma vez fica minha dica para que leiam os comentários trecho abaixo.
/****
*** LoadParams
**
** Esta rotina carrega as variáveis globais com os
** valores recuperados do registro.
*/
NTSTATUS
LoadParams(__in PUNICODE_STRING pusRegistryPath)
{
HANDLE hKey = NULL;
NTSTATUS nts = STATUS_SUCCESS;
OBJECT_ATTRIBUTES ObjAttributes;
UNICODE_STRING usStringParam = RTL_CONSTANT_STRING(L"String");
UNICODE_STRING usDWordParam = RTL_CONSTANT_STRING(L"DoubleWord");
ULONG ulBytes = 0;
PWCHAR pwzBuffer = NULL;
PKEY_VALUE_PARTIAL_INFORMATION pValueInfo = NULL;
__try
{
__try
{
//-f--> Monta o ObjectAttribute
InitializeObjectAttributes(&ObjAttributes,
pusRegistryPath,
OBJ_CASE_INSENSITIVE,
NULL,
NULL);
//-f--> Tenta abrir a chave do registro
nts = ZwOpenKey(&hKey,
GENERIC_READ,
&ObjAttributes);
if (!NT_SUCCESS(nts))
ExRaiseStatus(nts);
//-f--> Como o valor tem tamanho fixo, podemos determinar
// o tamanho do buffer necessário para ler o valor do
// registro.
ulBytes = sizeof(KEY_VALUE_PARTIAL_INFORMATION) +
sizeof(ULONG) - sizeof(UCHAR);
//-F--> Aloca o buffer para a leitura.
pValueInfo = (PKEY_VALUE_PARTIAL_INFORMATION)
ExAllocatePoolWithTag(PagedPool,
ulBytes,
_KRN_REG_TAG);
if (!pValueInfo)
ExRaiseStatus(STATUS_INSUFFICIENT_RESOURCES);
//-f--> Aqui fazemos a leitura.
nts = ZwQueryValueKey(hKey,
&usDWordParam,
KeyValuePartialInformation,
pValueInfo,
ulBytes,
&ulBytes);
if (!NT_SUCCESS(nts))
ExRaiseStatus(nts);
//-f--> Nos certificamos de que lemos um DWORD
ASSERT(pValueInfo->Type == REG_DWORD);
//-f--> Aqui nós carregamos nossa variável global
// com o valor lido do registro.
g_ulParam = *(PULONG)pValueInfo->Data;
//-f--> Libera buffer da leitura e zera ponteiro.
// Você pode decidir alocar o buffer de leituras
// baseado no maior valor que você precisa ler,
// e assim, utilizar o mesmo buffer para ler todos
// os valores menores. Aqui, mais uma vez estou
// refazendo tudo para demonstrar.
ExFreePool(pValueInfo);
pValueInfo = NULL;
//-f--> Como a string pode ter qualquer tamanho no registro,
// vamos oferecer zero bytes de buffer de leitura para
// que a API nos diga quanto ela precisa para todo o buffer.
nts = ZwQueryValueKey(hKey,
&usStringParam,
KeyValuePartialInformation,
NULL,
0,
&ulBytes);
//-f--> Temos que receber um destes erros.
ASSERT(nts == STATUS_BUFFER_OVERFLOW ||
nts == STATUS_BUFFER_TOO_SMALL);
//-f--> Aqui alocamos o um buffer do tamanho
// solicitado pela API.
pValueInfo = (PKEY_VALUE_PARTIAL_INFORMATION)
ExAllocatePoolWithTag(PagedPool,
ulBytes,
_KRN_REG_TAG);
if (!pValueInfo)
ExRaiseStatus(STATUS_INSUFFICIENT_RESOURCES);
//-f--> Agora faremos a leitura novamente, mas agora
// oferencendo um buffer de tamanho descente.
nts = ZwQueryValueKey(hKey,
&usStringParam,
KeyValuePartialInformation,
pValueInfo,
ulBytes,
&ulBytes);
if (!NT_SUCCESS(nts))
ExRaiseStatus(nts);
//-f--> Temos que ter lido uma string.
ASSERT(pValueInfo->Type == REG_SZ);
//-f--> Agora vamos alocar o buffer que será utilizado para manter
// a string lida do registro. Assim podemos descartar o buffer
// utilizado pela leitura. O campo DataLength traz o tamanho da
// string unicode com o terminador zero. Repare que estra estrutura
// não traz uma UNICODE_STRING, e sim um array de WCHAR.
pwzBuffer = (PWCHAR)ExAllocatePoolWithTag(PagedPool,
pValueInfo->DataLength,
_KRN_REG_TAG);
if (!pwzBuffer)
ExRaiseStatus(STATUS_INSUFFICIENT_RESOURCES);
//-f--> Depois de algum tempo programando, você fica ligêro com algumas
// coisas. Aposto que alguém, e isso inclui a mim mesmo, um dia vai
// dar um Copy-Paste desta função para usar em outro lugar. Aqui,
// prevendo que a leitura do registro pode acontecer várias vezes,
// e assim, se houver a alocação de uma leitura anterior, vamos
// desalocá-la.
if (g_usParam.Length)
RtlFreeUnicodeString(&g_usParam);
//-f--> Inicializa string global com o buffer que acabamos de alocar.
// Apesar de setarmos um buffer para esta UNICODE_STRING, este
// ainda está vazio (Length=0)
RtlInitEmptyUnicodeString(&g_usParam,
pwzBuffer,
(USHORT)pValueInfo->DataLength);
//-f--> Aqui apendamos o WCSTR (array de WCHAR terminado em zero), lido
// do registro em nosso buffer vazio. Isso é similar a uma cópia,
// mas com uma incrivel vantagem. A de não utilizar wcslen().
// Liga não, é paranóia de purista. Assim deixamos por conta da API
// determinar o Length e MaximumLegth da UNICODE_STRING. Lembre-se
// de que o terminador zero não é contado como parte da UNICODE_STRING.
RtlAppendUnicodeToString(&g_usParam,
(PCWSTR)pValueInfo->Data);
}
__finally
{
//-f--> Faxina geral
if (pValueInfo)
ExFreePool(pValueInfo);
if (hKey)
ZwClose(hKey);
}
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
//-f--> Ops! I did it again.
nts = GetExceptionCode();
ASSERT(FALSE);
}
return nts;
}
Ufa! Quanto código! Nesta primeira parte do assunto busquei utilizar as APIs que mais se parecem com as APIs de User Mode. No próximo post trarei uma maneira diferente de fazer a mesma coisa que fizemos aqui.
Espero ter ajudado,
Até mais…
KernelReg.zip