Analisando Dumps com WinDbg e IDA
Caloni, 2008-01-10 computer blogApesar de ser recomendado que 100% dos componentes de um software esteja configurado corretamente para gerar símbolos na versão release, possibilitando assim a visualização do nome das funções internas através de um arquivo de dump (despejo) gerado na ocorrência de um crash, essa verdade só ocorre em 80% das vezes. Quis Murphy que dessa vez a única parte não "simbolizada" fosse a que gerou a tela azul em um Intel Quad Core que estou analisando esses dias.
Para incluir um programa novo em nosso leque de opções, vamos usar dessa vez uma ferramenta chamada IDA, um disassembler estático cujo nome é uma clara homenagem à nossa primeira programadora da história. E, é lógico, o WinDbg não poderá ficar de fora, já que ele será nosso analisador de dumps.
Tecnicamente falando, um dump nada mais é do que o conjunto de informações relevantes de um sistema em um determinado momento da execução, geralmente logo após um crash, onde tudo pára e morre. No caso do Windows, o crash é chamado de BSOD, Blue Screen Of Death, ou Tela Azul da Morte (bem macabro, não?). Do ponto de vista do usuário, é aquela simpática tela azul que aparece logo após o travamento da máquina.
Em algumas máquinas, essa tela nem mais é vista, pois o Windows XP é configurado automaticamente para exibir um simpático reboot que joga todos os seus dados não-salvos naquele momento para o limbo (ou, como diria meu amigo Thiago, para o "céu dos dados não-salvos antes de uma tela azul").
Dumps podem ser abertos por um depurador que entenda o tipo de dump gerado (Visual Studio, WinDbg, OllyDbg, IDA, sd, etc). Se estamos falando de aplicativos que travaram, o Visual Studio pode dar conta do recado. Se é realmente uma tela azul, o WinDbg é o mais indicado. Para abrir um dump no WinDbg, tudo que temos que fazer é usar o item do menu "File, Open Crash Dump" ou digitar direto da linha de comando: windbg -z meu-crash-dump-do-coracao.dmp.
Após alguns segundos, o WinDbg irá imprimir uma saída cheia de detalhes parecendo o terminal de um filme de raquerismo. Não se preocupe, com o tempo cada detalhe fará mais sentido (ou não). Geralmente a melhor idéia depois de abrir o dump é seguir o conselho do próprio WinDbg e usar o comando !analyze -v, e mais um monte de informações será plotada na tela. Se o arquivo aberto for um minidump ele irá conter apenas a pilha de chamada que causou a tela azul, o estados dos registradores e algumas informações sobre módulos carregados no kernel. A partir daí podemos extrair algumas informações úteis:
Microsoft (R) Windows Debugger Version 6.8.0004.0 X86
Copyright (c) Microsoft Corporation. All rights reserved.
Loading Dump File [C:\BlaBlaBla\caloni\My Documents\My Dumps\cagou-geral.dmp]
Mini Kernel Dump File: Only registers and stack trace are available
Symbol search path is: SRV*C:\Symbols*\\symbolserver\OSSYMBOLS*(...)
Executable search path is:
Windows XP Kernel Version 2600 (Service Pack 2) MP (4 procs) Free x86 compatible
Product: WinNt, suite: TerminalServer SingleUserTS
Built by: 2600.xpsp_sp2_rtm.040803-2158
Kernel base = 0x804d7000 PsLoadedModuleList = 0x8055c700
Debug session time: Sat Dec 29 07:47:12.734 2007 (GMT-3)
System Uptime: 0 days 0:15:13.728
Loading Kernel Symbols
.............
Loading User Symbols
Loading unloaded module list
.............
*******************************************************************************
* *
* Bugcheck Analysis *
* *
*******************************************************************************
Use !analyze -v to get detailed debugging information.
BugCheck C, {0, 0, 0, 0}
*** ERROR: Module load completed but symbols could not be loaded for mydriver.sys
Probably caused by : psseckbd.sys ( mydriver+199e )
Followup: MachineOwner
---------
Geralmente a melhor idéia agora é seguir o conselho do WinDbg e usar o comando !analyze -v.
0: kd> !analyze -v ERROR: FindPlugIns 8007007b ******************************************************************************* * * * Bugcheck Analysis * * * ******************************************************************************* MAXIMUM_WAIT_OBJECTS_EXCEEDED Arguments: Arg1: 00000000 Arg2: 00000000 Arg3: 00000000 Arg4: 00000000 Debugging Details: ------------------ CUSTOMER_CRASH_COUNT: 1 DEFAULT_BUCKET_ID: DRIVER_FAULT BUGCHECK_STR: 0xC PROCESS_NAME: System LAST_CONTROL_TRANSFER: from 804fa83f to 804f9c12 STACK_TEXT: bad17bc0 804fa83f 0000000c 00000004 883a8eac nt!KeBugCheck+0x14 bad17bfc bac9199e 00000004 e32e8928 00000000 nt!KeWaitForMultipleObjects+0x37 WARNING: Stack unwind information not available. Following frames may be wrong. bad17c50 bac914d7 88d851e8 00000000 883a8030 mydriver+0x199e bad17c7c bac94058 899c0f38 8058006a 899c0f38 mydriver+0x14d7 bad17c84 8058006a 899c0f38 8836b000 00000000 mydriver+0x4058 bad17d54 80580179 00000240 00000001 00000000 nt!IopLoadDriver+0x66c bad17d7c 80537757 00000240 00000000 89c3cda8 nt!IopLoadUnloadDriver+0x45 bad17dac 805ce794 b5de6cf4 00000000 00000000 nt!ExpWorkerThread+0xef bad17ddc 805450ce 80537668 00000001 00000000 nt!PspSystemThreadStartup+0x34 00000000 00000000 00000000 00000000 00000000 nt!KiThreadStartup+0x16 STACK_COMMAND: kb FOLLOWUP_IP: mydriver+199e bac9199e 8b44241c mov eax,dword ptr [esp+1Ch] SYMBOL_STACK_INDEX: 2 SYMBOL_NAME: mydriver+199e FOLLOWUP_NAME: MachineOwner MODULE_NAME: mydriver IMAGE_NAME: mydriver.sys DEBUG_FLR_IMAGE_TIMESTAMP: 42d2747c FAILURE_BUCKET_ID: 0xC_mydriver+199e BUCKET_ID: 0xC_mydriver+199e Followup: MachineOwner ---------
Esse é o resultado de um dos minidumps recebidos.
Um minidump contém apenas a pilha de chamada que causou a tela azul, o estados dos registradores e algumas informações sobre módulos carregados no kernel.
A partir daí podemos extrair algumas informações úteis, que eu sublinhei na saída do WinDbg. Na ordem de chegada:
Com isso em mãos, mesmo sem símbolos e nomes de funções no código, conseguiríamos achar o código responsável pelo BSOD. Porém, vamos imaginar por um momento que não foi tão fácil assim e fazer entrar em cena outra ferramenta indispensável nessas horas: o Interactive Disassembler.
No sítio do IDA podemos encontrar o download para uma versão gratuita do IDA, isso se usado com objetivos não-comerciais. Ou seja, para você que está lendo esse blogue por aprendizado, não terá nenhum problema você baixar essa versão e fazer alguns testes com seu driver favorito.
O funcionamento básico do IDA é bem básico, mesmo. Simplesmente escolhemos um executável para ele destrinchar e nos mostrar um assembly bem amigável, com todos os nomes de funções que ele puder deduzir. Como não temos os símbolos do próprio executável, as funções internas ganham "apelidos", como sub6669, loc13F35 e por aí vai. Isso não importa, já que temos nomes amigáveis de APIs para pesquisar no código-fonte e tentar encontrar as funções originais em C.
Pois bem. Como manda o figurino, o primeiro ponto do assembly que temos que procurar é o ponto em que uma função interna é chamada logo após IopLoadDriver, mydriver+0x4058. Por coincidência (ou não, já que essa é a função do IopLoadDriver), se trata da função inicial do executável, ou seja, provavelmente a função DriverEntry no código-fonte (obs: estamos analisando um driver feito para plataforma NT).
Como podemos ver pela imagem acima, o ponto de retorno é logo após uma chamada à função sub113F0, que não sei qual é. No entanto, o que eu sei é que logo no início é chamada a função IoIsWdmVersionAvailable, o que já nos permite fazer uma correlação com o código-fonte original. Após a chamada à IoIsWdmVersionAvailable, a próxima e última chamada de uma função é o que procuramos. Dessa forma, podemos ir caminhando até o ponto onde o driver chama o sistema operacional.
mydriver+0x14d7:
.text:000114B1 call ds:KeInitializeDpc
.text:000114B7 mov edx, dword_13000
...
.text:000114CA push 1
.text:000114CC call sub_11BE0
.text:000114D1 push eax
.text:000114D2 call sub_117D0.text:000114D7
add esi, 0D9Ch
.text:000114DD push esi
...
mydriver+0x199e:
.text:0001197C loc_1197C:
.text:00011994 push 0
...
.text:00011996 push ebx
.text:00011997 push edi
.text:00011998 call ds:KeWaitForMultipleObjects.text:0001199E
mov eax, [esp+30h+var_14]
.text:000119A2 mov edi, ds:ExFreePoolWithTag
...
.text:000119AD mov ecx, [esp+20h]
.text:000119B1 push 0
Por sorte o caminho não foi tão longo e cheguei rapidamente no ponto onde é chamada a função KeWaitForMultipleObject que, de acordo com o WinDbg e com a OSR, pode gerar uma tela azul se esperarmos por mais de três objetos e não especificarmos um buffer no parâmetro WaitBlockArray. Agora podemos olhar no fonte e ver por quantos objetos esperamos e tirar nossa própria conclusão do que está acontecendo.
//...
count = 0; // processors
mask = KeQueryActiveProcessors();
maskAux = mask;
while( maskAux )
{
if( maskAux & 1 )
count++;
maskAux >>= 1;
}
//...
KeWaitForMultipleObjects(count,
waitObjects,
WaitAll,
UserRequest,
KernelMode,
TRUE,
NULL,
NULL);
ExFreePool(...);
ExFreePool(...);
ExFreePool(...);
//...
Ora, ora. O número de processadores influencia no número de objetos que estaremos esperando na função de espera. Esse seria um bom motivo para gerar um MAXIMUMWAITOBJECTSEXCEEDED em máquinas onde existe mais de 3 processadores ativos, não? Talvez seja uma boa hora para atualizar esse código e torná-lo compatível com os novos Quad Core.
É importante, durantes os testes de desenvolvimento, sempre manter em dia uma versão debug (para o mundo kernel mode, versões checked) para que os primeiros problemas, geralmente os mais bestinhas, sejam pegos de forma rápida e eficiente. No entanto, um bom desenvolvedor não se limita a depurar com código-fonte. Ele deve estar sempre preparado para enfrentar problemas de falta da versão certa, informação pela metade, situação não-reproduzível. Para isso que servem as ferramentas maravilhosas que podemos usar no dia-a-dia. O IDA é mais uma das que deve estar sempre no cinto de utilidades do bom "debugador".