Cómo proteger procesos y extensiones de kernel en macOS

Hola Habr! Hoy me gustaría hablar sobre cómo puede proteger los procesos contra intrusos en macOS. Por ejemplo, es útil para un sistema antivirus o de respaldo, especialmente a la luz del hecho de que bajo macOS hay varias formas de "matar" el proceso. Lea sobre esto y sobre los métodos de protección bajo un gato.

imagen

La forma clásica de matar un proceso


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

Al igual que en sistemas similares a UNIX, macOS le permite interceptar cualquier señal al proceso, excepto dos: SIGKILL y SIGSTOP. En este artículo, la señal SIGKILL se considerará principalmente como una señal que da lugar al cierre de un proceso.

MacOS específicos


En macOS, la llamada al sistema kill en el núcleo XNU llama a la función psignal (SIGKILL, ...). Intentemos ver qué otras acciones de usuario en el espacio de usuario pueden llamar la función psignal. Eliminamos las llamadas a la función psignal en los mecanismos internos del kernel (aunque pueden no ser triviales, dejémoslas para otro artículo :): verificación de firma, errores de memoria, procesamiento de salida / finalización, violación de la protección de archivos, etc.

Comenzamos la descripción general con la función y la correspondiente llamada al sistema terminate_with_payload. Se puede ver que, además de la clásica llamada kill, existe un enfoque alternativo que es específico para el sistema operativo macOS y no se encuentra en BSD. Los principios operativos de ambas llamadas al sistema también están cerca. 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 “señalización”: si el proceso puede enviar una señal a otro proceso, el sistema no permite que ninguna aplicación elimine 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 inicia la forma estándar de crear demonios al inicio del sistema y controlar su vida útil. Llamaré la atención sobre el hecho de que el código fuente es para la versión anterior de launchctl antes de macOS 10.10, los ejemplos de código se dan a modo de ilustración. El launchctl moderno envía señales de launchd a través de XPC, la lógica de launchctl se le transfiere.

Consideremos cómo se detiene la aplicación. Antes de enviar una señal SIGTERM, intentan detener la aplicación utilizando 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));
		} 
...
<>

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

Matanza indirecta - límite de recursos


Se puede ver un caso más interesante en otra llamada al sistema process_policy . El uso estándar de esta llamada al sistema son los límites de recursos de la aplicación, por ejemplo, para el indexador, hay un límite en el tiempo del procesador y la cuota de memoria para que el sistema no disminuya significativamente las acciones de almacenamiento en caché de archivos. Si la aplicación ha alcanzado el límite de recursos, como se puede ver en la función proc_apply_resource_actions, la señal SIGKILL se envía al proceso.

Aunque esta llamada al sistema podría potencialmente matar un proceso, el sistema no verificó adecuadamente los derechos del proceso que causó la llamada al sistema. De hecho, existía una comprobación , pero es suficiente usar el indicador alternativo PROC_POLICY_ACTION_SET para omitir esta condición.

Por lo tanto, si "limita" la cuota de uso de la CPU por la aplicación (por ejemplo, permite que solo se ejecute 1 ns), puede eliminar cualquier proceso en el sistema. Entonces, el malware puede matar cualquier proceso en el sistema, incluido el proceso antivirus. También es interesante el efecto que ocurre cuando se mata un proceso con pid 1 (launchctl) - kernel panic cuando se trata de procesar una señal SIGKILL :)

imagen

¿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

: en primer lugar, el símbolo responsable de la posición de sysent en la memoria no es solo un símbolo privado del núcleo XNU, sino que tampoco se puede encontrar en los símbolos del núcleo. Deberá utilizar métodos de búsqueda heurísticos, por ejemplo, desmontaje dinámico de una función y buscar un puntero en ella.

En segundo lugar, la estructura de las entradas en la tabla depende de los indicadores con los que se construyó 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 en el indicador con el que se compiló el núcleo, como opción, comparar punteros con funciones conocidas.

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("%d\n", getpid());
    sleep(60); // could be replaced with other waiting primitive

    es_unsubscribe_all(cli);
    es_delete_client(cli);

    return 0;
}

Del mismo modo, puede registrar la Política MAC en el núcleo, que proporciona un método de protección de señal (política proc_check_signal), pero la API no es oficialmente compatible.

Protección de extensión de kernel


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

Para detectar el número de instancias de clase en el sistema, existe la utilidad ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

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

Al intentar descargar el controlador del sistema (comando kextunload), se llama a la función virtual "bool terminate (opciones de IOOptionBits)". Es suficiente devolver falso en la llamada a la función de terminación al intentar descargar para desactivar 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 en el arranque. Cuando la carga es limitada, 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 hacer una protección similar para IOUserClient. Las instancias de clase se pueden descargar utilizando la función de espacio de usuario IOKitLib "IOCatalogueTerminate (mach_port_t, uint32_t flag, io_name_t description);". Puede devolver falso en una llamada al comando "terminar" hasta que el espacio de usuario muera la aplicación, es decir, no hay llamada a la función clientDied.

Protección de archivos


Para proteger los archivos, es suficiente usar la API de Kauth, que le permite restringir el acceso a los archivos. Apple proporciona a los desarrolladores notificaciones sobre varios eventos en el ámbito, las operaciones KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA y KAUTH_VNODE_DELETE_CHILD son importantes para nosotros. Restringir el acceso a los archivos es más fácil en el camino: 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 con archivos, el sistema no autoriza el acceso a cada archivo, sino solo a la carpeta en sí, que fue renombrada. Es necesario comparar la ruta principal y restringir KAUTH_VNODE_DELETE para ello.

imagen

La desventaja de este enfoque puede ser un bajo rendimiento con un número creciente de prefijos. Para que la comparación no sea igual a O (prefijo * longitud), donde prefijo es el número de prefijos, longitud es la longitud de la cadena, puede usar una máquina de estado finito determinista (DFA) construida por prefijos.

Considere una forma de construir un DFA para un conjunto dado de prefijos. Inicializamos los cursores al comienzo de cada prefijo. Si todos los cursores apuntan al mismo carácter, entonces aumentamos cada cursor en un carácter y recordamos que la longitud de la misma línea es más de uno en uno. Si hay dos cursores con diferentes símbolos debajo de ellos, dividimos los cursores en grupos por el símbolo al que apuntan y repetimos el algoritmo para cada grupo.

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

Considera un ejemplo. Para un conjunto de prefijos ("/ foo / bar / tmp /", "/ var / db / foo /", "/ foo / bar / aba /", "foo / bar / aac /") puede obtener el siguiente DFA. La figura muestra solo las transiciones que conducen a otros estados, otras transiciones no serán finales.

imagen

Al pasar por los estados DKA, puede haber 3 casos.

  1. Se alcanzó el estado final: la ruta está protegida, restringimos las operaciones KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA y KAUTH_VNODE_DELETE_CHILD
  2. , “” ( -) — , KAUTH_VNODE_DELETE. , vnode , ‘/’, “/foor/bar/t”, .
  3. , . , .


El objetivo de las soluciones de seguridad desarrolladas es aumentar el nivel de seguridad del usuario y sus datos. Por un lado, este objetivo está garantizado por el desarrollo del producto de software Acronis que cubre las vulnerabilidades donde el sistema operativo en sí es "débil". Por otro lado, no debemos descuidar la mejora de aquellos aspectos de seguridad que pueden mejorarse en el lado del sistema operativo, especialmente porque el cierre de tales vulnerabilidades aumenta nuestra propia estabilidad como producto. La vulnerabilidad fue reportada por el equipo de seguridad de productos de Apple y se corrigió en macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

imagen

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

All Articles