Cómo proteger procesos y extensiones del kernel en macOS

¡Hola Habr! Hoy me gustaría hablar sobre cómo se pueden proteger los procesos de ataques de atacantes en macOS. Por ejemplo, esto es útil para un antivirus o un sistema de respaldo, especialmente porque en macOS hay varias formas de "matar" un proceso. Lea sobre esto y los métodos de protección debajo del corte.

Cómo proteger procesos y extensiones del kernel en macOS

La forma clásica de “matar” un proceso

Una forma bien conocida de "matar" un proceso es enviar una señal SIGKILL al proceso. A través de bash puedes llamar al estándar "kill -SIGKILL PID" o "pkill -9 NAME" para matar. El comando "kill" se conoce desde los tiempos de UNIX y está disponible no sólo en macOS, sino también en otros sistemas similares a UNIX.

Al igual que en los sistemas tipo UNIX, macOS le permite interceptar cualquier señal de un proceso excepto dos: SIGKILL y SIGSTOP. Este artículo se centrará principalmente en la señal SIGKILL como una señal que provoca la finalización de un proceso.

Detalles de macOS

En macOS, la llamada al sistema kill en el kernel XNU llama a la función psignal(SIGKILL,...). Intentemos ver qué otras acciones del usuario en el espacio de usuario pueden ser invocadas por la función psignal. Eliminemos las llamadas a la función psignal en los mecanismos internos del kernel (aunque pueden no ser triviales, las dejaremos para otro artículo 🙂: verificación de firmas, errores de memoria, manejo de salida/terminación, violaciones de protección de archivos, etc.

Comencemos la revisión con la función y la llamada al sistema correspondiente. terminar_con_carga útil. Se puede ver que además de la clásica llamada kill, existe un enfoque alternativo que es específico del sistema operativo macOS y no se encuentra en BSD. Los principios operativos de ambas llamadas al sistema también son similares. Son llamadas directas a la función del núcleo psignal. También tenga en cuenta que antes de finalizar un proceso, se realiza una verificación de "cansignal": si el proceso puede enviar una señal a otro proceso; el sistema no permite que ninguna aplicación finalice procesos del sistema, por ejemplo.

static int
terminate_with_payload_internal(struct proc *cur_proc, int target_pid, uint32_t reason_namespace,
				uint64_t reason_code, user_addr_t payload, uint32_t payload_size,
				user_addr_t reason_string, uint64_t reason_flags)
{
...
	target_proc = proc_find(target_pid);
...
	if (!cansignal(cur_proc, cur_cred, target_proc, SIGKILL)) {
		proc_rele(target_proc);
		return EPERM;
	}
...
	if (target_pid == cur_proc->p_pid) {
		/*
		 * psignal_thread_with_reason() will pend a SIGKILL on the specified thread or
		 * return if the thread and/or task are already terminating. Either way, the
		 * current thread won't return to userspace.
		 */
		psignal_thread_with_reason(target_proc, current_thread(), SIGKILL, signal_reason);
	} else {
		psignal_with_reason(target_proc, SIGKILL, signal_reason);
	}
...
}

lanzamiento

Se lanza la forma estándar de crear demonios al iniciar el sistema y controlar su vida útil. Tenga en cuenta que las fuentes son para la versión anterior de launchctl hasta macOS 10.10; los ejemplos de código se proporcionan con fines ilustrativos. El launchctl moderno envía señales de lanzamiento a través de XPC, la lógica de launchctl se ha trasladado a él.

Veamos cómo se detienen exactamente las aplicaciones. Antes de enviar la señal SIGTERM, se intenta detener la aplicación mediante la llamada al sistema "proc_terminate".

<launchctl src/core.c>
...
	error = proc_terminate(j->p, &sig);
	if (error) {
		job_log(j, LOG_ERR | LOG_CONSOLE, "Could not terminate job: %d: %s", error, strerror(error));
		job_log(j, LOG_NOTICE | LOG_CONSOLE, "Using fallback option to terminate job...");
		error = kill2(j->p, SIGTERM);
		if (error) {
			job_log(j, LOG_ERR, "Could not signal job: %d: %s", error, strerror(error));
		} 
...
<>

Debajo del capó, proc_terminate, a pesar de su nombre, puede enviar no solo psignal con SIGTERM, sino también SIGKILL.

Muerte indirecta: límite de recursos

Un caso más interesante se puede ver en otra llamada al sistema. política_proceso. Un uso común de esta llamada al sistema es limitar los recursos de la aplicación, como por ejemplo, para que un indexador limite el tiempo de CPU y las cuotas de memoria para que el sistema no se ralentice significativamente debido a las actividades de almacenamiento en caché de archivos. Si una aplicación ha alcanzado su límite de recursos, como se puede ver en la función proc_apply_resource_actions, se envía una señal SIGKILL al proceso.

Aunque esta llamada al sistema podría potencialmente matar un proceso, el sistema no verificó adecuadamente los derechos del proceso que llama a la llamada al sistema. realmente comprobando existió, pero es suficiente usar el indicador alternativo PROC_POLICY_ACTION_SET para evitar esta condición.

Por lo tanto, si "limita" la cuota de uso de CPU de la aplicación (por ejemplo, permite que solo se ejecute 1 ns), entonces puede finalizar cualquier proceso en el sistema. Por lo tanto, el malware puede matar cualquier proceso del sistema, incluido el proceso antivirus. También es interesante el efecto que ocurre al matar un proceso con pid 1 (launchctl): pánico del kernel al intentar procesar la señal SIGKILL :)

Cómo proteger procesos y extensiones del kernel en macOS

¿Como resolver el problema?

La forma más sencilla de evitar que se elimine un proceso es reemplazar el puntero de función en la tabla de llamadas del sistema. Desafortunadamente, este método no es trivial por muchas razones.

Primero, el símbolo que controla la ubicación de la memoria de sysent no solo es privado del símbolo del kernel XNU, sino que no se puede encontrar en los símbolos del kernel. Tendrá que utilizar métodos de búsqueda heurísticos, como desmontar dinámicamente la función y buscar un puntero en ella.

En segundo lugar, la estructura de las entradas de la tabla depende de los indicadores con los que se compiló el núcleo. Si se declara el indicador CONFIG_REQUIRES_U32_MUNGING, se cambiará el tamaño de la estructura; se agregará un campo adicional sy_arg_munge32. Es necesario llevar a cabo una verificación adicional para determinar con qué indicador se compiló el kernel o, alternativamente, comparar los punteros de función con los conocidos.

struct sysent {         /* system call table */
        sy_call_t       *sy_call;       /* implementing function */
#if CONFIG_REQUIRES_U32_MUNGING || (__arm__ && (__BIGGEST_ALIGNMENT__ > 4))
        sy_munge_t      *sy_arg_munge32; /* system call arguments munger for 32-bit process */
#endif
        int32_t         sy_return_type; /* system call return types */
        int16_t         sy_narg;        /* number of args */
        uint16_t        sy_arg_bytes;   /* Total size of arguments in bytes for
                                         * 32-bit system calls
                                         */
};

Afortunadamente, en las versiones modernas de macOS, Apple proporciona una nueva API para trabajar con procesos. La API de Endpoint Security permite a los clientes autorizar muchas solicitudes a otros procesos. Por lo tanto, puede bloquear cualquier señal a los procesos, incluida la señal SIGKILL, utilizando la API mencionada anteriormente.

#include <bsm/libbsm.h>
#include <EndpointSecurity/EndpointSecurity.h>
#include <unistd.h>

int main(int argc, const char * argv[]) {
    es_client_t* cli = nullptr;
    {
        auto res = es_new_client(&cli, ^(es_client_t * client, const es_message_t * message) {
            switch (message->event_type) {
                case ES_EVENT_TYPE_AUTH_SIGNAL:
                {
                    auto& msg = message->event.signal;
                    auto target = msg.target;
                    auto& token = target->audit_token;
                    auto pid = audit_token_to_pid(token);
                    printf("signal '%d' sent to pid '%d'n", msg.sig, pid);
                    es_respond_auth_result(client, message, pid == getpid() ? ES_AUTH_RESULT_DENY : ES_AUTH_RESULT_ALLOW, false);
                }
                    break;
                default:
                    break;
            }
        });
    }

    {
        es_event_type_t evs[] = { ES_EVENT_TYPE_AUTH_SIGNAL };
        es_subscribe(cli, evs, sizeof(evs) / sizeof(*evs));
    }

    printf("%dn", getpid());
    sleep(60); // could be replaced with other waiting primitive

    es_unsubscribe_all(cli);
    es_delete_client(cli);

    return 0;
}

De manera similar, se puede registrar una política MAC en el kernel, que proporciona un método de protección de señal (política proc_check_signal), pero la API no es compatible oficialmente.

Protección de extensión del kernel

Además de proteger los procesos del sistema, también es necesario proteger la propia extensión del kernel (kext). macOS proporciona un marco para que los desarrolladores desarrollen fácilmente controladores de dispositivos IOKit. Además de proporcionar herramientas para trabajar con dispositivos, IOKit proporciona métodos para apilar controladores utilizando instancias de clases de C++. Una aplicación en el espacio de usuario podrá "encontrar" una instancia registrada de la clase para establecer una relación kernel-espacio de usuario.

Para detectar la cantidad de instancias de clase en el sistema, existe la utilidad ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Cualquier extensión del kernel que desee registrarse con la pila de controladores debe declarar una clase que hereda de IOService, por ejemplo my_kext_ioservice en este caso. La conexión de aplicaciones de usuario provoca la creación de una nueva instancia de la clase que hereda de IOUserClient, en el ejemplo my_kext_iouserclient.

Al intentar descargar un controlador del sistema (comando kextunload), se llama a la función virtual "bool terminate (opciones IOOptionBits)". Es suficiente devolver falso en la llamada para terminar al intentar descargar para deshabilitar kextunload.

bool Kext::terminate(IOOptionBits options)
{

  if (!IsUnloadAllowed)
  {
    // Unload is not allowed, returning false
    return false;
  }

  return super::terminate(options);
}

IOUserClient puede establecer el indicador IsUnloadAllowed durante la carga. Cuando hay un límite de descarga, el comando kextunload devolverá el siguiente resultado:

admin@admins-Mac drivermanager % sudo kextunload ./test.kext
Password:
(kernel) Can't remove kext my.kext.test; services failed to terminate - 0xe00002c7.
Failed to unload my.kext.test - (iokit/common) unsupported function.

Se debe realizar una protección similar para IOUserClient. Las instancias de clases se pueden descargar utilizando la función de espacio de usuario de IOKitLib “IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t descripción);”. Puede devolver falso al llamar al comando "terminar" hasta que la aplicación del espacio de usuario "muera", es decir, no se llama a la función "clientDied".

