Skip to content
KitploitKITPLOIT
工具漏洞利用博客
Log in
提交
工具漏洞利用博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

订阅源联系隐私© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
CVE-2026-64468 — 非特权概念验证及针对 Linux 内核 Binder 释放后使用漏洞(CVE-2026-64468)的 x86_64 本地权限提升,附带 KASAN 实验环境及易受攻击/已修复版本的差异对比。 | Kitploit
工具/GitHubGitHub/aramosf/cve-2026-64468
权限提升漏洞利用框架漏洞分析漏洞利用学习与教育二进制利用
GitHubaramosf/cve-2026-64468

CVE-2026-64468

非特权概念验证及针对 Linux 内核 Binder 释放后使用漏洞(CVE-2026-64468)的 x86_64 本地权限提升,附带 KASAN 实验环境及易受攻击/已修复版本的差异对比。

查看仓库
297281个月前Kitploit 审核通过

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2026-64468 — Linux 内核 Binder binder_free_transaction() 进程生命周期释放后使用

实时 QEMU/KVM 运行:未打补丁的易受攻击内核报告释放后使用,已修复内核则不会

本仓库包含针对 Linux 内核 Binder 释放后使用漏洞的内容,该漏洞已由上游提交 f223d27a546c1e1f48d38fd67760e78f068fe8c4 修复:

  • binder_chain_64468.c — 一个无特权的概念验证程序,可触发该漏洞并让内核自行证明,附带 KASAN 实验室和易受攻击/已修复的差分测试(lab/、run.sh、verify.sh)。
  • exploit.c — 一个针对 x86_64 的自包含本地权限提升程序。使用 gcc -O2 -pthread -o exploit exploit.c 编译,以普通用户身份运行,最终获得 root shell。
  • demo/ — 一个实验室,在未打补丁的内核上启动真实的 Debian 13 用户空间,因此该漏洞利用程序可由目标自身的 gcc 编译,并在其随后接管的那台机器上运行。

所有内容均以普通用户身份(uid/gid 1000,无能力,无命名空间)针对未打任何补丁的纯上游内核运行。

警告

此代码有意竞态内核对象生命周期,然后劫持内核控制流。竞态失败会破坏内核堆状态,并可能导致机器崩溃或挂起。请仅在您拥有的隔离、一次性虚拟机中运行。切勿在宿主机上运行。

状态与范围

状态声明
已确认该漏洞真实存在,可从无特权进程触达,且上游修复已将其消除。
已演示对濒临消亡的 binder_proc 的易受攻击解引用在未打补丁的内核上自然且反复发生,而在已修复内核上从未发生。
已演示完整的释放后使用,由 KASAN 在未打补丁的内核上报告。
已演示使用攻击者控制的字节回收已释放的 binder_proc、内核控制流劫持,以及在 x86_64 上从无特权用户提权至 uid 0。
未声明针对特定厂商或 Android 设备的任何结果。仅测试了下列上游内核,均在 x86_64 上。
未声明随附的漏洞利用程序无需修改即可针对发行版内核运行。请参阅受影响系统:它需要针对每个内核的偏移量,而且在所调查的每个通用发行版上,binder 设备首先就无法被无特权用户触达。

漏洞

binder_free_transaction() 在 t->lock 下从事务中读取目标进程,释放该锁,然后获取目标进程的内部锁:```c spin_lock(&t->lock); target_proc = t->to_proc; spin_unlock(&t->lock);

if (target_proc) {
	binder_inner_proc_lock(target_proc);   /* use after free */
没有任何东西能让 `target_proc` 在该间隙中保持存活。一个正在被并行销毁的进程可能在此期间到达 `binder_proc_dec_tmpref() -> kfree()`,因此锁会在已释放的内存上被获取。上游修复方案在仍持有 `t->lock` 的同时固定 `t->to_thread`,这会使所属进程保持存活,直到内部锁被使用并释放。

该漏洞由 **Alice Ryhl** 报告,并由 **Carlos Llamas** 修复,两人均来自 Google。[原始报告](https://lore.kernel.org/all/[email protected]/) 附带了相关的 KASAN 跟踪信息。

### 到达易受攻击的访问点

唯一能够到达*外部* `to_proc` 的调用者是 `binder_send_failed_reply()`,并且它仅在 `t->from` 为 `NULL` 时才遍历到 `t->from_parent`:```c
	target_thread = binder_get_txn_from_and_acq_inner(t);
	if (target_thread) { ...; binder_free_transaction(t); return; }
	next = t->from_parent;
	binder_free_transaction(t);
	t = next;

from_parent 恰好在一处被赋值,而该赋值在几行之前受到 binder 的坏事务栈检查的保护:一个线程只有在栈顶是它正在接收的事务时,才能发送同步事务。因此,对于该链的每一环,子进程的发送方和父进程的接收方都是同一个线程。

这带来一个尖锐的后果。binder_thread_release() 在持有 proc->inner_lock 的情况下遍历正在消亡线程的栈,并写入以下两者:``` iteration j : [holds child->lock ] child->from = NULL ; unlock child->lock spin_lock(parent->lock) iteration j+1 : [holds parent->lock] parent->to_proc = NULL

in **同一遍历的相邻迭代中**,每次迭代都持有各自的
`t->lock`。遍历者只有在释放了 `child->lock` 之后,才能得知 `child->from == NULL`,并且需要 `parent->lock` 来获取自身的快照。因此,整个机会窗口就位于该次遍历的 `spin_unlock(&child->lock)` 与
`spin_lock(&parent->lock)` 之间——仅相隔几条指令。释放遍历持有自旋锁,在该处无法被抢占;只有中断才能延迟它。

这正是该竞态条件极为狭窄的原因,也是概念验证和利用程序都依赖概率的原因。

### 概念验证构建了什么

`binder_chain_64468.c` 构建了能够触达易受攻击访问的最短链,从而使遍历者在该窗口内的工作量尽可能小——仅两次 `t->lock` 获取和一次 `kfree()`,不涉及外部 `inner_proc_lock`、无 `wake_up`,也无回复投递:```
  B thread i   --e2 (sync, code 0x4442414b)-->  P thread Y_i
  P thread Y_i --e1 (sync, code 0x54414c4c)-->  B thread i   (nested target)

  B thread i   stack: [ e2 outgoing , e1 incoming (top) ]
  P thread Y_i stack: [ e2 incoming , e1 outgoing (top) ]

P 随后关闭其 binder fd,因此 binder_deferred_release() 会释放 Y_1..Y_K 并最终释放 binder_proc,而每个 B 线程 同时发出 BINDER_THREAD_EXIT:``` binder_thread_release(B_i) nulls e1->to_proc (own proc) and e2->from binder_send_failed_reply(e1) e1->from == NULL once Y_i was released -> binder_free_transaction(e1) target_proc already NULL, kfree(e1) -> binder_free_transaction(e2) target_proc == P <-- vulnerable access

第三个进程是活页夹上下文管理器,仅用于分发另外两个进程所需的句柄。`exploit.c` 完全复用了这一构造。

### 两个独立条件

一份 KASAN 报告需要同时满足以下两点:

* **易受攻击的访问** —— 遍历者必须在上方所述的狭窄窗口内获取 `parent->lock`,从而快照到一个仍然存活的 `to_proc`;以及
* **窗口内的释放落地** —— 遍历者随后必须在释放 `t->lock` 与获取受害者内部锁之间失去其 CPU,并保持离线状态,直到延迟释放完成并释放 `binder_proc`。

第一个条件有其自身的判定依据,无需依赖 KASAN:活页夹会打印```
binder: binder_free_proc: Unexpected outstanding_txns -1

每当这种情况发生时,因为遍历器和 binder_thread_release() 随后都会对同一事务的同一个计数器进行递减。请注意,这本身并非漏洞/修复之间的差异——修复阻止的是释放,而不是第二次递减——因此它在两个内核上都会出现。这里使用它只是为了展示易受攻击的代码路径被执行。

从释放后使用到 root

概念验证止步于对已释放内存的 4 字节递减。要将其转化为 uid 0 需要四样东西,而它们都不来自漏洞本身:该漏洞不会泄露任何信息。

1. 共享缓存

struct binder_proc 为 648 字节,通过普通的 GFP_KERNEL kzalloc 分配——而非 __GFP_ACCOUNT。因此它会落入 kmalloc-1k,与该大小的所有其他未记账分配放在一起,并且不会被隔离在 kmalloc-cg-* 之后。仅此一点就使得该对象可以被回收。

2. 跟得上的回收泵

kfree() 发生的时刻无法从用户空间观察到,遍历器再次触碰该对象的时刻同样无法观察到,因此没有任何可供计时对齐的时机。因此喷射以泵的形式运行:它持续分配并释放 kmalloc-1k 对象,在运行延迟释放的那个 CPU 上执行,只要遍历器在运行就持续进行。

为此使用 System V 消息。alloc_msg() 是一个普通的未记账 kmalloc,包含 48 字节头部加负载,因此一条 976 字节的消息就是一次 1024 字节的分配;配额是按队列而非按 uid 计算的;并且 msgrcv() 会同步释放。实测吞吐量:每秒约 198,000 次分配,零失败。

首先尝试了 add_key/user_key_payload,但它是个陷阱。其负载受按 uid 的字节配额(kernel.keys.maxbytes,默认 20000)限制,该配额只有在密钥垃圾收集器销毁密钥时才会释放,因此紧凑的分配/释放循环会在毫秒内耗尽它:实测 27,151 次成功分配,对应 2,121,009 次失败——98.7% 的喷射在静默地做无用功,看起来就像一场永远赢不到槽位的喷射。KEYCTL_INVALIDATE 使情况更糟(4,775 次成功),因为它会排队等待 GC 工作。

