C3 OS X and iOS 系统中的一些技术概述
由于 OSX 是从 BSD 上构建而来的, 它继承了其中许多的内核功能, 包括 POSIX 系统调用, 一些 BSD 扩展(比如内 kernel queue), 以及 BSD 强制访问控制(MAC)层.
不过直接将 OSX 视作是另外一种 BSD 系统是完完全全的误解. 苹果在 BSD 上精细地构建了许多设施, 比如最为重要的 sandbox 机制, 并且 OSX 和 iOS 中将许多 BSD 组件都进行了完整替换. 例如比较脆弱的 /etc
文件, 原本是用来存放系统配置的, 在 OSX 中被完全替换. 标准的 UNIX syslog 机制在 OSX 中被替换为了更强大的 Apple System Log.
另外还有一些新技术, 比如 Apple Events 和 FSEvents 则完全是闭源的.
下面首先介绍若干基于 BSD 的 API, 然后将注意力放到对应苹果的特定 API 上. API 的讨论是基于用户模式的, 包括若干详细例子和实验. 内核态下的 API 讨论, 将放到 14 章进行.
BSD API
虽然 XNU 的核心是 Mach, 但 XNU 面 向用户态的主要接口都是 BSD 的. OSX 和 iOS 都提供了一套 POSIX 兼容的系统调用, 以及特定于 BSD 的一套 API. 并且苹果还更进一步实现了若干额外功能, 这些功能还被反向移植到了 BSD 和 OpenDarwin.
sysctl
sysctl 命令是一种访问内核内部状态的标准方式, 在 4.4BSD 中被引入, 并且在其他的 UNIX 系统(比如 Linux, 通过 /proc/sys
目录提供支持)中也有.
使用 sysctl, 管理员可以直接查询内核变量的值, 以获取重要的运行时诊断数据. 在某些情况下, 修改内核变量的值, 可以直接改变内核的行为. 不过内核中只有少部分的变量被暴露了出来以供修改, 不过这些值会在决定内核功能中扮演关键角色.
sysctl 命令实际是 sysctl
库函数的封装, 而这个库函数又是 __sysctl
系统调用的封装. 被暴露出来的内核变量可以通过它们对应的 MIB(基础管理信息 Management Information Base)名字进行访问. MIB 命名规范借用了 SNMP(Simple Network Management Protocol)协议中的一些做法, 将变量分类到不同的名字空间中.
在 XNU 中支持相当多的硬编码的名字空间, 如下表所示:
可以看到, 名字空间有一个整数表示, 因此变量可以通过整数数组来表示. 通过库函数 sysctlnametomib
可以将名字字符串转换为对应的整数, 不过通常都不需要这么做, 因为可以使用 sysctlbyname
通过名字直接找到变量.
每个名空间都可能直接包含变量(比如 kern.ostype), 或在子名空间中包含变量(比如 kern.ipc.somaxconn), 这两种情况下, 都可以通过完整名字或名字对应的 MIB 整数值来访问变量. 前面也提到过, 通过 sysctlnametomib
将名字转换为整数值, 但目前没有将整数值转换为名字的功能.
使用 sysctl
命令可以查询所有被暴露的值, 并且可以修改那些能够改变值的变量. 但由于历史原因, 目前还无法遍历 MIB 去拿到一个变量和它们的值的列表. 虽然这个命令可以使用 -A
参数输出一个列表, 但这个列表是固定的(一些预定义的入口, 可以在 <sys/sysctl.h>
中找到, CTL_NAMES
以及相关的宏). 在 OSX 的 sysctl 命令是重新实现了的, 以便能够匹配当前的内核版本, 而在 iOS 中苹果没有提供这个命令.
内核组件在运行时可以注册额外的 sysctl 值, 甚至是额外的名空间. 比较典型的例子是 security 名空间(主要被 sandbox kext 使用), 以及 appleprofile 名空间(由 AppleProfileFamily kexts 注册).
sysctl 变量的整个范围涵盖了从一些基础的 debug 变量到控制整个子系统的可读写变量. 比如在内核中有一个鲜为人知的 kdebug
功能, 它整个都是基于 sysctl 库函数的. 另外如 ps
或 netstat
命令依赖 sysctl
系统调用来获取完整的 PID 列表/活动的套接字列表(虽然这些列表一样可以通过其他途径取得).
kqueues
kqueues 是 BSD 中的内核事件通知机制. kqueue 是一个描述符, 它会阻塞等待一个满足特定类型和分类的事件发生.
一个内核/用户模式进程可以在这个描述符上等待事件, 这样可以简单高效地同步一个或多个进程.
kqueues 和它们对应的 kevents 共同构成了内核异步 I/O 的基础(它也是 POSIX 的 poll
和 select
系统调用所依赖的机制).
使用不带任何参数的 kqueue
系统调用可以在用户模式下创建 kqueue. 然后可以通过 EV_SET
宏设置关注的事件, 这样可以生产一个 kevent 结构. 对应 kevent
或 kevent64
系统调用, 来设置事件过滤器.
系统预定义了一系列的过滤器, 如下所示:
-
EVFILT_MACHPORT: Monitors a Mach port or port set and returns if a message has been received.
-
EVFILT_PROC: Monitors a specified PID for execve(2), exit(2), fork(2), wait(2), or signals.
-
EVFILT_READ:
For files, returns when the file pointer is not at EOF.
For sockets, pipes, and FIFOs, returns when there is data to read (such as select(2)).
-
EVFILT_SESSION: Monitors an audit session (described in the next section).
-
EVFILT_SIGNAL: Monitors a specific signal to the process, even if the signal is currently ignored by the process.
-
EVFILT_TIMER: A periodic timer with up to nanosecond resolution.
-
EVFILT_WRITE:
For files, unsupported.
For sockets, pipes, and FIFOs, returns when data may be written. Returns buffer space available in event data.
-
EVFILT_VM: Virtual memory Notifications. Used for memory pressure handling (discussed in Chapter 14).
-
EVFILT_VNODE: Filters file (vnode)-specific system calls such as rename(2), delete(2), unlink(2), link(2), and others.
下面的例子用于获取指定 PID 进程的相关事件:
void main (int argc, char **argv) {
pid_t pid; // 待监听的 PID
int kq; // 对应的 kqueue 文件描述符
int rc; // 收集返回值
int done;
struct kevent ke;
pid = atoi(argv[1]); // 传入的 PID
kq = kqueue();
// kqueue 创建失败则直接退出
if (kq == -1) { perror("kqueue"); exit(2); }
// 设置监听对应 PID 进程的 fork 和 exec 事件
EV_SET(&ke, pid, EVFILT_PROC, EV_ADD,
NOTE_EXIT | NOTE_FORK | NOTE_EXEC , 0, NULL);
// 注册事件
rc = kevent(kq, &ke, 1, NULL, 0, NULL);
// 如果此时返回值小于零, 表示出错, 直接退出.
if (rc < 0) { perror ("kevent"); exit (3); }
done = 0;
while (!done) {
// 清零 kevent 结构体
memset(&ke, '\0', sizeof(struct kevent));
// 这个调用会阻塞当前线程, 直到发生对应事件
rc = kevent(kq, NULL, 0, &ke, 1, NULL);
if (rc < 0) { perror ("kevent"); exit (4); }
if (ke.fflags & NOTE_FORK)
printf("PID %d fork()ed\n", ke.ident);
if (ke.fflags & NOTE_EXEC)
printf("pid %d has exec()ed\n", ke.ident);
if (ke.fflags & NOTE_EXIT) {
printf("pid %d has exited\n", ke.ident);
done++;
}
}
Auditing (OS X)
待续...
OSX 和 IOS 系统中的特定技术
macOS 在近年来引入了许多非常现代的技术, 不过其中部分仍然是私有的. 下面就来挨个看一下.
FSEvents 底层 API
所有的现代操作系统都为开发者提供了文件系统通知相关的 API. 这些 API 可以让用户程序快速简单地响应文件的增/删/改操作. 为此, 在 Windows 中提供了它自己的 MJ_DIRECTORY_CONTROL
机制, Linux 提供了 inotify
. 而 macOS 和 iOS 则提供了 FSEvents
.
FSEvents 在概念上和 Linux 的 inotify 比较类似, 都是让一个进程或线程获取一个文件描述符, 然后在这个描述符上进行 read
调用, read 系统调用会先阻塞, 直到某个事件发生, 事件发生后, 在接收缓存中就包含事件的详细信息, 这样该进程就可以知道到底发生了什么, 然后对事件作出响应(比如展示一个图标).
相比 inotify, FSEvents 就要复杂一些(或说更优雅一些), 它的处理过程如下:
-
观察者进程(或线程)从 FSEvents 机制中获取一个句柄(即获取
/dev/fsevents
的句柄, 它是一个虚拟设备). -
观察者而后对该句柄发起一个特殊的
ioctl
操作:FSEVENTS_CLONE
.这个操作的定义如下(具体可以看苹果的开源代码, 另外还有 filemon 的实现):
#define FSEVENTS_CLONE _IOW('s', 1, fsevent_clone_args)
ioctl 允许设置特定事件的监听, 这样只有所关心的事件(对应特定文件的特定操作)才会被发送. 下表列出了所有目前支持的事件(下面表中的内容也在苹果开源代码中有):
FSEvents 的运行本质上就是插入到了内核的 VFS 中对系统调用进行反馈. 每个被支持的事件发生后, 都会添加一个待处理通知到克隆(FSEVENTS_CLONE)的文件描述符.
-
使用
ioctl
系统调用, 观察者可以设置自己想要的事件细节信息, 比如通过设置FSEVENTS_WANT_COMPACT_EVENTS
可以获取更精简的信息, 而设置FSEVENTS_WANT_EXTENDED_INFO
则可以获取更完整的信息, 还有NEW_FSEVENTS_DEVICE_FILTER
可以过滤不感兴趣的设备. -
ioctl 调用后, 观察者可以使用一个无限循环, 在循环中对文件描述符进行
read
调用, 每次read
调用返回后, 都会在传入的缓冲区中填充事件记录的数组. 对结果的处理需要一些技巧, 因为单个操作就可以返回多条记录(个数可变). 如果发生了事件被丢弃的情况(比如内核缓冲区满), 则一个特殊的事件FSE_EVENTS_DROPPED
会被加入到事件记录中.
fsevents.h
在各个版本的 XNU 中变化很大(上面给出的链接中可以发现), 虽然是公共 API, 但主要用户还是下面三个:
coreservicesd
: 它是苹果内部的一个对 Core Service 相关服务提供支持的守护进程, 比如支持 launch service.mds
: 它是 Spotlight 的服务器. Spotlight 重度依赖 FSEvents, 它通过事件通知来找到并索引新文件.fseventsd
: 一个通用的用户空间守护进程, 和coreservicesd
一起被存放在 CoreServices framework 中. 在每个卷的根目录中都会有一个.fseventsd
目录, 如果在这个目录中放一个名为 “no_log” 的文件, 就可以让fseventsd
不记录该卷的文件事件日志.
OC 和 C 都可以使用 CoreServices Framework 的 FSEventStreamCreate
族 API, 这个 Framework 实际是底层操作的一个封装, 将底层的同步的, 且阻塞式的 API 封装为异步的, 且事件驱动的 API, 由于苹果提供了此 API 非常完善的文档以及编程引导, 因此下面主要将注意力放到底层的同步 API 上.
FSEvents 的例子: 文件系统事件观测器
准备实现一个 FSEvents 客户端, 用于监听特定 path 对应文件的改变, 当事件发生时会打印对应信息. 这个例子的功 能和 fs_usage
命令类似, 不过 fs_usage
并不依赖 FSEvents, 而是依赖 kdebug
API.
下面的代码是这个客户端的实现骨架(完整实现可以看这个链接):
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/ioctl.h> // for _IOW, a macro required by FSEVENTS_CLONE
#include <sys/types.h> // for uint32_t and friends, on which fsevents.h relies
#include <sys/fsevents.h>
// 64 KB 的事件接收缓冲区大小
#define BUFSIZE 64 * 1024
// #pragma pack 作用是改变结构体成员的内存对齐方式.
// 不使用这条指令的情况下,编译器默认采取 #pragma pack(8), 也就是8字节的默认对齐
#pragma pack(1)
typedef struct kfs_event_a {
uint16_t type;
uint16_t refcount;
pid_t pid;
} kfs_event_a;
typedef struct kfs_event_arg {
uint16_t type;
uint16_t pathlen;
char data[0];
} kfs_event_arg;
#pragma pack()
int print_event (void *buf, int off) {
// 这里不演示对事件接收 buffer 的处理, 读者可按需进行.
printf("Event!\n");
return (off);
}
void main (int argc, char **argv) {
int fsed, cloned_fsed;
int i;
int rc;
fsevent_clone_args clone_args;
char buf[BUFSIZE];
int8_t events[FSE_MAX_EVENTS];
fsed = open ("/dev/fsevents", O_RDONLY); // 打开虚拟设备文件 `/dev/fsevents 获取文件描述符
if (fsed < 0) { // 打开失败
perror ("open");
exit(1);
}
// 准备一个事件 mask 的处理: 这里允许所有事件类型的接收
// (如果要单独设置事件的屏蔽, 可以在 fsevents.h 中去对应看)
for (int i = 0; i < FSE_MAX_EVENTS; i++) {
events[i] = FSE_REPORT;
}
memset(&clone_args, '\0', sizeof(clone_args)); // 刷 0 保证数据正常.
// 将 clone_args 的 fd 指针成员指向本地用于接收克隆后的文件描述符的变量
clone_args.fd = &cloned_fsed;
clone_args.event_queue_depth = 10; // 事件队列的最大长度
clone_args.event_list = events; // 事件屏蔽设置
clone_args.num_events = FSE_MAX_EVENTS; // 事件屏蔽数组的长度
rc = ioctl(fsed, FSEVENTS_CLONE, &clone_args); // 获取克隆的文件描述符
if (rc < 0) { perror ("ioctl"); exit(2); } // 克隆失败, 退出...
close (fsed); // 关闭之前打开的 /dev/fsevents 文件
// 通过一个循环来阻塞式地监听事件
while ((rc = read(cloned_fsed, buf, BUFSIZE)) > 0) {
// rc 即读取到的事件数据总大小(bytes)
int offInBuf = 0;
while (offInBuf < rc) {
struct kfs_event_a *fse = (struct kfs_event_a *)(buf + offInBuf);
struct kfs_event_arg *fse_arg;
struct fse_info *fse_inf;
if (offInBuf) { printf ("Next event: %d\n", offInBuf); };
offInBuf += print_event(buf, offInBuf); // 具体解析
// ...
}
// ...
}
OSX 和 iOS 系统中的安全机制
OSX 上的病毒或恶意软件非常少, 这正是苹果系统的优势. 但实际上更大的原因在于 PC 占的市场份额实在太大了, 恶意软件作者是花同样的时间攻击世界上 90% 的机器还是只选 5% 的 Mac 入侵? 答案一目了然, macOS 或 Linux 只是很少吸引恶意软件作者的关注罢了. 还有一个原因是 UNIX 家族的系统一直都秉持最小特权原则. 不过随着 macOS 的市场占有率逐步上升, 可能这种情况就有所改变了. 另外一些领域专家也在嘲笑苹果的安全机制效率低下且过时.
但实际上, 苹果的应用安全机制要比 windows 的 UAC 要优秀地多. 和安卓相比, iOS 也要牢靠地多. 在 macOS 中的绝大部分所谓的"病毒"实际也是特洛伊木马, 因为它们往往只能利用不知情的用户才能发挥作用.
代码签名(Code Signing)
在软件被认为安全之前, 它的来源必须被验证. 如果一个 APP 是从某个网站下载的, 则它肯定会存在很大安全风险, 但如果软件的来源可以被验证, 并且也可以验证软件在传输过程中是未被修改的, 则可以很大程度地减少安全风险.
代码签名机制就提供了这样的保障. 代码签名使用的是类似访问网站时建立 SSL 连接相同的 X.509v3 证书(连接发起者会使用私钥去对公钥进行签名). 苹果鼓励开发者对自己的应用进行签名并对身份进行认证.
这个过程的关键是签名人的公钥必须被验证方提前知晓. 苹果将自己的证书内嵌到了 macOS 和 iOS 的 keychain 中(和 windows 的做法类似), 并且实际也是唯一的根授权.
可以使用 security
命令导出系统的 keychains, 如下所示:
$ security dump-keychain /Library/Keychains/System.keychain | grep labl
"labl"<blob>="com.apple.systemdefault"
"labl"<blob>="com.apple.systemdefault"
"labl"<blob>="com.apple.kerberos.kdc"
"labl"<blob>="Apple Worldwide Developer Relations Certification Authority"
苹果开发了一个特殊的语言来定义代码签名需求, 可以通过 csreq
命令展示.
另外还有一个 codesign
命令, 允许开发者对它们的 app 进行签名, 并且可以验证/展示当前的签名信息, 如果要签名的话, 就必须有一个合法的证书, 要获取这个证书, 就要加入苹果的开发者计划(99美元一年). 比如展示某个 APP 的签名信息:
$ codesign -dv /Applications/AppCode.app
Executable=/Applications/AppCode.app/Contents/MacOS/appcode
Identifier=com.jetbrains.AppCode
Format=app bundle with Mach-O universal (x86_64 arm64)
CodeDirectory v=20500 size=737 flags=0x10000(runtime) hashes=14+5 location=embedded
Signature size=9063
Timestamp=May 11, 2022 at 3:42:05 AM
Info.plist entries=28
TeamIdentifier=2ZEFAR8TH3
Runtime Version=11.0.0
Sealed Resources version=2 rules=13 files=5312
Internal requirements count=1 size=184
iOS 的 APP 签名是必须的, 而 macOS 上则是可选的. 在 iOS 上未经过代码签名的任何应用都无法执行, 因为它们会直接被内核杀死(前提是奇迹出现的情况下, 能够将未签名的应用放到 iOS 文件系统上...)
沙盒(Sandboxing)
沙盒近年逐渐成为了苹果生态不可或缺的部分, 它的基于一个简单且针对 APP 安全的主要宗旨: 不可信的 APP 必须被隔离运行, 即使用一个完全隔离的环境, 所有的操作都是受限的.
针对某个 APP 而言, 在 iOS 上的"沙盒监狱"主要限制是:
- 无法"突破" APP 自己的目录. 该 APP 只能看到自己的目录(
/var/mobile/Applications/<app-GUID>
), 这个目录被作为根目录. 因此, 该 APP 不可能获知其他已安装的 APP 相关信息, 也无法访问系统文件. - 无法直接访问系统中的任何其他进程, 即便这些进程的 UID 相同. APP 看到的是整个系统中只有自己的进程在运行...
- 无法直接使用任何的硬件设备, 只能通过苹果提供的 Framework 使用.
- 无法动态生成代码, mmap 和 mprotect 系统调用的底层实现在内部进行了修改.
- 只能在用户手机上进行被允许的操作, 且 APP 在 iOS 上(除了苹果自己的 APP 外)根本没有获取 root 权限的机会.
Entitlements 可以让某些 APP 获取稍高一些的自由, 而苹果自己的一些 APP 也的确有 root 权限.
待续...