domingo, 30 de noviembre de 2008

Ocultación de procesos en Windows: DKOM

DKOM (Direct Kernel Object Manipulation) es una técnica que, basándose en el acceso al espacio de memoria del kernel, nos vá a permitir modificar el estado de los objetos del sistema. En concreto, nos vá a permitir:

  • Ocultar procesos, drivers o puertos.
  • Elevar el nivel de privilegio de un hilo y por tanto del proceso al que pertenece.
  • Dificultar el análisis forense del sistema.

En esta entrada vamos a centrarnos en la ocultación de procesos, y para ello analizaremos una prueba de concepto que he desarrollado con dicha finalidad, dkomdriver. Para programarlo he consultado diferentes fuentes de información las cuales incluyo en el apartado de bibliografía.

Modo usuario vs modo kernel

Con el fín de garantizar la seguridad de los datos manejados por el kernel, Windows implementa una separación entre los diferentes procesos que se ejecutan en el sistema. De este modo podemos distinguir un modo usuario, donde se ubican la mayoría de las aplicaciones de los usuarios y un modo kernel, donde se permite el acceso a toda la memoria del sistema e instrucciones para el manejo directo del hardware.

Todo lo anterior se cimenta en la separación en niveles de privilegios implementada por los microprocesadores actuales los cuales distinguen hasta cuatro zonas o anillos, desde ring 0 a ring 4, siendo el número más bajo el que ofrece mayores privilegios y el más alto el menos privilegiado. La mayoría de sistemas operativos actuales, entre los que se incluiría Windows, solo utilizan dos de estos niveles, ring 3 o modo usuario y ring 0 o modo kernel.

Algunos ejemplos de aplicaciones en modo kernel serían los procesos y servicios que se ejecutan en segundo plano o los drivers de dispositivo para manejar el hardware instalado en nuestro equipo. Y esto último será lo que analizaremos, un driver de sistema que nos permitirá acceder a la memoria manejada por el kernel, alterando su contenido para ocultar determinados procesos.

Herramientas y paquetes necesarios

Lo primero que vamos a necesitar para programar nuestro driver será el Windows Server 2003 DDK, el cual podremos descargar en formato ISO desde aquí. Con esta versión podremos crear paquetes compatibles con los siguientes sistemas operativos:
  • Windows 2003 Server, a partir del SP1, en todas sus ediciones.
  • Windows XP Profesional y Home, a partir del SP1.
  • Windows 2000, a partir del SP3, también en todas sus ediciones.

También vamos a utilizar el fichero de cabecera ntifs.h, el cual está incluido en el Windows Server 2003 IFS (Installable File System) Kit. La distribución de este paquete no es gratuita y puede solicitarse su envío por parte de Microsoft.

No obstante también existe una versión libre de este fichero publicada por Bo Brantén y que será suficiente para nuestros propósitos. Podremos encontrar la última release para ntifs.h aquí.

Por último, y como herramientas de ayuda durante el proceso de desarrollo utilizaremos WinObj y DbgView.

Interfaz entre el modo kernel y el modo usuario

En primer lugar descargaremos el código de la prueba de concepto que he desarrollado, y que podremos encontrar aquí.

Obviaremos el contenido del fichero de cabecera ntifs.h y empezaremos por el fichero que ejerce labores de interfaz entre el componente en modo usuario, instdrv, y el driver en modo kernel, dkomdriver.

Si analizamos el fichero de cabecera interface.h encontraremos, en primer lugar, la definición de una nueva orden IOCTL. Este tipo de órdenes permiten la comunicación desde un programa en modo usuario o desde otro controlador. Su formato:
#define IOCTL_DRV_HIDE (ULONG) CTL_CODE(FILE_DEVICE_UNKNOWN,

0x801,
METHOD_BUFFERED,
FILE_WRITE_ACCESS)

Una vez definida la orden, puede enviarse mediante la función DeviceIoControl, la cual será manejada por el subsistema de administración de E/S del kernel de Windows; este último generará una estructura IRP, en cuyo interior se colocará la orden, y la dirigirá al driver de dispositivo adecuado.

