Paano protektahan ang mga proseso at mga extension ng kernel sa macOS

Hello, Habr! Ngayon gusto kong pag-usapan kung paano mo mapoprotektahan ang mga proseso mula sa mga pag-atake ng mga umaatake sa macOS. Halimbawa, ito ay kapaki-pakinabang para sa isang antivirus o backup system, lalo na dahil sa ilalim ng macOS mayroong ilang mga paraan upang "patayin" ang isang proseso. Basahin ang tungkol dito at mga paraan ng proteksyon sa ilalim ng hiwa.

Paano protektahan ang mga proseso at mga extension ng kernel sa macOS

Ang klasikong paraan upang "patayin" ang isang proseso

Ang isang kilalang paraan upang "patayin" ang isang proseso ay ang magpadala ng signal ng SIGKILL sa proseso. Sa pamamagitan ng bash maaari mong tawagan ang karaniwang "kill -SIGKILL PID" o "pkill -9 NAME" para pumatay. Ang utos na "kill" ay kilala mula noong mga araw ng UNIX at magagamit hindi lamang sa macOS, kundi pati na rin sa iba pang mga sistemang tulad ng UNIX.

Tulad ng sa mga sistemang tulad ng UNIX, pinapayagan ka ng macOS na ma-intercept ang anumang signal sa isang proseso maliban sa dalawa - SIGKILL at SIGSTOP. Pangunahing tututukan ang artikulong ito sa signal ng SIGKILL bilang isang senyales na nagiging sanhi ng pagkapatay ng isang proseso.

mga detalye ng macOS

Sa macOS, tinatawag ng kill system call sa XNU kernel ang psignal(SIGKILL,...) function. Subukan nating makita kung ano ang ibang mga aksyon ng user sa userspace na matatawag ng psignal function. Tanggalin natin ang mga tawag sa psignal function sa mga panloob na mekanismo ng kernel (bagaman maaaring hindi mahalaga ang mga ito, ngunit iiwan natin ang mga ito para sa isa pang artikulo 🙂 - pag-verify ng lagda, mga error sa memorya, paghawak ng exit/terminate, mga paglabag sa proteksyon ng file, atbp.

Simulan natin ang pagsusuri gamit ang function at ang kaukulang system call wakasan_may_bayad. Makikita na bilang karagdagan sa klasikong kill call, mayroong isang alternatibong diskarte na partikular sa macOS operating system at hindi matatagpuan sa BSD. Ang mga prinsipyo ng pagpapatakbo ng parehong mga tawag sa system ay magkatulad din. Ang mga ito ay direktang tawag sa kernel function na psignal. Tandaan din na bago pumatay ng isang proseso, isang "cansignal" na pagsusuri ay isinasagawa - kung ang proseso ay maaaring magpadala ng isang senyas sa isa pang proseso; ang system ay hindi pinapayagan ang anumang application na patayin ang mga proseso ng system, halimbawa.

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);
	}
...
}

inilunsad

Ang karaniwang paraan upang lumikha ng mga daemon sa pagsisimula ng system at kontrolin ang kanilang buhay ay inilunsad. Pakitandaan na ang mga pinagmulan ay para sa lumang bersyon ng launchctl hanggang sa macOS 10.10, ang mga halimbawa ng code ay ibinigay para sa mga layuning naglalarawan. Ang modernong launchctl ay nagpapadala ng mga senyales ng paglulunsad sa pamamagitan ng XPC, ang lohika ng launchctl ay inilipat dito.

Tingnan natin kung paano eksaktong huminto ang mga application. Bago ipadala ang signal ng SIGTERM, sinusubukang ihinto ang application gamit ang "proc_terminate" system call.

<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));
		} 
...
<>

Sa ilalim ng hood, ang proc_terminate, sa kabila ng pangalan nito, ay maaaring magpadala hindi lamang ng psignal na may SIGTERM, kundi pati na rin sa SIGKILL.

Indirect Kill - Limitasyon sa Resource

Ang isang mas kawili-wiling kaso ay makikita sa isa pang tawag sa system proseso_patakaran. Ang karaniwang paggamit ng system call na ito ay upang limitahan ang mga mapagkukunan ng application, tulad ng para sa isang indexer na limitahan ang oras ng CPU at mga quota ng memorya upang ang system ay hindi makabuluhang mapabagal ng mga aktibidad sa pag-cache ng file. Kung naabot ng isang application ang limitasyon ng mapagkukunan nito, tulad ng makikita mula sa function na proc_apply_resource_actions, isang signal ng SIGKILL ang ipapadala sa proseso.

Bagama't ang system call na ito ay maaaring potensyal na pumatay ng isang proseso, hindi sapat na sinuri ng system ang mga karapatan ng proseso na tumatawag sa system call. Talagang sinusuri umiral, ngunit sapat na na gamitin ang alternatibong flag na PROC_POLICY_ACTION_SET upang laktawan ang kundisyong ito.

Kaya, kung "limitahan" mo ang quota sa paggamit ng CPU ng application (halimbawa, pinapayagan lamang ang 1 ns na tumakbo), maaari mong patayin ang anumang proseso sa system. Kaya, maaaring patayin ng malware ang anumang proseso sa system, kabilang ang proseso ng antivirus. Kawili-wili din ang epekto na nangyayari kapag pinapatay ang isang proseso na may pid 1 (launchctl) - kernel panic kapag sinusubukang iproseso ang signal ng SIGKILL :)

Paano protektahan ang mga proseso at mga extension ng kernel sa macOS

Paano malulutas ang problema?

Ang pinakasimpleng paraan upang maiwasang mapatay ang isang proseso ay ang palitan ang function pointer sa system call table. Sa kasamaang palad, ang pamamaraang ito ay hindi mahalaga para sa maraming mga kadahilanan.

Una, ang simbolo na kumokontrol sa lokasyon ng memorya ng sysent ay hindi lamang pribado sa simbolo ng kernel ng XNU, ngunit hindi makikita sa mga simbolo ng kernel. Kakailanganin mong gumamit ng heuristic na mga paraan ng paghahanap, tulad ng dynamic na pag-disassemble ng function at paghahanap ng pointer dito.

Pangalawa, ang istraktura ng mga entry sa talahanayan ay nakasalalay sa mga flag kung saan ang kernel ay pinagsama-sama. Kung ang CONFIG_REQUIRES_U32_MUNGING na watawat ay idineklara, ang laki ng istraktura ay mababago - isang karagdagang field ay idaragdag sy_arg_munge32. Kinakailangang magsagawa ng karagdagang pagsusuri upang matukoy kung aling bandila ang pinagsama-sama ng kernel, o bilang kahalili, suriin ang mga pointer ng function laban sa mga kilala.

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

Sa kabutihang palad, sa mga modernong bersyon ng macOS, nagbibigay ang Apple ng bagong API para sa pagtatrabaho sa mga proseso. Ang Endpoint Security API ay nagbibigay-daan sa mga kliyente na pahintulutan ang maraming kahilingan sa iba pang mga proseso. Kaya, maaari mong harangan ang anumang signal sa mga proseso, kabilang ang signal ng SIGKILL, gamit ang nabanggit na 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;
}

Katulad nito, maaaring irehistro ang isang MAC Policy sa kernel, na nagbibigay ng paraan ng proteksyon ng signal (policy proc_check_signal), ngunit hindi opisyal na sinusuportahan ang API.

Proteksyon ng kernel extension

Bilang karagdagan sa pagprotekta sa mga proseso sa system, ang pagprotekta sa mismong kernel extension (kext) ay kinakailangan din. Ang macOS ay nagbibigay ng balangkas para sa mga developer upang madaling bumuo ng mga driver ng IOKit device. Bilang karagdagan sa pagbibigay ng mga tool para sa pagtatrabaho sa mga device, ang IOKit ay nagbibigay ng mga pamamaraan para sa pag-stack ng driver gamit ang mga pagkakataon ng mga klase ng C++. Ang isang application sa userspace ay magagawang "makahanap" ng isang rehistradong instance ng klase upang magtatag ng isang kernel-userspace na relasyon.

Upang makita ang bilang ng mga instance ng klase sa system, mayroong ioclasscount utility.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Anumang kernel extension na gustong magparehistro sa driver stack ay dapat magdeklara ng klase na nagmana mula sa IOService, halimbawa my_kext_ioservice sa kasong ito. Ang pagkonekta ng mga application ng user ay nagiging sanhi ng paglikha ng bagong instance ng klase na nagmana mula sa IOUserClient, sa halimbawa my_kext_iouserclient.

Kapag sinusubukang i-unload ang isang driver mula sa system (kextunload command), ang virtual function na "bool terminate(IOOptionBits options)" ay tinatawag. Ito ay sapat na upang ibalik ang false sa tawag upang wakasan kapag sinusubukang i-unload upang hindi paganahin ang kextunload.

bool Kext::terminate(IOOptionBits options)
{

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

  return super::terminate(options);
}

Ang IsUnloadAllowed flag ay maaaring itakda ng IOUserClient kapag naglo-load. Kapag may limitasyon sa pag-download, ibabalik ng kextunload command ang sumusunod na output:

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.

Ang katulad na proteksyon ay dapat gawin para sa IOUserClient. Maaaring i-unload ang mga pagkakataon ng mga klase gamit ang IOKitLib userspace function na “IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);”. Maaari kang magbalik ng false kapag tumatawag sa command na "terminate" hanggang sa "mamatay" ang userspace application, ibig sabihin, ang function na "clientDied" ay hindi tinatawag.

Proteksyon ng file

