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

Bonjour, Habr ! Aujourd'hui, j'aimerais parler de la manière de protéger les processus contre les intrusions malveillantes sous macOS. Par exemple, cela est utile pour un antivirus ou un système de sauvegarde, notamment en raison du fait qu'il existe plusieurs moyens de "tuer" un processus sous macOS. Vous pouvez lire sur ce sujet et les méthodes de protection ci-dessous.

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

Méthode classique pour "tuer" un processus

La méthode bien connue pour "tuer" un processus consiste à envoyer le signal SIGKILL au processus. Via bash, on peut utiliser les commandes standards "kill -SIGKILL PID" ou "pkill -9 NAME" pour le faire. La commande "kill" existe depuis l'époque des UNIX et est accessible non seulement sous macOS, mais aussi dans d'autres systèmes de type UNIX.

Tout comme dans les systèmes de type UNIX, macOS permet d'intercepter tous les signaux envoyés à un processus, sauf deux : SIGKILL et SIGSTOP. Cet article examinera principalement le signal SIGKILL, comme signal qui déclenche la destruction d'un processus.

Spécificités de macOS

Dans macOS, l'appel système kill dans le noyau XNU déclenche la fonction psignal(SIGKILL,…). Voyons quelles autres actions de l'utilisateur dans l'espace utilisateur peuvent invoquer la fonction psignal. Nous exclurons les appels à la fonction psignal dans les mécanismes internes du noyau (bien qu'ils puissent ne pas être triviaux, nous les laisserons pour un autre article 🙂 — vérification de signature, erreurs de mémoire, traitement des exit/terminate, violation de la protection des fichiers, etc.).

Commençons notre tour d'horizon avec la fonction et l'appel système correspondant terminate_with_payload. Il est évident que, en plus de l'appel classique à kill, il existe une approche alternative qui est spécifique au système d'exploitation macOS et qui ne se rencontre pas dans BSD. Les principes de fonctionnement des deux appels système sont également similaires. Ils représentent des appels directs à la fonction du noyau psignal. Notons également qu'avant de tuer un processus, un contrôle "cansignal" est effectué – peut-on envoyer un signal d'un processus à un autre, le système ne permet pas à n'importe quelle 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) {
			t /*
		 * psignal_thread_with_reason() pend un SIGKILL sur le thread spécifié ou
		 * retourne si le thread ou la tâche sont déjà en cours de terminaison. Dans tous les cas, le
		 * thread actuel ne reviendra pas à l'espace utilisateur.
		 * /
		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. Je tiens à souligner que les sources fournies concernent une ancienne version de launchctl avant macOS 10.10, les exemples de code sont donnés à titre d'illustration. L'actuel launchctl envoie des signaux à launchd via XPC, et la logique de launchctl a été transférée en lui.

Examinons comment les applications sont effectivement arrêtées. Avant d'envoyer le signal SIGTERM, l'application essaie d'être arrêtée à l'aide de l'appel système "proc_terminate".

...
	error = proc_terminate(j->p, &sig);
	if (error) {
		job_log(j, LOG_ERR | LOG_CONSOLE, "Impossible d'arrêter le job : %d : %s", error, strerror(error));
		job_log(j, LOG_NOTICE | LOG_CONSOLE, "Utilisation de l'option de secours pour arrêter le job...");
		error = kill2(j->p, SIGTERM);
		if (error) {
			job_log(j, LOG_ERR, "Impossible de signaler le job : %d : %s", error, strerror(error));
		}
...

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

Meurtre indirect - limitation sur les 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 la limitation des ressources des applications, par exemple pour un indexeur, une limitation sur le quota de temps processeur et de mémoire, afin que le système ne ralentisse pas de manière significative à cause des actions de mise en cache de fichiers. Si l'application atteint la limitation des ressources, comme on peut le voir dans la fonction proc_apply_resource_actions, un signal SIGKILL est envoyé au processus.

Bien que cet appel système puisse potentiellement provoquer la mort d'un processus, le système ne vérifiait pas adéquatement les droits du processus appelant l'appel système. En réalité, la vérification existait, mais il suffit d'utiliser le drapeau alternatif PROC_POLICY_ACTION_SET pour contourner cette condition.

À partir de là, si vous "limitez" le quota d'utilisation du CPU par l'application (par exemple, en permettant uniquement 1 ns d'exécution), cela peut aboutir à la terminaison de n'importe quel processus dans le système. De cette manière, un logiciel malveillant peut tuer n'importe quel processus sur le système, y compris celui de l'antivirus. L'effet intéressant se produit également lors de la terminaison d'un processus avec pid 1 (launchctl) — un panic du noyau au moment de tenter de traiter le signal SIGKILL 🙂

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

Comment résoudre le problème ?

Le moyen le plus direct d'empêcher la terminaison d'un processus est de remplacer le pointeur de fonction dans la table des appels système. Malheureusement, cette méthode est non triviale pour de nombreuses raisons.

Tout d'abord, le symbole qui correspond à la position de sysent en mémoire est non seulement un symbole privé du noyau XNU, mais il ne peut également pas être trouvé parmi les symboles du noyau. Il faudra utiliser des méthodes heuristiques de recherche, comme le désassemblage dynamique de la fonction et la recherche de pointeur à l'intérieur.

Deuxièmement, la structure des enregistrements dans la table dépend des drapeaux avec lesquels le noyau a été compilé. Si le drapeau CONFIG_REQUIRES_U32_MUNGING est déclaré, la taille de la structure sera modifiée — un champ supplémentaire sera ajouté. sy_arg_munge32. Il est nécessaire de faire une vérification supplémentaire pour s'assurer avec quel drapeau le noyau a été compilé, par exemple, en comparant les pointeurs de fonction avec ceux connus.

struct sysent {         
        /* table des appels système */
        sy_call_t       *sy_call;       /* fonction d'implémentation */
#if CONFIG_REQUIRES_U32_MUNGING || (__arm__ && (__BIGGEST_ALIGNMENT__ > 4))
        sy_munge_t      *sy_arg_munge32; /* modificateur des arguments d'appel système pour processus 32 bits */
#endif
        int32_t         sy_return_type; /* types de retour d'appels système */
        int16_t         sy_narg;        /* nombre d'arguments */
        uint16_t        sy_arg_bytes;   /* Taille totale des arguments en octets pour
                                         * appels système 32 bits
                                         */
};

Heureusement, dans les versions modernes de macOS, Apple fournit une nouvelle API pour travailler avec les processus. L'API de sécurité des endpoints permet aux clients d'autoriser de nombreuses requêtes vers d'autres processus. Ainsi, il est possible de bloquer tous les signaux vers les processus, y compris le signal SIGKILL grâce à l'API mentionnée ci-dessus.

#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 même, un MAC Policy peut être enregistré dans le noyau, fournissant une méthode de protection contre les signaux (policy proc_check_signal), mais l'API n'est pas officiellement supportée.

Protection de l'extension du noyau

En plus de protéger les processus dans le système, une protection de l'extension du noyau (kext) elle-même est également nécessaire. macOS fournit aux développeurs un cadre pour un développement facile de pilotes de périphériques via IOKit. En plus de fournir des moyens de travailler avec des périphériques, IOKit offre des méthodes de empilement de pilotes (driver stacking) par le biais d'instances de classes C++. Une application en espace utilisateur pourra « trouver » une instance de classe enregistrée pour établir une connexion entre le noyau et l'espace utilisateur.

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

my_kext_ioservice = 1
my_kext_iouserclient = 1

Toute extension de noyau cherchant à s'enregistrer dans la pile de pilotes doit déclarer une classe héritée de IOService, par exemple, my_kext_ioservice dans ce cas. La connexion d'applications utilisateur provoque la création d'une nouvelle instance de classe, qui hérite de IOUserClient, dans l'exemple my_kext_iouserclient.

Lors de la tentative de déchargement d'un pilote du système (commande kextunload), la fonction virtuelle « bool terminate(IOOptionBits options) » est appelée. Il suffit de retourner false lors de l'appel de la fonction terminate durant la tentative de déchargement pour interdire kextunload.

bool Kext::terminate(IOOptionBits options)
{

  if (!IsUnloadAllowed)
  {
    // Le déchargement n'est pas autorisé, retour de false
    return false;
  }

  return super::terminate(options);
}

Le drapeau IsUnloadAllowed peut être défini par IOUserClient lors du chargement. Lorsqu'il y a une restriction de chargement, la commande kextunload renverra la sortie suivante :

