kernel的保护机制

前言

我们从隔离、访问控制、异常检测、随机化这四种方式来介绍内核中的防御机制。

隔离

在内核的防御机制中,根据隔离的主体,我们将隔离分为两种

  • 内核态和用户态的隔离
  • 内核自身内部不同对象间的隔离

内核态和用户态的隔离

这里主要有

  • 默认:用户态不可直接访问内核态的数据、执行内核态的代码
  • SMEP:内核态不可执行用户态的代码
  • SMAP:内核态不可访问用户态的数据
  • KPTI:用户态不可看到内核态的页表;内核态不可执行用户态的代码(模拟)

用户代码不可执行

起初,在内核态执行代码时,可以直接执行用户态的代码。那如果攻击者控制了内核中的执行流,就可以执行处于用户态的代码。由于用户态的代码是攻击者可控的,所以更容易实施攻击。为了防范这种攻击,研究者提出当位于内核态时,不能执行用户态的代码。在 Linux 内核中,这个防御措施的实现是与指令集架构相关的。

x86 - SMEP - Supervisor Mode Execution Protection

x86 下对应的保护机制的名字为 SMEP。CR4 寄存器中的第 20 位用来标记是否开启 SMEP 保护。

20180220141919-fc10512e-1605-1

开启

默认情况下,SMEP 保护是开启的。

如果是使用 qemu 启动的内核,我们可以在 -append 选项中添加 +smep 来开启 SMEP。

关闭

/etc/default/grub 的如下两行中添加 nosmep

1
2
GRUB_CMDLINE_LINUX_DEFAULT="quiet"  
GRUB_CMDLINE_LINUX="initrd=/install/initrd.gz"

然后运行 update-grub 并且重启系统就可以关闭 smep。

如果是使用 qemu 启动的内核,我们可以在 -append 选项中添加 nosmep 来关闭 SMEP

状态查看

通过如下命令可以检查 SMEP 是否开启,如果发现了 smep 字符串就说明开启了 smep 保护,否则没有开启。

1
grep smep /proc/cpuinfo
Attack SMEP

把 CR4 寄存器中的第 20 位置为 0 后,我们就可以执行用户态的代码。一般而言,我们会使用 0x6f0 来设置 CR4,这样 SMAP 和 SMEP 都会被关闭。

内核中修改 cr4 的代码最终会调用到 native_write_cr4,当我们能够劫持控制流后,我们可以执行内核中的 gadget 来修改 CR4。从另外一个维度来看,内核中存在固定的修改 cr4 的代码,比如在 refresh_pce 函数、set_tsc_mode 等函数里都有。

用户数据不可访问

如果内核态可以访问用户态的数据,也会出现问题。比如在劫持控制流后,攻击者可以通过栈迁移将栈迁移到用户态,然后进行 ROP,进一步达到提权的目的。在 Linux 内核中,这个防御措施的实现是与指令集架构相关的。

x86 - SMAP - Supervisor Mode Access Protection

x86 下对应的保护机制的名字为 SMAP。CR4 寄存器中的第 21 位用来标记是否开启 SMEP 保护。

20180220141919-fc10512e-1605-1

开启

默认情况下,SMAP 保护是开启的。

如果是使用 qemu 启动的内核,我们可以在 -append 选项中添加 +smap 来开启 SMAP。

关闭

/etc/default/grub 的如下两行中添加 nosmap

1
2
GRUB_CMDLINE_LINUX_DEFAULT="quiet"  
GRUB_CMDLINE_LINUX="initrd=/install/initrd.gz"

然后运行 update-grub ,重启系统就可以关闭 smap。

如果是使用 qemu 启动的内核,我们可以在 -append 选项中添加 nosmap 来关闭 SMAP。

状态查看

通过如下命令可以检查 SMAP 是否开启,如果发现了 smap 字符串就说明开启了 smap 保护,否则没有开启。

1
grep smap /proc/cpuinfo
Attack SMEP

这里给出几种方式。

设置 CR4 寄存器

把 CR4 寄存器中的第 21 位置为 0 后,我们就可以访问用户态的数据。一般而言,我们会使用 0x6f0 来设置 CR4,这样 SMAP 和 SMEP 都会被关闭。

内核中修改 cr4 的代码最终会调用到 native_write_cr4,当我们能够劫持控制流后,我们就可以执行内核中对应的 gadget 来修改 CR4。从另外一个维度来看,内核中存在固定的修改 cr4 的代码,比如在 refresh_pce 函数、set_tsc_mode 等函数里都有。

