Hook de COM no WinDbg

Caloni, 2007-09-18 computer blog

Continuando com o tema hooks no WinDbg vamos aqui "hookear" e analisar as chamadas de métodos de um objeto COM. O que será feito aqui é o mesmo experimento feito para uma palestra de engenharia reversa que apresentei há um tempo atrás, mas com as opções de pause, rewind, replay e câmera lenta habilitadas.

Antes de começar, se você não sabe nada sobre COM, não deveria estar aqui, mas nunca é tarde para aprender. Pra começar, vamos dar uma olhada na representação da interface IUnknown em UML e em memória:

Como podemos ver, para implementar o polimorfismo os endereços das funções virtuais de uma classe são colocados em uma tabela, a chamada vtable, famosa tanto no COM quanto no C++. Existe uma tabela para cada classe-base polimórfica, e não para cada objeto. Se fosse para cada objeto não faria sentido deixar esses endereços "do lado de fora" do leiaute. E não seria nada simples e elegante fazer uma cópia desse objeto.

Assim, quando você chama uma função virtual de um objeto o código em assembly irá chamar o endereço que estiver na posição correspondente ao método chamado dentro da vtable. Se você chama AddRef, por exemplo, que é o segundo método na tabela, será chamado o endereço da posição número dois. Com isso, mesmo desconhecendo de que tipo é o objeto a função certa será chamada porque existe um ponteiro para essa tabela no início da interface.

Sabendo de tudo isso, agora sabemos como teoricamente proceder para colocar uns breakpoints nessas chamadas:

Note que o breakpoint não é colocado dentro da tabela, o que seria absurdo. Uma tabela são dados e dados geralmente não são executados (eu disse geralmente). Porém, usamos a tabela para saber onde está o começo da função para daí colocar a parada nesse endereço, que por fazer parte do código da função é (quem diria!) executado.

Agora vamos sair da teoria e tentar fazer as coisas mais ou menos parecidas na prática. O nosso sorteado desse artigo foi o IMalloc, a interface de alocação de memória do COM, que existe desde a época em que não se sabia direito pra que esse tal de COM iria servir. O IMalloc é definido como se segue:

   MIDL_INTERFACE("00000002-0000-0000-C000-000000000046")
   IMalloc : public IUnknown
   {
     public:
     virtual void *STDMETHODCALLTYPE Alloc( 
       /* [in] */ SIZE_T cb) = 0;
   
     virtual void *STDMETHODCALLTYPE Realloc( 
       /* [in] */ void *pv,
       /* [in] */ SIZE_T cb) = 0;
   
     virtual void STDMETHODCALLTYPE Free( 
       /* [in] */ void *pv) = 0;
   
     virtual SIZE_T STDMETHODCALLTYPE GetSize( 
       /* [in] */ void *pv) = 0;
   
     virtual int STDMETHODCALLTYPE DidAlloc( 
       void *pv) = 0;
   
     virtual void STDMETHODCALLTYPE HeapMinimize(void) = 0;
   };

Nesse experimento, como iremos interceptar quando alguém aloca ou desaloca memória, nossos alvos são os métodos Alloc e Free. Para saber onde eles estão na tabela, é só contar, começando pelos métodos do IUnknown, que é de quem o IMalloc deriva. Se houvessem mais derivações teríamos que contar da primeira interface até a última. Portanto: QueryInterface um, AddRef dois, Release três, Alloc quatro, Realloc cinco, Free seis. OK. Contar foi a parte mais fácil.

Agora iremos precisar interceptar primeiro a função que irá retornar essa interface, pois do contrário não saberemos onde fica a vtable. Nesse caso, a função é a ole32!CoGetMalloc. Muitas vezes você irá usar a ole32!CoCreateInstance(Ex) ou a CoGetClassObject diretamente na DLL que pretende interceptar. Outras vezes, você receberá o ponteiro em alguma ocasião diversa. O importante é conseguir o ponteiro de alguma forma.

Nesse exemplo iremos obter o ponteiro através de um aplicativo de teste trivial, ignorando todas aquelas proteções antidebugging que podem estar presentes no momento da reversa, feitos por alguém que lê meu blog (quanta pretensão!):

   /** @brief A stupid sample to 
     show WinDbg COM hooking! */
   #include <windows.h>
   #include <objbase.h>
   #include <objidl.h>
    
   int main()
   {
     CoInitialize(NULL);
     IMalloc* malloc = 0;
   
     if( CoGetMalloc(1, &malloc) == 0 )
     {
       if( void* pAlloc 
         = malloc->Alloc(0x1000) )
       {
         malloc->Free(pAlloc);
       }
   
       malloc->Release();
     }
   
     CoUninitialize();
   } 

Vamos fazer de conta que é desnecessário dizer como se compila o fonte acima.

   
   cl /c imalloc-hook.cpp
   link imalloc-hook.obj ole32.lib

Agora é só depurar!

