Skip to content

Détecter l'abus de syscalls avec macOS Endpoint Security en 2026

Correspondance pratique entre les syscalls macOS et les événements Endpoint Security pour la détection en EDR moderne.

Publié le 5 min de lecture

Pendant dix ans, « écrire un EDR pour macOS » signifiait surtout écrire une extension noyau. Cette porte s'est fermée avec macOS 11 — les kexts sont dépréciés, les kexts signés requièrent notarisation, et les System Extensions sont le chemin supporté. La moitié espace-utilisateur de ce chemin est le framework Endpoint Security (ES) : un flux d'événements médiés par le noyau qui expose ~80 événements curatés à un es_client_t utilisateur. En 2026, ES est la seule voie pratique pour livrer un produit de détection moderne sur macOS.

Mais ES n'est pas un traceur de syscalls. Il expose un sous-ensemble d'événements système choisis à la main, souvent à un niveau d'abstraction plus élevé que le syscall sous-jacent, et il n'y a pas d'auto-mapping. Si vous venez d'un EDR Linux avec kprobes partout vers un déploiement macOS, il vous faut une table de traduction.

Ce post est cette table — plus les pièges.

La forme d'un événement ES

Chaque événement arrive comme un es_message_t :

typedef struct {
    es_event_type_t   event_type;       // ex. ES_EVENT_TYPE_NOTIFY_OPEN
    es_action_type_t  action_type;      // AUTH (vous pouvez bloquer) ou NOTIFY (enregistrer seul)
    es_process_t     *process;          // qui l'a fait
    es_event_t        event;            // payload propre au type d'événement
    uint64_t          time;             // mach_continuous_time
    uint64_t          mach_time;
} es_message_t;

Les événements AUTH bloquent jusqu'à la réponse — vous laissant refuser une opération en temps réel. Les NOTIFY sont fire-and-forget. La plupart des événements au niveau syscall existent dans les deux saveurs.

La correspondance : syscalls BSD

Pour la surface POSIX, les correspondances les plus importantes :

Syscall (avec lien)Événement ESAuth ?
openES_EVENT_TYPE_*_OPENoui
read, writenon exposé — utilisez OPEN + suivi de fd
unlink, unlinkatES_EVENT_TYPE_*_UNLINKoui
renameES_EVENT_TYPE_*_RENAMEoui
linkES_EVENT_TYPE_*_LINKoui
mount, unmountES_EVENT_TYPE_*_MOUNT, _UNMOUNToui
execve, posix_spawnES_EVENT_TYPE_*_EXECoui
forkES_EVENT_TYPE_NOTIFY_FORKnon
exit, killES_EVENT_TYPE_NOTIFY_EXIT, _SIGNALnon
chmod, fchmod, fchmodatES_EVENT_TYPE_*_SETMODEoui
chown, fchown, lchown, fchownatES_EVENT_TYPE_*_SETOWNERoui
setxattr, fsetxattrES_EVENT_TYPE_*_SETEXTATTRoui
clonefileat, fclonefileatES_EVENT_TYPE_*_CLONEoui
csops, csops_audittokenES_EVENT_TYPE_NOTIFY_CS_INVALIDATED (lié)non

Les grandes absences sont read et write : ES ne les expose pas. Le volume serait intenable. Les défenseurs suivent les descripteurs intéressants en s'abonnant à OPEN et en mémorisant le vnode + fd résolu ; les read/write suivants se déduisent du contexte.

La correspondance : Mach traps

Le côté Mach est là où ES devient vraiment intéressant, car rien d'autre n'expose ces événements :

Mach trap (avec lien)Événement ESAuth ?
task_for_pidES_EVENT_TYPE_*_GET_TASKoui
task_name_for_pidES_EVENT_TYPE_*_GET_TASK_NAMEoui
task_read_for_pidES_EVENT_TYPE_*_GET_TASK_READoui
task_inspect_for_pidES_EVENT_TYPE_*_GET_TASK_INSPECToui
IPC Mach en généralnon exposé
_kernelrpc_mach_port_*_trap, ports en généralnon exposé

L'événement vedette pour la sécurité est GET_TASK — c'est ainsi qu'on attrape le prérequis de toute injection de code macOS. Abonnez-vous à AUTH_GET_TASK, construisez une allowlist de binaires débogueurs/profilers légitimes (lldb, sample, vmmap, Xcode, Activity Monitor, Instruments), et refusez le reste. Faux positifs très bas sur un poste utilisateur typique.

