Hej, Habr! Idag skulle jag vilja prata om hur man kan skydda processer frÄn intrÄng av illasinnade anvÀndare i macOS. Detta Àr sÀrskilt anvÀndbart för antivirusprogram eller backup-system, sÀrskilt med tanke pÄ att det finns flera sÀtt att "döda" en process under macOS. LÀs mer om detta och metoderna för skydd nedan.
Klassiskt sÀtt att "döda" en process
Ett vÀlkÀnt sÀtt att "döda" en process Àr att skicka en SIGKILL-signal till processen. Via bash kan man anvÀnda de standardmÀssiga kommandona "kill -SIGKILL PID" eller "pkill -9 NAME" för att döda. Kommandot "kill" har funnits sedan UNIX-tiden och Àr tillgÀngligt inte bara i macOS utan Àven i andra UNIX-liknande system.
Precis som i UNIX-liknande system tillÄter macOS att fÄnga upp alla signaler till en process förutom tvÄ - SIGKILL och SIGSTOP. Denna artikel kommer fokusera pÄ signalen SIGKILL, vilken Àr den signal som orsakar att processen dödas.
Specifika egenskaper för macOS
I macOS anropar systemanropet kill i XNU-kĂ€rnan funktionen psignal(SIGKILL,âŠ). LĂ„t oss undersöka vilka andra anvĂ€ndarhandlingar i userspace som kan anropa funktionen psignal. Vi kommer att utesluta anrop av funktionen psignal i kĂ€rnans interna mekanismer (Ă€ven om de kan vara intressanta, sparar vi dem till en annan artikel đ â signaturkontroll, minnesfel, exit/terminate-hantering, filskyddsövertrĂ€delser osv.).
Vi börjar översikten med funktionen och det motsvarande systemanropet . Det Ă€r tydligt att förutom det klassiska kill-anropet finns det ett alternativt tillvĂ€gagĂ„ngssĂ€tt som Ă€r specifikt för operativsystemet macOS och som inte förekommer i BSD. Principerna för hur bĂ„da systemanropen fungerar Ă€r ocksĂ„ liknande. De utgör direkta anrop av kĂ€rnfunktionen psignal. LĂ„t oss ocksĂ„ notera att innan en process dödas görs en kontroll av âcansignalâ â om processen kan skicka en signal till en annan process, tillĂ„ter systemet inte att nĂ„gon applikation dödar systemprocesser till exempel.
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) {
d/*
* psignal_thread_with_reason() kommer att lÀgga en SIGKILL i kö för den specificerade trÄden eller
* ÄtervÀnda om trÄden och/eller uppgiften redan hÄller pÄ att avslutas. PÄ ett eller annat sÀtt,
* kommer den aktuella trÄden inte att ÄtervÀnda till anvÀndarutrymmet.
* /
psignal_thread_with_reason(target_proc, current_thread(), SIGKILL, signal_reason);
} else {
psignal_with_reason(target_proc, SIGKILL, signal_reason);
}
...
}
launchd
Det standardiserade sÀttet att skapa daemoner vid systemstart och styra deras livstid Àr launchd. Jag pÄpekar att kÀllkoden som ges Àr för en gammal version av launchctl före macOS 10.10, kodexemplen ges enbart som illustration. Den moderna launchctl skickar signaler till launchd via XPC, logiken för launchctl har flyttats dit.
LĂ„t oss titta pĂ„ hur stopp av applikationer verkligen utförs. Innan en SIGTERM-signal skickas försöker systemanropet âproc_terminateâ att stoppa applikationen.
<launchctl src/core.c>
...
error = proc_terminate(j->p, &sig);
if (error) {
job_log(j, LOG_ERR | LOG_CONSOLE, "Kunde inte avsluta uppdrag: %d: %s", error, strerror(error));
job_log(j, LOG_NOTICE | LOG_CONSOLE, "AnvÀnder fallback-alternativ för att avsluta uppdrag...");
error = kill2(j->p, SIGTERM);
if (error) {
job_log(j, LOG_ERR, "Kunde inte signalera uppdrag: %d: %s", error, strerror(error));
}
...
<>
Under huven kan proc_terminate, trots sitt namn, skicka inte bara psignal med SIGTERM, utan ocksÄ SIGKILL.
Indirekt dödande â resursbegrĂ€nsning
Ett mer intressant fall kan ses i ett annat systemanrop . Det standardiserade anvÀndandet av detta systemanrop Àr resursbegrÀnsningar för applikationer, exempelvis för indexerare begrÀnsning av kvoten för processortid och minne, sÄ att systemet inte avsevÀrt fördröjs av filens cachenoteringar. Om applikationen nÄr resursbegrÀnsningen, som man kan se frÄn funktionen proc_apply_resource_actions, sÄ skickas en SIGKILL-signal till processen.
Trots att detta systemanrop potentiellt kan utföra dödande av processen, har systemet inte korrekt kontrollerat rÀttigheterna för den process som kallar systemanropet. Faktum Àr att kontrollen , men det Àr tillrÀckligt att anvÀnda det alternativa flaggan PROC_POLICY_ACTION_SET för att kringgÄ detta villkor.
Om man "begrĂ€nsar" CPU-anvĂ€ndningen av en applikation (till exempel genom att tillĂ„ta att endast 1 ns körs), kan man döda vilket process som helst i systemet. En skadlig programvara kan döda vilken process som helst, inklusive antivirusprocessen. Det Ă€r ocksĂ„ intressant att notera effekten av att döda processen med pid 1 (launchctl) â kernel panic uppstĂ„r nĂ€r man försöker hantera signalen SIGKILL đ

Hur löser man problemet?
Det mest uppenbara sÀttet att förhindra dödandet av en process Àr att ersÀtta pekaren till funktionen i systemanropstabellen. TyvÀrr Àr detta tillvÀgagÄngssÀtt inte trivialt av mÄnga anledningar.
För det första Àr symbolen som ansvarar för sysent-positionen i minnet inte bara en privat symbol i XNU-kÀrnan, utan kan ocksÄ inte hittas bland kÀrnsymbolerna. Man mÄste anvÀnda heuristiska sökmetoder, till exempel dynamisk disassemblage av funktionen och leta efter pekaren dÀr.
För det andra beror strukturen för poster i tabellen pĂ„ flaggorna med vilka kĂ€rnan har byggts. Om flaggan CONFIG_REQUIRES_U32_MUNGING har deklarerats, kommer storleken pĂ„ strukturen att Ă€ndras â ett extra fĂ€lt lĂ€ggs till. . Det Ă€r nödvĂ€ndigt att utföra en ytterligare kontroll av vilket flagga kĂ€rnan har kompilerats med, som ett alternativ kan man jĂ€mföra pekarna till funktionerna med kĂ€nda.
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
*/
};
Lyckligtvis erbjuder Apple i moderna versioner av macOS ett nytt API för arbete med processer. Endpoint Security API tillÄter klienter att godkÀnna mÄnga förfrÄgningar till andra processer. PÄ sÄ sÀtt kan man blockera alla signaler till processer, inklusive signalen SIGKILL genom det ovan nÀmnda API:t.
#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Ä liknande sÀtt kan man i kÀrnan registrera en MAC Policy, som tillhandahÄller en metod för skydd mot signaler (policy proc_check_signal), men API:t stöds inte officiellt.
Skydd av kÀrntillÀgg
Förutom att skydda processerna i systemet Àr skyddet av sjÀlva kÀrnexpansionen (kext) ocksÄ nödvÀndigt. macOS erbjuder ett ramverk för utvecklare för att enkelt kunna utveckla enhetsdrivrutiner med IOKit. Förutom att tillhandahÄlla verktyg för att arbeta med enheter möjliggör IOKit metoder för att stapla drivrutiner (driver stacking) med hjÀlp av instanser av C++-klasser. Applikationen i userspace kan "hitta" en registrerad klassinstans för att etablera en koppling mellan kernel och userspace.
För att upptÀcka antalet klassinstanser i systemet finns det ett verktyg som heter ioclasscount.
my_kext_ioservice = 1
my_kext_iouserclient = 1
Varje kÀrnexpansion som vill registrera sig i drivrutinsstapeln mÄste deklarera en klass som Àrver frÄn IOService, som i exemplet my_kext_ioservice. Anslutningen av anvÀndarapplikationer medför skapandet av en ny klassinstans som Àrver frÄn IOUserClient, i exemplet my_kext_iouserclient.
Vid försök att avlasta drivrutinen frÄn systemet (kommandot kextunload) anropas den virtuella funktionen "bool terminate(IOOptionBits options)". Det rÀcker med att returnera false vid anropet av terminate-funktionen för att förhindra kextunload.
bool Kext::terminate(IOOptionBits options)
{
if (!IsUnloadAllowed)
{
\/\/ Avlastning Àr inte tillÄten, returnerar false
return false;
}
return super::terminate(options);
}
Flaggan IsUnloadAllowed kan stÀllas in av IOUserClient vid laddning. Vid begrÀnsningar för avlastning kommer kommandot kextunload att returnera följande utdata:
admin@admins-Mac drivermanager % sudo kextunload .\/test.kext
Lösenord:
(kernel) Kan inte ta bort kext my.kext.test; tjÀnster kunde inte avslutas - 0xe00002c7.
Misslyckades med att avlasta my.kext.test - (iokit\/common) otillÄten funktion.
En liknande skyddsÄtgÀrd krÀvs ocksÄ för IOUserClient. Klassinstanser kan avlastas med anvÀndarutrymmesfunktionen IOKitLib "IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);". Det gÄr att returnera false vid anropet av kommandot "terminate" sÄ lÀnge anvÀndarutrymmesapplikationen inte "dör", dvs. tills funktionen "clientDied" anropas.
Skydd av filer
För att skydda filer rĂ€cker det att anvĂ€nda Kauth API, vilket möjliggör begrĂ€nsad Ă„tkomst till filer. Apple erbjuder utvecklare notifikationer om olika hĂ€ndelser i scopet, för oss Ă€r viktiga operationer KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA och KAUTH_VNODE_DELETE_CHILD. Att begrĂ€nsa Ă„tkomst till filer Ă€r enklast att göra efter sökvĂ€g â vi anvĂ€nder API:et "vn_getpath" för att hĂ€mta sökvĂ€gen till filen och gör en jĂ€mförelse av prefixet. Observera att för att optimera omdöpning av sökvĂ€gar till mappar med filer, auktoriserar systemet inte Ă„tkomst till varje fil, utan endast till den mapp som döpts om. Det Ă€r nödvĂ€ndigt att jĂ€mföra den överordnade sökvĂ€gen och begrĂ€nsa KAUTH_VNODE_DELETE för den.

