Как да защитим процесите и разширенията на ядрото в macOS

Здравейте, Хабр! Днес бих искал да поговорим за начините, по които можем да защитим процесите от атаки на злонамерени лица в macOS. Например, това е полезно за антивируса или системата за резервно копиране, особено предвид факта, че в macOS има няколко начина да "убиеш" процес. За това и методите за защита прочетете по-долу.

Как да защитим процесите и разширенията на ядрото в macOS

Класическият начин да "убиеш" процес

Всеки знае, че начинът да "убиеш" процес е да изпратиш сигнал SIGKILL на процеса. В bash можеш да извикаш стандартните "kill -SIGKILL PID" или "pkill -9 NAME" за убийство. Командата "kill" е известна още от времето на UNIX и е достъпна не само в macOS, но и в други UNIX-подобни системи.

Както и в UNIX-подобните системи, 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

Стандартният начин за създаване на демонски приложения при стартиране на системата и управление на времето им на живот е 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, "Неуспешно приключване на работа: %d: %s", error, strerror(error));
		job_log(j, LOG_NOTICE | LOG_CONSOLE, "Използване на резервна опция за приключване на работа...");
		error = kill2(j->p, SIGTERM);
		if (error) {
			job_log(j, LOG_ERR, "Неуспешно сигнализиране на работа: %d: %s", error, strerror(error));
		} 
...
<>

Под капака proc_terminate, въпреки името си, може да изпраща не само psignal с SIGTERM, но и SIGKILL.

Непряко убиване — ограничение на ресурсите

По-интересен случай може да се наблюдава в друг системен повик. process_policy. Стандартното използване на това системно повикване е за ограничаване на ресурсите на приложения, например за индексатор ограничение на квота на процесорното време и паметта, за да не забавя системата в резултат на действията по кеширане на файлове. Ако приложението е достигнало ограничението на ресурсите, какво може да се види от функцията proc_apply_resource_actions, на процеса се изпраща сигнал SIGKILL.

Въпреки че това системно повикване може потенциално да убива процеси, системата не е проверила адекватно правата на процеса, който извиква системното повикване. Всъщност проверката съществуваше, но е достатъчно да се използва алтернативния флаг PROC_POLICY_ACTION_SET, за да се заобиколи това условие.

Ако ограничите квотата за използване на CPU от приложението (например, разрешавайки да се изпълнява само 1 ns), можете да убиете всеки процес в системата. Така, злонамерен софтуер може да убие всеки процес в системата, включително и антивирусния. Интересен е и ефектът, който се получава при убийството на процес с pid 1 (launchctl) — изместване в ядрото при опит да обработи сигнал SIGKILL 🙂

Как да защитим процесите и разширенията на ядрото в macOS

Как да решите проблема?

Най-простият начин да забраните убиването на процеса е да замените указателя на функцията в таблицата за системни повиквания. За съжаление, този метод не е тривиален по много причини.

На първо място, символът, който отговаря за местоположението на sysent в паметта, не само е частен символ на ядрото XNU, но не може да бъде намерен сред символите на ядрото. Ще трябва да се използват евристични методи за търсене, например динамично дизасемблиране на функцията и търсене на указателя в нея.

На второ място, структурата на записите в таблицата зависи от флаговете, с които е компилирано ядрото. Ако е обявен флагът CONFIG_REQUIRES_U32_MUNGING, размерът на структурата ще се промени — ще бъде добавено допълнително поле. sy_arg_munge32. Необходимо е да се направи допълнителна проверка относно това, с какъв флаг е компилирано ядрото, като вариант, да се сверят указателите на функциите с известни.

struct sysent {         /* таблица за системни повиквания */
        sy_call_t       *sy_call;       /* изпълняваща функция */
#if CONFIG_REQUIRES_U32_MUNGING || (__arm__ && (__BIGGEST_ALIGNMENT__ > 4))
        sy_munge_t      *sy_arg_munge32; /* мунджер на аргументите за 32-битови процеси */
#endif
        int32_t         sy_return_type; /* типове на връщане от системни повиквания */
        int16_t         sy_narg;        /* брой аргументи */
        uint16_t        sy_arg_bytes;   /* Обща големина на аргументите в байтове за
                                         * 32-битови системни повиквания
                                         */
};

За щастие, в съвременните версии на macOS Apple предлага ново API за работа с процеси. 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++. Приложение в потребителското пространство ще може да "намери" регистрирания екземпляр на класа, за да установи връзка kernel-userspace.

За откритие на броя на екземплярите на класовете в системата съществува утилита ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Всяко ядро на разширение, което иска да се регистрира в стека на драйверите, трябва да декларира клас, наследен от IOService, например my_kext_ioservice в този случай. Свързването на потребителски приложения предизвиква създаването на нов екземпляр на класа, който наследява от IOUserClient, в примера my_kext_iouserclient.

При опит за изтегляне на драйвера от системата (команда kextunload) се извиква виртуалната функция “bool terminate(IOOptionBits options)”. Достатъчно е да върнете false при извикването на функцията terminate при опит за изтегляне, за да забраните kextunload.

bool Kext::terminate(IOOptionBits options)
{

  if (!IsUnloadAllowed)
  {
    // Изтеглянето не е разрешено, връщане на false
    return false;
  }

  return super::terminate(options);
}

Флагът IsUnloadAllowed може да бъде зададен от IOUserClient при зареждане. При ограничение на изтеглянето командата kextunload ще върне следния изход:

admin@admins-Mac drivermanager % sudo kextunload ./test.kext
Парола:
(kernel) Не може да се премахне kext my.kext.test; услугите не успяха да се прекратят - 0xe00002c7.
Неуспешно изтегляне на my.kext.test - (iokit/common) неподдържана функция.

Аналогична защита трябва да бъде направена и за IOUserClient. Екземплярите на класовете могат да бъдат изтеглени с помощта на функцията от потребителското пространство IOKitLib “IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);”. Може да се върне false при извикването на командата “terminate”, докато приложението в потребителското пространство не “умре”, т.е. не бъде извикана функцијата “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, който прикрива уязвимостите, где "слабата" е самата операционна система. От друга страна, не бива да пренебрегваме и укрепването на тези аспекти на сигурността, които могат да бъдат подобрени на страната на ОС, особено че закриването на подобни уязвимости повишава собствената ни устойчивост като продукт. Уязвимостта беше докладвана на екипа за сигурност на продуктите на Apple и беше поправена в macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Как да защитим процесите и разширенията на ядрото в macOS

Всичко това може да бъде направено само ако вашият инструмент е официално инсталиран в ядрото. Тоест за външно и нежелано софтуер няма такива вратички. Въпреки това, както виждате, дори за защитата на легитимни програми, като антивирус и система за архивиране, се изисква усилие. Но сега новите продукти Acronis за macOS ще имат допълнителна защита от извеждане от системата.

Източник: habr.com

Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри 🔥 Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри | ProHoster