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实现细节详解