lunes, 17 de enero de 2011

Un paseo por la memoria

Resulta obvio que la frecuencia de publicación ha bajado, por no decir cesado completamente, de unos meses hasta ahora; y es que mucho ha cambiado mi vida en este tiempo. El nacimiendo de mi hija Claudia, primer y principal condicionante, siendo el segundo haber superado mi obsesión maniaco/compulsiva por tener que publicar algo de forma periódica en el blog, perdiendo totalmente de vista el motivo que me llevó a crearlo: disfrutar aprendiendo y contando lo que aprendo.

Sirva esto y mi promesa como declaración de intenciones para decir que este proyecto no está ni mucho menos muerto y que, en la medida de lo posible, seguiré publicando.

Lo suyo hubiera sido preceder lo que voy a contar con un post teórico sobre las interioridades de la gestión de la memoria en sistemas Windows, las implicaciones de la memoria virtual y el sistema de paginación, etc, etc. Pero me temo que tendrá que ser al revés porque me ha costado mucho comprender un poco lo que voy a contar y si me lo hubieran tenido que enseñar a mí, me hubiera gustado que así fuera.

Lo que vamos a ver es como analizar de forma manual un volcado de memoria de un sistema Windows creado mediante win32dd para obtener, de forma relativamente sencilla, los mismos resultados que obtuvimos en Usando la estructura KPCR en nuestro beneficio. Y para ello lo primero que haremos será convertir el volcado raw a formato Microsoft Crash Dump utilizando otro de los binarios contenidos en el paquete Moonsools Windows Memory Toolkit Community Edition de Matthieu Suiche:

D:\_memory\tools>bin2dmp.exe volcado.dd volcado.dmp

bin2dmp - 1.0.20100405 - (Community Edition)
Convert raw memory dump images into Microsoft crash dump files.
Copyright (C) 2007 - 2010, Matthieu Suiche
Copyright (C) 2009 - 2010, MoonSols

Initializing memory descriptors... Done.
Looking for kernel variables... Done.
Loading file... Done.

Rewritting CONTEXT for Windbg...
-> Context->SegCs at physical address 0x00000000075165AC is already equal to 08
-> Context->SegDs at physical address 0x0000000007516588 is already equal to 23
-> Context->SegEs at physical address 0x0000000007516584 is already equal to 23
-> Context->SegFs at physical address 0x0000000007516580 is already equal to 30
-> Context->SegGs at physical address 0x000000000751657C is already equal to 00
-> Context->SegSs at physical address 0x00000000075165B8 is already equal to 10

[0x000000007FFAF000 of 0x000000007FFAF000]

Total time for the conversion: 2 minutes 11 seconds.

Los ficheros de volcado de memoria de Microsoft tienen una cabecera de 4096 bytes que sintetiza los datos más relevantes para facilitar el análisis mediante el debugger, y también para ayudarnos a nosotros:
D:\_memory\tools>dmp_header.py volcado.dmp
typedef struct _DUMP_HEADER32 {
+0x000 Signature : PAGE
+0x004 ValidDump : DUMP
+0x008 MajorVersion : 15
+0x00c MinorVersion : 6002
+0x010 DirectoryTableBase : 0x00122000
+0x014 PfnDataBase : 0x81d84850
+0x018 PsLoadedModuleList : 0x81d64c70
+0x01c PsActiveProcessHead : 0x81d5a990
+0x020 MachineImageType : 0x0000014c
+0x024 NumberOfProcessors : 2
+0x028 BugCheckCode : 0x4d415454
+0x02c BugCheckParameter1 : 0x00000001
+0x030 BugCheckParameter2 : 0x00000002
+0x034 BugCheckParameter3 : 0x00000003
+0x038 BugCheckParameter4 : 0x00000004
+0x05c PaeEnabled : 1
+0x05d KdSecondaryVersion : 65
+0x060 KdDebuggerDataBlock : 0x81d44c98
+0x064 PhysicalMemoryBlock
NumberOfRuns : 1
NumberOfPages : 0x0007ffaf
Run[1].BasePage : 0x00000000
Run[1].PageCount : 0x0007ffaf
+0x7d0 Exception
ExceptionCode : 0x80000003
ExceptionFlags : 1
ExceptionRecord : 0
ExceptionAddress : 0xdeadbabe
NumberParameters : 0
+0xf88 DumpType : FULL
+0xf8c MiniDumpFields : 1162297680
+0xf90 SecondaryDataState : 1162297680
+0xf94 ProductType : 1162297680
+0xf98 SuiteMask : 1162297680
+0xf9c WriterStatus : 1162297680
+0xfa0 RequiredDumpSpace : 2147155968 bytes
+0xfb8 SystemUpTime : 4992030524978970960
+0xfc0 SystemTime : 'Sat Jan 15 10:29:25.286 2011 (UTC)'
} DUMP_HEADER32, *PDUMP_HEADER32;

El resultado anterior lo he obtenido utilizando un script de fabricación propia, tan feo e incompleto que me dá verguenza hasta publicarlo, pero como siempre hasta ahora lo he hecho, pues esta vez no iba a ser menos. Está "desarrollado" con Python utilizando la librería ctypes, de la cual tuve noticias leyendo un libro más que recomendable, Gray Hat Python.

[TO DO] Crear de una vez por todas una sección en el blog con lecturas que considero interesantes para agrupar las que voy mostrando en "LIBROS QUE LEO" y tratar de incluir mis impresiones a modo de reviews.[/TO DO]

La variable KdDebuggerDataBlock contiene un puntero a la dirección donde se almacena la estructura _KDDEBUGGER_DATA64 en el volcado de memoria, así que veamos que hay allí:
D:\_memory\tools>DumpHex.exe /s81d44c98 /L128 volcado.dd
DumpHex Version 1.0.1
Copyright (c) 2003 Robert Bachmann

81D44C98h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
81D44CA8h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
81D44CB8h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
81D44CC8h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
81D44CD8h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
81D44CE8h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
81D44CF8h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
81D44D08h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

Un par observaciones:
  • Utilizamos ahora el volcado en formato raw pero hubiéramos podido utilizar de igual forma el Microsoft crash dump sólo con una salvedad: si tenemos en cuenta que el fichero contiene una cabecera de 4096 bytes con información para el debugger tendremos que sumarle 1000 a cada dirección en formato hexadecimal para obtener lo que buscamos.
  • Para conseguir el resultado anterior he utilizado una herramienta de línea de comandos no incluida en la instalación de Windows Vista, DumpHex. No obstante si alguno de vosotros sabéis la forma de hacerlo utilizando alguna herramienta integrada me encantaría conocerla.

Repasando el resultado obtenido salta a la vista que no hemos encontrado lo que íbamos buscando pero, ¿por qué?

Direcciones virtuales vs Direcciones físicas

Sin meterme demasiado en profundidad, aunque prometo hacerlo cuando asiente conceptos y me sienta realmente preparado para ello, baste decir que la memoria virtual y la paginación son trucos utilizados por el sistema operativo para engañar a los procesos, de forma que piensen que cuentan con mayor cantidad de memoria de la que realmente disponen. Y eso es lo que la variable KdDebuggerDataBlock contiene, una dirección virtual.

Resumiendo, tenemos que "traducir" una dirección virtual en una dirección física y ese es el proceso que vamos a seguir. Para conseguirlo tendremos que pasar por varios niveles de indirección que están perfectamente resumidos en la siguiente imagen del libro Windows Internals, Fifth Edition:


En este caso se trata de un sistema x86 de 32 bits ejecutando el kernel PAE (ntkrnlpa.exe), y es que resulta que desde el Service Pack 2 para Windows XP todos utilizan este kernel a no ser que se indique explícitamente lo contrario en las opciones del cargador de arranque. Esto es así porque, siempre que el procesador lo soporte, Windows viene con DEP habilitado y, para que DEP funcione, es necesario utilizar el kernel PAE.

El primer paso para realizar el proceso será convertir la dirección virtual a formato binario para extraer el primer índice:
0x81d44c98 = 10000001110101000100110010011000
10 -> Page Directory Pointers Index

Necesitaremos también el valor asociado al registro cr3 que podremos obtener de la ejecución de mi dirty python script. En concreto el parámetro es:
+0x010 DirectoryTableBase   : 0x00122000

