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-2024-53141_lts #150

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
180 changes: 180 additions & 0 deletions pocs/linux/kernelctf/CVE-2024-53141_lts/docs/exploit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Introduction
IP sets are a framework inside the Linux kernel. Depending on the type, an IP set may store IP addresses, networks, (TCP/UDP) port numbers, MAC addresses, interface names or combinations of them in a way, which ensures lightning speed when matching an entry against a set. One of the features called `bitmap:ip`. The bitmap:ip set type uses a memory range, where each bit represents one IP address and can store up to 65535 (B-class network) entries.

Linux kernel implemented it in `net/netfilter/ipset/ip_set_bitmap_ip.c`. We can use netlink to interact with this component.

# Vulnerability Overview
CVE-2024-53141 is vulnerability in ipset component. This bug is present in `bitmap_ip_uadt` function, that fails to handle `IPSET_ATTR_CIDR` parameters.

```C
static int
bitmap_ip_uadt(struct ip_set *set, struct nlattr *tb[],
enum ipset_adt adt, u32 *lineno, u32 flags, bool retried)
{
if (ip < map->first_ip || ip > map->last_ip) // [1]
return -IPSET_ERR_BITMAP_RANGE;
...
if (tb[IPSET_ATTR_IP_TO]) {
ret = ip_set_get_hostipaddr4(tb[IPSET_ATTR_IP_TO], &ip_to);
if (ret)
return ret;
if (ip > ip_to) {
swap(ip, ip_to);
if (ip < map->first_ip) // [2]
return -IPSET_ERR_BITMAP_RANGE;
}
} else if (tb[IPSET_ATTR_CIDR]) {
u8 cidr = nla_get_u8(tb[IPSET_ATTR_CIDR]);
...
ip_set_mask_from_to(ip, ip_to, cidr); // [3]
} else {
ip_to = ip;
}

if (ip_to > map->last_ip) // [4]
return -IPSET_ERR_BITMAP_RANGE;

for (; !before(ip_to, ip); ip += map->hosts) { // [5]
e.id = ip_to_id(map, ip);
ret = adtfn(set, &e, &ext, &ext, flags);
```

When `tb[IPSET_ATTR_IP_TO]` is not present but `tb[IPSET_ATTR_CIDR]` exists, `ip` and `ip_to` calculated via `ip_set_mask_from_to` based on `tb[IPSET_ATTR_CIDR]`. It's possible to have `ip` less than the actual `map->first_ip` and there's no check of `ip` after that.

Consider when we have `map->first_ip` to be `0xffffffcb` and `map->last_ip` be `0xffffffff`, then we pass `IPSET_ATTR_IP` with value `0xffffffff` and `IPSET_ATTR_CIDR` with `3`, the result after [3] will make `ip` equal to `0xe0000000` and `ip_to` equal to `0xffffffff`, it would pass the check at [1], [4] and continue proceed to [5]. Therefore, the loop at [5] can proceed outside range ip that allocated (`map->first_ip` until `map->last_ip`).

# Primitives

`adtfn` will resolve to `bitmap_ip_add` function (defined as `mtype_add` in `net/netfilter/ipset/ip_set_bitmap_gen.h`, mtype is just macro to `bitmap_ip`). It will fetch extension `x` from `map.extensions` with `e.id` as index.
```c
#define get_ext(set, map, id) ((map)->extensions + ((set)->dsize * (id)))

static int
mtype_add(struct ip_set *set, void *value, const struct ip_set_ext *ext,
struct ip_set_ext *mext, u32 flags)
{
struct mtype *map = set->data;
const struct mtype_adt_elem *e = value;
void *x = get_ext(set, map, e->id);
int ret = mtype_do_add(e, map, flags, dsize: set->dsize);
```
`e.id` comes from `ip_to_id` from previous function, if we craft such `id` it can lead to out-of-bounds later when that function perform on `x`.

```c
static u32
ip_to_id(const struct bitmap_ip *m, u32 ip)
{
return ((ip & ip_set_hostmask(m->netmask)) - m->first_ip) / m->hosts;
}
```
We also can control the size of map (`bitmap_ip` type) by passing `first_ip` and `last_ip` when we initially create the ip set.

