Rodrigo Strauss :: Blog
WinDbg Live Programming: Fazendo um log das chamadas à MessageBox
Nesse exemplo, nós faremos uma espécie de log de todas as chamadas para MessageBox que um processo fizer. Toda vez que um MessageBox for chamado, a string passada será passada também para OutputDebugString. Dessa forma, é possível usar o DebugView da Sysinternals para salvar isso em um arquivo (ou somente ver no WinDbg mesmo).
Para fazer isso, vamos criar uma "função" em tempo real (em assembly), e redirecionar o fluxo do programa para ela toda vez que o MessageBoxExW for chamado. No Windows NT (NT/2000/XP/2003), a função MessageBox nada mais faz do que chamar MessageBoxEx, que é quem realmente faz o trabalho. Além disso, quando chamamos MessageBoxA (quando você compila seu programa em ANSI, MessageBox é um define para MessageBoxA), ela só "traduz" sua string para UNICODE e repasa para versão W. Sendo assim, fazendo nosso hook em MessageBoxExW vamos pegar todas as variantes.
Chega de teoria e vamos à escovação de bits:
A primeira coisa que precisamos fazer é alocar memória para a nossa "função". Isso pode ser feito usando o comando ".dvalloc" do WinDbg, que aloca memória no espaço de endereçamento do processo. Vamos alocar 1kb de memória:
0:000> .dvalloc 1000 Allocated 1000 bytes starting at 00230000
Agora temos a nossa memória. Vamos salvar o endereço dela no pseudo-registrador $t0 do WinDbg. O WinDbg possui 20 pseudo-registradores ($t0 até $t19) para usos como esse. Para isso, usaremos o comando "r", que altera o valor de um registrador:
0:000> r $t0 = 0x00230000
Como as strings enviadas para o MessageBox não necessariamente são terminadas com CRLF, vamos colocar um na nossa memória para podermos dar uma quebra de linha na string que enviaremos ao OutputDebugString. Fazemos isso usando o comando "ezu" para gravar uma string UNICODE na nossa memória (cujo endereço está em $t0). Depois de fazer isso, vamos usar o comando "db" para ver a memória, pasando L0xF no final para dizer que só queremos ver 0xF bytes:
0:000> ezu $t0 "\r\n" 0:000> db $t0 L0xF 00230000 0d 00 0a 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
Agora que já temos nosso "ENTER" na memória, vamos à parte que interessa: escrever o código. Vamos escrever o código logo depois da string que acabamos de gravar. Como a string é UNICODE, seu tamanho em bytes é 6 (dois caracteres UNICODE mais o 00 00 no final). Vamos gravar o início do nosso código no pseudo registrador $t1 e escrevê-lo para chamar o OutputDebugString, passando o ponteiro da string que veio em EAX como parâmetro. Aqui vamos usar o comando "A" para começar a escrever o código assembly e depois usaremos o comando "U" para verificar se o disassembly ficou como queríamos:
0:000> r $t1 = $t0 + 0x6 0:000> a $t1 00230006 push @eax push @eax 00230007 call kernel32!OutputDebugStringW call kernel32!OutputDebugStringW 0023000c push 0x00230000 push 0x00230000 00230011 call kernel32!OutputDebugStringW call kernel32!OutputDebugStringW 00230016 int 3 int 3 00230017 0:000> u $t1 00230006 50 push eax 00230007 e8aaf54f79 call KERNEL32!OutputDebugStringW (7972f5b6) 0023000c 6800002300 push 0x230000 00230011 e8a0f54f79 call KERNEL32!OutputDebugStringW (7972f5b6) 00230016 cc int 3 00230017 0000 add [eax],al 00230019 0000 add [eax],al 0023001b 0000 add [eax],al
Duas coisas devem ser notadas no trecho acima. A primeira é que vamos chamar o OutputDebugString duas vezes, uma para string recebida em EAX e outra para o nosso CRLF. O ideal seria concatenar isso em um novo buffer, mas para uma aplicação single threaded isso funciona sem problemas. A segunda coisa é que coloquei um "int 3" no final do código. Isso é um breakpoint, e caso o processador passe do meu código (eu vou redirecionar isso antes com um BP), ele vai parar no breakpoint forçado e eu posso resolver o problema. O código depois do "int 3" deve ser ignorado, é o disassembly de "00 00 00 00".
Nosso próximo passo é fazer o WinDbg redicionar o fluxo do programa para a nossa função toda vez que o MessageBoxExW for chamado. Para isso vamos usar o suporte que o BP (comando para breakpoint) nos dá para executar comandos toda vez que um breakpoint for atingido. Vou mostrar o comando primeiro e explicar depois:
0:000> bp user32!MessageBoxExW "r $t2 = @eip ; r eax = poi(@esp+8) ; r eip = $t1 ; g"
Os comandos na string depois do BP serão executados assim que o breakpoint for atingido. Note que são vários comandos separados por ";". Vamos à explicação:
- r $t2 = @eip: Vamos salvar o ponteiro de instrução do processador (EIP) em $t2. Tem um "@" antes para dizer ao WinDbg para interpretar isso como um registrador, e não tentar procurar symbols para isso;
- r eax = poi(@esp+8): Esse é fácil :-) Vamos colocar o apontado do segundo parâmetro da pilha no registrador EAX. Quando chegamos numa função, o primero parâmetro está em ESP+0x4, o segundo em ESP+0x8, etc. Dessa forma estaremos colocando em EAX o ponteiro para a string que foi passada no segundo parâmetro da MessageBoxEx;
- r eip = $t1: Aqui vamos colocar o endereço da nossa "função" em EIP. Isso faz com que o processador comece a executar as instruções que estão na nossa "função".
- g: Hey, Ho, Let's Go!
Resumindo: Quando chegarmos em MessageBoxExW, vamos colocar a string em EAX, salvar a instrução atual em $t2 e mandar o processador para a nossa "função".
Agora que já fizemos o redirecionamento para nossa "função", precisamos fazer com que o processador volte para a MessageBoxExW depois de executar nosso código, fazendo com que o programa siga seu fluxo normalmente. Para isso, vamos usar a mesma técnica que usamos no primeiro breakpoint, modificar o EIP:
0:000> bp $t0 + 0x16 "r eip = $t2 ; g"
Se você chamar um "U $t0 + 0x16", vai ver que esse é o endereço do nosso "int 3", que vem logo depois do nosso código. Aqui nós estamos colocando um breakpoint que será atigindo logo depois da segunda chamada à OutputDebugString. A única coisa que temos que fazer nesse momento é restaurar o EIP usando o conteúdo antigo dele que salvamos em $t2 e deixar o programa seguir seu curso. Com isso teremos o nosso log.
Pronto, isso deve funcionar. Abra no WinDbg qualquer programa que chame um MessageBox, siga os passos acima, e veja as mensagens mostradas no prompt do WinDbg. Teste também a visualização com o DebugView.
Isso tudo funcionou porque OutputDebugString é __stdcall (ou seja, a função chamada que restaura a pilha). Se a função fosse __cdecl (quem chamou restaura a pilha), o nosso assembly ficaria um pouco maior. A única API Win32 que eu sei que é __cdecl é a wsprintf, porque ela recebe parâmetros dinamicamente.
Segue o log completo dos comandos usados para isso:
0:000> .dvalloc 1000 Allocated 1000 bytes starting at 00230000 0:000> r $t0 = 0x00230000 0:000> ezu $t0 "rn" 0:000> db $t0 L0xF 00230000 0d 00 0a 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 0:000> r $t1 = $t0 + 0x6 0:000> a $t1 00230006 push @eax push @eax 00230007 call kernel32!OutputDebugStringW call kernel32!OutputDebugStringW 0023000c push 0x00230000 push 0x00230000 00230011 call kernel32!OutputDebugStringW call kernel32!OutputDebugStringW 00230016 int 3 int 3 00230017 0:000> u $t1 00230006 50 push eax 00230007 e8aaf54f79 call KERNEL32!OutputDebugStringW (7972f5b6) 0023000c 6800002300 push 0x230000 00230011 e8a0f54f79 call KERNEL32!OutputDebugStringW (7972f5b6) 00230016 cc int 3 00230017 0000 add [eax],al 00230019 0000 add [eax],al 0023001b 0000 add [eax],al 0:000> db 0x230000 00230000 0d 00 0a 00 00 00 50 e8-aa f5 4f 79 68 00 00 23 ......P...Oyh..# 00230010 00 e8 a0 f5 4f 79 00 00-00 00 00 00 00 00 00 00 ....Oy.......... 00230020 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00230030 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00230040 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00230050 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00230060 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00230070 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 0:000> bp user32!MessageBoxExW "r $t2 = @eip ; r eax = poi(@esp+8) ; r eip = $t1 ; g" 0:000> bp $t0 + 0x16 "r eip = $t2 ; g"
E aqui vai o fonte do programa simples que eu fiz para testar:
#define WIN32_LEAN_AND_MEAN
#define UNICODE
#define _UNICODE
#include <windows.h>
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
wchar_t wzBuffer[128];
DWORD dwInterval = 1000, dwCount = 10;
for(DWORD a = 0 ; a < dwCount ; a++)
{
wsprintf(wzBuffer, L"mensagem %d", a);
MessageBox(NULL, wzBuffer, L"LOG", MB_OK);
Sleep(dwInterval);
}
return 0;
}
Para maiores informações veja o help do WinDbg (RTFM). Boa escovação de bits!
Em 06/05/2005 16:42, por Rodrigo Strauss





De fato é uma solução que pode ser empregada para hook de APIs em geral. Mas devido à complexidade do procedimento, o melhor é encapsular tudo isso numa macro esperta =).
Mas falando sobre assemblar em WinDbg, vc poderia explicar o que nós fazíamos para desabilitar a impressão de todas as linhas de logs dos drivers que depurávamos O=).