Pozdravljeni, Habr! Danes bi rad govoril o tem, kako lahko zaščitite procese pred napadi napadalcev v macOS. To je na primer uporabno za protivirusni sistem ali sistem za varnostno kopiranje, še posebej, ker pod macOS obstaja več načinov za "ubijanje" procesa. Preberite o tem in načinih zaščite pod rezom.
Klasičen način "ubijanja" procesa
Dobro znan način za "ubijanje" procesa je pošiljanje signala SIGKILL procesu. Skozi bash lahko pokličete standardni »kill -SIGKILL PID« ali »pkill -9 NAME« za uničenje. Ukaz »kill« je znan že iz časov UNIX-a in ni na voljo le v sistemu macOS, ampak tudi v drugih sistemih, podobnih UNIX-u.
Tako kot v sistemih, podobnih UNIX-u, vam macOS omogoča prestrezanje vseh signalov procesu razen dveh - SIGKILL in SIGSTOP. Ta članek se bo osredotočil predvsem na signal SIGKILL kot signal, ki povzroči zaustavitev procesa.
posebnosti macOS
V sistemu macOS sistemski klic kill v jedru XNU pokliče funkcijo psignal(SIGKILL,...). Poskusimo videti, katera druga uporabniška dejanja v uporabniškem prostoru lahko pokliče funkcija psignal. Izločimo klice funkcije psignal v notranjih mehanizmih jedra (čeprav morda niso trivialni, jih bomo pustili za drug članek 🙂 - preverjanje podpisa, napake v pomnilniku, obravnavanje izhoda/prekinitve, kršitve zaščite datotek itd. .
Začnimo pregled s funkcijo in pripadajočim sistemskim klicem
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
Standardni način za ustvarjanje demonov ob zagonu sistema in nadzor nad njihovo življenjsko dobo je launchd. Upoštevajte, da so viri za staro različico launchctl do macOS 10.10, primeri kode so na voljo za ilustracijo. Moderni launchctl pošilja signale launchd prek XPC, logika launchctl je bila premaknjena vanj.
Poglejmo, kako natančno se ustavijo aplikacije. Pred pošiljanjem signala SIGTERM se poskuša aplikacija zaustaviti s sistemskim klicem “proc_terminate”.
<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));
}
...
<>
Pod pokrovom lahko proc_terminate kljub svojemu imenu pošlje ne samo psignal s SIGTERM, ampak tudi SIGKILL.
Posredna usmrtitev - Omejitev virov
Bolj zanimiv primer lahko vidimo v drugem sistemskem klicu
Čeprav bi ta sistemski klic potencialno lahko uničil proces, sistem ni ustrezno preveril pravic procesa, ki kliče sistemski klic. Pravzaprav preverjam
Torej, če "omejite" kvoto uporabe procesorja aplikacije (na primer, dovolite samo 1 ns za izvajanje), potem lahko ubijete kateri koli proces v sistemu. Tako lahko zlonamerna programska oprema ubije kateri koli proces v sistemu, vključno s protivirusnim procesom. Zanimiv je tudi učinek, ki se pojavi pri ubijanju procesa s pid 1 (launchctl) - panika jedra pri poskusu obdelave signala SIGKILL :)
Kako rešiti težavo?
Najpreprostejši način za preprečitev uničenja procesa je zamenjava funkcijskega kazalca v tabeli sistemskih klicev. Na žalost je ta metoda netrivialna iz več razlogov.
Prvič, simbol, ki nadzoruje pomnilniško lokacijo sysenta, ni samo zaseben za simbol jedra XNU, ampak ga ni mogoče najti v simbolih jedra. Uporabiti boste morali hevristične metode iskanja, kot je dinamično razstavljanje funkcije in iskanje kazalca v njej.
Drugič, struktura vnosov v tabeli je odvisna od zastavic, s katerimi je bilo prevedeno jedro. Če je deklarirana zastavica CONFIG_REQUIRES_U32_MUNGING, bo velikost strukture spremenjena – dodano bo dodatno polje
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
*/
};
Na srečo Apple v sodobnih različicah macOS ponuja nov API za delo s procesi. Endpoint Security API omogoča odjemalcem avtorizacijo številnih zahtev drugim procesom. Tako lahko z zgoraj omenjenim API-jem blokirate vse signale procesom, vključno s signalom SIGKILL.
#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;
}
Podobno je mogoče v jedru registrirati pravilnik MAC, ki zagotavlja metodo zaščite signala (pravilnik proc_check_signal), vendar API ni uradno podprt.
Zaščita razširitve jedra
Poleg zaščite procesov v sistemu je nujna tudi zaščita same razširitve jedra (kext). macOS ponuja razvijalcem ogrodje za preprost razvoj gonilnikov naprav IOKit. Poleg zagotavljanja orodij za delo z napravami IOKit ponuja metode za zlaganje gonilnikov z uporabo primerkov razredov C++. Aplikacija v uporabniškem prostoru bo lahko "našla" registrirani primerek razreda za vzpostavitev razmerja jedro-uporabniški prostor.
Za zaznavanje števila primerkov razreda v sistemu obstaja pripomoček ioclasscount.
my_kext_ioservice = 1
my_kext_iouserclient = 1
Vsaka razširitev jedra, ki se želi registrirati v skladu gonilnikov, mora deklarirati razred, ki deduje od IOService, na primer my_kext_ioservice v tem primeru. Povezovanje uporabniških aplikacij povzroči ustvarjanje novega primerka razreda, ki deduje od IOUserClient, v primeru my_kext_iouserclient.
Ko poskušate odstraniti gonilnik iz sistema (ukaz kextunload), se pokliče navidezna funkcija »bool terminate(IOOptionBits options)«. Dovolj je, da vrnete false pri klicu za prekinitev, ko poskušate razbremeniti, da onemogočite kextunload.
bool Kext::terminate(IOOptionBits options)
{
if (!IsUnloadAllowed)
{
// Unload is not allowed, returning false
return false;
}
return super::terminate(options);
}
Zastavico IsUnloadAllowed lahko med nalaganjem nastavi IOUserClient. Ko obstaja omejitev prenosa, bo ukaz kextunload vrnil naslednje rezultate:
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.
Podobno zaščito je treba narediti za IOUserClient. Primerke razredov je mogoče razložiti s funkcijo uporabniškega prostora IOKitLib “IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);”. Ko kličete ukaz »terminate«, lahko vrnete false, dokler aplikacija uporabniškega prostora ne »umre«, to pomeni, da funkcija »clientDied« ni poklicana.
Zaščita datotek
Za zaščito datotek zadostuje uporaba API-ja Kauth, ki omogoča omejitev dostopa do datotek. Apple razvijalcem zagotavlja obvestila o različnih dogodkih v obsegu, za nas so pomembne operacije KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA in KAUTH_VNODE_DELETE_CHILD. Najlažji način za omejitev dostopa do datotek je po poti - uporabljamo API “vn_getpath”, da pridobimo pot do datoteke in primerjamo predpono poti. Upoštevajte, da za optimizacijo preimenovanja poti mape datotek sistem ne dovoli dostopa do vsake datoteke, ampak samo do same mape, ki je bila preimenovana. Potrebno je primerjati nadrejeno pot in zanjo omejiti KAUTH_VNODE_DELETE.
Pomanjkljivost tega pristopa je lahko nizka zmogljivost, saj se število predpon povečuje. Da zagotovite, da primerjava ni enaka O(predpona*dolžina), kjer je predpona število predpon, dolžina dolžina niza, lahko uporabite deterministični končni avtomat (DFA), zgrajen s predponami.
Oglejmo si metodo za konstruiranje DFA za dani nabor predpon. Kazalce inicializiramo na začetku vsake predpone. Če vsi kazalci kažejo na isti znak, povečajte vsak kazalec za en znak in ne pozabite, da je dolžina iste vrstice večja za en znak. Če sta dva kurzorja z različnimi simboli, ju razdelite v skupine glede na simbol, na katerega kažeta, in ponovite algoritem za vsako skupino.
V prvem primeru (vsi znaki pod kazalci so enaki) dobimo stanje DFA, ki ima le en prehod vzdolž iste vrstice. V drugem primeru dobimo tabelo prehodov velikosti 256 (število znakov in maksimalno število skupin) v naslednja stanja, ki jih dobimo z rekurzivnim klicem funkcije.
Poglejmo si primer. Za nabor predpon (»/foo/bar/tmp/«, »/var/db/foo/«, »/foo/bar/aba/«, »foo/bar/aac/«) lahko dobite naslednje DFA. Slika prikazuje le prehode, ki vodijo v druga stanja; ostali prehodi ne bodo dokončni.
Pri prehodu skozi stanja DKA so lahko 3 primeri.
- Končno stanje je doseženo - pot je zaščitena, omejujemo operacije KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA in KAUTH_VNODE_DELETE_CHILD
- Končno stanje ni bilo doseženo, vendar se je pot “končala” (dosežen je bil ničelni terminator) - pot je nadrejena, potrebno je omejiti KAUTH_VNODE_DELETE. Upoštevajte, da če je vnode mapa, morate na koncu dodati '/', sicer jo lahko omeji na datoteko »/foor/bar/t«, kar ni pravilno.
- Končno stanje ni bilo doseženo, pot se ni končala. Nobena od predpon ne ustreza tej, ne uvajamo omejitev.
Zaključek
Cilj varnostnih rešitev, ki se razvijajo, je povečati stopnjo varnosti uporabnika in njegovih podatkov. Po eni strani je ta cilj dosežen z razvojem programskega izdelka Acronis, ki zapira tiste ranljivosti, kjer je sam operacijski sistem »šibek«. Po drugi strani pa ne smemo zanemariti krepitve tistih varnostnih vidikov, ki jih je mogoče izboljšati na strani OS, še posebej, ker zapiranje takšnih ranljivosti poveča našo lastno stabilnost kot izdelek. Ranljivost je bila prijavljena ekipi za varnost izdelkov Apple in je bila odpravljena v macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).
Vse to je mogoče storiti le, če je bil vaš pripomoček uradno nameščen v jedro. To pomeni, da za zunanjo in neželeno programsko opremo ni takih vrzeli. Vendar, kot lahko vidite, tudi zaščita zakonitih programov, kot so protivirusni sistemi in sistemi za varnostno kopiranje, zahteva delo. Toda zdaj bodo novi izdelki Acronis za macOS imeli dodatno zaščito pred razkladanjem iz sistema.
Vir: www.habr.com