如何在macOS上保护进程和内核扩展

哈Ha!今天,我想谈一谈如何保护进程免受macOS中入侵者的侵害。例如,它对于防病毒或备份系统很有用,尤其是考虑到在macOS下有多种“杀死”进程的方法。阅读有关它以及猫的保护方法的信息。

图片

杀死进程的经典方法


众所周知的“杀死”进程的方法是发送有关SIGKILL进程的信号。通过bash,您可以调用标准的“ kill -SIGKILL PID”或“ pkill -9 NAME”进行杀死。从UNIX开始,kill命令就为人所知,不仅在macOS上可用,而且在其他类似UNIX的系统上也可用。

与在类似UNIX的系统中一样,macOS允许您拦截除两个信号SIGKILL和SIGSTOP之外的任何到进程的信号。在本文中,SIGKILL信号将主要被视为导致进程终止的信号。

MacOS的细节


在macOS上,XNU内核中的kill系统调用将调用psignal函数(SIGKILL,...)。让我们尝试看看用户空间中还有哪些其他用户操作可以调用psignal函数。我们消除了内核内部机制中对psignal函数的调用(尽管它们可能很简单,让我们留给其他文章:)-签名验证,内存错误,退出/终止处理,违反文件保护等。

我们从函数和相应的系统调用Terminate_with_payload开始概述可以看出,除了经典的kill调用之外,还有另一种方法专用于macOS操作系统,而在BSD中找不到。两个系统调用的操作原理也很接近。它们是对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);
	}
...
}

发射


启动了在系统启动时创建守护程序并控制其生存期的标准方法。我将提请注意以下事实:源代码适用于macOS 10.10之前的旧版本的launchctl,下面给出了代码示例作为说明。现代的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,但它不仅可以发送带有SIGTERM的信号,而且还可以发送SIGKILL。

间接杀死-资源限制


在另一个process_policy系统调用中可以看到更有趣的情况。此系统调用的标准用法是应用程序资源限制,例如,对于索引器,处理器时间和内存配额受到限制,因此系统不会因文件缓存操作而明显变慢。从proc_apply_resource_actions函数可以看出,如果应用程序已达到资源限制,则SIGKILL信号将发送到该进程。

尽管此系统调用可能会杀死进程,但系统并未充分检查引起系统调用的进程的权限。实际上,存在检查,但是使用替代标志PROC_POLICY_ACTION_SET绕过此条件就足够了。

因此,如果您通过应用程序“限制” CPU使用量配额(例如,仅允许执行1 ns),则可以终止系统中的任何进程。因此,恶意软件可以杀死系统上的任何进程,包括防病毒进程。同样有趣的是,使用pid 1(launchctl)杀死进程时发生的效果-尝试处理SIGKILL信号时内核出现恐慌:)

图片

如何解决问题?


防止进程被杀死的最直接方法是替换系统调用表中的函数指针。不幸的是,由于许多原因,该方法并不简单:

首先,负责系统在内存中位置的符号不仅是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("%d\n", 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时,在调用终止函数时返回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标志,io_name_t描述);”来卸载类实例。您可以在调用“终止”命令之前返回false,直到应用程序死亡的用户空间消失为止,也就是说,没有对clientDied函数的调用。

文件保护


要保护文件,使用Kauth API就足够了,该API允许您限制对文件的访问。 Apple为开发人员提供了有关范围内各种事件的通知,操作KAUTH_VNODE_DELETE,KAUTH_VNODE_WRITE_DATA和KAUTH_VNODE_DELETE_CHILD对我们很重要。限制访问文件是最简单的方法-我们使用API​​“ vn_getpath”来获取文件的路径并比较路径前缀。请注意,为了优化带有文件的文件夹路径的重命名,系统不会授权访问每个文件,而只授权对已重命名的文件夹本身的访问。有必要比较父路径并为其限制KAUTH_VNODE_DELETE。

图片

这种方法的缺点可能是前缀数量增加,性能低下。为了使比较不等于O(prefix * length),其中prefix是前缀的数量,length是字符串的长度,可以使用由前缀构造的确定性有限状态机(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软件产品来确保的,该产品涵盖了操作系统本身“弱”的漏洞。另一方面,我们不应忽略可以在OS方面改进的那些安全方面,尤其是因为消除此类漏洞可以提高我们作为产品的稳定性。该漏洞由Apple产品安全团队报告,已在macOS 10.14.5(https://support.apple.com/zh-CN/HT210119)中修复。

图片

仅当您的实用程序已正式安装在内核中时,所有这些操作才能完成。也就是说,对于外部和不需要的软件,没有这样的漏洞。但是,正如您所看到的,即使要保护诸如防病毒和备份系统之类的合法程序,也必须努力工作。但是现在,用于macOS的新Acronis产品将具有防止从系统中卸载的附加保护。

All Articles