logo
Contato | Sobre...        
rebarba rebarba

Rodrigo Strauss :: Blog

Add to Google

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


  
 
 
Comentários
Wanderley Caloni Jr | website | e-mail | em 06/05/2005 | #
Rodrigo Strauss | website | em 06/05/2005 | #
Algo a dizer?
Nome:


Site:


E-mail:


Escreva o número vinte e seis:


 Não mostre meu e-mail no site, não serve pra nada mesmo...

Comentário





Os comentários devem ser sobre assuntos relativos ao post, eu provavelmente apagarei comentários totalmente offtopic. Se quiser me enviar uma mensagem, use o formulário de contato. E não esqueça: isso é um site pessoal e eu me reservo o direito de apagar qualquer comentário ofensivo ou inapropriado.
rebarba rebarba
  ::::