Driver para ocultar procesos y archivos

publicado en: drivers | 0

Este artículo describe un controlador que oculta los procesos y archivos mediante el método de empalme.

Este artículo es la continuación de un conjunto de artículos sobre la ocultación y detección de archivos y procesos en el sistema operativo. Supongo que ya habrán leído los artículos Driver to Hide Processes and Files y Simple SST Unhooker.

El artículo Simple SST Unhooker representa el método para resolver el problema de la sustitución de la dirección de la función en la tabla SST. En este artículo, describiré los métodos de programa de enganche de funciones, lo que hará que la metodología descrita en el artículo Simple SST Unhooker no funcione.

Introducción a la base de empalme

En el artículo Driver to Hide Processes and Files , se escribe mucho sobre por qué necesitamos la interceptación de las funciones del sistema. En este artículo, quiero centrar su atención en la comparación de las tecnologías de interceptación, sus ventajas y desventajas.

Podemos dividir los métodos de interceptación en los siguientes grupos:

  • Sustitución de la dirección de la función real (modificación de las tablas IAT, modificación de las tablas SSDT/IDT);
  • Cambio directo de la función (empalme, interceptación en el modo de núcleo con la modificación del cuerpo de la función);
  • Sustitución directa de todo el componente de la aplicación/sistema (por ejemplo, sustitución de la biblioteca por una función de destino).

También podemos dividirlos por el modo de ejecución de la siguiente manera:

  • Usuario ( anillo3 ) métodos : modificación de tablas IAT, empalme. Su peculiaridad es que no se puede cambiar nada en el comportamiento del kernel del sistema operativo y sus extensiones;
  • Modo kernel : modificación de las tablas SSDT/IDT, interceptación en el modo kernel con la modificación del cuerpo de la función. Con la ayuda de esto, se puede modificar la estructura de datos y el código de cualquier parte del sistema operativo y de las aplicaciones.

Empalme

El empalme es un método de interceptación de las funciones de la API mediante el cambio del código de la función objetivo. Normalmente, se cambian los primeros 5 bytes de la función. En lugar de ellos, se inserta un salto a la función especificada por el desarrollador. Para que la operación se realice correctamente, la aplicación que intercepta la función debe permitir la ejecución del código que se cambió durante el empalme. Para ello, la aplicación guarda la parte de memoria que se está sustituyendo y, una vez que la función de interceptación termina su trabajo, la aplicación restaura la parte de función guardada y permite la ejecución completa de la función real.

Especificaciones de la tecnología

Esta tecnología depende de la plataforma y por eso necesita un control y una comprobación minuciosos del sistema para la correspondencia de las versiones. También necesita el control de la función para la correspondencia con el objetivo. Las funciones del sistema pueden cambiar cuando aparecen nuevos parches y actualizaciones de Windows (especialmente, el Service Pack para Windows) y también como resultado de las modificaciones de otras aplicaciones. Los errores al trabajar con esta tecnología pueden causar BSOD. Al mismo tiempo, esta tecnología ayuda a realizar la interceptación global de las funciones de la API e influye de tal manera en todos los procesos del sistema. Al enganchar el SSDT, llegamos a la función redefinida sólo si se llama a través de la tabla de llamadas del sistema. Cuando se utiliza el empalme, la función será llamada en cada llamada a la función original.

Alcance del empalme y métodos de detección

El empalme se utiliza en el software que debe realizar lo siguiente:

  • Funciones del sistema de vigilancia;
  • Mecanismo de ganchos en Windows;
  • Diferentes programas maliciosos. Esta es la principal tecnología de ocultación para los rootkits del nivel de usuario.

Estructura del proyecto

Para aclarar las ventajas y desventajas de esta tecnología, decidí no escribir el código desde el principio antes de la sustitución de la dirección en SST. Pero decidí hacer algunos cambios necesarios en el código del controlador fuente del artículo Driver to Hide Processes and Files para que funcionara evitando el SSTUnhooker y se basara en el método de empalme.

