Linux内核笔记之KASLR

changelog:
2019年01月20日 初稿
2020年03月1日 增加kaslr_early_init的处理说明

本文基于linux 4.19.0,体系结构是arm64
KASLR
KASLR, kernel address space layout randomization,内核地址空间布局随机化,是linux内核的一个非常重要的安全机制。KASLR技术可以让kernel image映射的地址相对于链接地址有个偏移,安全性上有一定的提升。

内核源码实现
KASLR的实现原理比较简单,在内核启动阶段,获取一个kernel image的偏移值,偏移值可以通过dtb或者bios传递。
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,可以反复启动内核,查看函数的偏移地址是否每次都不一样即可。

参考文章
kaslr-蜗窝科技
深入linux内核防御机制——kaslr实现细节详解

你可能感兴趣的:(linux读核笔记,KASLR,Linux安全特性,Linux,内核,内核地址空间布局随机化,arm64)