Как да защитаваме процесите и разширенията на ядрото в 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) {
			signal_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".

...
	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 за работа с процеси. 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_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”, докато приложението в userspace не „умре“, т.е. не се извика функцията “clientDied”.

Защита на файлове

За защита на файловете е достатъчно да се използва Kauth API, който позволява ограничаване на достъпа до файловете. Apple предоставя на разработчиците известия за различни събития в обхвата, за нас са важни операциите 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