【CVE.0x0E】CVE-2024-26816 漏洞分析及利用

本文最后更新于:2025年8月2日 凌晨

KASLR:AM I A JOKE TO YOU?

0x00. 一切开始之前

CVE-2024-26816 是一个发生在 Linux kernel 的 Xen 子模块当中的信息泄漏漏洞,得益于 Xen 模块在开启了 CONFIG_XEN_PV=y 编译选项时把 startup_xen 的地址写到了非特权用户可读的 /sys/kernel/notes 文件当中,攻击者可以通过读取该文件泄漏出内核加载的基地址,从而通过这个漏洞绕过 KASLR

该漏洞的 CVSS 分数为 5.5 ,影响版本包括但不限于 2.6.23~4.19.3114.20~5.4.2735.5~5.10.2145.11~5.15.1535.16~6.1.836.2~6.6.236.7~6.7.116.8~6.8.26.8.3~6.8.12,因为漏洞分数是 5.5 所以本文我们选用 5.5 版本的内核源码进行分析:)

0x01. 漏洞分析

我们首先看 startup_xen 这个符号都在哪出现过,在 5.5 版本的内核当中这个符号只出现过三次, startup_xen 本身是一个在开启了 CONFIG_XEN_PV=y 编译选项时用来标识段基址的符号,定义于 /arch/x86/xen/xen-head.S 当中:

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
#ifdef CONFIG_XEN_PV
__INIT
SYM_CODE_START(startup_xen)
UNWIND_HINT_EMPTY
cld

/* Clear .bss */
xor %eax,%eax
mov $__bss_start, %_ASM_DI
mov $__bss_stop, %_ASM_CX
sub %_ASM_DI, %_ASM_CX
shr $__ASM_SEL(2, 3), %_ASM_CX
rep __ASM_SIZE(stos)

mov %_ASM_SI, xen_start_info
mov $init_thread_union+THREAD_SIZE, %_ASM_SP

#ifdef CONFIG_X86_64
/* Set up %gs.
*
* The base of %gs always points to fixed_percpu_data. If the
* stack protector canary is enabled, it is located at %gs:40.
* Note that, on SMP, the boot cpu uses init data section until
* the per cpu areas are set up.
*/
movl $MSR_GS_BASE,%ecx
movq $INIT_PER_CPU_VAR(fixed_percpu_data),%rax
cdq
wrmsr
#endif

jmp xen_start_kernel
SYM_CODE_END(startup_xen)
__FINIT
#endif

同时在 /arch/x86/xen/xen-head.S 当中有如下配置,当开启了 CONFIG_XEN_PV=y 编译选项时,我们有这样一条 ELFNOTE ,写入了 startup_xen 的地址:

1
2
3
#ifdef CONFIG_XEN_PV
ELFNOTE(Xen, XEN_ELFNOTE_ENTRY, _ASM_PTR startup_xen)
#endif

这个 ELFNOTE 是个什么东西?让我们看看/include/linux/elfnote.h 当中的注释,简而言之意思就是说这些信息会被放到 obj 文件的名为 .note.* 的段当中,最后在链接时被放到内核 ELF 镜像 vmlinux 的一个名为 .notes 的段当中,段的 p_typePT_NOTE ,每个 note 的包含 nametypedesc 三部分,对于前面定义的 ELFNOTE,我们的 desc 为指向 startup_xen 的指针:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
/*
* Helper macros to generate ELF Note structures, which are put into a
* PT_NOTE segment of the final vmlinux image. These are useful for
* including name-value pairs of metadata into the kernel binary (or
* modules?) for use by external programs.
*
* Each note has three parts: a name, a type and a desc. The name is
* intended to distinguish the note's originator, so it would be a
* company, project, subsystem, etc; it must be in a suitable form for
* use in a section name. The type is an integer which is used to tag
* the data, and is considered to be within the "name" namespace (so
* "FooCo"'s type 42 is distinct from "BarProj"'s type 42). The
* "desc" field is the actual data. There are no constraints on the
* desc field's contents, though typically they're fairly small.
*
* All notes from a given NAME are put into a section named
* .note.NAME. When the kernel image is finally linked, all the notes
* are packed into a single .notes section, which is mapped into the
* PT_NOTE segment. Because notes for a given name are grouped into
* the same section, they'll all be adjacent the output file.
*
* This file defines macros for both C and assembler use. Their
* syntax is slightly different, but they're semantically similar.
*
* See the ELF specification for more detail about ELF notes.
*/

