Enquanto estudava os “pedaços” em modo kernel dos sistemas de anti-hacking atuais (GG, XTrap e HShield), tive a curiosidade de saber o que queriam fazer registrando uma rotina de notificação de criação de processos. No caso do GG, talvez injetar o aquele DLL maligno em todos os processos, sejam inocentes ou não.
Para registrar um callback que será chamado para cada processo criado, se usa a API:
NTSTATUS PsSetCreateProcessNotifyRoutine( IN PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine, IN BOOLEAN Remove );
Seria interessante colocar um breakpoint nesta API, assim poderia-se obter o endereço da rotina de callback e o estudar mecanismo. Talvez, se evitássemos que a rotina fosse registrada, mas ainda sim retornando STATUS_SUCCESS, não teriamos o driver do sistema anti-hacking monitorando os novos processos! Vou dar uma olhada nisso com mais tempo depois…
Pode-se acessar o trecho de memória onde o Kernel guarda as rotinas atualmente registradas pelo endereço em PspCreateProcessNotifyRoutine, e a quantidade delas em PspCreateProcessNotifyRoutineCount. Vamos dar uma olhada o que estes ponteiros têm:
80559400 01 00 00 00 00 00 00 00-f8 e0 fb 81 38 42 f9 81 …………8B.. 80559410 00 2e 1e 82 18 2f 1e 82-e8 2c 1e 82 00 00 00 00 …../…,…… … kd> d PspCreateProcessNotifyRoutine 805593e0 b7 0f 05 e1 00 00 00 00-00 00 00 00 00 00 00 00 ……………. 805593f0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ……………. …
Nosso ponteiro que guarda a quantidade de rotinas atualmente registradas mostra 1 rotina. Realmente, podemos ver 1 endereço armazenado em PspCreateProcessNotifyRoutine. Este endereço deve ser o ponteiro para a rotina. Vamos testá-lo:
e1050fb7 f8 clc e1050fb8 0000 add byte ptr [eax],al e1050fba 0000 add byte ptr [eax],al e1050fbc 0d73125103 or eax,3511273h e1050fc1 0401 add al,1 e1050fc3 00434d add byte ptr [ebx+4Dh],al e1050fc6 4e dec esi e1050fc7 e201 loop e1050fca
Não aparenta ser uma entrada de alguma rotina. Ainda, o endereço está alocado na faixa 0xE1000000. Bem, segundo o layout de memória do Kernel do Windows, a faixa 0xE1000000 pertence a memória Pool Paginada (é Paged Pool, não devia ter “tentado” traduzir, eu sei…). É lá a grande “piscina” que a maioria dos componentes do Kernel tem seu heap. Isso nos dá uma certeza – pausa – que os ponteiros em PspCreateProcessNotifyRoutine apontam para dados/estruturas, que provavelmente vão conter o endereço real da rotina. Se dermos uma olhada na implementação da rotina:
nt!PsSetCreateProcessNotifyRoutine: 805c46d2 8bff mov edi,edi 805c46d4 55 push ebp 805c46d5 8bec mov ebp,esp 805c46d7 53 push ebx 805c46d8 33db xor ebx,ebx 805c46da 385d0c cmp byte ptr [ebp+0Ch],bl 805c46dd 56 push esi 805c46de 57 push edi 805c46df 7465 je nt!PsSetCreateProcessNotifyRoutine+0x74 (805c4746) …
nt!PsSetCreateProcessNotifyRoutine+0x74: 805c4746 53 push ebx 805c4747 ff7508 push dword ptr [ebp+8] 805c474a e8e1d30300 call nt!ExAllocateCallBack (80601b30) 805c474f 8bf0 mov esi,eax 805c4751 3bf3 cmp esi,ebx 805c4753 7507 jne nt!PsSetCreateProcessNotifyRoutine+0x8a (805c475c) 805c4755 b89a0000c0 mov eax,0C000009Ah 805c475a eb2a jmp nt!PsSetCreateProcessNotifyRoutine+0xb4 (805c4786)
Hmm… ExAllocateCallBack? Isso é uma pista. O MSDN não ajudou muito, não existe esta API. Não-documentado? Pelo nome, temos uma idéia sobre o que faz. Uma breve análise leva a:
805c4747 ff7508 push dword ptr [ebp+8] <- Rotina a ser registrada 805c474a e8e1d30300 call nt!ExAllocateCallBack (80601b30)
Necessita de 2 argumentos, sendo o primeiro, a rotina a ser registrada (ebp+8) e o segundo NULL (EBX foi zerado em 805CE50A). Vamos carregar algum driver que chame a rotina PsSetCreateProcessNotifyRoutine, para que possamos executar passo-a-passo e ver o resultado da função ExAllocateCallBack. Retornou 0xe15713f8. Se olharmos dentro desta rotina (não vou colar aqui, não interessa muito), vamos ver que é alocado um bloco de memória no pool com o tag “Cbrb”. Então, é preenchido com os 2 argumentos. O bloco alocado tem 12 bytes de tamanho. Porém, o primeiro DWORD é colocado como 0 pelo ExAllocateCallback. Algo como: struct { DWORD Zero; PVOID Rotina; DWORD Desconhecido; } CALLBACK; O que há no callback alocado então?
e15713f8 00 00 00 00 40 c4 cc f8-00 00 00 00 0d 73 12 51 ….@4…….s.Q
Opa. Realmente, o segundo DWORD é o endereço de uma rotina que foi solicitado o registro (é proveniente de um pequeno driver de teste que escrevi para este propósito).
Agora, temos o conhecimento suficiente para conseguir o endereço real das rotinas, a partir dos ponteiros na lista de rotinas PspCreateProcessNotifyRoutine. Não, o Kernel não fará mais nada de estranho, já chequei (tirando o detalhe que virá logo em seguida). O resto da função checará se há espaço para mais uma rotina, caso sim, acrescentá-la na lista e incrementar o contador.
De volta ao PspCreateProcessNotifyRoutine:
805593e0 ff 13 57 e1 00 00 00 00-00 00 00 00 00 00 00 00 ..W……..
Hmm… nosso Callback não estava em 0xe15713f8? O endereço na lista está 7 bytes a frente. Então, para obtermos o segundo membro do Callback (endereço da rotina real), teremos que fazer [PspCreateProcessNotifyRoutine[i] – 7 + 4]. Voltamos para o início da estrutura (-7) e pegamos o segundo membro (+4). Veremos:
e15713fc 40 c4 cc f8 00 00 00 00-36 00 00 00 03 02 0f 0c @…….6…….
Hmm! Se não é nossa rotina real 😉 0xf8ccc440:
CREATE_PROC!NotifyRoutine [d:\driverdev\createproc\crtprc.c @ 13]: f8ccc440 8bff mov edi,edi f8ccc442 55 push ebp f8ccc443 8bec mov ebp,esp f8ccc445 0fb64510 movzx eax,byte ptr [ebp+10h] f8ccc449 f7d8 neg eax f8ccc44b 1bc0 sbb eax,eax f8ccc44d 83e00b and eax,0Bh f8ccc450 83c04e add eax,4Eh
Ótimo! Vamos testar em um sistema com mais rotinas registradas:
80562940 2f 17 45 e1 6f 64 79 e1-bf 48 79 e1 e7 67 5f e1 /.E.ody..Hy..g_. 80562950 47 64 5f e1 00 00 00 00-00 00 00 00 00 00 00 00 Gd_………….
Vemos que temos 5 Callback alocados.
80562960 05 00 00 00 00 00 00 00-b8 7b ad 89 b8 aa ab 88 ………{……
O PspCreateProcessNotifyRoutineCount confirma. Vamos ver alguns:
e145172c 1c 1e 6d ba 00 00 00 00-0e 00 4c 45 03 06 02 00 ..m…….LE…. 0: kd> u ba6d1e1c spdi+0x28e1c: ba6d1e1c 55 push ebp ba6d1e1d 8bec mov ebp,esp ba6d1e1f 807d1000 cmp byte ptr [ebp+10h],0 ba6d1e23 0f85ed000000 jne spdi+0x28f16 (ba6d1f16) ba6d1e29 83651000 and dword ptr [ebp+10h],0 ba6d1e2d 56 push esi ba6d1e2e 57 push edi ba6d1e2f ff15d4c06eba call dword ptr [spdi+0x430d4 (ba6ec0d4)]
e15f6444 20 a1 e1 a7 00 00 00 00-f0 1c 78 e1 03 08 03 00 ………x….. 0: kd> u a7e1a120 IsDrv122+0x9120: a7e1a120 55 push ebp a7e1a121 8bec mov ebp,esp a7e1a123 51 push ecx a7e1a124 51 push ecx a7e1a125 807d1000 cmp byte ptr [ebp+10h],0 a7e1a129 53 push ebx a7e1a12a 56 push esi a7e1a12b 57 push edi
Analisando o PsSetCreateProcessNotifyRoutine no Windows 2000, percebe-se que ele utiliza uma maneira muito mais simples: é armazenado os endereços reais na lista PspCreateProcessNotifyRoutine. Não existem estruturas Callbacks para isso. Apenas:
const DWORD MAX_ROUTINES = 8; PVOID RegisteredNotifyRoutines[MAX_ROUTINES];
Do Vista em diante, este limite de rotinas foi aumentado para 64.
Estudos aplicados estão por vir.