Përshëndetje, Habr! Sot do të flas për mënyrat si mund të mbrohen proceset nga sulmet e hakerëve në macOS. Kjo është e dobishme sidomos për antivirusin ose sistemin e kopjimit, veçanërisht për shkak se në macOS ekzistojnë disa mënyra për të "vrarë" një proces. Më këtë dhe metodat e mbrojtjes lexoni më poshtë.
Mënyra klasike për të "vrarë" një proces
Një mënyrë e njohur për të "vrarë" një proces është dërgimi i sinjalit SIGKILL procesit. Në bash, mund të thërrasësh metodat standarde "kill -SIGKILL PID" ose "pkill -9 NAME" për t'i bërë atë. Komanda "kill" është e njohur që nga koha e UNIX-it dhe është e disponueshme jo vetëm në macOS, por edhe në sisteme të tjera UNIX-like.
Po ashtu si në sistemet UNIX-like, macOS lejon kapjen e çdo sinjali për procesin përveç dy - SIGKILL dhe SIGSTOP. Ky artikull do të shqyrtojë kryesisht sinjalin SIGKILL, si sinjal që shkakton vrasjen e procesit.
Specifika e macOS
Në macOS, thirrja sistemike kill në bërthamën XNU thërret funksionin psignal(SIGKILL,…). Le të shohim se cilat veprime të tjera të përdoruesit në userspace mund të thërrasin funksionin psignal. Do të përjashtojmë thirrjet e funksionit psignal në mekanizmat e brendshëm të bërthamës (ndonëse ato mund të jenë jo triviale, por do t’i lëmë ato për një artikull tjetër 🙂 — verifikimi i nënshkrimit, gabimet e memories, trajtimi exit/terminate, shkelja e mbrojtjes së skedarëve, etj.).
Le të fillojmë shqyrtimin me funksionin dhe thirrjen përkatëse sistemike. . Është e dukshme se përveç thirrjes klasike kill, ekziston një qasje alternative, e cila është specifike për sistemin operativ macOS dhe nuk gjendet në BSD. Parimet e funksionimit të të dy thirrjeve sistemike janë gjithashtu të ngjashme. ato përbëjnë thirrje direkte të funksionit të bërthamës psignal. Po ashtu, le të vëmë re se para se të vrasë procesin bëhet një kontroll “cansignal” – nëse procesi mund të dërgojë sinjal tjetër procesi, sistemi nuk lejon asnjë aplikacion të vrasë proceset sistemike, për shembull.
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);
}
...
}
launchd
Mënyra standarde për të krijuar demonë në nisjen e sistemit dhe për të kontrolluar kohëzgjatjen e tyre është launchd. Dua të theksoj se burimet janë për një version të vjetër të launchctl para macOS 10.10, shembujt e kodit janë dhënë për ilustim. Launchctl modern dërgon sinjale në launchd përmes XPC, logjika e launchctl është transferuar tek ai.
Le të shqyrtojmë se si ndalohet saktësisht aplikacionet. Para dërgimit të sinjalit SIGTERM, aplikacionet përpiqen të ndalen përmes thirrjes sistemore “proc_terminate”.
...
error = proc_terminate(j->p, &sig);
if (error) {
job_log(j, LOG_ERR | LOG_CONSOLE, "Nuk e mundi të përfundojë punën: %d: %s", error, strerror(error));
job_log(j, LOG_NOTICE | LOG_CONSOLE, "Duke përdorur opsionin rezervë për të përfunduar punën...");
error = kill2(j->p, SIGTERM);
if (error) {
job_log(j, LOG_ERR, "Nuk e mundi të dërgojë sinjalin punës: %d: %s", error, strerror(error));
}
...
Pavarësisht emrit të tij, proc_terminate mund të dërgojë jo vetëm psignal me SIGTERM, por edhe SIGKILL.
Vrasja indirekte — kufizim në burime
Një rast më interesant mund të shihet në një thirrje tjetër të sistemit . Përdorimi standard i kësaj thirrjeje të sistemit — kufizime në burimet e aplikacioneve, p.sh. për indeksuesin, një kufizim në kuotën e kohës së procesorit dhe memories, në mënyrë që sistemi të mos ngadalësohet ndjeshëm për shkak të veprimeve të ruajtjes në skedar. Nëse aplikacioni arrin kufizimin e burimeve, siç mund të shihet nga funksioni proc_apply_resource_actions, atij i dërgohet sinjali SIGKILL.
Megjithëse kjo thirrje e sistemit mund të prodhojë potencialisht vrasjen e procesit, sistemi nuk e ka kontrolluar adekuat të drejtat e procesit që thërret thirrjen e sistemit. Në të vërtetë, kontrolli , porashtu mjafton të përdorni një flamur alternativ PROC_POLICY_ACTION_SET për të anashkaluar këtë kusht.
Nga këtu, nëse "kufizoni" kuotën e përdorimit të CPU për aplikacionin (për shembull, lejoni të ekzekutohet vetëm 1 ns), mund të vrisni çdo proces në sistem. Në këtë mënyrë, një malware mund të eliminojë çdo proces në sistem, duke përfshirë edhe procesin e antivirusit. Po ashtu, efekti që ndodh kur vritet procesi me pid 1 (launchctl) është mjaft interesant — panic i kernel-it kur përpiqet të përpunojë sinjalin SIGKILL 🙂

Si të zgjidhni problemin?
Mënyra më e drejtpërdrejtë për të ndaluar vrasjen e procesit është të zëvendësosh treguesin e funksionit në tabelën e thirrjeve sistemike. Fatkeqësisht, ky metodë është jo triviale për shumë arsye.
Së pari, simboli që ka të bëjë me vendndodhjen e sysent në memorie është një simbol privat i bërthamës XNU dhe nuk mund të gjendet në simbolet e bërthamës. Do të duhet të përdorim metoda heuristike për kërkimin, siç është disassamble dinamike e funksionit dhe kërkimi i treguesit në të.
Së pari, struktura e regjistrimeve në tabelë varet nga flamujt me të cilët është ndërtuar bërthama. Nëse është shpallur flamuri CONFIG_REQUIRES_U32_MUNGING, atëherë madhësia e strukturës do të ndryshojë - do të shtohet një fushë shtesë. . Është e nevojshme të kryhet një kontroll shtesë për atë me cilin flamur është kompiluar bërthama, si një mundësi të krahasohen treguesit në funksione me ato të njohura.
struct sysent { /* tabela e thirrjeve sistemike */
sy_call_t *sy_call; /* funksioni i implementimit */
#if CONFIG_REQUIRES_U32_MUNGING || (__arm__ && (__BIGGEST_ALIGNMENT__ > 4))
sy_munge_t *sy_arg_munge32; /* muesi i argumenteve të thirrjeve sistemike për procesin 32-bit */
#endif
int32_t sy_return_type; /* tipet e kthimeve të thirrjeve sistemike */
int16_t sy_narg; /* numri i argumenteve */
uint16_t sy_arg_bytes; /* Madhësia totale e argumenteve në bytes për
* thirrjet sistemike 32-bit
*/
};
Për fat të mirë, në versionet moderne të macOS, Apple ofron një API të ri për të punuar me proceset. Endpoint Security API lejon klientët të autorizojnë shumë kërkesa për procese të tjera. Kështu, është e mundur të bllokohet çdo sinjal për proceset, duke përfshirë sinjalin SIGKILL me anë të API-së të përmendur më lart.
#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;
}
Njëlloj si në kernel, mund të regjistrohet një Politika MAC, e cila ofron një metodë mbrojtjeje nga sinjalet (policy proc_check_signal), megjithëse API nuk mbështetet zyrtarisht.
Mbrojtja e zgjerimit të kernelit
Përveç mbrojtjes së proceseve në sistem, është e nevojshme edhe mbrojtja e vetë zgjerimit të kernelit (kext). macOS ofron një kornizë për zhvilluesit për zhvillimin e lehtë të drejtuesve të pajisjeve IOKit. Përveç ofrimit të mjeteve për punë me pajisjet, IOKit siguron metoda për grumbullimin e drejtuesve (driver stacking) përmes instancave të klasave C++. Një aplikacion në userspace mund të "gjejë" një instancë të regjistruar të klasës për të vendosur një lidhje midis kernelit dhe userspace.
Për të zbuluar numrin e instancave të klasave në sistem, ekziston një utilitar i quajtur ioclasscount.
my_kext_ioservice = 1
my_kext_iouserclient = 1
Çdo zgjerim kernelit që dëshiron të regjistrohet në grumbullin e drejtuesve është i detyruar të shpallë një klasë të trashëguar nga IOService, për shembull, my_kext_ioservice në këtë rast. Lidha e aplikacioneve të përdoruesit shkakton krijimin e një instancë të re të klasës që trashëgohet nga IOUserClient, në shembullin my_kext_iouserclient.
Kur gjatë përpjekjes për të shkarkuar drejtuesin nga sistemi (komanda kextunload), thirret funksioni virtual “bool terminate(IOOptionBits options)”. Mjafton të kthehet false në thirrjen e funksionit terminate gjatë përpjekjes për shkarkim, për të ndaluar kextunload.
bool Kext::terminate(IOOptionBits options)
{
if (!IsUnloadAllowed)
{
// Shkarkimi nuk lejohet, po kthehet false
return false;
}
return super::terminate(options);
}
Flamuri IsUnloadAllowed mund të vendoset nga IOUserClient gjatë ngarkimit. Kur ndalohet ngarkimi, komanda kextunload do të kthejë daljen e mëposhtme:
admin@admins-Mac drivermanager % sudo kextunload ./test.kext
Fjalëkalimi:
(kernel) Nuk mund të hiqet kext my.kext.test; shërbimet dështuan për t'u ndaluar - 0xe00002c7.
Dështoi të shkarkojë my.kext.test - (iokit/common) funksion i pavlefshëm.
Një mbrojtje të ngjashme duhet të bëhet edhe për IOUserClient. Instancat e klasave mund të shkaktohen përmes funksionit të userspace IOKitLib “IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);”. Mund të kthehen false në thirrjen e komandës “terminate” derisa aplikacioni i userspace të “vdesë”, domethënë, derisa të ndodhi thirrja e funksionit “clientDied”.
Mbrojtja e skedarëve
Për mbrojtjen e skedareve, mjafton të përdorni Kauth API, i cili lejon kufizimin e aksesit në skedare. Apple u siguron zhvilluesve njoftime për ngjarje të ndryshme në scope, ku për ne janë të rëndësishme operacionet KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA dhe KAUTH_VNODE_DELETE_CHILD. Kufizimi i aksesit në skedare është më i thjeshtë nëpërmjet rrugës — përdorim API-në “vn_getpath” për të marrë rrugën drejt skedarit dhe bëjmë krahasimin e prefix-it të rrugës. Vlen të theksohet se për optimizimin e rinovimit të rrugëve të dosjeve, sistemi nuk autorizon aksesin në çdo skedar, por vetëm në dosjen që është rinovuar. Është e nevojshme të bëhet krahasimi i rrugës prind dhe të kufizohet KAUTH_VNODE_DELETE për të.

