Cómo crear un motor antivirus

Envío

Calificación de Usuario4, 54 (13 votos)

Introducción

Al recorrer los foros de techies, a menudo veo a algunas personas (y muchas no muy experimentadas) preguntando «Cómo hago un antivirus», a veces con lenguajes no muy adaptados (bat, PHP, PHP) y teniendo una idea equivocada de lo que es un antivirus y cómo debe construirse.

También he visto muchos «softwares Antivirus» hechos por niños, con muy pocas personas que aún están en la escuela y alrededor de 4 horas al día de codificación en varias semanas. No estoy diciendo que los niños no son expertos, pero estoy diciendo que la construcción de un motor antivirus necesita mucha gente calificada con trabajo a tiempo completo más mucho tiempo para lanzar un software decente o mucho dinero para pagarles 🙂 (en caso de que no sean voluntarios).

Por lo tanto, cubriré aquí las pautas para una codificación antivirus básica, para Windows y en C/C++. Uno puede encontrar aquí los indicadores para diseñar un motor antivirus, o simplemente aprender cómo se construyen la mayoría de ellos.

Protección

Para una buena protección, un antivirus debe tener al menos un controlador, para poder ejecutar código en el núcleo y, en general, tener acceso a las API del núcleo. Comenzando con Vista, Microsoft entendió que la industria de los antivirus necesitaba claves para ingresar al núcleo y activar filtros en lugares estratégicos, como el sistema de archivos, el registro y la red. No se sorprenda si construir un antivirus para sistemas pre-Vista puede ser un verdadero dolor, porque no fue diseñado para esto.

  • Sin embargo, en los sistemas Pre-Vista, las compañías de antivirus solían usar funciones similares a rootkit para proteger las puertas (incluso si Microsoft no lo recomendaba en absoluto) y poder proteger su sistema. Usaron lo que llamamos «Hooks» (desvíos de API para fines de filtrado).
  • En Vista+, Microsoft proporcionó API para insertar nuestro controlador de bajo nivel entre las llamadas de usuario y las API del núcleo. De esta manera, es fácil registrar un producto antivirus en el núcleo. Además, ese tipo de sistema basado en el registro nos permite enviar la seguridad de nuestro sistema en capas, donde pueden cohabitar varios productos con diferentes objetivos. Este no era el caso de los ganchos, ya que la implementación dependía totalmente del producto.

NOTA: No cubriré las soluciones alternativas con ganchos para sistemas pre-Vista, porque es fácil de encontrar en Internet, y porque necesitaría un capítulo completo para explicar cómo engancharse, dónde engancharse, etc., Pero debe saber que es la misma idea que las API del núcleo, excepto que debe implementar lo que Microsoft proporcionó en los sistemas Vista+.

Para aprender acerca de la codificación de los conductores, se puede comprobar que los enlaces útiles:
http://msdn.microsoft.com/en-us/library/windows/hardware/gg490655.aspx
http://www.codeproject.com/Articles/9504/Driver-Development-Part-1-Introduction-to-Drivers

Para aprender acerca de los ganchos, se puede comprobar que el ejemplo básico:
http://www.unknowncheats.me/forum/c-and-c/59147-writing-drivers-perform-kernel-level-ssdt-hooking.html

Proceso

Lo primero que debe proteger al usuario es el lanzamiento de procesos maliciosos. Esto es lo básico. El antivirus debe registrar una devolución de llamada PsSetCreateProcessNotifyRoutineEx. Al hacer esto, en cada creación de proceso, y antes de que el hilo principal comience a ejecutarse (y cause cosas maliciosas), se notifica la devolución de llamada del antivirus y recibe toda la información necesaria.

Recibe el nombre del proceso, el objeto de archivo, el PID, etc. Como el proceso está pendiente, el controlador puede decirle a su servicio que analice la memoria del proceso en busca de cualquier cosa maliciosa. Si encuentra algo, el conductor simplemente establecerá CreationStatus en FALSO y regresará.