copy_from/to_user

在劫持控制流后,攻击者可以调用 copy_from_usercopy_to_user 来访问用户态的内存。这两个函数会临时清空禁止访问用户态内存的标志。

KPTI - Kernel Page Table Isolation

KPTI 机制最初的主要目的是为了缓解 KASLR 的绕过以及 CPU 侧信道攻击。

在 KPTI 机制中,内核态空间的内存和用户态空间的内存的隔离进一步得到了增强。

  • 内核态中的页表包括用户空间内存的页表和内核空间内存的页表。
  • 用户态的页表只包括用户空间内存的页表以及必要的内核空间内存的页表,如用于处理系统调用、中断等信息的内存。

File:Kernel page-table isolation.svg

在 x86_64 的 PTI 机制中,内核态的用户空间内存映射部分被全部标记为不可执行。也就是说,之前不具有 SMEP 特性的硬件,如果开启了 KPTI 保护,也具有了类似于 SMEP 的特性。此外,SMAP 模拟也可以以类似的方式引入,只是现在还没有引入。因此,在目前开启了 KPTI 保护的内核中,如果没有开启 SMAP 保护,那么内核仍然可以访问用户态空间的内存,只是不能跳转到用户态空间执行 Shellcode。

Linux 4.15 中引入了 KPTI 机制,并且该机制被反向移植到了 Linux 4.14.11,4.9.75,4.4.110。

开启与关闭

如果是使用 qemu 启动的内核,我们可以在 -append 选项中添加 kpti=1 来开启 KPTI。

如果是使用 qemu 启动的内核,我们可以在 -append 选项中添加 nopti 来关闭 KPTI。

状态查看

我们可以通过以下两种方式来查看 KPTI 机制是否开启。

1
2
3
4
5
/home/pwn # dmesg | grep 'page table'
[ 0.000000] Kernel/User page tables isolation: enabled
/home/pwn # cat /proc/cpuinfo | grep pti
fpu_exception : yes
flags : ... pti smep smap
Attack KPTI

KPTI 机制和 SMAP 、SMEP 不太一样,由于与源码紧密结合,似乎没有办法在运行时刻关闭。

修改页表

在开启 KPTI 后,用户态空间的所有数据都被标记了 NX 权限,但是,我们可以考虑修改对应的页表权限,使其拥有可执行权限。当内核没有开启 smep 权限时,我们在修改了页表权限后就可以返回到用户态,并执行用户态的代码。

SWITCH_TO_USER_CR3_STACK

在开启 KPTI 机制后,用户态进入到内核态时,会进行页表切换;当从内核态恢复到用户态时,也会进行页表切换。那么如果我们可以控制内核执行返回用户态时所执行的切换页表的代码片段,也就可以正常地返回到用户态。

通过分析内核态到用户态切换的代码,我们可以得知,页表的切换主要靠SWITCH_TO_USER_CR3_STACK 汇编宏。因此,我们只需要能够调用这部分代码即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
.macro SWITCH_TO_USER_CR3_STACK scratch_reg:req
pushq %rax
SWITCH_TO_USER_CR3_NOSTACK scratch_reg=\scratch_reg scratch_reg2=%rax
popq %rax
.endm
.macro SWITCH_TO_USER_CR3_NOSTACK scratch_reg:req scratch_reg2:req
ALTERNATIVE "jmp .Lend_\@", "", X86_FEATURE_PTI
mov %cr3, \scratch_reg

ALTERNATIVE "jmp .Lwrcr3_\@", "", X86_FEATURE_PCID

/*
* Test if the ASID needs a flush.
*/
movq \scratch_reg, \scratch_reg2
andq $(0x7FF), \scratch_reg /* mask ASID */
bt \scratch_reg, THIS_CPU_user_pcid_flush_mask
jnc .Lnoflush_\@

/* Flush needed, clear the bit */
btr \scratch_reg, THIS_CPU_user_pcid_flush_mask
movq \scratch_reg2, \scratch_reg
jmp .Lwrcr3_pcid_\@

.Lnoflush_\@:
movq \scratch_reg2, \scratch_reg
SET_NOFLUSH_BIT \scratch_reg

.Lwrcr3_pcid_\@:
/* Flip the ASID to the user version */
orq $(PTI_USER_PCID_MASK), \scratch_reg

.Lwrcr3_\@:
/* Flip the PGD to the user version */
orq $(PTI_USER_PGTABLE_MASK), \scratch_reg
mov \scratch_reg, %cr3
.Lend_\@:
.endm

事实上,我们不仅希望切换页表,还希望能够返回到用户态,因此我们这里也需要复用内核中返回至用户态的代码。内核返回到用户态主要有两种方式:iret 和 sysret。下面详细介绍。

iret

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
SYM_INNER_LABEL(swapgs_restore_regs_and_return_to_usermode, SYM_L_GLOBAL)
#ifdef CONFIG_DEBUG_ENTRY
/* Assert that pt_regs indicates user mode. */
testb $3, CS(%rsp)
jnz 1f
ud2
1:
#endif
POP_REGS pop_rdi=0

/*
* The stack is now user RDI, orig_ax, RIP, CS, EFLAGS, RSP, SS.
* Save old stack pointer and switch to trampoline stack.
*/
movq %rsp, %rdi
movq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp
UNWIND_HINT_EMPTY

/* Copy the IRET frame to the trampoline stack. */
pushq 6*8(%rdi) /* SS */
pushq 5*8(%rdi) /* RSP */
pushq 4*8(%rdi) /* EFLAGS */
pushq 3*8(%rdi) /* CS */
pushq 2*8(%rdi) /* RIP */

/* Push user RDI on the trampoline stack. */
pushq (%rdi)

/*
* We are on the trampoline stack. All regs except RDI are live.
* We can do future final exit work right here.
*/
STACKLEAK_ERASE_NOCLOBBER

SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi

/* Restore RDI. */
popq %rdi
SWAPGS
INTERRUPT_RETURN

可以看到,通过伪造如下的栈,然后跳转到 movq %rsp, %rdi,我们就可以同时切换页表和返回至用户态。

1
2
3
4
5
6
7
fake rax
fake rdi
RIP
CS
EFLAGS
RSP
SS

sysret

在使用 sysret 时,我们首先需要确保 rcx 和 r11 为如下的取值

1
2
rcx, save the rip of the code to be executed when returning to userspace
r11, save eflags

然后构造如下的栈

1
2
fake rdi
rsp, the stack of the userspace

最后跳转至 entry_SYSCALL_64 的如下代码,即可返回到用户态。

1
2
3
4
5
6
SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi

popq %rdi
popq %rsp
swapgs
sysretq
signal handler

我们也可以考虑在用户态注册 signal handler 来执行位于用户态的代码。在这种方式下,我们无需切换页表。

内核自身内部不同对象间的隔离

堆块隔离

GFP_KERNEL & GFP_KERNEL_ACCOUNT 的隔离

GFP_KERNELGFP_KERNEL_ACCOUNT 是内核中最为常见与通用的分配 flag,常规情况下他们的分配都来自同一个 kmem_cache ——即通用的 kmalloc-xx

在 5.9 版本之前GFP_KERNELGFP_KERNEL_ACCOUNT 存在隔离机制,在 这个 commit 中取消了隔离机制,自内核版本 5.14 起,在 这个 commit 当中又重新引入:

  • 对于开启了 CONFIG_MEMCG_KMEM 编译选项的 kernel 而言(默认开启),其会为使用 GFP_KERNEL_ACCOUNT 进行分配的通用对象创建一组独立的 kmem_cache ——名为 kmalloc-cg-\* ,从而导致使用这两种 flag 的 object 之间的隔离。
SLAB_ACCOUNT

根据描述,如果在使用 kmem_cache_create 创建一个 cache 时,传递了 SLAB_ACCOUNT 标记,那么这个 cache 就会单独存在,不会与其它相同大小的 cache 合并。

1
2
3
4
5
6
7
8
9
10
11
12
Currently, if we want to account all objects of a particular kmem cache,
we have to pass __GFP_ACCOUNT to each kmem_cache_alloc call, which is
inconvenient. This patch introduces SLAB_ACCOUNT flag which if passed to
kmem_cache_create will force accounting for every allocation from this
cache even if __GFP_ACCOUNT is not passed.

This patch does not make any of the existing caches use this flag - it
will be done later in the series.

Note, a cache with SLAB_ACCOUNT cannot be merged with a cache w/o
SLAB_ACCOUNT, i.e. using this flag will probably reduce the number of
merged slabs even if kmem accounting is not used (only compiled in).

在早期,许多结构体(如 cred 结构体)对应的堆块并不单独存在,会和相同大小的堆块使用相同的 cache。在 Linux 4.5 版本引入了这个 flag 后,许多结构体就单独使用了自己的 cache。然而,根据上面的描述,这一特性似乎最初并不是为了安全性引入的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Mark those kmem allocations that are known to be easily triggered from
userspace as __GFP_ACCOUNT/SLAB_ACCOUNT, which makes them accounted to
memcg. For the list, see below:

- threadinfo
- task_struct
- task_delay_info
- pid
- cred
- mm_struct
- vm_area_struct and vm_region (nommu)
- anon_vma and anon_vma_chain
- signal_struct
- sighand_struct
- fs_struct
- files_struct
- fdtable and fdtable->full_fds_bits
- dentry and external_name
- inode for all filesystems. This is the most tedious part, because
most filesystems overwrite the alloc_inode method.

The list is far from complete, so feel free to add more objects.
Nevertheless, it should be close to "account everything" approach and
keep most workloads within bounds. Malevolent users will be able to
breach the limit, but this was possible even with the former "account
everything" approach (simply because it did not account everything in
fact).

访问控制

访问控制是指内核通过对某些对象添加访问控制,使得内核中相应的对象具有一定的访问控制要求,比如不可写,或者不可读。

信息泄漏

dmesg_restrict

考虑到内核日志中可能会有一些地址信息或者敏感信息,研究者提出需要对内核日志的访问进行限制。

该选项用于控制是否可以使用 dmesg 来查看内核日志。当 dmesg_restrict 为 0 时,没有任何限制;当该选项为 1 时,只有具有 CAP_SYSLOG 权限的用户才可以通过 dmesg 命令来查看内核日志。

1
2
3
4
5
6
7
8
9
10
dmesg_restrict:

This toggle indicates whether unprivileged users are prevented
from using dmesg(8) to view messages from the kernel's log buffer.
When dmesg_restrict is set to (0) there are no restrictions. When
dmesg_restrict is set set to (1), users must have CAP_SYSLOG to use
dmesg(8).

The kernel config option CONFIG_SECURITY_DMESG_RESTRICT sets the
default value of dmesg_restrict.

kptr_restrict

该选项用于控制在输出内核地址时施加的限制,主要限制以下接口

  • 通过 /proc 获取的内核地址
  • 通过其它接口(有待研究)获取的地址

具体输出的内容与该选项配置的值有关

  • 0:默认情况下,没有任何限制。
  • 1:使用 %pK 输出的内核指针地址将被替换为 0,除非用户具有 CAP_ SYSLOG 特权,并且 group id 和真正的 id 相等。
  • 2:使用 %pK 输出的内核指针都将被替换为 0 ,即与权限无关。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kptr_restrict:

This toggle indicates whether restrictions are placed on
exposing kernel addresses via /proc and other interfaces.

When kptr_restrict is set to 0 (the default) the address is hashed before
printing. (This is the equivalent to %p.)

When kptr_restrict is set to (1), kernel pointers printed using the %pK
format specifier will be replaced with 0's unless the user has CAP_SYSLOG
and effective user and group ids are equal to the real ids. This is
because %pK checks are done at read() time rather than open() time, so
if permissions are elevated between the open() and the read() (e.g via
a setuid binary) then %pK will not leak kernel pointers to unprivileged
users. Note, this is a temporary solution only. The correct long-term
solution is to do the permission checks at open() time. Consider removing
world read permissions from files that use %pK, and using dmesg_restrict
to protect against uses of %pK in dmesg(8) if leaking kernel pointer
values to unprivileged users is a concern.

When kptr_restrict is set to (2), kernel pointers printed using
%pK will be replaced with 0's regardless of privileges.

当开启该保护后,攻击者就不能通过 /proc/kallsyms 来获取内核中某些敏感的地址了,如 commit_creds、prepare_kernel_cred。

Misc

__ro_after_init

介绍

Linux 内核中有很多数据都只会在 __init 阶段被初始化,而且之后不会被改变。使用 __ro_after_init 标记的内存,在 init 阶段结束后,不能够被再次修改。

攻击

我们可以使用 set_memory_rw(unsigned long addr, int numpages) 来修改对应页的权限。

mmap_min_addr