Protección de archivos

Para proteger archivos, basta con utilizar la API Kauth, que le permite restringir el acceso a los archivos. Apple proporciona a los desarrolladores notificaciones sobre varios eventos en el alcance; para nosotros, las operaciones KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA y KAUTH_VNODE_DELETE_CHILD son importantes. La forma más sencilla de restringir el acceso a los archivos es mediante la ruta: utilizamos la API "vn_getpath" para obtener la ruta al archivo y comparar el prefijo de la ruta. Tenga en cuenta que para optimizar el cambio de nombre de las rutas de las carpetas de archivos, el sistema no autoriza el acceso a cada archivo, sino solo a la carpeta cuyo nombre ha sido renombrado. Es necesario comparar la ruta principal y restringir KAUTH_VNODE_DELETE para ella.

Cómo proteger procesos y extensiones del kernel en macOS

La desventaja de este enfoque puede ser el bajo rendimiento a medida que aumenta el número de prefijos. Para garantizar que la comparación no sea igual a O (prefijo * longitud), donde el prefijo es el número de prefijos y la longitud es la longitud de la cadena, puede utilizar un autómata finito determinista (DFA) creado por prefijos.

Consideremos un método para construir un DFA para un conjunto determinado de prefijos. Inicializamos los cursores al principio de cada prefijo. Si todos los cursores apuntan al mismo carácter, aumente cada cursor en un carácter y recuerde que la longitud de la misma línea es mayor en uno. Si hay dos cursores con símbolos diferentes, divida los cursores en grupos según el símbolo al que apuntan y repita el algoritmo para cada grupo.

