Como proteger processos e extensões de kernel no macOS

Olá, Habr! Hoje gostaria de falar sobre como você pode proteger processos contra ataques de invasores no macOS. Por exemplo, isso é útil para um antivírus ou sistema de backup, especialmente porque no macOS existem várias maneiras de “matar” um processo. Leia sobre isso e os métodos de proteção sob o corte.

Como proteger processos e extensões de kernel no macOS

A maneira clássica de “matar” um processo

Uma maneira bem conhecida de “matar” um processo é enviar um sinal SIGKILL ao processo. Através do bash você pode chamar o padrão “kill -SIGKILL PID” ou “pkill -9 NAME” para matar. O comando “kill” é conhecido desde os tempos do UNIX e está disponível não apenas no macOS, mas também em outros sistemas semelhantes ao UNIX.

Assim como em sistemas do tipo UNIX, o macOS permite interceptar quaisquer sinais para um processo, exceto dois - SIGKILL e SIGSTOP. Este artigo se concentrará principalmente no sinal SIGKILL como um sinal que causa a interrupção de um processo.

Especificações do macOS

No macOS, a chamada de sistema kill no kernel XNU chama a função psignal(SIGKILL,...). Vamos tentar ver quais outras ações do usuário no espaço do usuário podem ser chamadas pela função psignal. Vamos eliminar chamadas para a função psignal nos mecanismos internos do kernel (embora possam não ser triviais, vamos deixá-las para outro artigo 🙂 - verificação de assinatura, erros de memória, manipulação de saída/encerramento, violações de proteção de arquivos, etc. .

Vamos começar a revisão com a função e a chamada de sistema correspondente terminar_com_payload. Percebe-se que além da chamada kill clássica, existe uma abordagem alternativa específica do sistema operacional macOS e não encontrada no BSD. Os princípios operacionais de ambas as chamadas de sistema também são semelhantes. São chamadas diretas para a função psignal do kernel. Observe também que antes de encerrar um processo, é realizada uma verificação “cansignal” – se o processo pode enviar um sinal para outro processo; o sistema não permite que nenhum aplicativo elimine processos 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);
	}
...
}

launchd

A maneira padrão de criar daemons na inicialização do sistema e controlar sua vida útil é launchd. Observe que as fontes são para a versão antiga do launchctl até macOS 10.10; exemplos de código são fornecidos para fins ilustrativos. O launchctl moderno envia sinais launchd via XPC, a lógica launchctl foi movida para ele.

Vejamos exatamente como os aplicativos são interrompidos. Antes de enviar o sinal SIGTERM, tenta-se parar a aplicação usando a chamada de 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));
		} 
...
<>

Nos bastidores, proc_terminate, apesar do nome, pode enviar não apenas psignal com SIGTERM, mas também SIGKILL.

Morte Indireta - Limite de Recursos

Um caso mais interessante pode ser visto em outra chamada de sistema política_de_processo. Um uso comum dessa chamada de sistema é limitar os recursos do aplicativo, como para um indexador limitar o tempo de CPU e as cotas de memória para que o sistema não fique significativamente lento pelas atividades de cache de arquivos. Se um aplicativo atingiu seu limite de recursos, como pode ser visto na função proc_apply_resource_actions, um sinal SIGKILL é enviado ao processo.

Embora esta chamada de sistema pudesse potencialmente matar um processo, o sistema não verificou adequadamente os direitos do processo que chamou a chamada de sistema. Na verdade verificando existia, mas basta usar o sinalizador alternativo PROC_POLICY_ACTION_SET para contornar esta condição.

Portanto, se você “limitar” a cota de uso da CPU do aplicativo (por exemplo, permitindo a execução de apenas 1 ns), poderá encerrar qualquer processo no sistema. Assim, o malware pode matar qualquer processo do sistema, incluindo o processo antivírus. Também interessante é o efeito que ocorre ao encerrar um processo com pid 1 (launchctl) - kernel panic ao tentar processar o sinal SIGKILL :)

Como proteger processos e extensões de kernel no macOS

Como resolver o problema?

A maneira mais direta de evitar que um processo seja eliminado é substituir o ponteiro de função na tabela de chamadas do sistema. Infelizmente, este método não é trivial por vários motivos.

Primeiro, o símbolo que controla a localização da memória do sysent não é apenas privado do símbolo do kernel XNU, mas não pode ser encontrado nos símbolos do kernel. Você terá que usar métodos de pesquisa heurística, como desmontar dinamicamente a função e procurar um ponteiro nela.

Em segundo lugar, a estrutura das entradas na tabela depende dos sinalizadores com os quais o kernel foi compilado. Se o sinalizador CONFIG_REQUIRES_U32_MUNGING for declarado, o tamanho da estrutura será alterado - um campo adicional será adicionado sy_arg_munge32. É necessário realizar uma verificação adicional para determinar com qual flag o kernel foi compilado ou, alternativamente, verificar os ponteiros de função em relação aos conhecidos.

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
                                         */
};

Felizmente, nas versões modernas do macOS, a Apple fornece uma nova API para trabalhar com processos. A API Endpoint Security permite que os clientes autorizem muitas solicitações para outros processos. Assim, você pode bloquear quaisquer sinais para processos, incluindo o sinal SIGKILL, usando a API mencionada acima.

#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;
}

Da mesma forma, uma política MAC pode ser registrada no kernel, que fornece um método de proteção de sinal (política proc_check_signal), mas a API não é oficialmente suportada.

Proteção de extensão do kernel