NTSTATUS PsSetCreateProcessNotifyRoutineEx( _In_ PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine, _In_ BOOLEAN Remove);
VOID CreateProcessNotifyEx( _Inout_ PEPROCESS Process, _In_ HANDLE ProcessId, _In_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo);
typedef struct _PS_CREATE_NOTIFY_INFO { SIZE_T Size; union { ULONG Flags; struct { ULONG FileOpenNameAvailable :1; ULONG Reserved :31; }; }; HANDLE ParentProcessId; CLIENT_ID CreatingThreadId; struct _FILE_OBJECT *FileObject; PCUNICODE_STRING ImageFileName; PCUNICODE_STRING CommandLine; NTSTATUS CreationStatus;} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;

Hilos

En la misma idea que para los procesos, los hilos pueden ser una forma para que las cosas maliciosas causen daños. Por ejemplo, se puede inyectar código en un proceso legítimo e iniciar un subproceso remoto en ese código dentro del contexto del proceso (¿fácil de seguir? 🙂 ). De esa manera, un proceso legítimo puede hacer cosas maliciosas.

Podemos filtrar nuevos hilos con la devolución de llamada PsSetCreateThreadNotifyRoutine. Cada vez que se crea un hilo, se notifica al antivirus con el TID y el PID. Por lo tanto, es capaz de buscar en el código de dirección de inicio del subproceso, analizarlo y detener el subproceso o reanudarlo.

NTSTATUS PsSetCreateThreadNotifyRoutine( _In_ PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine);

VOID(*PCREATE_THREAD_NOTIFY_ROUTINE) ( IN HANDLE ProcessId, IN HANDLE ThreadId, IN BOOLEAN Create );

Imágenes

La tercera amenaza dinámica se refiere a las imágenes que se pueden cargar en la memoria. Una imagen es un archivo PE, ya sea un archivo EXE, un archivo DLL o un archivo SYS. Para recibir notificaciones de imágenes cargadas, simplemente registre PsSetLoadImageNotifyRoutine. Esa devolución de llamada nos permite ser notificados cuando la imagen se carga en la memoria virtual, incluso si nunca se ejecuta. Entonces podemos detectar cuando un proceso intenta cargar una DLL, cargar un controlador o iniciar un nuevo proceso.

La devolución de llamada obtiene información sobre la ruta completa de la imagen (útil para el análisis estático), y lo más importante en mi opinión, la dirección base de la imagen (para el análisis en memoria). Si la imagen es maliciosa, el antivirus puede usar pequeños trucos para evitar la ejecución, como analizar la imagen en memoria e ir al punto de entrada, luego llamar al código operativo de ensamblaje «ret» para anularlo.