admin@admins-Mac drivermanager % sudo kextunload ./test.kext
Mot de passe :
(kernel) Impossible de supprimer kext my.kext.test ; les services n'ont pas pu se terminer - 0xe00002c7.
Échec du déchargement de my.kext.test - (iokit/common) fonction non prise en charge.

Une protection similaire doit être mise en œuvre également pour IOUserClient. Les instances de classes peuvent être déchargées à l'aide de la fonction en espace utilisateur IOKitLib « IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description); ». On peut retourner false lors de l'appel de la commande « terminate » tant que l'application en espace utilisateur n'est pas « morte », c'est-à-dire qu'il n'y aura pas d'appel à la fonction « clientDied ».

Protection des fichiers

Pour protéger les fichiers, il suffit d'utiliser l'API Kauth, qui permet de restreindre l'accès aux fichiers. Apple fournit aux développeurs des notifications sur divers événements dans le scope, pour nous, les opérations KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA et KAUTH_VNODE_DELETE_CHILD sont importantes. Il est plus simple de limiter l'accès aux fichiers par chemin - nous utilisons l'API « vn_getpath » pour obtenir le chemin du fichier et effectuons une comparaison avec le préfixe du chemin. Notons 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 qui a été renommé. Il est nécessaire de comparer le chemin parent et de restreindre KAUTH_VNODE_DELETE pour celui-ci.

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

Un inconvénient de cette approche peut être une faible performance lorsque le nombre de préfixes augmente. Pour que la comparaison ne soit pas égale à O(prefix * length), où prefix est le nombre de préfixes et length la longueur de la chaîne, on peut utiliser un automate fini déterministe (AFD) construit à partir des préfixes.

Considérons la manière de construire un AFD pour cet ensemble de préfixes. Nous initialisons des curseurs au début de chaque préfixe. Si tous les curseurs pointent vers le même caractère, nous déplaçons chaque curseur de un caractère et retenons que la longueur de la chaîne identique augmente d'une unité. Si deux curseurs existent et que les caractères sous eux sont différents, nous divisons les curseurs en groupes selon le caractère vers lequel ils pointent, puis nous répétons l'algorithme pour chaque groupe.

Dans le premier cas (tous les caractères sous les curseurs sont identiques), nous obtenons un état de l'AFD qui a uniquement une transition par chaîne identique. Dans le deuxième cas, nous obtenons une table de transitions de taille 256 (nombre de caractères et nombre maximum de groupes) vers les états suivants, obtenu lors de l'appel récursif de la fonction.

Considérons un exemple. Pour l'ensemble de préfixes (“/foo/bar/tmp/”, “/var/db/foo/”, “/foo/bar/aba/”, “foo/bar/aac/”), on peut obtenir l'AFD suivant. Sur l'image, seules les transitions menant à d'autres états sont indiquées, les autres transitions ne seront pas finales.

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

Lors du passage par les états de l'AFD, trois cas peuvent se présenter.

  1. Un état final a été atteint - le chemin est protégé, nous limitons les opérations KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA et KAUTH_VNODE_DELETE_CHILD.
  2. L'état final n'a pas été atteint, mais le chemin "s'est terminé" (un terminateur nul a été atteint) - le chemin est parent, il est nécessaire de restreindre KAUTH_VNODE_DELETE. Notons que si le vnode est un dossier, il faut ajouter ‘/’ à la fin, sinon une restriction pourrait s'appliquer au fichier “/foor/bar/t”, ce qui est incorrect.
  3. L'état final n'a pas été atteint, le chemin ne s'est pas terminé. Aucun des préfixes ne correspond, aucune restriction n'est appliquée.

Conclusion

L'objectif des solutions de sécurité en développement 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 Acronis, qui comble les vulnérabilités là où le système d'exploitation lui-même est "faible". D'autre part, il ne faut pas négliger le renforcement des aspects de sécurité qui peuvent être améliorés du côté de l'OS, d'autant plus que la fermeture de telles vulnérabilités renforce notre propre résilience en tant que produit. La vulnérabilité a été signalée par l'Apple Product Security Team et a été corrigée dans macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

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

Tout cela ne peut être fait que si votre utilitaire a été officiellement installé dans le noyau. Cela signifie qu'il n'y a pas de failles pour les logiciels externes et indésirables. Cependant, comme vous le voyez, même pour protéger des programmes légitimes, tels que les antivirus et les systèmes de sauvegarde, il faut 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.

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster