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.
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 ES | Auth ? |
|---|---|---|
open | ES_EVENT_TYPE_*_OPEN | oui |
read, write | non exposé — utilisez OPEN + suivi de fd | — |
unlink, unlinkat | ES_EVENT_TYPE_*_UNLINK | oui |
rename | ES_EVENT_TYPE_*_RENAME | oui |
link | ES_EVENT_TYPE_*_LINK | oui |
mount, unmount | ES_EVENT_TYPE_*_MOUNT, _UNMOUNT | oui |
execve, posix_spawn | ES_EVENT_TYPE_*_EXEC | oui |
fork | ES_EVENT_TYPE_NOTIFY_FORK | non |
exit, kill | ES_EVENT_TYPE_NOTIFY_EXIT, _SIGNAL | non |
chmod, fchmod, fchmodat | ES_EVENT_TYPE_*_SETMODE | oui |
chown, fchown, lchown, fchownat | ES_EVENT_TYPE_*_SETOWNER | oui |
setxattr, fsetxattr | ES_EVENT_TYPE_*_SETEXTATTR | oui |
clonefileat, fclonefileat | ES_EVENT_TYPE_*_CLONE | oui |
csops, csops_audittoken | ES_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 ES | Auth ? |
|---|---|---|
task_for_pid | ES_EVENT_TYPE_*_GET_TASK | oui |
task_name_for_pid | ES_EVENT_TYPE_*_GET_TASK_NAME | oui |
task_read_for_pid | ES_EVENT_TYPE_*_GET_TASK_READ | oui |
task_inspect_for_pid | ES_EVENT_TYPE_*_GET_TASK_INSPECT | oui |
| IPC Mach en général | non exposé | — |
_kernelrpc_mach_port_*_trap, ports en général | non 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— invocationssudo.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
-
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()retourneES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED. -
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.
-
Explosion d'événements souscrits. S'abonner à
NOTIFY_OPENsystem-wide donne des milliers d'événements/sec sur un Mac normal. Filtrez tôt pares_process_t.signing_idet envisagez de muter les daemons Apple signés. -
AUTH_OPENest un risque perf. Les événements auth suropenralentissent visiblement le système. La plupart des EDR de production utilisentNOTIFY_OPENplus duAUTH_OPENsélectif pour les chemins sensibles (Keychain, Documents, jars de cookies navigateur). -
Aucun argument syscall hors de ce qu'ES expose. Voir le
flagsdeopen? Il est dans le payload (es_event_open_t::fflag). Voir leprotdemmap? 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
- Le post compagnon
task_for_pid : le Mach trap le plus dangereux de macOSexplique ce que le noyau fait réellement quandAUTH_GET_TASKse déclenche. - Le catalogue de référence donne l'historique par version de chaque syscall, qui se marie avec la stabilité des event-types ES.
- Tracer les syscalls macOS avec dtrace sur Apple Silicon couvre l'alternative bas-niveau quand ES ne suffit pas.