kaslr全称为kernel address space layout randomization,是linux内核是一种非常重要的安全机制,可以使kernel image与链接地址相比,映射的执行地址有偏移,使内核符号地址随机,提高内核的安全性和防攻击能力。
KASLR实现原理比较简单,在内核启动阶段,获得一个kernel image偏移值可以通过dtb或者bios传输,或随机源。
Linux内核支持arm、x86_64、PowerPC在不同的架构下,不同的架构,kaslr实现方式不同,但核心思想是增加随机偏移。在内核启动阶段,通过获取随机值,随机偏移内核加载地址。偏移值可以通过dtb传输也可以基于随机源生成。内核数据随机映射完成后,还需要重新定位符号地址,校正内核代码的符号地址,以确保内核代码的正常执行。以arm64 5.10内核为例,kaslr因此,在代码实现过程中,主要是通过改变原来固定的内存布局来提高内核安全性,kaslr与内存功能有很强的耦合关系,Linux内存的虚拟地址空间布局与内核编译配置有关,不同的配置会产生不同的地址空间模型,以4KB pages 4 levels (48-bit)例如,虚拟地址空间模型如下:(详细内存布局信息可参考内核文件:Documentation/arm64/memory.rst)
如图所示,Arm64虚拟地址空间可分为两部分:核心地址空间和用户地址空间。通常,核心地址空间由核心代码分配,所有过程共享核心地址空间,用户地址空间由用户状态过程占据。在内核启动阶段,内核镜像的加载、解压和运行均在内核地址空间内完成,kaslr内核内存布局也主要在这里随机调整,arm64内存布局主要分为三个部分:内核镜像、线性映射区和module在随机过程中,随机种子产生如下:
rm随机种子通过64dtb获取文件,所以会dtb地区建立映射,从dtb文件解析kaslr-seed同时获得属性配置dtb里面的command line配置,
开启kaslr后,head.S中调用
>>get_cmdline
判断它是否存在nokaslr当没有随机种子或配置参数时nokaslr参数时,kaslr关闭并修改相应的功能kaslr_status状态变量为KASLR_DISABLED_CMDLINE或者KASLR_DISABLED_NO_SEED。由dtb文件获取到kaslr-seed配置后,种子将根据不同的处理方法进行处理,分别用于镜像、线性区域和module随机区域,如下图所示:
offset对于镜像的随机偏移值,内核需要确保偏移地址2M对齐,同时通过(VA_BITS_MIN - 2)限制内核随机范围vmalloc区域中间的一半,避免使用头部和尾部的1/4区域,以避免跟其他的内存分配特性冲突。memstart_offset_seed内核是线性区域的随机种子seed高16位作为线性区域的随机值。Arm64当内存初始化时,内核通过线性映射将物理内存完全映射到虚拟地址空间的线性映射区域,如下图所示:
随机范围是线性区减去物理内存的大小,限制偏移粒度ARM64_MEMSTART_ALIGN(256MB),线性区的使用主要涉及virt_to_phys和phys_to_virt代码实现如下:
以virt_to_phys为例,通过virt_to_phys可以看到接口的实现memstart_addr物理地址的起始值,PAGE_OFFSET是虚拟地址中核心地址空间的起始位置,物理地址与虚拟地址之间存在以下转换关系:物理地址 = 虚拟地址 – PAGE_OFFSET memstart_addr 因此,内核可以通过调整来调整memstart_addr线性区域映射关系的随机偏移值。Kernel系统启动初期,__primary_switched函数调用kaslr_early_init函数初始化随机偏移值并保存x在23个寄存器中,如下图所示:
后续进行kernel image虚拟地址映射将在内核映射中添加虚拟地址kaslr随机偏移(x23寄存器),如下图所示:
完成kernel image虚拟地址映射后,需要调用__relocate_kernel函数实现如下:
__relocate_kernel符号重定位主要用于重定位段,重定位段包含内核执行过程中需要使用的变量符号,如_stext、_etext,链接时确定这些符号对应的地址,使能够kaslr之后,kernel image运行过程中的虚拟地址与编译确定的原始虚拟地址不同,若不进行重定位操作,则内核不能正常执行。
使能条件
kaslr功能通过CONFIG_RANDOMIZE_BASE进行控制。 cmdline中不能存在nokaslr参数,否则kaslr不被使能。
随机种子
通过dts指定随机种子如下:
通过内核符号地址信息,可以观察内核符号加载状态,以此判断kaslr特性是否生效,命令如下: echo 0 > /proc/sys/kernel/kptr_restrict head /proc/kallsyms
openEuler Kernel SIG
openEuler kernel 源代码仓库:https://gitee.com/openeuler/kernel 欢迎大家多多 star、fork,多多参与社区开发,多多贡献补丁。关于贡献补丁请参考:如何参与 openEuler 内核开发
arm64 linux从4.1x阶段默认配置都打开了CONFIG_EFI_STUB, 默认选择UEFI的启动方式。为什么ARM选择UEFI替换DTB启动方式,可以参考linaro的这篇文章(http://www.linaro.org/blog/when-will-uefi-and-acpi-be-ready-on-arm/)
本篇也基于UEFI启动方式分析下内核KASLR的实现,当前主要通过bios实现 EFI_RNG_PROTOCOL协议来传递硬件熵。
从linux内核的启动流程开始分析,head.S(arch/arm64/kernel/head.S)是vmlinux的入口。
_head: /* * DO NOT MODIFY. Image header expected by Linux boot-loaders. */ #ifdef CONFIG_EFI ------ EFI启动配置 /* * This add instruction has no meaningful effect except that * its opcode forms the magic "MZ" signature required by UEFI. */ add x13, x18, #0x16 ----------- 把自己伪装成一个UEFI image,kernel需要符合PE格式 b stext ----------stext的功能是获取处理器类型和机器类型信息,并创建临时的页表,然后开启MMU功能, 是内核启动的分支 #else b stext // branch to kernel start, magic .long 0 // reserved #endif le64sym _kernel_offset_le // --------- kernel在RAM中加载的偏移,如果等于0,表示加载到RAM的0地址的位置上 le64sym _kernel_size_le // --------- kernel的大小 le64sym _kernel_flags_le // --------- kernel的一些属性 .quad 0 // reserved .quad 0 // reserved .quad 0 // reserved .ascii "ARM\x64" // Magic number #ifdef CONFIG_EFI .long pe_header - _head // Offset to the PE header.
pe_header: __EFI_PE_HEADER
__EFI_PE_HEADER定义在efi-head.S文件中(arch/arm64/kernel/efi-head.S)
.macro __EFI_PE_HEADER .long PE_MAGIC coff_header: .short IMAGE_FILE_MACHINE_ARM64 // --------- 表示machine type是AArch64 .short section_count // ------------- 该PE文件有多少个section .long 0 // --------------- 该文件的创建时间 .long 0 // -------------- 符号表信息 .long 0 // -------------- 符号表中的符号的数目 .short section_table - optional_header // ---------- optional header的长度 .short IMAGE_FILE_DEBUG_STRIPPED | \ IMAGE_FILE_EXECUTABLE_IMAGE | \ IMAGE_FILE_LINE_NUMS_STRIPPED // ----------- Characteristics,具体的含义请查看PE规格书
optional_header: .short PE_OPT_MAGIC_PE32PLUS // PE32+ format .byte 0x02 // MajorLinkerVersion .byte 0x14 // MinorLinkerVersion .long __initdata_begin - efi_header_end // ----------- 正文段的大小 .long __pecoff_data_size // --------- data段的大小 .long 0 // ------- bss段的大小 .long __efistub_entry - _head // 加载到memory后入口函数 .long efi_header_end - _head // ----------- 代码段在image file中的偏移
可以看出,加载到memory后的入口函数是__efistub_entry, 它是在哪里定义的呢? 查看Makefile(arch/arm64/kernel/Makefile)可以发现
OBJCOPYFLAGS := --prefix-symbols=__efistub_ $(obj)/%.stub.o: $(obj)/%.o FORCE $(call if_changed,objcopy)
编译的对象会有一个预加载的符号__efistub_, 主要作用是为了防止命名冲突,所以真正的入口函数是 entry, 定义在efi-entry.S文件中(arch/arm64/kernel/efi-entry.S)
ENTRY(entry) ------------ entry的入口函数 /* * Create a stack frame to save FP/LR with extra space * for image_addr variable passed to efi_entry(). */ stp x29, x30, [sp, #-32]! mov x29, sp
/* * Call efi_entry to do the real work. * x0 and x1 are already set up by firmware. Current runtime * address of image is calculated and passed via *image_addr. * * unsigned long efi_entry(void *handle, * efi_system_table_t *sys_table, * unsigned long *image_addr) ; */ adr_l x8, _text add x2, sp, 16 str x8, [x2] bl efi_entry ----------真正的入口函数是efi_entry cmn x0, #1 b.eq efi_load_fail
上面代码我们主要关注的是bl efi-entry,现在我们找到了内核中的入口函数的实现
efi_entry(void *handle, efi_system_table_t *sys_table, unsigned long *image_addr) ;
efi_entry定义在/drivers/firmware/efi/libstub/arm-stub.c中,
unsigned long efi_entry(void *handle, efi_system_table_t *sys_table, unsigned long *image_addr) { .... * Get the command line from EFI, using the LOADED_IMAGE * protocol. We are going to copy the command line into the * device tree, so this can be allocated anywhere. */ cmdline_ptr = efi_convert_cmdline(sys_table, image, &cmdline_size); --- (1) if (!cmdline_ptr) { pr_efi_err(sys_table, "getting command line via LOADED_IMAGE_PROTOCOL\n"); goto fail; }
status = handle_kernel_image(sys_table, image_addr, &image_size, &reserve_addr, &reserve_size, dram_base, image); ------ (2) if (status != EFI_SUCCESS) { pr_efi_err(sys_table, "Failed to relocate kernel\n"); goto fail_free_cmdline; }
if (fdt_addr) { ---- (3) pr_efi(sys_table, "Using DTB from command line\n"); } else { /* Look for a device tree configuration table entry. */ fdt_addr = (uintptr_t)get_fdt(sys_table, &fdt_size); if (fdt_addr) pr_efi(sys_table, "Using DTB from configuration table\n"); }
if (!fdt_addr) pr_efi(sys_table, "Generating empty DTB\n");
.... new_fdt_addr = fdt_addr; status = allocate_new_fdt_and_exit_boot(sys_table, handle, &new_fdt_addr, efi_get_max_fdt_addr(dram_base), ----- (4) initrd_addr, initrd_size, cmdline_ptr, fdt_addr, fdt_size);
... }
(1) efi_entry 通过efi_convert_cmdline从uefi中拿到cmdline, 然后将cmdline从utf16转成utf8返回。
(2) efi_entry中会调用handle_kernel_image, 重定位内核。
handle_kernel_image(/drivers/firmware/efi/libstub/arm64-stub.c): efi_status_t handle_kernel_image(efi_system_table_t *sys_table_arg, unsigned long *image_addr, unsigned long *image_size, unsigned long *reserve_addr, unsigned long *reserve_size, unsigned long dram_base, efi_loaded_image_t *image) { efi_status_t status; unsigned long kernel_size, kernel_memsize = 0; void *old_image_addr = (void *)*image_addr; unsigned long preferred_offset; u64 phys_seed = 0; // kaslr-seed, 默认为0 //内核使能CONFIG_RANDOMIZE_BASE if (IS_ENABLED(CONFIG_RANDOMIZE_BASE)) { //确保command line没有传递nokaslr参数,如果传递nokaslr则关闭KASLR if (!nokaslr()) { // 通过EFI_RNG_PROTOCOL获取BIOS传递过来的随机值 status = efi_get_random_bytes(sys_table_arg, sizeof(phys_seed), (u8 *)&phys_seed); if (status == EFI_NOT_FOUND) { pr_efi(sys_table_arg, "EFI_RNG_PROTOCOL unavailable, no randomness supplied\n"); } else if (status != EFI_SUCCESS) { pr_efi_err(sys_table_arg, "efi_get_random_bytes() failed\n"); return status; } } else { pr_efi(sys_table_arg, "KASLR disabled on kernel command line\n"); } } // 保证kernel位于VMALLOC区域大小的范围 preferred_offset = round_down(dram_base, MIN_KIMG_ALIGN) + TEXT_OFFSET; if (preferred_offset < dram_base) preferred_offset += MIN_KIMG_ALIGN; kernel_size = _edata - _text; kernel_memsize = kernel_size + (_end - _edata); // 如果随机值不为0并且CONFIG_RANDOMIZE_BASE配置打开, 所以BIOS在产生随机值时需要做一个判断 if (IS_ENABLED(CONFIG_RANDOMIZE_BASE) && phys_seed != 0) { //如果未设置CONFIG_DEBUG_ALIGN_RODATA,则在区间[0,MIN_KIMG_ALIGN]中生成一个不违反此内核的事实对齐约束的位移。 u32 mask = (MIN_KIMG_ALIGN - 1) & ~(EFI_KIMG_ALIGN - 1); u32 offset = !IS_ENABLED(CONFIG_DEBUG_ALIGN_RODATA) ? (phys_seed >> 32) & mask : TEXT_OFFSET; //保证传递的偏移地址2M地址对齐 offset |= TEXT_OFFSET % EFI_KIMG_ALIGN; // 在一个随机的物理地址加载内核 *reserve_size = kernel_memsize + offset; status = efi_random_alloc(sys_table_arg, *reserve_size, MIN_KIMG_ALIGN, reserve_addr, (u32)phys_seed); *image_addr = *reserve_addr + offset; } else { if (*image_addr == preferred_offset) return EFI_SUCCESS; *image_addr = *reserve_addr = preferred_offset; *reserve_size = round_up(kernel_memsize, EFI_ALLOC_ALIGN); status = efi_call_early(allocate_pages, EFI_ALLOCATE_ADDRESS, EFI_LOADER_DATA, *reserve_size / EFI_PAGE_SIZE, (efi_physical_addr_t *)reserve_addr); } if (status != EFI_SUCCESS) { *reserve_size = kernel_memsize + TEXT_OFFSET; status = efi_low_alloc(sys_table_arg, *reserve_size, MIN_KIMG_ALIGN, reserve_addr); if (status != EFI_SUCCESS) { pr_efi_err(sys_table_arg, "Failed to relocate kernel\n"); *reserve_size = 0; return status; } *image_addr = *reserve_addr + TEXT_OFFSET; } memcpy((void *)*image_addr, old_image_addr, kernel_size); return EFI_SUCCESS; }
handle_kernel_image的主要作用是在一个随机的物理地址中加载内核。
(3) 如果是acpi启动,没有fdt的情况下会生成一个fdt
(4) 在allocate_new_fdt_and_exit_boot -> update_fdt中将之前获取的内容(如cmdline ptr, seed)copy到chosen中
现在我们获取了一个保存关键信息的fdt. 现在我们重新回到head.S的流程 如果使能了KASLR的内核配置(CONFIG_RANDOMIZE_BASE)
__primary_switch: #ifdef CONFIG_RANDOMIZE_BASE mov x19, x0 // preserve new SCTLR_EL1 value mrs x20, sctlr_el1 // preserve old SCTLR_EL1 value #endif
bl __enable_mmu #ifdef CONFIG_RELOCATABLE bl __relocate_kernel #ifdef CONFIG_RANDOMIZE_BASE ldr x8, =__primary_switched // ----- 跳转到primary_switched adrp x0, __PHYS_OFFSET blr x8
//-------- 在x23 寄存器中有一个KASLR位移,我们需要通过丢弃当前的内核映射并创建一个新的映射。 pre_disable_mmu_workaround msr sctlr_el1, x20 // ------------------ 关闭MMU isb bl __create_page_tables // ------------------ 创建页表映射
tlbi vmalle1 // -------- 删除TBL dsb nsh
msr sctlr_el1, x19 // -------- 打开MMU isb ic iallu // 获取刷新指令 dsb nsh isb
bl __relocate_kernel // ------------ relocate kernel #endif #endif ldr x8, =__primary_switched adrp x0, __PHYS_OFFSET br x8 ENDPROC(__primary_switch)
现在看下primary_switched中的处理
__primary_switched: adrp x4, init_thread_union add sp, x4, #THREAD_SIZE adr_l x5, init_task msr sp_el0, x5 // Save thread_info
adr_l x8, vectors // load VBAR_EL1 with virtual msr vbar_el1, x8 // vector table address isb
stp xzr, x30, [sp, #-16]! mov x29, sp
str_l x21, __fdt_pointer, x5 // Save FDT pointer
ldr_l x4, kimage_vaddr // Save the offset between sub x4, x4, x0 // the kernel virtual and str_l x4, kimage_voffset, x5 // physical mappings
// Clear BSS adr_l x0, __bss_start mov x1, xzr adr_l x2, __bss_stop sub x2, x2, x0 bl __pi_memset dsb ishst // Make zero page visible to PTW
#ifdef CONFIG_KASAN bl kasan_early_init #endif #ifdef CONFIG_RANDOMIZE_BASE tst x23, ~(MIN_KIMG_ALIGN - 1) // already running randomized? b.ne 0f mov x0, x21 // pass FDT address in x0 bl kaslr_early_init // parse FDT for KASLR options cbz x0, 0f // KASLR disabled? just proceed orr x23, x23, x0 // record KASLR offset ldp x29, x30, [sp], #16 // we must enable KASLR, return ret // to __primary_switch() 0: #endif add sp, sp, #16 mov x29, #0 mov x30, #0 b start_kernel ENDPROC(__primary_switched)
在配置CONFIG_RANDOMIZE_BASE后,会进入kaslr_early_init的流程
u64 __init kaslr_early_init(u64 dt_phys) { void *fdt; u64 seed, offset, mask, module_range; const u8 *cmdline, *str; int size;
/* * Set a reasonable default for module_alloc_base in case * we end up running with module randomization disabled. */ module_alloc_base = (u64)_etext - MODULES_VSIZE;
/* * Try to map the FDT early. If this fails, we simply bail, * and proceed with KASLR disabled. We will make another * attempt at mapping the FDT in setup_machine() */ early_fixmap_init(); fdt = __fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL); if (!fdt) return 0;
/* * Retrieve (and wipe) the seed from the FDT */ seed = get_kaslr_seed(fdt); ------------- (1) if (!seed) return 0;
/* * Check if 'nokaslr' appears on the command line, and * return 0 if that is the case. */ cmdline = get_cmdline(fdt); str = strstr(cmdline, "nokaslr"); -------------- (2) if (str == cmdline || (str > cmdline && *(str - 1) == ' ')) return 0;
mask = ((1UL << (VA_BITS - 2)) - 1) & ~(SZ_2M - 1); ----------(3) offset = BIT(VA_BITS - 3) + (seed & mask);
/* use the top 16 bits to randomize the linear region */ memstart_offset_seed = seed >> 48; ----- (4)
if (IS_ENABLED(CONFIG_RANDOMIZE_MODULE_REGION_FULL)) { /* * Randomize the module region over a 4 GB window covering the * kernel. This reduces the risk of modules leaking information * about the address of the kernel itself, but results in * branches between modules and the core kernel that are * resolved via PLTs. (Branches between modules will be * resolved normally.) */ module_range = SZ_4G - (u64)(_end - _stext); module_alloc_base = max((u64)_end + offset - SZ_4G, (u64)MODULES_VADDR); } else { /* * Randomize the module region by setting module_alloc_base to * a PAGE_SIZE multiple in the range [_etext - MODULES_VSIZE, * _stext) . This guarantees that the resulting region still * covers [_stext, _etext], and that all relative branches can * be resolved without veneers. */ module_range = MODULES_VSIZE - (u64)(_etext - _stext); module_alloc_base = (u64)_etext + offset - MODULES_VSIZE; }
/* use the lower 21 bits to randomize the base of the module region */ module_alloc_base += (module_range * (seed & ((1 << 21) - 1))) >> 21; module_alloc_base &= PAGE_MASK;
return offset; }
(1)kaslr_early_init会根据之前生成的fdt种获取kaslr-seed
(2) 解析cmdline, 确保没有传递nokaslr参数
(3) 保证传递的偏移地址2M地址对齐,并且保证kernel位于VMALLOC区域大小的一半地址空间以下 (VA_BITS - 2)。当VA_BITS=48时,mask=0x0000_3fff_ffe0_0000。
(4) 随机化线性映射区地址
回到上面流程,kaslr_early_init获取的偏移地址offset保存在x23寄存器中。然后重新创建kernel image的映射。
创建映射的函数是__create_page_tables。 函数也定义在head.S文件中,主要是为了映射内核在vmalloc域的随机地址空间. 此处还有一个__relocate_kernel的跳转,有什么用呢?例如链接脚本中常见的几个变量_text、_etext、_end。这几个你应该很熟悉,他们是一个地址并且他们的值是链接的时候确定下来,那么现在使能kaslr的情况下,代码中再访问_text的值就很明显不是运行时的虚拟地址,而是链接时候的值。因此,__relocate_kernel函数可以负责重定位这些变量。保证访问这些变量的值依然是正确的值。
因为我这边没有一个实现了EFI_RNG_PROTOCAL的BIOS,所以我对内核代码进行了修改,主要验证下KASLR的整个流程是否ok.
上面已经说过,获取kaslr-seed主要通过efi_get_random_bytes(drivers/firmware/efi/libstub/random.c)
efi_status_t efi_get_random_bytes(efi_system_table_t *sys_table_arg, unsigned long size, u8 *out) { efi_guid_t rng_proto = EFI_RNG_PROTOCOL_GUID; efi_status_t status; struct efi_rng_protocol *rng;
// *out即使返回的随机值,可以在这里手动赋予一个值,每次启动都重新赋一个 *out = 0x12345678; return EFI_SUCCESS; status = efi_call_early(locate_protocol, &rng_proto, NULL, (void **)&rng); if (status != EFI_SUCCESS) return status;
return rng->get_rng(rng, NULL, size, out); }
增加红色代码,修改完成后,多更改几次*out的返回值,查看函数的偏移地址是否每次都不一样即可。 使用如下命令即可: cat /proc/kallsyms | grep do_fork
总结 如果内核想要使用KASLR的功能,需要保证配置CONFIG_RADOMIZE_BASE和CONFIG_RANDOMIZE_TEXT_OFFSET打开,并且启动参数cmdline中不要添加nokaslr.
kaslr的主要流程可以分为以下几步: 1.通过handle_kernel_image在一个随机的物理地址加载内核 2. 通过kaslr_early_init获取内核映射偏移地址,然后映射内核在vmalloc域的一个随机虚拟地址 3. 映射一些变量以及符号表,偏移地址和image一样
如果需要验证KASLR,可以反复启动内核,查看函数的偏移地址是否每次都不一样即可。