Los cambios se realizan sólo en el proyecto HideDriver y sólo en el PrcessHook . Esto permitirá comparar dos enfoques diferentes en un proyecto.

Lado técnico

Entonces, ¿cómo funciona el empalme desde el punto de vista del código? Para responder a esta pregunta, necesitamos refrescar nuestros conocimientos sobre los acuerdos de llamadas de subrutinas y sobre la pila.

Acuerdo de llamada y pila

El acuerdo de llamada define las siguientes peculiaridades del proceso de utilización de subrutinas:

  • Ubicación de los parámetros de entrada de la subrutina y los valores devueltos por ella. Las variantes más extendidas son las siguientes:
    • En registros;
    • En la pila;
    • En la memoria asignada dinámicamente.
  • Orden de transferencia de los parámetros. Cuando se utiliza la pila, define el orden en que los parámetros deben colocarse para apilarlos; cuando se utilizan registros, define el orden de comparación de los parámetros y los registros. Las variantes son las siguientes:
    • Los parámetros de orden directo se colocan en el mismo orden en que se enumeran en la descripción de la subrutina.
    • El orden inverso… los parámetros se pasan del final al principio. Simplifica la implementación de subrutinas con un número indefinido de parámetros de tipos aleatorios.
  • Lo que devuelve el puntero de la pila a la posición inicial:
    • Subrutina llamada – esto corta el número de comandos que se requieren para la llamada de la subrutina ya que los comandos de la recuperación del puntero de la pila se escriben una sola vez al final de la subrutina;
    • Programa de llamada – en este caso, la llamada se hace más complicada pero el uso de las subrutinas con el número y tipo de parámetros variables se hace más fácil.
  • ¿Qué comando usar para llamar la subrutina y qué comando usar para volver al programa principal? Por ejemplo, puede llamar a la subrutina mediante llamada cercana, llamada lejana y pushf/call lejana (para el retorno, use retn, retf, iret, correspondientemente) en el modo estándar x86.
  • El contenido de los registros del procesador que la subrutina debe restaurar antes de la devolución.

Los acuerdos de llamada dependen de la arquitectura de la máquina objetivo y del compilador.

En nuestro caso, realizamos el empalme del API. Como se sabe, es del tipo __stdcall. Usando este tipo, los argumentos se pasan a través de la pila en orden de derecha a izquierda. La subrutina llamada realiza la limpieza de la pila.

La pila se parece a lo siguiente cuando estamos al principio de la función si tiene el tipo de llamada stdcall:

i1

La pila se cambia por la instrucción de llamada del ensamblador. Transfiere el control a la subrutina y escribe la dirección de retorno a la pila. La instrucción ret devuelve el control al lado de la llamada y toma la dirección de retorno de la parte superior de la pila.

Revisión del código

Ahora, podemos preparar el modelo que funcionará de la siguiente manera.

i2

Puede ver la aplicación de este esquema en el ejemplo de nuestro conductor.

Primero, tenemos que declarar a los propios manipuladores. Para ello, necesitamos usar las siguientes definiciones:

                #define DEFINE_POST_HANDLER(Name, ImplName,ParamsData) N - vacío estático __stdcall ImplName(unsigned long * pRetValue, ParamsData * pData, Nunknown long dwOldEax); vacío estático __declspec(naked) void __stdcall Name()  ~ -asm pushad  ~ -asm push eax  ~ -asm mov eax, [esp+0x28]  ~ -asm push eax  ~ -asm lea eax, [esp+0x28]  ~ -asm push eax  ~ -asm call ImplName  ~ -asm popad  ~ -asm ret 4 } //------------------------------------------------------------------------------------------------- #define DEFINE_FORE_HANDLER(Name, ImplName) N - vacío estático __stdcall ImplName(sin firmar largo * pFirstData); vacío estático __declspec(naked) void __stdcall Name()  ~ -asm pushad  ~ -asm lea eax, [esp+0x28]  ~ -asm push eax  ~ -asm call ImplName  ~ -asm popad  ~ -asm ret }

