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 attaques d'attaquants sous macOS. Par exemple, cela est utile pour un antivirus ou un système de sauvegarde, d’autant plus que sous macOS il existe plusieurs manières de « tuer » un processus. Lisez à ce sujet et les méthodes de protection sous la coupure.

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

La manière classique de « tuer » un processus

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

Tout comme dans les systèmes de type UNIX, macOS vous permet d'intercepter tous les signaux envoyés à un processus, sauf deux : SIGKILL et SIGSTOP. Cet article se concentrera principalement sur le signal SIGKILL en tant que signal qui provoque l'arrêt d'un processus.

Spécificités de 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 être appelées par la fonction psignal. É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, gestion des sorties/terminations, violations de protection des fichiers, etc. .

Commençons la revue avec la fonction et l'appel système correspondant terminate_with_payload. On constate qu’en plus du kill call classique, il existe une approche alternative spécifique au système d’exploitation macOS et introuvable dans BSD. Les principes de fonctionnement des deux appels système sont également similaires. Ce sont des appels directs à la fonction psignal du noyau. Notez également qu'avant de tuer un processus, une vérification « 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);
	}
...
}

lancé

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. Veuillez noter que les sources concernent l'ancienne version de launchctl jusqu'à macOS 10.10, des exemples de code sont fournis à titre illustratif. Le launchctl moderne envoie des signaux launchd via XPC, la logique launchctl y a été déplacée.

Voyons exactement comment les applications sont arrêtées. Avant d'envoyer le signal SIGTERM, l'application est tentée d'être arrêtée à 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 un signal p avec SIGTERM, mais aussi SIGKILL.

Tuerie indirecte – Limite de ressources

Un cas plus intéressant peut être vu dans un autre appel système processus_politique. Une utilisation courante de cet appel système consiste à limiter les ressources de l'application, par exemple pour qu'un indexeur limite le temps CPU et les quotas de mémoire afin que le système ne soit pas significativement ralenti par les activités de mise en cache des fichiers. Si une application a atteint sa limite de ressources, comme le montre la fonction proc_apply_resource_actions, un 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é de manière adéquate les droits du processus appelant l'appel système. En fait, je vérifie existé, 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 de l’application (par exemple, n’autorisez que 1 ns à s’exécuter), vous pouvez alors tuer n’importe quel processus du système. Ainsi, le malware peut tuer n’importe quel processus du système, y compris le processus antivirus. L'effet qui se produit lors de la suppression d'un processus avec le pid 1 (launchctl) est également intéressant - panique du noyau lors de la tentative de traitement du signal SIGKILL :)

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

Comment résoudre le problème?

Le moyen le plus simple d'empêcher la suppression d'un processus consiste à remplacer le pointeur de fonction dans la table des appels système. Malheureusement, cette méthode n’est pas triviale pour plusieurs raisons.

Premièrement, le symbole qui contrôle l'emplacement mémoire du système est non seulement privé du symbole du noyau XNU, mais ne peut pas être trouvé dans les symboles du noyau. Vous devrez utiliser des méthodes de recherche heuristiques, telles que le désassemblage dynamique de la fonction et la recherche d'un pointeur dans celle-ci.

Deuxièmement, la structure des entrées dans le tableau dépend des options avec lesquelles le noyau a été compilé. Si le flag 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 d'effectuer une vérification supplémentaire pour déterminer avec quel indicateur le noyau a été compilé, ou bien de vérifier les pointeurs de fonction par rapport à ceux connus.

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 propose une nouvelle API pour travailler avec les processus. L'API Endpoint Security permet aux clients d'autoriser de nombreuses requêtes adressées à d'autres processus. Ainsi, vous pouvez bloquer tous les signaux envoyés aux processus, y compris le signal SIGKILL, à l'aide de 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, une stratégie MAC peut être enregistrée 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 du 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ériques IOKit. En plus de fournir des outils pour travailler avec des périphériques, IOKit fournit des méthodes d'empilement de pilotes à l'aide d'instances de classes C++. Une application dans l'espace utilisateur sera capable de « trouver » une instance enregistrée de la classe pour établir une relation noyau-espace utilisateur.

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

my_kext_ioservice = 1
my_kext_iouserclient = 1

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