mmap_min_addr 是用来对抗 NULL Pointer Dereference 的,指定用户进程通过 mmap 可以使用的最低的虚拟内存地址。

异常检测

通过对内核中发生的异常行为进行检测,我们可以缓解一定的攻击。

Kernel Stack Canary

Canary 是一种典型的检测机制。在 Linux 内核中,Canary 的实现是与架构相关的,所以这里我们分别从不同的架构来介绍。

x86

在 x86 架构中,同一个 task 中使用相同的 Canary。

开启

在编译内核时,我们可以设置 CONFIG_CC_STACKPROTECTOR 选项,来开启该保护。

关闭

我们需要重新编译内核,并关闭编译选项才可以关闭 Canary 保护。

状态检查

我们可以使用如下方式来检查是否开启了 Canary 保护

  1. checksec
  2. 人工分析二进制文件,看函数中是否有保存和检查 Canary 的代码

特点

可以发现,x86 架构下 Canary 实现的特点是同一个 task 共享 Canary。

攻击

根据 x86 架构下 Canary 实现的特点,我们只要泄漏了一次系统调用中的 Canary,同一 task 的其它系统调用中的 Canary 也就都被泄漏了。

随机化

我们可以通过增加内核的随机性来提高安全性。

KASLR

在开启了 KASLR 的内核中,内核的代码段基地址等地址会整体偏移。

开启与关闭

如果是使用 qemu 启动的内核,我们可以在 -append 选项中添加 kaslr 来开启 KASLR。

如果是使用 qemu 启动的内核,我们可以在 -append 选项中添加 nokaslr 来关闭 KASLR。

Attack

通过泄漏内核某个段的地址,就可以得到这个段内的所有地址。比如当我们泄漏了内核的代码段地址,就知道内核代码段的所有地址。

FGKASLR

鉴于 KASLR 的不足,有研究者实现了 FGKASLR。FGKASLR 在 KASLR 基地址随机化的基础上,在加载时刻,以函数粒度重新排布内核代码。

实现

FGKASLR 的实现相对比较简单,主要在两个部分进行了修改。目前,FGKASLR 只支持 x86_64 架构。

编译阶段

FGKASLR 利用 gcc 的编译选项 -ffunction-sections 把内核中不同的函数放到不同的 section 中。 在编译的过程中,任何使用 C 语言编写的函数以及不在特殊输入节的函数都会单独作为一个节;使用汇编编写的代码会位于一个统一的节中。

编译后的 vmlinux 保留了所有的节区头(Section Headers),以便于知道每个函数的地址范围。同时,FGKASLR 还有一个重定位地址的扩展表。通过这两组信息,内核在解压缩后就可以乱序排列函数。

最后的 binary 的第一个段包含了一个合并节(由若干个函数合并而成)、以及若干其它单独构成一个节的函数。

加载阶段

在解压内核后,会首先检查保留的符号信息,然后寻找需要随机化的 .text.* 节区。其中,第一个合并的节区 (.text) 会被跳过,不会被随机化。后面节区的地址会被随机化,但仍然会与 .text 节区相邻。同时,FGKASLR 修改了已有的用于更新重定位地址的代码,不仅考虑了相对于加载地址的偏移,还考虑了函数节区要被移动到的位置。

为了隐藏新的内存布局,/proc/kallsyms 中符号使用随机的顺序来排列。在 v4 版本之前,该文件中的符号按照字母序排列。

通过分析代码,我们可以知道,在 layout_randomized_image 函数中计算了最终会随机化的节区,存储在 sections 里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/*
* now we need to walk through the section headers and collect the
* sizes of the .text sections to be randomized.
*/
for (i = 0; i < shnum; i++) {
s = &sechdrs[i];
sname = secstrings + s->sh_name;

if (s->sh_type == SHT_SYMTAB) {
/* only one symtab per image */
if (symtab)
error("Unexpected duplicate symtab");

symtab = malloc(s->sh_size);
if (!symtab)
error("Failed to allocate space for symtab");

memcpy(symtab, output + s->sh_offset, s->sh_size);
num_syms = s->sh_size / sizeof(*symtab);
continue;
}

if (s->sh_type == SHT_STRTAB && i != ehdr->e_shstrndx) {
if (strtab)
error("Unexpected duplicate strtab");

strtab = malloc(s->sh_size);
if (!strtab)
error("Failed to allocate space for strtab");

memcpy(strtab, output + s->sh_offset, s->sh_size);
}

if (!strcmp(sname, ".text")) {
if (text)
error("Unexpected duplicate .text section");

text = s;
continue;
}

if (!strcmp(sname, ".data..percpu")) {
/* get start addr for later */
percpu = s;
continue;
}

if (!(s->sh_flags & SHF_ALLOC) ||
!(s->sh_flags & SHF_EXECINSTR) ||
!(strstarts(sname, ".text")))
continue;

sections[num_sections] = s;

num_sections++;
}
sections[num_sections] = NULL;
sections_size = num_sections;

