نحوه محافظت از فرآیندها و پسوندهای هسته در macOS

سلام، هابر! امروز می خواهم در مورد نحوه محافظت از فرآیندها در برابر حملات مهاجمان در macOS صحبت کنم. به عنوان مثال، این برای یک آنتی ویروس یا سیستم پشتیبان مفید است، به ویژه از آنجایی که تحت macOS چندین راه برای "کشتن" یک فرآیند وجود دارد. در مورد این و روش های محافظت در زیر برش بخوانید.

نحوه محافظت از فرآیندها و پسوندهای هسته در macOS

روش کلاسیک برای "کشتن" یک فرآیند

یک راه شناخته شده برای "کشتن" یک فرآیند، ارسال یک سیگنال SIGKILL به فرآیند است. از طریق bash می‌توانید استاندارد «kill -SIGKILL PID» یا «pkill -9 NAME» را برای کشتن فراخوانی کنید. فرمان "کشتن" از زمان یونیکس شناخته شده است و نه تنها در macOS، بلکه در سایر سیستم های مشابه یونیکس نیز موجود است.

درست مانند سیستم‌های مشابه یونیکس، macOS به شما امکان می‌دهد هر سیگنالی را به یک فرآیند به جز دو سیگنال - SIGKILL و SIGSTOP رهگیری کنید. این مقاله در درجه اول بر سیگنال SIGKILL به عنوان سیگنالی که باعث از بین رفتن یک فرآیند می شود، تمرکز می کند.

مشخصات macOS

