你好,哈布尔! 今天我想谈谈如何保护 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 函数的调用(尽管它们可能很重要,我们将把它们留到另一篇文章中 🙂 - 签名验证、内存错误、退出/终止处理、文件保护违规等。
我们先从函数和对应的系统调用开始回顾
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使用配额(例如,只允许运行1纳秒),那么你可以杀死系统中的任何进程。 因此,恶意软件可以杀死系统上的任何进程,包括防病毒进程。 同样有趣的是当杀死 pid 1 (launchctl) 的进程时发生的效果 - 尝试处理 SIGKILL 信号时内核发生恐慌:)
如何解决问题?
防止进程被杀死的最直接的方法是替换系统调用表中的函数指针。 不幸的是,由于多种原因,这种方法并不简单。
首先,控制sysent内存位置的符号不仅是XNU内核符号私有的,而且在内核符号中也找不到。 您必须使用启发式搜索方法,例如动态反汇编函数并在其中搜索指针。
其次,表中条目的结构取决于编译内核时使用的标志。 如果声明了 CONFIG_REQUIRES_U32_MUNGING 标志,结构的大小将被更改 - 将添加一个附加字段
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。
这种方法的缺点可能是随着前缀数量的增加而导致性能低下。 为了确保比较不等于O(prefix*length),其中prefix是前缀的数量,length是字符串的长度,可以使用由前缀构建的确定性有限自动机(DFA)。
让我们考虑一种为给定的前缀集构建 DFA 的方法。 我们在每个前缀的开头初始化游标。 如果所有光标都指向同一个字符,则将每个光标增加一个字符,并记住同一行的长度要大一。 如果有两个带有不同符号的光标,则根据它们指向的符号将光标分组,并对每组重复该算法。
在第一种情况下(光标下的所有字符都相同),我们得到的 DFA 状态在同一行上只有一个转换。 在第二种情况下,我们得到一个大小为 256(字符数和最大组数)到通过递归调用函数获得的后续状态的转换表。
让我们看一个例子。 对于一组前缀(“/foo/bar/tmp/”、“/var/db/foo/”、“/foo/bar/aba/”、“foo/bar/aac/”),您可以获得以下内容DFA。 该图仅显示导致其他状态的转换;其他转换不会是最终的。
当经过DKA状态时,可能有3种情况。
- 已达到最终状态 - 路径受到保护,我们限制操作 KAUTH_VNODE_DELETE、KAUTH_VNODE_WRITE_DATA 和 KAUTH_VNODE_DELETE_CHILD
- 未达到最终状态,但路径“结束”(到达空终止符)-路径是父路径,有必要限制 KAUTH_VNODE_DELETE。 注意,如果vnode是文件夹,则需要在末尾添加'/',否则可能将其限制为文件“/foor/bar/t”,这是不正确的。
- 最终的状态还没有达到,道路还没有结束。 没有一个前缀与此匹配,我们不引入限制。
结论
正在开发的安全解决方案的目标是提高用户及其数据的安全级别。 一方面,这一目标是通过Acronis软件产品的开发来实现的,该产品封闭了操作系统本身“薄弱”的那些漏洞。 另一方面,我们不应该忽视加强那些可以在操作系统方面改进的安全方面,特别是因为关闭这些漏洞可以提高我们自身作为产品的稳定性。 该漏洞已报告给 Apple 产品安全团队,并已在 macOS 10.14.5 中修复 (https://support.apple.com/en-gb/HT210119)。
只有当您的实用程序已正式安装到内核中时,所有这一切才能完成。 也就是说,不存在此类外部软件和不需要的软件的漏洞。 然而,正如您所看到的,即使保护防病毒和备份系统等合法程序也需要付出努力。 但现在适用于 macOS 的新 Acronis 产品将提供额外的保护,防止从系统卸载。
来源: habr.com