可以看到,只有同时满足以下条件的节区才会参与随机化

  • 节区名以 .text 开头
  • section flags 中包含SHF_ALLOC
  • section flags 中包含SHF_EXECINSTR

因此,通过以下命令,我们可以知道

  • __ksymtab 不会参与随机化
  • .data 不会参与随机化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
> readelf --section-headers -W vmlinux| grep -vE " .text|AX"
...
[36106] .rodata PROGBITS ffffffff81c00000 e1e000 382241 00 WA 0 0 4096
[36107] .pci_fixup PROGBITS ffffffff81f82250 11a0250 002ed0 00 A 0 0 16
[36108] .tracedata PROGBITS ffffffff81f85120 11a3120 000078 00 A 0 0 1
[36109] __ksymtab PROGBITS ffffffff81f85198 11a3198 00b424 00 A 0 0 4
[36110] __ksymtab_gpl PROGBITS ffffffff81f905bc 11ae5bc 00dab8 00 A 0 0 4
[36111] __ksymtab_strings PROGBITS ffffffff81f9e074 11bc074 027a82 01 AMS 0 0 1
[36112] __init_rodata PROGBITS ffffffff81fc5b00 11e3b00 000230 00 A 0 0 32
[36113] __param PROGBITS ffffffff81fc5d30 11e3d30 002990 00 A 0 0 8
[36114] __modver PROGBITS ffffffff81fc86c0 11e66c0 000078 00 A 0 0 8
[36115] __ex_table PROGBITS ffffffff81fc8740 11e6738 001c50 00 A 0 0 4
[36116] .notes NOTE ffffffff81fca390 11e8388 0001ec 00 A 0 0 4
[36117] .data PROGBITS ffffffff82000000 11ea000 215d80 00 WA 0 0 8192
[36118] __bug_table PROGBITS ffffffff82215d80 13ffd80 01134c 00 WA 0 0 1
[36119] .vvar PROGBITS ffffffff82228000 14110d0 001000 00 WA 0 0 16
[36120] .data..percpu PROGBITS 0000000000000000 1413000 02e000 00 WA 0 0 4096
[36122] .rela.init.text RELA 0000000000000000 149eec0 000180 18 I 36137 36121 8
[36124] .init.data PROGBITS ffffffff822b6000 14a0000 18d1a0 00 WA 0 0 8192
[36125] .x86_cpu_dev.init PROGBITS ffffffff824431a0 162d1a0 000028 00 A 0 0 8
[36126] .parainstructions PROGBITS ffffffff824431c8 162d1c8 01e04c 00 A 0 0 8
[36127] .altinstructions PROGBITS ffffffff82461218 164b214 003a9a 00 A 0 0 1
[36129] .iommu_table PROGBITS ffffffff82465bb0 164fbb0 0000a0 00 A 0 0 8
[36130] .apicdrivers PROGBITS ffffffff82465c50 164fc50 000038 00 WA 0 0 8
[36132] .smp_locks PROGBITS ffffffff82468000 1651610 007000 00 A 0 0 4
[36133] .data_nosave PROGBITS ffffffff8246f000 1658610 001000 00 WA 0 0 4
[36134] .bss NOBITS ffffffff82470000 165a000 590000 00 WA 0 0 4096
[36135] .brk NOBITS ffffffff82a00000 1659610 02c000 00 WA 0 0 1
[36136] .init.scratch PROGBITS ffffffff82c00000 1659620 400000 00 WA 0 0 32
[36137] .symtab SYMTAB 0000000000000000 1a59620 30abd8 18 36138 111196 8
[36138] .strtab STRTAB 0000000000000000 1d641f8 219a29 00 0 0 1
[36139] .shstrtab STRTAB 0000000000000000 1f7dc21 0ed17b 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)

性能开销

FGKASLR 对于性能的影响主要来自于两个阶段:启动,运行。