Lorsque vous essayez de décharger un pilote du système (commande kextunload), la fonction virtuelle « bool terminate(IOOptionBits options) » est appelée. Il suffit de renvoyer false lors de l'appel à terminer lors de la tentative de déchargement 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 lors du chargement. Lorsqu'il existe une limite de téléchargement, la commande kextunload renvoie le résultat suivant :

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 classes peuvent être déchargées à l'aide de la fonction d'espace utilisateur IOKitLib « IOCatalogueTerminate (mach_port_t, uint32_t flag, io_name_t description) ; ». Vous pouvez renvoyer false lors de l'appel de la commande « terminate » jusqu'à ce que l'application de l'espace utilisateur « meure », c'est-à-dire que la fonction « clientDied » ne soit pas appelée.

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 cadre ; pour nous, les opérations KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA et KAUTH_VNODE_DELETE_CHILD sont importantes. Le moyen le plus simple de restreindre l'accès aux fichiers est par chemin - nous utilisons l'API « vn_getpath » pour obtenir le chemin d'accès au fichier et comparer le préfixe du chemin. A noter que pour optimiser le renommage des chemins des dossiers de 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 celui-ci.

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

L'inconvénient de cette approche peut être une faible performance à mesure que le nombre de préfixes augmente. Pour vous assurer que la comparaison n'est pas égale à O(prefix*length), où prefix est le nombre de préfixes, length est la longueur de la chaîne, vous pouvez utiliser un automate fini déterministe (DFA), construit par préfixes.

Considérons une méthode pour construire un DFA pour un ensemble donné de préfixes. On initialise les curseurs au début de chaque préfixe. Si tous les curseurs pointent vers le même caractère, augmentez chaque curseur d'un caractère et rappelez-vous que la longueur de la même ligne est supérieure d'un caractère. S'il y a deux curseurs avec des symboles différents, divisez les curseurs en groupes en fonction du symbole vers lequel ils pointent et répétez l'algorithme pour chaque groupe.

Dans le premier cas (tous les caractères sous les curseurs sont les mêmes), on obtient un état DFA qui n'a qu'une seule transition le long de la même ligne. Dans le second cas, on obtient un tableau de transitions de taille 256 (nombre de caractères et nombre maximum de groupes) vers les états suivants obtenus en appelant récursivement la fonction.

Regardons un exemple. Pour un ensemble de préfixes (« /foo/bar/tmp/ », « /var/db/foo/ », « /foo/bar/aba/ », « foo/bar/aac/ »), vous pouvez obtenir ce qui suit DFAE. La figure ne montre que les transitions menant à d’autres états ; les autres transitions ne seront pas définitives.

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

En passant par les états DKA, il peut y avoir 3 cas.

  1. L'état final est atteint - le chemin est protégé, on limite 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é" (le terminateur nul a été atteint) - le chemin est un parent, il faut limiter KAUTH_VNODE_DELETE. Notez que si vnode est un dossier, vous devez ajouter un '/' à la fin, sinon cela risque de le limiter au fichier « /foor/bar/t », ce qui est incorrect.
  3. L’état final n’a pas été atteint, le chemin n’a pas pris fin. Aucun des préfixes ne correspond à celui-ci, nous n'introduisons aucune restriction.

Conclusion

L'objectif des solutions de sécurité en cours de développement est d'augmenter le niveau de sécurité de l'utilisateur et de ses données. D'une part, cet objectif est atteint grâce au développement du produit logiciel Acronis, qui corrige 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 le renforcement des aspects de sécurité qui peuvent être améliorés du côté du système d'exploitation, d'autant plus que la suppression de ces vulnérabilités augmente notre propre stabilité en tant que produit. La vulnérabilité a été signalée à 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).

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

Tout cela ne peut être fait que si votre utilitaire a été officiellement installé dans le noyau. Autrement dit, il n’existe pas de failles de ce type pour les logiciels externes et indésirables. Cependant, comme vous pouvez le constater, même la protection de programmes légitimes tels que les systèmes antivirus et de sauvegarde nécessite du travail. Mais désormais, les nouveaux produits Acronis pour macOS bénéficieront d'une protection supplémentaire contre le déchargement du système.

Source: habr.com

Ajouter un commentaire