martes, 5 de junio de 2012

La SSDT vista desde cerca

La SSDT es sin lugar a dudas una de las estructuras internas manejadas por Windows sobre la que, posiblemente, más literatura exista, por lo que éste artículo no es ninguna novedad. No obstante me la he encontrado en mi camino y como dijo Albert Einstein: "No entiendes realmente algo a menos que seas capaz de explicárselo a tu abuela".

En concreto, y para simplificar el proceso, he analizado el funcionamiento del system service dispatching para un sistema Windows XP SP3 x86. Así que, voy a intentar explicaros, que es lo que yo he entendido.

Desde el modo usuario al modo kernel

Empezaremos desde arriba y en lo más alto tenemos los "Environment Subsystems". Su función es la de proporcionar un interfaz (mediante librerías DLL) para un subconjunto de las llamadas al sistema gestionadas por el kernel de Windows, de forma que puedan ser utilizadas desde las aplicaciones en modo usuario.

Por ejemplo, para el subsistema de Windows tenemos las DLLs Kernel32.dll, Advapi32.dll, User32.dll y Gdi32.dll las cuales exportan las diferentes funciones disponibles en la API de Windows o, lo que es lo mismo, conforman el interfaz. Todas estas librerías importan (llaman a) funciones desde la DLL ntdll.dll que, al igual que las anteriores, se ejecuta en modo usuario. Veámoslo en un diagrama extraído del libro "Windows Internals 5th Edition":


La librería ntdll.dll constituye el intermediario entre el modo usuario y el modo kernel. Esta librería exporta una serie de funciones que sirven únicamente como punto de entrada para sus homólogas exportadas por ntoskrnl.exe y que, en su conjunto, se conocen como la API Nativa de Windows. Dado que se trata de una serie de funciones que actúan a bajo nivel su uso directo no está recomendado y de hecho no están oficialmente documentadas.

Importante destacar que en Windows XP SP3 x86 el binario que aloja el kernel de Windows es ntkrnlpa.exe, y no ntoskrnl.exe. Para explicarlo me citaré a mí mismo:

"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."

Ya hemos llegado a ntdll.dll, ¿cómo sigue el camino desde el modo usuario para una función que necesita ejecutar alguna de las llamadas al sistema ofrecidas por el kernel?

Si ejecutamos, por ejemplo, el bloc de notas de Windows (notepad.exe) y desde WinDbg nos "asociamos" a él (menu File, Attach to a Process ...) vamos a seguir manualmente el camino para la llamada a la función CreateFileW; podemos encontrar en el desensamblado la llamada a la función correspondiente importada desde kernel32:
0:001> u CreateFileW+1B0 L1
kernel32!CreateFileW+0x359:
7c8109b0 ff150810807c call dword ptr [kernel32!_imp__NtCreateFile (7c801008)]

Si ahora utilizamos Dependency Walker para ver la librería desde la que se importa NtCreateFile confirmaremos que se trata de ntdll.dll:


La llamada a CreateFileW importada desde kernel32.dll y realizada por notepad.exe se transforma en una llamada a NtCreateFile en ntdll.dll, ¿cómo saltamos al modo kernel desde aquí e invocamos el servicio adecuado?
0:001> u NtCreateFile L4
ntdll!NtCreateFile:
7c91d0ae b825000000 mov eax,25h
7c91d0b3 ba0003fe7f mov edx,offset SharedUserData!SystemCallStub (7ffe0300)
7c91d0b8 ff12 call dword ptr [edx]
7c91d0ba c22c00 ret 2Ch

La primera línea coloca en el registro eax un valor, 0x25, que será utilizado como índice dentro de una tabla (la SSDT) para localizar la llamada a NtCreateFile en la API nativa de Windows. A continuación se realiza una llamada a una función utilizando un puntero que apunta a 7ffe0300. Veamos a que apunta, y las instrucciones que componen dicha función:
0:001> ln poi(7ffe0300)
(7c91e4f0) ntdll!KiFastSystemCall | (7c91e4f4) ntdll!KiFastSystemCallRet
Exact matches:
ntdll!KiFastSystemCall = <no type information>
0:001> u KiFastSystemCall L2
ntdll!KiFastSystemCall:
7c91e510 8bd4 mov edx,esp
7c91e512 0f34 sysenter

