有趣的Linux内核堆喷技术:prctl anon_vma_name解析

本文详细介绍了使用prctl PR_SET_VMA_ANON_NAME作为新型堆喷技术的方法,该技术可针对kmalloc-8到kmalloc-96缓存进行堆喷,通过动态大小的anon_vma_name对象实现内存分配与释放,并可通过/proc/pid/maps文件读取泄露信息。

prctl anon_vma_name: 一种有趣的Linux内核堆喷技术

TLDR

prctl PR_SET_VMA (PR_SET_VMA_ANON_NAME) 可作为一种(可能是新的!)堆喷方法,针对 kmalloc-8 到 kmalloc-96 缓存。被喷的对象 anon_vma_name 是动态大小的,范围可以从大于4字节到最大84字节。该对象可以通过 prctl 系统调用轻松分配和释放,并且可以通过读取 /proc/pid/maps 文件获取泄露信息。这种方法的优点是不需要从 cg/其他缓存进行跨缓存攻击(与 msg_msg 等其他对象不同),因为 anon_vma_name 是使用 GFP_KERNEL 标志分配的。

引言和背景故事

在实习期间,我遇到了一个内核 pwn CTF 挑战,目的是学习一些基础技术。涉及的漏洞是一个导致写入的竞争条件,挑战的一部分包括泄露一个随机生成的密钥,该密钥会在数据写入内存位置之前与数据进行异或操作。然而,写入的方式意味着它会从目标内存位置的开头写入大量字节(由于不知道异或密钥,写入是不可控的),并且会破坏许多常见可喷对象的头部,如 msg_msg、setxattr 或 add_key。对象大小的限制以及其分配到 GFP_KERNEL kmalloc 缓存也意味着,为了喷一些这些常见对象,我不得不执行跨缓存攻击,而对于某些对象(如 sk_buff),跨缓存可能涉及不同阶的页面 :(((

在绝望中寻找一个稍微不那么烦人的可喷对象时,我开始查看系统调用。理想情况下,我想要一些东西:

  • 可以从用户空间分配和释放
  • 可以从用户空间读取
  • 相当无用/有无用的头部(这样如果我用随机垃圾覆盖头部并尝试释放对象,不会导致内核恐慌)

符合这些条件的对象可能是那些存储字符串的对象,这些字符串是某些东西的名称(例如主机名),尽管 uname 使用的结构(我原本希望是一个可行的喷)不幸地分配在栈上。然后,我找到了 prctl。

什么是 prctl?

根据 Linux man 页面,prctl() 操作调用线程或进程行为的各个方面。

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

它做了很多不同的事情,但我感兴趣的选项是 PR_SET_VMA 及其子选项 PR_SET_VMA_ANON_NAME。基本上,PR_SET_VMA 为虚拟内存区域 arg3 设置 arg2 中定义的属性,大小为 arg4。arg5 指定要设置的属性值。如果 arg2 是 PR_SET_VMA_ANON_NAME,它将为一个匿名虚拟内存区域设置一个名称。这听起来像是有无用、可喷的结构!

为了使这工作,必须启用选项 CONFIG_ANON_VMA_NAME。据我所知,这在默认的 ubuntu 内核配置中是启用的。对于这个挑战,我使用的是 Linux 内核版本 6.1.37。

让我们看看 anon_vma_name 结构:

1
2
3
4
5
6
7
8
struct anon_vma_name {
  struct kref kref;
  char name[]; /* 名称需要在末尾,因为它是动态大小的。 */
};

struct kref {
  refcount_t refcount;
};

看起来非常无用!头部将是4字节(因为 struct kref 是4字节),而 name 是动态大小的。名称字符数组的最大大小是80,因此这个对象的大小范围可以从大于4字节到84字节。

让我们跟踪内核代码流:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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 有自己的副本 */
			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;
}

在上面的代码中,它检查([1])输入的名称是否有有效的可打印字符。如果检查通过([2]),则调用 anon_vma_name_alloc:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct anon_vma_name *anon_vma_name_alloc(const char *name)
{
	struct anon_vma_name *anon_name;
	size_t count;

	/* 为 anon_name->name 末尾的 NUL 终止符添加1 */
	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;
}

这里([3]),结构通过 kmalloc 分配,并且由于指定了 GFP_KERNEL,它将进入正常的 kmalloc 缓存。这非常方便,因为我们可以基本上将其喷入从 kmalloc-8 到 kmalloc-96 的任何缓存,并且我们可以避免从 cg 缓存进行任何烦人的跨缓存!

让我们看看如何从喷中读取(感谢我的导师的这一部分!)。查看 show_map_vma 函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
	if (file) {
		seq_pad(m, ' ');
		/*
		 * 如果用户通过 prctl(PR_SET_VMA ...) 命名了这个匿名共享内存,
		 * 使用提供的名称。
		 */
		if (anon_name)
			seq_printf(m, "[anon_shmem:%s]", anon_name->name);
		else
			seq_file_path(m, file, "\n");
		goto done;
	}

由于使用了新设置的名称,我们可以通过 /proc/pid/maps 文件从中读取,从而可能从 maps 文件中泄露内核内存中的内容。然而,这种方法的限制是,如果要泄露的信息包含空字节,它将打印到那些空字节然后停止。如果你幸运并且有一个没有空字节的内核文本/堆指针,这种方法可能用于绕过 KASLR。

让我们看看喷如何被释放:

1
2
3
4
5
6
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);
}

推测这个对象是如何释放的类似于文件是如何释放的。当进程死亡,或者如果再次调用 prctl 并将名称缓冲区设置为 NULL,引用计数会递减。如果引用计数变为0,anon_vma_name 对象被释放。

如何喷 anon_vma_name

喷 anon_vma_name 很简单:只需使用带有 PR_SET_VMA 和 PR_SET_VMA_ANON_NAME 参数的 prctl 系统调用。一种方法如下所示(代码不好请见谅):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#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);
    }
}

分配的名称需要不同,因为如果使用相同的名称,内核会重用相同的 anon_vma_name 对象,喷将失败。这在内核代码中显示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static inline
struct anon_vma_name *anon_vma_name_reuse(struct anon_vma_name *anon_name)
{
	/* 防止 anon_name 引用计数过早饱和 */
	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);
}

在 gdb 中,喷看起来如下(喷上方的垃圾数据是我想读取的,由于挑战的工作方式,它被写入与喷对象相同的地址):

[图片描述]

为了从喷中读取,你只需要 cat /proc/pid/maps 文件!

[图片描述]

你也可以在代码中以编程方式完成:

[图片描述]

最后的十六进制数是写入 anon_vma_name 对象的泄露密钥,与第一张图片中的相同。

正如我的实习生同事很好地指出的,这相当于通过任务管理器从内核内存泄露东西 :D

要释放喷,你只需要再次使用 prctl 系统调用,但这次将名称缓冲区设置为 NULL。

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

结论

prctl 的 PR_SET_VMA 可以用作一种很好的方便堆喷到 kmalloc-8 到 kmalloc-96。感谢 Billy 的指导,我希望在接下来的几周里学习并找到更多有趣的东西!:D

参考文献

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计