乐于分享
好东西不私藏

iOS 26.4 如何限制系统进程使用 JavaScriptCore

iOS 26.4 如何限制系统进程使用 JavaScriptCore

背景

JavaScriptCore 是 Apple 系统自带的 js 引擎。它不只服务于 Safari 和 WebKit,也可以被原生程序通过链接 JavaScriptCore.framework 直接调用执行 js。

原本正常的系统框架,攻击者却可以把 JavaScriptCore 当成系统内置解释器,通过代码注入技术把后利用逻辑寄生在已有进程里。通过脚本访问原生接口,不需要下载可执行文件,也不会产生新的进程。

这种借助系统自带脚本解释器运行代码的思路并不是新鲜事。哪怕只看 iOS 平台的公开研究,Google Project Zero 在 2019 年演示 iMessage 零点击利用的时候就已经用过了。

DarkSword 是比较近的现实案例。2026 年 3 月,iVerify、Google TAG 和 Lookout 联合披露了 DarkSword 工具包针对 iOS 用户大批量发起攻击的事件。它在完成沙箱逃逸和系统提权之后,并不启动独立 payload 进程,而是借用系统中已经存在的服务,在其中运行基于 JavaScriptCore 的代码来实现 C2 逻辑和提取系统敏感信息。甚至提权利用本身也是用 js 实现的。

所谓的无文件代码执行大体步骤如下:

  • 使用 dlopen 动态链接 JavaScriptCore.framework
  • 调用 JSContext 创建 js 执行环境
  • 绑定系统 native API(bridge)和内存读写能力到上下文
  • 运行 js

上面的步骤在浏览器、混合式应用或者需要脚本能力的系统组件里并不奇怪。但对某些关键系统进程(比如用户态核心的 launchd)来说,如果它们开始运行 js,那肯定是非预期行为。苹果的思路就是在预设的系统进程列表当中彻底禁用 JavaScriptCore。

根据 WebKit 的历史记录来看,在 2025 年 8 月的提交 d5e7d2a3eeeeab55e93553b2fc91fc61327a6ffb 就引入了针对性的防御,应该是早于 DarkSword 活动的时间。而它恰好是 Apple 对这种滥用解释器的利用技术的防御,应该是早已设计好的加固能力的一部分,只是到 26.4 才正式发布。

本文就来分析一下这个防御机制的具体实现。

新的 Entitlement

从 iOS 26.4 开始,launchd 以及若干高风险进程的 entitlement 就多了一个 com.apple.security.script-restrictions

注:entitlement 是苹果操作系统的机制。将一段 property list——键值对数据,与代码签名绑定后保存在可执行文件中,用来为操作系统标记特殊的权限

这些进程多与缩略图生成和 BlastDoor 有关

在苹果官方文档当中已经提到了如下 entitlement 可以启用包括 MTE 在内的多种加固措施,甚至对第三方开发者也开放了:

  • com.apple.security.hardened-process
  • com.apple.security.hardened-process.enhanced-security-version-string
  • com.apple.security.hardened-process.checked-allocations
  • com.apple.security.hardened-process.platform-restrictions-string
  • com.apple.security.hardened-process.dyld-ro

它们可以单独展开写各自的文章,篇幅限制就不深入了。

这个 script-restrictions 并没有出现在文档中。尝试在 Xcode iOS 工程中添加,构建之后会被悄悄移除;如果手工调用 codesign 命令签名 app 加上,安装过程会被真机拒绝。

所以目前这个机制不仅文档没有写,也不对第三方 app 开发者开放。不开放就算了,这个加固对第三方应用确实没任何作用,反而不少开发者都喜欢业务逻辑用 js 写,一套代码跑多端。禁了不是给自己找不快。

来实测验证一下。虽然 iOS 不让测试 app 用这个 entitlement,但是 macOS 26.5 没问题。

test_jsc.c