Upang protektahan ang mga file, sapat na ang paggamit ng Kauth API, na nagbibigay-daan sa iyong paghigpitan ang pag-access sa mga file. Nagbibigay ang Apple sa mga developer ng mga abiso tungkol sa iba't ibang kaganapan sa saklaw; para sa amin, ang mga pagpapatakbong KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA at KAUTH_VNODE_DELETE_CHILD ay mahalaga. Ang pinakamadaling paraan upang paghigpitan ang pag-access sa mga file ay sa pamamagitan ng path - ginagamit namin ang "vn_getpath" API upang makuha ang path patungo sa file at ihambing ang prefix ng path. Tandaan na upang ma-optimize ang pagpapalit ng pangalan ng mga path ng folder ng file, hindi pinahihintulutan ng system ang pag-access sa bawat file, ngunit sa mismong folder lamang na pinalitan ng pangalan. Kinakailangang ihambing ang landas ng magulang at paghigpitan ang KAUTH_VNODE_DELETE para dito.

Paano protektahan ang mga proseso at mga extension ng kernel sa macOS

Ang disbentaha ng diskarteng ito ay maaaring mababa ang pagganap habang tumataas ang bilang ng mga prefix. Upang matiyak na ang paghahambing ay hindi katumbas ng O(prefix*length), kung saan ang prefix ay ang bilang ng mga prefix, ang haba ay ang haba ng string, maaari kang gumamit ng deterministic finite automaton (DFA) na binuo ng mga prefix.

Isaalang-alang natin ang isang paraan para sa pagbuo ng isang DFA para sa isang naibigay na hanay ng mga prefix. Sinisimulan namin ang mga cursor sa simula ng bawat prefix. Kung ang lahat ng cursor ay tumuturo sa parehong character, pagkatapos ay dagdagan ang bawat cursor ng isang character at tandaan na ang haba ng parehong linya ay mas malaki ng isa. Kung mayroong dalawang cursor na may magkaibang mga simbolo, hatiin ang mga cursor sa mga pangkat ayon sa simbolo na kanilang itinuturo at ulitin ang algorithm para sa bawat pangkat.

Sa unang kaso (lahat ng mga character sa ilalim ng mga cursor ay pareho), nakakakuha kami ng isang estado ng DFA na may isang paglipat lamang sa parehong linya. Sa pangalawang kaso, nakakakuha kami ng talahanayan ng mga transition na may sukat na 256 (bilang ng mga character at maximum na bilang ng mga grupo) sa mga kasunod na estado na nakuha sa pamamagitan ng recursively na pagtawag sa function.

Tingnan natin ang isang halimbawa. Para sa isang hanay ng mga prefix (“/foo/bar/tmp/”, “/var/db/foo/”, “/foo/bar/aba/”, “foo/bar/aac/”) maaari mong makuha ang sumusunod DFA. Ang figure ay nagpapakita lamang ng mga transition na humahantong sa ibang mga estado; ang ibang mga transition ay hindi magiging pinal.

Paano protektahan ang mga proseso at mga extension ng kernel sa macOS

Kapag dumaan sa mga estado ng DKA, maaaring mayroong 3 kaso.

  1. Naabot na ang huling estado - protektado ang landas, nililimitahan namin ang mga pagpapatakbong KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA at KAUTH_VNODE_DELETE_CHILD
  2. Ang pangwakas na estado ay hindi naabot, ngunit ang landas ay "natapos" (ang null terminator ay naabot) - ang landas ay isang magulang, ito ay kinakailangan upang limitahan ang KAUTH_VNODE_DELETE. Tandaan na kung ang vnode ay isang folder, kailangan mong magdagdag ng '/' sa dulo, kung hindi, maaari itong limitahan sa file na “/foor/bar/t”, na hindi tama.
  3. Ang huling estado ay hindi naabot, ang landas ay hindi nagtatapos. Wala sa mga prefix ang tumutugma sa isang ito, hindi kami nagpapakilala ng mga paghihigpit.

Konklusyon

Ang layunin ng mga solusyon sa seguridad na binuo ay upang mapataas ang antas ng seguridad ng user at ng kanyang data. Sa isang banda, ang layuning ito ay nakakamit sa pamamagitan ng pagbuo ng produkto ng software ng Acronis, na nagsasara sa mga kahinaan kung saan ang operating system mismo ay "mahina". Sa kabilang banda, hindi natin dapat pabayaan ang pagpapalakas sa mga aspeto ng seguridad na maaaring mapabuti sa panig ng OS, lalo na dahil ang pagsasara ng mga naturang kahinaan ay nagpapataas ng ating sariling katatagan bilang isang produkto. Ang kahinaan ay iniulat sa Apple Product Security Team at naayos na sa macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Paano protektahan ang mga proseso at mga extension ng kernel sa macOS

Ang lahat ng ito ay magagawa lamang kung ang iyong utility ay opisyal na na-install sa kernel. Iyon ay, walang ganoong mga butas para sa panlabas at hindi gustong software. Gayunpaman, tulad ng nakikita mo, kahit na ang pagprotekta sa mga lehitimong programa tulad ng antivirus at backup system ay nangangailangan ng trabaho. Ngunit ngayon ang mga bagong produkto ng Acronis para sa macOS ay magkakaroon ng karagdagang proteksyon laban sa pag-unload mula sa system.

Pinagmulan: www.habr.com

Magdagdag ng komento