@table注解详解_Linux KASLR机制详解

本文详细介绍了Linux内核的KASLR(Kernel Address Space Layout Randomization)机制,强调了内存映射作为理解内核基础的重要性。文章分为三个部分,分别讲述内存映射基础、Linux内核文件结构和内核加载地址与虚拟地址的随机化。通过实例和代码解析,阐述了内核加载地址和虚拟地址分开随机化的必要性以及如何在不同场景下进行内核内存布局。
摘要由CSDN通过智能技术生成

在详细介绍KASLR机制之前,先提一句,也是我个人一个切身的体会吧,在你想了解Linux内核的各种各样功能之前,首先要搞清楚一个最基础的功能那就是内存映射,个人感觉内存映射管理是你深入了解操作系统内核最最最基础的部分,如果这部分没有彻底搞清楚,未来在你深入Linux内核的代码尤其是基于EPT的内存虚拟化部分(x86架构),会有很多功能或代码,会觉的理解不深,似懂非懂的。

第一部分 罗里吧嗦(内存映射)

这里讲一下内存映射最核心的一些概念,只要懂了这些不管是32位还是64位映射,你都不会迷茫^_^。

首先明确几个概念:逻辑地址,虚拟地址(也被称作线性地址),物理地址。

注意:CPU只认识虚拟地址或线性地址。

1.1 逻辑地址:我们都知道CPU有个EIP寄存器,用以指向下一条指令的地址,但它是指向当前代码段的offset,因此需要和CS段寄存器配合,CS指向的段描述符里记录了当前运行的代码段的基地址,这样通过CS:EIP(基地址+EIP)获的程序下一条指令的虚拟地址或线性地址,因此这时EIP表示的地址就被称为逻辑地址。

注意:64位长模式下,除了GS和FS段寄存器能被设置为基地址base不等于0外,其它CS,DS,ES,SS段寄存器都只能被设置为0,因此这时逻辑地址和虚拟(或线性)地址是一样的,因此统称为虚拟或线性地址了。

1.2 虚拟或线性地址:就是CPU的寻址能力,也就是CS:EIP,CPU只会处理虚拟或线性地址。

CPU根据是否开启分页有两种处理方式:

1.2.1 开启保护未开启分页

虚拟或线性地址(CS:EIP)直接送往地址总线(只开启保护未开启分页),这时虚拟或线性地址到物理地址的映射是实地址映射的。

b816d3cbf40bded35158fe8c3d1edc1f.png
图1

1.2.2 开启保护和分页

虚拟或线性地址(CS:EIP)送往MMU由MMU通过CR3 paging structure完成虚拟或线性地址

到实际物理地址的映射,最后将得到的物理地址送往地址总线访存。

cfbd22b76bc010a84160aa71428ac11b.png
图2

1.3 物理地址:送往地址总线的地址。(也就是最终的访存地址)

对于如何通过CR3进行分页映射以及分段寻址的详细过程,可以参考赵炯老师的linux0.11内核注释这本书,里面有详细的讲解,至于在64位长模式下是如何通过PML进行4级映射及内存虚拟化guest virtual address是如何通过PML进行4级映射的就要看官方的手册了。

第二部分 Linux kernel文件结构

好了,该言归正传了,KASLR (kernel address space layout randomize)是为了提高内核的安全性,将kernel随机的加载到不同的物理地址运行,内核在自引导及decompressed后,会通过判断kaslr命令行参数是否enable来确定是否对加载内核的物理地址和内核运行的虚拟地址进行随机化操作,注意:这里是内核被加载的物理地址和运行时的虚拟地址被分开随机化了,decouple了,所以可以给物理地址和虚拟地址分配不同的随机化值。

2.1 Linux kernel本身的layout详解:

5470fcd0dacd1e5f1f9b17b9f20c00fe.png
图3

图3注解:

arch/x86/boot/setup.ld负责实模式代码的链接。

arch/x86/boot/compressed/vmlinux.lds.S负责保护模式代码的链接工作。

如上图3所示,整个kernel文件由两部分组成:实模式代码保护模式代码

2.1.1 实模式代码

header.S中专门开辟了一部分空间用于存储boot params参数,这些参数都是linux boot protocol协议定义的只读或可写参数,bootloader程序(例如grub)会根据boot params中定义的setup_sects参数,将bzImage分割成实模式代码和保护模式代码两个部分,其中保护模式代码会被bootloader加载到>=0x100000(1M)内存处,具体的位置有boot params中定义的code32_start参数决定,实模式代码被bootloader加载到x~x+0x8000这32K低地址区间内,其具体位置由bootloader决定,这个协议没有规定。

具体可以查看boot协议:https://www.kernel.org/doc/html/latest/x86/boot.html

如下图4所示:实模式代码(kernel setup & kernel boot sector)所占区间不能超过0xA0000。

4e236c92c9b873fa81ef3f026e8cedda.png
图4

2.1.2保护模式代码

如上图3所示,该部分有三块组成:

(1)head_32/64.S文件的前半部分

(2)vmlinux.bin.gz(包含vmlinux.bin和vmlinux.relocs(kaslr=enable))

(3)head_32/64.S后半部分+decompressing code

在arch/x86/boot/compressed/head_64.S中定义了两个section:

Section name分别为:

(1)“.head.text”

(2)“.text”

具体定义:如下图5所示

5cdbf40068303407be337f307aaaae8e.png
图5

根据vmlinux.lds.S中的链接定义:

62caa357b87e3ed20bc4d5aec3bfc501.png
图6上半部分

86148933a0cd2f67a976f4d32a3ce5f7.png
图6 下半部分

由上图6所示,head_64.S在链接的时候会以“relocated:”标号为界,relocated之前的代码被链接在compressed kernel之前,relocated标号之后的代码以及decompressing code都被链接在compressed kernel之后。

至于section(.rodata..compressed)是由mkpiggy.c文件生成的piggy.S中定义的,如下图7所示:

ace260acf8ac227a4d16172c81f2636d.png
图7

所以根据上图6中vmlinux.lds.S的链接规则就可得到图3中protect mode kernel的文件结构。

2.1.3 内核重定位表(vmlinux.relocs)

内核重定位表用于对内核虚拟地址的重定位操作,我们知道内核的默认虚拟基地址是: 0xffffffff81000000(内核占用0xffffffff80000000~0xffffffffC0000000这1G虚拟地址空间),当我们在编译内核的时候,如果设置.config文件中的CONFIG_RANDOMIZE_BASE=y,那么在将compressed kernel解压到randomized physical address后,还要对kernel中的虚拟地址进行randomize,这时就要知道内核中哪些地方的虚拟地址需要relocate,内核重定位表就记录了内核中所有需要重定位的虚拟地址的位置。

第三部分 内核加载地址和虚拟地址分开随机化

在内核加载地址和虚拟地址没有decouple之前,他们是使用相同的随机化地址的,因为内核虚拟地址只能在0xffffffff80000000~0xffffffffC0000000这1G地址空间,这就意味着randomize value只能在<512M的范围内选取(因为目前内核的大小最大是不超过512M),如果randomize_value>512M的话,那么内核虚拟地址被relocated后的某些值有可能是>0xffffffffC0000000的,这样就超出了内核的虚拟地址空间了,所以现在将加载地址和虚拟地址分开随机化,这样加载地址就不受512M范围的约束了。

3.1 不开启KASLR机制内核内存布局

当设置.config文件中的CONFIG_RANDOMIZE_BASE=n后,内核代码中引用的虚拟地址不需要relocate,加载内核的物理地址由boot params中的参数code32_start(default:0x100000,1M)参数和#define CONFIG_PHYSICAL_START 0x1000000(16M)这个编译时定义的默认值决定。

3.1.1 boot params参数: code32_start

该参数定义在arch/x86/boot/header.S中,仅被bootloader(grub)使用,用于将内核的保护模式代码加载到1M内存地址处,当内核的实地址代码运行完各种寄存器,CPU check和某些boot params的初始化操作后,会进入保护模式并跳转到1M地址处执行head_64.S中start_32,还记得之前介绍过head_64.S被relocated标号分为两个部分吗,relocated标号的前半部分主要也就进行一些初始化操作,但是最主要的工作是设置CPU进入64位长模式,并且根据zoffset.h和voffset.h中的内核压缩后的大小及解压后的大小,将当前位置的内核代码copy到指定位置(这里定义为cp_to_dest_addr)

下面通过4个文件:voffset.h, zoffset.h, vmlinux.S和header.S详解这个cp_to_dest_addr是如何计算的。

Vmlinux.S是由arch/x86/boot/compressed/vmlinux文件反汇编得到的(objdump -S vmlinux > vmlinux.S)。

该arch/x86/boot/compressed/vmlinux文件结构就是图3所示的protect-mode kernel.

ba363c0ebebc06079048b8821d7b1606.png
图8

950680d04e85a042827f6e18cf1244b8.png
图9

8e41779c247223273ee9525f8c4c28da.png
图10

由上面图8,9,10中注释可以看出zoffset中宏定义参数的值是怎么得到的,但有个值ZO__end=0x0652000和图9末尾地址0x635dfe差距还是蛮大的,这里有必要解释下,这就要参考一下图6 arch/x86/boot/compressed/vmlinux.lds.S这个文件的后半段。

在后半段定义了.rodata, .got, .data, .bss, .pg_table这些段,.pg_table及结束地址是以page_size(4K)对齐的,其中pg_table是早期的PML4 paging structure 共占用6 pages=24K,用于早期仅管理4G的物理内存,所以ZO__end=0x0652000就好理解了。

3.1.2 计算解压decompressed kernel所需的buffer大小

大家知道kernel可以用不同的压缩格式压缩,这里以GZIP举例说明。

因为这里讲的是kernel kaslr disbale状态下计算buffer的,所以用下图11说明buffer的大概位置。

3fd0e1a2deee07378741783159f5ad4a.png
图11

bootloader首先将bzImage分割加载到0~16M地址空间的0~1M和1M~16M两个空间(根据bzImage中代码的类型:实模式和保护模式),随后当执行到保护模式的代码后,最终会将

图3中的protect-mode kernel按照从高地址到低地址的方向复制到16M+buffer_size处,

注意: 复制是从高到低的(开启STD),也就是SI指向protect-mode kernel的高地址,

DI=16M+buffer_size.

下图12中的代码就是负责复制操作的。

569944231398561516508125439d943e.png
图12

下面就要计算这个buffer_size是如何计算得到的。

我们知道GZ压缩格式首先是通过LZ77算法进行压缩,然后再利用huffman进行编码.

详细格式参考:http://www.faqs.org/rfcs/rfc1952.html

解压的内容是放在16M地址开始处的,这里要考虑到解压的内容会不会覆盖掉还未来得及压缩的部分。

4665e5177d1a1c543339e601a8d179e5.png
图13

f0b5ab0f76b9edc7b5dfdce01dc2a6ba.png
图14

上图13和14举例说明这种风险,buffer如果设置为未压缩内核大小的话,解压的时候有很大风险,所以一定要加一个extra_bytes来避免这种风险。

内核计算extra_bytes = (uncompressed_size >> 12) + 65536 + 128是以压缩出现的最坏情况处理的,也就是压缩后文件>未压缩文件,在arch/x86/boot/header.S中有详细注释。

3.1.3 当把压缩内核copy到指定的buffer后,执行如下图15的代码,跳到buffer空间head_64.S的relocated标号处执行了,而不是当前<16M空间的head_64.S的标号relocated.

5ef1f2f5ec48333cd4616da8d3512d7d.png
图15

因为在执行图15代码之前会执行如下图16的代码

c0124dceb8dcb0ef2bf319d254b1f45c.png
图16

此时rbx中存储的是图3中protect-mode kernel起始代码在buffer中的位置,

所以relocated(%rbx)这种相对寻址就定位到buffer中存储的head_64.S 中定义的relocated位置了,所以也说这种寻址是PIC code.

通过反汇编vmlinux.bin文件,如下图17所示:

d30b09448c931babd518510f4d5746b8.png
图17

由上图17再结合图9中定义的ZO__text=0x6320f0可得head_64.S标号relocated后的代码和decompressing code都是链接在compressed kernel后面的。

在relocated中会调用extract_kernel将compressed kernel解压到16M地址开始处,随后跳转到arch/x86/kernel/head_64.S的开始处执行内核早期的内核初始化操作,其中包括物理加载地址的relocate操作,这部分内容将在下一节详细阐述。

3.2 开启KASLR机制内核内存布局

在调用extract_kernel之前,开启KASLR机制的内存布局和不开启KASLR的布局是一样的,下面看看extract_kernel在开启KASLR后有哪些额外的操作。

如下图18所示:

b7c74baa5d2eed0a15f8c438068a35c5.png
图18

下面会详细介绍parse_elf和handle_relocations这个两个函数。

3.2.1 parse_elf函数详解

如下图19和20所示:

ddeafd00a9f4bf3830d0fb3806a61cfd.png
图19

图19显示的是load segment在可执行文件中的位置, 默认被加载运行的物理地址及默认的虚拟基地址。

Parse_elf就是将这些load segment从文件中提取出来,放在指定位置上。

下图20就是详细介绍如何处理这些load segment的。

5f144f4be9d59dbb9dfe5cd37e33ca9d.png
图20

下一节将详细介绍物理加载地址(dest)和虚拟地址(virt_addr)为什么能够进行随机化。

3.2.3 内核代码物理加载地址和虚拟地址的随机化

如上图3所示,protect-mode kernel其实被分割为三个部分,其中compressed kernel才是真正的linux内核,其他两个部分是通过gcc -fPIE选项编译的,因此bootloader可以把protect-mode kernel加载到任意物理地址处对compressed kernel进行解压。

而protect-mode kernel中非compressed kernel部分,主要是负责真正内核的解压及其虚拟地址relocate,当然还有其它的初始化操作。

文件arch/x86/boot/compressed/Makefile文件中定义了protect-mode kernel中非compressed kernel部分在编译和链接的时候都加了-PIE选项,这样代码中的寻址都是相对寻址没有绝对寻址,而且也没有对外部符号的引用,因此也就没有GOT表,这样才能保证PIE code能随机加载到任意物理地址运行且不依赖外部过程或变量。

所以PIC和PIE的本质区别就是:是否依赖外部符号(过程或变量),也就是是否有GOT表或GOT,PLT,dyn(动态链接)。

内核编译后,在arch/x86/boot/compressed/目录下可以发现三个文件:vmlinux.bin, vmlinux.bin.gz和vmlinux。

生成顺序是:

Vmlinux.bin+vmlinux.relocs(kaslr enable) > vmlinux.bin.gz

Arch/x86/boot/compressed/Head_64.S+vmlinux.bin.gz+decompresssing code > vmlinux

Realmode code(header.S and others) + vmlinux > bzImage

Vmlinux.bin是真正的内核可执行文件,其内容(开头部分)如下图21:

fb559092ee13f869312290a3c09870b4.png
图21

图21表明内核默认的虚拟基地址是0xffffffff81000000且其入口函数是arch/x86/kernel/head_64.S的startup_64,这部分真正的内核代码不是PIC code。

Vmlinux的内容如下图22所示:

1a131f599602ee9586e335cfc3a700b7.png
图22

如上图22所示,vmlinux的虚拟基地址是0x00,

虚拟基地址在arch/x86/boot/compressed/vmlinux.lds.S中定义。

下面再看vmlinux的program headers

如下图23所示,这个可执行文件的虚拟基地址是0x00,在文件2M偏移处。

入口点是虚拟地址0x200处,也就是startup_64函数。

这部分代码都是PIE code,寻址都是相对寻址(也就是offset),没有对绝对虚拟地址的引用,

因此可以被bootloader加载到任意物理地址运行。

9afd68b68434aac02086954be61cab01.png
图23

分析完这三个文件,下面要讲vmlinux.bin文件是如何可以被加载到任意物理地址运行的,且其虚拟地址是如何被重定位到随机虚拟地址的。

3.2.3.1 vmlinux.bin(真正内核代码)物理地址随机化

Vmlinux.bin的入口函数是arch/x86/kernel/head_64.S中定义的startup_64,在内核还没有运行在自己的虚拟地址空间(0xffffffff81000000)之前,head_64.S中的代码必须是PIE,但vmlinux的编译是加-fno-pic选项的,所以这就要求我们手工将head_64.S写成PIE code。

这样做的目的:将vmlinux.bin加载到任意物理地址后,head_64.S能根据被重定位的虚拟基地址重新初始化PML512 paging structure,这样内核才可以正确的运行在已经被重定位的虚拟地址空间,这是因为之前定义的pgtable占用6pages,是实地址映射4G物理地址的,也就意味着只能管理4G虚拟地址空间,而现在内核的默认基地址是0xffffffff81000000,被重定位后一定是 > 0xffffffff81000000的,所以要重新初始化一个PML512 paging structure, 所以这里明白吧,head_64.S一定手工写成PIE code.

下图24是arch/x86/kernel/head_64.S的部分内容,让我们看看这里是如何实现PIE的。

从图中可以看出,我们手工写的汇编代码,是通过offset(%rip)这种相对寻址方式,实现PIE的。

020911712eb1798f51fd58fc7392f24e.png
图24上半部分

继续图24后续的代码

7c233131836e67127cf31f58ff85cf33.png
图24下半部分

以上是PML512 paging structure中Level4和Level3中引用的PDPT和PDT表物理基地址的重定位操作,为什么没有对level2表中的表项进行重定位,下面将详细讲如何进行虚拟地址的

重定位操作。

914bd82cf974fe3c7358070c7ca0a941.png
图25

如上图25所示,内核这0xffffffff80000000到0xffffffffC0000000 的1G虚拟地址空间,是1:1顺序映射到0~1G的物理内存的。

这就是为什么默认的物理加载地址是LOAD_PHYSICAL_ADDRESS=16M, 默认的虚拟地址的基地址是0xffffffff81000000, 如果KASLR=disable,那么内核的虚拟基地址0xffffffff81000000整好是映射到加载物理地址16M处的,所以下图26的head_64.S中的代码虽然还会执行,但是%rbp=0,所以虚拟地址是不需要remapping的.

efb9f6a1dd12b419f3cfa846c92071c5.png
图26

下面终于可以开始详解讲解handle_relocations函数,它是虚拟地址随机化的核心。

static void handle_relocations(void *output, unsigned long output_len,

unsigned long virt_addr)

参数output: 随机选取的物理加载地址16M alingned

参数output_len: 解压后vmlinux.bin+vmlinux.relocs的大小

参数virt_addr: 随机选取的虚拟地址16M aligned。

这里有一点要注意:之前详细讲了在没有开启KALSR的情况下,是没有relocs文件的,所以

Arch/x86/boot/compressed/head_64.S中会将protect-mode kernel部分copy到默认的物理加载地址LOAD_PHYSICAL_ADDRESS=16M的某处。

这时是需要计算extra_bytes,并得到output_len+=extra_bytes, 最终将protect-mode kernel复制到LOAD_PHYSICAL_ADDRESS+output_len作为顶点的地址处,这样运行decompressing code将解压后的kernel放在16M地址开始处时,才不会覆盖未解压的kernel.

具体示意图如下图27所示:

e8df62aed72db8acd818c75dcbe6aa48.png

图27

当没开启KASLR时,被解压的kernel是放在output_pointer指向的16M地址开始处的,所以当output_pointer移动过快,超过了input_pointer(指向未解压内核)时会有问题,所以才要计算最坏情况下extra_bytes的大小。

当开启KASLR时,有两种情况:1: output_pointer=16M,2: output_pointer=(16*(n+1),n>=1)

第一种情况就必须计算extra_bytes了。

第二种情况就不必计算extra_bytes了(output_pointer不可能和input_poiner重叠)。

下面来看看该函数的具体的一些代码片段。

634223e356cb89a3aedba38434ed36b9.png
图28_b

下面代码中的注释详细描述了多个relocs表的位置关系。

c7c966c16f10699f1aec1cbec3a41f76.png
图28_c

下面的代码就是处理物理加载地址和虚拟地址decouple后的到不同随机化值的处理过程

0b5d2b00f9e7e8c44a6077909c1e6c37.png
图28_d

下面以32bit relocs表中最后一个要被重定位的虚拟地址为例,详述虚拟地址重定位过程。

下图是vmlinux.relocs中最先被处理的32bit relocs表,以0x0000为结束标志。

bb9ab122b55cff193272e2416f404762.png
图29

如上图29所示,32bit relocs表中最后一个要被重定位的虚拟地址是0x81000016, 所以

该虚拟地址在vmlinux.bin中的offset=0x81000016-0x81000000=0x16.

下图30就是vmlinux.bin的二进制文件。

0aaf6c6c406f8248a8dbd5518302f475.png
图30

如上图30所示,0x200016处就是要被重定位的地方,对应的虚拟地址就是0xFFFFFFFF81000016,再根据下图31所示。

96a5dcd1a810e34e268295f830f57edb.png
图31

要被加载的load类型segment是从文件vmlinux.bin中offset=0x200000=2M处开始的,所以

要被重定位的虚拟地址0xFFFFFFFF81000016在内存中的位置就是output+0x16=32M+0x16.

所以图28中extended=32M+0x16,故 *(uint32_t *)ptr += delta,由图21可知虚拟地址0xFFFFFFFF81000016处存储的值是0x1000000=16M和图30中0x200016存储的值是一致的。

所以此处重定位后的值=0x1000000+virt_addr - LOAD_PHYSICAL_ADDR=virt_addr=128M.

所以arch/x86/boot/kernel/head_64.S中第一个要被重定位的虚拟地址:

对应的指令就是:subq $_text - __START_KERNEL_map, %rbp

其对应的操作码是 :fffffff81000013: 48 81 ed 00 00 00 01 //sub $0x1000000,%rbp

Relocate后的操作码是:fffffff81000013: 48 81 ed 00 00 00 08 //sub $0x8000000,%rbp

最终%rbp=32M-128M=-96M, 哈哈看到没,终于跟上面图24中计算的%rbp=-96M就完全对应上了

终于写完了,其实内核boot和kaslr这块是非常绕的,也是非常精彩的一部分,但是要想条理清晰的写出来真的是太繁琐了,自觉的写的很凌乱,因为涉及到的知识点很多,没有一定基础的话看了肯定还是云山雾绕的,就这样了,希望对那些想了解这块的朋友能有点帮助吧,总之,linux内核一定要深入到毛细血管。

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值