En nackdel med detta tillvÀgagÄngssÀtt kan vara lÄg prestanda nÀr antalet prefix ökar. För att undvika att jÀmförelsen blir O(prefix*length), dÀr prefix Àr antal prefix och length Àr strÀngens lÀngd, kan vi anvÀnda en deterministisk Àndlig automat (DEA) byggd pÄ prefix.
LÄt oss övervÀga metoden för att bygga DEA för denna uppsÀttning av prefix. Initiera pekarna i början av varje prefix. Om alla pekare pekar pÄ samma symbol, öka varje pekare med en symbol och kom ihÄg att lÀngden pÄ den gemensamma strÀngen ökar med en. Om det finns tvÄ pekare med olika symboler, dela pekarna i grupper efter symbolen de pekar pÄ och upprepa algoritmen för varje grupp.
I det första fallet (nÀr alla symboler under pekarna Àr desamma) fÄr vi ett DEA-tillstÄnd som har en enda övergÄng för den gemensamma strÀngen. I det andra fallet fÄr vi en övergÄngstabell av storlek 256 (antal symboler och maximalt antal grupper) för de följande tillstÄnden, erhÄllna genom rekursivt anrop av funktionen.
LĂ„t oss övervĂ€ga ett exempel. För uppsĂ€ttningen av prefix (â/foo/bar/tmp/â, â/var/db/foo/â, â/foo/bar/aba/â, âfoo/bar/aac/â) kan vi fĂ„ följande DEA. I bilden visas endast övergĂ„ngarna som leder till andra tillstĂ„nd, andra övergĂ„ngar kommer inte att vara slutgiltiga.

