*OS的安全机制-上

Published: 2024年12月18日

In Kernel.

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调用机制

  1. 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_BlobIndextype(槽位类型)和 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 IDv0x20200+
[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()  CSMPPL/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_LIBRARIESDYLD_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   SIGKILLiOS 默认)          
                     ├── 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_CONFIGURATIONANY_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_UNRESTRICTEDcom.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;
    }

social