如何保護行程和內核擴展 macOS

大家好,Habr!今天我想和大家聊聊如何保護您的進程免受惡意攻擊。 macOS例如,這對於防毒或備份系統非常有用,尤其是在考慮到以下情況的情況下: macOS 有幾種方法可以「終止」進程。請閱讀以下內容,以了解更多資訊和保護方法。

如何保護行程和內核擴展 macOS

「殺死」進程的經典方法

終止進程的常用方法是向其發送 SIGKILL 訊號。在 bash 中,可以使用標準的 `kill -SIGKILL PID` 或 `pkill -9 NAME` 指令來終止進程。 `kill` 命令自 UNIX 時代就已存在,並且不僅適用於 bash。 macOS但也適用於其他類 UNIX 系統。

就像在類UNIX系統一樣, macOS 允許您攔截 SIGKILL 和 SIGSTOP 以外的任何進程訊號。本文將主要關注 SIGKILL 訊號,該訊號會導致進程終止。

特異性 macOS

В macOS XNU 核心中的 kill 系統呼叫會呼叫 psignal(SIGKILL,…) 函數。我們來看看使用者空間中還有哪些使用者操作會呼叫 psignal 函數。我們將過濾掉核心內部機制中對 psignal 函數的呼叫(儘管這些呼叫可能比較複雜,但我們將在另一篇文章中討論它們:))——例如簽名驗證、記憶體錯誤、退出/終止處理、檔案安全違規等等。

讓我們從函數和對應的系統呼叫開始回顧 終止_使用_有效載荷很明顯,除了經典的 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) {
		/*
		 * 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);
	}
...
}

啟動

在系統啟動時建立守護進程並控制其生命週期的標準方法是使用 launchd。請注意,原始程式碼適用於舊版的 launchctl,早於 launchctl 的早期版本。 macOS 10.10 版本中提供了程式碼範例,僅供參考。現代的 launchctl 透過 XPC 向 launchd 發送訊號,並且 launchctl 的邏輯已移至 launchd 中。

讓我們看看應用程式究竟是如何被停止的。在發送 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 的名稱如此,但它不僅可以發送帶有 SIGTERM 的 psignal,還可以發送 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。端點安全性 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策略,它提供了一種保護訊號的方法(策略proc_check_signal),但該API不受官方支援。

內核擴充保護

除了保護系統中的進程之外,還需要保護核心擴充(kext)本身。 macOS IOKit 為開發者提供了一個框架,方便他們輕鬆開發 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);
}

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);”卸載類別實例。您可以在“終止”命令呼叫時返回 false,直到用戶空間應用程式“死亡”,即沒有對“clientDied”函數的呼叫。

文件保護

要保護文件,只需使用 Kauth API,它允許您限制對文件的存取。 Apple 為開發者提供了各種事件範圍內的通知,對我們來說重要的操作是 KAUTH_VNODE_DELETE、KAUTH_VNODE_WRITE_DATA 和 KAUTH_VNODE_DELETE_CHILD。限製檔案存取的最簡單方法是透過路徑 - 我們使用「vn_getpath」API 來取得檔案路徑並比較路徑前綴。請注意,為了優化資料夾路徑的重命名,系統不會授權存取每個文件,而只授權存取重命名的資料夾本身。需要比較父路徑,並對其進行KAUTH_VNODE_DELETE限制。

如何保護行程和內核擴展 macOS

這種方法的缺點是隨著前綴數量的增加,性能可能會降低。為了使比較不等於 O(prefix*length),其中 prefix 是前綴的數量,length 是字串的長度,可以使用基於前綴構建的確定性有限自動機 (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

為具有 DDoS 保護、VPS VDS 服務器的站點購買可靠的主機 🔥 購買具備 DDoS 防護的可靠網站寄存服務,包括 VPS 和 VDS 伺服器 | ProHoster