در macOS، فراخوانی سیستم kill در هسته XNU تابع psignal(SIGKILL،...) را فراخوانی می کند. بیایید سعی کنیم ببینیم که سایر اقدامات کاربر در فضای کاربر را می توان توسط تابع psignal فراخوانی کرد. بیایید فراخوانی‌های تابع psignal را در مکانیسم‌های داخلی هسته حذف کنیم (اگرچه ممکن است بی‌اهمیت باشند، اما آنها را به مقاله دیگری واگذار می‌کنیم - تأیید امضا، خطاهای حافظه، مدیریت خروج/خاتمه، نقض حفاظت از فایل، و غیره.

اجازه دهید بررسی را با تابع و فراخوانی سیستم مربوطه شروع کنیم terminate_with_payload. مشاهده می شود که علاوه بر تماس کلاسیک kill، رویکرد جایگزینی نیز وجود دارد که مختص سیستم عامل macOS است و در BSD یافت نمی شود. اصول عملیاتی هر دو فراخوانی سیستم نیز مشابه است. آنها فراخوانی مستقیم به سیگنال تابع هسته هستند. همچنین توجه داشته باشید که قبل از از بین بردن یک فرآیند، یک بررسی "کنسیگنال" انجام می شود - آیا فرآیند می تواند سیگنالی را به فرآیند دیگری ارسال کند یا خیر؛ برای مثال، سیستم به هیچ برنامه ای اجازه نمی دهد که فرآیندهای سیستم را از بین ببرد.

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

راه اندازی شد

راه استاندارد برای ایجاد دیمون در راه اندازی سیستم و کنترل طول عمر آنها راه اندازی شده است. لطفاً توجه داشته باشید که منابع مربوط به نسخه قدیمی launchctl تا macOS 10.10 هستند، نمونه‌های کد برای اهداف توضیحی ارائه شده است. Launchctl مدرن سیگنال های راه اندازی شده را از طریق 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 را با SIGTERM، بلکه SIGKILL ارسال کند.

کشتن غیر مستقیم - محدودیت منابع

مورد جالب تری را می توان در فراخوانی دیگر سیستم مشاهده کرد فرآیند_سیاست. استفاده متداول از این فراخوانی سیستم، محدود کردن منابع برنامه است، مانند ایندکس کننده برای محدود کردن زمان CPU و سهمیه حافظه به طوری که سیستم به طور قابل توجهی توسط فعالیت های ذخیره فایل کند نمی شود. همانطور که از تابع 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 {         /* 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، اپل یک API جدید برای کار با فرآیندها ارائه می دهد. Endpoint Security API به مشتریان اجازه می دهد تا بسیاری از درخواست ها را برای سایر فرآیندها مجاز کنند. بنابراین، با استفاده از API فوق الذکر، می توانید هر سیگنالی را به فرآیندها، از جمله سیگنال SIGKILL، مسدود کنید.

#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 proc_check_signal) را ارائه می دهد، اما API به طور رسمی پشتیبانی نمی شود.

حفاظت از پسوند هسته

علاوه بر محافظت از فرآیندهای موجود در سیستم، محافظت از پسوند هسته (kext) نیز ضروری است. macOS چارچوبی را برای توسعه دهندگان فراهم می کند تا به راحتی درایورهای دستگاه IOKit را توسعه دهند. علاوه بر ارائه ابزارهایی برای کار با دستگاه ها، IOKit روش هایی را برای انباشتن درایورها با استفاده از نمونه هایی از کلاس های C++ ارائه می کند. یک برنامه کاربردی در فضای کاربر قادر خواهد بود یک نمونه ثبت شده از کلاس را برای ایجاد یک رابطه هسته-فضای کاربر پیدا کند.

برای تشخیص تعداد نمونه های کلاس در سیستم، ابزار ioclasscount وجود دارد.

my_kext_ioservice = 1
my_kext_iouserclient = 1

هر برنامه افزودنی هسته ای که بخواهد با پشته راننده ثبت نام کند، باید کلاسی را که از IOService به ارث می برد، اعلام کند، برای مثال my_kext_ioservice در این مورد. اتصال برنامه های کاربر باعث ایجاد نمونه جدیدی از کلاس می شود که از IOUserClient به ارث می برد، در مثال my_kext_iouserclient.

هنگام تلاش برای تخلیه درایور از سیستم (فرمان kextunload)، تابع مجازی "bool terminate(IOOptionBits options)" فراخوانی می شود. برای خاتمه دادن به هنگام تلاش برای تخلیه برای غیرفعال کردن kextunload کافی است که در تماس false را برگردانید.

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 انجام شود. نمونه‌های کلاس‌ها را می‌توان با استفاده از تابع فضای کاربری IOKitLib "IOCatalogueTerminate(mach_port_t، پرچم uint32_t، io_name_t توضیحات)" بارگیری کرد. می‌توانید هنگام فراخوانی فرمان «terminate» تا زمانی که اپلیکیشن فضای کاربری «dies»، یعنی تابع «clientDied» فراخوانی نشود، false را برگردانید.

حفاظت از فایل

برای محافظت از فایل ها کافی است از Kauth API استفاده کنید که به شما امکان می دهد دسترسی به فایل ها را محدود کنید. اپل اعلان‌هایی را درباره رویدادهای مختلف در این حوزه به توسعه‌دهندگان ارائه می‌کند؛ برای ما، عملیات KAUTH_VNODE_DELETE، KAUTH_VNODE_WRITE_DATA و KAUTH_VNODE_DELETE_CHILD مهم هستند. ساده ترین راه برای محدود کردن دسترسی به فایل ها توسط مسیر است - ما از API "vn_getpath" برای دریافت مسیر فایل و مقایسه پیشوند مسیر استفاده می کنیم. توجه داشته باشید که برای بهینه سازی تغییر نام مسیرهای پوشه فایل، سیستم اجازه دسترسی به هر فایل را نمی دهد، بلکه فقط به خود پوشه ای که تغییر نام داده شده است. لازم است مسیر والد را مقایسه کنید و KAUTH_VNODE_DELETE را برای آن محدود کنید.

نحوه محافظت از فرآیندها و پسوندهای هسته در macOS

نقطه ضعف این روش ممکن است عملکرد پایین با افزایش تعداد پیشوندها باشد. برای اطمینان از اینکه مقایسه با O (پیشوند*طول) برابر نیست، جایی که پیشوند تعداد پیشوندها، طول طول رشته است، می توانید از یک خودکار محدود قطعی (DFA) ساخته شده توسط پیشوندها استفاده کنید.

بیایید روشی را برای ساخت یک DFA برای یک مجموعه معین از پیشوندها در نظر بگیریم. ما مکان نماها را در ابتدای هر پیشوند مقدار دهی اولیه می کنیم. اگر همه مکان نماها به یک کاراکتر اشاره می کنند، هر مکان نما را یک کاراکتر افزایش دهید و به یاد داشته باشید که طول همان خط یک عدد بزرگتر است. اگر دو نشانگر با نمادهای مختلف وجود دارد، مکان نماها را با توجه به نمادی که به آن اشاره می کنند به گروه ها تقسیم کنید و الگوریتم را برای هر گروه تکرار کنید.

در حالت اول (همه کاراکترهای زیر نشانگرها یکسان هستند)، یک حالت DFA دریافت می کنیم که تنها یک انتقال در امتداد همان خط دارد. در حالت دوم، جدولی از انتقال با اندازه 256 (تعداد کاراکترها و حداکثر تعداد گروه ها) به حالت های بعدی را می گیریم که با فراخوانی بازگشتی تابع به دست می آید.

بیایید به یک مثال نگاه کنیم. برای مجموعه ای از پیشوندها ("/foo/bar/tmp/"، "/var/db/foo/"، "/foo/bar/aba/"، "foo/bar/aac/") می توانید موارد زیر را دریافت کنید. DFA. شکل فقط انتقال هایی را نشان می دهد که به حالت های دیگر منتهی می شوند؛ سایر انتقال ها نهایی نخواهند بود.

نحوه محافظت از فرآیندها و پسوندهای هسته در macOS

هنگام عبور از ایالات DKA، ممکن است 3 مورد وجود داشته باشد.

  1. وضعیت نهایی رسیده است - مسیر محافظت می شود، ما عملیات KAUTH_VNODE_DELETE، KAUTH_VNODE_WRITE_DATA و KAUTH_VNODE_DELETE_CHILD را محدود می کنیم
  2. وضعیت نهایی به دست نیامد، اما مسیر "به پایان رسید" (پایان دهنده تهی رسید) - مسیر یک والد است، لازم است KAUTH_VNODE_DELETE را محدود کنید. توجه داشته باشید که اگر vnode یک پوشه است، باید یک '/' را در پایان اضافه کنید، در غیر این صورت ممکن است آن را به فایل "/foor/bar/t" محدود کند، که نادرست است.
  3. به حالت نهایی نرسید، راه تمام نشد. هیچ یک از پیشوندها با این یکی مطابقت ندارند، ما محدودیتی را معرفی نمی کنیم.

نتیجه

هدف از راه حل های امنیتی در حال توسعه افزایش سطح امنیت کاربر و داده های او است. از یک طرف، این هدف با توسعه محصول نرم افزاری Acronis به دست می آید، که آسیب پذیری هایی را که در آن سیستم عامل خود "ضعیف" است، می بندد. از سوی دیگر، ما نباید از تقویت جنبه‌های امنیتی که در بخش سیستم‌عامل قابل بهبود هستند غافل شویم، به خصوص که بستن چنین آسیب‌پذیری‌ها ثبات خود را به عنوان یک محصول افزایش می‌دهد. این آسیب پذیری به تیم امنیتی محصول اپل گزارش شده است و در macOS 10.14.5 (https://support.apple.com/en-gb/HT210119) رفع شده است.

نحوه محافظت از فرآیندها و پسوندهای هسته در macOS

همه این کارها فقط در صورتی انجام می شود که برنامه کاربردی شما به طور رسمی در هسته نصب شده باشد. یعنی چنین حفره هایی برای نرم افزارهای خارجی و ناخواسته وجود ندارد. با این حال، همانطور که می بینید، حتی محافظت از برنامه های قانونی مانند آنتی ویروس و سیستم های پشتیبان نیز نیاز به کار دارد. اما اکنون محصولات جدید Acronis برای macOS محافظت بیشتری در برابر تخلیه از سیستم خواهند داشت.

منبع: www.habr.com

اضافه کردن نظر