NÀr vi passerar genom DEA-tillstÄnden kan tre fall uppstÄ.
- Det slutliga tillstĂ„ndet har uppnĂ„tts â vĂ€gen Ă€r skyddad, vi begrĂ€nsar operationerna KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA och KAUTH_VNODE_DELETE_CHILD.
- Det slutgiltiga tillstĂ„ndet nĂ„ddes inte, men vĂ€gen "slutade" (nollterminator nĂ„ddes) â vĂ€gen Ă€r överordnad, KAUTH_VNODE_DELETE mĂ„ste begrĂ€nsas. Observera att om vnode Ă€r en mapp mĂ„ste "\/" lĂ€ggas till i slutet, annars kan det begrĂ€nsas till filen "\/foor\/bar\/t", vilket Ă€r felaktigt.
- Det slutgiltiga tillstÄndet nÄddes inte, vÀgen slutade inte. Inga av prefixen matchar detta, vi inför inga begrÀnsningar.
Slutsats
Syftet med de utvecklade sÀkerhetslösningarna Àr att höja nivÄn pÄ anvÀndarsÀkerhet och deras data. à ena sidan uppnÄs detta syfte genom att utveckla programvaran Acronis, som tÀcker de sÄrbarheter dÀr operativsystemet sjÀlv Àr "svagt". à andra sidan bör man inte förbise de sÀkerhetsaspekter som kan förbÀttras pÄ OS-sidan, sÀrskilt eftersom stÀngning av sÄdana sÄrbarheter ökar vÄr egen motstÄndskraft som produkt. SÄrbarheten rapporterades av Apple Product Security Team och ÄtgÀrdades i macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Allt detta kan endast göras om din verktyg var officiellt installerad i kÀrnan. Det vill sÀga för extern och oönskad mjukvara finns inga sÄdana bakdörrar. Men som du ser, Àven för skydd av legitima program som antivirus och backup-system, krÀvs det ÄtgÀrder. Men nu kommer nya produkter frÄn Acronis för macOS att ha extra skydd mot avlastning frÄn systemet.
KĂ€lla: habr.com
