CLR via WinDBG - Introducción (importancia de las bases)

CLR via Windbg va a intentar ser una serie de posts donde veamos int.detacherioridades de la plataforma .Net a través de la herramienta de depuración WinDBG, nos conectaremos a procesos, veremos la relaciones entre clases del framework, utilización de memoria, Garbage colector, depuración de problemas típicos....

Antes de empezar la serie dedicada a .Net tenemos que hacer algo de trabajo previo para familiarizarnos con la estructura de un proceso Win32 y depuración básica con WindBG (no voy a entrar en threads, hilos...son lineas generales, para preguntas concretas utilizad los comentarios del blog), nada demasiado complicado, será como hacer un croquis de una habitación para saber cómo movernos por ella con los ojos cerrados ;) 

Aunque todavía no os haya dado info para instalar la herramienta, cargar procesos... iré poniendo salidas de diferentes comandos para que veáis algo de parte práctica.

So....let's kick it!

Al compilar estamos haciendo que nuestro código fuente se aloje en un archivo con una estructura preparada para cargarse en memoria ( Portable Executable ), ya sea para ejecutarse por si mismo o para ser un recurso para otro código. En esa estructura podemos encontrar información como: Dirección base donde se intentará cargar el módulo en la memoria, la Import Address Table, cabeceras con información relativa a la ejecución, variables globales, cabecera CLR en caso de ser un proceso .NET...

Cuando esa estructura se carga en memoria y se le asignan recursos para ejecutarse podemos hablar de que forma parte de un proceso. Con el depurador, podemos adjuntarnos a procesos y obtener cualquier información de estos, como las cabeceras, el código, la memoria virtual del proceso (hasta 2Gb si no utilizamos ningún flag /3gb), los diferentes threads con sus callstacks...

Una vez el proceso esta en ejecución, lo que nos interesa a nosotros es, o bien obtener un volcado de memoria con toda la información relativa al proceso (importante cuando hay un problema mientras el proceso se ejecuta y no podemos pararlo porque sigue dando un servicio) o bien nos adjuntamos a el y lo vamos depurando mientras se va ejecutando. En cualquier caso, para que un depurador se adjunte a un proceso (ya sea para volcar información a un archivo o para depurarlo en vivo), lo que pasa entre bastidores es que el proceso del depurador hace un createRemoteThread para poder trabajar en el proceso remoto y acceder a sus estructuras.

De modo que, si según nos adjuntamos a un proceso listamos todos sus threads, veremos que uno de ellos es un thread recién creado. (Tened en cuenta que lo que véis a continuación en una pila de llamadas, de modo que hay q leerlo de abajo a arriba)

ChildEBP RetAddr
0181fc48 7780f0a9 ntdll!DbgBreakPoint
0181fc78 76423833 ntdll!DbgUiRemoteBreakin+0x3c
0181fc84 777ba9bd kernel32!BaseThreadInitThunk+0xe
0181fcc4 00000000 ntdll! _RtlUserThreadStart+0x23

IMP --> Hace falta tener privilegios sobre el proceso al que nos vamos a adjuntar, de modo que o bien lo creamos nosotros (arrancamos el proceso desde el propio depurador), o bien somos administradores.

 

Batallita Las bases son muy importantes para saber por donde nos movemos, . Como no vamos a hacer un curso de arquitectura de computadores, ni de compiladores, las bases irán saliendo según vayamos avanzando. Intentar'e explicarlas para los que las tengan más oxidadas :)

Tuve un caso donde una sección crítica se quedaba huérfana y no había forma de saber porqué, una opción era poner un breakpoint en las funciones EnterCriticalSection y LeaveCriticalSection de ntdll para ver quien pedía/liberaba la sección crtítica . El problema es que si yo pongo un breakpoint en esas funciones....van a dispararse les llame el módulo que les llame!!! Lo que a mi me interesaba era que se disparasen sólo cuando llamase un módulo específico.

Ahí entraba en juego la IAT (import address table). Es una tabla de consulta para cuando un módulo quiere llamar a un API WIndows.Un programa compilado no sabe la situación de memoria en la que se encurta las librerías de las que depende, hace falta un salto intermedio. De modo que ahi tenemos la información que queremos...dónde va a ir mi módulo justo antes de llamar a las APIs que me interesan ;) Sólo falta saber la dirección para poner un breakpoint :)

Supongamos un módulo SAMPLE1...obtenemos su dirección base (!peb te da la del proceso, ahora no quiero el proceso, quiero un módulo concreto)

 

   0:004> lm    --> miro los módulos cargados en memoria
   start end module name
   2f220000 2f239000 SAMPLE1
   6d9b0000 6dc75000 ONINTL 
   6e2e0000 6e318000 rapistub

Con su dirección base, voy a volcar las cabeceras

   0:004> !dh 2f220000

   File Type: EXECUTABLE IMAGE
   FILE HEADER VALUES
   14C machine (i386)
   4 number of sections
   ...
   1000 [ 260] address [size] of Import Address Table Directory
   ...

Y ahora que se donde empieza (2f220000+1000) y lo que ocupa (260), vuelco esa sección de la memoria (cambio los nombres de las funciones para legibilidad...y pq no quiero q se vean los nombres reales ;) )

   0:004> dps 2f220000+1000 2f220000+1000+260
   2f221000 76427fae kernel32!FuncionA
   2f221004 76409145 kernel32!FuncionB
   2f221008 763e91ab kernel32!EnterCriticalSection
   2f22100c 76427b1c kernel32!LeaveCriticalSecion
   2f221010 76427a2c kernel32!FuncionE
   2f221014 763e1d27 user32!FuncionF
   2f221018 764245a7 user32!FuncionG
   .......

Lo que obtengo son las direcciones donde estan esas funciones, de modo que sé donde poner el breakpoint para trazar la información de la llamada, en SAMPLE1+1008 y SAMPLE1+100c ( estos bp's siempre hay que hacerlas relativas a la dirección base, no vaya a ser que haya una realocation)

 

Recursos para profundizar para el que quiera

- Especificación PE/COFF

- Artículo de formato de PE 

Próximo post de la serie CLR via WinDBG - Introducción (Símbolos)