Como protexer os procesos e as extensións do núcleo en macOS

Ola, Habr! Hoxe gustaríame falar de como pode protexer os procesos dos ataques de atacantes en macOS. Por exemplo, isto é útil para un sistema antivirus ou de copia de seguridade, especialmente porque en macOS hai varias formas de "matar" un proceso. Lea sobre isto e os métodos de protección baixo o corte.

Como protexer os procesos e as extensións do núcleo en macOS

A forma clásica de "matar" un proceso

Unha forma coñecida de "matar" un proceso é enviar un sinal SIGKILL ao proceso. A través de bash podes chamar ao estándar "kill -SIGKILL PID" ou "pkill -9 NAME" para matar. O comando "matar" coñécese desde os tempos de UNIX e está dispoñible non só en macOS, senón tamén noutros sistemas similares a UNIX.

Do mesmo xeito que nos sistemas tipo UNIX, macOS permítelle interceptar calquera sinal a un proceso excepto dous: SIGKILL e SIGSTOP. Este artigo centrarase principalmente no sinal SIGKILL como un sinal que fai que un proceso se mate.

especificacións de macOS

En macOS, a chamada do sistema de matar no núcleo XNU chama á función psignal(SIGKILL,...). Imos tentar ver que outras accións do usuario no espazo de usuario poden ser chamadas pola función psignal. Eliminamos as chamadas á función psignal nos mecanismos internos do núcleo (aínda que non sexan triviais, deixarémolas para outro artigo 🙂 - verificación de sinaturas, erros de memoria, tratamento de saída/finalización, violacións da protección de ficheiros, etc.) .

Comecemos a revisión coa función e a correspondente chamada ao sistema terminar_con_carga útil. Pódese ver que ademais da clásica chamada de matar, hai un enfoque alternativo específico para o sistema operativo macOS e que non se atopa en BSD. Os principios de funcionamento de ambas as chamadas de sistema tamén son similares. Son chamadas directas á función do núcleo psignal. Teña en conta tamén que antes de matar un proceso, realízase unha comprobación de "cansignal": se o proceso pode enviar un sinal a outro proceso; o sistema non permite que ningunha aplicación elimine os procesos do sistema, por exemplo.

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);
	}
...
}

lanzado

Lanzase a forma estándar de crear daemons ao iniciar o sistema e controlar a súa vida útil. Teña en conta que as fontes son para a versión antiga de launchctl ata macOS 10.10, os exemplos de código ofrécense con fins ilustrativos. Launchctl moderno envía sinais launchd a través de XPC, a lóxica launchctl foi movida a el.

Vexamos como se deteñen exactamente as aplicacións. Antes de enviar o sinal SIGTERM, téntase deter a aplicación mediante a chamada ao 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));
		} 
...
<>

Baixo o capó, proc_terminate, a pesar do seu nome, pode enviar non só psignal con SIGTERM, senón tamén SIGKILL.

Matar indirecto: límite de recursos

Un caso máis interesante pódese ver noutra chamada do sistema proceso_política. Un uso común desta chamada ao sistema é limitar os recursos da aplicación, como un indexador para limitar o tempo da CPU e as cotas de memoria para que o sistema non se ralentice significativamente polas actividades de almacenamento en caché de ficheiros. Se unha aplicación alcanzou o seu límite de recursos, como se pode ver na función proc_apply_resource_actions, envíase un sinal SIGKILL ao proceso.

Aínda que esta chamada ao sistema podería matar un proceso, o sistema non comprobou adecuadamente os dereitos do proceso que chama a chamada ao sistema. En realidade, comprobando existiu, pero é suficiente usar a marca alternativa PROC_POLICY_ACTION_SET para evitar esta condición.

Polo tanto, se "limita" a cota de uso da CPU da aplicación (por exemplo, permitindo que só se execute 1 ns), pode matar calquera proceso do sistema. Así, o malware pode matar calquera proceso do sistema, incluído o proceso antivirus. Tamén é interesante o efecto que se produce ao matar un proceso con pid 1 (launchctl) - pánico do núcleo ao intentar procesar o sinal SIGKILL :)

Como protexer os procesos e as extensións do núcleo en macOS

Como resolver o problema?

A forma máis sinxela de evitar que un proceso sexa eliminado é substituír o punteiro de función na táboa de chamadas do sistema. Desafortunadamente, este método non é trivial por moitas razóns.

En primeiro lugar, o símbolo que controla a localización da memoria do sysent non só é privado do símbolo do núcleo XNU, senón que non se pode atopar nos símbolos do núcleo. Terá que usar métodos de busca heurística, como desmontar dinámicamente a función e buscar un punteiro nela.

En segundo lugar, a estrutura das entradas da táboa depende das marcas coas que se compilou o núcleo. Se se declara a bandeira CONFIG_REQUIRES_U32_MUNGING, cambiarase o tamaño da estrutura; engadirase un campo adicional sy_arg_munge32. É necesario realizar unha comprobación adicional para determinar con que bandeira se compilou o núcleo ou, alternativamente, comprobar os punteiros de función contra os coñecidos.

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, nas versións modernas de macOS, Apple ofrece unha nova API para traballar con procesos. A API de Endpoint Security permite aos clientes autorizar moitas solicitudes a outros procesos. Así, pode bloquear calquera sinal aos procesos, incluído o sinal SIGKILL, usando a 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;
}

Do mesmo xeito, pódese rexistrar unha política MAC no núcleo, que proporciona un método de protección de sinal (política proc_check_signal), pero a API non está soportada oficialmente.

Protección da extensión do núcleo

