Hej, Habr! I dag vil jeg gerne tale om, hvordan du kan beskytte processer mod angreb fra angribere i macOS. For eksempel er dette nyttigt til et antivirus- eller backupsystem, især da der under macOS er flere måder at "dræbe" en proces på. Læs om dette og beskyttelsesmetoder under snittet.
Den klassiske måde at "dræbe" en proces
En velkendt måde at "dræbe" en proces på er at sende et SIGKILL-signal til processen. Gennem bash kan du kalde standarden "kill -SIGKILL PID" eller "pkill -9 NAME" for at dræbe. "kill"-kommandoen har været kendt siden UNIX-dagene og er tilgængelig ikke kun på macOS, men også på andre UNIX-lignende systemer.
Ligesom i UNIX-lignende systemer giver macOS dig mulighed for at opsnappe alle signaler til en proces undtagen to - SIGKILL og SIGSTOP. Denne artikel vil primært fokusere på SIGKILL-signalet som et signal, der får en proces til at blive dræbt.
macOS-specifikationer
På macOS kalder kill-systemkaldet i XNU-kernen funktionen psignal(SIGKILL,...). Lad os prøve at se, hvilke andre brugerhandlinger i brugerområdet kan kaldes af psignal-funktionen. Lad os luge ud opkald til psignal-funktionen i kernens interne mekanismer (selvom de kan være ikke-trivielle, lader vi dem stå til en anden artikel 🙂 - signaturbekræftelse, hukommelsesfejl, exit/terminate-håndtering, filbeskyttelsesovertrædelser osv. .
Lad os starte gennemgangen med funktionen og det tilsvarende systemkald
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);
}
...
}
lanceret
Standardmåden til at skabe dæmoner ved systemstart og kontrollere deres levetid er lanceret. Bemærk venligst, at kilderne er til den gamle version af launchctl op til macOS 10.10, kodeeksempler er givet til illustrative formål. Moderne launchctl sender lancerede signaler via XPC, launchctl logik er blevet flyttet til den.
Lad os se på, hvordan applikationer præcist stoppes. Før SIGTERM-signalet sendes, forsøges applikationen at blive stoppet ved hjælp af "proc_terminate"-systemkaldet.
<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));
}
...
<>
Under motorhjelmen kan proc_terminate, på trods af sit navn, sende ikke kun psignal med SIGTERM, men også SIGKILL.
Indirekte dræb - Ressourcegrænse
En mere interessant sag kan ses i et andet systemkald
Selvom dette systemkald potentielt kunne dræbe en proces, kontrollerede systemet ikke tilstrækkeligt rettighederne til den proces, der kalder systemkaldet. Tjekker faktisk
Derfor, hvis du "begrænser" applikationens CPU-brugskvote (for eksempel tillader kun 1 ns at køre), så kan du dræbe enhver proces i systemet. Således kan malwaren dræbe enhver proces på systemet, inklusive antivirusprocessen. Også interessant er effekten, der opstår, når en proces dræbes med pid 1 (launchctl) - kernepanik, når man forsøger at behandle SIGKILL-signalet :)
Hvordan løses problemet?
Den mest ligetil måde at forhindre en proces i at blive dræbt er at erstatte funktionsmarkøren i systemkaldstabellen. Desværre er denne metode ikke-triviel af mange grunde.
For det første er symbolet, der styrer sysents hukommelsesplacering, ikke kun privat for XNU-kernesymbolet, men kan ikke findes i kernesymboler. Du bliver nødt til at bruge heuristiske søgemetoder, såsom dynamisk adskillelse af funktionen og søgning efter en pointer i den.
For det andet afhænger strukturen af indtastninger i tabellen af de flag, som kernen blev kompileret med. Hvis CONFIG_REQUIRES_U32_MUNGING-flaget erklæres, vil størrelsen af strukturen blive ændret - et ekstra felt vil blive tilføjet
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
*/
};
Heldigvis leverer Apple i moderne versioner af macOS en ny API til at arbejde med processer. Endpoint Security API giver klienter mulighed for at godkende mange anmodninger til andre processer. Således kan du blokere alle signaler til processer, inklusive SIGKILL-signalet, ved hjælp af ovennævnte API.
#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;
}
På samme måde kan en MAC-politik registreres i kernen, som giver en signalbeskyttelsesmetode (policy proc_check_signal), men API'et er ikke officielt understøttet.
Kerneludvidelsesbeskyttelse
Udover at beskytte processer i systemet er det også nødvendigt at beskytte selve kerneudvidelsen (kext). macOS giver en ramme for udviklere til nemt at udvikle IOKit-enhedsdrivere. Ud over at levere værktøjer til at arbejde med enheder, giver IOKit metoder til driverstabling ved hjælp af forekomster af C++-klasser. En applikation i brugerområdet vil være i stand til at "finde" en registreret forekomst af klassen for at etablere et kerne-brugerområde-forhold.
For at detektere antallet af klasseforekomster i systemet er der hjælpeprogrammet ioclasscount.
my_kext_ioservice = 1
my_kext_iouserclient = 1
Enhver kerneudvidelse, der ønsker at registrere sig med driverstakken, skal deklarere en klasse, der arver fra IOService, for eksempel my_kext_ioservice i dette tilfælde. Tilslutning af brugerapplikationer forårsager oprettelsen af en ny instans af klassen, der arver fra IOUserClient, i eksemplet my_kext_iouserclient.
Når du forsøger at fjerne en driver fra systemet (kextunload-kommando), kaldes den virtuelle funktion "bool terminate(IOOptionBits options)". Det er nok at returnere falsk på opkaldet for at afslutte, når du prøver at losse for at deaktivere kextunload.
bool Kext::terminate(IOOptionBits options)
{
if (!IsUnloadAllowed)
{
// Unload is not allowed, returning false
return false;
}
return super::terminate(options);
}
IsUnloadAllowed-flaget kan indstilles af IOUserClient ved indlæsning. Når der er en downloadgrænse, vil kommandoen kextunload returnere følgende output:
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.
Tilsvarende beskyttelse skal udføres for IOUserClient. Forekomster af klasser kan fjernes ved hjælp af IOKitLib-brugerrumsfunktionen "IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);". Du kan returnere false, når du kalder kommandoen "terminate", indtil userspace-applikationen "dør", det vil sige, at "clientDied"-funktionen ikke kaldes.
Filbeskyttelse
For at beskytte filer er det nok at bruge Kauth API, som giver dig mulighed for at begrænse adgangen til filer. Apple giver udviklere meddelelser om forskellige hændelser i omfanget; for os er operationerne KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA og KAUTH_VNODE_DELETE_CHILD vigtige. Den nemmeste måde at begrænse adgangen til filer på er via sti - vi bruger "vn_getpath" API til at få stien til filen og sammenligne stipræfikset. Bemærk, at for at optimere omdøbningen af filmappestier, tillader systemet ikke adgang til hver fil, men kun til selve mappen, der er blevet omdøbt. Det er nødvendigt at sammenligne den overordnede sti og begrænse KAUTH_VNODE_DELETE for den.
Ulempen ved denne fremgangsmåde kan være lav ydeevne, efterhånden som antallet af præfikser stiger. For at sikre at sammenligningen ikke er lig med O(præfiks*længde), hvor præfiks er antallet af præfikser, længde er længden af strengen, kan du bruge en deterministisk finit automat (DFA) bygget af præfikser.
Lad os overveje en metode til at konstruere en DFA for et givet sæt præfikser. Vi initialiserer markørerne i begyndelsen af hvert præfiks. Hvis alle markører peger på det samme tegn, skal du øge hver markør med ét tegn og huske, at længden af den samme linje er én længere. Hvis der er to markører med forskellige symboler, opdel markørerne i grupper efter det symbol, de peger på, og gentag algoritmen for hver gruppe.
I det første tilfælde (alle tegnene under markørerne er de samme), får vi en DFA-tilstand, der kun har én overgang langs den samme linje. I det andet tilfælde får vi en tabel over overgange af størrelse 256 (antal tegn og maksimalt antal grupper) til efterfølgende tilstande opnået ved rekursivt at kalde funktionen.
Lad os se på et eksempel. For et sæt præfikser ("/foo/bar/tmp/", "/var/db/foo/", "/foo/bar/aba/", "foo/bar/aac/") kan du få følgende DFA. Figuren viser kun overgange, der fører til andre stater; andre overgange vil ikke være endelige.
Ved gennemgang af DKA-staterne kan der være tale om 3 tilfælde.
- Den endelige tilstand er nået - stien er beskyttet, vi begrænser operationerne KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA og KAUTH_VNODE_DELETE_CHILD
- Den endelige tilstand blev ikke nået, men stien "sluttede" (nul-terminatoren blev nået) - stien er en forælder, det er nødvendigt at begrænse KAUTH_VNODE_DELETE. Bemærk, at hvis vnode er en mappe, skal du tilføje et '/' i slutningen, ellers kan det begrænse det til filen "/foor/bar/t", hvilket er forkert.
- Den endelige tilstand blev ikke nået, vejen sluttede ikke. Ingen af præfikserne matcher denne, vi indfører ikke begrænsninger.
Konklusion
Målet med de sikkerhedsløsninger, der udvikles, er at øge sikkerhedsniveauet for brugeren og dennes data. På den ene side opnås dette mål ved udviklingen af Acronis-softwareproduktet, som lukker de sårbarheder, hvor selve operativsystemet er "svagt". På den anden side bør vi ikke forsømme at styrke de sikkerhedsaspekter, der kan forbedres på OS-siden, især da lukning af sådanne sårbarheder øger vores egen stabilitet som et produkt. Sårbarheden blev rapporteret til Apple Product Security Team og er blevet rettet i macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).
Alt dette kan kun gøres, hvis dit hjælpeprogram er blevet officielt installeret i kernen. Det vil sige, at der ikke er sådanne smuthuller for ekstern og uønsket software. Men som du kan se, kræver selv beskyttelse af legitime programmer såsom antivirus- og sikkerhedskopieringssystemer arbejde. Men nu vil nye Acronis-produkter til macOS have yderligere beskyttelse mod aflæsning fra systemet.
Kilde: www.habr.com