Seguiremos con el análisis y pasaremos a examinar las dos constantes siguientes. Baste decir, de momento, que se corresponden con un nombre de dispositivo y un enlace simbólico a dicho dispositivo:
const WCHAR dkomDeviceName[] = L"\\Device\\dkomdriver";

const WCHAR dkomDeviceLink[] = L"\\DosDevices\\dkomdriver";

Por último se define una estructura para almacenar la información referente a un proceso. Los dos campos que la forman se corresponden con el PID del proceso y el offset para el elemento Flink, el cual, como ya sabemos, apunta al siguiente proceso de la lista mantenida por el kernel de Windows:
typedef struct typeProc2Hide{

int iPid;
int iFlinkOffset;
}proc2Hide;

Código fuente del driver (modo kernel)

Todo el código del driver para la prueba de concepto se centraliza en el fichero dkomdriver.c, el cual analizaremos ahora.

El punto de entrada para el driver lo forma la función DriverEntry, llamada cuando éste es cargado en memoria, y que sería el equivalente a la función main de C o WinMain para las aplicaciones gráficas de Windows.

Ahora llega uno de los puntos neurálgicos, la creación del objeto de dispositivo. Para que el driver de dispositivo sea visible desde la aplicación en modo usuario el administrador de objetos de Windows debe conocer su existencia, y aquí es donde comprenderemos el uso de WinObj. Una vez abierta la aplicación podremos observar el contenido completo del espacio de nombres del administrador de objetos de Windows. En concreto, las secciones de interés cuando ejecutemos el PoC serán Device, Driver y GLOBAL??.

Primero crearemos el objeto dispositivo, comprobando si se creó correctamente:
RtlInitUnicodeString(&deviceNameUnicode,dkomDeviceName);

ntstatus = IoCreateDevice(driverObject,
0,
&deviceNameUnicode,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE,
&dkomDevice);
if(!NT_SUCCESS(ntstatus)){
DbgPrint("Fallo al crear el dispositivo\n");
return ntstatus;
}

y a continuación crearemos un enlace simbólico, para simplificar la utilización del objeto anterior (este paso no es estrictamente necesario pero facilitará el uso del driver por parte de las aplicaciones win32):
RtlInitUnicodeString(&dosDeviceNameUnicode,dkomDeviceLink);

ntstatus = IoCreateSymbolicLink(&dosDeviceNameUnicode,
&deviceNameUnicode);
if(!NT_SUCCESS(ntstatus))
{
IoDeleteDevice(dkomDevice);
DbgPrint("Fallo al crear el enlace simbolico\n");
return ntstatus;
}

A partir de este momento ya podremos abrir un manejador al driver del kernel desde el espacio del usuario mediante el enlace simbólico al objeto de dispositivo.

Ahora crearemos los punteros a las funciones que conforman las funcionalidades ofrecidas por el driver. En concreto, cada una de las siguientes funciones se ejecutarán como respuesta a los distintos tipos de IRPs:
driverObject->MajorFunction[IRP_MJ_CREATE] = dkomCreate;

Se ejecutará en respuesta a la creación de un manejador para el driver.
driverObject->MajorFunction[IRP_MJ_CLOSE] = dkomClose;

Se ejecutará en respuesta al cierre del manejador para el driver.
driverObject->MajorFunction[IRP_MJ_READ] = dkomRead;

Se ejecutará cuando se lance una operación de lectura desde los datos del driver.
driverObject->MajorFunction[IRP_MJ_WRITE] = dkomWrite;

Se ejecutará cuando se lance una operación de escritura de datos hacia el driver.
driverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = dkomControl;

Se ejecutará cuando se envíe un código IOCTL hacia el driver.
driverObject->DriverUnload = dkomUnload;

Llamada cuando se elimine el servicio encargado de cargar el driver en la memoria.

En este PoC únicamente he implementado las funciones dkomControl y dkomUnload. El resto de funciones sólo muestran un mensaje de debug en respuesta a su llamada.

Dentro de la función dkomUnload, la cual marca la eliminación del driver de la memoria del sistema, se elimina en primer lugar el enlace simbólico para el objeto de dispositivo y a continuación el objeto de dispositivo en sí.