Ahí tenemos el "interruptor" que cambia al modo kernel y ejecuta la llamada al sistema contenida en eax con la lista de parámetros apuntada por el registro edx. El manejador para la instrucción sysenter se ubica durante el arranque del sistema en el registro con índice 0x176 de la tabla MSR, y que podemos identificar desde una sesión de windbg en modo kernel:
lkd> rdmsr 176
msr[176] = 00000000`80541590
lkd> ln 80541590
(80541590) nt!KiFastCallEntry | (8054169e) nt!KiServiceExit
Exact matches:
nt!KiFastCallEntry = <no type information>

Una vez finalizado el trabajo en modo kernel el system service dispatcher utiliza la instrucción sysexit para retornar al modo usuario. Para poder ejecutar éste retorno sin problemas el sistema genera un "trap frame", que no es otra cosa que una estructura utilizada para almacenar el valor de determinados registros del procesador (un subconjunto del contexto del thread) antes de cambiar al modo kernel. También se establece a 1 el valor de _ETHREAD->Tcb.PreviousMode. Podemos comprobar qué almacena un "trap frame" analizando su definición mediante el siguiente comando de windbg en modo kernel (su contenido no se ha incluido por no alargar innecesariamente el artículo):
lkd> dt nt!_KTRAP_FRAME

Mencionar que sysenter se utiliza en sistemas ejecutándose en procesadores Pentium II y superiores. Para otro tipo de procesadores se utilizan mecanismos o instrucciones diferentes (p.e. para micros anteriores al Pentium II se utiliza la instrucción 0x2e para generar la interrupción y en ocasiones el mecanismo de vuelta al modo usuario se lanza utilizando la instrucción iretd).

Por último, para evitar que la lista de parámetros para la llamada al sistema pueda ser alterada desde el modo usuario durante el tiempo que se cambia al modo kernel el system service dispatcher se encarga de copiar los argumentos al stack del modo kernel así como de comprobar que los parámetros son correctos y que los posibles buffers a utilizar sean accesibles tanto para lectura como para escritura.

Desde el modo kernel al modo kernel

También existe la posibilidad que se necesite de la ejecución de una llamada al sistema desde un driver ejecutándose en modo kernel; ¿cómo se realiza este proceso si ya hemos mencionado que Microsoft no soporta la utilización directa de la API Nativa de Windows? La solución la tenemos en las funciones Zw* existentes, que sí están soportadas y documentadas en la MSDN.

Si utilizamos Dependency Walker para ver las funciones exportadas por ntkrnlpa.exe encontraremos, entre otras, las correspondientes a la API nativa de Windows, y otra serie de funciones (algunas menos), con nombres similares a las anteriores pero prefijadas por Zw. Si desensamblamos, por ejemplo, la equivalente a NtCreateFile desde una sesión de WinDbg en modo kernel:
lkd> u nt!ZwCreateFile L6
nt!ZwCreateFile:
8050003c b825000000 mov eax,25h
80500041 8d542404 lea edx,[esp+4]
80500045 9c pushfd
80500046 6a08 push 8
80500048 e874140400 call nt!KiSystemService (805414c1)
8050004d c22c00 ret 2Ch

En este caso se llama directamente al system service dispatcher KiSystemService, sin necesidad de la copia de parámetros al stack del modo kernel y almacenamiento del contexto del thread (o creación del "trap frame") que se realizan cuando la llamada proviene del modo usuario (valor PreviousMode del thread igual a 1).

Puntualizar que una vez terminados los pasos previos asignados a KiSystemService se salta de forma incondicional a KiFastCallEntry:
8053d4f0 e9d8000000      jmp     nt!KiFastCallEntry+0x8d (8053d5cd)

concretamente el salto nos sitúa en la parte donde se obtiene la llamada al sistema apuntada por eax así como se determina la tabla SSDT a utilizar. Anteriormente a Windows XP SP3 el proceso era al revés, y desde KiFastCallEntry (utilizada para las llamadas desde el modo usuario) se producía un salto incondicional a KiSystemService.

La tabla SSDT

En las llamadas al sistema hemos visto como se utiliza un índice para acceder a la función concreta que se desea utilizar dentro de una tabla, la cual se conoce como tabla SSDT. La ubicación de esta tabla en memoria está apuntada por la variable KeServiceDescriptorTable, la cual es exportada directamente por el kernel de Windows (ntkrnlpa.exe, como ya hemos visto). Podemos comprobarlo con Dependency Walker o utilizando dicha variable en un sesión de windbg:
lkd> dds KeServiceDescriptorTable L4
80552fa0 80501b8c nt!KiServiceTable
80552fa4 00000000
80552fa8 0000011c
80552fac 80502000 nt!KiArgumentTable

Pasando esto a lenguaje C tendríamos la siguiente estructura, incluyendo comentarios para entender mejor el propósito de cada uno de los campos que la componen. Para la definición de ésta y las siguientes estructuras de datos necesarias me he basado en el libro "Undocumented Windows 2000 Secrets":
typedef struct _SYSTEM_SERVICE_TABLE
{
ULONG *ServiceTableBase; // Tabla SSDT
ULONG *ServiceCounterTableBase; // Solo para debug
ULONG NumberOfServices; // Num. de elementos en la SSDT
PVOID *ParamTableBase; // Tabla de parametros
}
SYSTEM_SERVICE_TABLE;

Sabiendo ésto podemos volcar de forma sencilla el contenido completo de dicha tabla SSDT desde la sesión de windbg:
lkd> dds KiServiceTable L11c
80501b8c 80599948 nt!NtAcceptConnectPort
80501b90 805e6db6 nt!NtAccessCheck
80501b94 805ea5fc nt!NtAccessCheckAndAuditAlarm
80501b98 805e6de8 nt!NtAccessCheckByType
80501b9c 805ea636 nt!NtAccessCheckByTypeAndAuditAlarm
...

o, dado que cada entrada de la tabla ocupa 4 bytes, podemos utilizar el siguiente comando (visto en el libro "Windows Internals 5th Edition") para obtener la llamada del sistema correspondiente a un determinado índice:
lkd> ln poi(KiServiceTable + 25 * 4)
(8056e27c) nt!NtCreateFile | (8056e2b6) nt!NtCreateNamedPipeFile
Exact matches:
nt!NtCreateFile = <o type information>

Hasta aquí parece fácil, pero la cosa se complica un poco dado que Windows reserva espacio para 4 tablas SSDT, de las cuales en principio sólo se utilizan dos. La primera ya la conocemos y la segunda almacena las llamadas al sistema relacionadas con las funciones de la interfaz gráfica de Windows. De nuevo, trasladando esto a lenguaje C tendríamos:
typedef struct _SERVICE_DESCRIPTOR_TABLE
{
SYSTEM_SERVICE_TABLE ntoskrnl; // ntkrnlpa.exe en XP >= SP2
SYSTEM_SERVICE_TABLE win32k; // win32k.sys
SYSTEM_SERVICE_TABLE table3; // No usada (reservada)
SYSTEM_SERVICE_TABLE table4; // No usada (reservada)
}
SERVICE_DESCRIPTOR_TABLE;

Parece que en Windows 2000 se utilizaba una tercera para las funciones exportadas por el driver spud.sys del servidor Internet Information Services, pero en Windows XP SP3 después de la instalación de dicho servidor parece que el mecanismo ha variado.

La segunda tabla SSDT conecta la interfaz GDI (gdi32.dll) y la interfaz del Windows Manager (user32.dll) con las llamadas al sistema almacenadas en win32k.sys, que se ejecuta en modo kernel. Dado que no todos los threads utilizan el subsistema gráfico de Windows no necesitan el acceso a los servicios almacenados en dicha tabla.

Resulta que durante el proceso de inicialización del subsistema gráfico de Windows éste se encarga de llamar a la función KeAddSystemServiceTable (exportada por ntkrnlpa.exe) a fín de registrar todas las llamadas del sistema que es capaz de gestionar; pero en lugar de actualizar la SERVICE_DESCRIPTOR_TABLE apuntada por la variable KeServiceDescriptorTable ésta es copiada a otra ubicación, tras lo cual se añade la tabla de funciones exportadas por win32k.sys y se genera una nueva variable, KeServiceDescriptorTableShadow (no exportada por ntkrnlpa.exe), que apunta a la nueva SERVICE_DESCRIPTOR_TABLE. De nuevo podemos confirmar este punto desde una sesión de WinDbg:
lkd> .reload
lkd> dds KeServiceDescriptorTableShadow L10
80552f60 80501b8c nt!KiServiceTable
80552f64 00000000
80552f68 0000011c
80552f6c 80502000 nt!KiArgumentTable
80552f70 bf999b80 win32k!W32pServiceTable
80552f74 00000000
80552f78 0000029b
80552f7c bf99a890 win32k!W32pArgumentTable
80552f80 00000000
80552f84 00000000
80552f88 00000000
80552f8c 00000000
80552f90 00000000
80552f94 00000000
80552f98 00000000
80552f9c 00000000

y como antes podemos volcar todo el contenido:
lkd> dds W32pServiceTable L29b
bf999b80 bf935f7e win32k!NtGdiAbortDoc
bf999b84 bf947b29 win32k!NtGdiAbortPath
bf999b88 bf88ca52 win32k!NtGdiAddFontResourceW
bf999b8c bf93f6f0 win32k!NtGdiAddRemoteFontToDC
bf999b90 bf949140 win32k!NtGdiAddFontMemResourceEx
...

o sólo la llamada del sistema correspondiente a un determinado índice:
lkd> ln poi(W32pServiceTable + 25 * 4)
(bf8e634c) win32k!NtGdiCreateMetafileDC | (bf8e63b4) win32k!NtGdiGetMiterLimit
Exact matches:
win32k!NtGdiCreateMetafileDC = <no type information>

A pesar que la variable KeServiceDescriptorTableShadow no se exporta podemos utilizar en nuestros programas el offset respecto a KeServiceDescriptorTable para obtener acceso a la SSDT Shadow, pero esto no es un método recomendable ya que dicho offset varía entre diferentes versiones de Windows e incluso con diferentes parches aplicados. En Windows XP SP3 el offset es 0x40h:
lkd> dds KeServiceDescriptorTable-40 L10
80552f60 80501b8c nt!KiServiceTable
80552f64 00000000
80552f68 0000011c
80552f6c 80502000 nt!KiArgumentTable
80552f70 bf999b80 win32k!W32pServiceTable
80552f74 00000000
80552f78 0000029b
80552f7c bf99a890 win32k!W32pArgumentTable
80552f80 00000000
80552f84 00000000
80552f88 00000000
80552f8c 00000000
80552f90 00000000
80552f94 00000000
80552f98 00000000
80552f9c 00000000

¿KeServiceDescriptorTable o KeServiceDescriptorTableShadow?

Si analizamos la estructura de datos en memoria que representa un thread:
lkd> dt _ETHREAD -b
ntdll!_ETHREAD
+0x000 Tcb : _KTHREAD
+0x000 Header : _DISPATCHER_HEADER
...
+0x0e0 ServiceTable : Ptr32
...

podemos ver como dentro del Tcb existe una variable que apunta a la ServiceTable, o lo que es lo mismo a la SSDT. Resulta que, por defecto, para cada nuevo thread se asigna la dirección de la KeServiceDescriptorTable en el campo _ETHREAD->Tcb.ServiceTable. Si el thread precisa la ejecución de cualquiera de las llamadas al sistema exportadas por win32k.sys se sustituye dicho campo por la dirección de la KeServiceDescriptorTableShadow. Por defecto todos los threads se crean como no-GUI dado que no todos precisan ejecutar llamadas al subsistema gráfico.

En el siguiente ejemplo desde una sesión de debug en modo kernel el proceso implícito es windbg y el thread implícito trabaja por defecto con llamadas al sistema para win32k.sys. Podemos confirmarlo primero obteniendo información detallada del thread:
lkd> !thread
THREAD 89529020 Cid 01d0.0cd4 Teb: 7ffdd000 Win32Thread: e29f9318 WAIT: (WrUserRequest) UserMode Non-Alertable
8942de68 SynchronizationEvent
Not impersonating
...

Se confirma que es un thread GUI dado que el valor del campo Win32Thread (perteneciente a la estructura _KTHREAD), es distinto de 0 ya que apunta a una ubicación de memoria que contiene el ID del thread. Ahora extraeremos el valor del campo ServiceTable:
lkd> ?? @$thread->Tcb.ServiceTable
void * 0x80552f60
lkd> ln 80552f60
(80552f60) nt!KeServiceDescriptorTableShadow | (80552fa0) nt!KeServiceDescriptorTable
Exact matches:
nt!KeServiceDescriptorTableShadow = <no type information>

Veámoslo ahora para un thread sin funciones de la GUI. Para ello primero tendremos que asociarnos a un proceso no-GUI (entendiendo por tal uno que no utilice llamadas al sistema desde win32k), luego a uno de sus threads y desde allí consultar el valor del campo ServiceTable:
lkd> !process 0 4 System
PROCESS 863c6830 SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000
DirBase: 00af8000 ObjectTable: e1001c20 HandleCount: 248.
Image: System

THREAD 863c65b8 Cid 0004.0008 Teb: 00000000 Win32Thread: 00000000 READY
THREAD 863c5da8 Cid 0004.0010 Teb: 00000000 Win32Thread: 00000000 WAIT
THREAD 863c5b30 Cid 0004.0014 Teb: 00000000 Win32Thread: 00000000 WAIT
THREAD 863c58b8 Cid 0004.0018 Teb: 00000000 Win32Thread: 00000000 WAIT
THREAD 863c5640 Cid 0004.001c Teb: 00000000 Win32Thread: 00000000 WAIT
...

Podemos utilizar cualquiera de los threads que engloba el proceso ya que en el listado anterior el puntero Win32Thread es igual a 0.
lkd> .thread 863c65b8
Implicit thread is now 863c65b8
lkd> ?? @$thread->Tcb.ServiceTable
void * 0x80552fa0
lkd> ln 80552fa0
(80552fa0) nt!KeServiceDescriptorTable | (80552fe0) nt!KeMinimumIncrement
Exact matches:
nt!KeServiceDescriptorTable = <no type information>

Como hemos mencionado cuando el thread se inicia el valor de ServiceTable apunta a KeServiceDescriptorTable, es decir, la SSDT por defecto ya que se trata de un thread no-GUI. ¿Cómo se realiza el cambio del campo del thread para que apunte a KeServiceDescriptorTableShadow y por lo tanto se transforma en un thread GUI? Y una vez actualizado dicho campo, ¿como se distingue si el índice para una posible llamada al sistema se corresponde con la tabla de ntoskrnl o con la tabla de win32k?

Desensamblando y terminando

Ya hemos visto que cuando se utiliza KiSystemService desde el modo kernel para ejecutar una llamada al sistema al final de la misma se produce un salto incondicional a KiFastCallEntry, utilizada desde el modo usuario para el mismo propósito, así que parece lógico analizar dicha función a partir de ese punto:
lkd> u KiFastCallEntry+0x8d L9
nt!KiFastCallEntry+0x8d:
8053d5cd 8bf8 mov edi,eax
8053d5cf c1ef08 shr edi,8
8053d5d2 83e730 and edi,30h
8053d5d5 8bcf mov ecx,edi
8053d5d7 03bee0000000 add edi,dword ptr [esi+0E0h]
8053d5dd 8bd8 mov ebx,eax
8053d5df 25ff0f0000 and eax,0FFFh
8053d5e4 3b4708 cmp eax,dword ptr [edi+8]
8053d5e7 0f8345fdffff jae nt!KiBBTUnexpectedRange (8053d332)

En la primera instrucción únicamente se copia el índice para la llamada al sistema en el registro edi de forma que se pueda manipular su valor sin perder la función apuntada.

En la segunda y tercera instrucción se obtiene el índice para la tabla SSDT donde se ubica la llamada al sistema. Esto es:
  • 0x00 para KiServiceTable
  • 0x10 para W32pServiceTable
En la quinta instrucción [esi+0E0h] se corresponde con _ETHREAD->Tcb.ServiceTable, de forma que:
  • Si _ETHREAD->Tcb.ServiceTable = KeServiceDescriptorTable, o lo que es lo mismo, es un thread no-GUI:
    • Si edi = 0x00 -> edi apunta a KiServiceTable
    • Si edi = 0x10 -> edi apunta a 00000000

  • Si _ETHREAD->Tcb.ServiceTable = KeServiceDescriptorTableShadow, o lo que es lo mismo, es un thread GUI:
    • Si edi = 0x00 -> edi apunta a KiServiceTable
    • Si edi = 0x10 -> edi apunta a W32pServiceTable
En la séptima instrucción dejamos almacenado en eax únicamente el índice para la llamada al sistema, independientemente de la tabla donde ésta se ubique.

En la octava instrucción [edi+8] apunta al campo NumberOfServices de la SYSTEM_SERVICE_TABLE, es decir, al número máximo de llamadas al sistema apuntadas por la SSDT, pero también puede apuntar a 0 si el thread es no-GUI pero la llamada al sistema está dentro de la W32pServiceTable. En cualquier caso, si el índice proporcionado excede éste límite se llama a KiBBTUnexpectedRange para manejar la excepción.

Supongamos que se dá el caso de que la tabla apuntada por edi es la 0x10 pero el thread es no-GUI. En la primera instrucción dentro de KiBBTUnexpectedRange se comprueba éste punto:
nt!KiBBTUnexpectedRange:
8053d332 83f910 cmp ecx,10h

y se realiza la conversión del thread no-GUI a thread GUI:
8053d337 52              push    edx
8053d338 53 push ebx
8053d339 e8e2530800 call nt!PsConvertToGuiThread (805c2720)

Dentro de esta función lo que sucede, básicamente es:
  1. Se amplia el tamaño del stack y se comienza a utilizarlo (MmCreateKernelStack & KeSwitchKernelStack).
  2. Se modifica el valor de _KTHREAD->ServiceTable pasando de apuntar a KeServiceDescriptorTable a KeServiceDescriptorTableShadow.
  3. Se ejecutan PspW32ProcessCallout y PspW32ThreadCallout.

Una vez finalizado el proceso y convertido a thread-GUI se retorna nuevamente a la parte de KiFastCallEntry analizada con anterioridad:
8053d34a 0f847d020000    je      nt!KiFastCallEntry+0x8d (8053d5cd)

De vuelta a KiFastCallEntry, y después de varias comprobaciones e instrucciones más la ejecución final de la llamada al sistema deseada se produce en la siguiente instrucción:
8053d636 ffd3            call    ebx

Y aqui termina nuestro viaje. En una próxima entrada, y ahora que conocemos el proceso de System Service Dispatching lo analizaremos desde otro punto de vista y veremos sus implicaciones.

To Be Continued

Referencias

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

Inside the Native API.

NT (and XP) Native API Compression (And how the NT API works).

Get system call address from SSDT.

Nt vs. Zw - Clearing Confusion On The Native API.

System Call Optimization with the SYSENTER Instruction.

Inside KiSystemService.

Question about KiFastCallEntry & KiSystemService.

Windows Kernel Internals - Win32K.sys.

How GUI Thread Conversion on Svr03 Breaks the SEH Chain.

3 comentarios:

silverhack dijo...

IMPRESIONANTE!!
Me ha encantado y deseando leer la siguiente entrada!
Me has despejado varias dudas!

neofito dijo...

¡Muchas gracias monstruo!

Saludos

Trojan dijo...

Como bien dices no es ninguna novedad , pero lo importante es como has desarrollado el tema y eso a mi impresión es lo que hay que destacar, junto con la importancia que tiene este tema. Las personas que trabajamos en servicios informáticos Barcelona valoramos este tipo de trabajos y esperamos que sigsn con la calidad de información que brindas ¡Gracias!