Cách bảo vệ tiến trình và phần mở rộng kernel trên macOS

Xin chào, Habr! Hôm nay tôi muốn nói về cách bạn có thể bảo vệ các tiến trình khỏi các cuộc tấn công của những kẻ tấn công trong macOS. Ví dụ: điều này hữu ích cho hệ thống chống vi-rút hoặc sao lưu, đặc biệt vì trong macOS, có một số cách để "tiêu diệt" một quy trình. Đọc về điều này và các phương pháp bảo vệ dưới vết cắt.

Cách bảo vệ tiến trình và phần mở rộng kernel trên macOS

Cách cổ điển để “giết chết” một quy trình

Một cách phổ biến để “hủy” một tiến trình là gửi tín hiệu SIGKILL đến tiến trình đó. Thông qua bash, bạn có thể gọi tiêu chuẩn “kill -SIGKILL PID” hoặc “pkill -9 NAME” để tiêu diệt. Lệnh “kill” đã được biết đến từ thời UNIX và không chỉ có sẵn trên macOS mà còn trên các hệ thống giống UNIX khác.

Cũng giống như trong các hệ thống giống UNIX, macOS cho phép bạn chặn bất kỳ tín hiệu nào tới một quy trình ngoại trừ hai tín hiệu - SIGKILL và SIGSTOP. Bài viết này sẽ chủ yếu tập trung vào tín hiệu SIGKILL như một tín hiệu khiến quá trình bị hủy.

chi tiết cụ thể về macOS

Trên macOS, lệnh gọi hệ thống kill trong nhân XNU gọi hàm psignal(SIGKILL,...). Hãy thử xem những hành động nào khác của người dùng trong không gian người dùng có thể được gọi bằng hàm psignal. Hãy loại bỏ các lệnh gọi đến hàm psignal trong các cơ chế bên trong của kernel (mặc dù chúng có thể không tầm thường nhưng chúng tôi sẽ dành chúng cho một bài viết khác 🙂 - xác minh chữ ký, lỗi bộ nhớ, xử lý thoát/chấm dứt, vi phạm bảo vệ tệp, v.v. .

Hãy bắt đầu đánh giá với chức năng và lệnh gọi hệ thống tương ứng chấm dứt_với_payload. Có thể thấy, ngoài kill call cổ điển, còn có một cách tiếp cận khác dành riêng cho hệ điều hành macOS và không có trong BSD. Nguyên tắc hoạt động của cả hai cuộc gọi hệ thống cũng tương tự nhau. Chúng là các lệnh gọi trực tiếp tới hàm psignal của kernel. Cũng lưu ý rằng trước khi hủy một tiến trình, việc kiểm tra “cansignal” sẽ được thực hiện - liệu tiến trình đó có thể gửi tín hiệu đến một tiến trình khác hay không; ví dụ: hệ thống không cho phép bất kỳ ứng dụng nào hủy các tiến trình hệ thống.

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

khởi động

Cách tiêu chuẩn để tạo daemon khi khởi động hệ thống và kiểm soát thời gian tồn tại của chúng là launchd. Xin lưu ý rằng các nguồn dành cho phiên bản cũ của launchctl lên đến macOS 10.10, các ví dụ về mã được cung cấp nhằm mục đích minh họa. Launchctl hiện đại gửi tín hiệu launchd qua XPC, logic launchctl đã được chuyển sang nó.

Hãy xem chính xác các ứng dụng bị dừng như thế nào. Trước khi gửi tín hiệu SIGTERM, ứng dụng sẽ được cố gắng dừng bằng lệnh gọi hệ thống “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));
		} 
...
<>

Dưới mui xe, proc_terminate, mặc dù tên của nó, có thể gửi không chỉ psignal với SIGTERM mà còn cả SIGKILL.

Giết gián tiếp - Giới hạn tài nguyên

Một trường hợp thú vị hơn có thể được thấy trong một cuộc gọi hệ thống khác quy trình_chính sách. Cách sử dụng phổ biến của lệnh gọi hệ thống này là để giới hạn tài nguyên ứng dụng, chẳng hạn như để bộ lập chỉ mục giới hạn thời gian CPU và hạn mức bộ nhớ để hệ thống không bị chậm đáng kể do các hoạt động lưu vào bộ nhớ đệm tệp. Nếu một ứng dụng đã đạt đến giới hạn tài nguyên, như có thể thấy từ hàm proc_apply_resource_actions, tín hiệu SIGKILL sẽ được gửi đến quy trình.

Mặc dù lệnh gọi hệ thống này có khả năng giết chết một tiến trình nhưng hệ thống đã không kiểm tra đầy đủ các quyền của tiến trình gọi lệnh gọi hệ thống. Trên thực tế đang kiểm tra tồn tại, nhưng chỉ cần sử dụng cờ thay thế PROC_POLICY_ACTION_SET để bỏ qua điều kiện này là đủ.

