Як абараняць працэсы і пашырэнні ядра ў macOS

Прывітанне, Хабр! Сёння мне хацелася б пагаварыць аб тым, як можна абараніць працэсы ад замахаў зламыснікаў у macOS. Напрыклад, гэта карысна для антывіруса ці сістэмы рэзервовага капіявання, асабліва ў святле таго што пад macOS існуе адразу некалькі спосабаў "забіць" працэс. Аб гэтым і аб метадах абароны чытайце пад катом.

Як абараняць працэсы і пашырэнні ядра ў macOS

Класічны спосаб "забіць" працэс

Усім вядомы спосаб "забіць" працэс – паслаць сігнал аб SIGKILL працэсу. Праз bash можна выклікаць стандартныя "kill -SIGKILL PID" ці "pkill -9 NAME" для забойства. Каманда "kill" вядома яшчэ з часоў UNIX і даступная не толькі ў macOS, але і на іншых UNIX-like сістэмах.

Таксама як і ў UNIX-like сістэмах, macOS дазваляе перахапіць любыя сігналы да працэсу акрамя двух - SIGKILL і SIGSTOP. У гэтым артыкуле будзе ў першую чаргу разглядацца сігнал SIGKILL, як сігнал, які спараджае забойства працэсу.

Спецыфіка macOS

У macOS сістэмны выклік kill у ядры XNU выклікае функцыю psignal(SIGKILL,…). Паспрабуем паглядзець, якія яшчэ дзеянні карыстача ў userspace можа выклікаць функцыю psignal. Адсеім выклікі функцыі psignal ва ўнутраных механізмах ядра (хоць і яны могуць быць нетрывіяльнымі, але пакінем іх для іншага артыкула 🙂 — праверка подпісу, памылкі памяці, апрацоўка exit/terminate, парушэнне абароны файлаў і да т.п.

Пачнём агляд з функцыі і адпаведнага сістэмнага выкліку terminate_with_payload. Відаць, што апроч класічнага выкліку kill існуюць альтэрнатыўны падыход, які спецыфічны для аперацыйнай сістэмы macOS і не сустракаецца ў BSD. Прынцыпы працы абодвух сістэмных выклікаў таксама блізкія. Яны ўяўляюць сабой прамыя выклікі функцыі ядра psignal. Таксама зварачальны ўвага, што перад забойствам працэсу вырабляецца праверка “cansignal” – ці можа працэс адправіць сігнал іншаму працэсу, сістэма не дапушчае любому з дадаткам забіваць сістэмныя працэсы напрыклад.

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. Звярну ўвагу на тое, што зыходнікі прыведзены для старой версіі launchctl да macOS 10.10, прыклады кода прыведзены ў якасці ілюстрацыі. Сучасны launchctl адпраўляе сігналы launchd праз XPC, логіка launchctl перанесена ў яго.

Разгледзім як менавіта вырабляецца прыпынак прыкладанняў. Перад адпраўкай сігналу SIGTERM, прыкладанне спрабуюць спыніць пры дапамозе сістэмнага выкліку "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));
		} 
...
<>

Пад капотам proc_terminate, нягледзячы на ​​сваю назву, можа адпраўляць не толькі psignal c SIGTERM, але і SIGKILL.

Ускоснае забойства - абмежаванне на рэсурсы

Цікавейшы выпадак можна ўбачыць у іншым сістэмным выкліку process_policy. Стандартнае выкарыстанне гэтага сістэмнага выкліку - абмежаванні рэсурсаў прыкладанняў, напрыклад для індэксара абмежаванне на квоту працэсарнага часу і памяці, каб сістэма не істотна запавольвалася ад дзеянняў кэшавання файла. Калі дадатак дасягнула абмежаванні на рэсурсы, як можна ўбачыць з функцыі proc_apply_resource_actions, працэсу адпраўляецца сігнал SIGKILL.

Нягледзячы на ​​тое, што дадзены сістэмны выклік можа патэнцыйна здзяйсняць забойства працэсу, сістэма не правярала адэкватна правы працэсу, які выклікае сістэмны выклік. На самой справе праверка існавала, але дастаткова выкарыстоўваць альтэрнатыўны сцяг PROC_POLICY_ACTION_SET для абыходу гэтай умовы.

