如何保護 macOS 上的進程和核心擴展

你好,哈布爾! 今天我想談談如何保護 macOS 中的進程免受攻擊者的攻擊。 例如,這對於防毒或備份系統很有用,特別是因為在 macOS 下有多種方法可以「殺死」進程。 了解這一點以及切割下的保護方法。

如何保護 macOS 上的進程和核心擴展

「殺死」進程的經典方法

「殺死」進程的一種眾所周知的方法是向進程發送 SIGKILL 訊號。 透過bash你可以呼叫標準的「kill -SIGKILL PID」或「pkill -9 NAME」來殺死。 「kill」指令自 UNIX 時代以來就已為人所知,不僅可在 macOS 上使用,還可在其他類 UNIX 系統上使用。

就像在類 UNIX 系統中一樣,macOS 允許您攔截進程的任何訊號,除了兩個訊號 - SIGKILL 和 SIGSTOP。 本文將主要關注 SIGKILL 訊號作為導致進程終止的訊號。

macOS 細節

在 macOS 上,XNU 核心中的 Kill 系統呼叫呼叫 psignal(SIGKILL,...) 函數。 讓我們試著看看 psignal 函數可以呼叫使用者空間中的哪些其他使用者操作。 讓我們在內核的內部機制中清除對 psignal 函數的呼叫(儘管它們可能很重要,但我們將把它們留到另一篇文章中 🙂 - 簽名驗證、記憶體錯誤、退出/終止處理、文件保護違規, ETC。

我們先從函數和對應的系統呼叫開始回顧 終止有效負載。 可以看出,除了經典的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。 請注意,原始程式碼適用於 macOS 10.10 之前的舊版本 launchctl,提供程式碼範例僅供說明之用。 現代launchctl透過XPC發送launchd訊號,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,儘管它的名字如此,不僅可以發送帶有 SIGTERM 的 psignal,還可以發送 SIGKILL。

間接殺死 - 資源限制

更有趣的情況可以在另一個系統呼叫中看到 流程策略。 此系統呼叫的常見用途是限制應用程式資源,例如索引器限制 CPU 時間和記憶體配額,以便系統不會因檔案快取活動而顯著減慢。 如果應用程式已達到其資源限制(從 proc_apply_resource_actions 函數可以看出),則會向進程發送 SIGKILL 訊號。

儘管此系統呼叫可能會終止進程,但係統沒有充分檢查呼叫該系統呼叫的進程的權限。 實際檢查 存在,但使用替代標誌 PROC_POLICY_ACTION_SET 就足以繞過此條件。

因此,如果你「限制」應用程式的CPU使用配額(例如,只允許運行1奈秒),那麼你可以殺死系統中的任何進程。 因此,惡意軟體可以殺死系統上的任何進程,包括防毒進程。 同樣有趣的是當殺死 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 允許客戶端向其他進程授權許多請求。 因此,您可以使用上述 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,它提供了訊號保護方法(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 flag, io_name_t description);」卸載類別的實例。 呼叫“terminate”命令時可以返回 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軟體產品的開發來實現的,該產品封閉了作業系統本身「薄弱」的那些漏洞。 另一方面,我們不應該忽視加強那些可以在作業系統方面改進的安全方面,特別是因為關閉這些漏洞可以提高我們自身作為產品的穩定性。 該漏洞已報告給 Apple 產品安全團隊,並已在 macOS 10.14.5 中修復 (https://support.apple.com/en-gb/HT210119)。

如何保護 macOS 上的進程和核心擴展

只有當您的實用程式已正式安裝到核心中時,所有這些才能完成。 也就是說,不存在此類外部軟體和不必要的軟體的漏洞。 然而,正如您所看到的,即使保護防毒和備份系統等合法程序也需要付出努力。 但現在適用於 macOS 的新 Acronis 產品將提供額外的保護,防止從系統卸載。

來源: www.habr.com

添加評論