Ademais de protexer os procesos no sistema, tamén é necesario protexer a propia extensión do núcleo (kext). macOS ofrece un marco para que os desenvolvedores poidan desenvolver facilmente controladores de dispositivos IOKit. Ademais de proporcionar ferramentas para traballar con dispositivos, IOKit ofrece métodos para apilar controladores mediante instancias de clases C++. Unha aplicación no espazo de usuario poderá "buscar" unha instancia rexistrada da clase para establecer unha relación entre o núcleo e o espazo de usuario.

Para detectar o número de instancias de clase no sistema, existe a utilidade ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Calquera extensión do núcleo que queira rexistrarse na pila de controladores debe declarar unha clase que herda de IOService, por exemplo my_kext_ioservice neste caso. A conexión de aplicacións de usuario provoca a creación dunha nova instancia da clase que herda de IOUserClient, no exemplo my_kext_iouserclient.

Cando se tenta descargar un controlador do sistema (comando kextunload), chámase á función virtual "bool terminate(IOOptionBits options)". Basta con devolver false na chamada para finalizar cando se tenta descargar para desactivar o kextunload.

bool Kext::terminate(IOOptionBits options)
{

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

  return super::terminate(options);
}

A marca IsUnloadAllowed pódese establecer polo IOUserClient ao cargar. Cando hai un límite de descarga, o comando kextunload devolverá a seguinte saída:

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.

Debe facerse unha protección similar para IOUserClient. As instancias de clases pódense descargar usando a función de espazo de usuario de IOKitLib "IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);". Pode devolver false ao chamar ao comando "terminate" ata que a aplicación de espazo de usuario "morra", é dicir, non se chame a función "clientDied".

Protección de ficheiros

Para protexer os ficheiros, abonda con usar a API Kauth, que che permite restrinxir o acceso aos ficheiros. Apple proporciona aos desenvolvedores notificacións sobre varios eventos no ámbito; para nós, as operacións KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA e KAUTH_VNODE_DELETE_CHILD son importantes. A forma máis sinxela de restrinxir o acceso aos ficheiros é a través da ruta: usamos a API "vn_getpath" para obter a ruta do ficheiro e comparar o prefixo da ruta. Teña en conta que para optimizar o cambio de nome das rutas do cartafol de ficheiros, o sistema non autoriza o acceso a cada ficheiro, senón só ao propio cartafol que foi renomeado. É necesario comparar a ruta pai e restrinxir KAUTH_VNODE_DELETE para ela.

Como protexer os procesos e as extensións do núcleo en macOS

A desvantaxe deste enfoque pode ser o baixo rendemento a medida que aumenta o número de prefixos. Para garantir que a comparación non é igual a O (prefixo * lonxitude), onde o prefixo é o número de prefixos, a lonxitude é a lonxitude da cadea, pode usar un autómata finito determinista (DFA) construído por prefixos.

Consideremos un método para construír un DFA para un determinado conxunto de prefixos. Inicializamos os cursores ao comezo de cada prefixo. Se todos os cursores apuntan ao mesmo carácter, aumenta cada cursor nun carácter e lembra que a lonxitude da mesma liña é maior en un. Se hai dous cursores con símbolos diferentes, divide os cursores en grupos segundo o símbolo que apuntan e repite o algoritmo para cada grupo.

No primeiro caso (todos os caracteres debaixo dos cursores son iguais), obtemos un estado DFA que só ten unha transición ao longo da mesma liña. No segundo caso, obtemos unha táboa de transicións de tamaño 256 (número de caracteres e número máximo de grupos) a estados posteriores obtidas mediante a chamada recursiva á función.

Vexamos un exemplo. Para un conxunto de prefixos (“/foo/bar/tmp/”, “/var/db/foo/”, “/foo/bar/aba/”, “foo/bar/aac/”) pode obter o seguinte DFA. A figura mostra só as transicións que conducen a outros estados; outras transicións non serán definitivas.

Como protexer os procesos e as extensións do núcleo en macOS

Ao pasar polos estados de DKA, pode haber 3 casos.

  1. Alcanzouse o estado final: o camiño está protexido, limitamos as operacións KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA e KAUTH_VNODE_DELETE_CHILD
  2. Non se chegou ao estado final, pero o camiño "rematou" (alcanzouse o terminador nulo) - o camiño é un pai, é necesario limitar KAUTH_VNODE_DELETE. Teña en conta que se vnode é un cartafol, cómpre engadir un '/' ao final, se non, pode limitalo ao ficheiro "/foor/bar/t", que é incorrecto.
  3. Non se chegou ao estado final, o camiño non rematou. Ningún dos prefixos coincide con este, non introducimos restricións.

Conclusión

O obxectivo das solucións de seguridade que se están a desenvolver é aumentar o nivel de seguridade do usuario e dos seus datos. Por unha banda, este obxectivo conséguese co desenvolvemento do produto software Acronis, que pecha aquelas vulnerabilidades onde o propio sistema operativo é "débil". Por outra banda, non debemos descoidar o fortalecemento daqueles aspectos de seguridade que se poden mellorar no lado do SO, sobre todo porque o peche deste tipo de vulnerabilidades aumenta a nosa propia estabilidade como produto. A vulnerabilidade informouse ao equipo de seguranza do produto de Apple e solucionouse en macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Como protexer os procesos e as extensións do núcleo en macOS

Todo isto só se pode facer se a súa utilidade foi instalada oficialmente no núcleo. É dicir, non hai tales lagoas para o software externo e non desexado. Non obstante, como podes ver, mesmo protexer programas lexítimos, como antivirus e sistemas de copia de seguridade, require traballo. Pero agora os novos produtos Acronis para macOS terán protección adicional contra a descarga do sistema.

Fonte: www.habr.com

Engadir un comentario