Abra o WinDbg. Na opção "File, Open Executable" selecionamos a nossa vítima, cujo nome você escolhe na hora de compilar o fonte acima. Aqui ele irá chamar imalloc-hook.exe. A seguir, colocamos um breakpoint na função da ole32, mandamos rodar, e esperamos a parada do código:

   0:000> bp ole32!CoGetMalloc
   0:000> bl
   0 e 774ddcf8 0001 (0001) 0:**** ole32!CoGetMalloc
   0:000> g
   Breakpoint 0 hit
   ModLoad: 76360000 7637d000 C:\WINDOWS\system32\IMM32.DLL
   ...
   ModLoad: 746e0000 7472b000 C:\WINDOWS\system32\MSCTF.dll
   eax=0012ff7c ebx=00000000 ecx=775e67f0 edx=775e67f0 esi=00000001 edi=00403374
   eip=774ddcf8 esp=0012ff70 ebp=0012ffc0 iopl=0 nv up ei pl zr na pe nc
   cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
   ole32!CoGetMalloc:
   774ddcf8 8bff mov edi,edi

Maravilha. Alguém chamou a função que queríamos (quem será?). Agora podemos dar uma olhada na pilha e no protótipo da CoGetMalloc:

   
   HRESULT CoGetMalloc(DWORD dwMemContext, LPMALLOC *ppMalloc);
   0:000> dd esp L3
   0012ff70 0040101d 00000001 0012ff7c
   0:000> dd poi(esp+8) L1
   0012ff7c  00000000

Como podemos ver nos parâmetros da pilha o nosso chamador passou certinho o valor 1 no campo reservado e um ponteiro no segundo parâmetro para uma área onde, se der tudo certo, será escrito o endereço de um IMalloc, que podemos chamar carinhosamente de this. De início vemos que a variável está zerada. Agora vamos executar a função até a saída e examinar os resultados.

   
   0:000> bp /1 /c @$csp @$ra;g
   Breakpoint 1 hit
   eax=00000000 ebx=00000000 ecx=775e6034 edx=775e67f0 esi=00000001 edi=00403374
   eip=0040101d esp=0012ff7c ebp=0012ffc0 iopl=0         nv up ei pl zr na pe nc
   cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
   IMalloc+0x101d:
   0040101d 85c0         test    eax,eax
   0:000> dd 0012ff7c L1  ; o endereço da variável
   0012ff7c  775e6034     ; o endereço da interface
   0:000> dd 775e6034 L1  ; onde está a vtable?
   775e6034  775e600c     ; o endereço da vtable
   0:000> dd 775e600c
   775e600c  77562cfb 774dcf29 774dcf29 774dd00d ; a vtable ! ! !
   775e601c  774dd665 774dcfe8 774dd400 77562d46 ; a vtable ! ! !
   775e602c  77562d6e 775e6034 775e600c 774c0000 ; a vtable ! ! !
   775e603c  00000000 00000000 00154d70 774cbff4
   775e604c  00000000 00000000 00000000 00000000
   ...

E não é que tudo deu certo? A variável foi preenchida, e partir dela demos uma espiadela nos endereços das funções da vtable. Nós pegamos o valor da variável que foi preenchida (o endereço da interface) e obtemos os seus primeiros 4 bytes (o endereço da vtable) e listamos o seu conteúdo (a própria vtable!). Agora basta usarmos o resultados de nossas contagens lá em cima e colocarmos os breakpoints nas funções corretas. E mandar rodar. E analisar os resultados.

   0:000> bp 774dd00d ".echo IMalloc::Alloc"
   0:000> bp 774dcfe8 ".echo IMalloc::Free"
   0:000> g
   IMalloc::Alloc
   eax=775e6034 ebx=00000000 ecx=775e600c edx=774dd00d esi=00000001 edi=00403374
   eip=774dd00d esp=0012ff70 ebp=0012ffc0 iopl=0         nv up ei pl zr na pe nc
   cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
   ole32!IsValidIid+0xe4:
   774dd00d 8bff            mov     edi,edi
   0:000> dd esp L3
   0012ff70  00401031 775e6034 00001000 ; o this é nosso, e foi pedido para alocar 4KB (0x1000)
   0:000> bp /1 /c @$csp @$ra;g ; Step Out para pegar o retorno
   Breakpoint 3 hit
   eax=001597f0 ebx=00000000 ecx=7c9106eb edx=00150608 esi=00000001 edi=00403374
   eip=00401031 esp=0012ff7c ebp=0012ffc0 iopl=0         nv up ei pl nz na pe nc
   cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
   IMalloc+0x1031:
   00401031 85c0            test    eax,eax
   0:000> reax
   eax=001597f0 ; esse é o endereço da memória alocada
   g
   IMalloc::Free
   eax=774dcfe8 ebx=00000000 ecx=775e6034 edx=775e600c esi=00000001 edi=00403374
   eip=774dcfe8 esp=0012ff70 ebp=0012ffc0 iopl=0         nv up ei pl nz na pe nc
   cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
   ole32!IsValidIid+0xbf:
   774dcfe8 8bff            mov     edi,edi
   0:000> dd esp L3
   0012ff70  00401041 775e6034 001597f0 ; nosso this e endereço alocado (pedindo pra desalocar)
   g ; é isso aí

Note que a função pode eventualmente ser chamada internamente (pelo próprio objeto) ou até por outro objeto que não estamos interessados em interceptar (lembre-se que os métodos de uma classe são compartilhados por todos os objetos). Por isso é importante sempre dar uma olhada no primeiro parâmetro, que é o this que obtemos primeiramente.

Com isso termina o nosso pequeno experimento de como é possível interceptar chamadas COM simplesmente contando e usando o WinDbg. OK, talvez um pouquinho a mais, mas nada de quebrar a cabeça.

[why_is_my_dll_locked] [aquisicao_de_recurso_e_inicializacao]