## OOB Write to Kernel Heap Leak
By crafting `e.id` it can lead OOB write, for example when we set the comment in sets, it can spill the kernel heap address in the next chunk because `comment` in `ip_set_init_comment` is outside the range.
```c
static int
mtype_add(struct ip_set *set, void *value, const struct ip_set_ext *ext,
struct ip_set_ext *mext, u32 flags)
{
struct mtype *map = set->data;
const struct mtype_adt_elem *e = value;
void *x = get_ext(set, map, e->id);
...
if (SET_WITH_COMMENT(set))
ip_set_init_comment(set, ext_comment(x, set), ext);
}
ip_set_init_comment(struct ip_set *set, struct ip_set_comment *comment,
const struct ip_set_ext *ext)
{
...
c = kmalloc(size: sizeof(*c) + len + 1, GFP_ATOMIC);
...
rcu_assign_pointer(comment->c, c);
}
```
This is the path that we used to leak kernel heap addr, by shaping heap to `..[skbuff][bitmap_ip][skbuff][skbuff]..` the OOB write of kernel addr will spilled to the next chunk which is socket buffer.

In our exploit we choose to work on kmalloc-cg-1024 in this step, we allocate `bitmap_ip` and spray socket buffer at kmalloc-cg-1024. `ip_set_init_comment` will spilled kernel heap addr from kmalloc-192 to the socket buffer.

By receiving socket buffer, we can get kernel heap address to use it for the next step exploitation.

## OOB Write Arbitrary Value
Another type of OOB that we can use is OOB write with arbitrary value. This works using set that contain `counter`.
```c
static inline void
ip_set_init_counter(struct ip_set_counter *counter,
const struct ip_set_ext *ext)
{
if (ext->bytes != ULLONG_MAX)
atomic64_set(v: &(counter)->bytes, i: (long long)(ext->bytes));
if (ext->packets != ULLONG_MAX)
atomic64_set(v: &(counter)->packets, i: (long long)(ext->packets));
}
static int
mtype_add(struct ip_set *set, void *value, const struct ip_set_ext *ext,
struct ip_set_ext *mext, u32 flags)
{
struct mtype *map = set->data;
const struct mtype_adt_elem *e = value;
void *x = get_ext(set, map, e->id);
...
if (SET_WITH_COUNTER(set))
ip_set_init_counter(ext_counter(x, set), ext);
}
```
We can craft the value in `ext->bytes` or `ext->packets` then it will set to the outside the bounds of allocated heap chunk.

## OOB Write to Use-After-Free
To unlock more useful primitives, we use `msg_msgseg` to convert this OOB to use after free. The idea is by controlling `msg_msgseg.next` (first 8 byte of msg_msgseg) we can perform arbitrary free by reach `free_msg` function.
```c
void free_msg(struct msg_msg *msg)
{
struct msg_msgseg *seg;

security_msg_msg_free(msg);

seg = msg->next;
kfree(msg);
while (seg != NULL) {
struct msg_msgseg *tmp = seg->next; //

cond_resched();
kfree(seg);
seg = tmp;
}
}
```

In this step choose to work on kmalloc-cg-2048, we allocate `bitmap_ip` and spray `msg_msgseg` at kmalloc-cg-2048. The victim object (`struct bitmap_ip`) is allocated with `GFP_KERNEL_ACCOUNT` so it's guarantee can reside in the same slab cache with `msg_msgseg` object. So we put `bitmap_ip` right before `msg_msgseg`, then perform OOB write so we can write arbitrary value on `msg_msgseg.next`.

By using arbitrary free, we choose `pipe_buffer` as another victim object because it's familiar for us and easy to plan for the next step of exploit. But the kernel heap leak primitives we had only allocate buffer on generic kernel cache (kmalloc-192), so we kind of guess a little bit to get `pipe_buffer` address that located in account cache. Basically, from our observation we can calculate `leak_addr ~(0x10000000 - 1)` with `leak_addr` as kernel heap addr leaked from kmalloc-192 chunk, we can guess `pipe_buffer` in confident probability.

## Use-After-Free to Control RIP
In this step, we already free `pipe_buffer` by arbitrary free. Next, we reclaim victim `pipe_buffer` using socket buffer. We spray socket buffer and hope it placed at same address as `pipe_buffer`. Then, we write to the pipe and it will filled one of our socket buffer with `pipe_buffer` content.

By reading that socket, we can read the content of pipe buffer written before to leak kernel text (from pipe_buffer->ops). We can calculate kernel base and calculate our rop gadget address.

Controlling RIP, we simply free the that socket buffer and reallocate with new one that overwrite `pipe_buffer->ops` to our controlled heap chunk. By closing the pipe we can control kernel execution to our rop chain that we already prepared.