Адсюль калі "абмежаваць" квоту выкарыстання CPU дадаткам (напрыклад дазволіць выконвацца толькі 1 ns), то можна зрабіць забойства любога працэсу ў сістэме. Так, зловред можа забіць любы працэс на сістэме, у тым ліку і працэс антывіруса. Таксама цікавы эфект, які атрымліваецца пры забойстве працэсу з pid 1 (launchctl) - kernel panic пры спробе апрацаваць сігнал SIGKILL 🙂

Як абараняць працэсы і пашырэнні ядра ў macOS

Як вырашаць праблему?

Самы прамалінейны спосаб забараніць забіваць працэс - падмяніць паказальнік на функцыю ў табліцы сістэмных выклікаў. Нажаль, дадзены спосаб з'яўляецца нетрывіяльным па шматлікіх чынніках.

Па-першае, сімвал, які адказвае за становішча sysent у памяці, не толькі з'яўляецца прыватным знака ядра XNU, але і не можа быць знойдзены ў сімвалах ядра. Прыйдзецца выкарыстоўваць эўрыстычныя метады пошуку, напрыклад дынамічнае дызасэмбляванне функцыі і пошук паказальніка ў ёй.

Па-другое, структура запісаў у табліцы залежыць ад сцягоў, з якімі было сабрана ядро. Калі абвешчаны сцяг CONFIG_REQUIRES_U32_MUNGING, то памер структуры будзе зменены - дададзена дадатковае поле sy_arg_munge32. Неабходна рабіць дадатковую праверку на тое, з якім сцягам было скампілявана ядро, як варыянт звяраць паказальнікі на функцыі з вядомымі.

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

На шчасце, у сучасных версіях macOS Apple падае новае API для працы з працэсамі. Endpoint Security API дазваляе кліентамі аўтарызаваць многія запыты да іншых працэсаў. Так, можна заблакаваць любыя сігналы да працэсы, у тым ліку сігнал SIGKILL пры дапамозе вышэйзгаданага 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;
}

Аналагічна ў ядры можна зарэгістраваць MAC Policy, які падае метад абароны ад сігналаў (policy proc_check_signal), аднак API не падтрымліваецца афіцыйна.

Абарона пашырэння ядра

Апроч абароны працэсаў у сістэме абавязкова неабходна і абарона самага пашырэння ядра (kext). macOS дае для распрацоўшчыкаў фрэймворк для зручнай распрацоўкі драйвераў прылад IOKit. Апроч прадастаўлення сродкаў працы з прыладамі, IOKit забяспечвае метады сцякавання драйвераў (driver stacking) пры дапамозе асобнікаў класаў C++. Прыкладанне ў userspace зможа "знайсці" зарэгістраваны асобнік класа для ўсталявання сувязі kernel-userspace.

Для выяўлення колькасці асобнікаў класаў у сістэме існуе ўтыліта ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Любое пашырэнне ядра, якое жадае зарэгістравацца ў стэку драйвераў, абавязана абвясціць клас, успадкаваны ад IOService, напрыклад, my_kext_ioservice у дадзеным выпадку. Падлучэнне карыстацкіх прыкладанняў выклікае стварэнне новага асобніка класа, які ўспадкоўваецца ад IOUserClient, у прыкладзе my_k.

Пры спробе выгрузкі драйвера з сістэмы (каманда kextunload) выклікаецца віртуальная функцыя "bool terminate (IOOptionBits options)". Досыць вярнуць false на выкліку функцыі terminate пры спробе выгрузкі, каб забараніць kextunload.

bool Kext::terminate(IOOptionBits options)
{

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

  return super::terminate(options);
}

Сцяг IsUnloadAllowed можа быць выстаўлены IOUserClient пры загрузцы. Пры абмежаванні на загрузку каманда kextunload верне наступную выснову:

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.

Аналагічную абарону неабходна зрабіць і для IOUserClient. Асобнікі класаў можна выгрузіць пры дапамозе userspace функцыі IOKitLib "IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);". Можна вяртаць false на выкліку каманды "terminate" пакуль userspace прыкладанне не "памрэ", гэта значыць не будзе выклік функцыі "clientDied".

Абарона файлаў

Для абароны файлаў дастаткова выкарыстоўваць Kauth API, які дазваляе абмяжоўваць доступ да файлаў. Apple дае распрацоўнікам натыфікацыі аб розных падзеях у scope, для нас важныя аперацыі KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA і KAUTH_VNODE_DELETE_CHILD. Абмяжоўваць доступ да файлаў прасцей за ўсё па шляху - выкарыстоўваем API "vn_getpath" для атрымання шляху да файла і вырабляем параўнанне прэфікса шляху. Заўважым, што для аптымізацыі перайменавання шляхоў тэчак з файламі, сістэма не аўтарызуе доступ да кожнага файла, але толькі да самай тэчкі, якую перайменавалі. Неабходна рабіць параўнанне бацькоўскага шляху і абмяжоўваць KAUTH_VNODE_DELETE для яе.

