前言 & 背景
CVE-2024-26816
1:/sys/kernel/notes
的内容是当前运行的 vmlinux 镜像的 .notes
节,里面存有内核函数 hypercall_page
的地址。此信息可用于绕过 KASLR 保护。
探究内容:x86 内核启动过程中,KASLR 对内核整体镜像做偏移时,是如何修改 .notes
节中的 hypercall_page
地址的?
分析过程
初始链接阶段
对于 OLK-6.6 的 x86 内核,若编译时开启了 CONFIG_XEN
选项,则 arch/x86/xen/xen-head.S
会将汇编函数 hypercall_page
加入到 vmlinux 镜像的 .note.Xen
节:
1/*
2 * arch/x86/xen/xen-head.S
3 *
4 * It is included in arch/x86/kernel/head_64.S:
5 * #include "../../x86/xen/xen-head.S"
6 */
7
8#ifdef CONFIG_XEN
9SYM_CODE_START(hypercall_page)
10 ...
11SYM_CODE_END(hypercall_page)
12
13 /* .note.Xen */
14 ELFNOTE(Xen, XEN_ELFNOTE_HYPERCALL_PAGE, _ASM_PTR hypercall_page)
15
16#endif
链接脚本 arch/x86/kernel/vmlinux.lds.S
中指出,vmlinux 的 .notes
节由各链接文件的 .note.*
节组合形成:
1/* arch/x86/kernel/vmlinux.lds.S */
2SECTIONS
3{
4 .text : ... {
5 ...
6 }
7 /* include/asm-generic/vmlinux.lds.h */
8 RO_DATA(PAGE_SIZE) --(expand)--> NOTES --(expand)-->
9 .notes : ... {
10 BOUNDED_SECTION_BY(.note.*, _notes)
11 }
12 .data : ... {
13 ...
14 }
15}
16
17/*
18 生成 arch/x86/kernel/vmlinux.lds 中的:
19 .notes : AT(ADDR(.notes) - 0xffffffff80000000) { __start_notes = .; KEEP(*(.note.*)) __stop_notes = .; }
20*/
在链接生成可执行2的 vmlinux 时,arch/x86/Makefile
指定了链接器选项 --emit-relocs
:
1ifdef CONFIG_X86_NEED_RELOCS
2LDFLAGS_vmlinux := --emit-relocs --discard-none
3else
4LDFLAGS_vmlinux :=
5endif
该选项为 vmlinux 中所有涉及*重定位(Relocation)*的节3生成对应的 .rela.*
节。由于 .notes
节带有 hypercall_page
,而后者是一个全局符号4涉及重定位,因此会生成 .rela.notes
节。注意 rela.notes
的 .info
字段(即下方的 Inf
)的值为 22,为 .notes
的序号。
1$ readelf -SW vmlinux
2[Nr] Name Type Address Off Size ES Flg Lk Inf Al
3...
4[22] .notes NOTE ffffffff8255a2fc 175a2fc 0000f0 00 A 0 0 4
5[23] .rela.notes RELA 0000000000000000 18571990 000018 18 I 81 22 8
“链接后”阶段
vmlinux 完成链接并生成后,arch/x86/Makefile.postlink
会被触发。此文件的开头注释解释了这一*阶段(Pass)*的工作:
- Separate relocations from vmlinux into vmlinux.relocs.
- Strip relocations from vmlinux.
具体体现为该文件中 $(call cmd,relocs)
和 $(call cmd,strip_relocs)
两个过程:
1CMD_RELOCS = arch/x86/tools/relocs
2OUT_RELOCS = arch/x86/boot/compressed
3quiet_cmd_relocs = RELOCS $(OUT_RELOCS)/$@.relocs
4 cmd_relocs = \
5 mkdir -p $(OUT_RELOCS); \
6 $(CMD_RELOCS) $@ > $(OUT_RELOCS)/$@.relocs; \
7 $(CMD_RELOCS) --abs-relocs $@
8
9# scripts/Makefile.lib
10quiet_cmd_strip_relocs = RSTRIP $@
11 cmd_strip_relocs = \
12 $(OBJCOPY) --remove-section='.rel.*' --remove-section='.rel__*' \
13 --remove-section='.rela.*' --remove-section='.rela__*' $@
14
15# `@true` prevents complaint when there is nothing to be done
16
17vmlinux: FORCE
18 @true
19ifeq ($(CONFIG_X86_NEED_RELOCS),y)
20 $(call cmd,relocs)
21 $(call cmd,strip_relocs)
22endif
重点关注 relocs
过程,将 cmd_relocs
中的 Makefile 语句展开后得到如下 Bash 命令:
1mkdir -p arch/x86/boot/compressed
2arch/x86/tools/relocs vmlinux > arch/x86/boot/compressed/vmlinux.relocs
3arch/x86/tools/relocs --abs-relocs vmlinux
通过解读 arch/x86/tools/relocs
工具的源码,其实质是按照 ELF 格式解析 vmlinux,通过节头表(Section Header)遍历所有的节,找到其中的 .rela
节并从 r_offset
5 中获取所有需要被重定位修改的代码地址,将这些地址罗列成一个列表 relocs64
,并最终将其输出形成 vmlinux.relocs
。
1static int do_reloc64(..., Elf_Rel *rel, ...) {
2 unsigned r_type = ELF64_R_TYPE(rel->r_info);
3 ElfW(Addr) offset = rel->r_offset;
4 switch (r_type) {
5 case R_X86_64_64:
6 add_reloc(&relocs64, offset);
7 break;
8 }
9}
10
11static void emit_relocs(int as_text, int use_real_mode) {
12 do_reloc = do_reloc64;
13 walk_relocs(do_reloc); // iterate through all sections, process those of .rela
14 sort_relocs(&relocs64);
15 for (i = 0; i < relocs64.count; i++)
16 write_reloc(relocs64.offset[i], stdout);
17}
随后,根据 arch/x86/boot/compressed/Makefile
,将 vmlinux.bin
与 vmlinux.relocs
前后拼接在一起形成 vmlinux.bin.all
,并最终经过压缩形成内核镜像产物。
1# vmlinux.bin is:
2# vmlinux stripped of debugging and comments
3# vmlinux.bin.all is:
4# vmlinux.bin + vmlinux.relocs
5vmlinux.bin.all-y := $(obj)/vmlinux.bin
6vmlinux.bin.all-$(CONFIG_X86_NEED_RELOCS) += $(obj)/vmlinux.relocs
实践验证方式
可以通过编译一个简单的 C 程序,来了解重定位节的内容。
1# 向链接器传入 --emit-relocs 选项, 2# 生成带有重定位节的可执行文件 3echo "int main() { return 0; }" | gcc -x c - -Wl,--emit-relocs -g 4 5# 解析重定位节,其中 .rela.text 6# 会包含关于 main() 的重定位信息 7readelf -r ./a.out 8 9# 通过 GDB 查看其 Offset 列的地址,可以看到 10# 就是 start() 调用 main() 的位置
内核启动阶段
在完成建立早期页表
、进入 64 位模式等一系列工作后,执行流转入内核镜像自带的解压器,开始执行解压内核的工作,即 arch/x86/boot/compressed/misc.c
中的 extract_kernel()
。此过程涉及内核地址随机化(KASLR):
1/*
2 * input_{data,len}: 压缩镜像的起始地址和长度,全局变量
3 */
4extract_kernel()
5 choose_random_location(input_data, input_len, &output, ..., &virt_addr)
6 random_addr = find_random_phys_addr(min_addr, output_size);
7 *output = random_addr;
8 random_addr = find_random_virt_addr(LOAD_PHYSICAL_ADDR, output_size);
9 *virt_addr = random_addr;
10 entry_offset = decompress_kernel(outbuf:output, virt_addr, ...)
11 __decompress(input_data, input_len, ..., outbuf, output_len, ...)
12 handle_relocations(outbuf, output_len, virt_addr)
KASLR 的本质是整体内核镜像的随机偏移。可以看到,choose_random_location()
选定的物理地址空间偏移量和虚拟地址空间偏移量分别体现在 output
和 virt_addr
两个地址上。__decompress()
将内核解压缩至 output
地址,此时 output
上承载的就是上文提到的 vmlinux.bin.all
。随后 handle_relocations()
开始解析 vmlinux.bin.all
,从中找到 vmlinux.relocs
,并结合这些重定向信息以及随机偏移,对内核镜像中各个需要重定位的位置实施修改。
1void handle_relocations(void *output, unsigned long output_len, unsigned long virt_addr)
2{
3 ...
4 /*
5 * ... Each relocation table entry is the kernel
6 * address of the location which needs to be updated stored as a
7 * 32-bit value which is sign extended to 64 bits.
8 *
9 * Format is:
10 *
11 * kernel bits...
12 * 0 - zero terminator for 64 bit relocations
13 * 64 bit relocation repeated
14 * 0 - zero terminator for inverse 32 bit relocations
15 * 32 bit inverse relocation repeated
16 * 0 - zero terminator for 32 bit relocations
17 * 32 bit relocation repeated
18 *
19 * So we work backwards from the end of the decompressed image.
20 */
21 for (reloc = output + output_len - sizeof(*reloc); *reloc; reloc--) {
22 ...
23 }
24#ifdef CONFIG_X86_64
25 while (*--reloc) {
26 ...
27 }
28 for (reloc--; *reloc; reloc--) {
29 ...
30 }
31#endif
32}
注意:此刻解压缩器应该已经在使用虚拟地址,并且(早期)页表已有建立,但这个页表应该实现的是 VA-PA 完全一致的映射,即 VA 的值完全等于 PA。而 .rela.*
节中记录的是内核加载的虚拟地址6:
1/* arch/x86/kernel/vmlinux.lds.S */
2SECTIONS
3{
4 . = __START_KERNEL;
5 .text : ... {
6 ...
7 }
8 ...
9}
10/* arch/x86/include/asm/page_types.h */
11#define LOAD_PHYSICAL_ADDR ((CONFIG_PHYSICAL_START ...
12#define __START_KERNEL (__START_KERNEL_map + LOAD_PHYSICAL_ADDR)
13/* arch/x86/include/asm/page_64_types.h */
14#define __START_KERNEL_map _AC(0xffffffff80000000, UL)
因此有了 map
这个变量,作用于 extended
上。这个 extended
是(由 .rela.*
记录的)内核镜像中引用了某个全局符号的位置,本应属于内核编译时地址,但由于有链接器脚本,该地址等价于内核加载虚拟地址。由于此刻正在(通过 self map)使用物理地址,因此通过 map
将其转变为带偏移的内核加载物理地址。
最后再对这些内核镜像位置实施修改,添加运行时虚拟地址的随机偏移量。
1void handle_relocations(void *output, unsigned long output_len, unsigned long virt_addr)
2{
3 unsigned long delta, map, ptr;
4 unsigned long min_addr = (unsigned long)output;
5
6 delta = min_addr - LOAD_PHYSICAL_ADDR;
7
8 /*
9 * The kernel contains a table of relocation addresses. Those
10 * addresses have the final load address of the kernel in virtual
11 * memory. We are currently working in the self map. So we need to
12 * create an adjustment for kernel memory addresses to the self map.
13 * This will involve subtracting out the base address of the kernel.
14 */
15 map = delta - __START_KERNEL_map;
16
17 if (IS_ENABLED(CONFIG_X86_64))
18 delta = virt_addr - LOAD_PHYSICAL_ADDR;
19
20 ...
21
22 /* 64 bit relocation repeated */
23 for (reloc--; *reloc; reloc--) {
24 long extended = *reloc;
25 extended += map; // 内核镜像中某个使用到全局符号的位置(全局符号被调用处)
26
27 ptr = (unsigned long)extended;
28 if (ptr < min_addr || ptr > max_addr)
29 error("64-bit relocation outside of kernel!\n");
30
31 *(uint64_t *)ptr += delta; // 修改镜像中的指令内容,调整全局符号的地址
32 }
33}
参考资料
- 问题初始披露与讨论
- NVD - CVE-2024-26816 以及后续修复
- Executable and Linkable Format 101 Part 3: Relocations
- Relocation – Generic ABI (gABI) 4+
sh_link
andsh_info
Interpretation – Sections – Generic ABI (gABI) 4+
-
与 CVE 关联的补丁并不能修复这个问题,要打上后续修补补丁:76e9762d6637 (“x86/boot: Ignore relocations in .notes sections in walk_relocs() too”) 才能真正完成修复。 ↩︎
-
即类型为
ET_EXEC
的 ELF 文件。 ↩︎ -
可理解为带有*可重定位(relocatable)*的符号。 ↩︎
-
关于这些与重定位相关的字段的具体含义,可以阅读 System V ABI 的描述。 ↩︎
-
用到的
__START_KERNEL_map
为 x86 虚拟地址空间排布 中映射至物理地址 0 处的虚拟地址,由此开始的 512 MB 的空间为内核代码的映射区。 ↩︎