#include<stdio.h>#include<stdint.h>#include<dlfcn.h>intmain(void){    uint64_t (*os_security_config_get)(void) =        (uint64_t (*)(void))dlsym(RTLD_DEFAULT, "os_security_config_get");    if (!os_security_config_get) {        fprintf(stderr, "system too old\n");        return 1;    }    uint64_t cfg = os_security_config_get();    printf("os_security_config_get() = 0x%llx  | SCRIPT_RESTRICTIONS(0x40)=%s"            "  HARDENED_HEAP(0x1)=%s TPRO(0x2)=%s GUARD_OBJECTS(0x100)=%s\n",            (unsigned long long)cfg,            (cfg & 0x40)  ? "ON"  : "off",            (cfg & 0x1)   ? "on"  : "off",            (cfg & 0x2)   ? "on"  : "off",            (cfg & 0x100) ? "on"  : "off");    void *jsc = dlopen("/System/Library/Frameworks/JavaScriptCore.framework/JavaScriptCore", RTLD_NOW);    if (!jsc) { printf("dlopen(JavaScriptCore) FAILED: %s\n"dlerror()); return 2; }    void* (*JSGlobalContextCreate)(void*)               = dlsym(jsc, "JSGlobalContextCreate");    void* (*JSStringCreateWithUTF8CString)(const char*) = dlsym(jsc, "JSStringCreateWithUTF8CString");    void* (*JSEvaluateScript)(void*,void*,void*,void*,int,void**) = dlsym(jsc, "JSEvaluateScript");    double (*JSValueToNumber)(void*,void*,void**)       = dlsym(jsc, "JSValueToNumber");    printf("dlopen JSC=%p  JSGlobalContextCreate=%p\n", jsc, (void*)JSGlobalContextCreate);    void *ctx = JSGlobalContextCreate(NULL);            // <- traps here under restrictions    printf("JSGlobalContextCreate -> %p\n", ctx);    void *src = JSStringCreateWithUTF8CString("40 + 2");    void *exc = NULL;    void *res = JSEvaluateScript(ctx, src, NULLNULL0, &exc);    double out = JSValueToNumber(ctx, res, NULL);    printf("40 + 2 = %g\n", out);    return 0;}

ent.plist

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plistPUBLIC"-//Apple//DTD PLIST 1.0//EN""http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plistversion="1.0"><dict>    <key>com.apple.security.script-restrictions</key>    <true/></dict></plist>

编译后加上自签名的 entitlement:

cc test_jsc.c -o test_jsccodesign -s - --entitlements ent.plist --force test_jsc

预期的行为是进程会抛异常,所以挂 lldb 跑:

➜  jsc_test lldb test_jsc(lldb) target create "test_jsc"Current executable set to '/Users/cc/Projects/phrack/jsc_test/test_jsc' (arm64).(lldb) rProcess 49313 launched: '/Users/cc/Projects/phrack/jsc_test/test_jsc' (arm64)os_security_config_get() = 0x40  | SCRIPT_RESTRICTIONS(0x40)=ON  HARDENED_HEAP(0x1)=off TPRO(0x2)=off GUARD_OBJECTS(0x100)=offdlopen JSC=0x365fa85a0  JSGlobalContextCreate=0x1a786a9bcProcess 49313 stopped* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BREAKPOINT (code=1, subcode=0x1a7ac6854)    frame #0: 0x00000001a7ac6854 JavaScriptCore`WTF::makePagesFreezable(void*, unsigned long) + 304JavaScriptCore`WTF::makePagesFreezable:->  0x1a7ac6854 <+304>: brk    #0xc471    0x1a7ac6858 <+308>: brk    #0x1    0x1a7ac685c <+312>: adrp   x1, 5970    0x1a7ac6860 <+316>: add    x1, x1, #0xb44 ; "/AppleInternal/Library/BuildRoots/4~CN9QugCEa5fya5kkZXSbadtTe9oVa3sO3gsEwzc/Library/Caches/com.apple.xbs/TemporaryDirectory.sbZNe3/Sources/WTF/Source/WTF/wtf/PageBlock.cpp"Target 0: (test_jsc) stopped.

程序没有打印 42,而是触发崩溃。去掉 entitlement 之后则一切正常。

到这里这个演示就结束了。也就是如果进程签名里有这个非公开的 com.apple.security.script-restrictions,JavaScriptCore 库可以被动态链接,但无法创建可用的执行上下文。

如果读者想知道是如何实现的,可以继续看下去。

实现

状态查询 API

上面的测试代码出现了一个 iOS 26 新加入的 API os_security_config_get