El contenido del registro cr3 apunta a la primera tabla implicada en la traducción de direcciones, la Page Directory Pointers Table. Existe una sola por proceso con 4 entradas de 8 bytes cada una. Para obtener la dirección base de la siguiente tabla y continuar así con el proceso de traducción usaremos la siguiente fórmula:
cr3 + (PDPI * sizeof(PDPE)) = 0x122000 + (0x2 * 0x8) = 0x122010

D:\_memory\tools>DumpHex.exe /s122010 /L4 /nc volcado.dd
DumpHex Version 1.0.1
Copyright (c) 2003 Robert Bachmann

00122010h: 01 50 12 00

La segunda tabla que buscamos es la Page Directory Table. Pueden haber hasta 4 por proceso con 512 entradas cada una, de nuevo con un tamaño de 8 bytes cada entrada. El puntero obtenido, y que apunta a la base de la tabla apropiada, sería 0x125001 (el volcado está en litle endian), pero para que sea válido tenemos que descartar los 12 primeros bits ya que las páginas se alinean en memoria en bloques de 4KB:
pfn = 0x125001 & 0xfffff000 = 0x125000

Convertiremos nuevamente la dirección virtual origen de todo el proceso a binario y extraeremos la dirección del siguiente índice:
0x81d44c98 = 10 000001110101000100110010011000
000001110 -> Page Directory Index

Para obtener el puntero a la base de la tercera tabla aplicaremos la fórmula:
pfn + PDI * sizeof(PDE) = 0x125000 + (0xe * 0x8) = 0x125070

D:\_memory\tools>DumpHex.exe /s125070 /L4 /nc volcado.dd
DumpHex Version 1.0.1
Copyright (c) 2003 Robert Bachmann

00125070h: E3 09 C0 01

El valor obtenido para el puntero a la base de la Page Table es igual a 0x1c009e3. Tal y como hicimos antes descartaremos los 12 primeros bits:
pfn = 1c009e3 & 0xfffff000 = 0x1c00000

Los 12 bits descartados son interpretados como flags con el siguiente significado:
  1 0 0 1 1 1 1 0 0 0 1 1
| | | | | | | | | | | |
| | | | | | | | | | | Valid
| | | | | | | | | | Write/Read
| | | | | | | | | Owner
| | | | | | | | Write Through
| | | | | | | Cache disabled
| | | | | | Accessed
| | | | | Dirty
| | | | Large Page
| | | Global
| | Copy-on-write
| Prototype PTE
Writable

De los anteriores dos flags resultan especialmente relevantes: Valid y Large Page. El primero nos dice que la página que contiene la dirección deseada no ha sido paginada (por lo tanto esta en nuestro fichero de volcado) y el segundo indica que se trata de una página utilizada por el kernel, y como se trata de un sistema PAE, el tamaño es de 2MB. Esto último condiciona el resto del proceso ya que tenemos que olvidarnos del procedimiento habitual, que implicaría seguir un último nivel de indirección (consultar el gráfico), para obtener el offset dentro de la página definitiva.

Dicho lo anterior lo que nos queda de la conversión a binario de la dirección virtual se utilizaría directamente como offset en la página de memoria correspondiente, así que:
0x81d44c98 = 10 000001110 101000100110010011000
101000100110010011000 -> Page Byte Offset

que utilizando la siguiente y última fórmula nos dá:
pfn + offset = 0x1c00000 + 0x144C98 = 0x1D44C98

D:\_memory\tools>DumpHex.exe /s1d44c98 /L128 volcado.dd
DumpHex Version 1.0.1
Copyright (c) 2003 Robert Bachmann

