كيفية حماية العمليات وملحقات kernel على macOS

مرحبا يا هابر! أود اليوم أن أتحدث عن كيفية حماية العمليات من المتطفلين في macOS. على سبيل المثال ، إنه مفيد لنظام مضاد للفيروسات أو نظام نسخ احتياطي ، خاصة في ضوء حقيقة أنه في نظام macOS هناك عدة طرق "لقتل" العملية. اقرأ عن ذلك وعن طرق الحماية تحت القط.

صورة

الطريقة الكلاسيكية لقتل العملية


الطريقة المعروفة "لقتل" العملية هي إرسال إشارة حول عملية SIGKILL. من خلال bash ، يمكنك استدعاء "kill -SIGKILL PID" القياسي أو "pkill -9 NAME" للقتل. كان أمر kill معروفًا منذ UNIX وهو متاح ليس فقط على macOS ، ولكن أيضًا على أنظمة أخرى شبيهة بـ UNIX.

كما هو الحال في الأنظمة الشبيهة بـ UNIX ، يتيح لك macOS اعتراض أي إشارات للعملية باستثناء إشارتين - SIGKILL و SIGSTOP. في هذه المقالة ، سيتم اعتبار إشارة SIGKILL في المقام الأول على أنها إشارة تؤدي إلى قتل العملية.

تفاصيل MacOS


على نظام macOS ، تستدعي استدعاء نظام القتل في XNU kernel وظيفة psignal (SIGKILL ، ...). دعونا نحاول أن نرى ما هي إجراءات المستخدم الأخرى في مساحة المستخدمين التي يمكن أن تستدعي وظيفة psignal. نحن نستبعد المكالمات إلى وظيفة psignal في الآليات الداخلية للنواة (على الرغم من أنها قد تكون غير بديهية ، دعنا نتركها لمقال آخر :) - التحقق من التوقيع ، أخطاء الذاكرة ، معالجة الخروج / الإنهاء ، انتهاك حماية الملف ، إلخ.

نبدأ النظرة العامة بالوظيفة واستدعاء النظام المقابل terminate_with_payload. يمكن ملاحظة أنه بالإضافة إلى دعوة القتل الكلاسيكية ، هناك نهج بديل خاص بنظام التشغيل macOS ولا يوجد في BSD. مبادئ التشغيل لكلا النظامين قريبة أيضًا. وهي مكالمات مباشرة إلى وظيفة kernel psignal. لاحظ أيضًا أنه قبل قتل العملية ، يتم إجراء فحص "إشارة" - سواء كانت العملية يمكنها إرسال إشارة إلى عملية أخرى ، فإن النظام لا يسمح لأي تطبيق بقتل عمليات النظام على سبيل المثال.

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

انطلاق


الطريقة القياسية لإنشاء daemons عند بدء تشغيل النظام والتحكم في حياتهم هي إطلاق. سألفت الانتباه إلى حقيقة أن شفرة المصدر للإصدار القديم من 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.

القتل غير المباشر - حد الموارد


يمكن رؤية حالة أكثر إثارة للاهتمام في استدعاء نظام process_policy آخر . الاستخدام القياسي لاستدعاء النظام هذا هو حدود موارد التطبيق ، على سبيل المثال ، بالنسبة للمفهرس ، هناك حد على وقت المعالج وحصة الذاكرة حتى لا يتباطأ النظام بشكل كبير من إجراءات التخزين المؤقت للملفات. إذا وصل التطبيق إلى حد الموارد ، كما يمكن رؤيته من وظيفة proc_apply_resource_actions ، يتم إرسال إشارة SIGKILL إلى العملية.

على الرغم من أن استدعاء النظام هذا من المحتمل أن يقتل عملية ، لم يقم النظام بالتحقق بشكل كافٍ من حقوق العملية التي تسببت في استدعاء النظام. في الواقع، وهو الاختيار موجودة ، ولكن يكفي أن استخدام PROC_POLICY_ACTION_SET العلم بديل لتجاوز هذا الشرط.

ومن ثم ، إذا قمت بـ "تحديد" حصة استخدام وحدة المعالجة المركزية بواسطة التطبيق (على سبيل المثال ، السماح بتنفيذ 1 ns فقط) ، فعندئذ يمكنك قتل أي عملية في النظام. لذلك ، يمكن أن تقتل البرامج الضارة أي عملية على النظام ، بما في ذلك عملية مكافحة الفيروسات. من المثير للاهتمام أيضًا التأثير الذي يحدث عندما يتم قتل العملية باستخدام pid 1 (launchctl) - ذعر النواة عند محاولة معالجة إشارة SIGKILL :)

صورة

كيفية حل هذه المشكلة؟


تتمثل الطريقة الأكثر وضوحًا لمنع قتل العملية في استبدال مؤشر الوظائف في جدول استدعاء النظام. لسوء الحظ ، هذه الطريقة غير تافهة لأسباب عديدة

، أولاً ، الرمز المسؤول عن موقع sysent في الذاكرة ليس فقط رمزًا خاصًا لـ XNU kernel ، ولكن أيضًا لا يمكن العثور عليه في رموز kernel. سيكون عليك استخدام طرق البحث الإرشادية ، على سبيل المثال ، التفكيك الديناميكي للوظيفة والبحث عن مؤشر بداخلها.

ثانيًا ، يعتمد هيكل الإدخالات في الجدول على الأعلام التي تم بناء النواة بها. إذا تم الإعلان عن 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 واجهة برمجة تطبيقات جديدة للعمل مع العمليات. تتيح واجهة برمجة تطبيقات Endpoint Security للعملاء تفويض العديد من الطلبات للعمليات الأخرى. لذلك ، يمكنك حظر أي إشارات للعمليات ، بما في ذلك إشارة 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("%d\n", getpid());
    sleep(60); // could be replaced with other waiting primitive

    es_unsubscribe_all(cli);
    es_delete_client(cli);

    return 0;
}

