Detectar al conductor…

publicado en: tutoriales | 0

Este artículo es la continuación del proyecto previamente publicado Hide Driver. Al igual que el primer artículo, este no pretende ser completo y original. El propósito principal es representar la información complicada de una manera más popular.
El método de ocultamiento descrito en el artículo anterior es muy simple y ampliamente conocido. Ahora pretendo describir el método de detección de tales archivos y procesos ocultos de manera simple y fácil de entender. Este método está acompañado por el código desarrollado para ilustrar las palabras.

Proyectos utilizados

Me gustaría agradecer a las personas que desarrollaron los proyectos que se enumeran a continuación – ellos hicieron que la aplicación de este proyecto sea más fácil:

  • BOOST
    • Unidad de pruebas Freamwork
    • Bind & Función
    • no copiable
  • STLPort
  • Wildmat
  • CppLib
  • Refuerzo del depurador del núcleo de Windows para máquinas virtuales

Niveles de ocultación

Así que empecemos. En primer lugar debemos considerar dónde se puede realizar la interceptación y cómo se puede evitar.

En este ejemplo describí los niveles de ocultación de archivos, pero también es válido para la ocultación de procesos, con una pequeña corrección.

Levels of hiding files on Windows NT

El método descrito en este artículo funciona a nivel de SSDT. Es uno de los niveles más sencillos para hacer algunos cambios (objetos ocultos particulados) y por eso es natural considerarlo primero.

Obsérvese que para detectar una información oculta debemos trabajar a un nivel inferior al nivel en el que se realiza la ocultación.

Método de detección

El método de ocultación que utiliza el SSDT se basa en el hecho de que todas las llamadas del sistema pasan por esta tabla (ver más detalles en el artículo Hide Driver). Para evitar la interceptación a nivel de SSDT debemos entender cómo se utiliza esta tabla.

Tabla de descripción de servicios del sistema

Esta tabla almacena indicadores de las funciones del sistema. Esta tabla se utiliza para encontrar una función por índice.

Ejemplo. Usa WinDBG para ver lo que el SSDT está almacenando.

kd²; dds KiServiceTable L100
80501030 8059849a nt!NtAcceptConnectPort
80501034 805e5666 nt!NtAccessCheck
...
805010c4 8056d14c nt!NtCrearArchivo
...

Nota que todos los punteros apuntan a las funciones nt!NtXXX.

Por lo tanto, para evitar la sustitución de la dirección del SSDT debemos obtener la dirección original de la función nt!NtXXX, por ejemplo mediante la función MmGetSystemRoutineAddress().

Llamadas originales de Hadler

Pero antes de empezar con las llamadas de las funciones originales, consideremos cómo funcionan.

Si no está familiarizado con el desarrollo de controladores para Windows, le interesará saber que hay funciones en el modo de núcleo que sólo se diferencian por el prefijo, Zw o Nt.

MSDN dice: Si la llamada ocurre en modo de usuario, debes usar el nombre «NtCreateFile» en lugar de «ZwCreateFile».

Diferencia entre las funciones de ZwXXX y NtXXX.

Para encontrar la diferencia podemos usar Windbg.

To find difference we can use Windbg.

La información más importante está marcada en el ejemplo. La llamada en el primer caso se traduce a la función nt!KiSystemService, y 25h es el índice de esta función en la tabla SSDT para el sistema de prueba. Así podemos concluir que la función ZwXXX llama a la función NtXXX usando SSDT.

Procediendo de esta manera podemos descubrir que la única diferencia entre las funciones de Nt y Zw es la modificación del parámetro Modo Anterior.

Esquemáticamente la función ZwXXX puede ser representada como sigue:

…pre..;
ZwXXX(){KPROCESSOR_MODE prevMode = KeSetPreviousMode(Kernel);NtXXX();KeSetPreviousMode(prevMode);}
…y el de la empresa..;

Significa que para llamar a la función original no sólo debemos saber su dirección sino también establecer de antemano el modo previo = modo del núcleo.

Tenga en cuenta que el modo anterior afecta al nivel de los privilegios. Los mangos del núcleo no son accesibles para el hilo con el Modo anterior igual al modo de usuario.

Evitar el modo ExGetPreviousMode()

Para evitar la comprobación del Modo Anterior utilizamos el hecho de que todos los hilos del sistema tienen el Modo Anterior igual al modo del núcleo.

Hay dos métodos para iniciar algún código en el contexto del sistema: Elementos de trabajo e hilos del sistema. Yo uso ambos. Work Items es mejor para la detección de procesos ocultos porque es más rápido, y por lo tanto las acciones correspondientes toman un tiempo muy corto. Para la detección de archivos ocultos es mejor usar Hilos de Sistema porque la detección puede tomar mucho tiempo.

Utilidad para el inicio de la función en el contexto del sistema

Estas utilidades inician la función dada en el contexto del sistema utilizando Work Items (ver la función IoQueueWorkItem()). Tan pronto como se utiliza С++ y las excepciones son la parte esencial de la misma, estas utilidades también transmiten std::exception y las heredadas de la misma, que fueron lanzadas desde Work Item, hasta el punto en que la función fue llamada desde. Los parámetros de entrada son el puntero de la función y los parámetros que deben ser transmitidos a esta función.

[Código del archivo srcdrvUtilsWorkItemUtils.h]

…pre..;
#pragma oncenamespace utils{void CallFromWorkerThread(PVOID routine,PVOID params);template<class ParamsType>inline voidCallFromWorkerThreadEx(void (*routine)(ParamsType*),ParamsType* params){CallFromWorkerThreadEx((PVOID)routine,(PVOID)params);}}
…y la rutina..;