En el primer caso (todos los caracteres debajo de los cursores son iguales), obtenemos un estado DFA que tiene solo una transición en la misma línea. En el segundo caso, obtenemos una tabla de transiciones de tamaño 256 (número de caracteres y número máximo de grupos) a estados posteriores obtenidos llamando recursivamente a la función.

Veamos un ejemplo. Para un conjunto de prefijos (“/foo/bar/tmp/”, “/var/db/foo/”, “/foo/bar/aba/”, “foo/bar/aac/”) puede obtener lo siguiente DFA. La figura muestra sólo las transiciones que conducen a otros estados; otras transiciones no serán definitivas.

Cómo proteger procesos y extensiones del kernel en macOS

Al pasar por los estados de CAD, pueden darse 3 casos.

  1. Se ha alcanzado el estado final: la ruta está protegida, limitamos las operaciones KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA y KAUTH_VNODE_DELETE_CHILD
  2. No se alcanzó el estado final, pero la ruta "terminó" (se alcanzó el terminador nulo): la ruta es principal, es necesario limitar KAUTH_VNODE_DELETE. Tenga en cuenta que si vnode es una carpeta, debe agregar un '/' al final; de lo contrario, puede limitarlo al archivo “/foor/bar/t”, lo cual es incorrecto.
  3. No se llegó al estado final, el camino no terminó. Ninguno de los prefijos coincide con éste, no introducimos restricciones.

Conclusión

El objetivo de las soluciones de seguridad que se desarrollan es aumentar el nivel de seguridad del usuario y de sus datos. Por un lado, este objetivo se logra mediante el desarrollo del producto de software Acronis, que cierra aquellas vulnerabilidades en las que el propio sistema operativo es "débil". Por otro lado, no debemos descuidar el fortalecimiento de aquellos aspectos de seguridad que se pueden mejorar en el lado del sistema operativo, especialmente porque cerrar dichas vulnerabilidades aumenta nuestra propia estabilidad como producto. La vulnerabilidad se informó al equipo de seguridad de productos de Apple y se solucionó en macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Cómo proteger procesos y extensiones del kernel en macOS

Todo esto sólo se puede hacer si su utilidad se ha instalado oficialmente en el kernel. Es decir, no existen tales lagunas para el software externo y no deseado. Sin embargo, como puede ver, incluso proteger programas legítimos, como antivirus y sistemas de respaldo, requiere trabajo. Pero ahora los nuevos productos de Acronis para macOS tendrán protección adicional contra la descarga del sistema.

Fuente: habr.com

Añadir un comentario