Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions pocs/linux/kernelctf/CVE-2023-3610_cos/docs/exploit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
## Triggering the use-after-free and reallocation

To trigger the vulnerability we have to create a batch transaction.
The chain object will be freed immediately when rule creation fails, but processing of the transaction will continue, allowing us to reallocate the victim object from kmalloc-128.

A good option is to use a table user data attribute - we can choose an arbitrary size for the allocation and the whole buffer will be filled with our data.

Steps of the batch transaction:
1. Create the victim chain "v" with NFT_BINDING_FLAG.
2. Create a rule with a jump to "v" and an invalid expression to trigger removal of the chain.
3. Create a new table with user data containing a fake nft_chain object.

## Getting RIP control.

Following code is executed when nftables try to rollback the creation of the victim chain:
```
case NFT_MSG_NEWCHAIN:
if (nft_trans_chain_update(trans)) {
free_percpu(nft_trans_chain_stats(trans));
kfree(nft_trans_chain_name(trans));
nft_trans_destroy(trans);
} else {
if (nft_trans_chain_bound(trans)) {
nft_trans_destroy(trans);
break;
}
nft_use_dec_restore(&trans->ctx.table->use);
nft_chain_del(trans->ctx.chain);
nf_tables_unregister_hook(trans->ctx.net,
trans->ctx.table,
trans->ctx.chain);
```

The last call is interesting:

```
static void __nf_tables_unregister_hook(struct net *net,
const struct nft_table *table,
struct nft_chain *chain,
bool release_netdev)
{
struct nft_base_chain *basechain;
const struct nf_hook_ops *ops;

if (table->flags & NFT_TABLE_F_DORMANT ||
!nft_is_base_chain(chain)) [1]
return;
basechain = nft_base_chain(chain);
ops = &basechain->ops;

if (basechain->type->ops_unregister) // [2]
return basechain->type->ops_unregister(net, ops);

...
}

static void nf_tables_unregister_hook(struct net *net,
const struct nft_table *table,
struct nft_chain *chain)
{
return __nf_tables_unregister_hook(net, table, chain, false);
}
``

If we pass the checks for the basechain at [1], we will get RIP control through the ops_unregister function pointer in the nft_chain_type object at [2].

There's only one problem. nft_base_chain is a larger structure containing nft_chain:
```
struct nft_base_chain {
struct nf_hook_ops ops; /* 0 0x28 */

/* XXX last struct has 4 bytes of padding */

struct list_head hook_list; /* 0x28 0x10 */
const struct nft_chain_type * type; /* 0x38 0x8 */
/* --- cacheline 1 boundary (64 bytes) --- */
u8 policy; /* 0x40 0x1 */
u8 flags; /* 0x41 0x1 */

/* XXX 6 bytes hole, try to pack */

struct nft_stats * stats; /* 0x48 0x8 */
struct nft_chain chain; /* 0x50 0x78 */
/* --- cacheline 3 boundary (192 bytes) was 8 bytes ago --- */
struct flow_block flow_block; /* 0xc8 0x10 */

/* size: 216, cachelines: 4, members: 8 */
};
```

The type pointer is located before the nft_chain object we control.
The solution to this is to prepare a slab with fake base_chain objects preceding the victim chain in memory.
Due to freelist randomization we can not predict where in the slab our chain will be located, but we can just allocate all objects except one and then run the batch transaction so that the victim chain is the one making the slab full.
We also need to allocate a full slab of fake base chains before that, just in case our victim will be at the very beginning of the slab.

To ensure good stability we use the zoneinfo parsing technique to detect the allocation of the new kmalloc-128 slab.
This is done in the prepare_slab() function.
We also need a place to store the fake nft_chain_type object pointed to by nft_base_chain->type and warncomm technique was used for that.

## Pivot to ROP

When ops_unregister is called, RSI contains a pointer to the ops field at the beginning of the fake base chain object.

This means we only need 2 gadgets to pivot to the ROP chain:


```
push rsi
jmp qword ptr [rsi + 0xF]
```

and

```
pop rsp
```


## Second pivot

Our objects are quite small (128 bytes) and we have to jump over important fields like flags or type, so to be able to run the whole privilege escalation ROP we need to first pivot again to a large area.
This is done by choosing an unused read/write area in the kernel and using copy_user_generic_string() to copy the second stage ROP from userspace to that area.
Then we use a `pop rsp ; ret` gadget to pivot there.

## Privilege escalation

The ROP chain does the standard commit_creds(init_cred); switch_task_namespaces(pid, init_nsproxy); sequence and returns to the userspace.
69 changes: 69 additions & 0 deletions pocs/linux/kernelctf/CVE-2023-3610_cos/docs/vulnerability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
## Requirements to trigger the vulnerability

- CAP_NET_ADMIN in a namespace is required
- Kernel configuration: CONFIG_NF_TABLES
- User namespaces required: yes

## Commit which introduced the vulnerability

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=d0e2c7de92c7f2b3d355ad76b0bb9fc43d1beb87

## Commit which fixed the vulnerability

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?h=4bedf9eee016286c835e3d8fa981ddece5338795

## Affected kernel versions

Introduced in 5.9. Fixed in 5.15.119 and other stable trees.

## Affected component, subsystem

net/netfilter

## Description

Nftables uses batch transactions when updating the ruleset to attempt to have atomicity.
This approach was a source of many vulnerabilities, mainly of use-after-free type.

In this case the vulnerability affects the handling of bound chains.
This is a special type of a chain that is bound to an expression that refers to it (e.g. a jump) and removed at the same time as the referring expression.

If following objects are created in the transaction:
- a bound chain "victim"
- rule with an immediate expression referring to "victim" and an invalid expression after that.

then the transaction is aborted and chain "victim" is removed in nft_immediate_destroy(), but it is still on the commit list and \_\_nf_tables_abort() will try to destroy it again:

```
list_for_each_entry_safe_reverse(trans, next, &nft_net->commit_list,
list) {
switch (trans->msg_type) {
...
case NFT_MSG_NEWCHAIN:
if (nft_trans_chain_update(trans)) {
free_percpu(nft_trans_chain_stats(trans));
kfree(nft_trans_chain_name(trans));
nft_trans_destroy(trans);
} else {
if (nft_chain_is_bound(trans->ctx.chain)) { // [1]
nft_trans_destroy(trans);
break;
}
trans->ctx.table->use--;
nft_chain_del(trans->ctx.chain);
nf_tables_unregister_hook(trans->ctx.net,
trans->ctx.table,
trans->ctx.chain);
}
break;

...
list_for_each_entry_safe_reverse(trans, next,
&nft_net->commit_list, list) {
list_del(&trans->list);
nf_tables_abort_release(trans);
}
...
```

The code tries to check the bound status at [1], but trans->ctx.chain points to already freed memory (use-after-free).
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
INCLUDES =
LIBS = -pthread -ldl -lnftnl -lmnl
CFLAGS = -fomit-frame-pointer -static -fcf-protection=none

exploit: exploit.c kernelver_17412.101.42.h
gcc -o $@ exploit.c $(INCLUDES) $(CFLAGS) $(LIBS)

prerequisites:
sudo apt-get install libkeyutils-dev libnftnl-dev libmnl-dev
Binary file not shown.
Loading
Loading