Як абараняць працэсы і пашырэнні ядра ў macOS

Недахопам дадзенага падыходу можа стаць нізкая прадукцыйнасць пры ўзрастанні колькасці прэфіксаў. Для таго, каб параўнанне не было роўна O(prefix*length), дзе prefix - колькасць прэфіксаў, length - даўжыня радка, можна выкарыстоўваць дэтэрмінаваных канчатковы аўтамат (ДКА), пабудаваны па прэфіксах.

Разгледзім спосаб пабудовы ДКА для дадзенага набору прэфіксаў. Ініцыялізуем курсоры на пачатак кожнага прэфікса. Калі ўсе курсоры паказваюць на адзін і той жа сімвал, то павялічым кожны курсор на адзін сімвал і запомнім, што даўжыня аднолькавага радка большая на адзінку. Калі існуе два курсоры, сімвалы пад якімі розныя, падзелім курсоры на групы па сімвале, на якія яны паказваюць і паўторым алгарытм для кожнай групы.

У першым выпадку (усе знакі пад курсорамі аднолькавыя) атрымліваем стан ДКА, якое мае толькі адзін пераход па аднолькавым радку. У другім выпадку, атрымліваем табліцу пераходаў памерам 256 (колькасць знакаў і максімальная колькасць груп) у наступныя станы, атрыманыя пры рэкурсіўным выкліку функцыі.

Разгледзім прыклад. Для набору прэфіксаў (“/foo/bar/tmp/”, “/var/db/foo/”, “/foo/bar/aba/”, “foo/bar/aac/”) можна атрымаць наступны ДКА. На малюнку паказаны толькі пераходы, якія вядуць у іншыя станы, іншыя пераходы не будуць з'яўляцца канчатковымі.

Як абараняць працэсы і пашырэнні ядра ў macOS

Пры праходжанні па станах ДКА можа аказацца 3 выпадкі.

  1. Быў дасягнуты фінальны стан - шлях з'яўляецца абароненым, абмяжоўваем аперацыі KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA і KAUTH_VNODE_DELETE_CHILD
  2. Не было дасягнута фінальнае стан, але шлях "скончыўся" (быў дасягнуты нуль-тэрмінатар) - шлях з'яўляецца бацькоўскім, неабходна абмяжоўваць KAUTH_VNODE_DELETE. Заўважым, што калі vnode з'яўляецца тэчкай, трэба дадаць у канец '/', у адваротным выпадку можа вырабляцца абмежаванне да файла "/foor/bar/t", што няслушна.
  3. Не было дасягнута фінальнае стан, шлях не скончыўся. Ніводны з прэфіксаў не адпавядае дадзеным, не ўводны абмежаванні.

Заключэнне

Мэтай распрацоўваных сек'юрыці-рашэнняў з'яўляецца павышэнне ўзроўню бяспекі карыстальніка і яго даных. З аднаго боку гэтая мэта забяспечваецца распрацоўкай праграмнага прадукта Acronis, які зачыняе тыя ўразлівасці, дзе "слабая" сама аперацыйная сістэма. З іншага боку не варта грэбаваць і ўзмацненнем тых аспектаў бяспекі, якія можна палепшыць на боку OS, тым больш што зачыненне падобных уразлівасцяў павялічвае нашу ўласную ўстойлівасць як прадукта. Уразлівасць была паведамлена Apple Product Security Team і была выпраўлена ў macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Як абараняць працэсы і пашырэнні ядра ў macOS

Усё гэта можна зрабіць толькі ў тым выпадку, калі ваша ўтыліта была афіцыйна ўсталявана ў ядро. Гэта значыць для вонкавага і непажаданага ПА няма такіх шчылін. Аднак, як вы бачыце, нават для абароны легітымных праграм, такіх як антывірус і сістэма рэзервовага капіявання, даводзіцца папрацаваць. Але затое зараз новыя прадукты Acronis для macOS будуць мець дадатковую абарону ад выгрузкі з сістэмы.

Крыніца: habr.com

Дадаць каментар