01D44C98h: EC 8F F6 81 EC 8F F6 81 00 00 00 00 00 00 00 00 ýÅ÷üýÅ÷ü........
01D44CA8h: 4B 44 42 47 30 03 00 00 00 D0 C4 81 00 00 00 00 KDBG0....ð-ü....
01D44CB8h: B8 8A CF 81 00 00 00 00 00 00 00 00 00 00 00 00 ©è¤ü............
01D44CC8h: 28 01 08 00 18 00 01 00 00 A0 CF 81 00 00 00 00 (........á¤ü....
01D44CD8h: 00 00 00 00 00 00 00 00 70 4C D6 81 00 00 00 00 ........pLÍü....
01D44CE8h: 90 A9 D5 81 00 00 00 00 B4 A9 D5 81 00 00 00 00 É®iü....¦®iü....
01D44CF8h: 10 F2 D4 81 00 00 00 00 80 F6 D4 81 00 00 00 00 .=Èü....Ç÷Èü....
01D44D08h: 14 40 D8 81 00 00 00 00 D0 49 D8 81 00 00 00 00 .@Ïü....ðIÏü....

Y esto, definitivamente era lo que buscábamos, aunque no resulte obvio a simple vista y necesite de un poco de interpretación:
_KDDEBUGGER_DATA64
+0x000 DBGKD_DEBUG_DATA_HEADER64 : _DBGKD_DEBUG_DATA_HEADER64
+0x000 LIST_ENTRY64 : _LIST_ENTRY64
+0x000 Flink : 0x81f68fec
+0x008 Blink : 0x81f68fec
+0x010 OwnerTag : 0x4742444b (KDBG)
+0x014 Size : 0x000330
+0x018 KernBase : 0xffffffff81c4d000
+0x020 BreakpointWithStatus : 0xffffffff81cf8ab8
+0x028 SavedContext : 0
+0x030 ThCallbackStack : 0x128
+0x038 NextCallback : 0x8
+0x040 FramePointer : 0x18
+0x048 PaeEnabled : 0x1
+0x050 KiCallUserMode : 0xffffffff81cfa000
+0x058 KeUserCallbackDispatcher : 0
+0x060 PsLoadedModuleList : 0xffffffff81d64c70
+0x068 PsActiveProcessHead : 0xffffffff81d5a990
+0x070 PspCidTable : 0xffffffff81d5a9b4
+0x078 ExpSystemResourcesList : 0xffffffff81d4f210
+0x080 ExpPagedPoolDescriptor : 0xffffffff81d4f680
+0x088 ExpNumberOfPagedPools : 0xffffffff81d84014
+0x090 KeTimeIncrement : 0xffffffff81d849d0
...

Si hubiera dicho esto al principio seguro que no habrías llegado hasta aquí, pero lo cierto es que existe una forma infinitamente más simple de traducir una dirección virtual a su correspondiente dirección física utilizando el debugger de Microsoft. Una vez abierto el fichero volcado.dmp:
0: kd> !vtop 0 81d44c98
X86VtoP: Virt 81d44c98, pagedir 122000
X86VtoP: PAE PDPE 122010 - 0000000000125001
X86VtoP: PAE PDE 125070 - 0000000001c009e3
X86VtoP: PAE Large page mapped phys 1d44c98
Virtual address 81d44c98 translates to physical address 1d44c98.

Bonus

Primero Moyix y ahora Jamie Levy extendiendo sus avances, nos descubren que, utilizando una firma única derivada de los datos que hemos extraido a lo largo de este post resulta posible identificar el sistema operativo del volcado objeto de análisis.

Parece ser que la variable Type de la estructura DBGKD_DEBUG_DATA_HEADER64 es diferente para cada versión+service pack de Windows. Si a esto le añadimos que la coletilla de la LIST_ENTRY64 también resulta diferente para cada tipo de procesador ya tenemos una firma que, situándola dentro del offset adecuado, nos permitirá identificar unívocamente la versión de Windows de la que procede la imagen. Para este caso:
00 00 00 00 00 00 00 00 4B 44 42 47 30 03 = Windows Vista Service Pack 2

Y esto ha sido todo, en breve más, y mejor, espero.

Referecias

Microsoft Windows Internals, Fifth Edition
David Solomon, Mark Russinovich y Alex Ionescu

DMP File Structure

Win32dd – Memory Imaging

Understanding the kernel address space on 32-bit Windows Vista

Understanding !PTE , Part 1: Let’s get physical

Understanding !PTE, Part2: Flags and Large Pages

Part 3: Understanding !PTE - Non-PAE and X64