3. 被回收对象必须包含的内容

遍历器会触碰已释放 binder_proc 的四个字段(偏移量通过 pahole 在目标构建上测得):

偏移量字段遍历器执行的操作
108int outstanding_txns对其递减
113bool is_frozen读取它
120wait_queue_head_t freeze_wait若 outstanding_txns == 0 && is_frozen 则遍历它
624spinlock_t inner_lock获取并释放它

当 outstanding_txns == 1 且 is_frozen == 1 时,递减达到零,遍历器调用 wake_up_interruptible_all(&proc->freeze_wait)。随后 __wake_up_common 计算 curr = head.next - 24 并调用 *(head.next - 8):一个从 head.next 所指向位置读取的函数指针,而该值由被回收的对象提供。

4. 来自侧信道的两个地址

该指针必须到达攻击者控制的内存,且位于内核地址空间,而该漏洞不泄露任何信息。这两个地址都来自预取计时——与 KASLD(Brendan Coles,MIT)相同的信道,本实现即源自该项目:

  • 内核文本。 对已映射内核地址的预取会在页表遍历中解析并退出,其速度可测量地快于对未映射地址的预取,即使该访问在架构上永远不会可见。扫描文本范围的 2 MiB 槽位会显示镜像为一段快速槽位;其第一个槽位即 _text。

  • 直接映射。 启用 CONFIG_RANDOMIZE_MEMORY 后,直接映射以 1 GiB 为单位随机化,因此也必须定位它。与文本不同,它覆盖全部 RAM,因此它是映射槽位中最长的连续段。需要两项改进才能使其可用:

    • 在 KVM 下,单次预取区分映射与未映射仅相差约 4 个周期,这无法在噪声中存活,因此每次采样对一批 400 次预取计时(实测 311 对 523 周期——可区分);
    • 在争用激烈的主机上,单次扫描不可靠——单客户机时 10/10 正确,五台客户机同时运行时为 3/5——因此扫描运行五次,并要求多数一致。若无共识,漏洞利用会报告失败并停止,而不是对着未验证的地址开火。

    运行可靠起始的位置并非 page_offset_base 本身,而是内核可能以 1 GiB 页映射的第一个槽位——即覆盖物理 4 GiB 的那个。在其之下,PCI 空洞和固件保留区强制使用 2 MiB 页,其更长的遍历在此处无法与未映射区分。该槽位正是喷射所需的,因此使用它。

随后约 60% 的物理内存被填充为同一精心构造的 4 KiB 页的副本,因此无论布局最终如何,从该锚点起的固定偏移都由该构造页支撑。

攻击链```

freeze_wait.head.next -> entry1 (in the sprayed page)

entry1.func = mov 0x28(%rdi),%rdi ; mov 0x18(%rdi),%rax ; jmp *0x58(%rax) RDI <- entry1+0x28 = &cred RAX <- cred+0x18 jmp *(RAX+0x58) = commit_creds -> commit_creds(cred)

entry2.func = mov $-1,%rax ; ret __wake_up_common breaks out on ret < 0

没有栈迁移,也没有 `iretq`:被劫持的线程正常地从它的 `ioctl()` 返回,然后只是以 root 身份回到用户空间。

### 伪造的 cred,以及那个本会浪费它的 bug

分发器 gadget 从 `cred+0x18` 获取它的 `RAX`,而 `cred+0x18` 是
`euid`/`egid`,所以链执行后 `euid` 立即变成内核指针的低半部分。
这只是外观问题。真正的问题在于 `prepare_creds()` ——
**每个后续的 `fork()` 和 `execve()` 都会调用它** —— 会解引用三个字段,且没有 NULL 检查:```c
	get_group_info(new->group_info);        /* refcount_inc(&gi->usage) */
	get_uid(new->user);                     /* refcount_inc(&u->__count) */
	new->ucounts = get_ucounts(new->ucounts);

伪造的凭据若将其置为 NULL,会得到 uid 0,然后在第一次 execve 时使机器崩溃——也就是说,该漏洞利用会报告成功,却立刻摧毁目标系统。因此,利用链将 user、ucounts 和 group_info 指向真实的内核全局变量 root_user、init_ucounts 和 init_groups,这些变量的地址来自同一个 _text 基址。设置好这些之后,提权线程在 init_user_ns 中持有 CAP_SETUID,于是它调用 setresuid(0,0,0),内核便会用一份干净、由内核分配的 root 凭据覆盖伪造的凭据。只有在这之后,才会执行其他任何操作。

权限通过一份 setuid-root 的漏洞利用二进制副本传递给父进程,该副本在 exec 启动 shell 之前会自行删除,因此 root shell 在主进程中于干净的终端上运行,且不会留下任何 setuid 残留。

受影响系统

仅凭 uname -r 并不足够。厂商内核通常会移植 binder 修复,而不会将版本号改为对应的主线版本;请检查源码或软件包变更日志。

下载工具