Linux Kernel 中的保护机制和攻击方法
Linux Kernel 中的保护机制和攻击方法
KASLR
使用此选项后,每次系统启动时,内核代码在内存中的位置都是随机的,如果想要调用到内核上的代码段,我们可能需要泄露指针来计算当前内核的基地址
当不开启 kaslr
的时候,默认的内核虚拟机基地址是:0xffffffff81000000
开启与关闭
如果是使用 qemu 启动的内核,可以在 qemu 的 -append
选项中添加 kaslr
来开启,关闭则使用 nokaslr
绕过 KASLR
- 根据泄露内核地址指针,通过偏移计算出内核基址和内核偏移
- 让 关闭
kaslr
时的目标内核函数地址加上内核偏移,计算得到目标函数地址(其中关闭kaslr
时的内核函数地址可以通过在nokaslr
时,在root
权限下读取/proc/kallsyms
得到)
FGKASLR - Function Granular KASLR
更细粒度的内核地址空间随机化,在加载时刻,按照函数级别的细粒度来重排内核代码。简单来说就是函数与函数之间也被打乱了,不能通过 leak_addr - offset
的方法得到内核基址和其他函数地址。
实现
利用 GCC 的编译选项 -ffunction-sections
把内核中不同的函数放到不同的 section 中,在编译的过程中,任何使用 C 语言编写的函数以及不在特殊输入节的函数都会作为一个独立的 section,使用汇编编写的代码会唯一一个统一的 section 中。
通过编译后 vmlinux 保留的所有的节区头(Section Headers)就可以知道每个函数的地址范围,同时,FGKASLR 还有一个重定位地址的扩展表。结合这两组信息,就可以乱序重排函数了。
开启与关闭
- 开启:在编译 FGKASLR 内核时,需要开启
CONFIG_FG_KASLR=y
选项,FGKASLR 也支持模块的随机化,可以使用CONFIG_MODULE_FG_KASLR=y
开启模块的 FGKASLR - 关闭:在
--append
使用nokaslr
关闭 KASLR 的同时也会关闭 FGKASLR,但是也可以单独使用nofgkaslr
来关闭 FGKALSR
绕过 FG_KASLR
以函数粒度随机化,如果函数内的某个地址泄露了,那么函数内部的相对地址也可知
.text
节区不参与函数随机化。因此,一旦知道其中的某个地址,就可以获取该节区所有的地址。系统调用的入口代码都在该节区内,主要是因为这些代码都是汇编代码,该节区具有以下不错的 gadgetswapgs_restore_regs_and_return_to_usermode
,可以用于 bypass KPTImemcpy
内存拷贝sync_regs
,可以把 rax 放到 rdi 中
内核符号表
__ksymtab
相对于内核镜像的偏移是固定的,因此,如果我们可以泄露数据,那就可以借此泄露出其他的符号地址- 根据
kernel_base + offset
计算出__ksymtab
地址(__ksymtab
中每个记录项的名字的格式为__ksymtab_func_name
,以prepare_kernel_cred
为例,对应的记录项的名字为__ksymtab_prepare_kernel_cred
) - 根据
__ksymtab
获取对应符号记录项的地址 function_addr = __ksymtab_function_addr + value_offset - 2^32
1 2 3 4 5
struct kernel_symbol { int value_offset; int name_offset; int namespace_offset; };
- 根据
如果存在任意地址读写时,可以通过暴力搜索内存比对字节码的方法找到想要的 gadget 和 字符串,也可以暴力搜索堆空间找到
cred
结构体改掉
SMEP - Supervisor Mode Execution Protection
用户代码不可执行,由于内核在 R0 层,比 R3 层的用户态代码权限更高,所以起初在内核态执行代码时,可以直接执行用户态的代码。那如果攻击者控制了内核中的执行流,就可以直接执行用户态的代码,更容易实施攻击。在 Linux 内核中,这个防御的实现是与指令集相关的,在 x86 下对应的保护机制的名字为 SMEP,CR4 寄存器中的第 20 位用来标记是否开启 SMEP 保护
开启与关闭
默认情况下,SMEP 保护是开启的
如果是使用 qemu 启动的内核,可以在 qemu 的 -cpu
选项中添加 +smep
来开启 SMEP,如果不启用可不加
攻击 SMEP
设置 CR4 寄存器:把 CR4 寄存器中的第 20 位置为 0 后,我们就可以执行用户态的代码。一般而言,会使用 0x6f0 (011011110000b) 来设置 CR4,这样 SMAP 和 SMEP 都会被关闭
SMAP - Supervisor Mode Access Protecton
用户数据不可访问,在劫持内核控制流程后,可以通过栈迁移的手段将栈迁移到用户态来进行 ROP,由于用户态中控制数据容易,更容易实施攻击。在 Linux 内核中,这个防御的实现是与指令集相关的,在 x86 下对应的保护机制的名字为 SMAP,CR4 寄存器中第 21 位 来标记是否开启 SMAP 保护
开启与关闭
默认情况下,SMAP 保护是开启的
如果是使用 qemu 启动的内核,可以在 qemu 的 -cpu
选项中添加 +smap
来开启 SMEP,如果不启用可不加
攻击 SMAP
设置 CR4 寄存器:像 SMEP 的攻击手法一样,如果我们可以控制程序流程,就可以通过执行内核中对应的 gadget 来修改 CR4 为 0x6f0
copy_from_user / copy_to_user:在劫持控制流后,攻击者可以调用 copy_from_user
和 copy_to_user
来访问用户态内存,这两个函数会临时清空 SMAP 标志位,同时这两个函数在编写内核程序时也经常用到
|
|
|
|
KPTI - Kernel Page Table Isolation
Linux 采用四级页表结构(PGD -> PUD -> PMD -> PTE),其中 CR3 寄存器用以储存当前 PGD 的地址,在开启了 KPTI(内核页表隔离)后,内核空间和用户空间的 PGD 从原来的同一个被分割成了两个 PGD,但是为了提高切换的速度,这两个 PGD 放在一段连续的内存中,如下图所示
其中内核空间的 PGD 在低地址,用户空间的 PGD 在高地址,一张表占用 4K 内存,两张表共占用 8K 内存,两者在物理内存相邻并且相差 0x1000 字节,这样只需要将 CR3 的第 13 位取反就能完成页表切换的操作(其中 1 为 用户空间的 PGD,0 为 内核空间的 PGD)
开启 KPTI 后的这两张页表存在一定的区别
- 用户页表上除了有对用户内存空间的完整映射,还有少量的内核代码(例如系统调用入口点、中断处理等)
- 内核页表中有内核内存空间和用户内存空间的完整映射,但其中用户地址空间部分对应的部分不拥有可执行权限(NX),类似于
SMEP
保护,但目前还没有引入模拟出SMAP
保护,无法使用ret2usr
,但是可以用用户态的数据来进行栈迁移 ROP
攻击 KPTI
我们不能像之前那样直接 swapgs; iret
直接的返回用户态,而是在返回用户态前还要把用户进程的页表给切换回来,可以借助一个用于完成内核态页表切换回到用户态页表的函数 swapgs_restore_regs_and_return_to_usermode
中的 gadget,该函数可以直接从 mov rdi, rsp
开始执行,大概进行了如下操作
|
|
其中的 or rdi, 0x1000
即设置了 CR3 寄存器第 13 位 为 1 从而指向用户内存空间的 PGD
在布置 ROP 的时候,只需要布置出如下栈布局即可(在 iretq
时,栈顶应为 user_rip
):
|
|
其中,
fake rax
和fake rdi
的内容不重要,可以用任意数据填充user_rip
可以填写返回用户态后要执行的函数,例如提权后执行的system('/bin/bash')
user_cs
、user_rflsgs
、user_sp
、user_ss
可以通过在用户态执行以下代码读取到
|
|
堆块隔离
如果在使用 kmem_cache_create
创建一个 cache 时,传递了 SLAB_ACCOUNT
标记,那么这个 cache 就会单独存在,不会与其他相同大小的 cache 合并。
在早期,许多结构体对应的堆块并不单独存在,会和相同大小的堆块使用相同的 cache。在 Linux 4.5 版本引入了这个 flag 后,许多结构体就单独使用了自己的 cache,以下结构体都拥有独立的 cache。
threadinfo
task_struct
task_delay_info
pid
cred
mm_struct
vm_area_struct
andvm_region (nommu)
anon_vma
andanon_vma_chain
signal_struct
sighand_struct
fs_struct
files_struct
fdtable
andfdtable->full_fds_bits
dentry
andexternal_name
- inode for all filesystems