MACF
和其他现代操作系统一样,Darwin不只依赖DAC,而是通过MAC实现强大的访问控制能力,MACF(Mandatory Access Control Framework)是XNU内核中的强制访问控制框架,它源自 TrustedBSD 项目,Apple将其移植到Darwin/XNU并做了大量定制,它在内核的大量关键位置(包括进程、文件、网络、IPC、Mach 等所有子系统)埋了callout点,另一方面内核扩展开发者可以根据每个callout点注册自己的检查策略,在执行每个受保护的操作前都会顺序调用注册的多个策略,只要有一个策略禁止执行则操作直接返回失败
关键数据结构
struct label — 安全标签
struct label {
struct label **l_owner; // 反向指针,指向持有此 label 的对象,用于一致性验证
long l_perpolicy[MAC_MAX_SLOTS]; // 每个策略独占一个 slot,存储策略私有数据(指针或整数)
};
Label 以只读内存区(ZONE_ID_MAC_LABEL)的 zone 分配,防止被恶意篡改。内核对象持有标签的方式:
- ucred->cr_label — 进程凭证标签
- vnode->v_label — 文件 vnode 标签
- socket->so_label — socket 标签
- mount->mnt_mntlabel — 挂载点标签
struct mac_policy_ops — 策略操作向量,这是 MACF 中最核心的结构,在最新版本91中包含100+ 个函数指针,按对象类型分组:
| 对象类别 | hook 示例 |
|---|---|
| 凭证(cred) | mpo_cred_label_init, mpo_cred_label_associate, mpo_cred_check_label_update_execve |
| vnode | mpo_vnode_check_open, mpo_vnode_check_exec, mpo_vnode_check_write, mpo_vnode_check_rename |
| 进程(proc) | mpo_proc_check_fork, mpo_proc_check_debug, mpo_proc_check_signal, mpo_proc_check_launch_constraints |
| socket | mpo_socket_check_create, mpo_socket_check_connect, mpo_socket_check_bind |
| Mach IPC | mpo_exc_action_check_exception_send, mpo_proc_check_get_task_with_flavor |
| 挂载点(mount) | mpo_mount_check_mount, mpo_mount_check_remount, mpo_mount_check_umount |
| IOKit | mpo_iokit_check_open, mpo_iokit_check_set_properties, mpo_iokit_check_get_property |
| POSIX IPC | mpo_posixsem_check_create, mpo_posixshm_check_mmap |
| SysV IPC | mpo_sysvmsg_*, mpo_sysvsem_*, mpo_sysvshm_* |
| 系统级 | mpo_system_check_reboot, mpo_system_check_kas_info, mpo_kext_check_load |
| 策略生命周期 | mpo_policy_init, mpo_policy_initbsd, mpo_policy_destroy, mpo_policy_syscall |
其Hook命名存在如下规律:
- mpo_<object>_check_<op>:访问控制,返回 0=允许,非0=拒绝(EPERM/EACCES等)
- mpo_<object>_label_<op>:标签生命周期(init/associate/destroy/copy 等),返回 void
- mpo_<object>_notify_<op>:事件通知,返回 void,不影响控制流
- mpo_policy_<op>:策略自身生命周期
struct mac_policy_conf — 策略配置
struct mac_policy_conf {
const char *mpc_name; /** 策略唯一短名(如Sandbox, CHECK, VNG) */
const char *mpc_fullname; /** 描述性全名 */
char const * const *mpc_labelnames; /** 策略管理的label命名空间 */
unsigned int mpc_labelname_count; /** number of managed label namespaces */
const struct mac_policy_ops *mpc_ops; /** 指向mac_policy_ops的指针 */
int mpc_loadtime_flags; /** 加载时行为标志 */
int *mpc_field_off; /** 指向label slot变量的指针(由框架填写分配到的 slot 编号) */
int mpc_runtime_flags; /** 运行时标志(MPC_RUNTIME_FLAG_REGISTERED标志注册成功) */
mpc_t mpc_list; /** List reference */
void *mpc_data; /** module data */
};
mac_policy_list_t 全局策略列表:
struct mac_policy_list {
u_int numloaded;
u_int max;
u_int maxindex;
u_int staticmax; // 静态/动态策略高水位线,低于此值的为静态策略(无需锁),高于为动态策略
u_int chunks;
u_int freehint;
struct mac_policy_list_element *entries; // 策略指针数组,macOS 上可动态扩展(每次增 8 个),其他平台固定大小
};
Hook调用机制
MAC_POLICY_ITERATE用于遍历策略,它分为两段,在静态策略段(0 ~ staticmax-1)是无需任何锁,直接访问,在动态策略段(staticmax ~ maxindex)它通过mac_policy_list_conditional_busy()增加 busy 计数后访问,遍历完后减 busy 计数:
#define MAC_POLICY_ITERATE(...) do { \
struct mac_policy_conf *mpc; \
u_int i; \
\
for (i = 0; i < mac_policy_list.staticmax; i++) { \
mpc = mac_policy_list.entries[i].mpc; \
if (mpc == NULL) \
continue; \
\
__VA_ARGS__ \
} \
if (mac_policy_list_conditional_busy() != 0) { \
for (; i <= mac_policy_list.maxindex; i++) { \
mpc = mac_policy_list.entries[i].mpc; \
if (mpc == NULL) \
continue; \
\
__VA_ARGS__ \
} \
mac_policy_list_unbusy(); \
} \
} while (0)
2.MAC_CHECK 用于权限检查,只有全部策略都允许才通过,只要有一个拒绝则拒绝:
#define MAC_CHECK(check, args...) do { \
error = 0; \
MAC_POLICY_ITERATE({ \
if (mpc->mpc_ops->mpo_ ## check != NULL) { \
MAC_CHECK_CALL(check, mpc); \
int __step_err = mpc->mpc_ops->mpo_ ## check (args); \
MAC_CHECK_RSLT(check, mpc); \
error = mac_error_select(__step_err, error); \
} \
}); \
} while (0) // 初始error=0,任何策略返回错误,则mac_error_select合并,最终有任何错误就拒绝
当多个策略返回不同错误码时,mac_error_select是按语义严重性选择更合适的错误码返回给上层,保证错误信息有意义
3.MAC_GRANT用于授权语义(如特权检查),有一个允许就通过:
#define MAC_GRANT(check, args...) do { \
error = EPERM; \
MAC_POLICY_ITERATE({ \
if (mpc->mpc_ops->mpo_ ## check != NULL) { \
DTRACE_MACF3(mac__call__ ## check, void *, mpc, int, error, int, MAC_ITERATE_GRANT); \
int __step_res = mpc->mpc_ops->mpo_ ## check (args); \
if (__step_res == 0) { \
error = 0; \
} \
DTRACE_MACF2(mac__rslt__ ## check, void *, mpc, int, __step_res); \
} \
}); \
} while (0) // 初始error=EPERM,任何策略返回0则通过
4.MAC_PERFORM 完全不关心返回值,用于标签初始化/销毁/复制等管理操作,以及事件通知:
#define MAC_PERFORM(operation, args...) do { \
MAC_POLICY_ITERATE({ \
if (mpc->mpc_ops->mpo_ ## operation != NULL) { \
MAC_PERFORM_CALL(operation, mpc); \
mpc->mpc_ops->mpo_ ## operation (args); \
MAC_PERFORM_RSLT(operation, mpc); \
} \
}); \
} while (0)
Code Sign
代码签名通过 LC_CODE_SIGNATURE加载命令定位在二进制中,它本质上是一个 linkedit_data_command,指向 __LINKEDIT 段中的签名数据,它最外层是一个 SuperBlob,也是一个索引容器:
typedef struct __SC_SuperBlob {
uint32_t magic; /* magic number */
uint32_t length; /* total length of SuperBlob */
uint32_t count; /* number of index entries following */
CS_BlobIndex index[]; /* (count) entries */
/* followed by Blobs in no particular order as indicated by offsets in index */
} CS_SuperBlob
__attribute__ ((aligned(1)));
每个子blob通过 CS_BlobIndex 的 type(槽位类型)和 offset(相对SuperBlob起始的偏移)来索引:
typedef struct __BlobIndex {
uint32_t type; /* type of entry */
uint32_t offset; /* offset of entry */
} CS_BlobIndex
__attribute__ ((aligned(1)));
核心结构
CodeDirectory
CodeDirectory是整个代码签名的核心,它用哈希链的方式保护了二进制的每个页面,以及所有其他blob:
typedef struct __CodeDirectory {
uint32_t magic; /* magic number (CSMAGIC_CODEDIRECTORY) */
uint32_t length; /* total length of CodeDirectory blob */
uint32_t version; /* CD兼容版本,决定哪些字段有效(0x20100/0x20200/.../0x20600) */
uint32_t flags; /* 与 `CS_ADHOC`、`CS_HARD`、`CS_KILL` 等标志对应 */
uint32_t hashOffset; /* 哈希槽数组中index=0的位置偏移(负数索引是特殊槽) */
uint32_t identOffset; /* 签名标识符(bundle ID / 路径)字符串的偏移 */
uint32_t nSpecialSlots; /* 特殊哈希槽数量(负索引,覆盖 entitlements/requirements 等) */
uint32_t nCodeSlots; /* 代码页哈希槽数量(每个页面一个条目) */
uint32_t codeLimit; /* 被签名覆盖的最大文件偏移,下面还有个for 64位的 */
uint8_t hashSize; /* 每个哈希值的字节数 */
uint8_t hashType; /* 哈希算法(1=SHA1, 2=SHA256, 3=SHA256_Truncated, 4=SHA384) */
uint8_t platform; /* 非零表示这是Apple平台二进制(用于TrustCache查询) */
uint8_t pageSize; /* log2(页大小),通常为 12(4KB)或 14(16KB) */
uint32_t spare2; /* unused (must be zero) */
char end_earliest[0];
/* Version 0x20100 */
uint32_t scatterOffset; /* offset of optional scatter vector */
char end_withScatter[0];
/* Version 0x20200 */
uint32_t teamOffset; /* Team ID 字符串偏移 */
char end_withTeam[0];
/* Version 0x20300 */
uint32_t spare3; /* unused (must be zero) */
uint64_t codeLimit64; /* limit to main image signature range, 64 bits */
char end_withCodeLimit64[0];
/* Version 0x20400 */
uint64_t execSegBase; /* 这几个是可执行段范围和控制标志 */
uint64_t execSegLimit; /* limit of executable segment */
uint64_t execSegFlags; /* executable segment flags */
char end_withExecSeg[0];
/* Version 0x20500 */
uint32_t runtime; /* Hardened Runtime 版本号 */
uint32_t preEncryptOffset;
char end_withPreEncryptOffset[0];
/* Version 0x20600 */
uint8_t linkageHashType;
uint8_t linkageApplicationType;
uint16_t linkageApplicationSubType;
uint32_t linkageOffset;
uint32_t linkageSize;
char end_withLinkage[0];
/* followed by dynamic content as located by offset fields above */
} CS_CodeDirectory
__attribute__ ((aligned(1)));
这里面execSegFlags 的含义如下:
#define CS_EXECSEG_MAIN_BINARY 0x1 /* executable segment denotes main binary */
#define CS_EXECSEG_ALLOW_UNSIGNED 0x10 /* allow unsigned pages (for debugging) */
#define CS_EXECSEG_DEBUGGER 0x20 /* main binary is debugger */
#define CS_EXECSEG_JIT 0x40 /* JIT enabled */
#define CS_EXECSEG_SKIP_LV 0x80 /* OBSOLETE: skip library validation */
#define CS_EXECSEG_CAN_LOAD_CDHASH 0x100 /* can bless cdhash for execution */
#define CS_EXECSEG_CAN_EXEC_CDHASH 0x200 /* can execute blessed cdhash */
哈希槽的内存布局(CodeDirectory 之后):
HashSlot[-nSpecialSlots] ... HashSlot[-1] ← 特殊槽(从 hashOffset 往前算)
HashSlot[0] ... HashSlot[nCodeSlots-1] ← 代码槽(页面哈希)
[identifer string] ← 签名标识符
[teamid string] ← Team ID(v0x20200+)
[scatter vector] ← 可选的 scatter 向量
特殊槽定义(负索引的含义):
slot[-1] = Info.plist 的哈希
slot[-2] = Requirements blob 的哈希
slot[-3] = Resource Directory 的哈希
slot[-4] = Application specific(保留)
slot[-5] = Entitlements XML blob 的哈希
slot[-7] = DER Entitlements blob 的哈希
slot[-8] = Launch Constraint Self 的哈希
slot[-9] = Launch Constraint Parent 的哈希
slot[-10] = Launch Constraint Responsible 的哈希
slot[-11] = Library Constraint 的哈希
CDHash
CDHash是对整个 CodeDirectory blob做哈希,结果截断到20字节,支持三种hash算法,如SHA1/SHA256/SHA384等,从dyld中可以看到这个计算逻辑:
void MachOFile::forEachCDHashOfCodeSignature(const void* codeSigStart, size_t codeSignLen,
void (^callback)(const uint8_t cdHash[20])) const
{
forEachCodeDirectoryBlob(codeSigStart, codeSignLen, ^(const void *cdBuffer) {
const CS_CodeDirectory* cd = (const CS_CodeDirectory*)cdBuffer;
uint32_t cdLength = htonl(cd->length);
uint8_t cdHash[20];
if (cd->hashType == CS_HASHTYPE_SHA384) {
uint8_t digest[CCSHA384_OUTPUT_SIZE];
ccdigest(di, tempBuf, cdLength, cd, digest);
memcpy(cdHash, digest, 20);
callback(cdHash);
内核中CDHash存储在 cs_blob.csb_cdhash[CS_CDHASH_LEN](注意它始终只有前20字节)。
Entitlements
Entitlements即特权,它和Android的应用特权机制类似,可以声明各自有什么特权,在使用相应功能时,功能实现方会去验证使用者是否有该权限,功能实现方包括内核和用户态的各种服务,普通应用只能申请Apple允许的特权或非Apple官方的特权(但没啥意义)。Entitlements blob有两种格式并列存在:
1.XML plist格式是我们开发时用的,易读的格式 (slot 5, magic 0xfade7171):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" ...>
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key><true/>
<key>com.apple.developer.team-identifier</key><string>ABCD1234</string>
<key>application-identifier</key><string>ABCD1234.com.example.app</string>
<key>keychain-access-groups</key>
<array><string>ABCD1234.*</string></array>
<!-- 内核级敏感 entitlements -->
<key>com.apple.private.security.no-sandbox</key><true/>
<key>task_for_pid-allow</key><true/>
<key>get-task-allow</key><true/>
</dict>
</plist>
2.DER 格式 (slot 7, magic 0xfade7172)是相同entitlements的ASN.1/DER编码版本,用于 CoreEntitlements 框架的高效查询
两种格式的哈希都记录在 CodeDirectory 对应的特殊槽中,内核验证时会检查blob内容的哈希是否与槽中记录的一致:
int csblob_get_entitlements(struct cs_blob *csblob, void **out_start, size_t *out_length)
{
const CS_GenericBlob *entitlements = csblob->csb_entitlements_blob;
// 查找特殊槽中的期望哈希
embedded_hash = find_special_slot(code_dir, csblob->csb_hashtype->cs_size, CSSLOT_ENTITLEMENTS);
// 对实际 blob 内容做哈希
csblob->csb_hashtype->cs_init(&context);
csblob->csb_hashtype->cs_update(&context, entitlements, ntohl(entitlements->length));
csblob->csb_hashtype->cs_final(computed_hash, &context);
// 比较
if (memcmp(computed_hash, embedded_hash, csblob->csb_hashtype->cs_size) != 0) {
return EBADEXEC;
}
Requirements
Requirements用于限制应用的执行环境,比如只能以某个特定应用的子进程方式启动,Requirements blob (magic 0xfade0c01) 是一组命名的要求集合,采用前缀表达式(prefix expression)字节码编码,分为以下几种 RequirementType:
| 类型 | 含义 |
|---|---|
kSecHostRequirementType (1) |
宿主进程必须满足的要求 |
kSecGuestRequirementType (2) |
guest 代码必须满足的要求 |
kSecDesignatedRequirementType (3) |
该代码自己的设计要求(DR),用于第三方校验 |
kSecLibraryRequirementType (4) |
对加载的库的要求 |
kSecPluginRequirementType (5) |
对插件的要求 |
常见的 Requirement 表达式操作码:
// libsecurity_codesigning/lib/requirement.h
opFalse, // 永远为 false
opTrue, // 永远为 true
opIdent, // identifier == "xxx"
opAppleAnchor, // anchor apple(由 Apple 根证书签名)
opAnchorHash, // anchor 的 SHA1 = [hash]
opInfoKeyValue, // info["key"] == "value"
opAnd, // expr AND expr
opOr, // expr OR expr
opCDHash, // cdhash == [hash]
opNot, // NOT expr
opInfoKeyField, // info["key"] ~= pattern
opEntitlementField, // entitlement["key"] ~= pattern
opCertField, // cert[N][field] ~= pattern
opTrustedCert, // cert[N] trusted
opTrustedCerts, // anchor trusted
opCertGeneric, // cert[N][OID] ~= pattern
opAppleGenericAnchor, // anchor apple generic
opPlatform, // platform == N
比如典型的Developer ID应用的DR:
anchor apple generic
and certificate leaf[field.1.2.840.113635.100.6.1.9] exists
CMS
这是传统的 X.509/CMS 签名(magic 0xfade0b01),它包含签名者证书链(从叶证书到Apple Root CA),对CodeDirectory内容的数字签名(RSA 或 ECDSA)和签名的时间戳。Ad-hoc签名(-s -)没有这个blob,其CDHash直接受信或经由TrustCache验证
Launch Constraint
CSMAGIC_EMBEDDED_LAUNCH_CONSTRAINT = 0xfade8181, /* Light weight code requirement */
macOS 13+ 引入,包含在 slot 8/9/10/11 中,用于约束进程的启动上下文(自身签名要求、父进程要求、负责任进程要求、库约束),比 Requirements 更轻量。
代码签名标志
| 标志 | 值 | 含义 | 典型来源 |
|---|---|---|---|
CS_VALID |
0x00000001 | 进程代码签名当前有效 | 成功加载签名后设置 |
CS_ADHOC |
0x00000002 | Ad-hoc 签名(无证书,仅 CDHash) | codesign -s - |
CS_GET_TASK_ALLOW |
0x00000004 | 允许其他进程获取此进程的 task port(调试器用) | entitlement |
CS_INSTALLER |
0x00000008 | 有installer特权 | |
CS_FORCED_LV |
0x00000010 | 加固系统策略需要库验证 | |
CS_INVALID_ALLOWED |
0x00000020 | 只在macOS上有,任务端口允许页无效 | |
CS_HARD |
0x00000100 | 拒绝映射未签名或无效页面,只是返回失败 | iOS 始终开启 |
CS_KILL |
0x00000200 | 页面验证失败时SIGKILL进程 | iOS 始终开启 |
CS_CHECK_EXPIRATION |
0x00000400 | 需要做有效期检查 | |
CS_RESTRICT |
0x00000800 | dyld 受限模式(禁止 DYLD_* 环境变量等) | CS_RESTRICT flag in CD |
CS_ENFORCEMENT |
0x00001000 | 强制代码签名检查 | iOS 始终;macOS 按进程 |
CS_REQUIRE_LV |
0x00002000 | 所加载的库必须通过Library Validation,见验证流程 | Hardened Runtime 启用时 |
CS_ENTITLEMENTS_VALIDATED |
0x00004000 | ||
CS_RUNTIME |
Hardened Runtime 模式 | --options runtime |
|
CS_PLATFORM_BINARY |
是 Apple 平台二进制(来自 TrustCache) | AMFI 查询 TrustCache | |
CS_DEBUGGED |
曾被调试器附加,允许无效页面 | cs_allow_invalid() |
验证过程
execve 阶段:完整签名验证
调用链:
execve(2)
└── exec_activate_image() [kern_exec.c]
└── exec_mach_imgact()
└── parse_machfile() [mach_loader.c]
└── 遍历 load commands
└── LC_CODE_SIGNATURE → load_code_signature()
└── ubc_cs_blob_add()
├── cs_blob_init_validated() ← 结构完整性检查
├── reconstitute_code_signature()
├── register_code_signature_monitor() ← CSM(PPL/TXM)
├── validate_main_binary_check()
├── mac_vnode_check_signature() ← MACF → AMFI
├── reconstitute_code_signature_2nd_stage()
├── setup_multilevel_hashing()
├── accelerate_entitlement_queries()
└── csblob_parse_teamid()
└── process_signature() [kern_exec.c]
├── proc_csflags_update() ← 将 csflags 写入 proc
├── vm_map_switch_protect() ← CS_HARD/CS_KILL → map 保护
├── vm_map_cs_enforcement_set() ← CS_ENFORCEMENT → VM map
└── mac_proc_check_launch_constraints() ← Launch Constraints
ubc_cs_blob_add() 内部详细步骤:
/*
* Create the struct cs_blob abstract data type which will get attached to
* the vnode object. This function also validates the structural integrity
* of the code signature blob being passed in.
*
* We initialize a temporary blob whose contents are then copied into an RO
* blob which we allocate from the read-only allocator.
*/
error = cs_blob_init_validated(addr, size, &tmp_blob, &cd);
① cs_blob_init_validated:结构完整性验证
- 检查 magic number 是否为 CSMAGIC_EMBEDDED_SIGNATURE
- 验证 SuperBlob 内各 BlobIndex 的 offset/length 不越界
- 找到最佳 CodeDirectory(优先 SHA256/SHA384)
- 计算并存储 CDHash
② mac_vnode_check_signature (MACF → AMFI):这是最关键的验证步骤:
```4795:4811:xnu/bsd/kern/ubc_subr.c
if CONFIG_MACF
unsigned int cs_flags = tmp_blob.csb_flags;
unsigned int signer_type = tmp_blob.csb_signer_type;
error = mac_vnode_check_signature(
vp,
&tmp_blob,
imgp,
&cs_flags,
&signer_type,
flags,
platform);
AMFI(`AppleMobileFileIntegrity.kext`)作为 MAC policy 被调用,执行:
- 查询 **TrustCache**:CDHash 是否在 Apple 维护的信任缓存中(平台二进制)
- 验证 **CMS 签名**:使用 CoreTrust 验证证书链和对 CodeDirectory 的签名
- 验证 **entitlements**:检查 entitlements 是否被该 Team ID 允许
- 设置 `csb_platform_binary`、`csb_signer_type` 等字段
③ **代码签名监控器(Code Signing Monitor)**:
在支持 PPL(Page Protection Layer,arm64e A12+)或 TXM(Trusted Execution Monitor,SPTM 架构)的设备上:
```4763:4781:xnu/bsd/kern/ubc_subr.c
#if CODE_SIGNING_MONITOR
error = register_code_signature_monitor(
vp,
&tmp_blob,
(vm_offset_t)tmp_blob.csb_cd - (vm_offset_t)tmp_blob.csb_mem_kaddr);
签名数据会被锁定到由 PPL/TXM 控制的内存页中,即使内核被攻破也无法篡改。
运行时(Page Fault)阶段:页面级签名验证
调用链:
访问可执行内存页 → Page Fault
└── vm_fault() [vm_fault.c]
└── vm_fault_enter()
└── vm_page_validate_cs()
└── vm_page_validate_cs_mapped_slow()
└── cs_validate_page() [ubc_subr.c]
└── cs_validate_hash() ← 核心哈希检查
├── 从 CodeDirectory 的代码槽找到页面对应的期望哈希
└── 对页面数据计算实际哈希
└── 比较,不一致 → VMP_CS_TAINTED
└── 如果 cs_violation == TRUE:
└── cs_invalid_page() [kern_cs.c]
├── CS_KILL → SIGKILL
├── CS_HARD → 拒绝映射(返回错误)
└── 否则 → 清除 CS_VALID(允许运行但标记无效)
cs_validate_hash() 的核心逻辑:
```5773:5931:xnu/bsd/kern/ubc_subr.c static boolean_t cs_validate_hash( struct cs_blob blobs, memory_object_t pager, memory_object_offset_t page_offset, const void data, vm_size_t bytes_processed, unsigned tainted) { // ... for (blob = blobs; blob != NULL; blob = blob->csb_next) { // 找到覆盖此页面偏移的 blob offset = page_offset - blob->csb_base_offset; // ... cd = blob->csb_cd; // 根据页偏移计算槽索引,取出期望哈希 hash = hashes(cd, (uint32_t)(offset >> blob->csb_hash_pageshift), hashtype->cs_size, lower_bound, upper_bound); } // 对页面数据计算实际哈希并比较 hashtype->cs_init(&mdctx); hashtype->cs_update(&mdctx, data, size); hashtype->cs_final(actual_hash, &mdctx);
if (bcmp(expected_hash, actual_hash, hashtype->cs_size) != 0) {
*tainted |= CS_VALIDATE_TAINTED; // 标记为污染
}
`cs_invalid_page()` 的处理策略:
```248:296:xnu/bsd/kern/kern_cs.c
int
cs_invalid_page(addr64_t vaddr, boolean_t *cs_killed)
{
// ...
flags = proc_getcsflags(p);
/* CS_KILL triggers a kill signal, and no you can't have the page. Nothing else. */
if (flags & CS_KILL) {
flags |= CS_KILLED;
cs_procs_killed++;
send_kill = 1;
retval = 1;
}
/* CS_HARD means fail the mapping operation so the process stays valid. */
if (flags & CS_HARD) {
retval = 1;
proc_csflags_update(p, flags);
} else {
if (flags & CS_VALID) {
flags &= ~CS_VALID;
cs_procs_invalidated++;
dylib 加载阶段:Library Validation
当进程有 CS_REQUIRE_LV(Hardened Runtime 开启,或 com.apple.security.cs.require-lv),dyld在 dlopen/链接时会通过 F_ADDFILESIGS 向内核注册库的签名,AMFI会额外检查:
- 库的 Team ID 必须与主程序一致,或
- 库必须是 Apple 平台二进制
```37:38:xnu/bsd/sys/codesign.h
define CLEAR_LV_ENTITLEMENT "com.apple.private.security.clear-library-validation"
define OVERRIDE_PLUGIN_HOST_ENTITLEMENT "com.apple.private.security.override-plugin-host-detection"
#### 动态库注入的拦截(CS_RESTRICT)
```47:47:xnu/osfmk/kern/cs_blobs.h
#define CS_RESTRICT 0x00000800 /* tell dyld to treat restricted */
当进程设置了 CS_RESTRICT,dyld 会:
- 忽略 DYLD_INSERT_LIBRARIES、DYLD_LIBRARY_PATH 等环境变量
- 阻止向进程注入非必要的动态库
这是对 dyld 层面注入攻击的第一道防线。
下面是完整的验证流程图:
用户态 内核态
┌────────────┐
execve(binary) ──→│ kern_exec │
└─────┬──────┘
│ parse Mach-O
┌─────▼──────────────────────────────────────────┐
│ load_code_signature() │
│ 1. 从 __LINKEDIT 读取签名数据到内核内存 │
│ 2. ubc_cs_blob_add() │
│ ├── cs_blob_init_validated() │
│ │ 结构检查 + 计算 CDHash │
│ ├── register_code_signature_monitor() │
│ │ [PPL/TXM] 锁定签名数据 │
│ ├── mac_vnode_check_signature() │
│ │ └── AMFI.kext │
│ │ ├── 查 TrustCache → platform? │
│ │ ├── CoreTrust 验证 CMS 签名 │
│ │ ├── 验证 entitlements 合法性 │
│ │ └── 设置 signer_type │
│ └── 将 cs_blob 挂载到 vnode │
└─────┬───────────────────────────────────────────┘
│ 成功后
┌─────▼──────────────────┐
│ process_signature() │
│ - 写入进程 csflags │
│ - 配置 VM map 保护 │
│ - 检查 Launch Constraints│
└─────┬──────────────────┘
│
┌─────▼──────────────┐
│ 进程运行中 │
└─────┬──────────────┘
│ 访问代码页
┌─────▼───────────────────────────────────────┐
│ Page Fault → vm_fault_enter() │
│ vm_page_validate_cs_mapped_slow() │
│ └── cs_validate_page() │
│ └── cs_validate_hash() │
│ 取 CD 中代码槽的期望哈希 │
│ 对实际页面数据计算哈希 │
│ 不匹配 → VMP_CS_TAINTED │
│ │
│ cs_invalid_page() │
│ ├── CS_KILL → SIGKILL(iOS 默认) │
│ ├── CS_HARD → 拒绝 mmap(返回 EACCES) │
│ └── 否则 → 清除 CS_VALID(降级运行) │
└─────────────────────────────────────────────┘
相关工具
AMFI
TXM
三种实现方式对应不同安全级别:
| 模式 | 文件 | 适用 | 安全级别 |
|---|---|---|---|
xnu.c |
无监控器 | 旧硬件/非安全内核 | 内核 R/W 可绕过 |
ppl.c (PMAP_CS) |
PPL 保护 | A12+ arm64e | PPL 环境保护 cs_blob |
txm.c (SPTM/TXM) |
TXM 保护 | A17/M3+ | 硬件级隔离保护 |
#if !CODE_SIGNING_MONITOR
/*
* We don't have a monitor environment available. This means someone with a kernel
* memory exploit will be able to corrupt code signing state. There is not much we
* can do here, since this is older HW.
*/
PPL/TXM 模式下,cs_blob 结构存放在只有监控器可写的内存页中,内核本身只有只读权限,大幅提升了抵御内核漏洞利用的能力。
Sandbox
SIP
System Integrity Protection(系统完整性保护)是一种依赖于MACF/Sandbox实现的安全机制,它进一步限制即使有ROOT权限也不能修改受保护的系统资源,包括改文件和内存等,它的整体架构如下:
┌─────────────────────────────────────────────────────────────┐
│ 用户空间 │
│ csrutil / csrctl / csr_check() / csr_get_active_config() │
│ ↓ syscall #483 (csrctl) │
├─────────────────────────────────────────────────────────────┤
│ XNU 内核 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ kern_csr.c(CSR核心) │ │
│ │ csr_bootstrap() → csr_check() → csr_get_active() │ │
│ └───────────────────────┬─────────────────────────────┘ │
│ │ csr_check(CSR_ALLOW_XXX) │
│ ┌─────────────────┼─────────────────────┐ │
│ ▼ ▼ ▼ │
│ 文件系统保护 Mach IPC 保护 调试保护 │
│ (SF_RESTRICTED) (task_for_pid/port) (kernel debugger) │
│ │ │ │ │
│ VFS层继承/检查 host_set_special_port debug.c │
│ vfs_subr.c ipc_tt.c panic_init() │
│ │
│ DTrace 保护 NVRAM 保护 Kext 保护 恢复OS 保护 │
│ dtrace_subr.c IONVRAM.cpp OSKext.cpp chunklist.c │
└─────────────────────────────────────────────────────────────┘
↑
┌──────────────────────┐
│ 配置来源 │
│ ARM: Device Tree │
│ /chosen/asmb/lp-sip0 │
│ x86: boot_args │
│ csrActiveConfig │
└──────────────────────┘
可以看到这里面有个很重要的概念,CSR(Configurable Security Restrictions,可配置安全限制)
CSR 配置类型
/*
* Copyright (c) 2014 Apple Inc. All rights reserved.
csr_config_t 是一个 32 位无符号整数,每个 bit 代表一项限制是否被放开:
| 标志位 | 位值 | 含义 |
|---|---|---|
CSR_ALLOW_UNTRUSTED_KEXTS |
1<<0 |
允许加载未签名的内核扩展 |
CSR_ALLOW_UNRESTRICTED_FS |
1<<1 |
允许无限制访问文件系统(含受保护目录) |
CSR_ALLOW_TASK_FOR_PID |
1<<2 |
允许 task_for_pid() 访问任意进程 |
CSR_ALLOW_KERNEL_DEBUGGER |
1<<3 |
允许内核调试器连接 |
CSR_ALLOW_APPLE_INTERNAL |
1<<4 |
开启 Apple 内部特性(IUOU/IUOS 设备专用) |
CSR_ALLOW_UNRESTRICTED_DTRACE |
1<<5 |
允许 DTrace 无限制使用(含破坏性探测器) |
CSR_ALLOW_UNRESTRICTED_NVRAM |
1<<6 |
允许无限制读写 NVRAM |
CSR_ALLOW_DEVICE_CONFIGURATION |
1<<7 |
允许设备配置模式 |
CSR_ALLOW_ANY_RECOVERY_OS |
1<<8 |
允许启动任意恢复系统(跳过签名校验) |
CSR_ALLOW_UNAPPROVED_KEXTS |
1<<9 |
允许未经用户审批的 Kext |
CSR_ALLOW_EXECUTABLE_POLICY_OVERRIDE |
1<<10 |
允许覆盖可执行文件策略 |
CSR_ALLOW_UNAUTHENTICATED_ROOT |
1<<11 |
允许未认证的根卷(SSV 签名绕过) |
CSR_ALLOW_RESEARCH_GUESTS |
1<<12 |
允许研究型来宾系统 |
重要标志组:
#define CSR_VALID_FLAGS (CSR_ALLOW_UNTRUSTED_KEXTS | \
CSR_ALLOW_UNRESTRICTED_FS | \
CSR_ALLOW_TASK_FOR_PID | \
CSR_ALLOW_KERNEL_DEBUGGER | \
CSR_ALLOW_APPLE_INTERNAL | \
CSR_ALLOW_UNRESTRICTED_DTRACE | \
CSR_ALLOW_UNRESTRICTED_NVRAM | \
CSR_ALLOW_DEVICE_CONFIGURATION | \
CSR_ALLOW_ANY_RECOVERY_OS | \
CSR_ALLOW_UNAPPROVED_KEXTS | \
CSR_ALLOW_EXECUTABLE_POLICY_OVERRIDE | \
CSR_ALLOW_UNAUTHENTICATED_ROOT | \
CSR_ALLOW_RESEARCH_GUESTS)
#define CSR_ALWAYS_ENFORCED_FLAGS (CSR_ALLOW_DEVICE_CONFIGURATION | CSR_ALLOW_ANY_RECOVERY_OS)
/* Flags set by `csrutil disable`. */
#define CSR_DISABLE_FLAGS (CSR_ALLOW_UNTRUSTED_KEXTS | \
CSR_ALLOW_UNRESTRICTED_FS | \
CSR_ALLOW_TASK_FOR_PID | \
CSR_ALLOW_KERNEL_DEBUGGER | \
CSR_ALLOW_APPLE_INTERNAL | \
CSR_ALLOW_UNRESTRICTED_DTRACE | \
CSR_ALLOW_UNRESTRICTED_NVRAM)
CSR_ALWAYS_ENFORCED_FLAGS:无论 SIP 是否关闭,DEVICE_CONFIGURATION和ANY_RECOVERY_OS始终单独受控,不受csrutil disable影响CSR_DISABLE_FLAGS:执行csrutil disable时设置的 7 个标志
初始化流程
极早期启动 —— csr_bootstrap()
CSR 通过 STARTUP(TUNABLES, STARTUP_RANK_FIRST, csr_bootstrap) 注册,在内核最早期阶段(甚至在 VM/PMAP 初始化之前,在 panic_init() 之前)运行:
__startup_func
static void
csr_bootstrap(void)
{
DTEntry entry;
uint64_t uint64_value;
bool config_active = false;
bool bool_value;
csr_config = 0; // start out fully restrictive
if (SecureDTLookupEntry(0, "/chosen/asmb", &entry) == kSuccess &&
_csr_get_dt_uint64(&entry, "lp-sip0", &uint64_value)) {
csr_config = (uint32_t)uint64_value; // Currently only 32 bits used.
config_active = true;
}
// ...
}
STARTUP(TUNABLES, STARTUP_RANK_FIRST, csr_bootstrap);
为什么必须极早初始化?
因为 panic_init() 在 STARTUP_RANK_MIDDLE 运行,而 panic_init() 内部会调用 csr_check() 来判断是否允许内核调试——所以 CSR 必须在此之前初始化完成。
两种平台的初始化路径
ARM 平台(新式,CONFIG_CSR_FROM_DT)— 基于 Device Tree
启动流程:
BootROM → iBoot → Device Tree 构建
└─ /chosen/asmb/lp-sip0 → CSR 配置 (32位)
└─ /chosen/asmb/lp-sip1 → UNAUTHENTICATED_ROOT 标志
└─ /chosen/osenvironment → 环境检测
└─ /chosen/internal-use-only-unit → IUOU 设备检测
核心逻辑:
csr_config = 0; // 默认全部限制
// 1. 从 DT 读取配置
if (SecureDTLookupEntry(0, "/chosen/asmb", &entry) == kSuccess &&
_csr_get_dt_uint64(&entry, "lp-sip0", &uint64_value)) {
csr_config = (uint32_t)uint64_value;
config_active = true;
}
// 2. IUOU/IUOS 设备才允许 APPLE_INTERNAL,否则强制清除
if (!_csr_is_iuou_or_iuos_device()) {
csr_config &= ~CSR_ALLOW_APPLE_INTERNAL;
}
// 3. 恢复环境/还原环境/DarwinOS ramdisk 自动放开文件系统
if (_csr_is_recovery_environment() || _csr_is_restore_environment()
|| _csr_is_darwinos_ramdisk()) {
csr_config |= CSR_ALLOW_UNRESTRICTED_FS;
}
// 4. UNAUTHENTICATED_ROOT 单独由 lp-sip1 控制
if (_csr_get_dt_bool(&entry, "lp-sip1", &bool_value) && bool_value) {
csr_config |= CSR_ALLOW_UNAUTHENTICATED_ROOT;
}
x86 平台(旧式)— 基于 NVRAM/Boot Args
// csr_bootstrap: 检查是否从特殊 booter (BaseSystem) 启动
boot_args *args = (boot_args *)PE_state.bootArgs;
if (args->flags & kBootArgsFlagCSRBoot) {
csr_allow_all = 1; // 允许所有
}
// csr_get_active_config: 从 boot_args 读取 csrActiveConfig
if (args->flags & kBootArgsFlagCSRActiveConfig) {
*config = args->csrActiveConfig & CSR_VALID_FLAGS;
}
x86 上 SIP 配置存储在 NVRAM 的 csr-active-config 变量中,由 iBoot 读取后写入 boot_args.csrActiveConfig。
配置的不可篡改性
csr_config 变量被声明为:
static SECURITY_READ_ONLY_LATE(csr_config_t) csr_config = 0;
SECURITY_READ_ONLY_LATE 宏将其放入只读段,在内核启动完成后该内存区域会被硬件(KTRR/CTRR)锁定为只读,内核启动后任何代码(包括 root 进程)都无法修改。
保护机制
➜ ~ csrutil status
System Integrity Protection status: unknown (Custom Configuration).
Configuration:
Apple Internal: disabled
Kext Signing: enabled
Filesystem Protections: enabled
Debugging Restrictions: disabled
DTrace Restrictions: enabled
NVRAM Protections: enabled
BaseSystem Verification: enabled
Boot-arg Restrictions: enabled
Kernel Integrity Protections: enabled
Authenticated Root Requirement: enabled
This is an unsupported configuration, likely to break in the future and leave your machine in an unknown state.
文件系统保护(SF_RESTRICTED 标志机制)
SIP 的文件系统保护不是通过路径黑名单实现的,而是通过vnode的 SF_RESTRICTED 系统标志(0x00080000)实现。
标志继承机制(在 vfs_subr.c 中):
/* Inherit SF_RESTRICTED bit from destination directory only */
if (VATTR_IS_ACTIVE(vap, va_flags)) {
VATTR_SET(vap, va_flags,
((vap->va_flags & ~(UF_DATAVAULT | SF_RESTRICTED)))); /* Turn off from source */
if (VATTR_IS_ACTIVE(dvap, va_flags)) {
VATTR_SET(vap, va_flags,
vap->va_flags | (dvap->va_flags & (UF_DATAVAULT | SF_RESTRICTED)));
}
- 新建文件时,自动从父目录继承
SF_RESTRICTED标志 - 即使拷贝文件,源文件的
SF_RESTRICTED也会被清除,由目标目录决定是否添加
移动/重命名时的标志传播(kpi_vfs.c):
/*
* If moved to a new directory that is restricted,
* set the restricted flag on the item moved.
*/
if (_err == 0) {
_err = vnode_flags(tdvp, &tdfflags, ctx);
if (_err == 0) {
uint32_t inherit_flags = tdfflags & (UF_DATAVAULT | SF_RESTRICTED);
if (inherit_flags) {
// Apply restricted flag to moved item
将文件移入受保护目录时,自动继承 SF_RESTRICTED。
共享缓存映射保护(vm_unix.c):
在SIP开启时,只有带 SF_RESTRICTED 标志的文件才能被映射到共享区域,防止攻击者替换dyld shared cache。
Kext Cache加载保护(OSKext.cpp):
SIP开启时,Kernel Collection路径必须具有 SF_RESTRICTED 标志,否则拒绝加载。
内核调试保护
boolean_t
kernel_debugging_restricted(void)
{
#if XNU_TARGET_OS_OSX
#if CONFIG_CSR
if (csr_check(CSR_ALLOW_KERNEL_DEBUGGER) != 0) {
return TRUE;
}
#endif /* CONFIG_CSR */
return FALSE;
#else
return FALSE;
#endif
}
注:在 iOS/tvOS 等平台上,
kernel_debugging_restricted()始终返回 FALSE,因为这些平台有更底层的安全机制(如 KTRR/SPTM)而非依赖 SIP。
DTrace保护
SIP开启时DTrace功能受限:
- dtrace_is_restricted() 返回 true → 禁用破坏性(destructive)探测器
- dtrace_are_restrictions_relaxed() 检查 CSR_ALLOW_APPLE_INTERNAL → 内部开发设备可适度放宽
还影响 sysctl 读取进程环境变量(kern_sysctl.c):SIP 启用且不是受限 DTrace 时,外部进程无法通过 sysctl KERN_PROCARGS2 读取目标进程的环境变量。
Mach IPC保护
switch (which) {
case THREAD_KERNEL_PORT:
#if CONFIG_CSR
if (csr_check(CSR_ALLOW_KERNEL_DEBUGGER) == 0) {
// 只有 SIP 禁用时(Mach-on-Mach 仿真场景),
// 才允许用户态设置线程的 kernel special port
...
return result;
}
#endif
return KERN_NO_ACCESS;
保护范围:
- thread_set_special_port(线程kernel port):SIP 开启时禁止用户态设置
- task_set_special_port(task kernel/host port):SIP 开启时禁止用户态设置
- host_set_special_port:非内核任务、非 init 进程设置时需要 CSR_ALLOW_TASK_FOR_PID
- Exception Port 保护:SIP 开启时,异常端口的身份保护策略强制生效
- mach_port_construct(Provisional Reply Port):SIP 开启时记录遥测数据
NVRAM 保护
IONVRAM.cpp 初始化时检查 CSR_ALLOW_APPLE_INTERNAL,决定是否允许内部构建特权操作。NVRAM 变量保护还与代码签名标志 CS_NVRAM_UNRESTRICTED(com.apple.rootless.restricted-nvram-variables.heritable entitlement)结合使用。
恢复 OS 签名保护
#if CONFIG_CSR
if (err && (csr_check(CSR_ALLOW_ANY_RECOVERY_OS) == 0)) {
AUTHPRNT("CSR_ALLOW_ANY_RECOVERY_OS set, allowing unauthenticated root image");
err = 0;
enforced = FALSE;
}
#endif
启动 Recovery OS 时,内核会验证根镜像的 chunklist 签名。若 CSR_ALLOW_ANY_RECOVERY_OS 被设置(在配置模式下),则跳过签名校验——这允许 csrutil 工具本身在 Recovery OS 中运行。
平台二进制位保护
case CS_OPS_CLEARPLATFORM:
// 只有在 DEVELOPMENT/DEBUG 构建 + Apple Internal SIP 放开时
// 才能清除进程的 CS_PLATFORM_BINARY 标志
if (csr_check(CSR_ALLOW_APPLE_INTERNAL) != 0) {
error = ENOTSUP;
break;
}