PsSetCreateProcessNotifyRoutine no Windows XP

Mar 20, 2008  

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:

kd> d PspCreateProcessNotifyRoutineCount
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:

kd> u e1050fb7
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:

kd> u PsSetCreateProcessNotifyRoutine
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)
...
kd> u 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:

805c4746 53              push    ebx     <- NULL (805ce50a 33db  xor  ebx,ebx)
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?

kd> d e15713f8
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:

kd> d 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:

kd> d e15713ff-7+4
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:

kd> u f8ccc440
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:

0: kd> d PspCreateProcessNotifyRoutine
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.

0: kd> d PspCreateProcessNotifyRoutineCount
80562960  05 00 00 00 00 00 00 00-b8 7b ad 89 b8 aa ab 88  .........{......

O PspCreateProcessNotifyRoutineCount confirma. Vamos ver alguns:

0: kd> d e145172f-7+4
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)]
0: kd> d e15f6447-3
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. :)