Do đó, nếu bạn “giới hạn” hạn ngạch sử dụng CPU của ứng dụng (ví dụ: chỉ cho phép chạy 1 ns), thì bạn có thể tắt bất kỳ tiến trình nào trong hệ thống. Do đó, phần mềm độc hại có thể giết chết bất kỳ tiến trình nào trên hệ thống, bao gồm cả tiến trình chống vi-rút. Điều thú vị nữa là hiệu ứng xảy ra khi hủy một tiến trình với pid 1 (launchctl) - kernel hoảng loạn khi cố xử lý tín hiệu SIGKILL :)

Cách bảo vệ tiến trình và phần mở rộng kernel trên macOS

Làm thế nào để giải quyết vấn đề?

Cách đơn giản nhất để ngăn chặn một tiến trình bị giết là thay thế con trỏ hàm trong bảng gọi hệ thống. Thật không may, phương pháp này không tầm thường vì nhiều lý do.

Đầu tiên, ký hiệu kiểm soát vị trí bộ nhớ của sysent không chỉ riêng tư đối với ký hiệu hạt nhân XNU mà còn không thể tìm thấy trong các ký hiệu hạt nhân. Bạn sẽ phải sử dụng các phương pháp tìm kiếm heuristic, chẳng hạn như phân tách động hàm và tìm kiếm con trỏ trong đó.

Thứ hai, cấu trúc của các mục trong bảng phụ thuộc vào các cờ mà kernel được biên dịch. Nếu cờ CONFIG_REQUIRES_U32_MUNGING được khai báo, kích thước của cấu trúc sẽ thay đổi - một trường bổ sung sẽ được thêm vào sy_arg_munge32. Cần phải thực hiện kiểm tra bổ sung để xác định xem hạt nhân đã được biên dịch bằng cờ nào, hoặc cách khác, kiểm tra các con trỏ hàm so với các cờ đã biết.

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
                                         */
};

May mắn thay, trong các phiên bản macOS hiện đại, Apple cung cấp một API mới để làm việc với các quy trình. API bảo mật điểm cuối cho phép khách hàng ủy quyền nhiều yêu cầu cho các quy trình khác. Do đó, bạn có thể chặn bất kỳ tín hiệu nào tới các quy trình, bao gồm cả tín hiệu SIGKILL, bằng cách sử dụng API nêu trên.

#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;
}

Tương tự, Chính sách MAC có thể được đăng ký trong kernel, cung cấp phương thức bảo vệ tín hiệu (chính sách proc_check_signal), nhưng API không được hỗ trợ chính thức.

Bảo vệ phần mở rộng hạt nhân

Ngoài việc bảo vệ các tiến trình trong hệ thống, việc bảo vệ chính phần mở rộng kernel (kext) cũng rất cần thiết. macOS cung cấp một khuôn khổ để các nhà phát triển dễ dàng phát triển trình điều khiển thiết bị IOKit. Ngoài việc cung cấp các công cụ để làm việc với các thiết bị, IOKit còn cung cấp các phương pháp xếp chồng trình điều khiển bằng cách sử dụng các phiên bản của lớp C++. Một ứng dụng trong không gian người dùng sẽ có thể “tìm” một phiên bản đã đăng ký của lớp để thiết lập mối quan hệ kernel-userspace.

Để phát hiện số lượng phiên bản lớp trong hệ thống, có tiện ích ioclasscount.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Bất kỳ phần mở rộng kernel nào muốn đăng ký với ngăn xếp trình điều khiển đều phải khai báo một lớp kế thừa từ IOService, ví dụ như my_kext_ioservice trong trường hợp này. Việc kết nối các ứng dụng người dùng sẽ tạo ra một phiên bản mới của lớp kế thừa từ IOUserClient, trong ví dụ my_kext_iouserclient.

Khi cố gắng dỡ trình điều khiển khỏi hệ thống (lệnh kextunload), hàm ảo “bool terminating(IOOptionBits options)” sẽ được gọi. Việc trả về false trong lệnh gọi chấm dứt khi cố gắng dỡ tải để tắt kextunload là đủ.

bool Kext::terminate(IOOptionBits options)
{

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

  return super::terminate(options);
}

Cờ IsUnloadAllowed có thể được IOUserClient đặt khi tải. Khi có giới hạn tải xuống, lệnh kextunload sẽ trả về kết quả đầu ra sau:

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.

Việc bảo vệ tương tự phải được thực hiện đối với IOUserClient. Các phiên bản của lớp có thể được tải xuống bằng cách sử dụng hàm không gian người dùng IOKitLib “IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t description);”. Bạn có thể trả về false khi gọi lệnh “chấm dứt” cho đến khi ứng dụng không gian người dùng “chết”, tức là hàm “clientDied” không được gọi.

bảo vệ tập tin