Një mangësi e këtij qasjeje mund të jetë performanca e ulët në rritjen e numrit të prefix-eve. Për të siguruar që krahasimi të mos jetë O(prefix*length), ku prefix është numri i prefix-eve dhe length është gjatësia e vargut, mund të përdorim një automatë të fundëm deterministik (DFA), të ndërtuar sipas prefix-eve.
Le të shqyrtojmë metodën e ndërtimit të DKA për këtë grup prefixesh. Inicializojmë kursoret në fillim të çdo prefixi. Nëse të gjithë kursoret tregojnë për të njëjtin simbol, rrisim secilin kursor me një simbol dhe mbajmë mend se gjatësia e vargjeve të njëjta është rritur me një. Nëse ka dy kursore, simbolet nën të cilat ndodhen janë të ndryshme, ndajmë kursoret në grupe sipas simbolit që ato tregojnë dhe përsërisim algoritmin për secilën grup.
Në rastin e parë (kur të gjitha simbolet nën kursore janë të njëjta) përftojmë gjendjen DKA, e cila ka vetëm një kalim sipas vargut të njëjtë. Në rastin e dytë, marrim një tabelë kalimi me madhësi 256 (numri i simboleve dhe numri maksimal i grupeve) për gjendjet e ardhshme, të marra gjatë thirrjes rekurzive të funksionit.
Le të shqyrtojmë një shembull. Për grupin e prefixeve (“/foo/bar/tmp/”, “/var/db/foo/”, “/foo/bar/aba/”, “foo/bar/aac/”) mund të përftojmë këtë DKA. Në ilustrim janë treguar vetëm kalimet që çojnë në gjendje të tjera, kalimet e tjera nuk do të jenë përfundimtare.

