Kiel protekti procezojn kaj kernajn etendaĵojn ĉe macOS

Saluton, Habr! Hodiaŭ mi ŝatus paroli pri kiel vi povas protekti procezojn kontraŭ atakoj de atakantoj en macOS. Ekzemple, ĉi tio estas utila por kontraŭvirusa aŭ rezerva sistemo, precipe ĉar sub macOS ekzistas pluraj manieroj "mortigi" procezon. Legu pri ĉi tio kaj protektaj metodoj sub la tranĉo.

Kiel protekti procezojn kaj kernajn etendaĵojn ĉe macOS

La klasika maniero "mortigi" procezon

Bonkonata maniero "mortigi" procezon estas sendi SIGKILL-signalon al la procezo. Per bash vi povas nomi la norman "kill -SIGKILL PID" aŭ "pkill -9 NAME" por mortigi. La komando "mortigi" estas konata ekde la tempoj de UNIX kaj disponeblas ne nur en macOS, sed ankaŭ en aliaj UNIX-similaj sistemoj.

Same kiel en UNIX-similaj sistemoj, macOS permesas vin kapti ajnajn signalojn al procezo krom du - SIGKILL kaj SIGSTOP. Ĉi tiu artikolo ĉefe koncentriĝos pri la SIGKILL-signalo kiel signalo, kiu igas procezon esti mortigita.

specifaĵoj de macOS

Ĉe macOS, la kill sistemvoko en la XNU-kerno vokas la psignal(SIGKILL,...) funkcion. Ni provu vidi, kiajn aliajn uzantajn agojn en uzantspaco povas esti nomitaj per la psignal-funkcio. Ni forigu alvokojn al la psignal-funkcio en la internaj mekanismoj de la kerno (kvankam ili povas esti ne-trivialaj, sed ni lasos ilin por alia artikolo 🙂 - subskriba konfirmo, memoreraroj, eliro/fino-traktado, dosierprotekto malobservoj, ktp.

Ni komencu la revizion per la funkcio kaj la responda sistemvoko fini_kun_utila ŝarĝo. Oni povas vidi, ke krom la klasika killvoko, ekzistas alternativa aliro, kiu estas specifa por la macOS-operaciumo kaj ne troviĝas en BSD. La mastrumaj principoj de ambaŭ sistemvokoj ankaŭ estas similaj. Ili estas rektaj vokoj al la kerna funkcio psignal. Ankaŭ notu, ke antaŭ mortigi procezon, oni faras "cansignal" kontrolon - ĉu la procezo povas sendi signalon al alia procezo; la sistemo ne permesas al iu ajn aplikaĵo mortigi sistemajn procezojn, ekzemple.

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

La norma maniero krei demonojn ĉe la ekfunkciigo de la sistemo kaj kontroli ilian vivdaŭron estas lanĉita. Bonvolu noti, ke la fontoj estas por la malnova versio de launchctl ĝis macOS 10.10, kodekzemploj estas provizitaj por ilustraj celoj. Moderna launchctl sendas launchd-signalojn per XPC, launchctl-logiko estis movita al ĝi.

Ni rigardu kiel ekzakte aplikoj estas haltigitaj. Antaŭ sendi la signalon SIGTERM, oni provas haltigi la aplikaĵon uzante la sistemvokon "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));
		} 
...
<>

Sub la kapuĉo, proc_terminate, malgraŭ sia nomo, povas sendi ne nur psignalon kun SIGTERM, sed ankaŭ SIGKILL.

Nerekta Mortigo - Rimeda Limo

Pli interesa kazo videblas en alia sistemvoko procezo_politiko. Ofta uzo de tiu sistemvoko estas limigi aplikaĵresursojn, kiel ekzemple por indeksilo por limigi CPU-tempon kaj memorkvotojn tiel ke la sistemo ne estas signife bremsita per dosieraj kaŝmemoraktivecoj. Se aplikaĵo atingis sian rimedan limon, kiel videblas el la funkcio proc_apply_resource_actions, signalo SIGKILL estas sendita al la procezo.

Kvankam ĉi tiu sistemvoko povus eble mortigi procezon, la sistemo ne adekvate kontrolis la rajtojn de la procezo vokanta la sistemvokon. Fakte kontrolante ekzistis, sed sufiĉas uzi la alternativan flagon PROC_POLICY_ACTION_SET por preteriri ĉi tiun kondiĉon.