Além de proteger os processos do sistema, também é necessário proteger a própria extensão do kernel (kext). O macOS fornece uma estrutura para os desenvolvedores desenvolverem facilmente drivers de dispositivos IOKit. Além de fornecer ferramentas para trabalhar com dispositivos, o IOKit fornece métodos para empilhamento de drivers usando instâncias de classes C++. Uma aplicação no espaço do usuário será capaz de “encontrar” uma instância registrada da classe para estabelecer um relacionamento kernel-espaço do usuário.

Para detectar o número de instâncias de classe no sistema, existe o utilitário ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Qualquer extensão de kernel que deseje registrar-se na pilha de drivers deve declarar uma classe que herda de IOService, por exemplo my_kext_ioservice neste caso. A conexão de aplicativos de usuário causa a criação de uma nova instância da classe que herda de IOUserClient, no exemplo my_kext_iouserclient.

Ao tentar descarregar um driver do sistema (comando kextunload), a função virtual “bool termina (opções IOOptionBits)” é chamada. Basta retornar false na chamada para encerrar ao tentar descarregar para desabilitar o kextunload.

bool Kext::terminate(IOOptionBits options)
{

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

  return super::terminate(options);
}

O sinalizador IsUnloadAllowed pode ser definido pelo IOUserClient durante o carregamento. Quando houver um limite de download, o comando kextunload retornará 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.

Proteção semelhante deve ser feita para IOUserClient. Instâncias de classes podem ser descarregadas usando a função de espaço de usuário IOKitLib “IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);”. Você pode retornar falso ao chamar o comando “terminar” até que a aplicação do espaço do usuário “morra”, ou seja, a função “clientDied” não seja chamada.

Proteção de arquivos

Para proteger os arquivos, basta utilizar a API Kauth, que permite restringir o acesso aos arquivos. A Apple fornece aos desenvolvedores notificações sobre vários eventos no escopo; para nós, as operações KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA e KAUTH_VNODE_DELETE_CHILD são importantes. A maneira mais fácil de restringir o acesso aos arquivos é por caminho - usamos a API “vn_getpath” para obter o caminho do arquivo e comparar o prefixo do caminho. Observe que para otimizar a renomeação dos caminhos das pastas de arquivos, o sistema não autoriza o acesso a cada arquivo, mas apenas à própria pasta que foi renomeada. É necessário comparar o caminho pai e restringir KAUTH_VNODE_DELETE para ele.

Como proteger processos e extensões de kernel no macOS

A desvantagem desta abordagem pode ser o baixo desempenho à medida que o número de prefixos aumenta. Para garantir que a comparação não seja igual a O(prefixo*comprimento), onde prefixo é o número de prefixos, comprimento é o comprimento da string, você pode usar um autômato finito determinístico (DFA) construído por prefixos.

Vamos considerar um método para construir um AFD para um determinado conjunto de prefixos. Inicializamos os cursores no início de cada prefixo. Se todos os cursores apontarem para o mesmo caractere, aumente cada cursor em um caractere e lembre-se de que o comprimento da mesma linha é maior em um. Se houver dois cursores com símbolos diferentes, divida os cursores em grupos de acordo com o símbolo para o qual apontam e repita o algoritmo para cada grupo.

No primeiro caso (todos os caracteres sob os cursores são iguais), obtemos um estado DFA que possui apenas uma transição na mesma linha. No segundo caso, obtemos uma tabela de transições de tamanho 256 (número de caracteres e número máximo de grupos) para estados subsequentes obtidos pela chamada recursiva da função.

Vejamos um exemplo. Para um conjunto de prefixos (“/foo/bar/tmp/”, “/var/db/foo/”, “/foo/bar/aba/”, “foo/bar/aac/”) você pode obter o seguinte AFD. A figura mostra apenas transições que levam a outros estados; outras transições não serão finais.

Como proteger processos e extensões de kernel no macOS

Ao passar pelos estados DKA, pode haver 3 casos.

  1. O estado final foi alcançado - o caminho está protegido, limitamos as operações KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA e KAUTH_VNODE_DELETE_CHILD
  2. O estado final não foi alcançado, mas o caminho “terminou” (o terminador nulo foi atingido) - o caminho é pai, é necessário limitar KAUTH_VNODE_DELETE. Observe que se vnode for uma pasta, você precisa adicionar um '/' no final, caso contrário pode limitá-lo ao arquivo “/foor/bar/t”, o que está incorreto.
  3. O estado final não foi alcançado, o caminho não terminou. Nenhum dos prefixos corresponde a este, não introduzimos restrições.

Conclusão

O objetivo das soluções de segurança desenvolvidas é aumentar o nível de segurança do usuário e de seus dados. Por um lado, este objetivo é alcançado através do desenvolvimento do produto de software Acronis, que fecha as vulnerabilidades onde o próprio sistema operativo é “fraco”. Por outro lado, não devemos negligenciar o reforço dos aspectos de segurança que podem ser melhorados no lado do sistema operacional, especialmente porque a eliminação de tais vulnerabilidades aumenta a nossa própria estabilidade como produto. A vulnerabilidade foi relatada à equipe de segurança de produtos Apple e corrigida no macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Como proteger processos e extensões de kernel no macOS

Tudo isso só pode ser feito se o seu utilitário estiver oficialmente instalado no kernel. Ou seja, não existem tais brechas para software externo e indesejado. No entanto, como você pode ver, até mesmo proteger programas legítimos, como antivírus e sistemas de backup, exige trabalho. Mas agora os novos produtos Acronis para macOS terão proteção adicional contra descarga do sistema.

Fonte: habr.com

Adicionar um comentário