Comment protéger les processus et les extensions du noyau sur macOS

Bonjour, Habr! Aujourd'hui, je voudrais parler de la façon dont vous pouvez protéger les processus contre les intrus dans macOS. Par exemple, il est utile pour un antivirus ou un système de sauvegarde, surtout à la lumière du fait que sous macOS, il existe plusieurs façons de «tuer» le processus. Lisez à ce sujet et sur les méthodes de protection sous un chat.

image

La manière classique de tuer un processus


La manière bien connue de «tuer» un processus consiste à envoyer un signal concernant un processus SIGKILL. Grâce à bash, vous pouvez appeler le standard «kill -SIGKILL PID» ou «pkill -9 NAME» pour tuer. La commande kill est connue depuis UNIX et est disponible non seulement sur macOS, mais également sur d'autres systèmes de type UNIX.

Comme dans les systèmes de type UNIX, macOS vous permet d'intercepter tous les signaux du processus, sauf deux - SIGKILL et SIGSTOP. Dans cet article, le signal SIGKILL sera principalement considéré comme un signal provoquant la mort d'un processus.

Spécificités MacOS


Sur macOS, l'appel système kill dans le noyau XNU appelle la fonction psignal (SIGKILL, ...). Essayons de voir quelles autres actions utilisateur dans l'espace utilisateur peuvent appeler la fonction psignal. Nous éliminons les appels à la fonction psignal dans les mécanismes internes du noyau (bien qu'ils puissent être non triviaux, nous les laisserons pour un autre article :) - vérification de signature, erreurs de mémoire, traitement de sortie / arrêt, violation de la protection des fichiers, etc.

Nous commençons la vue d'ensemble avec la fonction et l'appel système correspondant terminate_with_payload. On peut voir qu'en plus de l'appel de mise à mort classique, il existe une approche alternative spécifique au système d'exploitation macOS et qui ne se trouve pas dans BSD. Les principes de fonctionnement des deux appels système sont également proches. Ce sont des appels directs à la fonction de noyau psignal. Notez également qu'avant de tuer un processus, une vérification de «cansignal» est effectuée - si le processus peut envoyer un signal à un autre processus, le système ne permet à aucune application de tuer des processus système par exemple.

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


La méthode standard pour créer des démons au démarrage du système et contrôler leur durée de vie est launchd. J'attirerai l'attention sur le fait que le code source est pour l'ancienne version de launchctl avant macOS 10.10, des exemples de code sont donnés à titre d'illustration. Le launchctl moderne envoie des signaux launchd via XPC, la logique de launchctl lui est transférée.

Voyons comment la demande est arrêtée. Avant d'envoyer un signal SIGTERM, ils essaient d'arrêter l'application à l'aide de l'appel système 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));
		} 
...
<>

Sous le capot, proc_terminate, malgré son nom, peut envoyer non seulement du psignal avec SIGTERM, mais aussi SIGKILL.

Élimination indirecte - limite de ressources


Un cas plus intéressant peut être vu dans un autre appel système process_policy . L'utilisation standard de cet appel système est des limites de ressources d'application, par exemple, pour l'indexeur, il y a une limite de temps processeur et de quota de mémoire afin que le système ne ralentisse pas de manière significative en raison des actions de mise en cache de fichiers. Si l'application a atteint la limite de ressources, comme le montre la fonction proc_apply_resource_actions, le signal SIGKILL est envoyé au processus.

Bien que cet appel système puisse potentiellement tuer un processus, le système n'a pas vérifié correctement les droits du processus à l'origine de l'appel système. En fait, une vérification existait , mais il suffit d'utiliser l'indicateur alternatif PROC_POLICY_ACTION_SET pour contourner cette condition.

Par conséquent, si vous «limitez» le quota d'utilisation du processeur par l'application (par exemple, n'autorisez que 1 ns à être exécuté), vous pouvez alors tuer n'importe quel processus du système. Ainsi, le malware peut tuer n'importe quel processus sur le système, y compris le processus antivirus. L'effet qui se produit quand un processus est tué avec pid 1 (launchctl) - panique du noyau en essayant de traiter un signal SIGKILL est également intéressant :)

image

Comment résoudre le problème?


La façon la plus simple d'empêcher un processus d'être tué est de remplacer le pointeur de fonction dans la table d'appels système. Malheureusement, cette méthode n'est pas triviale pour de nombreuses raisons

. Premièrement, le symbole qui est responsable de la position du sysent dans la mémoire n'est pas seulement un symbole privé du noyau XNU, mais ne peut pas non plus être trouvé dans les symboles du noyau. Vous devrez utiliser des méthodes de recherche heuristiques, par exemple, le démontage dynamique d'une fonction et rechercher un pointeur dans celle-ci.

Deuxièmement, la structure des entrées du tableau dépend des drapeaux avec lesquels le noyau a été construit. Si le drapeau CONFIG_REQUIRES_U32_MUNGING est déclaré, alors la taille de la structure sera modifiée - un champ supplémentaire sy_arg_munge32 est ajouté. Il est nécessaire d'effectuer une vérification supplémentaire sur l'indicateur avec lequel le noyau a été compilé, en option, comparer les pointeurs aux fonctions avec des fonctions connues.

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

Heureusement, dans les versions modernes de macOS, Apple fournit une nouvelle API pour travailler avec les processus. L'API Endpoint Security permet aux clients d'autoriser de nombreuses demandes à d'autres processus. Ainsi, vous pouvez bloquer tous les signaux aux processus, y compris le signal SIGKILL en utilisant l'API susmentionnée.

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

De même, vous pouvez enregistrer la politique MAC dans le noyau, qui fournit une méthode de protection du signal (politique proc_check_signal), mais l'API n'est pas officiellement prise en charge.

