วิธีปกป้องกระบวนการและส่วนขยายเคอร์เนลบน macOS

สวัสดีฮับ! วันนี้ฉันอยากจะพูดถึงวิธีที่คุณสามารถปกป้องกระบวนการจากการโจมตีของผู้โจมตีใน macOS ตัวอย่างเช่น สิ่งนี้มีประโยชน์สำหรับโปรแกรมป้องกันไวรัสหรือระบบสำรองข้อมูล โดยเฉพาะอย่างยิ่งเนื่องจากภายใต้ macOS มีหลายวิธีในการ "ฆ่า" กระบวนการ อ่านข้อมูลเกี่ยวกับสิ่งนี้และวิธีการป้องกันขณะถูกบาด

วิธีปกป้องกระบวนการและส่วนขยายเคอร์เนลบน macOS

วิธีคลาสสิกในการ "ฆ่า" กระบวนการ

วิธีที่รู้จักกันดีในการ "ฆ่า" กระบวนการคือการส่งสัญญาณ SIGKILL ไปยังกระบวนการ ด้วยการทุบตีคุณสามารถเรียกมาตรฐานว่า "kill -SIGKILL PID" หรือ "pkill -9 NAME" เพื่อฆ่า คำสั่ง “kill” เป็นที่รู้จักมาตั้งแต่สมัยของ UNIX และสามารถใช้ได้ไม่เพียงแต่บน macOS เท่านั้น แต่ยังสามารถใช้ได้บนระบบที่คล้ายกับ UNIX อื่นๆ ด้วย

เช่นเดียวกับในระบบที่คล้ายกับ UNIX macOS ช่วยให้คุณสามารถสกัดกั้นสัญญาณใดๆ ที่ไปยังกระบวนการยกเว้นสองสัญญาณ - SIGKILL และ SIGSTOP บทความนี้จะเน้นไปที่สัญญาณ SIGKILL เป็นหลักซึ่งเป็นสัญญาณที่ทำให้กระบวนการถูกฆ่า

ข้อมูลเฉพาะของ macOS

บน macOS การเรียกระบบ kill ในเคอร์เนล XNU จะเรียกใช้ฟังก์ชัน psignal(SIGKILL,...) เรามาดูกันว่าฟังก์ชัน psignal สามารถเรียกใช้การกระทำของผู้ใช้อื่นในพื้นที่ผู้ใช้ได้อย่างไร เรามากำจัดการเรียกใช้ฟังก์ชัน psignal ในกลไกภายในของเคอร์เนลกันดีกว่า (แม้ว่าพวกมันอาจจะไม่สำคัญ แต่เราจะทิ้งไว้สำหรับบทความอื่น 🙂 - การตรวจสอบลายเซ็น, ข้อผิดพลาดของหน่วยความจำ, การจัดการออก/ยุติ, การละเมิดการป้องกันไฟล์ ฯลฯ .

มาเริ่มการตรวจสอบด้วยฟังก์ชันและการเรียกของระบบที่เกี่ยวข้องกัน สิ้นสุด_with_payload. จะเห็นได้ว่านอกเหนือจาก kill call แบบคลาสสิกแล้ว ยังมีแนวทางอื่นที่ใช้เฉพาะกับระบบปฏิบัติการ 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);
	}
...
}

เปิดตัว

เปิดตัววิธีมาตรฐานในการสร้าง daemons เมื่อเริ่มต้นระบบและควบคุมอายุการใช้งาน โปรดทราบว่าแหล่งที่มามีไว้สำหรับ 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 ด้วย 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 เวอร์ชันใหม่ 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 สามารถลงทะเบียนในเคอร์เนลได้ ซึ่งมีวิธีการป้องกันสัญญาณ (นโยบาย 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) ฟังก์ชันเสมือน “บูลยุติ (ตัวเลือก IOOptionBits)” จะถูกเรียก การคืนค่าเท็จในการเรียกเพื่อยุติเมื่อพยายามยกเลิกการโหลดเพื่อปิดใช้งาน kextunload ก็เพียงพอแล้ว

bool Kext::terminate(IOOptionBits options)
{

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

  return super::terminate(options);
}

IOUserClient สามารถตั้งค่าสถานะ IsUnloadAllowed ได้เมื่อโหลด เมื่อมีขีดจำกัดการดาวน์โหลด คำสั่ง 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);” คุณสามารถคืนค่าเท็จได้เมื่อเรียกใช้คำสั่ง "terminate" จนกระทั่งแอปพลิเคชัน userspace "dies" นั่นคือฟังก์ชัน "clientDied" จะไม่ถูกเรียก

การป้องกันไฟล์

เพื่อปกป้องไฟล์ ก็เพียงพอแล้วที่จะใช้ Kauth API ซึ่งช่วยให้คุณจำกัดการเข้าถึงไฟล์ได้ Apple แจ้งนักพัฒนาเกี่ยวกับเหตุการณ์ต่างๆ ในขอบเขต สำหรับเรา การดำเนินการ 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/”) คุณจะได้รับสิ่งต่อไปนี้ ดีเอฟเอ รูปภาพแสดงเฉพาะการเปลี่ยนผ่านที่นำไปสู่สถานะอื่น ส่วนการเปลี่ยนผ่านอื่นๆ จะไม่ถือเป็นที่สิ้นสุด

วิธีปกป้องกระบวนการและส่วนขยายเคอร์เนลบน 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 ซึ่งปิดช่องโหว่เหล่านั้นที่ระบบปฏิบัติการ "อ่อนแอ" เอง ในทางกลับกัน เราไม่ควรละเลยการเสริมความแข็งแกร่งด้านความปลอดภัยที่สามารถปรับปรุงได้ในฝั่งระบบปฏิบัติการ โดยเฉพาะอย่างยิ่งเมื่อการปิดช่องโหว่ดังกล่าวจะเพิ่มเสถียรภาพของเราเองในฐานะผลิตภัณฑ์ ช่องโหว่ดังกล่าวได้รับการรายงานไปยังทีมรักษาความปลอดภัยผลิตภัณฑ์ของ Apple และได้รับการแก้ไขแล้วใน macOS 10.14.5 (https://support.apple.com/en-gb/HT210119)

วิธีปกป้องกระบวนการและส่วนขยายเคอร์เนลบน macOS

ทั้งหมดนี้สามารถทำได้ก็ต่อเมื่อยูทิลิตี้ของคุณได้รับการติดตั้งอย่างเป็นทางการในเคอร์เนล นั่นคือไม่มีช่องโหว่ดังกล่าวสำหรับซอฟต์แวร์ภายนอกและซอฟต์แวร์ไม่พึงประสงค์ อย่างไรก็ตาม อย่างที่คุณเห็น แม้แต่การปกป้องโปรแกรมที่ถูกกฎหมาย เช่น แอนตี้ไวรัสและระบบสำรองข้อมูลก็ยังต้องดำเนินการเช่นกัน แต่ตอนนี้ผลิตภัณฑ์ Acronis ใหม่สำหรับ macOS จะมีการป้องกันเพิ่มเติมจากการขนออกจากระบบ

ที่มา: will.com

เพิ่มความคิดเห็น