Escrevendo Aplicações Nativas para Windows - Parte 3
Por Davi Chaves
O objetivo desse artigo vai ser enumerar todos os processos e, em seguida enumerar todos as threads e handles de um processo específico.
NtQuerySystemInformation
Essa função será usada para pegarmos quase todo tipo de informação do sistema. O primeiro parametro dela é um valor de um enum SYSTEM_INFORMATION_CLASS
que vai definir que tipo de informação ela vai trazer. Abaixo a estrutura desse enum. Primeiramente, estaremos utilizando o valor SystemProcessInformation
para obtermos uma lista de todos os processos.
typedef enum _SYSTEM_INFORMATION_CLASS
{
SystemBasicInformation,
SystemProcessorInformation,
SystemPerformanceInformation,
SystemTimeOfDayInformation,
SystemPathInformation,
SystemProcessInformation,
SystemCallCountInformation,
SystemDeviceInformation,
SystemProcessorPerformanceInformation,
SystemFlagsInformation,
SystemCallTimeInformation,
SystemModuleInformation,
...
SystemWorkloadAllowedCpuSetsInformation,
SystemCodeIntegrityUnlockModeInformation,
SystemLeapSecondInformation,
SystemFlags2Information,
SystemSecurityModelInformation,
SystemCodeIntegritySyntheticCacheInformation,
SystemFeatureConfigurationInformation,
SystemFeatureConfigurationSectionInformation,
SystemFeatureUsageSubscriptionInformation,
SystemSecureSpeculationControlInformation,
MaxSystemInfoClass
} SYSTEM_INFORMATION_CLASS;
SystemProcessInformation
Para alguns valores do enum SYSTEM_INFORMATION_CLASS
é esperado um buffer de tamanho fixo, já para outros é esperado um buffer de tamanho variável. Para esse último caso, o comum é chamarmos a função passando um tamanho qualquer e checarmos pelo retorno da função. Caso o retorno seja STATUS_INFO_LENGTH_MISMATCH
, alocamos um buffer maior e chamamos a função novamente até encontrarmos o tamanho correto. Entretando, existe uma outra forma mais simples, porém errada, de fazermos esse processo. Basta chamarmos a função passando um tamanho zero e passarmos a referência de alguma variável no parâmetro ReturnLenght
. Dessa forma, o valor real necessário para alocar o buffer será retornado nela. Pelos testes que eu fiz, isso não vai funcionar sempre, apenas para alguns valores do enum SYSTEM_INFORMATION_CLASS
. Portanto, use apenas para testes.
NTSTATUS status = 0;
PVOID pbuffer = nullptr;
ULONG size = 0;
// Query Process Information
status = NtQuerySystemInformation(SystemProcessInformation, pbuffer, 0, &size);
printf("Bytes: %u\n", size);
// Reserves memory
status = NtAllocateVirtualMemory(NtCurrentProcess(), &pbuffer, 0, (PSIZE_T)&size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!NT_SUCCESS(status)) {
printf("Error reserving memory (status: 0x%X)\n", status);
return status;
}
status = NtQuerySystemInformation(SystemProcessInformation, pbuffer, size, nullptr);
if (!NT_SUCCESS(status)) {
printf("Error SystemProcessInformation (status: 0x%X)\n", status);
return status;
}
SYSTEM_PROCESS_INFORMATION *p = (SYSTEM_PROCESS_INFORMATION*)pbuffer;
Perceba que a primeira chamada do NtQuerySystemInformation
está sendo passado um tamanho zero. Dessa forma, a função vai retornar um erro como valor do status e vai assinalar o valor da variável size com o valor necessário para alocar a estrutura. Com esse valor em mãos, podemos alocar memória suficiente e chamar novamente a função. Ao final, temos um buffer contendo informações de (quase) todos os processos. Em seguida, vamos iterar por essa lista printando os valores relevantes para nós.
while(true) {
printf("PID: %6u PPID: %6u T: %4u H: %6u Name: %wZ\n", HandleToULong(p->UniqueProcessId), HandleToULong(p->InheritedFromUniqueProcessId),p->NumberOfThreads, p->HandleCount, &p->ImageName);
if (p->NextEntryOffset == 0)
break;
p = (SYSTEM_PROCESS_INFORMATION*)((PBYTE)p + p->NextEntryOffset);
}
Com isso, temos um código que itera por (quase) todos os processos. Podemos transformar isso em uma função que retorna apenas as informações de um processo específico. Basta passar um pid (process id) desejado e compararmos dentro do loop.
if (pid == HandleToULong(p->UniqueProcessId)) {
// printf("... process information here ...");
// EnumThreads(p);
break;
}
Thread Enumeration
O processo de enumeração de threads em um processo é bem simples. Em cada objeto do tipo SYSTEM_PROCESS_INFORMATION
temos um campo contendo o número de threads e o primeiro elemento de um array de SYSTEM_THREAD_INFORMATION
, que contem as informações que queremos.
typedef struct _SYSTEM_PROCESS_INFORMATION
{
ULONG NextEntryOffset;
ULONG NumberOfThreads; // Número de threads
// ...
SYSTEM_THREAD_INFORMATION Threads[1]; // Primeiro elemento do array
} SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;
typedef struct _SYSTEM_THREAD_INFORMATION
{
LARGE_INTEGER KernelTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER CreateTime;
ULONG WaitTime;
PVOID StartAddress;
CLIENT_ID ClientId;
KPRIORITY Priority;
LONG BasePriority;
ULONG ContextSwitches;
KTHREAD_STATE ThreadState;
KWAIT_REASON WaitReason;
} SYSTEM_THREAD_INFORMATION, *PSYSTEM_THREAD_INFORMATION;
Dessa forma, só precisamos iterar por esse array printando os campos que estamos interessados. Além disso, note que o campo ThreadState
é do tipo KTHREAD_STATE
, que se trata de um enum com todos os estados possíveis de uma thread. É possível fazer uma função super simples que transforma esse valor em uma string para ajudar na leitura.
void EnumThreads(SYSTEM_PROCESS_INFORMATION* p) {
SYSTEM_THREAD_INFORMATION* t = p->Threads;
for (ULONG i = 0; i < p->NumberOfThreads; i++) {
printf("TID: %6u Pri: %2u Address: 0x%p State: %s\n",
HandleToULong(t->ClientId.UniqueThread),
t->Priority,
t->StartAddress,
// Função de mapeamento de estado para string
ThreadStateToString(t->ThreadState));
t++;
}
}
PID: 4144 PPID: 2900 T: 7 H: 649 Name: procexp64.exe
TID: 9616 Pri: 15 Address: 0x0000000000000000 State: Waiting
TID: 8932 Pri: 13 Address: 0x00007FFA93BEA2D0 State: Waiting
TID: 4408 Pri: 13 Address: 0x00007FFA93BEA2D0 State: Waiting
TID: 5640 Pri: 13 Address: 0x00007FFA93BEA2D0 State: Waiting
TID: 6260 Pri: 15 Address: 0x00007FFA93BEA2D0 State: Waiting
TID: 9988 Pri: 13 Address: 0x00007FFA93BEA2D0 State: Waiting
TID: 8216 Pri: 15 Address: 0x00007FFA93BEA2D0 State: Waiting
SystemExtendedHandleInformation
O método para obter a lista de handles em um sistema é o mesmo método de obter a lista de handles de processos. Entretando, por algum motivo, o ReturnLenght
não retorna o real valor necessário para alocar essa estrutura. Portanto, por simplicidade, irei apenas alocar um grande número de bytes e esperar que seja o suficiente.
NTSTATUS EnumHandles(DWORD pid) {
NTSTATUS status = 0;
PVOID pbuffer = nullptr;
ULONG size = 1 << 24; // 8 MB
// Reserves memory
status = NtAllocateVirtualMemory(NtCurrentProcess(), &pbuffer, 0, (PSIZE_T)&size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// ... check error
status = NtQuerySystemInformation(SystemExtendedHandleInformation, pbuffer, size, nullptr);
// ... check error
SYSTEM_HANDLE_INFORMATION_EX* p = (SYSTEM_HANDLE_INFORMATION_EX*)pbuffer;
for (ULONG_PTR i = 0; i < p->NumberOfHandles; i++) {
SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX& h = p->Handles[i];
if (pid && h.UniqueProcessId != pid)
continue;
printf("PID: %6u H: 0x%X Access: 0x%08X Address: 0x%p\n",
(ULONG)h.UniqueProcessId, (ULONG)h.HandleValue, h.GrantedAccess, h.Object);
}
}
Abaixo um pedaço da saída do programa. Recomendo comparar esses valores com os valores mostrados pelo procexp.exe.
PID: 4144 H: 0x4 Access: 0x001F0003 Address: 0xFFFFBD8F66B810E0
PID: 4144 H: 0x8 Access: 0x001F0003 Address: 0xFFFFBD8F66B82DE0
PID: 4144 H: 0xC Access: 0x00000001 Address: 0xFFFFBD8F668B3180
PID: 4144 H: 0x10 Access: 0x001F0003 Address: 0xFFFFBD8F69295500
PID: 4144 H: 0x14 Access: 0x000F00FF Address: 0xFFFFBD8F6C5772C0
PID: 4144 H: 0x18 Access: 0x00100002 Address: 0xFFFFBD8F60274820
PID: 4144 H: 0x1C Access: 0x00000001 Address: 0xFFFFBD8F668B46A0
PID: 4144 H: 0x20 Access: 0x00100002 Address: 0xFFFFBD8F60274930
PID: 4144 H: 0x24 Access: 0x00000001 Address: 0xFFFFBD8F668B34C0
PID: 4144 H: 0x28 Access: 0x00000804 Address: 0xFFFFBD8F69294EF0
PID: 4144 H: 0x2C Access: 0x00000804 Address: 0xFFFFBD8F692949B0
PID: 4144 H: 0x30 Access: 0x00000804 Address: 0xFFFFBD8F692956D0
PID: 4144 H: 0x34 Access: 0x00000003 Address: 0xFFFFA60BF573ACE0
PID: 4144 H: 0x38 Access: 0x001F0003 Address: 0xFFFFBD8F66B82F60
PID: 4144 H: 0x3C Access: 0x001F0003 Address: 0xFFFFBD8F66B82360
...
Conclusão
Utilizando a função NtQuerySystemInformation
, podemos obter uma grande variedade de informações do sistema de um modo bastante simples. No próximo artigo vamos utilizar essas funções para realizar uma dll injection de forma nativa.