Protection des extensions du noyau


En plus de protéger les processus dans le système, la protection de l'extension du noyau elle-même (kext) est également nécessaire. macOS fournit un cadre permettant aux développeurs de développer facilement des pilotes de périphérique IOKit. En plus de fournir des outils pour travailler avec des périphériques, IOKit fournit des méthodes d'empilement de pilotes utilisant des instances de classes C ++. Une application dans l'espace utilisateur pourra «trouver» une instance enregistrée de la classe pour établir une connexion noyau-espace utilisateur.

Pour détecter le nombre d'instances de classe dans le système, l'utilitaire ioclasscount existe.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Toute extension de noyau qui souhaite s'inscrire sur la pile du pilote doit déclarer une classe héritée d'IOService, par exemple, my_kext_ioservice dans ce cas. La connexion des applications utilisateur créera une nouvelle instance de la classe qui hérite de IOUserClient, dans l'exemple my_kext_iouserclient.

Lorsque vous essayez de décharger le pilote du système (commande kextunload), la fonction virtuelle «bool terminate (options IOOptionBits)» est appelée. Il suffit de retourner false lors de l'appel à la fonction terminate lorsque vous essayez de décharger pour désactiver kextunload.

bool Kext::terminate(IOOptionBits options)
{

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

  return super::terminate(options);
}


L'indicateur IsUnloadAllowed peut être défini par IOUserClient au démarrage. Lorsque le chargement est limité, la commande kextunload renvoie la sortie suivante:

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.

Une protection similaire doit être effectuée pour IOUserClient. Les instances de classe peuvent être déchargées à l'aide de la fonction d'espace utilisateur IOKitLib «IOCatalogueTerminate (mach_port_t, indicateur uint32_t, io_name_t description);». Vous pouvez retourner false lors d'un appel à la commande «terminate» jusqu'à ce que l'espace utilisateur de l'application meure, c'est-à-dire qu'il n'y ait aucun appel à la fonction clientDied.

Protection des fichiers


Pour protéger les fichiers, il suffit d'utiliser l'API Kauth, qui vous permet de restreindre l'accès aux fichiers. Apple fournit aux développeurs des notifications sur divers événements dans la portée, les opérations KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA et KAUTH_VNODE_DELETE_CHILD sont importantes pour nous. Restreindre l'accès aux fichiers est plus facile en cours de route - nous utilisons l'API «vn_getpath» pour obtenir le chemin d'accès au fichier et comparer le préfixe du chemin d'accès. Notez que pour optimiser le renommage des chemins des dossiers contenant des fichiers, le système n'autorise pas l'accès à chaque fichier, mais uniquement au dossier lui-même, qui a été renommé. Il est nécessaire de comparer le chemin parent et de restreindre KAUTH_VNODE_DELETE pour cela.

image

L'inconvénient de cette approche peut être de faibles performances avec un nombre croissant de préfixes. Pour que la comparaison ne soit pas égale à O (préfixe * longueur), où préfixe est le nombre de préfixes, longueur est la longueur de la chaîne, vous pouvez utiliser une machine à états finis déterministe (DFA) construite par des préfixes.

Considérez un moyen de créer un DFA pour un ensemble de préfixes donné. Nous initialisons les curseurs au début de chaque préfixe. Si tous les curseurs pointent vers le même caractère, alors nous augmentons chaque curseur d'un caractère et nous rappelons que la longueur de la même ligne est plus d'un. S'il y a deux curseurs avec des symboles différents sous eux, nous divisons les curseurs en groupes par le symbole vers lequel ils pointent et répétons l'algorithme pour chaque groupe.

Dans le premier cas (tous les caractères sous les curseurs sont les mêmes), nous obtenons l'état DFA, qui n'a qu'une seule transition sur la même ligne. Dans le second cas, nous obtenons une table de transition de taille 256 (nombre de caractères et nombre maximum de groupes) dans les états suivants obtenus en appelant récursivement la fonction.

Prenons un exemple. Pour un ensemble de préfixes ("/ foo / bar / tmp /", "/ var / db / foo /", "/ foo / bar / aba /", "foo / bar / aac /"), vous pouvez obtenir le DFA suivant. La figure montre uniquement les transitions menant à d'autres états, les autres transitions ne seront pas définitives.

image

Lors du passage dans les États de la DKA, il peut y avoir 3 cas.

  1. L'état final a été atteint - le chemin est protégé, nous restreignons les opérations KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA et KAUTH_VNODE_DELETE_CHILD
  2. , “” ( -) — , KAUTH_VNODE_DELETE. , vnode , ‘/’, “/foor/bar/t”, .
  3. , . , .


Le but des solutions de sécurité développées est d'augmenter le niveau de sécurité de l'utilisateur et de ses données. D'une part, cet objectif est assuré par le développement du produit logiciel Acronis qui couvre les vulnérabilités où le système d'exploitation lui-même est «faible». D'un autre côté, nous ne devons pas négliger l'amélioration de ces aspects de sécurité qui peuvent être améliorés du côté du système d'exploitation, d'autant plus que la fermeture de ces vulnérabilités augmente notre propre stabilité en tant que produit. La vulnérabilité a été signalée par l'équipe de sécurité des produits Apple et a été corrigée dans macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

image

Tout cela ne peut être fait que si votre utilitaire a été officiellement installé dans le noyau. Autrement dit, il n'y a pas de telles lacunes pour les logiciels externes et indésirables. Cependant, comme vous pouvez le voir, même pour protéger des programmes légitimes tels que des antivirus et des systèmes de sauvegarde, vous devez travailler dur. Mais maintenant, les nouveaux produits Acronis pour macOS bénéficieront d'une protection supplémentaire contre le déchargement du système.

All Articles