Pour l'IPC Mach brute, pas d'événement. Si vous devez inspecter le flux de messages, retour à DTrace (voir Tracer les syscalls macOS avec dtrace sur Apple Silicon) ou à l'instrumentation côté noyau.

Ce qu'ES vous donne que les syscalls ne donnent pas

ES n'est pas qu'un miroir des syscalls — il expose aussi des événements macOS de haut niveau sans équivalent syscall direct :

  • ES_EVENT_TYPE_NOTIFY_LOGIN_LOGIN / _LOGOUT — cycle de vie de session à la TCC.
  • ES_EVENT_TYPE_*_XPC_CONNECT — établissement de connexion XPC (l'intention d'un canal IPC, pas les messages eux-mêmes).
  • ES_EVENT_TYPE_*_BTM_LAUNCH_ITEM_ADD — ajouts Background Task Management (détection de persistance).
  • ES_EVENT_TYPE_*_SUDO — invocations sudo.
  • ES_EVENT_TYPE_*_SCREENSHARING_ATTACH — début de session de partage d'écran.

Pour la détection de persistance, les événements BTM_* sont bien plus utiles que de surveiller les écritures LaunchAgent via OPEN — BTM est l'endroit où Apple a consolidé l'enregistrement des launch-items dans macOS 13+, et il expose chaque Login Item, LaunchAgent, LaunchDaemon et Helper comme un événement structuré.

Pièges pratiques

  1. Vous avez besoin d'un Developer ID payant et de l'entitlement Endpoint Security. Apple l'accorde au cas par cas aux éditeurs de sécurité. Sans lui, es_new_client() retourne ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED.

  2. Les événements AUTH ont une deadline stricte. Chaque type a sa fenêtre de réponse (5 secondes pour la plupart). Si vous la ratez, le système auto-allow et marque votre client non-réactif. Construisez un pool de workers ; pas d'appels réseau synchrones dans le handler.

  3. Explosion d'événements souscrits. S'abonner à NOTIFY_OPEN system-wide donne des milliers d'événements/sec sur un Mac normal. Filtrez tôt par es_process_t.signing_id et envisagez de muter les daemons Apple signés.

  4. AUTH_OPEN est un risque perf. Les événements auth sur open ralentissent visiblement le système. La plupart des EDR de production utilisent NOTIFY_OPEN plus du AUTH_OPEN sélectif pour les chemins sensibles (Keychain, Documents, jars de cookies navigateur).

  5. Aucun argument syscall hors de ce qu'ES expose. Voir le flags de open ? Il est dans le payload (es_event_open_t::fflag). Voir le prot de mmap ? Non exposé. Pour tout ce qui sort du payload curaté, retour à DTrace.

Un client minimal

Le squelette utile le plus court :

es_client_t *client = NULL;
es_new_client_result_t r = es_new_client(&client, ^(es_client_t *c, const es_message_t *msg) {
    if (msg->event_type == ES_EVENT_TYPE_AUTH_GET_TASK) {
        // Par défaut : autoriser. Construisez votre allowlist ici.
        es_respond_auth_result(c, msg, ES_AUTH_RESULT_ALLOW, false);
    }
});
if (r != ES_NEW_CLIENT_RESULT_SUCCESS) { /* gérer */ }

es_event_type_t events[] = { ES_EVENT_TYPE_AUTH_GET_TASK };
es_subscribe(client, events, sizeof(events) / sizeof(events[0]));

Un watchdog task_for_pid en 10 lignes. La production y ajoute le suivi de généalogie de processus, la validation de signature, et un canal d'événements expédié en TLS.

Pour aller plus loin

Articles associés

Comment task_for_pid fonctionne, pourquoi Apple le verrouille, et ce que son modèle d'entitlements implique pour les outils de sécurité macOS.
Guide pratique pour tracer syscalls BSD et Mach traps avec DTrace sur macOS moderne — prérequis SIP, scripts qui marchent, et que faire quand les probes Apple disparaissent.
Ce qui se passe vraiment quand un programme arm64 sur Mac émet une instruction SVC — du stub utilisateur jusqu'au dispatcher BSD, et retour.