- 参考:
- Endpoint Security | Apple Developer Documentation
- macOS - Endpoint Security 系列头文件注释
- 说明:
- 还请读者阅读“macOS - Endpoint Security 系列头文件注释”,本文无法替代该“注释”。
- 欢迎转载,转载时请注明出处。
- 文中不当之处,欢迎大家指正。
目录
一、概念介绍
- 说明:
- 本文中的概念和相关名词为作者个人的理解和提炼,非官方说明,最为准确和贴切的概念及其说明,请读者阅读官网文档和相关源码头文件的注释。
- 本节中被加粗的名词,还请读者在阅读本文是稍加注意并理解其含义,本文会严格引用这些名词来表达相关的概念。
1.1 框架简介
macOS的 Endpoint Security Framework 能够让开发者监控系统中的事件以检测恶意活动的存在。这些事件包括诸如文件创建、文件更名、目录遍历、进程创建、会话登录等。
ES框架的使用非常简单。首先,调用es_new_client()创建一个ES客户端,同时定义好该ES客户端的事件处理函数。其次,调用es_subscribe()订阅你感兴趣的事件类型。随后,当你订阅的事件发生时,系统会调用你定义好的事件处理函数,你的事件处理函数即可处理各类事件。
在实践中,你可以将ES框架集成在你自己的进程中。也可以利用System Extension机制,另起进程,使其被系统托管,并通过IPC方式与你的主进程传递数据。
1.2 事件分类
ES框架的事件分为两类。ES框架根据事件的动作类型,将事件大体上分为了AUTH事件和NOTIFY事件。
AUTH事件为授权类型的事件。在你订阅的事件发生时,系统将调用你的事件处理函数。你的事件处理函数需要决策,该事件是否可以执行。在你的决策期间,将阻塞事件的进行。因此,你需要在一定时间内处理AUTH事件,越快越好。如果超过事件处理期限,系统将终止你的进程。(在macOS-13.5中,测试了部分的AUTH事件,其deadline在32s左右)
NOTIFY事件为通知类型的事件。在你订阅的事件发生时,系统将调用你的事件处理函数。你的事件处理函数无法决定该事件是否可以执行,只能感知到与该事件相关的进程、文件等信息。在你的事件处理函数函数执行期间,不会阻塞事件的进行。当然,你仍需要快速的处理NOTIFY事件,越快越好,否则,将会出现丢事件的情况。(每个ES客户端都有一个事件队列,事件处理函数过慢,则会导致事件队列满。在macOS-13.5中,测试发现事件队列的大小在3074左右)
1.3 事件降噪
ES框架能够让你过滤掉不需要的事件数据。例如,你订阅了OPEN事件,那么系统上所进程产生的OPEN事件,都会给到你的ES客户端。在实际中,这大概率不是你所期待的。对于那些系统进程、系统APP所产生的OPEN事件,你或许并不关心。ES框架提供了es_mute系列的API来支持你过滤掉不需要的事件数据,“mute”翻译为“消音”,但在本文中,我会将这样的行为称作“事件降噪”。
ES框架的es_mute系列API支持三种不同的“事件降噪”方式。能够让你根据“进程ID”、“进程文件路径”、“目标文件路径”来进行事件过滤。你可以仅用其中一种方式,也可以组合使用来达到更有效的过滤。(当然,你也要承担组合使用而导致程序逻辑复杂的风险,后面会阐述)
“进程ID”:指的是进程的Audit Token ID,这个ID的定义是一个无符号整型的数组,数组大小为8,你可以从中获取到进程的PID。
“进程路径”:指的是进程的可执行文件路径。
“目标路径”:你可以理解为进程的操作对象。需要明确的是,在不同的事件类型中,“目标路径”的定义是不同的,“目标路径”的数量也是不同的。例如,OPEN事件的“目标路径”是被打开的文件(一个“目标路径”),而RENAME事件的“目标路径”是 重命名前的路径 和 重命名后的路径(两个“目标路径”)。
例如,你的ES客户端订阅了OPEN事件,但“/usr/libexec/”目录下所有可执行文件对应的进程所产生的OPEN事件你都不关心。那么,你可以调用es_mute系列API来处置这个路径。API会将“/usr/libexec/”加入到一个集合中。当OPEN事件产生时,ES框架会将这次事件对应的“进程路径”在这个集合中查询。如果查询成功,那么这次OPEN事件将不会给到你的ES客户端,查询失败,则OPEN事件会给到你的ES客户端。
我将这样的集合称为“事件降噪集合”。而这样的“降噪集合”有三个,分别是“进程ID降噪集合”、“进程路径降噪集合”、“目标路径降噪集合”。这些集合恰如其名,分别主要根据“进程ID”、“进程路径”、“目标路径”来进行过滤匹配。当然,也还有其他的匹配条件——事件类型、匹配方式。
1.4 降噪集合
“事件降噪集合”中存储的是一个个的“事件降噪元组”。在你使用es_mute系列API来处置你的路径时,ES框架会根据你的API类型、API参数来生成对应的“事件降噪元组”,并将其放入合适的“事件降噪集合”中。
“进程ID降噪集合”的元组类似于:{ “进程ID”, “事件类型” }。例如,你使用es_mute_process()来处置一个“101”的“进程ID”(不要忘了“进程ID”是指进程的Audit Token ID,它绝对不是“101”这样的),那么对应的元组为:{ “101”,“*” },语义为:“进程ID”为“101”的进程,其产生的所有事件,都将被过滤。那么,“101”进程所产生的OPEN事件、RENAME事件等等,都不会给到你的ES客户端了。
“进程路径降噪集合”和“目标路径降噪集合”的元组类似于:{ “路径匹配类型”,“路径”, “事件类型” }。“路径匹配类型”指的是“es_mute_path_type_t”枚举类型(参见:“2.1 类型” -> “es_mute_path_type_t”)。
例如,你使用es_mute_path_events()处置了“/usr/libexec/”,并指定“路径匹配类型”为“ES_MUTE_PATH_TYPE_PREFIX”,指定事件为“ES_EVENT_TYPE_NOTIFY_LINK”,那么对应的元组为:{ “ES_MUTE_PATH_TYPE_PREFIX”,“/usr/libexec/”,“ES_EVENT_TYPE_NOTIFY_LINK” },语义为:可执行文件路径前缀包含“/usr/libexec”的进程,它们所产生的LINK事件,都将被过滤。
至此,你成功屏蔽了这些进程的LINK事件,但是,如果你的ES客户端同时也订阅了UNLINK事件,那么,这些进程产生的UNLINK事件还是会给到你的ES客户端。
添加与删除总是成对的。是的,你可以从“事件降噪集合”中删除相应“事件降噪元组”。ES框架提供了es_unmute系列API来做这样的事。不过es_unmute系列API在使用时,有一些点是需要注意的,参见:“2.2.3 事件降噪” -> “es_unmute_path”
默认的“事件降噪元组”。当你调用es_new_client()创建好一个ES客户端后,一些系统路径会被加入到你ES客户端的“事件降噪集合”中,这些路径对应的元组,便是默认的“事件降噪元组”。(测试时发现,这些默认“事件降噪元组”中的事件类型,全是AUTH类型的——测试中没发现NOTIFY类型,不能保证一定没有NOTIFY类型。)
默认“事件降噪元组”的存在,一方面是为了防止ES客户端之间因AUTH事件而陷入授权循环(死锁),另一方面是为了避免因ES客户端处理AUTH事件不及时而引发系统“看门狗”的超时恐慌。(参见: “3.1 默认的“降噪元组”)
1.5 事件聚焦
ES框架能够让你只得到感兴趣的事件数据。看完“事件降噪”后,是否觉得意犹未尽?是的,“事件降噪”可以帮你过滤掉不需要的事件数据,但是,在实际中,即使过滤掉一些事件数据后,你的ES客户端接到的事件数据量还是很大。你完全可以省去过滤的步骤,直接只获取你感兴趣的事件数据。ES框架提供了es_invert_muting() API来支持你实现这样的需求。“invert”是“翻转”的意思,在本文中,我会将这样的行为称作“事件聚焦”。
关于“事件降噪集合”的状态。es_invert_muting()确实只对“事件降噪集合”的状态做了一个“翻转”的动作。 “事件降噪集合”的默认状态是“降噪态”,即:事件数据命中了集合中的元组后,就被过滤掉。“事件降噪集合”的另外一个状态是“聚焦态”,即:只有事件数据命中了集合中的元组后,才会被上报。es_invert_muting()可以将“事件降噪集合”的状态在“降噪态”与“聚焦态”之间切换。
es_invert_muting()只是改变“事件降噪集合”的状态。当你第一次调用es_invert_muting()将某个“事件降噪集合”从“降噪态”变为“聚焦态”后,第二次调用es_invert_muting()改变该集合的状态时,该集合的状态将从“聚焦态”改为“降噪态”。所以,在实践中,调用es_invert_muting()处理某个“事件降噪集合”后,你都应该要对该集合的状态了然于心。
注意, “事件降噪集合”一词是“进程ID降噪集合”、“进程路径降噪集合”、“目标路径降噪集合”的代词。所以,es_invert_muting()可以分别改变这三个集合的状态,具体是改变哪一个“集合”,由es_invert_muting()的第2个参数来指定,我将该参数称为“事件聚焦类型”——指的是“es_mute_inversion_type_t”枚举类型,参见:“2.1 类型 -> es_mute_inversion_type_t”。这样,结合“事件降噪”,你的事件过滤组合选项又变多了。你可以让“进程ID降噪集合”处于“降噪态”,而让“进程路径降噪集合”、“目标路径降噪集合”都处于“聚焦态”。
还记得前面在“事件降噪”中提到的风险吗?你真的能理清组合使用三个“降噪集合”和“事件降噪”以及“事件聚焦”时的过滤链吗?没关系,在“编程参考”中,我会给你简明的代码示例,在“补充说明”中,我会给你清晰的过滤流程。
二、编程参考
- 说明:
- 以下所有“示例”中的代码,编程语言均为Objective-C
- 以下所有API的“声明”中,为了简洁,去掉了API 指针参数的“_Nullable”修饰关键字。
- 以下所有API,如未特殊说明,均为macOS-10.15引入的。
- 说明:
- 使用ES框架需要包含库文件:libEndpointSecurity.tbd
- 使用ES框架需要包含头文件:<EndpointSecurity/EndpointSecurity.h>
2.1 类型
- 说明:
- 本节仅列举了某些关键的和本文会涉及的数据类型,更多的数据类型,请读者参考ES系列头文件。
es_return_t
- 简述:
- ES API的通用返回值类型。
- 定义:
typedef enum { ES_RETURN_SUCCESS, // 成功 ES_RETURN_ERROR // 失败 } es_return_t;
es_new_client_result_t
- 简述:
- 代理创建接口:es_new_client() 的返回值类型。
- 定义:
typedef enum { ES_NEW_CLIENT_RESULT_SUCCESS, // 调用成功 ES_NEW_CLIENT_RESULT_ERR_INVALID_ARGUMENT, // 参数错误 ES_NEW_CLIENT_RESULT_ERR_INTERNAL, // 无法连接到ES子系统或其他错误。 ES_NEW_CLIENT_RESULT_ERR_NOT_ENTITLED, // 程序entitlement配置错误 ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED, // 程序缺少完全磁盘访问权限 ES_NEW_CLIENT_RESULT_ERR_NOT_PRIVILEGED, // 程序以非root权限运行 ES_NEW_CLIENT_RESULT_ERR_TOO_MANY_CLIENTS // 程序触达ES客户端上限,无法再创建ES客户端 } es_new_client_result_t;
-
说明:
-
遇到NOT_ENTITLED时,请检查Xcode工程配置的“Signing&Capabilities”中,是否添加了“Endpoint Security”。同时检查程序对应的entitlements文件中是否有“endpoint-security”相关的配置项存在。
-
遇到NOT_PERMITTED时,需要在“设置->隐式与安全性->完全磁盘访问权限”中,添加上你的程序。
-
遇到NOT_PRIVILEGED时,请将你的程序以root用户启动。
-
es_mute_path_type_t
- 简述:
- 对应本文中“路径匹配类型”一词。在es_mute_path系列API中,决定ES框架在“降噪”时匹配路径的方式。
- 注意,不要纠结以下“说明”、“注意”、“示例”中的内容,可以在看“2.2 接口”的过程中来回顾它们。
- 定义:
typedef enum { ES_MUTE_PATH_TYPE_PREFIX, // 根据“进程路径”执行“前缀匹配” ES_MUTE_PATH_TYPE_LITERAL, // 根据“进程路径”执行“字面匹配” ES_MUTE_PATH_TYPE_TARGET_PREFIX, // 根据“目标路径”执行“前缀匹配” ES_MUTE_PATH_TYPE_TARGET_LITERAL, // 根据“目标路径”执行“字面匹配” } es_mute_path_type_t;
- 说明:
- 你肯定也看出来了,es_mute_path_type_t 不仅决定了路径的匹配方式,还决定了路径的匹配类型。其决定了路径匹配方式是按“前缀配置”还是“字面匹配”,同时也决定了路径匹配类型是按“进程路径”还是“目标路径”。
- “前缀匹配”可以理解为“包含”,而“字面匹配”则是“相等”。在使用“前缀匹配”时,注意路径的特性,即以‘/’分割——我怀疑其内部实现采用的是“字典树”。
- 在使用ES_MUTE_PATH_TYPE_TARGET_PREFIX、ES_MUTE_PATH_TYPE_TARGET_LITERAL时,需要注意,并不是所有事件都支持这两个匹配类型。而且,“目标路径”的概念,不同的事件,其含义是不同的。参见:“3.1 事件的“目标路径”。
- 注意:
- 有的事件其“目标路径”只有一个,但有的事件,其“目标路径”有多个。对于那些有多个“目标路径”的事件,在“事件降噪”和“事件聚焦”时,其表现行为是不同的。
- 在不考虑“进程ID”和“进程路径”的情况下,对于具有多个"目标路径"的事件类型,在“事件降噪”时,需要所有“目标路径”同时匹配,才会被过滤。
- 在不考虑“进程ID”和“进程路径”的情况下,对于具有多个"目标路径"的事件类型,在“事件聚焦”时,只要其中一个“目标路径”匹配,就会被上报。
- 参见:“3.3 事件的“过滤流程””。
- 示例:
- 伪代码:
rename("/tmp/a.log", "/tmp/b.log"); es_mute_path(es_client, "/tmp/a.log", ES_MUTE_PATH_TYPE_TARGET_LITERAL); es_mute_path(es_client, "/tmp/b.log", ES_MUTE_PATH_TYPE_TARGET_LITERAL); // es_invert_muting(es_client, ES_MUTE_INVERSION_TYPE_TARGET_PATH);
-
在“事件降噪”时,只有当 重命名前的路径 和 重命名后的路径 全部路径命中所设置的“目标路径”,才会被过滤。 示例伪代码中,仅有如果仅第3行,而注释掉第4行,则RENAME事件还是会上报。必须加上第4行,才能将该RENAME事件过滤掉。
-
在“事件聚焦”时,只要当 重命名前的路径 或 重命名后的路径 任一路径命中所设置的“目标路径”,就会被上报。 示例伪代码中,取消第6行的“事件聚焦”的注释后,仅有第3行,或仅有第4行,均会上报该RENAME事件。
- 伪代码:
es_mute_inversion_type_t
- 简述:
- 对应本文中的“事件聚焦类型”一词。在调用es_invert_muting()时,决定该函数操作的“事件降噪集合”。
- 定义:
typedef enum { ES_MUTE_INVERSION_TYPE_PROCESS, // 对“进程ID降噪集合”进行一次状态转换 ES_MUTE_INVERSION_TYPE_PATH, // 对“进程路径降噪集合”进行一次状态转换 ES_MUTE_INVERSION_TYPE_TARGET_PATH, // 对“目标路径降噪集合”进行一次状态转换 ES_MUTE_INVERSION_TYPE_LAST, } es_mute_inversion_type_t;
2.2 接口
2.2.1 ES客户端
es_new_client()
- 声明:
es_new_client_result_t es_new_client(es_client_t **client, es_handler_block_t handler);
- 简述:
- 代理创建:创建一个ES客户端用于与ES子系统连接。
- 说明:
- 实践中,一定不要忘记处理 es_new_client() 的返回值,这非常重要。关于其返回值,参考本文:“2.1 类型 -> es_new_client_result_t()”
- 调用 es_new_client() 时的第二个参数是一个Block,可以理解为C++的Lambda表达式。该参数即你ES客户端的“事件处理函数”。前文中提到,每个ES客户端都有一个“事件队列”。当你的“事件处理函数”返回后,下一个事件才出队。
- 示例:
- 参见:“es_delete_client() -> 示例”
es_delete_client()
- 声明:
es_return_t es_delete_client(es_client_t *client);
- 简述:
- 代理析构:删除 es_new_client() 创建的ES客户端,并将其从ES子系统断连。
- 说明:
- 注意,ES源码注释中指出,es_new_client() 和 es_delete_client() 必须在同一个线程中调用。至于原因,有待调研。
- 示例:
// by OH-Coffee #import <Foundation/Foundation.h> #import <EndpointSecurity/EndpointSecurity.h> int main(int argc, const char * argv[]) { es_client_t *es_client = nil; do { es_new_client_result_t r_es_result = es_new_client(&es_client, ^(es_client_t *i_client, const es_message_t *i_message) { if(i_message->version >= 4) { NSLog(@"%llu", i_message->global_seq_num); } }); // 错误处理 switch (r_es_result) { case ES_NEW_CLIENT_RESULT_SUCCESS: { ; } break; case ES_NEW_CLIENT_RESULT_ERR_NOT_ENTITLED: case ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED: case ES_NEW_CLIENT_RESULT_ERR_NOT_PRIVILEGED: { ; } break; default: break; } } while (false); if(nil != es_client) { es_delete_client(es_client); es_client = nil; } }
es_clear_cache()
- 声明:
es_clear_cache_result_t es_clear_cache(es_client_t *client);
- 简述:
- ES客户端缓存清理:清理所有ES客户端缓存的AUTH结果。(我再研究研究)
2.2.2 事件订阅
es_subscribe()
- 声明:
es_return_t es_subscribe(es_client_t *client, const es_event_type_t *events, uint32_t event_count);
- 简述:
- 事件订阅:订阅给定的事件。
- 说明:
- 调用 es_subscribe() 订阅事件时,并不会把该ES客户端已订阅的事件取消掉(新增,而不是覆盖)。
- 单次订阅还是批处理式订阅。es_subscribe() 的第二个参数是一个数组,所以,你可以一起订阅多个事件,也可以一次订阅一个事件,通过多次调用 es_subscribe() 来订阅多个事件。但是,测试时发现,在程序运行过程中多次调用 es_subscribe() 可能会引发系统卡顿,在低版本macOS的虚拟机中的,尤为显著。
- 示例:
- 参见:“es_subscriptions() -> 示例”
es_unsubscribe()
- 声明:
es_return_t es_unsubscribe(es_client_t *client, const es_event_type_t *events, uint32_t event_count);
- 简述:
- 事件注销:注销给定的事件。
- 说明:
- 调用 es_unsubscribe() 注销事件时,仅会注销掉参数2中指定的事件,而其他已订阅的事件不会被注销。
- 在 es_subscribe() 的“说明”中提到系统卡顿的问题。测试时,采取调用一次 es_subscribe() 订阅一个事件、调用一次 es_unsubscribe() 注销一个事件的方式。所以,实践中,可能需要避免在程序运行时高频调用es_subscribe()和es_unsubscribe(),当然,请读者自行评估。
- 示例:
- 参见:“es_subscriptions() -> 示例”
es_unsubscribe_all()
- 声明:
es_return_t es_unsubscribe_all(es_client_t *client);
- 简述:
- 事件注销:注销所有已订阅的事件。
- 示例:
- 参见:“es_subscriptions() -> 示例”
es_subscriptions()
- 声明:
es_return_t es_subscriptions(es_client_t *client, size_t *count, es_event_type_t **subscriptions);
- 简述:
- 事件查询:查询已订阅的事件。
- 说明:
- 注意,es_subscriptions() 的第三个参数是一个指针,调用此函数后,该指针指向一片由函数内分配的内存,所以,需要调用者手动释放该内存。
- 示例:
// by OH-Coffee #import <Foundation/Foundation.h> #import <EndpointSecurity/EndpointSecurity.h> int main(int argc, const char * argv[]) { es_client_t *es_client = nil; do { // ES客户端创建 es_new_client_result_t r_es_result = es_new_client(&es_client, ^(es_client_t *i_client, const es_message_t *i_message) { if(i_message->version >= 4) { NSLog(@"%llu", i_message->global_seq_num); } }); if(ES_NEW_CLIENT_RESULT_SUCCESS != r_es_result) { break; } // 事件订阅 es_event_type_t es_events[] = { ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_LINK, ES_EVENT_TYPE_NOTIFY_UNLINK, }; uint32_t es_events_count = ( sizeof(es_events) / sizeof(es_events[0]) ); es_return_t r_es_return = es_subscribe(es_client, es_events, es_events_count); if(ES_RETURN_ERROR == r_es_return) { break; } // 事件查询 size_t es_events_num = 0; es_event_type_t *es_events_ptr = nil; r_es_return = es_subscriptions(es_client, &es_events_num, &es_events_ptr); if(ES_RETURN_ERROR == r_es_return) { break; } for(int i = 0; i < es_events_num; i++) { NSLog(@"%d", es_events_ptr[i]); } // 注意,es_subscriptions()之后的内存需要调用者释放 if(nil != es_events_ptr) { free(es_events_ptr); es_events_ptr = nil; } // 事件注销 r_es_return = es_unsubscribe(es_client, es_events, es_events_count); if(ES_RETURN_ERROR == r_es_return) { break; } r_es_return = es_unsubscribe_all(es_client); if(ES_RETURN_ERROR == r_es_return) { break; } } while (false); if(nil != es_client) { es_delete_client(es_client); es_client = nil; } }
2.2.3 事件降噪
- 强调:本文中的名词“进程ID”代指进程的Audit Token ID,即audit_token_t类型对象。
es_mute_process()
- 声明:
es_return_t es_mute_process(es_client_t *client, const audit_token_t *audit_token);
- 简述:
- 事件降噪:根据“进程ID”进行“事件降噪”。将给定“进程ID”及其所有事件加入“进程ID降噪集合”,即:给定“进程ID”所代表的进程 其产生的任何事件都不会给到对应的ES客户端。
- 示例:
- 参见:“es_muted_processes() -> 示例”
es_unmute_process()
- 声明:
es_return_t es_unmute_process(es_client_t *client, const audit_token_t *audit_token);
- 简述:
- 事件降噪取消:根据“进程ID”进行“事件降噪取消”。将给定“进程ID”及其所有事件从“进程ID降噪集合”中删除。即:给定“进程ID”所代表的进程 其产生的任何事件将会重新给到对应的ES客户端。
- 示例:
- 参见:“es_muted_processes() -> 示例”
es_muted_processes()
- 声明:
es_return_t es_muted_processes(es_client_t *client, size_t *count, audit_token_t **audit_tokens);
- 简述:
- 事件降噪查询:查询已被“降噪”的“进程ID”。
- 在macOS-12.0被标记为“废弃”,macOS-12.0+应该使用es_muted_processes_events()替代。
- 示例:
// by OH-Coffee #import <libproc.h> #import <bsm/libbsm.h> #import <Foundation/Foundation.h> #import <EndpointSecurity/EndpointSecurity.h> int main(int argc, const char * argv[]) { es_client_t *es_client = nil; do { // ES客户端创建 es_new_client_result_t r_es_result = es_new_client(&es_client, ^(es_client_t *i_client, const es_message_t *i_message) { do { pid_t pid = audit_token_to_pid(i_message->process->audit_token); if(pid == getpid()) { // 事件降噪 es_return_t r_es_return = es_mute_process(i_client, &i_message->process->audit_token); if(ES_RETURN_ERROR == r_es_return) { break; } } } while (false); }); if(ES_NEW_CLIENT_RESULT_SUCCESS != r_es_result) { break; } // 事件订阅 es_event_type_t es_events[] = { ES_EVENT_TYPE_NOTIFY_ACCESS, }; uint32_t es_events_count = ( sizeof(es_events) / sizeof(es_events[0]) ); es_return_t r_es_return = es_subscribe(es_client, es_events, es_events_count); if(ES_RETURN_ERROR == r_es_return) { break; } // 创建延时任务:3s后进行触发ACCESS事件 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ do { char path[1024]; bzero(path, sizeof(path)); int r_code = proc_pidpath(getpid(), path, sizeof(path)); if(0 >= r_code) { break; } if(0 == access(path, F_OK)) { ; } // 触发ACCESS事件 } while (false); }); // 创建延时任务:10s后进行事件降噪查询,并执行事件降噪取消 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ do { size_t audit_tokens_num = 0; audit_token_t *audit_tokens_ptr = nil; // 事件降噪查询 es_return_t r_es_return = es_muted_processes(es_client, &audit_tokens_num, &audit_tokens_ptr); if(ES_RETURN_ERROR == r_es_return) { break; } for(int i = 0; i < audit_tokens_num; i++) { if(getpid() == audit_token_to_pid(audit_tokens_ptr[i])) { NSLog(@"Yes, I got You!"); } // 事件降噪取消 r_es_return = es_unmute_process(es_client, &audit_tokens_ptr[i]); if(ES_RETURN_ERROR == r_es_return) { continue; } } // 注意,需要手动释放内存 if(nil != audit_tokens_ptr) { free(audit_tokens_ptr); audit_tokens_ptr = nil; } exit(0); // 进程退出 } while (false); }); } while (false); [[NSRunLoop currentRunLoop] run]; // 主线程陷入循环,后续的代码不会被调用 if(nil != es_client) { es_delete_client(es_client); es_client = nil; } }
es_mute_process_events()
- 声明:
es_return_t es_mute_process_events(es_client_t *client, const audit_token_t *audit_token, const es_event_type_t *events, size_t event_count);
- 简述:
- 事件降噪:根据“进程ID”,对指定的事件进行“事件降噪”。将给定“进程ID”的指定事件加入“进程ID降噪集合”,即:给定“进程ID”所代表的进程 其产生的指定事件将不会给到对应的ES客户端。
- macOS-12.0引入
- 说明:
- es_mute_process()是将指定进程的所有事件都降噪,而 es_mute_process_events() 可以将指定进程的某些事件降噪。后者的灵活性更高,但如果你的程序要兼容macOS-12.0以下的系统,那么很遗憾。
- 使用 es_mute_process_events() 时,你可以指定任何ES已支持的事件。尽管你在使用 es_subscribe() 时仅订阅了一个事件,你也可以在使用 es_mute_process_events() 时把所有事件都“降噪”,API不会报错——是的,es_mute_process()就是将所有事件都加入了“事件降噪集合”。
- 你可以将“es_release_muted_processes() -> 示例”第20行的注释取消,把第29行注释,然后把断点打在“事件降噪查询”处的代码,观察相关数据结构。
- 示例:
- 参见:“es_release_muted_processes() -> 示例”
es_unmute_process_events()
- 声明:
es_return_t es_unmute_process_events(es_client_t *client, const audit_token_t *audit_token, const es_event_type_t *events, size_t event_count);
- 简述:
- 事件降噪取消:根据“进程ID”,对指定的事件进行“事件降噪取消”。将给定“进程ID”的指定事件从“进程ID降噪集合”中删除,即:给定“进程ID”所代表的进程 其产生的指定事件将会给重新给到对应的ES客户端。
- macOS-12.0引入
- 示例:
- 参见:“es_release_muted_processes() -> 示例”
es_muted_processes_events()
- 声明:
es_return_t es_muted_processes_events(es_client_t *client, es_muted_processes_t **muted_processes);
- 简述:
- 事件降噪查询:查询已被“降噪”的“进程ID”及其被“降噪”的事件。
- macOS-12.0引入
- 说明:
- 注意,es_muted_processes_events() 的第二个参数是一个指针,调用此函数后,该指针指向一片由 函数内分配的内存,需要调用者使用es_release_muted_processes()来释放该内存。
- 示例:
- 参见:“es_release_muted_processes() -> 示例”
es_release_muted_processes()
- 声明:
void es_release_muted_processes(es_muted_processes_t *muted_processes);
- 简述:
- 事件降噪查询内存释放:释放 es_muted_processes_events() 分配的内存。
- macOS-12.0引入
- 示例:
// by OH-Coffee #import <libproc.h> #import <bsm/libbsm.h> #import <Foundation/Foundation.h> #import <EndpointSecurity/EndpointSecurity.h> int main(int argc, const char * argv[]) { es_client_t *es_client = nil; do { // ES客户端创建 es_new_client_result_t r_es_result = es_new_client(&es_client, ^(es_client_t *i_client, const es_message_t *i_message) { do { pid_t pid = audit_token_to_pid(i_message->process->audit_token); if(pid == getpid()) { // 可以试着将es_mute_process()打开,而把es_mute_process_events()注释,看看事件降噪查询时的区别。 // es_return_t r_es_return = es_mute_process(i_client, &i_message->process->audit_token); // 事件降噪 es_event_type_t es_events[] = { ES_EVENT_TYPE_NOTIFY_ACCESS, ES_EVENT_TYPE_NOTIFY_LINK, ES_EVENT_TYPE_NOTIFY_UNLINK, }; uint32_t es_events_count = ( sizeof(es_events) / sizeof(es_events[0]) ); es_return_t r_es_return = es_mute_process_events(i_client, &i_message->process->audit_token, es_events, es_events_count); if(ES_RETURN_ERROR == r_es_return) { break; } } } while (false); }); if(ES_NEW_CLIENT_RESULT_SUCCESS != r_es_result) { break; } // 事件订阅 es_event_type_t es_events[] = { ES_EVENT_TYPE_NOTIFY_ACCESS, }; uint32_t es_events_count = ( sizeof(es_events) / sizeof(es_events[0]) ); es_return_t r_es_return = es_subscribe(es_client, es_events, es_events_count); if(ES_RETURN_ERROR == r_es_return) { break; } // 创建延时任务:3s后进行触发ACCESS事件 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ do { char path[1024]; bzero(path, sizeof(path)); int r_code = proc_pidpath(getpid(), path, sizeof(path)); if(0 >= r_code) { break; } if(0 == access(path, F_OK)) { ; } // 触发ACCESS事件 } while (false); }); // 创建延时任务:10s后进行事件降噪查询,并执行事件降噪取消 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ do { es_muted_processes_t *es_muted_processes = nil; // 事件降噪查询 es_return_t r_es_return = es_muted_processes_events(es_client, &es_muted_processes); if(ES_RETURN_ERROR == r_es_return) { break; } for(int i = 0; i < es_muted_processes->count; i++) { if(getpid() == audit_token_to_pid(es_muted_processes->processes[i].audit_token)) { NSLog(@"Yes, I got You!"); // 事件降噪取消 r_es_return = es_unmute_process_events(es_client,&es_muted_processes->processes[i].audit_token, es_muted_processes->processes[i].events, es_muted_processes->processes[i].event_count); if(ES_RETURN_ERROR == r_es_return) { continue; } } } if(nil != es_muted_processes) { es_release_muted_processes(es_muted_processes); es_muted_processes = nil; } exit(0); // 进程退出 } while (false); }); } while (false); [[NSRunLoop currentRunLoop] run]; // 主线程陷入循环,后续的代码不会被调用 if(nil != es_client) { es_delete_client(es_client); es_client = nil; } }
es_mute_path_prefix()
- 声明:
es_return_t es_mute_path_prefix(es_client_t *client, const char *path_prefix);
- 简述:
- 事件降噪:根据“进程路径”,按“前缀匹配”的方式进行“事件降噪”。将给定“进程路径”及其所有事件加入“进程路径降噪集合”,即:将引发事件的进程的可执行文件路径,按“前缀匹配”方式,与给定的“进程路径”进行匹配,如果匹配命中,则相关进程产生的任何事件都不会给到对应的ES客户端。
-
macOS-12.0被标记为“废弃",macOS-12.0+应该使用es_mute_path()或es_mute_path_events()替代。
- 示例:
- 参见:“es_unmute_all_paths() -> 示例”
es_mute_path_literal()
- 声明:
es_return_t es_mute_path_literal(es_client_t *client, const char *path_literal);
- 简述:
- 事件降噪:根据“进程路径”,按“字面匹配”的方式进行“事件降噪”。将给定“进程路径”及其所有事件加入“进程路径降噪集合”,即:将引发事件的进程的可执行文件路径,按“字面匹配”方式,与给定的“进程路径”进行匹配,如果匹配命中,则相关进程产生的任何事件都不会给到对应的ES客户端。
- macOS-12.0被标记为“废弃",macOS-12.0+应该使用es_mute_path()或es_mute_path_events()替代。
- 示例:
- 参见:“es_unmute_all_paths() -> 示例”
es_unmute_all_paths()
- 声明:
es_return_t es_unmute_all_paths(es_client_t *client);
- 简述:
- 事件降噪取消:清空对应ES客户端的“进程路径降噪集合”。注意,仅清空“进程路径降噪集合”,而“进程ID降噪集合”、“目标路径降噪集合”不受影响。
- 示例:
// by OH-Coffee #import <libproc.h> #import <bsm/libbsm.h> #import <Foundation/Foundation.h> #import <EndpointSecurity/EndpointSecurity.h> int main(int argc, const char * argv[]) { es_client_t *es_client = nil; do { // ES客户端创建 es_new_client_result_t r_es_result = es_new_client(&es_client, ^(es_client_t *i_client, const es_message_t *i_message) { if(i_message->version >= 4) { NSLog(@"%llu", i_message->global_seq_num); } }); if(ES_NEW_CLIENT_RESULT_SUCCESS != r_es_result) { break; } es_return_t r_es_return = ES_RETURN_ERROR; // 事件降噪:“前缀匹配” r_es_return = es_mute_path_prefix(es_client, "/bin/"); if(ES_RETURN_ERROR == r_es_return) { break; } r_es_return = es_mute_path_prefix(es_client, "/opt/homebrew/bin/"); if(ES_RETURN_ERROR == r_es_return) { break; } // 事件降噪:“字面匹配” char path[1024]; bzero(path, sizeof(path)); int r_code = proc_pidpath(getpid(), path, sizeof(path)); if(0 >= r_code) { break; } r_es_return = es_mute_path_literal(es_client, path); if(ES_RETURN_ERROR == r_es_return) { break; } // 创建延时任务:3s后进行事件降噪取消 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ es_return_t r_es_return = es_unmute_all_paths(es_client); if(ES_RETURN_ERROR == r_es_return) { return; } }); // 事件订阅 es_event_type_t es_events[] = { ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_EXEC, ES_EVENT_TYPE_NOTIFY_FORK, ES_EVENT_TYPE_NOTIFY_EXIT, }; uint32_t es_events_count = ( sizeof(es_events) / sizeof(es_events[0]) ); r_es_return = es_subscribe(es_client, es_events, es_events_count); if(ES_RETURN_ERROR == r_es_return) { break; } } while (false); [[NSRunLoop currentRunLoop] run]; // 主线程陷入循环,后续的代码不会被调用 if(nil != es_client) { es_delete_client(es_client); es_client = nil; } }
es_mute_path()
- 声明:
es_return_t es_mute_path(es_client_t *client, const char *path, es_mute_path_type_t type);
- 简述:
- 事件降噪:根据“路径匹配类型”进行“事件降噪”。根据给定的“路径匹配类型”,将给定路径及其所有事件加入对应的“事件降噪集合”,即:将引发事件的相关路径,在相关“事件降噪集合”中,按相应的“路径匹配方式”进行匹配,如果匹配命中,则相关进程产生的任何事件都不会给到对应的ES客户端。
- macOS-12.0引入
- 示例:
- 参见:“es_unmute_paths() -> 示例”
es_unmute_path()
- 声明:
es_return_t es_unmute_path(es_client_t *client, const char *path, es_mute_path_type_t type);
- 简述:
- 事件降噪取消:根据“路径匹配类型”,将给定路径从相应的“事件降噪集合”中删除。
- macOS-12.0引入
- 说明:
-
回顾“1.4 降噪集合”,对于路径类型的“事件降噪”,内核维护着类似于 {“路径匹配类型”,“路径”,“事件类型” } 元组的集合,从集合中取消不存在的元组是不生效的。
-
例如,“进程路径降噪集合”中存在了一个 { "LITERAL", "/foo/bar", "*" } 的元组,使用es_unmute_path()、es_unmute_path_events()来取消“事件降噪”时:
es_unmute_path(es_client, "/foo/", ES_MUTE_PATH_TYPE_LITERAL); // 无效,因为: "/foo/" != "/foo/bar" es_unmute_path(es_client, "/foo/bar", ES_MUTE_PATH_TYPE_PREFIX); // 无效,因为: PREFIX != LITERAL // 以下均无效,因为作用集合不同。ES_MUTE_PATH_TYPE_TARGET_XXX指明其作用于“目标路径降噪集合” es_unmute_path(es_client, "/foo/bar", ES_MUTE_PATH_TYPE_TARGET_PREFIX); es_unmute_path(es_client, "/foo/bar", ES_MUTE_PATH_TYPE_TARGET_LITERAL); es_unmute_path(es_client, "/foo/bar", ES_MUTE_PATH_TYPE_LITERAL); // 有效,路径、匹配类型、集合完全对应。
-
注意,不要用目录层级的概念来臆想mute和unmute的行为。unmute时的元组信息必须要和mute时的元组信息一一对应才有效。
-
- 示例:
// by OH-Coffee #import <libproc.h> #import <bsm/libbsm.h> #import <Foundation/Foundation.h> #import <EndpointSecurity/EndpointSecurity.h> int main(int argc, const char * argv[]) { es_client_t *es_client = nil; do { // ES客户端创建 es_new_client_result_t r_es_result = es_new_client(&es_client, ^(es_client_t *i_client, const es_message_t *i_message) { if(i_message->version >= 4) { NSLog(@"%llu", i_message->global_seq_num); } }); if(ES_NEW_CLIENT_RESULT_SUCCESS != r_es_result) { break; } es_return_t r_es_return = ES_RETURN_ERROR; // 事件降噪:“进程路径”的“事件降噪” r_es_return = es_mute_path(es_client, "/bin/ps", ES_MUTE_PATH_TYPE_LITERAL); if(ES_RETURN_ERROR == r_es_return) { break; } r_es_return = es_mute_path(es_client, "/opt/homebrew/bin/", ES_MUTE_PATH_TYPE_PREFIX); if(ES_RETURN_ERROR == r_es_return) { break; } // 事件降噪:“目标路径”的“事件降噪” r_es_return = es_mute_path(es_client, "/bin/ps", ES_MUTE_PATH_TYPE_TARGET_LITERAL); if(ES_RETURN_ERROR == r_es_return) { break; } r_es_return = es_mute_path(es_client, "/opt/homebrew/bin/", ES_MUTE_PATH_TYPE_TARGET_PREFIX); if(ES_RETURN_ERROR == r_es_return) { break; } // 注意,在实践中,“字面匹配”一般应该具体到文件,而“前缀匹配”多用于目录 // 创建延时任务:3s后进行事件降噪取消s dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ es_return_t r_es_return = ES_RETURN_ERROR; r_es_return = es_unmute_path(es_client, "/bin/", ES_MUTE_PATH_TYPE_PREFIX); if(ES_RETURN_ERROR == r_es_return) { return; } r_es_return = es_unmute_path(es_client, "/bin/", ES_MUTE_PATH_TYPE_TARGET_PREFIX); if(ES_RETURN_ERROR == r_es_return) { return; } }); // 事件订阅 es_event_type_t es_events[] = { ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_EXEC, ES_EVENT_TYPE_NOTIFY_FORK, ES_EVENT_TYPE_NOTIFY_EXIT, }; uint32_t es_events_count = ( sizeof(es_events) / sizeof(es_events[0]) ); r_es_return = es_subscribe(es_client, es_events, es_events_count); if(ES_RETURN_ERROR == r_es_return) { break; } } while (false); [[NSRunLoop currentRunLoop] run]; // 主线程陷入循环,后续的代码不会被调用 if(nil != es_client) { es_delete_client(es_client); es_client = nil; } }
es_mute_path_events()
- 声明:
es_return_t es_mute_path_events(es_client_t *client, const char *path, es_mute_path_type_t type, const es_event_type_t *events, size_t event_count);
- 简述:
- 事件降噪:根据给定路径和“路径匹配类型”,对指定的事件进行“事件降噪”。请类比“es_mute_process_events()”
- macOS-12.0引入
- 说明:
- 前文中提到过,并不是所有的事件都支持“目标路径”的。
- 所以,在使用es_mute_path_events(),按“目标路径”来对一批事件进行“事件降噪”时,对不支持“目标路径”的事件来说,“降噪”不生效。
- 而且,这批事件中,只要其中一个事件支持“目标路径”,那么es_mute_path_events()的返回值就为ES_RETURN_SUCCESS,仅当这批事件都不支持“目标路径”时,es_mute_path_events()的返回值才为ES_RETURN_ERROR。
- 示例:
- 参见:“es_release_muted_paths() -> 示例”
es_unmute_path_events()
- 声明:
es_return_t es_unmute_path_events(es_client_t *client, const char *path, es_mute_path_type_t type, const es_event_type_t *events, size_t event_count);
- 简述:
- 事件降噪取消:根据给定路径和“路径匹配类型”,对指定的事件进行“事件降噪取消”。请类比“es_unmute_process_events()”
- macOS-12.0引入
- 示例:
- 参见:“es_release_muted_paths() -> 示例”
es_muted_paths_events()
- 声明:
es_return_t es_muted_paths_events(es_client_t *client, es_muted_paths_t **muted_paths);
- 简述:
- 事件降噪查询:查询已被“降噪”的路径及其被“降噪”的事件。
- macOS-12.0引入
- 说明:
- 注意,es_muted_paths_events() 的第二个参数是一个指针,调用此函数后,该指针指向一片由 函数内分配的内存,需要调用者使用 es_release_muted_paths() 来释放该内存。
- 示例:
- 参见:“es_release_muted_paths() -> 示例”
es_release_muted_paths()
- 声明:
void es_release_muted_paths(es_muted_paths_t *muted_paths);
- 简述:
- 事件降噪查询内存释放:释放 es_muted_paths_events() 分配的内存。
- macOS-12.0引入
- 示例:
// by OH-Coffee #import <libproc.h> #import <bsm/libbsm.h> #import <Foundation/Foundation.h> #import <EndpointSecurity/EndpointSecurity.h> int main(int argc, const char * argv[]) { es_client_t *es_client = nil; do { // ES客户端创建 es_new_client_result_t r_es_result = es_new_client(&es_client, ^(es_client_t *i_client, const es_message_t *i_message) { do { if(getpid() == audit_token_to_pid(i_message->process->audit_token)) { // 事件降噪:不要在意下面的事件,测试而已 es_event_type_t es_events[] = { ES_EVENT_TYPE_NOTIFY_LOGIN_LOGIN, ES_EVENT_TYPE_NOTIFY_LOGIN_LOGOUT, }; uint32_t es_events_count = ( sizeof(es_events) / sizeof(es_events[0]) ); es_return_t r_es_return = es_mute_path_events(i_client, i_message->process->executable->path.data, ES_MUTE_PATH_TYPE_LITERAL, es_events, es_events_count); if(ES_RETURN_ERROR == r_es_return) { break; } } } while (false); }); if(ES_NEW_CLIENT_RESULT_SUCCESS != r_es_result) { break; } // 事件订阅 es_event_type_t es_events[] = { ES_EVENT_TYPE_NOTIFY_SU, ES_EVENT_TYPE_NOTIFY_SUDO, ES_EVENT_TYPE_NOTIFY_ACCESS, }; uint32_t es_events_count = ( sizeof(es_events) / sizeof(es_events[0]) ); es_return_t r_es_return = es_subscribe(es_client, es_events, es_events_count); if(ES_RETURN_ERROR == r_es_return) { break; } // 创建延时任务:3s后进行触发ACCESS事件 NSString __block *proc_path = nil; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ do { char path[1024]; bzero(path, sizeof(path)); int r_code = proc_pidpath(getpid(), path, sizeof(path)); if(0 >= r_code) { break; } proc_path = [NSString stringWithUTF8String:path]; if(0 == access(path, F_OK)) { ; } // 触发ACCESS事件 } while (false); }); // 创建延时任务:10s后进行事件降噪查询,并执行事件降噪取消 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ do { es_muted_paths_t *es_muted_paths = nil; // 事件降噪查询 es_return_t r_es_return = es_muted_paths_events(es_client, &es_muted_paths); if(ES_RETURN_ERROR == r_es_return) { break; } for(int i = 0; i < es_muted_paths->count; i++) { // 知道为什么要加这条判断语句吗?回想一下"1.4 降噪集合"中提到的“默认的事件降噪集合” if(YES != [proc_path isEqual:@(es_muted_paths->paths[i].path.data)]) { continue; } // 事件降噪取消 r_es_return = es_unmute_path_events(es_client, es_muted_paths->paths[i].path.data, es_muted_paths->paths[i].type, es_muted_paths->paths[i].events, es_muted_paths->paths[i].event_count); if(ES_RETURN_ERROR == r_es_return) { continue; } } if(nil != es_muted_paths) { es_release_muted_paths(es_muted_paths); es_muted_paths = nil; } exit(0); // 进程退出 } while (false); }); } while (false); [[NSRunLoop currentRunLoop] run]; // 主线程陷入循环,后续的代码不会被调用 if(nil != es_client) { es_delete_client(es_client); es_client = nil; } }
es_unmute_all_target_paths()
- 声明:
es_return_t es_unmute_all_target_paths(es_client_t *client);
- 简述:
- 事件降噪取消:清空对应ES客户端的“目标路径降噪集合”。注意,仅清空“目标路径降噪集合”,而“进程ID降噪集合”、“进程路径降噪集合”不受影响。
- macOS-13.0引入
- 示例:
- 参见:“es_muting_inverted() -> 示例”
2.2.4 事件聚焦
es_invert_muting()
- 声明:
es_return_t es_invert_muting(es_client_t *client, es_mute_inversion_type_t mute_type);
- 简述:
-
事件聚焦:对“事件聚焦类型”指定的“事件降噪集合”的状态进行转换,可以达到“事件聚焦”的目的
-
macOS-13.0引入
-
-
说明:
-
在“描述”中,说的是“可以达到“事件聚焦”的目的”。回顾“1.5 事件聚焦”,因为es_invert_muting()只是改变了“事件降噪集合”的状态。所以,到底是“降噪”还是“聚焦”,取决于你调用es_invert_muting()的次数。
-
注意默认“事件降噪集合”的影响。回顾“1.4 降噪集合”,ES客户端创建后,一些系统路径会被加入到“事件降噪集合”。如果你没有考虑到默认“事件降噪集合”,那么,在你使用es_invert_muting()后,被“聚焦”的事件数据量将比你预期的要多的多。因为触发事件的路径,除了你调用es_mute系列API设置的那些路径,还有系统默认的路径。
-
“事件聚焦”只是改变了“事件降噪集合”的“状态”。首先,你要明确,这个“状态”修饰的是整个“事件降噪集合”,而不是集合中的每一个元组都有一个状态。其次,你要明确,es_invert_muting()只是改变了集合的状态,并没有清空集合中的元组!例如,当你使用es_invert_muting()将“进程路径降噪集合”从“降噪态”变为“聚焦态”之后,你再使用es_mute_path()将某个“进程路径”放入“进程路径降噪集合”,那么,这个“进程路径”所示进程产生的事件,也成为了你关注的事件。
-
谨慎地调用一些API。调用es_unmute_all_paths()、es_unmute_all_target_paths()后,它们会清空相应的“事件降噪集合”——清空,意味着那些系统默认路径也被清除了(想想这些路径的作用)。如果你的需求是“降噪”,那么不要轻易调用这两个API。如果你的需求是聚焦,那么也请三思为后行。当然,最终还是取决于你订阅了哪些事件。总的来说,如果你仅仅是订阅了NOTIFY类型,那么完全不必紧张。如果你订阅了AUTH类型的事件,那么请你花一分钟的时间,思考一下,当前的“事件降噪集合”中,有哪些路径,以及它们的状态是什么样的——尽管AUTH事件有超时机制,但AUTH事件处理不当,将给用户带来糟糕的体验。
-
说明一下,在我的测试中,并没有发现有系统默认的“进程ID”,仅有系统默认的路径。但是,在实践中,还是请你留意一下“进程ID降噪集合”。
-
- 示例:
- 参见:“es_muting_inverted() -> 示例”
es_muting_inverted()
- 声明:
es_mute_inverted_return_t es_muting_inverted(es_client_t *client, es_mute_inversion_type_t mute_type);
- 简述:
- 事件降噪集合状态查询:根据第2个参数,查询对应的“事件降噪集合”是否处于“聚焦态”。
- macOS-13.0引入
- 示例:
// by OH-Coffee #import <libproc.h> #import <bsm/libbsm.h> #import <Foundation/Foundation.h> #import <EndpointSecurity/EndpointSecurity.h> int main(int argc, const char * argv[]) { es_client_t *es_client = nil; char *path = new char[1024]; bzero(path, 1024); do { int r_code = proc_pidpath(getpid(), path, 1024); assert(r_code >= 0); // ES客户端创建 es_new_client_result_t r_es_result = es_new_client(&es_client, ^(es_client_t *i_client, const es_message_t *i_message) { // 由于使用了“事件聚焦”,ES客户端只会接收到本进程引发的事件。 assert( 0 == strcmp(path, i_message->process->executable->path.data) ); if(i_message->version >= 4) { NSLog(@"%llu, %s", i_message->global_seq_num, i_message->event.access.target->path.data); } }); if(ES_NEW_CLIENT_RESULT_SUCCESS != r_es_result) { break; } // 清空“进程路径降噪集合”、“目标路径降噪集合”,为“事件聚焦”做准备 es_unmute_all_paths(es_client); es_unmute_all_target_paths(es_client); // 将自身路径加入“进程路径降噪集合”、“目标路径降噪集合”,为“事件聚焦”做准备 es_mute_path(es_client, path, ES_MUTE_PATH_TYPE_LITERAL); es_mute_path(es_client, path, ES_MUTE_PATH_TYPE_TARGET_LITERAL); // “事件聚焦”。即:只关注 本进程 访问自身“进程路径”的ACCESS事件 es_invert_muting(es_client, ES_MUTE_INVERSION_TYPE_PATH); es_invert_muting(es_client, ES_MUTE_INVERSION_TYPE_TARGET_PATH); // 判断“进程路径降噪集合”、“目标路径降噪集合”是否已经从“降噪态”变为“聚焦态” assert(ES_MUTE_INVERTED == es_muting_inverted(es_client, ES_MUTE_INVERSION_TYPE_PATH)); assert(ES_MUTE_INVERTED == es_muting_inverted(es_client, ES_MUTE_INVERSION_TYPE_TARGET_PATH)); // 事件订阅 es_event_type_t es_events[] = { ES_EVENT_TYPE_NOTIFY_ACCESS, }; uint32_t es_events_count = ( sizeof(es_events) / sizeof(es_events[0]) ); es_return_t r_es_return = es_subscribe(es_client, es_events, es_events_count); assert(ES_RETURN_ERROR != r_es_return); } while (false); // 创建周期任务:3s后开始,每隔1s执行一次。 dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC, 1 * NSEC_PER_SEC); dispatch_source_set_event_handler(timer, ^{ if(0 == access(path, F_OK)) { ; } // 触发ACCESS事件 if(0 == access("/tmp", F_OK)) { ; } // 触发ACCESS事件 }); dispatch_resume(timer); [[NSRunLoop currentRunLoop] run]; // 主线程陷入循环,后续的代码不会被调用 if(nil != es_client) { es_delete_client(es_client); es_client = nil; } }
- 提问:
- 上述“示例”代码中,事件处理函数仅能获取到第66行的ACCESS事件,如何才能获取到第67行的ACCESS事件?(可以先看看“3.3 事件的“过滤流程””)
2.2.5 事件授权
- 说明:
- 你可能会有这样的好奇:同时订阅了AUTH_OPEN和NOTIFY_OPEN,如果AUTH_OPEN执行了阻断,那么,是否还会产生对应的NOTIFY_OPEN事件?
- 答案是会的。这可能会在一些应用场景上带来困扰:已经对AUTH_OPEN进行了阻断,也就不希望再接收到对应的NOTIFY_OPEN事件了。应对这样的场景,可以利用es_message_t的action、action_type来解决,参见:“2.3 消息 -> es_message_t”。
es_respond_auth_result()
- 声明:
es_respond_result_t es_respond_auth_result(es_client_t *client, const es_message_t *message, es_auth_result_t result, bool cache);
- 简述:
- 事件授权:决定某事件是否可以执行
- 说明:
- 将AUTH类型的事件加入缓存后,除非缓存失效,否则该AUTH类型的事件将不会被再次上报给对应的ES客户端,但与之对应的NOTIFY类型的事件还是会上报的。
- 缓存Key一般是事件所涉及的文件(源码注释中,用的是“files”,复数形式)。当事件所涉及的文件被修改时,会导致缓存结果失效。以EXEC事件为例,当主动调用exec()的进程可执行文件变化时,会导致缓存失效。当被exec()加载的文件发生变化时,也会导致缓存失效。所以,“所涉及的文件”,除了“进程路径文件”,还包括“目标路径文件”。不同事件,其“目标路径文件”的定义不同,所以,在决定使用“缓存”前,最好写一个Demo程序来验证一下,不要妄自揣测。
- 注意,并非所有事件都支持缓存。对不支持缓存的事件,即使在es_respond_flags_result()中显式的置位了缓存,也是无效的。
- 示例:
- 参见:“es_respond_flags_result() -> 示例”
es_respond_flags_result()
- 声明:
es_respond_result_t es_respond_flags_result(es_client_t *client, const es_message_t *message, uint32_t authorized_flags, bool cache);
- 简述:
- 事件授权:决定某事件是否可以执行——目前,仅OPEN事件需要使用此API。
- 第3个参数:
- 0:拒绝执行
-
UINT32_MAX:允许执行。
- 示例:
// by OH-Coffee #import <libproc.h> #import <bsm/libbsm.h> #import <Foundation/Foundation.h> #import <EndpointSecurity/EndpointSecurity.h> // 不建议调试以下代码...当然,你可以尝试 int main(int argc, const char * argv[]) { es_client_t *es_client = nil; char *path = new char[1024]; bzero(path, 1024); do { int r_code = proc_pidpath(getpid(), path, 1024); assert(r_code >= 0); // ES客户端创建 es_new_client_result_t r_es_result = es_new_client(&es_client, ^(es_client_t *i_client, const es_message_t *i_message) { if(ES_ACTION_TYPE_AUTH == i_message->action_type) { es_respond_result_t r_resp_result = ES_RESPOND_RESULT_SUCCESS; if(ES_EVENT_TYPE_AUTH_OPEN == i_message->event_type) { r_resp_result = es_respond_flags_result(i_client, i_message, UINT32_MAX, false); } else { r_resp_result = es_respond_auth_result(i_client, i_message, ES_AUTH_RESULT_ALLOW, false); } // 错误处理 switch (r_resp_result) { default: break; } } }); if(ES_NEW_CLIENT_RESULT_SUCCESS != r_es_result) { break; } // 事件订阅 es_event_type_t es_events[] = { ES_EVENT_TYPE_AUTH_OPEN, ES_EVENT_TYPE_AUTH_EXEC, ES_EVENT_TYPE_AUTH_LINK, ES_EVENT_TYPE_AUTH_UNLINK, }; uint32_t es_events_count = ( sizeof(es_events) / sizeof(es_events[0]) ); es_return_t r_es_return = es_subscribe(es_client, es_events, es_events_count); assert(ES_RETURN_ERROR != r_es_return); } while (false); [[NSRunLoop currentRunLoop] run]; // 主线程陷入循环,后续的代码不会被调用 if(nil != es_client) { es_delete_client(es_client); es_client = nil; } }
2.3 消息
es_message_t
- 简述:
- ES消息,事件处理函数的第二个参数。非常重要的数据结构,在实践中,它可以帮助你获取绝大多数你感兴趣的信息。
- 定义:
typedef struct { uint32_t version; // es_message_t的版本号 struct timespec time; // 事件发生时间 uint64_t mach_time; // 事件发生时间 uint64_t deadline; // 授权截止时间 es_process_t* process; // 进程信息 uint64_t seq_num; // 事件序列号; es_message_t.version >= 2 es_action_type_t action_type; // 动作类型 := [ AUTH, NOTIFY ] union { es_event_id_t auth; es_result_t notify; } action; es_event_type_t event_type; // 事件类型 es_events_t event; // 事件信息 es_thread_t* thread; // 线程信息; es_message_t.version >= 4 uint64_t global_seq_num; // 全局事件序列号; es_message_t.version >= 4 uint64_t opaque[]; // 此字段不应使用 } es_message_t;
- 字段概览:
- 简单介绍一下 es_message_t 中比较重要的几个属性。
- 属性:version,标记es_message_t的版本,在使用一些备注有版本号的属性时,必须要进行version比较才可用,否则对这些属性的访问所造成的影响是未定义的。
- 属性:deadline,标记AUTH事件的截止时间,你的程序必须需要在这个时间前处理AUTH事件,否则程序会被强行终止。
- 属性:process,标记了引发事件的进程信息。
- 属性:event,标记了事件的具体信息。es_events_t是一个联合体,每一个事件,在es_events_t中都有一个与之对应的结构体。
- 属性:seq_num和global_seq_num,两者均为事件数据的“计数器”,均在事件数据产生后,自加1。每个事件类型,有自己的seq_num,而global_seq_num对所有事件类型的事件数据计数。所以,你可以用这两个字段来判断是否丢事件了,因为它们是线性增加的。
- 字段说明:action_type和action
- action_type的类型为es_action_type_t,定义如下:
typedef enum { ES_ACTION_TYPE_AUTH, ES_ACTION_TYPE_NOTIFY } es_action_type_t;
action_type决定了在具体实践中,应该使用action.auth还是action.notify字段。action.auth包含响应事件时必须提供的对开发者不透明的验证ID,action.notify则包含对于AUTH事件的授权结果。
- action.notify的类型为es_result_t,定义如下:
typedef struct { es_result_type_t result_type; union { es_auth_result_t auth; uint32_t flags; uint8_t reserved[32]; } result; } es_result_t;
如果同时订阅了AUTH_OPEN和NOTIFY_OPEN,而AUTH_OPEN执行了阻断,那么,在对应的NOTIFY事件中,可以利用action.notify来得知AUTH_OPEN的结果。
result_type字段用于指示使用者,获取授权结果时,应该使用result.auth还是result.flags。如果AUTH_OPEN是阻断的,那么result.flags等于0,如果是放行的,那么result.flags等于UINT32_MAX。result.auth字段同理,只不过result.auth是个enum类型。
所以,如果对AUTH事件进行了阻断,且不希望再接收到对应的NOTIFY事件,那么,就需要在回调函数中,判断es_message_t中action.notify.result的auth或者flags来获取AUTH事件的结果,如果是阻断,则丢弃此NOTIFY事件数据即可。
- action_type的类型为es_action_type_t,定义如下:
三、补充说明
3.1 默认的“降噪元组”
- 简述:
- 关于默认的“事件降噪元组”的概念,参考本文:“1.4 降噪集合”。
- 不同系统版本,ES客户端中的默认“事件降噪元组”可能是不同的,以下仅为作者在macOS-13.5中测试时从默认“降噪元组”提取出来的路径。关于具体路径对应的事件类型、事件的动作类型,请自写Demo测试验证。
- 示例:
# 这些路径是从作者macOS-13.5上创建的ES客户端中提取出来的,不同的系统,这些路径或许是不同的。 /System/Library/PrivateFrameworks/SkyLight.framework/Versions/A/Resources/WindowServer /System/Library/PrivateFrameworks/TCC.framework/Support/tccd /System/Library/PrivateFrameworks/TCC.framework/Versions/A/Resources/tccd /usr/libexec/opendirectoryd /usr/libexec/watchdogd /usr/libexec/trustd /usr/libexec/syspolicyd /usr/libexec/sandboxd /usr/libexec/runningboardd /usr/libexec/amfid /usr/sbin/cfprefsd /usr/sbin/securityd /usr/sbin/spindump /usr/bin/tailspin /usr/bin/sample /usr/bin/heap
3.2 事件的“目标路径”
- 简述:
- 不同的事件,其“目标路径”的定义不同,而下面“参考”中的信息,拷贝自ES的头文件。
- PS:后续我可能会翻译成中文以便阅读,不过,这些句子对大家来说,应该是没有难度的。
-
参考:
事件 “目标路径”的定义 EXEC The file being executed OPEN The file being opened MMAP The file being memory mapped RENAME Both the source and destination path. SIGNAL The path of the process being signalled UNLINK The file being unlinked CLOSE The file being closed CREATE The path to the file that will be created or replaced GET_TASK The path of the process for which the task port is being retrieved LINK Both the source and destination path SETATTRLIST The file for which the attributes are being set SETEXTATTR The file for which the extended attributes are being set SETFLAGS The file for which flags are being set SETMODE The file for which the mode is being set SETOWNER The file for which the owner is being set WRITE The file being written to READLINK The symbolic link being resolved TRUNCATE The file being truncated CHDIR The new working directory GETATTRLIST The file for which the attribute list is being retrieved STAT The file for which the stat is being retrieved ACCESS The file for which access is being tested CHROOT The file which will become the new root UTIMES The file for which times are being set CLONE Both the source file and target path FCNTL The file under file control GETEXTATTR The file for which extended attributes are being retrieved LISTEXTATTR The file for which extended attributes are being listed READDIR The directory for whose contents will be read DELETEEXTATTR The file for which extended attribues will be deleted DUP The file being duplicated UIPC_BIND The path to the unix socket that will be created UIPC_CONNECT The file that the unix socket being connected is bound to EXCHANGEDATA The path of both file1 and file2 SETACL The file for which ACLs are being set PROC_CHECK The path of the process against which access is being checked SEARCHFS The path of the volume which will be searched PROC_SUSPEND_RESUME The path of the process being suspended or resumed GET_TASK_NAME The path of the process for which the task name port will be retrieved TRACE The path of the process that will be attached to REMOTE_THREAD_CREATE The path of the process in which the new thread is created GET_TASK_READ The path of the process for which the task read port will be retrieved GET_TASK_INSPECT The path of the process for which the task inspect port will be retrieved COPYFILE The path to the source file and the path to either the new file to be created or the existing file to be overwritten
3.3 事件的“过滤流程”
- 简述:
- “图3.3.1”是根据ES头文件中的文本图例再结合本文的内容进行绘制的。
- 每个事件产生后,都会严格按照“图3.3.1”进行过滤匹配。从图中可以看出,优先级从高到低分别为:“进程ID”、“进程路径”、“目标路径”。
- 当你不清楚事件为什么没有给到事件处理函数,或者对代码逻辑感到混乱时,可以借助“图3.3.1”进行梳理。
- 图例:
- 图3.3.1 - 事件的“过滤流程”:
- 图3.3.1 - 事件的“过滤流程”: