Hej, Habr! Idag skulle jag vilja prata om hur du kan skydda processer från attacker från angripare i macOS. Detta är till exempel användbart för ett antivirus- eller säkerhetskopieringssystem, särskilt eftersom det under macOS finns flera sätt att "döda" en process. Läs om detta och skyddsmetoder under snittet.
Det klassiska sättet att "döda" en process
Ett välkänt sätt att "döda" en process är att skicka en SIGKILL-signal till processen. Genom bash kan du kalla standarden "kill -SIGKILL PID" eller "pkill -9 NAME" för att döda. Kommandot "kill" har varit känt sedan UNIX-dagarna och är tillgängligt inte bara på macOS utan även på andra UNIX-liknande system.
Precis som i UNIX-liknande system låter macOS dig fånga upp alla signaler till en process utom två - SIGKILL och SIGSTOP. Den här artikeln kommer i första hand att fokusera på SIGKILL-signalen som en signal som gör att en process dödas.
macOS-specifikationer
På macOS anropar kill-systemanropet i XNU-kärnan funktionen psignal(SIGKILL,...). Låt oss försöka se vilka andra användaråtgärder i användarutrymmet som kan anropas av psignal-funktionen. Låt oss sålla bort anrop till psignal-funktionen i kärnans interna mekanismer (även om de kan vara icke-triviala, lämnar vi dem till en annan artikel 🙂 - signaturverifiering, minnesfel, exit/terminate-hantering, filskyddsöverträdelser, etc. .
Låt oss börja granskningen med funktionen och motsvarande systemanrop
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);
}
...
}
lansera
Standardsättet att skapa demoner vid systemstart och kontrollera deras livslängd lanseras. Observera att källorna är för den gamla versionen av launchctl upp till macOS 10.10, kodexempel tillhandahålls i illustrativt syfte. Modern launchctl skickar lanserade signaler via XPC, launchctl logik har flyttats till den.
Låt oss titta på exakt hur applikationer stoppas. Innan SIGTERM-signalen skickas, försöker man stoppa applikationen med hjälp av systemanropet "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));
}
...
<>
Under huven kan proc_terminate, trots sitt namn, skicka inte bara psignal med SIGTERM, utan även SIGKILL.
Indirekt dödande - Resursgräns
Ett mer intressant fall kan ses i ett annat systemsamtal
Även om detta systemanrop potentiellt skulle kunna döda en process, kontrollerade systemet inte på ett adekvat sätt rättigheterna för processen som anropade systemanropet. Kollar faktiskt
Därför, om du "begränsar" applikationens CPU-användningskvot (till exempel tillåter endast 1 ns att köras), kan du döda vilken process som helst i systemet. Således kan skadlig programvara döda alla processer på systemet, inklusive antivirusprocessen. Intressant är också effekten som uppstår när man dödar en process med pid 1 (launchctl) - kärnpanik när man försöker bearbeta SIGKILL-signalen :)
Hur löser man problemet?
Det enklaste sättet att förhindra att en process dödas är att ersätta funktionspekaren i systemanropstabellen. Tyvärr är denna metod icke-trivial av många skäl.
För det första är symbolen som styr sysents minnesplats inte bara privat för XNU-kärnsymbolen, utan kan inte hittas i kärnsymboler. Du måste använda heuristiska sökmetoder, som att dynamiskt demontera funktionen och söka efter en pekare i den.
För det andra beror strukturen på poster i tabellen på flaggorna som kärnan kompilerades med. Om CONFIG_REQUIRES_U32_MUNGING-flaggan deklareras kommer storleken på strukturen att ändras - ett ytterligare fält kommer att läggas till
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, i moderna versioner av macOS, tillhandahåller Apple ett nytt API för att arbeta med processer. Endpoint Security API tillåter klienter att auktorisera många förfrågningar till andra processer. Således kan du blockera alla signaler till processer, inklusive SIGKILL-signalen, med hjälp av ovan nämnda 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å liknande sätt kan en MAC-policy registreras i kärnan, vilket ger en signalskyddsmetod (policy proc_check_signal), men API:et stöds inte officiellt.
Kärnförlängningsskydd
Förutom att skydda processer i systemet är det också nödvändigt att skydda själva kärntillägget (kext). macOS tillhandahåller ett ramverk för utvecklare att enkelt utveckla IOKit-drivrutiner. Förutom att tillhandahålla verktyg för att arbeta med enheter, tillhandahåller IOKit metoder för drivrutinsstapling med instanser av C++-klasser. En applikation i användarutrymmet kommer att kunna "hitta" en registrerad instans av klassen för att upprätta ett förhållande mellan kärnan och användarutrymmet.
För att upptäcka antalet klassinstanser i systemet finns verktyget ioclasscount.
my_kext_ioservice = 1
my_kext_iouserclient = 1
Alla kärntillägg som vill registrera sig med drivrutinsstacken måste deklarera en klass som ärver från IOService, till exempel my_kext_ioservice i detta fall.Anslutning av användarapplikationer skapar en ny instans av klassen som ärver från IOUserClient, i exemplet my_kext_iouserclient.
När du försöker ladda bort en drivrutin från systemet (kextunload-kommando), anropas den virtuella funktionen "bool terminate(IOOptionBits options)". Det räcker med att returnera falskt på samtalet för att avsluta när man försöker lossa för att inaktivera kextunload.
bool Kext::terminate(IOOptionBits options)
{
if (!IsUnloadAllowed)
{
// Unload is not allowed, returning false
return false;
}
return super::terminate(options);
}
Flaggan IsUnloadAllowed kan ställas in av IOUserClient vid laddning. När det finns en nedladdningsgräns kommer kommandot kextunload att returnera följande utdata:
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.
Liknande skydd måste göras för IOUserClient. Förekomster av klasser kan laddas ur med hjälp av IOKitLib-användarutrymmesfunktionen "IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);". Du kan returnera false när du anropar kommandot "terminate" tills användarutrymmesapplikationen "dör", det vill säga funktionen "clientDied" anropas inte.
Filskydd
För att skydda filer räcker det att använda Kauth API, som låter dig begränsa åtkomsten till filer. Apple förser utvecklare med aviseringar om olika händelser i omfattningen; för oss är operationerna KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA och KAUTH_VNODE_DELETE_CHILD viktiga. Det enklaste sättet att begränsa åtkomst till filer är genom sökväg - vi använder "vn_getpath" API för att få sökvägen till filen och jämföra sökvägsprefixet. Observera att för att optimera byte av sökvägar för filmappar, tillåter systemet inte åtkomst till varje fil, utan endast till själva mappen som har bytt namn. Det är nödvändigt att jämföra den överordnade sökvägen och begränsa KAUTH_VNODE_DELETE för den.
Nackdelen med detta tillvägagångssätt kan vara låg prestanda när antalet prefix ökar. För att säkerställa att jämförelsen inte är lika med O(prefix*längd), där prefix är antalet prefix, längd är längden på strängen, kan du använda en deterministisk finit automat (DFA) byggd av prefix.
Låt oss överväga en metod för att konstruera en DFA för en given uppsättning prefix. Vi initierar markörerna i början av varje prefix. Om alla markörer pekar på samma tecken, öka sedan varje markör med ett tecken och kom ihåg att längden på samma rad är en längre. Om det finns två markörer med olika symboler, dela in markörerna i grupper enligt symbolen de pekar på och upprepa algoritmen för varje grupp.
I det första fallet (alla tecken under markörerna är samma) får vi ett DFA-tillstånd som bara har en övergång längs samma linje. I det andra fallet får vi en tabell över övergångar av storlek 256 (antal tecken och maximalt antal grupper) till efterföljande tillstånd som erhålls genom att rekursivt anropa funktionen.
Låt oss titta på ett exempel. För en uppsättning prefix ("/foo/bar/tmp/", "/var/db/foo/", "/foo/bar/aba/", "foo/bar/aac/") kan du få följande DFA. Bilden visar endast övergångar som leder till andra stater, andra övergångar kommer inte att vara slutgiltiga.
När man går igenom DKA-staterna kan det finnas 3 fall.
- Sluttillståndet har nåtts - sökvägen är skyddad, vi begränsar operationerna KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA och KAUTH_VNODE_DELETE_CHILD
- Det slutliga tillståndet nåddes inte, men sökvägen "slutade" (nollterminatorn nåddes) - sökvägen är en förälder, det är nödvändigt att begränsa KAUTH_VNODE_DELETE. Observera att om vnode är en mapp måste du lägga till en '/' i slutet, annars kan det begränsa den till filen "/foor/bar/t", vilket är felaktigt.
- Det slutliga tillståndet nåddes inte, vägen tog inte slut. Inget av prefixen matchar detta, vi inför inga begränsningar.
Slutsats
Målet med säkerhetslösningarna som utvecklas är att öka säkerhetsnivån för användaren och dennes data. Å ena sidan uppnås detta mål genom utvecklingen av Acronis mjukvaruprodukt, som stänger de sårbarheter där själva operativsystemet är "svagt". Å andra sidan bör vi inte försumma att stärka de säkerhetsaspekter som kan förbättras på OS-sidan, särskilt eftersom att stänga sådana sårbarheter ökar vår egen stabilitet som produkt. Sårbarheten rapporterades till Apple Product Security Team och har åtgärdats i macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).
Allt detta kan bara göras om ditt verktyg har officiellt installerats i kärnan. Det vill säga, det finns inga sådana kryphål för extern och oönskad programvara. Men som du kan se kräver även att skydda legitima program som antivirus- och säkerhetskopieringssystem arbete. Men nu kommer nya Acronis-produkter för macOS att ha ytterligare skydd mot avlastning från systemet.
Källa: will.com