Vamos a dejar la función de control momentáneamente, pero enseguida la retomaremos.

Código fuente del instalador (modo usuario)

Si abrimos el fichero de cabecera, instdrv.h, sólo encontraremos la definición de varias constantes, así como una lista enlazada para almacenar información relativa a determinados procesos que se encuentran en ejecución en el sistema.

Analicemos ahora el código del programa en modo usuario, instdrv.c. El programa, como cabría esperar, comienza su ejecución en la función main, donde se centra toda la lógica de funcionamiento.

Comienza analizando el número de argumentos recibidos, mostrando un mensaje con la ayuda para su ejecución. Los parámetros admitidos son:
  • on: se carga el driver en memoria y se realiza la ocultación de procesos.
  • off: se descarga el driver de la memoria y se elimina todo rastro del mismo.

El siguiente paso es obtener la versión del sistema operativo en que se está ejecutando la aplicación, utilizándose para ello la función getOsVersion. Si analizamos la lógica de la misma veremos que no recibe parámetros y se encarga de asignarle un valor a la variable global flinkvalue en función del sistema operativo y service pack detectado, retornando una constante (definida en instdrv.h) con la información del S.O.

Si recordamos lo indicado en el análisis de la estructura EPROCESS advertiremos que dicho valor no es más que el offset dentro del bloque ejecutivo de proceso donde podremos encontrar el campo flink, el cual es un puntero al siguiente proceso dentro de la lista doblemente enlazada que mantiene el kernel de Windows.

Seguidamente copiamos el driver de ocultación de procesos en la ruta adecuada y eliminamos el servicio dkomdriver por si se hubiera lanzado con anterioridad:
GetSystemDirectory(driverPath, MAX_PATH);

strcat(driverPath, "\\drivers\\dkomdriver.sys");
CopyFile("dkomdriver.sys", driverPath, FALSE);
 
removeDkomService(VERBOSE_OFF);

Las siguientes sentencias, se encargan de contactar con el Service Control Manager de Windows, crear un servicio, dkomdriver, utilizado para cargar el driver en memoria y, por último, iniciar el servicio, comprobando si se produce un error en algún punto.

Ahora sí, entramos realmente en faena ;)

Utilizando el enlace simbólico para el driver creado durante la carga en memoria abriremos un manejador para poder comunicarnos desde el modo usuario con la aplicación en modo kernel. Después llamamos a la función getProcessList:
pProcessElement* getProcessList(char *prefixString);

Esta función recibe como parámetro una cadena de texto indicando un prefijo para los nombres de los procesos a ocultar. Se encarga de generar una lista enlazada con los PIDs y retornar un puntero al primer elemento de la lista. Tal y como nos recordó el compañero hsec en un comentario para una de las entradas del blog obtener un listado de procesos desde una aplicación en modo usuario y utilizando la API de Windows para ello no es lo más acertado, pero no resulta especialmente relevante en este momento y para nuestro propósito.

Ahora que ya tenemos la lista de procesos a ocultar, y en el caso de que no se encuentre vacía, aplicaremos la técnica DKOM.

Comunicación con el modo kernel desde el modo usuario

Todavía desde la aplicación en modo usuario lanzamos un bucle encargado de recorrer la lista de PIDs para los procesos a ocultar. Para cada uno de los elementos de la lista efectuamos una llamada a DeviceIoControl:
if(!DeviceIoControl(hDevice,

(DWORD) IOCTL_DRV_HIDE,
(VOID *) &p2h,
sizeof(proc2Hide),
NULL,
0,
&dwBytesRet,
NULL))
{
printf("[ERROR] No ha podido enviarse codigo de control al driver\n");
CloseHandle(hDevice);
return FALSE;
}

Como código de control utilizamos el que definimos en la interfaz entre el modo kernel y el modo usuario (interface.h) y que no es otro que IOCTL_DRV_HIDE. Básicamente su propósito es el de solicitar la funcionalidad de ocultación de procesos.