Tial, se vi "limigas" la CPU-uzokvoton de la aplikaĵo (ekzemple permesu nur 1 ns funkcii), tiam vi povas mortigi ajnan procezon en la sistemo. Tiel, la malware povas mortigi ajnan procezon en la sistemo, inkluzive de la kontraŭvirusa procezo. Ankaŭ interesa estas la efiko, kiu okazas kiam oni mortigas procezon per pid 1 (launchctl) - kernelpaniko kiam oni provas prilabori la signalon SIGKILL :)

Kiel protekti procezojn kaj kernajn etendaĵojn ĉe macOS

Kiel solvi la problemon?

La plej simpla maniero malhelpi procezon esti mortigita estas anstataŭigi la funkciomontrilon en la sistema voka tabelo. Bedaŭrinde, ĉi tiu metodo estas ne-triviala pro multaj kialoj.

Unue, la simbolo kiu kontrolas la memorlokon de sysent estas ne nur privata al la XNU-kernsimbolo, sed ne povas esti trovita en kernaj simboloj. Vi devos uzi heŭristikajn serĉmetodojn, kiel dinamike malmunti la funkcion kaj serĉi montrilon en ĝi.

Due, la strukturo de enskriboj en la tabelo dependas de la flagoj kun kiuj la kerno estis kompilita. Se la flago CONFIG_REQUIRES_U32_MUNGING estas deklarita, la grandeco de la strukturo estos ŝanĝita - plia kampo estos aldonita sy_arg_munge32. Necesas fari plian kontrolon por determini per kiu flago la kerno estis kompilita, aŭ alternative, kontroli funkciomontrilojn kontraŭ konataj.

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
                                         */
};

Feliĉe, en modernaj versioj de macOS, Apple provizas novan API por labori kun procezoj. La Endpoint Security API permesas al klientoj rajtigi multajn petojn al aliaj procezoj. Tiel, vi povas bloki ajnajn signalojn al procezoj, inkluzive de la SIGKILL-signalo, uzante la supre menciitan 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;
}

Simile, MAC-Politiko povas esti registrita en la kerno, kiu disponigas signalan protektometodon (politiko proc_check_signal), sed la API ne estas oficiale subtenata.

Protekto pri etendo de kerno

Krom protekti procezojn en la sistemo, ankaŭ necesas protekti la kernel-etendaĵon mem (kext). macOS disponigas kadron por programistoj por facile evoluigi IOKit-aparatajn ŝoforojn. Aldone al disponigado de iloj por labori kun aparatoj, IOKit disponigas metodojn por ŝofora stakiĝo uzante kazojn de C++-klasoj. Apliko en la uzantspaco povos "trovi" registritan kazon de la klaso por establi kerno-uzantspacan rilaton.

Por detekti la nombron da klasokazoj en la sistemo, ekzistas la ioclasscount ilo.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Ajna kerna etendo kiu deziras registriĝi kun la ŝoforstako devas deklari klason kiu heredas de IOService, ekzemple my_kext_ioservice ĉi-kaze.Konekti uzantaplikaĵojn kaŭzas la kreadon de nova kazo de la klaso kiu heredas de IOUserClient, en la ekzemplo my_kext_iouserclient.

Kiam vi provas malŝarĝi ŝoforon de la sistemo (kextunload komando), la virtuala funkcio "bool terminate(IOOptionBits-opcioj)" estas vokita. Sufiĉas reveni false ĉe la voko por ĉesigi kiam oni provas malŝarĝi por malŝalti kextunload.

bool Kext::terminate(IOOptionBits options)
{

  if (!IsUnloadAllowed)
  {
    // Unload is not allowed, returning false
    return false;
  }

  return super::terminate(options);
}

La IsUnloadAllowed flago povas esti agordita de la IOUserClient dum ŝarĝo. Kiam estas elŝuta limo, la komando kextunload resendos la sekvan eligon:

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.

Simila protekto devas esti farita por IOUserClient. Kazoj de klasoj povas esti malŝarĝitaj uzante la IOKitLib uzantspacan funkcion "IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);". Vi povas redoni false kiam oni vokas la komandon "fino" ĝis la uzantspaca aplikaĵo "mortas", tio estas, la funkcio "clientDied" ne estas vokita.

Dosiera protekto