La función CallFromWorkerThreadEx es una función auxiliar, su tarea es comprobar si el puntero de la función transmitida como primer parámetro acepta los parámetros transmitidos con el segundo parámetro.

A continuación se indica la aplicación de esas funciones.

[Código del archivo srcdrvUtilsWorkItemUtils.сpp]

…pre..;
#incluir "drvCommon.h"#incluir "WorkItemUtils.h"#incluir "KernelEvent.h"#incluir <string>#incluir <stdexcept>//El objeto asociado con el driverextern PDEVICE_OBJECT gDeviceObject;namespace utils{typedef void (*OriginalRoutine)(PVOID);struct WorkItemParams{OriginalRoutine Routine;PVOID params;char* strError;size_t strErrorSize;KernelNotificationEvent* finishEvent;NTSTATUS status;};VOID WorkItemRoutine(IN PDEVICE_OBJECT DeviceObject,IN PVOID Context){WorkItemParams* routineParams = (WorkItemParams*)Context;routineParams->status = STATUS_SUCCESS;try{routineParams->Rutina(routineParams->params);}catch(std::exception& ex){routineParams->status = STATUS_UNSUCCESSFUL;std::string str(ex.what());size_t toWrite = min(str.size(),routineParams->strErrorSize – 1);memcpy(routineParams->strError,str.c_str(),toWrite);routineParams->strError[toWrite] = '';}routineParams->finishEvent->set();}void CallFromWorkerThread(PVOID routine,PVOID params){PIO_WORKITEM workItem = ::IoAllocateWorkItem(gDeviceObject);if(workItem == NULL)throw std::exception("Can't create work item.");utils::WorkItemGuard guard(workItem);char errorMsg[255];KernelNotificationEvent finishEvent;WorkItemParams wiParams = {(OriginalRoutine)routine,params,errorMsg,sizeof(errorMsg),&finishEvent};IoQueueWorkItem(workItem,&WorkItemRoutine,DelayedWorkQueue,&wiParams);finishEvent.wait();if( !NT_SUCCESS(wiParams.status) ){throw std::runtime_error(errorMsg);}}}
…y que no se puede hacer nada..;

Detección de procesos ocultos

La detección de los procesos ocultos en el nivel SSDT se realiza comparando los resultados obtenidos de la función original y la función cuya dirección se indica en la tabla SSDT.

En general, podemos dividir el proceso de detección de procesos ocultos en tres etapas:

  1. Recibiendo el puntero de la función.
  2. Llamada de función en el contexto del sistema.
  3. Análisis de los datos obtenidos de las dos funciones.

Recibiendo el puntero de la función

Recibir el puntero de la función de la tabla SSDT por su nombre está implementado en el código que se muestra a continuación. Debe realizarse porque la función puede tener diferentes índices en la tabla SSDT en diferentes versiones de SO.

Para minimizar el volumen de código mostrado, se omite el contenido de los archivos ServiceTableDef.h y VersionDependOffsets.h .

[Código del archivo srcdrvUtilsServiceTableUtils.h]

                #incluye "ServiceTableDef.h "#incluye "VersionDependOffsets.h "namespace utils{const UCHAR opcode_MovEax = 0xB8; // __asm move eax,Valueinline ULONG GetFunctionSSTIndex(PUNICODE_STRING function_name){/*Todas las funciones ZwXXX exportadas por NTOSKRNL.exe empieza con :mov eax, ULONG donde ULONG es el índice de la función NtXXX en SST*/// Tenga cuidado con la función MmGetSystemRoutineAddress, para// Windows XP(SP2) y baje esta función lanza la excepción SEH si// un nombre de rutina de sistema inválido le fue pasado.// El uso de __intentar/__excepción de bloqueo no es una buena solución ya que SEH// no es un contrato formal para esta API, por lo que no hay garantía// de que el sistema operativo siga en un estado estable después de que se haya capturado// la excepción.PVOID pTrueFuncPtr_ZW = MmGetSystemRoutineAddress(function_name);if(pTrueFuncPtr_ZW == NULL)throw std::exception(__FUNCTION__" No se puede obtener la dirección de la función.");// Comprobar el byte de comando para ver si el opcode// Comienza desde Vista DriverVerifier puede sustituir el código ntoskrnl.if( *( (PUCHAR)pTrueFuncPtr_ZW ) != opcode_MovEax )throw std::exception(__FUNCTION__" La dirección de la función apunta al código supuestamente.");// Saltar el byte de comando, mover al byte de índiceULONG funcIndex = *(PULONG)((PUCHAR) pTrueFuncPtr_ZW + 1);if( funcIndex == NULL)throw std::exception(__FUNCTION__" No se puede obtener la función índice SST");return funcIndex;}inline PVOID GetFunctionSSTPtr(ULONG fncIndex){PNTPROC ServiceTable=pNtoskrnl- >ServiceTable;ULONG TotalCount = pNtoskrnl-;ServiceLimit;if( fncIndex > TotalCount )throw std::exception(__FUNCTION__" Wrong SST index");return ServiceTable[fncIndex];}inline PVOID GetFunctionSSTPtr(PUNICODE_STRING function_name){return GetFunctionSSTPtr( GetFunctionSSTIndex(function_name) );}en línea PVOID GetFunctionSSTPtrEx(PUNICODE_STRING nombre_de_función){ULONG fncIndex;try{fncIndex = utils::GetFunctionSSTIndex(function_name);}catch(const std::exception&){/// Use offsetsfncIndex predefinido = utils::GetSSTOffsetByName(function_name- >Buffer,function_name- >Length/2);}return utils::GetFunctionSSTPtr(fncIndex);}}

Obtención de datos mediante llamadas de función

Según el algoritmo de detección de procesos ocultos, necesitamos llamar a las funciones Zw y Nt y comparar los resultados. Puedes ver estas funciones

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *