linux从开机开始分析之内存随机化实现

 

上一部分我们进入内核启动进程的最后阶段,但是跳过了一些重要的部分

linux内核的入口点是定义在main.c源代码中的start_kernel函数,该函数在内存中储存在LOAD_PHYSICAL_ADDR.该地址取决于 CONFIG_PHYSICAL_START 内核选项,默认是0x1000000

config PHYSICAL_START
    hex "Physical address where the kernel is loaded" if (EXPERT || CRASH_DUMP)
    default "0x1000000"
    ---help---
      This gives the physical address where the kernel is loaded.
      ...
      ...
      ...

这个值可以在配置时被改变,如果需要这个功能,内核配置选项中CONFIG_RANDOMIZE_BASE应该被打开

现在,linux内核解压到的物理地址以及加载地址会是随机的.这个选项有一部分是为了安全性考虑

 

页表初始化

在解压器找到一个随机内存范围来解压内核之前,身份映射页表应该被初始化.如果加载器使用16位或32位启动协议,页表会正常初始化.但是如果解压器选择的内存范围仅能在64位的上下文中使用,这时就会出现问题.这是为什么需要再次更新页表

随机化内核加载地址的第一步是建立新的身份映射页表,但首先,我们看看如何获取地址点

前一部分中,在切换到长模式以及跳转到到解压器入口点extract_kernel函数.随机内存以对choose_random_location函数的调用开始

void choose_random_location(unsigned long input,
                            unsigned long input_size,
                            unsigned long *output,
                            unsigned long output_size,
                            unsigned long *virt_addr)
{}

该函数的有五个参数

  • input
  • input_size
    • output;
    • output_isze;
    • virt_addr.
asmlinkage __visible void *extract_kernel(void *rmode, memptr heap,
                                          unsigned char *input_data,
                                          unsigned long input_len,
                                          unsigned char *output,
                                          unsigned long output_len)
{
  ...
  ...
  ...
  choose_random_location((unsigned long)input_data, input_len,
                         (unsigned long *)&output,
                         max(output_len, kernel_total_size),
                         &virt_addr);
  ...
  ...
  ...
}

这些参数通过汇编指令传递

leaq    input_data(%rip), %rdx

input_data由一个小项目mkpiggy生成.如果你自己尝试过编译linux内核.你会发现输出由该项目生成linux/arch/x86/boot/compressed/piggy.S.在这里,这个项目看起来是这样的

.section ".rodata..compressed","a",@progbits
.globl z_input_len
z_input_len = 6988196
.globl z_output_len
z_output_len = 29207032
.globl input_data, input_data_end
input_data:
.incbin "arch/x86/boot/compressed/vmlinux.bin.gz"
input_data_end:

如你所见,它包含了4个全局符号,前两个是z_input_lenz_output_len,这两个代表压缩的和解压后的vmlinux.bin.gz大小.第三个是input_data参数,指向linux内核镜像二进制文件(已经去除了debug信息,重定位信息)最后一个参数是input_data_end 指向镜像文件的末尾.

所以,在choose_random_location函数中,第一个参数是指向压缩内核镜像的指针.

第二个参数是z_input_len

第三第四个参数是解压内核镜像的地址和它需要的大小,这个地址来自于 arch/x86/boot/compressed/head_64.S ,是startup_32地址通过2mb边界对齐的结果.

大小由z_output_len决定,同样在piggy.s

最后一个参数是内核加载的虚拟地址,默认同步于物理加载地址

unsigned long virt_addr = LOAD_PHYSICAL_ADDR;

物理加载地址在配置选项中定义

#define LOAD_PHYSICAL_ADDR ((CONFIG_PHYSICAL_START \
                + (CONFIG_PHYSICAL_ALIGN - 1)) \
                & ~(CONFIG_PHYSICAL_ALIGN - 1))

我们覆盖了choose-random_location的参数,因此我们看一看它的实现

首先检查命令行中nokaslr选项

    if (cmdline_find_option_bool("nokaslr")) {
    warn("KASLR disabled: 'nokaslr' on cmdline.");
    return;
}

如果nokaslr被设置,则不适用随机地址;在内核文档中能看到对于这方面的信息.

kaslr/nokaslr [X86]

Enable/disable kernel and module base offset ASLR
(Address Space Layout Randomization) if built into
the kernel. When CONFIG_HIBERNATION is selected,
kASLR is disabled by default. When kASLR is enabled,
hibernation will be disabled.

假设我们不使用nokaslr参数, CONFIG_RANDOMIZE_BASE 选项可用,则将kaslr标志位加到内核加载标志中

boot_params->hdr.loadflags |= KASLR_FLAG;

接下来调用initialize_identity_maps()函数arch/x86/boot/compressed/kaslr_64.c

initialize_identity_maps()首先初始化x86_mapping_info结构体 ,命名为mapping_info

mapping_info.alloc_pgt_page = alloc_pgt_page;
mapping_info.context = &pgt_data;
mapping_info.page_flag = __PAGE_KERNEL_LARGE_EXEC | sev_me_mask;
mapping_info.kernpg_flag = _KERNPG_TABLE;

该结构体定义在 arch/x86/include/asm/init.h头文件中.

struct x86_mapping_info {
    void *(*alloc_pgt_page)(void *);
    void *context;
    unsigned long page_flag;
    unsigned long offset;
    bool direct_gbpages;
    unsigned long kernpg_flag;
};

结构体提供了内存映射的信息,在上一部分,我们已经覆盖04g的页表,但是这些页表在超出4g范围后就无法使用.因此initialize_identity_maps()函数为新的内存页表入口初始化内存.因此首先看看x86-mapping_info的定义.

alloc_pgt_page是一个用来获取使用空间的回调函数.context是是一个alloc_pgt_data的实例.通过它来追踪使用的页表. page-flagkernpg_flag属于页的标志.第一个flag设置PMDPUD入口.kernpg_flag提供为内核提供重写接口.offset代表虚拟地址和物理地址之间的差

alloc_pgt_page用来为页表入口点分配内存,检查一个新页的空间,分配它到alloc_pgt_datapgt_buf区.并返回新页的地址

entry = pages->pgt_buf + pages->pgt_buf_offset;
pages->pgt_buf_offset += PAGE_SIZE;

alloc_pgt_data结构体

struct alloc_pgt_data {
    unsigned char *pgt_buf;
    unsigned long pgt_buf_size;
    unsigned long pgt_buf_offset;
};

initialize_identity_maps 函数的最后目标是初始化pgdt_buf_sizepgt_buf_offset我们只在初始化阶段,因此将偏移设置成0;

pgt_data.pgt_buf_offset = 0;

pgt-buf_size被设置成 7782469632,这取决于使用的引导器是32位还是64位.对于pgt_buf也是同样的原则.如果引导器在startup_32加载内核,pgdt_buf指向已初始化页表的末尾,

pgt_data.pgt_buf = _pgtable + BOOT_INIT_PGT_SIZE;

这里,_pgtable指向_pgtable开头,

另外,如果使用startup_64 页表已经被引导自身处理完毕._pgtable只需要指向这些表

pgt_data.pgt_buf = _pgtable

页表的缓存被初始化,我们返回到choose_random_location

 

保留内存

在验证初始化页表后,选择一段随机的内存地址解压内核镜像,但我们不能随意选择内存地址,因为内存中有一部分保留空间.例如initrd和命令行所占的空间必须保留.这些保留空间由mem-avoid_init函数来实现.

mem_avoid_init(input, input_size, *output);

所有不安全的内存地区被收集在一个叫memavoid的数组里

struct mem_vector {
    unsigned long long start;
    unsigned long long size;
};

static struct mem_vector mem_avoid[MEM_AVOID_MAX];

MEM_AVOID_MAXmem_avoid_index枚举类型

enum mem_avoid_index {
    MEM_AVOID_ZO_RANGE = 0,
    MEM_AVOID_INITRD,
    MEM_AVOID_CMDLINE,
    MEM_AVOID_BOOTPARAMS,
    MEM_AVOID_MEMMAP_BEGIN,
    MEM_AVOID_MEMMAP_END = MEM_AVOID_MEMMAP_BEGIN + MAX_MEMMAP_REGIONS - 1,
    MEM_AVOID_MAX,
};

这两个都定义在 arch/x86/boot/compressed/kaslr.c

mem_avoid_init函数的实现中,主要目标是让mem_avoid数组通过mem_avoid_index的储存的保留地址信息

为新的映射缓冲区创建新的页.在mem_avoid_index函数中也是这么做的.

mem_avoid[MEM_AVOID_ZO_RANGE].start = input;
mem_avoid[MEM_AVOID_ZO_RANGE].size = (output + init_size) - input;
add_identity_map(mem_avoid[MEM_AVOID_ZO_RANGE].start,
         mem_avoid[MEM_AVOID_ZO_RANGE].size);

mem_avoid_init函数首先尝试禁止被解压内核占用的内存地址,将mem_avoid[MEM_AVOID_ZO_RANG]填充为入口点,以及所需内存的大小.随后调用add_indentity_map函数,该函数这一片内存设置认证.

void add_identity_map(unsigned long start, unsigned long size)
{
    unsigned long end = start + size;

    start = round_down(start, PMD_SIZE);
    end = round_up(end, PMD_SIZE);
    if (start >= end)
        return;

    kernel_ident_mapping_init(&mapping_info, (pgd_t *)top_level_pgt,
                  start, end);
}

round_up``round_down函数用来矫正开始和结束的地址偏移为2mb.

add_identity_map在最后调用kernel_ident_mapping_init.参数为已经初始化好的mapping_info实例.最高级页表的地址,,以及应该被新建的内存实例的开始结束地址.

kernel_ident_mapping_init函数为新页设置默认的标志位

if (!info->kernpg_flag)
    info->kernpg_flag = _KERNPG_TABLE;

然后开始建立新的2mb页入口(如果使用5级页表 则PGD -> P4D -> PUD -> PMD ,如果是4级页表,则 PGD -> PUD -> PMD)并连接到给定的地址.

for (; addr < end; addr = next) {
    p4d_t *p4d;

    next = (addr & PGDIR_MASK) + PGDIR_SIZE;
    if (next > end)
        next = end;

    p4d = (p4d_t *)info->alloc_pgt_page(info->context);
    result = ident_p4d_init(info, p4d, addr, next);

    return result;
}

在这个循环中首先为给定的地址寻找PGD,如果入口点的地址比给定地区的结束地址大,那么将大小设置为end

然后我们通过x86_mapping_info函数分配一个新页.调用ident_p4d_init函数,这个函数会为低一级的页表分配新页

到此

我们拥有了新的页入口接下来只需要为initrd和其他的一些数据建立页就行了

结束后返回choose_random_location函数

 

物理地址随机化

在保留内存被放在了mem_avoid后,为它们建立身份映射页表,我们选择最低的可用地址来进行解压

min_addr = min(*output, 512UL << 20);

该地址应该在第一个512mb之内,之所以选择512是为了避免低内存地址中的一些数据的干扰

然后开始选择物理地址和虚拟地址来加载内核

第一个物理地址是

random_addr = find_random_phys_addr(min_addr, output_size);

find_random_phys_addr()定义在 kasl,c 中.

static unsigned long find_random_phys_addr(unsigned long minimum,
                                           unsigned long image_size)
{
    minimum = ALIGN(minimum, CONFIG_PHYSICAL_ALIGN);

    if (process_efi_entries(minimum, image_size))
        return slots_fetch_random();

    process_e820_entries(minimum, image_size);
    return slots_fetch_random();
}

process_efi_entries函数的主要目标是寻找合适的可用的内存空间.如果编译内核或者是启动系统时没有EFI支持.我们会继续在e820空间中寻找这样的空间.所有被找到的可用的地址空间都被放在slot_areas中.

struct slot_area {
    unsigned long addr;
    int num;
};

#define MAX_SLOT_AREA 100

static struct slot_area slot_areas[MAX_SLOT_AREA];

内核会选择一个随机的下标.这个选择的程序在slots_fetch_random中实现.该函数能在 slot_areas结构中选择出一个随机的内存范围

slot = kaslr_get_random_long("Physical") % slot_max;

kaslr_get_random_long定义在arch/x86/lib/kaslr.c ,返回随机数.这个数字能以很多种方式获取(如 :使用时间戳,rdrand等)

 

虚拟地址随机化

物理地址随机选择后,我们从身份验证页表为获取地址

random_addr = find_random_phys_addr(min_addr, output_size);

if (*output != random_addr) {
        add_identity_map(random_addr, output_size);
        *output = random_addr;
}

从现在开始,output储存内存区域的基地址,现在我们只随机化了物理地址.也能像在x86_64架构下一样,随机化虚拟地址

if (IS_ENABLED(CONFIG_X86_64))
    random_addr = find_random_virt_addr(LOAD_PHYSICAL_ADDR, output_size);

*virt_addr = random_addr;

在其他架构下,物理地址和虚拟地址的随机化也是同样的流程,find_random_virt_addr找出需要的大小范围,它调用kaslr_get_random_long 来进行更深一层的工作.

 

Links

(完)