启动阶段

在启动阶段,FGKASLR

  • 需要解析内核的 ELF 文件来获取需要随机化的节区。
  • 会调用随机数生成器来确定每个节区需要存储的地址,并进行布局。
  • 会将原有解压的内核拷贝到另外一个地方,以便于避免内存破坏。
  • 会增加内核需要重定位的次数。
  • 需要检查每一个需要重定位的地址是否位于随机化的节区,如果是的话,需要调整一个新的偏移。
  • 会重新排列那些需要按照地址排序的数据表。

在一个现代化的系统上,启动一个测试的 VM,大概花费了 1s。

运行阶段

运行阶段的开销其实主要取决于具体的负载。不过由于原先相邻的函数可能被随机化被放在不同的地址,所以相对而言,整体性能应该会有所降低。

内存开销

在启动阶段,FGKASLR 需要较多的堆内存。因此,FGKASLR 可能不适用于具有较小内存的系统上。这些内存会在内核解压后被释放。

程序大小影响

FGKASLR 会引入额外的节区头部信息,因此会增加 vmlinux 文件的大小。在标准的配置下,vmlinux 的大小会增加 3%。压缩后的镜像大小大概会增加 15%。

开启与关闭

开启

如果想要开启内核的 FGKASLR,你需要开启 CONFIG_FG_KASLR=y 选项。

FGKASLR 也支持模块的随机化,尽管 FGKASLR 只支持 x86_64 架构下的内核,但是该特性可以支持其它架构下的模块。我们可以使用 CONFIG_MODULE_FG_KASLR=y 来开启这个特性。

关闭

通过在命令行使用 nokaslr 关闭 KASLR 也同时会关闭 FGKASLR。当然,我们可以单独使用 nofgkaslr 来关闭 FGKASLR。

缺点

根据 FGKASLR 的特点,我们可以发现它具有以下缺陷

  • 函数粒度随机化,如果函数内的某个地址知道了,函数内部的相对地址也就知道了。

  • .text
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    节区不参与函数随机化。因此,一旦知道其中的某个地址,就可以获取该节区所有的地址。有意思的是系统调用的入口代码都在该节区内,主要是因为这些代码都是汇编代码。此外,该节区具有以下一些不错的 gadget

    - swapgs_restore_regs_and_return_to_usermode,该部分的代码可以帮助我们绕过 KPTI 防护
    - memcpy 内存拷贝
    - sync_regs,可以把 RAX 放到 RDI 中

    - ```asm
    __ksymtab
    相对于内核镜像的偏移是固定的。因此,如果我们可以泄露数据,那就可以泄露出其它的符号地址,如 prepare_kernel_cred、commit_creds。具体方式如下 - 基于内核镜像地址获取 __ksymtab 地址 - 基于 __ksymtab 获取对应符号记录项的地址 - 根据符号记录项中具体的内容来获取对应符号的地址
  • data 节区相对于内核镜像的偏移也是固定的。因此在获取了内核镜像的基地址后,就可以计算出数据区数据的地址。这个节区有一些可以重点关注的数据

    • modprobe_path
__ksymtab 格式

__ksymtab 中每个记录项的名字的格式为 __ksymtab_func_name,以 prepare_kernel_cred 为例,对应的记录项的名字为__ksymtab_prepare_kernel_cred,因此,我们可以直接通过该名字在 IDA 里找到对应的位置,如下

1
2
3
__ksymtab:FFFFFFFF81F8D4FC __ksymtab_prepare_kernel_cred dd 0FF5392F4h
__ksymtab:FFFFFFFF81F8D500 dd 134B2h
__ksymtab:FFFFFFFF81F8D504 dd 1783Eh

__ksymtab 每一项的结构为

1
2
3
4
5
struct kernel_symbol {
int value_offset;
int name_offset;
int namespace_offset;
};

第一个表项记录了重定位表项相对于当前地址的偏移。那么,prepare_kernel_cred 的地址应该为 0xFFFFFFFF81F8D4FC-(2**32-0xFF5392F4)=0xffffffff814c67f0。实际上也确实如此。

1
2
.text.prepare_kernel_cred:FFFFFFFF814C67F0                 public prepare_kernel_cred
.text.prepare_kernel_cred:FFFFFFFF814C67F0 prepare_kernel_cred proc near ; CODE XREF: sub_FFFFFFFF814A5ED5+52↑p