Gjatë kalimit përmes gjendjeve të DKA mund të ndodhin 3 raste.
- Arrithet një gjendje përfundimtare - rruga është e sigurt, kufizoni operacionet KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA dhe KAUTH_VNODE_DELETE_CHILD
- Nuk është arritur një gjendje përfundimtare, por rruga "mbaroi" (u arrit zero-terminatori) - rruga është prind, është e nevojshme të kufizohet KAUTH_VNODE_DELETE. Vëreni se nëse vnode është një dosje, duhet të shtoni në fund ‘/’, ndryshe mund të ndodhi kufizimi në skedarin “/foor/bar/t”, që është e gabuar.
- Nuk është arritur një gjendje përfundimtare, rruga nuk mbaroi. Asnjë nga prefiksët nuk përputhet me këtë, nuk vendosim kufizime.
Përfundimi
Qëllimi i zgjidhjeve të sigurisë që po zhvillohen është rritja e nivelit të sigurisë për përdoruesin dhe të dhënat e tij. Nga njëra anë, ky qëllim sigurohet përmes zhvillimit të produktit software Acronis, i cili mbulon ato dobie ku "është e dobët" vetë sistemi operativ. Nga ana tjetër, nuk duhet të injorojmë forcimin e aspekteve të sigurisë që mund të përmirësohen nga ana e OS, veçanërisht sepse mbyllja e tillë e dobësive rrit stabilitetin tonë si produkt. Dobësia u raportua nga Apple Product Security Team dhe u rregullua në macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Të gjitha këto mund të bëhen vetëm nëse utilitari juaj është instaluar zyrtarisht në bllok. Kështu, për software që është i jashtëm dhe i padëshiruar nuk ka skema të tilla. Megjithatë, siç e shihni, edhe për mbrojtjen e programeve legjitime, si antivirus dhe sistemi i kopjimit, kërkohet përpjekje. Por tani produktet e reja Acronis për macOS do të kenë mbrojtje shtesë nga shkarkimi nga sistemi.
Burimi: habr.com