وبالمثل ، يمكنك تسجيل سياسة MAC في kernel ، والتي توفر طريقة حماية الإشارة (السياسة proc_check_signal) ، ولكن API غير مدعوم رسميًا.

حماية امتداد النواة


بالإضافة إلى حماية العمليات في النظام ، فإن حماية امتداد النواة نفسها (kext) ضرورية أيضًا. يوفر macOS إطار عمل للمطورين لتطوير برامج تشغيل الأجهزة IOKit بشكل ملائم. بالإضافة إلى توفير أدوات للعمل مع الأجهزة ، يوفر IOKit طرق تكديس برامج التشغيل باستخدام مثيلات فئات C ++. سيتمكن التطبيق الموجود في مساحة المستخدمين من "العثور" على مثيل مسجل للفئة لإنشاء اتصال مساحة مستخدمي kernel.

لاكتشاف عدد مثيلات الفئة في النظام ، توجد الأداة المساعدة ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

يجب أن يعلن أي ملحق kernel يرغب في التسجيل في مكدس برنامج التشغيل عن فئة موروثة من IOService ، على سبيل المثال ، my_kext_ioservice في هذه الحالة. سيؤدي توصيل تطبيقات المستخدم إلى إنشاء مثيل جديد للفئة التي ترث من IOUserClient ، في المثال my_kext_iouserclient.

عند محاولة تفريغ برنامج التشغيل من النظام (الأمر kextunload) ، يتم استدعاء الوظيفة الافتراضية "إنهاء bool (خيارات IOOptionBits)". يكفي إرجاع خطأ عند الاستدعاء إلى دالة الإنهاء عند محاولة إلغاء التحميل لتعطيل 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. يمكن إلغاء تحميل مثيلات الفصل باستخدام دالة مساحة المستخدمين IOKitLib "IOCatalogueTerminate (mach_port_t، uint32_t flag، io_name_t description)؛". يمكنك إرجاع خطأ في استدعاء إلى الأمر "إنهاء" إلى أن يموت المستخدمون في التطبيق ، أي أنه لا يوجد استدعاء للدالة clientDied.

حماية الملفات


لحماية الملفات ، يكفي استخدام Kauth API ، الذي يسمح لك بتقييد الوصول إلى الملفات. توفر Apple للمطورين إشعارات حول الأحداث المختلفة في النطاق ، وتعتبر العمليات KAUTH_VNODE_DELETE و KAUTH_VNODE_WRITE_DATA و KAUTH_VNODE_DELETE_CHILD مهمة بالنسبة لنا. يعد تقييد الوصول إلى الملفات أسهل على طول الطريق - نحن نستخدم API "vn_getpath" للحصول على المسار إلى الملف ومقارنة بادئة المسار. لاحظ أنه لتحسين إعادة تسمية مسارات المجلدات بالملفات ، لا يأذن النظام بالوصول إلى كل ملف ، ولكن فقط إلى المجلد نفسه ، الذي تمت إعادة تسميته. من الضروري مقارنة المسار الأصلي وتقييد KAUTH_VNODE_DELETE لذلك.

صورة

قد يكون عيب هذا النهج الأداء المنخفض مع زيادة عدد البادئات. لكي لا تكون المقارنة مساوية لـ O (البادئة * الطول) ، حيث البادئة هي عدد البادئات ، الطول هو طول السلسلة ، يمكنك استخدام آلة حالة محددة (DFA) تم إنشاؤها بواسطة البادئات.

فكر في طريقة لبناء DFA لمجموعة معينة من البادئات. نقوم بتهيئة المؤشرات في بداية كل بادئة. إذا كانت جميع المؤشرات تشير إلى نفس الحرف ، فإننا نزيد كل مؤشر بحرف واحد ونتذكر أن طول نفس السطر أكثر من واحد. إذا كان هناك مؤشران برموز مختلفة تحتهما ، فإننا نقسم المؤشرات إلى مجموعات حسب الرمز الذي يشيرون إليه ونكرر الخوارزمية لكل مجموعة.

في الحالة الأولى (جميع الأحرف الموجودة تحت المؤشر هي نفسها) ، نحصل على حالة DFA ، التي لديها انتقال واحد فقط على نفس السطر. في الحالة الثانية ، نحصل على جدول انتقالي بحجم 256 (عدد الأحرف والحد الأقصى لعدد المجموعات) في الحالات اللاحقة التي تم الحصول عليها عن طريق استدعاء الوظيفة بشكل متكرر.

تأمل في مثال. للحصول على مجموعة من البادئات ("/ foo / bar / tmp /" و "/ var / db / foo /" و "/ foo / bar / aba /" و "foo / bar / aac /") يمكنك الحصول على DFA التالي. يوضح الشكل فقط التحولات التي تؤدي إلى دول أخرى ، ولن تكون التحولات الأخرى نهائية.

صورة

عند المرور عبر حالات DKA ، قد تكون هناك 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).

صورة

كل هذا لا يمكن القيام به إلا إذا تم تثبيت الأداة المساعدة الخاصة بك رسميًا في النواة. أي أنه لا توجد مثل هذه الثغرات للبرامج الخارجية وغير المرغوب فيها. ومع ذلك ، كما ترى ، حتى لحماية البرامج الشرعية مثل أنظمة الحماية من الفيروسات وبرامج النسخ الاحتياطي ، عليك العمل بجد. ولكن الآن ، ستحصل منتجات Acronis الجديدة لنظام macOS على حماية إضافية ضد التفريغ من النظام.

All Articles