# Control RIP to ROP Chain
We reached this code path where we control all fields in `pipe_buffer`.
```c
static inline void pipe_buf_release(struct pipe_inode_info *pipe,
struct pipe_buffer *buf)
{
const struct pipe_buf_operations *ops = buf->ops;

buf->ops = NULL;
ops->release(pipe, buf);
}
```
By controlling `buf->ops->realese` we can redirect kernel execution. Noticing RSI register is the buffer we can control, we use these two gadgets to pivot the stack so we can use rop chain.
```
1. push rsi ; jmp qword ptr [rsi + 0x39]
2. pop rsp ; pop r15 ; ret
```

# ROP chain to Root Shell
We want to use `core_pattern` technique to get the root shell. `core_pattern` is kernel string variable contain executable path that will executed anytime user program crash, it's executed as high privilege, so we want this program that will gives us root shell.

Using any generic rop gadget available, we overwrite `core_pattern` with our program path using `copy_from_user` then simply call `msleep`. Another thread of our exploit notice the `/proc/sys/kernel/core_pattern` changes, it will try to crash itself so our exploit will executed as high privilege and gives us root shell to get the flag.
14 changes: 14 additions & 0 deletions pocs/linux/kernelctf/CVE-2024-53141_lts/docs/vulnerability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
- Requirements:
- Capabilites: CAP_NET_ADMIN
- Kernel configuration: CONFIG_IP_SET_BITMAP_IP=y
- User namespaces required: Yes
- Introduced by: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=72205fc68bd13109576aa6c4c12c740962d28a6c
- Fixed by: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=35f56c554eb1b56b77b3cf197a6b00922d49033d
- Affected Version: v2.7 - v6.12
- Affected Component: netfilter
- Syscall to disable: unshare
- URL: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-53141
- Cause: Out-of-bounds access
- Description: An out-of-bounds access vulnerability in the Linux kernel's netfilter.
When tb[IPSET_ATTR_IP_TO] is not present but tb[IPSET_ATTR_CIDR] exists,
the values of ip and ip_to are slightly swapped, the range check for ip is missed and causes out-of-bounds access. We recommend upgrading past commit 35f56c554eb1b56b77b3cf197a6b00922d49033d
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# taken from: https://github.com/google/security-research/blob/1bb2f8c8d95a34cafe7861bc890cfba5d85ec141/pocs/linux/kernelctf/CVE-2024-0193_lts/exploit/lts-6.1.67/Makefile

LIBMNL_DIR = $(realpath ./)/libmnl_build
LIBNFTNL_DIR = $(realpath ./)/libnftnl_build

LIBS = -L$(LIBNFTNL_DIR)/install/lib -L$(LIBMNL_DIR)/install/lib -lnftnl -lmnl
INCLUDES = -I$(LIBNFTNL_DIR)/libnftnl-1.2.5/include -I$(LIBMNL_DIR)/libmnl-1.0.5/include
CFLAGS = -static -s

exploit: exploit.c
gcc -o exploit exploit.c $(LIBS) $(INCLUDES) $(CFLAGS)


prerequisites: libnftnl-build

libmnl-build : libmnl-download
tar -C $(LIBMNL_DIR) -xvf $(LIBMNL_DIR)/libmnl-1.0.5.tar.bz2
cd $(LIBMNL_DIR)/libmnl-1.0.5 && ./configure --enable-static --prefix=`realpath ../install`
cd $(LIBMNL_DIR)/libmnl-1.0.5 && make -j`nproc`
cd $(LIBMNL_DIR)/libmnl-1.0.5 && make install

libnftnl-build : libmnl-build libnftnl-download
tar -C $(LIBNFTNL_DIR) -xvf $(LIBNFTNL_DIR)/libnftnl-1.2.5.tar.xz
cd $(LIBNFTNL_DIR)/libnftnl-1.2.5 && PKG_CONFIG_PATH=$(LIBMNL_DIR)/install/lib/pkgconfig ./configure --enable-static --prefix=`realpath ../install`
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 -j`nproc`
cd $(LIBNFTNL_DIR)/libnftnl-1.2.5 && make install

libmnl-download :
mkdir $(LIBMNL_DIR)
wget -P $(LIBMNL_DIR) https://netfilter.org/projects/libmnl/files/libmnl-1.0.5.tar.bz2

libnftnl-download :
mkdir $(LIBNFTNL_DIR)
wget -P $(LIBNFTNL_DIR) https://netfilter.org/projects/libnftnl/files/libnftnl-1.2.5.tar.xz

run:
./exploit

clean:
rm -f exploit
rm -rf $(LIBMNL_DIR)
rm -rf $(LIBNFTNL_DIR)
Binary file not shown.
Loading
Loading