Como podemos ver, aquí se declaran los comprobantes de la llamada de los manejadores y, al mismo tiempo, todos los registros se guardan en el estado por defecto.

Ahora, veamos la función que toma el control al principio de la ejecución de la función original:

                vacío estático __stdcall NtQuerySystemInfo_HookHandlerImpl(sin signo largo * pContexto){sin signo largo * pRetPoint = GET_RET_POINT_PTR(pContexto);struct NtQuerySystemInfo_HookHandler_Data * pParamsData = 0;void * pStub = 0;if (AllocatePostParams(&g_kernelNonPagedAllocator, sizeof(struct NtQuerySystemInfo_HookHandler_Data),NtQuerySystemInfo_HookHandler_PostHandler,(void**)&pParamsData, &pParamsData- >SystemInformationLength = GET_PARAM(pContext, 2);pParamsData- ]ReturnLength = GET_PARAM_PTR(pContext, 3);// establecer nuevo punto de retorno*pRetPoint = (largo sin signo)pStub;}

Esta función asigna memoria para los parámetros y crea un talón para la llamada POST_HANDLER. A continuación, guardamos la dirección de retorno original de la pila, establecemos la nueva dirección de retorno apuntando a nuestro manejador post-función y saltamos de nuevo a la función original. Después de su ejecución, llama a la instrucción ret. La última toma la dirección de la pila y hay un valor guardado de antemano para nuestro POST_HANDLER. Luego, llegamos a él y tenemos la pila con los parámetros originales y las direcciones de los parámetros de salida.

                vacío estático __stdcall NtQuerySystemInfo_HookHandler_PostHandlerImpl(unsigned long * pRetValue, struct NtQuerySystemInfo_HookHandler_Data * pData, unsigned long dwOldEax){*pRetValue = pData-{[dwRealIP]};if ( (pData-{[SystemInformationClass])= SystemProcessesAndThreadsInformation) || (dwOldEax != 0) ){MemoriaLibre(&g_núcleoNoAsignadorDePáginas,pDatos);return;}OcultarAlgoritmo::NtQuerySysInfoParams params = {pData-{ClaseDeInformaciónDelSistema,pData-{{{[#]}]}SuprimirAlgoritmo::HideProcess(params,gProcessChecker);FreeMemory(&g_kernelNonPagedAllocator, pData);}

Como podemos ver, la restauración de la dirección de retorno y la limpieza de la memoria asignada por FORE_HANDLER son las tareas de esta función. Aquí se realiza toda la lógica sobre el corte de los procesos requeridos de la lista de procesos. Me gustaría llamar su atención sobre el hecho de que el corte se realiza por el mismo algoritmo que en el artículo original de procesos ocultos.

La ejecución de la función termina en ella.

Método de empalme

En esta parte del artículo, estudiaremos el método de empalme de la función original.

Primero, necesitamos asignar memoria y escribir el código en ella, que será llamado por nuestros manejadores. Se realiza de la siguiente manera:

                int __stdcall InitializeHookEx(MemoryRWManager * pRwManager, unsigned char * pHook, unsigned char * pHandler){int status = 0;char buffer[CALL_SIZE];// Escribir la instrucción CALL en el buffer[0] = 0xE8; // 1 byte para CALL opcode*(size_t*)(buffer + 1) = (unsigned char *)pHandler - (pHook + CALL_SIZE); // 4 bytes para la dirección relativa// Copiar la instrucción CALL desde el buffer a la memorystatus del gancho = WriteMemoryWithWriter(pRwManager->pWriter, (char*)pHook, buffer, CALL_SIZE, 0);if (status)return STATUS_UNSUCCESSFUL;return STATUS_SUCCESS;}

Por supuesto, al haber asignado mem

Deja un comentario

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