Por protekti dosierojn, sufiĉas uzi la Kauth API, kiu permesas vin limigi aliron al dosieroj. Apple provizas programistojn per sciigoj pri diversaj eventoj en la amplekso; por ni, la operacioj KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA kaj KAUTH_VNODE_DELETE_CHILD estas gravaj. La plej facila maniero limigi la aliron al dosieroj estas per vojo - ni uzas la "vn_getpath" API por akiri la vojon al la dosiero kaj kompari la vojon prefikson. Notu, ke por optimumigi la renomadon de dosierujoj, la sistemo ne rajtigas aliron al ĉiu dosiero, sed nur al la dosierujo mem kiu estis renomita. Necesas kompari la gepatran vojon kaj limigi KAUTH_VNODE_DELETE por ĝi.

Kiel protekti procezojn kaj kernajn etendaĵojn ĉe macOS

La malavantaĝo de tiu aliro povas esti malalta efikeco kiam la nombro da prefiksoj pliiĝas. Por certigi, ke la komparo ne egalas al O (prefikso*longo), kie prefikso estas la nombro da prefiksoj, longo estas la longo de la ŝnuro, vi povas uzi determinisman finhavan aŭtomaton (DFA) konstruita per prefiksoj.

Ni konsideru metodon por konstrui DFA por donita aro de prefiksoj. Ni pravalorigas la kursorojn komence de ĉiu prefikso. Se ĉiuj kursoroj montras al la sama signo, tiam pliigu ĉiun kursoron je unu signo kaj memoru, ke la longo de la sama linio estas pli granda je unu. Se estas du kursoroj kun malsamaj simboloj, dividu la kursorojn en grupojn laŭ la simbolo, kiun ili montras kaj ripetu la algoritmon por ĉiu grupo.

En la unua kazo (ĉiuj signoj sub la kursoroj estas samaj), ni ricevas DFA-ŝtaton, kiu havas nur unu transiron laŭ la sama linio. En la dua kazo, ni ricevas tabelon de transiroj de grandeco 256 (nombro da signoj kaj maksimuma nombro da grupoj) al postaj statoj akiritaj per rekursie vokado de la funkcio.

Ni rigardu ekzemplon. Por aro da prefiksoj (“/foo/bar/tmp/”, “/var/db/foo/”, “/foo/bar/aba/”, “foo/bar/aac/”) vi povas ricevi la jenajn DFA. La figuro montras nur transirojn kondukantajn al aliaj ŝtatoj; aliaj transiroj ne estos finaj.

Kiel protekti procezojn kaj kernajn etendaĵojn ĉe macOS

Tra la DKA-ŝtatoj, povas esti 3 kazoj.

  1. La fina stato estas atingita - la vojo estas protektita, ni limigas la operaciojn KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA kaj KAUTH_VNODE_DELETE_CHILD
  2. La fina stato ne estis atingita, sed la vojo "finiĝis" (la nula finaĵo estis atingita) - la vojo estas gepatro, necesas limigi KAUTH_VNODE_DELETE. Notu, ke se vnode estas dosierujo, vi devas aldoni '/' ĉe la fino, alie ĝi povas limigi ĝin al la dosiero "/foor/bar/t", kiu estas malĝusta.
  3. La fina stato ne estis atingita, la vojo ne finiĝis. Neniu el la prefiksoj kongruas kun ĉi tiu, ni ne enkondukas restriktojn.

konkludo

La celo de la sekurecaj solvoj evoluantaj estas pliigi la nivelon de sekureco de la uzanto kaj liaj datumoj. Unuflanke, ĉi tiu celo estas atingita per la disvolviĝo de la programaro Acronis, kiu fermas tiujn vundeblecojn, kie la operaciumo mem estas "malforta". Aliflanke, ni ne devas neglekti plifortigi tiujn sekurecajn aspektojn plibonigeblajn flanke de OS, precipe ĉar fermi tiajn vundeblecojn pliigas nian propran stabilecon kiel produkto. La vundebleco estis raportita al la Apple Product Security Team kaj estis riparita en macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Kiel protekti procezojn kaj kernajn etendaĵojn ĉe macOS

Ĉio ĉi povas esti farita nur se via ilo estis oficiale instalita en la kernon. Tio estas, ne ekzistas tiaj kaŝpasejoj por ekstera kaj nedezirata programaro. Tamen, kiel vi povas vidi, eĉ protekti laŭleĝajn programojn kiel kontraŭvirusajn kaj rezervajn sistemojn postulas laboron. Sed nun novaj produktoj de Acronis por macOS havos plian protekton kontraŭ malŝarĝo de la sistemo.

fonto: www.habr.com

Aldoni komenton