#ifdef __ASSEMBLER__
/*
* Generate a structure with the same shape as Elf{32,64}_Nhdr (which
* turn out to be the same size and shape), followed by the name and
* desc data with appropriate padding. The 'desctype' argument is the
* assembler pseudo op defining the type of the data e.g. .asciz while
* 'descdata' is the data itself e.g. "hello, world".
*
* e.g. ELFNOTE(XYZCo, 42, .asciz, "forty-two")
* ELFNOTE(XYZCo, 12, .long, 0xdeadbeef)
*/
#define ELFNOTE_START(name, type, flags) \
.pushsection .note.name, flags,@note ; \
.balign 4 ; \
.long 2f - 1f /* namesz */ ; \
.long 4484f - 3f /* descsz */ ; \
.long type ; \
1:.asciz #name ; \
2:.balign 4 ; \
3:

#define ELFNOTE_END \
4484:.balign 4 ; \
.popsection ;

#define ELFNOTE(name, type, desc) \
ELFNOTE_START(name, type, "") \
desc ; \
ELFNOTE_END

现在我们来看这些 .note.name 段如何被处理的,在 /include/asm-generic/vmlinux.lds.h 当中将其合并为了一个 .note 段,其起始为 __start_notes ,末尾为 __stop_notes

lds 格式的 Linker Script 内容,可以参考 https://wiki.osdev.org/Linker_Scripts

1
2
3
4
5
6
7
#define NOTES								\
.notes : AT(ADDR(.notes) - LOAD_OFFSET) { \
__start_notes = .; \
KEEP(*(.note.*)) \
__stop_notes = .; \
} NOTES_HEADERS \
NOTES_HEADERS_RESTORE

最后在 /include/asm-generic/vmlinux.lds.h 当中这个段被放到 read-only 的大区块里:

1
2
3
4
5
6
7
8
9
10
/*
* Read only Data
*/
#define RO_DATA(align) \
. = ALIGN((align)); \
//...
NOTES \
\
. = ALIGN((align)); \
__end_rodata = .;

现在我们来看这些信息会被在内核的什么地方使用,首先转到 /sys/kernel/notes 的内核逻辑,在 /kernel/ksysfs.cksysfs_init() 当中会首先创建一个 "kernel" 目录,之后创建一个名为 "notes" 的文件:

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
static struct bin_attribute notes_attr __ro_after_init  = {
.attr = {
.name = "notes",
.mode = S_IRUGO,
},
.read = &notes_read,
};

//...