NTSTATUS PsSetLoadImageNotifyRoutine( _In_ PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine);
VOID (*PLOAD_IMAGE_NOTIFY_ROUTINE)( __in_opt PUNICODE_STRING FullImageName, __in HANDLE ProcessId, __in PIMAGE_INFO ImageInfo );
typedef struct _IMAGE_INFO { union { ULONG Properties; struct { ULONG ImageAddressingMode : 8; //code addressing mode ULONG SystemModeImage : 1; //system mode image ULONG ImageMappedToAllPids : 1; //mapped in all processes ULONG Reserved : 22; }; }; PVOID ImageBase; ULONG ImageSelector; ULONG ImageSize; ULONG ImageSectionNumber;} IMAGE_INFO, *PIMAGE_INFO;

Sistema de archivos

Una vez que cada cosa dinámica está protegida, un antivirus debería poder notificar al usuario sobre la marcha las cosas maliciosas, no solo cuando están a punto de comenzar. Un antivirus debería poder escanear archivos cuando el usuario abra una carpeta, un archivo o cuando se descarguen en el disco. Además, un antivirus debería poder protegerse a sí mismo, prohibiendo a cualquier programa eliminar sus archivos.

La forma de hacer todo esto, es instalar un controlador en el sistema de archivos, y más específicamente un minifiltro de un filtro heredado (forma antigua). Aquí hablaremos sobre el minifiltro.

Un minifiltro es un tipo específico de controlador, capaz de registrar devoluciones de llamada en cada operación de lectura/escritura realizada en el sistema de archivos (funciones principales IRP). Un IRP (Paquete de solicitud de interrupción) es un objeto utilizado para describir una operación de Lectura / Escritura en el disco, que se transmite junto con la pila de controladores. El minifiltro simplemente se insertará en esa pila y recibirá ese IRP para decidir qué hacer con él (permitir/denegar la operación).

Para ver un pequeño ejemplo de minifiltro, consulte ese enlace útil o ese. Las directrices de Microsoft están aquí.

También encontrará 2 ejemplos de la documentación de WDK aquí y aquí.

Una devolución de llamada de minifiltro básica se ve así. Hay 2 tipos de devolución de llamada, operación previa y operación posterior, que pueden filtrar antes o después de la consulta. Aquí hay un pseudo código de preoperaciónname:

FLT_PREOP_CALLBACK_STATUS PreOperationCallback (__inout PFLT_CALLBACK_DATA Data, __in PCFLT_RELATED_OBJECTS FltObjects, __deref_out_opt PVOID *CompletionContext){ ... if ( all_good ) { return FLT_PREOP_SUCCESS_NO_CALLBACK; } else { // Access denied Data->IoStatus.Information = 0; Data->IoStatus.Status = STATUS_ACCESS_DENIED; return FLT_PREOP_COMPLETE; } }

Registro

El registro es uno de los lugares más críticos para vigilar. Hay muchas maneras para que un malware mantenga la mano persistente en el sistema registrando una sola (o pocas) claves / valores en el registro. Los lugares más conocidos son las llaves de carrera y los Servicios. Este es también el lugar donde el antivirus puede ser derrotado (junto con el sistema de archivos), simplemente eliminando sus claves de controlador/servicio, para que ya no se reinicie al arrancar el sistema.

Esto no es una necesidad real para un antivirus para proteger los lugares de reinicio, la mayoría de ellos no, pero deben proteger sus claves de registro de instalación, para evitar ser derrotados fácilmente por malwares. Esto se puede hacer registrando CmRegisterCallback.

La devolución de llamada proporciona suficiente información para obtener el nombre completo de la clave, el tipo de acceso (Crear, Renombrar, Eliminar, Delete ) y el PID de la persona que llama. De esta manera, es fácil otorgar acceso o no a la llamada, configurando el campo de estado de la devolución de llamada Posterior a la operación.

NTSTATUS CmRegisterCallbackEx(_In_ PEX_CALLBACK_FUNCTION Function,_In_ PCUNICODE_STRING Altitude,_In_ PVOID Driver,_In_opt_ PVOID Context,_Out_ PLARGE_INTEGER Cookie,_Reserved_ PVOID Reserved);
EX_CALLBACK_FUNCTION RegistryCallback;NTSTATUS RegistryCallback( _In_ PVOID CallbackContext, _In_opt_ PVOID Argument1, _In_opt_ PVOID Argument2){ ... }Argument1 = typedef enum _REG_NOTIFY_CLASS { RegNtDeleteKey, RegNtPreDeleteKey = RegNtDeleteKey,...Argument2 = typedef struct _REG_POST_OPERATION_INFORMATION { PVOID Object; NTSTATUS Status; PVOID PreInformation; NTSTATUS ReturnStatus; PVOID CallContext; PVOID ObjectContext; PVOID Reserved;} REG_POST_OPERATION_INFORMATION, *PREG_POST_OPERATION_INFORMATION;

Red (Firewall)

Para proteger las puertas de todo el tráfico de Internet, que puede ser enorme en ciertos sistemas (servidores, usuarios de ancho de banda enorme) sin ser ralentizado por el cambio de contexto que tiene lugar en el país de usuario, no se recomienda en absoluto instalar un firewall que no tenga un controlador subyacente, a excepción de algunos filtros de navegador web que pueden ser suficientes para el tráfico http, pero que no protegerán contra la comunicación de malware dentro/fuera.

Para tener una implementación correcta del firewall, se debe codificar un NDIS, TDI u otro método para un controlador de filtrado de IP de bajo nivel. NDIS / TDI es un poco complicado de hacer, y requeriría mucho conocimiento (más que otros filtros en mi opinión).

De todos modos, aquí hay algunos consejos para comenzar a codificar un controlador de este tipo, las directrices de Microsoft y el antiguo tutorial de codeproject (pero aún es bueno para leer), un ejemplo de firewall NDIS y un ejemplo de firewall TDI. Aquí también hay una buena escritura sobre el truco de bypass de firewall NDIS, y una pequeña explicación sobre la pila de controladores de red,

Protección del territorio de usuario

La protección del territorio de usuario no es una necesidad, pero puede ser un módulo adicional contra Banqueros troyanos, y más específicamente contra espías de procesos. Por lo general, se inyectan en cada proceso, por varias razones.

En primer lugar, pueden (bajo demanda) eliminar el proceso si se ha identificado como malware (esto no debería suceder, porque se supone que los AV deben detenerlo antes de que se inicie). Siempre es más fácil detener un proceso cuando estás en su contexto.

En segundo lugar, son capaces de proteger procesos críticos, como los navegadores web, contra el enganche de malwares capaces de desviar y filtrar llamadas de API para recopilar contraseñas, información bancaria y redirigir el flujo de Internet a servidores de malware. Solo observan la modificación de IAT, el empalme, y también pueden establecer ganchos para evitar LoadLibray de una DLL de malware, y así prohibir ciertos métodos de inyección de código.

Una forma fácil de inyectar una DLL de protector en todos los procesos es usar la clave de registro AppInitDll para registrar la DLL de protector. Cargará la DLL en cada proceso iniciado en el sistema, tan pronto como vinculen al User32.imagen dll (la mayoría de ellos lo hacen).

Motor de análisis

El motor de análisis es una de las partes más importantes, es responsable de analizar muestras de archivos/memoria procedentes de los controladores. If debe ser rápido (incluso con una base de datos enorme), y debe ser capaz de manejar la mayoría de los tipos de archivos (ejecutables autoextraídos, Archivos-RAR, ZIP, Archivos empaquetados-UPX,)) y, por lo tanto, debe tener muchos módulos para hacer esto:

  • Desempaquetar: Ese módulo debe ser capaz de detectar y desempaquetar la mayoría de los empaquetadores conocidos (UPX, Armadillo, ASPack, ASP)
  • Motor de firmas: La base de datos de un antivirus contiene millones de firmas, y el motor debe ser capaz de buscarlas rápidamente en una muestra. Por lo tanto, un algoritmo muy poderoso debería ser parte de él. Algunos ejemplos : AhoCorasick, RabinKarp, algoritmos de coincidencia de cadenas.
  • Sandbox: Ese módulo no es necesario, pero sería una ventaja poder ejecutar muestras en una memoria limitada, sin ningún efecto en el sistema. Esto podría ayudar a desempaquetar muestras con empaquetadores desconocidos y ayudar al motor heurístico (ver después) a detectar llamadas a la API que podrían considerarse sospechosas o maliciosas. Un buen arenero aquí.
  • Motor heurístico: Como se dijo anteriormente, el motor heurístico no busca firmas, sino que busca comportamientos sospechosos(p. ej. ejemplo que abre una conexión en el sitio web hxxp: / / malware_besite. com). Esto se puede hacer mediante análisis estático o a través de una caja de arena.

Sintaxis de firma

La sintaxis de firma es el «diccionario» del idioma que entiende el motor de firmas. Es una forma de formalizar cuál es el patrón para encontrar, cómo buscarlo y dónde buscarlo en la muestra. La sintaxis tiene que ser lo suficientemente simple para que los investigadores la entiendan, lo suficientemente potente para manejar cada caso de uso y fácil de analizar para un mejor rendimiento del motor.

VirusTotal ha desarrollado una buena sintaxis y motor (proyecto Yara), que es de código abierto. Ese debería ser un buen puntero para crear tu propia sintaxis, o simplemente usarla. Aquí también hay un buen blog sobre cómo crear firmas para antivirus.

Ejemplo de firma:

rule silent_banker : banker{ meta: description = "This is just an example" thread_level = 3 in_the_wild = true strings: $a = {6A 40 68 00 30 00 00 6A 14 8D 91} $b = {8D 4D B0 2B C1 83 C0 27 99 6A 4E 59 F7 F9} $c = "UVODFRYSIHLNWPEJXQZAKCBGMT" condition: $a or $b or $c}

Autoprotección

La autoprotección es muy importante para un antivirus, para evitar ser derrotado por un malware y seguir protegiendo al usuario. Esta es la razón por la que un antivirus debe ser capaz de proteger su propia instalación y mantener la persistencia en el reinicio.

Hay varios lugares para proteger: Archivos, Claves de registro, Procesos / Subprocesos, Memoria.

  • La protección de archivos está implementada en el minifiltro, con reglas particulares sobre los archivos del antivirus (Sin acceso en eliminación, cambio de nombre, movimiento, escritura).
  • La protección del registro se convierte en el filtro del registro, con acceso denegado para las claves de registro del controlador y el servicio.
  • Los hilos de controladores están protegidos, porque es bastante imposible descargar el módulo del núcleo sin que se bloquee el sistema
  • Para poder proteger el servicio, que es un proceso de usuario, 2 soluciones:

  1. Lo más fácil sería agregar reglas para errores en el administrador de servicios y establecer cada regla de error en «reiniciar el servicio». De esta manera, cuando el administrador de servicios no detiene el servicio, se reinicia. Por supuesto, el servicio no debería poder aceptar comandos hasta que el sistema no se reinicie (o detenga).
491838-428-487-263x300
  1. El segundo método, que es más genérico, sería establecer devoluciones de llamada en los controladores de proceso con ObRegisterCallbacks.

Al establecer el ObjectType en PsProcessType y la Operación en OB_OPERATION_HANDLE_CREATE, recibe una devolución de llamada Previa y posterior a la operación, y puede devolver ACCESS_DENIED a ReturnStatus si el controlador de proceso consultado tiene Acceso otorgado que tiene derechos de terminación de proceso (o derechos de escritura de proceso, o cualquier cosa que pueda provocar un bloqueo/muerte), y por supuesto si el proceso es uno de los que el antivirus necesita proteger (su servicio, por ejemplo).

Por supuesto, también es necesario proteger el mango duplicado y el tipo de hilo Psthread para evitar cualquier método de terminación que requiera agarrar un mango en el proceso o un hilo. Aquí hay un pequeño ejemplo de uso de esa devolución de llamada.

NTSTATUS ObRegisterCallbacks( _In_ POB_CALLBACK_REGISTRATION CallBackRegistration, _Out_ PVOID *RegistrationHandle);
typedef struct _OB_CALLBACK_REGISTRATION { USHORT Version; USHORT OperationRegistrationCount; UNICODE_STRING Altitude; PVOID RegistrationContext; OB_OPERATION_REGISTRATION *OperationRegistration;} OB_CALLBACK_REGISTRATION, *POB_CALLBACK_REGISTRATION;
typedef struct _OB_OPERATION_REGISTRATION { POBJECT_TYPE *ObjectType; OB_OPERATION Operations; POB_PRE_OPERATION_CALLBACK PreOperation; POB_POST_OPERATION_CALLBACK PostOperation;} OB_OPERATION_REGISTRATION, *POB_OPERATION_REGISTRATION;
VOID ObjectPostCallback( _In_ PVOID RegistrationContext, _In_ POB_POST_OPERATION_INFORMATION OperationInformation);
typedef struct _OB_POST_OPERATION_INFORMATION { OB_OPERATION Operation; union { ULONG Flags; struct { ULONG KernelHandle :1; ULONG Reserved :31; }; }; PVOID Object; POBJECT_TYPE ObjectType; PVOID CallContext; NTSTATUS ReturnStatus; POB_POST_OPERATION_PARAMETERS Parameters;} OB_POST_OPERATION_INFORMATION, *POB_POST_OPERATION_INFORMATION;
typedef union _OB_POST_OPERATION_PARAMETERS { OB_POST_CREATE_HANDLE_INFORMATION CreateHandleInformation; OB_POST_DUPLICATE_HANDLE_INFORMATION DuplicateHandleInformation;} OB_POST_OPERATION_PARAMETERS, *POB_POST_OPERATION_PARAMETERS;

typedef struct _OB_POST_CREATE_HANDLE_INFORMATION { ACCESS_MASK GrantedAccess;} OB_POST_CREATE_HANDLE_INFORMATION, *POB_POST_CREATE_HANDLE_INFORMATION;

GUI (Interfaz Gráfica de Usuario)

Esta es la parte visible del iceberg. En mi opinión, una (tal vez LA) parte más importante si quieres vender tu producto. A los usuarios les encanta lo que es hermoso, fácil de usar e intuitivo. Incluso si no es 100% eficiente. La interfaz gráfica debe ser sexy.

La GUI es solo un shell vacío, hace solo tratamientos gráficos y envía / recibe comandos al núcleo (el servicio). También muestra barras de progreso, lo que se está analizando, proporciona configuración y, por lo tanto, aquí está la interfaz de usuario de Avast. Sexy, ¿verdad? 🙂

avast

Arquitectura

La arquitectura global podría ser algo que se vea así:

  1. GUI: Sin derechos administrativos ,DÉBIL
  2. DLL(s) de guardia : protección del navegador web, MEDIO
  3. Servicio : Derechos de administrador. Sirve como puerta de enlace al código del núcleo y toma decisiones junto con alguna base de datos, STRONG
  4. Controladores : Filtros del núcleo, STRONG

La interfaz gráfica de usuario no necesita ningún derecho administrativo, solo toma acciones del usuario y las transmite al servicio. También muestra el estado del producto. Nada más, este no es su objetivo. Si la interfaz gráfica de usuario es eliminada, esto no es un problema, ya que el servicio debería ser capaz de reiniciarla.

Las DLL guard (si las hay), se inyectan masivamente en todos los procesos, y deberían ser capaces de buscar ganchos IAT y/o hilos maliciosos. Deberían ser bastante difíciles de descargar o derrotar. No son fundamentales, sino importantes.

El servicio es el núcleo del producto. Debe ser imposible de matar, o al menos debe ser capaz de reiniciarse automáticamente al matar. El servicio es responsable de la comunicación entre todos los módulos del producto, envía comandos a los controladores, toma comandos del usuario y consulta la base de datos para el análisis de muestras. Este es el cerebro.

Los controladores del núcleo también son críticos. Son los tentáculos que recopilan información sobre todo lo que sucede en el sistema y los transmiten al servicio para que tome una decisión. También pueden negar el acceso a lugares vigilados, según la decisión del servicio.

Conclusión

Hacer un motor antivirus fuerte, confiable y estable es una tarea complicada, que necesita personas experimentadas con un conocimiento muy sólido en programación de kernel de Windows, programación de aplicaciones de Windows, diseño de GUI, arquitectura de software, análisis de malware,

Construir controladores estables también es una tarea complicada, porque un pequeño grano de arena puede bloquear todo el sistema. Necesita pruebas, pruebas y muchas pruebas. Cuando su motor esté listo y funcionando, solo tendrá que contratar investigadores para analizar malwares y agregar firmas a su base de datos 🙂 ¿Qué está esperando? ¡Vamos! Links

Enlaces

  • http://www.symantec.com/connect/articles/building-anti-virus-engine

Leave a Reply

Tu dirección de correo electrónico no será publicada.