Để bảo vệ tệp, việc sử dụng API Kauth là đủ, cho phép bạn hạn chế quyền truy cập vào tệp. Apple cung cấp cho các nhà phát triển thông báo về các sự kiện khác nhau trong phạm vi; đối với chúng tôi, các hoạt động KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA và KAUTH_VNODE_DELETE_CHILD rất quan trọng. Cách dễ nhất để hạn chế quyền truy cập vào tệp là bằng đường dẫn - chúng tôi sử dụng API “vn_getpath” để lấy đường dẫn đến tệp và so sánh tiền tố đường dẫn. Lưu ý rằng để tối ưu hóa việc đổi tên đường dẫn thư mục tệp, hệ thống không cấp quyền truy cập vào từng tệp mà chỉ cấp quyền truy cập vào chính thư mục đã được đổi tên. Cần phải so sánh đường dẫn gốc và hạn chế KAUTH_VNODE_DELETE cho nó.

Cách bảo vệ tiến trình và phần mở rộng kernel trên macOS

Nhược điểm của phương pháp này có thể là hiệu suất thấp khi số lượng tiền tố tăng lên. Để đảm bảo so sánh không bằng O(tiền tố*độ dài), trong đó tiền tố là số lượng tiền tố, độ dài là độ dài của chuỗi, bạn có thể sử dụng máy tự động hữu hạn xác định (DFA) được tạo bởi tiền tố.

Hãy xem xét một phương pháp xây dựng DFA cho một tập hợp tiền tố nhất định. Chúng tôi khởi tạo các con trỏ ở đầu mỗi tiền tố. Nếu tất cả các con trỏ trỏ đến cùng một ký tự thì hãy tăng mỗi con trỏ lên một ký tự và nhớ rằng độ dài của cùng một dòng sẽ lớn hơn một ký tự. Nếu có hai con trỏ có ký hiệu khác nhau, hãy chia con trỏ thành các nhóm theo ký hiệu mà chúng trỏ tới và lặp lại thuật toán cho mỗi nhóm.

Trong trường hợp đầu tiên (tất cả các ký tự bên dưới con trỏ đều giống nhau), chúng ta nhận được trạng thái DFA chỉ có một lần chuyển đổi dọc theo cùng một dòng. Trong trường hợp thứ hai, chúng ta nhận được một bảng chuyển đổi có kích thước 256 (số ký tự và số nhóm tối đa) sang các trạng thái tiếp theo thu được bằng cách gọi đệ quy hàm.

Hãy xem một ví dụ. Đối với một tập hợp các tiền tố (“/foo/bar/tmp/”, “/var/db/foo/”, “/foo/bar/aba/”, “foo/bar/aac/”), bạn có thể nhận được những thông tin sau DFA. Hình này chỉ hiển thị các chuyển đổi dẫn đến các trạng thái khác; các chuyển đổi khác sẽ không phải là cuối cùng.

Cách bảo vệ tiến trình và phần mở rộng kernel trên macOS

Khi đi qua các bang DKA có thể xảy ra 3 trường hợp.

  1. Đã đạt trạng thái cuối cùng - đường dẫn được bảo vệ, chúng ta giới hạn các thao tác KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA và KAUTH_VNODE_DELETE_CHILD
  2. Trạng thái cuối cùng chưa đạt được, nhưng đường dẫn đã “kết thúc” (đã đạt đến điểm kết thúc null) - đường dẫn là cha mẹ, cần giới hạn KAUTH_VNODE_DELETE. Lưu ý nếu vnode là một thư mục thì bạn cần thêm dấu '/' vào cuối, nếu không nó có thể giới hạn nó trong tập tin “/foor/bar/t”, điều này không chính xác.
  3. Trạng thái cuối cùng chưa đạt được, con đường không kết thúc. Không có tiền tố nào khớp với tiền tố này, chúng tôi không đưa ra các hạn chế.

Kết luận

Mục tiêu của các giải pháp bảo mật đang được phát triển là tăng mức độ bảo mật của người dùng và dữ liệu của họ. Một mặt, mục tiêu này đạt được nhờ việc phát triển sản phẩm phần mềm Acronis, giúp khắc phục những lỗ hổng mà bản thân hệ điều hành “yếu”. Mặt khác, chúng ta không nên bỏ qua việc tăng cường các khía cạnh bảo mật có thể được cải thiện về phía hệ điều hành, đặc biệt vì việc đóng các lỗ hổng như vậy sẽ làm tăng tính ổn định của chính chúng tôi với tư cách là một sản phẩm. Lỗ hổng này đã được báo cáo cho Nhóm bảo mật sản phẩm Apple và đã được sửa trong macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

Cách bảo vệ tiến trình và phần mở rộng kernel trên macOS

Tất cả điều này chỉ có thể được thực hiện nếu tiện ích của bạn đã được cài đặt chính thức vào kernel. Nghĩa là không có sơ hở nào như vậy đối với phần mềm bên ngoài và phần mềm không mong muốn. Tuy nhiên, như bạn có thể thấy, ngay cả việc bảo vệ các chương trình hợp pháp như hệ thống chống vi-rút và sao lưu cũng cần phải nỗ lực. Nhưng giờ đây, các sản phẩm Acronis mới dành cho macOS sẽ có thêm khả năng bảo vệ chống lại việc tải xuống khỏi hệ thống.

Nguồn: www.habr.com

Thêm một lời nhận xét