static int __init ksysfs_init(void)
{
int error;

kernel_kobj = kobject_create_and_add("kernel", NULL);
if (!kernel_kobj) {
error = -ENOMEM;
goto exit;
}
error = sysfs_create_group(kernel_kobj, &kernel_attr_group);
if (error)
goto kset_exit;

if (notes_size > 0) {
notes_attr.size = notes_size;
error = sysfs_create_bin_file(kernel_kobj, &notes_attr);
if (error)
goto group_exit;
}
//...

注意到其读取函数为 notes_read() ,该函数的主要作用便是拷贝 .notes 段的信息给用户:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* Make /sys/kernel/notes give the raw contents of our kernel .notes section.
*/
extern const void __start_notes __weak;
extern const void __stop_notes __weak;
#define notes_size (&__stop_notes - &__start_notes)

static ssize_t notes_read(struct file *filp, struct kobject *kobj,
struct bin_attribute *bin_attr,
char *buf, loff_t off, size_t count)
{
memcpy(buf, &__start_notes + off, count);
return count;
}

那么 vmlinux 的 .note 段在什么时候进入内存的呢?我们不难想到的是 vmlinux 在系统启动时会被 GRUB 作为 ELF 解析并载入内存,简而言之 Linux kernel 分为两部分,一份是 kernel loader ( arch/x86/boot/compressed ),另一份是实际的但经过压缩的 kernel,在计算机的启动链当中 GRUB 负责载入 loader,而 loader 负责解压并载入 kernel,对应 /arch/x86/boot/compressed/misc.c 当中的 extract_kernel() 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* The compressed kernel image (ZO), has been moved so that its position
* is against the end of the buffer used to hold the uncompressed kernel
* image (VO) and the execution environment (.bss, .brk), which makes sure
* there is room to do the in-place decompression. (See header.S for the
* calculations.)
*
* |-----compressed kernel image------|
* V V
* 0 extract_offset +INIT_SIZE
* |-----------|---------------|-------------------------|--------|
* | | | |
* VO__text startup_32 of ZO VO__end ZO__end
* ^ ^
* |-------uncompressed kernel image---------|
*
*/
asmlinkage __visible void *extract_kernel(void *rmode, memptr heap,
unsigned char *input_data,
unsigned long input_len,
unsigned char *output,
unsigned long output_len)

在该函数中 KASLR 在 choose_random_location() 当中获取偏移,之后通过 __decompress() 函数解压内核镜像,并通过 parse_elf() 函数解析 ELF header 并移动类型为 PT_LOAD 的段到对应位置,最后调用 handle_relocations() 处理需要重定位的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
	choose_random_location((unsigned long)input_data, input_len,
(unsigned long *)&output,
needed_size,
&virt_addr);

//...

debug_putstr("\nDecompressing Linux... ");
__decompress(input_data, input_len, NULL, NULL, output, output_len,
NULL, error);
parse_elf(output);
handle_relocations(output, output_len, virt_addr);
debug_putstr("done.\nBooting the kernel.\n");
return output;
}

handle_relocations() 函数主要就是解析 relocation 表并修正其中的值,基本上没什么好看的,这里就不贴代码了:)

简而言之 relocation table 当中存放了需要进行重定位的符号的地址,感兴趣的可以自行翻阅 ELF 规范,这里不再赘述

0x02. 漏洞利用

只需要直接读取 /sys/kernel/notes 就彳亍,偏移也是固定的,感觉没什么好说的:)唯一需要注意的可能就是指针的值是以小端序存储的:

0x03. 漏洞修复

该漏洞在 76e9762d66373354b45c33b60e9a53ef2a3c5ff2 等 commit 当中被修复(不同分支 commit id 不一样,但是内容相同, 懒得看哪个是 mainline 了 ),修复方式是不对 .notes 段当中的符号进行重定位,这样在开启 KASLR 的情况下 /sys/kernel/notes 当中的符号地址并不会被进行重定位修正,从而也就避免了 KASLR 被 bypass,笔者个人觉得这个修复算是中规中矩吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

diff --git a/arch/x86/tools/relocs.c b/arch/x86/tools/relocs.c
index b029fb81ebeee0..e7a44a7f617fbe 100644
--- a/arch/x86/tools/relocs.c
+++ b/arch/x86/tools/relocs.c
@@ -746,6 +746,15 @@ static void walk_relocs(int (*process)(struct section *sec, Elf_Rel *rel,
if (!(sec_applies->shdr.sh_flags & SHF_ALLOC)) {
continue;
}
+
+ /*
+ * Do not perform relocations in .notes sections; any
+ * values there are meant for pre-boot consumption (e.g.
+ * startup_xen).
+ */
+ if (sec_applies->shdr.sh_type == SHT_NOTE)
+ continue;
+
sh_symtab = sec_symtab->symtab;
sym_strtab = sec_symtab->link->strtab;
for (j = 0; j < sec->shdr.sh_size/sizeof(Elf_Rel); j++) {

【CVE.0x0E】CVE-2024-26816 漏洞分析及利用
https://arttnba3.github.io/2025/07/29/CVE-0X0E-CVE-2024-26816/
作者
arttnba3
发布于
2025年7月29日
许可协议