实际上一共有三个 API:

  • os_security_config_t os_security_config_get();:获取当前进程信息
  • int os_security_config_get_for_proc(pid_t pid, os_security_config_t *config);:获取 pid 对应进程,可能需要 root 权限
  • int os_security_config_get_for_task(task_t task, os_security_config_t *config);:获取 task 对应进程,需要先 task_for_pid,权限要求更高

它们都属于苹果官网文档一个很不起眼的模块 os,一共就没几个 API。

这个 API 返回一个 os_security_config_t,也就是一个 64 位的无符号整数:

/*! * @enum os_security_config_t * * @discussion * Supported security configurations that a process/task can have. * This is a bitmask type, allowing multiple configurations to be active. * * @constant OS_SECURITY_CONFIG_NONE * No security config flags set. * * @constant OS_SECURITY_CONFIG_HARDENED_HEAP * Indicates that the Hardened Heap configuration is enabled for the process/task. * This implies security-critical settings for the system memory allocator. * * @constant OS_SECURITY_CONFIG_TPRO * Indicates that Trusted Path Read-Only (TPRO) is enabled for the process/task. * * @constant OS_SECURITY_CONFIG_SCRIPT_RESTRICTIONS * Indicates Script Restrictions are enabled for the process/task. * * @constant OS_SECURITY_CONFIG_GUARD_OBJECTS * Indicates that the Guard Objects configuration is enabled for the process/task. */__API_AVAILABLE(macos(26.0), ios(26.0), tvos(26.0), watchos(26.0), visionos(26.0), driverkit(25.0))OS_OPTIONS(os_security_config, uint64_t,  OS_SECURITY_CONFIG_NONE = 0x0,  OS_SECURITY_CONFIG_HARDENED_HEAP OS_SWIFT_NAME(hardenedHeap) = 0x1,  OS_SECURITY_CONFIG_TPRO OS_SWIFT_NAME(trustedPathReadOnly) = 0x2,  OS_SECURITY_CONFIG_MTE OS_SWIFT_NAME(memoryTaggingExtension) = 0x4,  OS_SECURITY_CONFIG_SCRIPT_RESTRICTIONS OS_SWIFT_NAME(scriptRestrictions) = 0x40,  OS_SECURITY_CONFIG_GUARD_OBJECTS OS_SWIFT_NAME(guardObjects) = 0x100,);

在 SDK 里的注释写得挺清楚。眼尖的读者应该发现这几个 bit 正好对应了前文提到的各种 entitlement,甚至还有 MTE 的配置。这也是我为什么没法展开的原因,真写起来可太长了。

这组枚举对应了 XNU 内核的 task_security_config(对应版本 xnu-12377):

struct task_security_config {union {struct {uint16_t hardened_heap: 1,			    tpro: 1,#if HAS_MTE || HAS_MTE_EMULATION_SHIMS			    sec: 1,#else/* HAS_MTE || HAS_MTE_EMULATION_SHIMS */			reserved: 1,#endif			platform_restrictions_version: 3,			    script_restrictions: 1,			    ipc_containment_vessel: 1,			    guard_objects: 1;uint8_t hardened_process_version;		};uint32_t value;	};};

在 XNU 开源代码里并没有找到这结构初始化的部分,只能去 kernelcache 里反编译。伪代码太长就不贴了,直接交叉引用字符串 com.apple.security.hardened-process.dyld-ro 就能定位到。

这个函数除了以上提到的加固 entitlement,还检测如下几个键,并针对性地调整防御等级:

  • 第三方浏览器引擎相关(感谢欧盟):com.apple.developer.web-browser-engine.{host,rendering,networking,webcontent}
  • driverkit:com.apple.developer.driverkit

标志位传递到用户态有三种途径:

  • 当前进程:启动时由 applev[] 传递,一次性初始化,缓存在全局变量
  • 获取 pid:通过 proc_pidinfo 调用
  • 获取 task:通过 task_info 调用

反编译 libsystem_platform.dylib!os_security_config_get 仅有一行代码:

uint64_t os_security_config_get(){  return __restrictions_config & 0x40 | __security_config & 0x107;}

这两个全局变量(注:其实有一个不可写)由 __os_security_config_init 负责初始化。

内核参数传递

XNU 内核在启动进程时,除了常用的 argv[] 和 envp[] 之外还会通过栈传递一个 apple[] 字符串数组。如果当前进程有安全相关的配置,就会以 security_config=0x??? 的格式传递给用户态:

{#define SECURITY_CONFIG_KEY "security_config="char security_config_str[strlen(SECURITY_CONFIG_KEY) + HEX_STR_LEN + 1];	snprintf(security_config_str, sizeof(security_config_str),	    SECURITY_CONFIG_KEY "0x%x", task_get_security_config(task));	error = exec_add_user_string(imgp, CAST_USER_ADDR_T(security_config_str), UIO_SYSSPACEFALSE);	imgp->ip_applec++;}#if HAS_MTE || HAS_MTE_EMULATION_SHIMSif (task_has_sec(task)) {const char *sec_transition_shims = "has_sec_transition=1";		error = exec_add_user_string(imgp, CAST_USER_ADDR_T(sec_transition_shims), UIO_SYSSPACEFALSE);if (error) {			printf("Failed to add security translation shims notification\n");goto bad;		}		imgp->ip_applec++;/* Push down MTE-specific configuration options that allocators may be interested into. */#define SEC_TRANSITION_POLICY_KEY "sec_transition_policy="char sec_transition_policy[strlen(SEC_TRANSITION_POLICY_KEY) + HEX_STR_LEN + 1];		snprintf(sec_transition_policy, sizeof(sec_transition_policy),		    SEC_TRANSITION_POLICY_KEY "0x%x", task_get_sec_policy(task));		error = exec_add_user_string(imgp, CAST_USER_ADDR_T(sec_transition_policy), UIO_SYSSPACEFALSE);		imgp->ip_applec++;	}#endif/* HAS_MTE || HAS_MTE_EMULATION_SHIMS */

针对 MTE 还专门有一个 has_sec_transition=1 的配置。

dyld 在启动进程时负责初始化栈和启动参数(argc、argv、envp 和 apple 等),并传递给主程序的入口点,以及在每次载入 dylib 框架的时候调用其 _mod_init_func 注册的初始化函数。

apple[] 字符串数组最终传递顺序:

  • dyld
  • libSystem.B.dylib!libSystem_initializer
  • libsystem_platform.dylib!__libplatform_init
  • libsystem_platform.dylib!__os_security_config_init

也就是每个进程全局初始化一次。

进程状态初始化

有趣的点在 __os_security_config_init 函数内部的实现。其使用 simple_getenv 提取先前提到来自内核的字符串参数,然后解析十六进制字符串作为初始的配置值,也就是我们之前提到最终会存入的 os_security_config_t

这里有一个有意思的分支:

if (v7 & OS_SECURITY_CONFIG_GUARD_OBJECTS)      __security_config |= OS_SECURITY_CONFIG_GUARD_OBJECTS;if (v7 & OS_SECURITY_CONFIG_SCRIPT_RESTRICTIONS ){      address = (mach_vm_address_t)&os_script_config_storage;      if ( mach_vm_map(mach_task_self_, &address,                       0x4000//                        0,                                 // mask                       VM_FLAGS_OVERWRITE | VM_FLAGS_PERMANENT,                       MEMORY_OBJECT_NULL, 0, FALSE,      // object / offset / copy                       VM_PROT_NONE,                      // cur_protection                       VM_PROT_NONE,                      // max_protection                       VM_INHERIT_COPY) )                 // inheritance (== VM_INHERIT_DEFAULT)          __os_security_config_init_cold_2();      address = (mach_vm_address_t)&__restrictions_config;      if ( mach_vm_map(mach_task_self_, &address,                       0x40000,                       VM_FLAGS_OVERWRITE | VM_FLAGS_PERMANENT,                       MEMORY_OBJECT_NULL, 0, FALSE,                       VM_PROT_READ | VM_PROT_WRITE,      // cur_protection                       VM_PROT_READ | VM_PROT_WRITE,      // max_protection                       VM_INHERIT_COPY) )          __os_security_config_init_cold_3();      __restrictions_config = OS_SECURITY_CONFIG_SCRIPT_RESTRICTIONS;      result = mach_vm_protect(mach_task_self_, address,                               0x4000,                               /*set_maximum=*/ TRUE,                               VM_PROT_READ);      if ( (kern_return_t)result )          __os_security_config_init_cold_4();  }

代码没有简单地把值存到全局变量,而是使用位运算拆成了两个部分:__security_config 和 __restrictions_config

目前 __restrictions_config 仅仅用来保存 OS_SECURITY_CONFIG_SCRIPT_RESTRICTIONS 状态,而一旦检测到这个状态启用,libsystem_platform 会锁定其所在的内存页面的访问权限为只读。不仅如此,还有另一个名为 os_script_config_storage 的符号,直接连读权限都取消了。什么鬼?

WebKit 的处理

我们刚才绕了一大圈从内核到链接器到系统库,主要为了演示如何禁用 JavaScriptCore。所以这个符号当然是 WebKit 在用。直接上源码:

staticboolscriptingIsForbidden(){    return processHasEntitlement("com.apple.security.script-restrictions"_s);}voidinitialize(){    ...    WTF::makePagesFreezable(&os_script_config_storage, OpcodeConfigSizeToProtect);    if (g_jscConfig.vmEntryDisallowed || scriptingIsForbidden()) [[unlikely]] {        g_jscConfig.vmEntryDisallowed = true;        WTF::permanentlyFreezePages(&os_script_config_storage, OpcodeConfigSizeToProtect,                                    WTF::FreezePagePermission::None);        return;    }    WTF::compilerFence();    ...}

乍一看 WebKit 也是检测标准的 entitlement 来拒绝初始化,但实际上压根走不到那个分支。

早在上一行 WTF::makePagesFreezable 调用,如果当前进程禁止 JavaScriptCore,libsystem_platform.dylib 就会把 os_script_config_storage 所在内存标记为不可访问。

// Works together with permanentlyFreezePages().voidmakePagesFreezable(void* base, size_t size){    RELEASE_ASSERT(roundUpToMultipleOf(pageSize(), size) == size);#if PLATFORM(COCOA)    mach_vm_address_t addr = std::bit_cast<uintptr_t>(base);    auto flags = VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE | VM_FLAGS_PERMANENT;    auto attemptVMMapping = [&] {        auto result = mach_vm_map(mach_task_self(), &addr, size, pageSize() - 1, flags, MEMORY_OBJECT_NULL, 0false, VM_PROT_READ | VM_PROT_WRITE, VM_PROT_READ | VM_PROT_WRITE, VM_INHERIT_DEFAULT);        return result;    };    auto result = attemptVMMapping();#if PLATFORM(IOS_FAMILY_SIMULATOR)    if (result != KERN_SUCCESS) {        flags &= ~VM_FLAGS_PERMANENT; // See rdar://75747788.        result = attemptVMMapping();    }#endif    RELEASE_ASSERT(result == KERN_SUCCESS);#else    UNUSED_PARAM(base);    UNUSED_PARAM(size);#endif}

因此 makePagesFreezable 直接就抛异常(RELEASE_ASSERT)了。

以上的内存权限也是为了防止漏洞利用在拥有读写原语之后修改 __restrictions_config。照这样看来 Apple 认为剩余的几个 flag 并不需要只读保护。可能是因为读取时机的区别,其余的 bit 主要影响内存分配器(对应开源代码的 libmalloc)的行为,而它们早在进程初始化阶段就处理完了。

小结

一个一句话能说清的需求:禁止某些系统进程使用 JavaScriptCore,牵扯到了一堆组件来实现。

内核将 entitlement 的设定转换成 task_security_config,再通过 apple[] 数组传递给用户态初始化逻辑,而用户态的状态消费者则有被动的内存访问失败和显式检查 processHasEntitlement两层把关,让 JavaScriptCore 初始化失败。

参考资料

  • https://developer.apple.com/documentation/Xcode/enabling-enhanced-security-for-your-app
  • https://developer.apple.com/documentation/javascriptcore/jscontext
  • https://developer.apple.com/documentation/os/os_security_config_get
  • https://github.com/apple-oss-distributions/xnu
  • https://github.com/apple-oss-distributions/dyld
  • https://github.com/apple-oss-distributions/libsystem