A continuación le pasamos al driver un puntero a una estructura con los datos referentes a uno de los procesos a ocultar. Seguramente la forma en que esta parte está implementada no sea la más elegante de las posibles, pero mi C no dá para más. El siguiente valor que utilizamos se corresponde con el tamaño que ocupa en memoria la estructura con los datos del proceso.

El resto de parámetros no son utilizados por lo que su descripción no resulta necesaria. En todo caso puede consultarse la documentación de la MSDN para la función DeviceIoControl.

Conforme vamos pasando los procesos al modo kernel eliminamos los elementos de la lista enlazada de procesos y liberamos la memoria reservada de forma dinámica.

Una vez terminada la "conversación" con el driver de ocultación de procesos eliminamos el manejador de dispositivo utilizado para ello.

Aplicación de la técnica DKOM, volvemos al modo kernel

Retomaremos ahora la función DkomControl, la cual se encargará de gestionar las órdenes IRP de control emitidas desde el modo usuario. Para ello obtenemos en primer lugar un puntero a la pila donde se almacenan todos los parámetros destinados al controlador:
pStack = IoGetCurrentIrpStackLocation(Irp);

A continuación, y mediante el uso de una estructura switch, controlamos las sentencias a ejecutar en función del código IOCTL recibido desde el modo usuario. En nuestro ejemplo únicamente existe una orden de control definida, IOCTL_DRV_HIDE.

Obtenemos ahora la estructura correspondiente a un proceso y que ha sido enviada desde el modo usuario. Conociendo el PID del proceso a ocultar y utilizando una función del API de Microsoft obtenemos un puntero al bloque EPROCESS, ejecutando el resto de sentencias sólo si la llamada tuvo éxito:
if(PsLookupProcessByProcessId((PVOID)p2h->iPid,&pEProcess) == STATUS_SUCCESS)

{
...
}
else
DbgPrint("No se ha obtenido la direccion del EPROCESS para el PID\n",p2h->iPid);

Vale, ahora ya tenemos la dirección del bloque EPROCESS y sabemos que si le sumamos el offset adecuado (p2h->iFlinkOffset) encontraremos el campo Flink, el cual es un puntero a la dirección de memoria en la que se encuentra el siguiente proceso de la lista mantenida por el kernel:
uEProcessAddr = (ULONG) pEProcess;

pProcessList = (LIST_ENTRY *)(uEProcessAddr + p2h->iFlinkOffset);

Pues con esto y los siguientes pasos vamos a ocultar un proceso, llamémosle P. Primero hacemos que el campo Flink del proceso anterior a P apunte al valor del campo Flink del proceso P, es decir, al siguiente proceso:
*((ULONG *) pProcessList->Blink) = (ULONG) pProcessList->Flink;

Ahora hacemos que el campo Blink del proceso posterior a P apunte al valor del campo Blink del proceso P, es decir, al proceso anterior:
*((ULONG *) pProcessList->Flink + 1) = (ULONG) pProcessList->Blink;

Y por último, y para evitar un error en la ejecución del programa, hacemos que los campos Blink y Flink del proceso P apunten a una dirección de memoria válida, en este caso a sí mismos.
pProcessList->Flink = (LIST_ENTRY *) &(pProcessList->Flink);

pProcessList->Blink = (LIST_ENTRY *) &(pProcessList->Flink);

Pruebas finales

Vale, ahora ya entendemos como funciona el programa internamente. Para generar los ejecutables finales utilizaremos Dev-C++ para la aplicación en modo usuario y el DDK para obtener el driver.

Si lanzamos la aplicación sin parámetros obtendremos un mensaje con la ayuda para su ejecución; en principio no reviste mucha dificultad dado que solo admite los flags on y off.

Carguemos el driver en memoria mediante el flag on, pero antes iniciaremos la aplicación DbgView. Tenemos que asegurarnos que la rueda dentada que aparece bajo el menú de la aplicación no esté tachada, ya que de esta forma podremos ver los mensajes de debug a nivel del kernel. Y esa es precisamente la funcionalidad que tratábamos de obtener mediante las diferentes llamadas a DbgPrint a lo largo del código del driver, dkomdriver.c. De esta forma podremos saber el punto exacto de ejecución en que nos encontramos.

Si hemos lanzado la aplicación pero antes no hemos iniciado ningún ejecutable cuyo nombre esté precedido por hide_ solo observaremos los diferentes mensajes que genera la aplicación pero no nos aprovecharemos de su funcionalidad. No obstante es el momento adecuado para lanzar WinObj y observar, en las secciones Device, Driver y GLOBAL?? nuestro dispositivo, driver y enlace simbólico respectivamente. Si ahora lanzamos nuevamente la aplicación pero con el flag off y reiniciamos la ejecución de WinObj observaremos como han desaparecido.

Aprovechemos ahora la funcionalidad de ocultación de procesos; renombremos alguna aplicación como por ejemplo putty a hide_putty y ejecutémoslo. Obtengamos su PID con el comando tasklist y lancemos nuevamente nuestra aplicación con el flag on. Ahora sí, si volvemos a lanzar tasklist observaremos como el pid para nuestro hide_putty ha desaparecido de la lista.

Si en lugar de tasklist hubiésemos empleado cualquiera de las herramientas indicadas en la entrada del blog "Herramientas para el listado de procesos" el resultado hubiera sido similar. Esto es debido a que para mostrar la información de los diferentes procesos en ejecución confían en la lista mantenida internamente por el kernel de Windows y son, por lo tanto, vulnerables a la técnica DKOM.

Sin embargo, y debido a su funcionamiento interno, psinfo si nos serviría. Nos serviría dado que en lugar de confiar en la lista mantenida por el kernel realiza un recorrido mediante fuerza bruta por toda la lista de identificadores de proceso válidos y, por lo tanto, encontraría el PID para el proceso o procesos ocultos. Dicha técnica aparece indicada en la documentación que acompaña a ProcL, esta sí, una herramienta robusta para la detección de procesos ocultos mediante diversas técnicas. Otras herramientas, aunque seguro que no son las únicas: gmer y CodeWalker.

Código y ejecutable: dkomdriver.zip

Bibliografía

Rootkits - Subverting the Windows kernel
Greg Hoglund y James Butler
Addison Wesley Professional

Professional Rootkits
Ric Vieler
Wrox Press

Programming the Microsoft Windows Driver Model, Second Edition
Walter Oney
Microsoft Press

Estructura y desarrollo de controladores de dispositivo en Windows NT/2000

Introducción a la programación de drivers en Windows

Dkom Process Hider

Addendum 04/04/2009:

Estaba intentando hacer unas pruebas con el driver que aparece en este artículo y el antivirus AVG ha empezado a cantar como un loco: Rootkit-Agent.CG

Hasta hace solo unos días no era detectado, y por curiosidad lo he subido a virustotal. Pues bién, parece que, de momento, únicamente AVG lo detecta como software malicioso. ¡Ya era hora!

3 comentarios:

Anónimo dijo...

Me encanta, es una versión muchísimo mejor de la que esta en mi PDF, todo perfectamente explicado y entendible, felicidades :D

Por cierto, aunque mi nick aquí sea hSec, mi nick real es Hendrix xDDD

Cuando tenga algo más de tiempo te parase la herramienta esa que te comente, para la detección de procesos ocultos con DKOM, estoy teniendo un problema muy raro con la IOCTL's, nunca me había pasado :S

Un saludo y reitero las felicidades, publicas unos artículos interesantes :D

neofito dijo...

Hola Hendrix

Muchas gracias por tus halagos, pero lo único que he hecho realmente ha sido centralizar y organizar un poco toda la documentación que existe.

Por otra parte me alegro mucho de que los artículos te resulten interesantes; espero poder seguir manteniendo ese interés.

Anónimo dijo...

Buenas neofito, me estoy introduciendo en la programación en modo kernel y la verdad es que lo explicas de una forma totalmente llana, si es verdad que a veces tengo que ir a mirar en msdn pero simplemente por falta de conocimientos anteriores. Gracias por el artículo y espero devorar más en cuanto los hagas ;)


Saludos!