O Windows e funciona usando as funções que estão nas dlls na pasta "system32".Dependendo de qual dll pertença a função,ela vai ter dois caminhos para seguir até chegar ao kernel,se for da dll Kernel32, ela ainda vai ter que passar por uma função na ntdll,se for da User32,ela ainda vai chamar uma função na Gdi32,
se for da Gdi32 ,ela não vai chamar outra função de nenhuma outra dll.
Tanto a gdi32 quanto a ntdll ainda ficam no espaço de endereço do usuário(0 à 0x80000000).Geralmente a ntdll é carregada no endereço 0x7C900000,e a gdi32 em 0x77E50000.
Suas funções chamam ainda uma outra função,"KiFastSystemCall(0x7FFE0300)" e esta finalmente usa uma instrução "sysenter" para entrar no kernel.
O que essa instrução faz é carregar os valores que estão guardados nos "MSRs", nescessários para que o processador funcione corretamente agora que vai entrar no kernel.
SYSENTER_CS_MSR - Pega esse valor e põe no registro de segmento "CS" ,agora o processador sabe da onde deve executar código.
SYSENTER_EIP_MSR - Pega esse valor e põe no registro "EIP" ,agora o processador sabe qual deve ser a próxima instrução a ser executada.
SYSENTER_ESP_MSR - Pega esse valor e põe no registro "ESP",agora o processador sabe que a stack que ele deve se basear é a do kernel.
Feito isso o processador alterna do privilégio 3 para o 0.Como o msr SYSENTER_EIP_MSR é quem tinha qual a próxima instrução à ser executada,logo ele é quem tinha o endereço de KiSystemService.
Ou seja,o código de KiSystemService é o primeiro código que é executado toda vez que há uma transição de modo de usuário para modo kernel,essa função fica no módulo "ntoskrnl.exe",que nada mais é do que o próprio kernel rodando.
Vamos ver o que acontece realmente usando as chamadas que a função "CloseHandle" na kernel32 faz como exemplo:
BOOL WINAPI PseudoCloseHandle( _In_ HANDLE hObject )
{
NtClose(hObject); }
NTSTATUS WINAPI NtClose(_In_ HANDLE Handle)
{_asm MOV EAX,0x19;
KiFastSystemCall();
}
void KiFastSystemCall()
{
_asm mov edx,esp;
_asm sysenter;
}
Comentários
Toda função que chama KiFastSystemCall,antes põe um valor no registro eax,esse valor vai servir como entrada numa tabela de funções no kernel chamada "KiServiceTable" para funções vindas da kernel32 ,ou "W32pServiceTable" para funções vindas da Gdi32.
NtClose passa o valor "0x19" para o registro eax,logo é possível assumir que na entrada 0x19,existe uma função que representa NtClose no kernel.
KiFastSystemCall guarda o valor do registro "esp"(que é o endereço da stack atual do programa em modo de usuario)em edx.
Antes de mostrar KiSystemService,é preciso dizer que funções vindas da kernel32 chamam funções implementadas no próprio módulo ntoskrnl(onde fica a KiSystemService)e funções vindas da gdi32 chamam funções implementadas no módulo win32k.sys(que é um outro módulo que fica no espaço de endereço do kernel também).
Também é preciso dizer que o kernel usa duas estruturas para alcançar essas tabelas,SystemServiceTable e ServiceDescriptorTable :
struct SystemServiceTable
{
//Se for do módulo ntoskrnl,então aponta para KiServiceTable, se for para a Win32k então aponta //para W32pServiceTable
PULONG_PTR ServiceTableBase;
PULONG ServiceCounterTableBase; //Contador de chamadas
ULONG NumberOfServices; //Número de funções na tabela ServiceTableBase
PUCHAR ParamTableBase; //Tabela com a quantidade de argumentos que cada função pega
}
struct ServiceDescriptorTable
{
SystemServiceTable ntoskrnl;
SystemServiceTable win32k;
}
Uma parte do que KiSystemService faz é :
void PseudoKiSystemService(_KTHREAD *ArgThread,
_KTRAP_FRAME *ArgTrapFrame,
ULONG Instruction )
{
KPCR *kpcraux = (KPCR_N_OPACA*)KIP0PCRADDRESS;
KTHREAD *kthread_global = kpcraux->Prcb->CurrentThread;
unsigned int *stack_usuario;
_asm mov stack_usuario,edx;
unsigned int _indexsystemcall;
_asm mov _indexsystemcall,eax; //EAX tem a entrada na tabela de funções
//Ve qual das SystemServiceDescriptorTable vai ser usada
_indexsystemcall = (_indexsystemcall>>8)& 0x30;
SystemServiceTable *ssdtapoio = (SystemServiceTable*)(
(unsigned int)kthread_global->ServiceTable + _indexsystemcall );
unsigned int eaxapoio;
_asm mov eaxapoio,eax;
eaxapoio &= 0xFFF;
if(eaxapoio <= ssdtapoio->NumberOfServices)
{
//Verifica se algum desenho na gdi batch deve ser despejado
if(_indexsystemcall == 0x10 && ((char*)kpcraux->NtTib.Self->GdiBatchCount) != 0)
{
KeGdiFlushUserBatch(eaxapoio,stack_usuario);
}
//Incrementa o contador de chamadas do sistema
*(unsigned int*)kpcraux->Prcb->KeSystemCalls) +=1;
//Pega o número de argumentos que a função recebe
unsigned char num_argapoio = ssdtapoio->ParamTableBase[eaxapoio];
//Pega o endereço da função requisitada
ULONG_PTR func_endapoio = ssdtapoio->ServiceTableBase[eaxapoio];
if(stack_usuario <= (unsigned int*)MmUserProbeAddress)
{
unsigned int *pstackkernel;
_asm mov pstackkernel,esp;
while(num_argapoio > 0)
{
*pstackkernel= *stack_usuario;
pstackkernel++;
stack_usuario++;
num_argapoio - = 1;
}
*func_endapoio; //Chama a função
}
}
Comentários
A maioria do código da função é fazendo checagens de segurança ,verificação de breakpoints para debuggers e salvando informações para a troca de contexto,então isso foi removido pra manter o foco no tópico
1 - EAX tem o valor de entrada para uma das tabelas de funções,veja que esse valor é usado para dois fins,um para determinar qual tabela de funções usar(se indexsystemcall for 0,então usa a KiServiceTable,se for 16 então usa a W32pServiceTable),veja que o membro da thread atual (estrutura KTHREAD),"ServiceTable",aponta para uma estrutura "SERVICE_DESCRIPTOR_TABLE",no qual tem membros do tipo "SystemServiceTable",que por sua vez tem em seu membro "ServiceTableBase",a tabela à ser usada.
2 - Depois é verificado se a index passada está dentro da quantidade de funções que essa tabela tem
3 - Depois do número de argumentos que a função recebe e o endereço da função serem pegos,ainda é verificado se o endereço da stack é menor que o endereço onde começa o kernel (MmUserProbeAddress) ,então é feito um loop que copia os argumentos da stack de modo usuário para a stack de modo kernel
4 - Agora a função é chamada