内核基础知识
kernel
用于管理软件发出的数据I/O
要求的程序,将要求转义为指令交给CPU
和其他组件处理,包括 I/O
,权限控制,系统调用,进程管理,内存管理等,kernel 的 crash 通常会引起重启
- 控制并与硬件进行交互
- 提供应用能运行的环境
Ring Model
cpu
分为ring 0-3
四个特权级别,ring0
仅os
使用,ring3
所以程序可用,大多操作系统仅ring0
和ring3
Loadable Kernel Modules(LKMs)
可加载核心模块 (或直接称为内核模块) ,是内核空间的可执行程序,包括驱动程序(设备驱动、文件系统驱动)和内核扩展模块,和用户态的可执行程序相同,Linux
下为 ELF
,Windows
下为 exe/dll
,mac
下为 MACH-O
模块可以被单独编译,但不能单独运行,运行时被链接到内核作为内核的一部分在内核空间运行,这与运行在用户控件的进程不同
Linux
内核之所以提供模块机制,是因为它本身是一个单内核 (monolithic kernel
)。单内核的优点是效率高,因为所有的内容都集合在一起,但缺点是可扩展性和可维护性相对较差,模块机制就是为了弥补这一缺陷。
相关指令
- insmod: 将指定模块加载到内核中
- rmmod: 从内核中卸载指定模块
- lsmod: 列出已经加载的模块
- modprobe: 添加或删除模块,加载模块时会查找依赖关系
大多数
CTF
中的kernel vulnerability
也出现在LKM
中。
syscall
系统调用,指的是用户空间的程序向操作系统内核请求需要更高权限的服务,系统调用提供用户程序与操作系统间的接口,部分库函数实际上是对系统调用的封装
ioctl
NAME
ioctl - control device
SYNOPSIS
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
ioctl
也是一个系统调用,用于与设备通信
int ioctl(int fd, unsigned long request, ...)
的第一个参数为打开设备 (open
) 返回的文件描述符,第二个参数为用户程序对设备的控制命令,再后边的参数则是一些补充参数,与设备有关
内核被设计成可扩展的,可以加入一个称为设备驱动的模块,驱动的代码允许在内核空间运行而且可以对设备直接寻址。对设备驱动的请求是一个以设备和请求号码为参数的
ioctl
调用,如此内核就允许用户空间访问设备驱动进而访问设备而不需要了解具体的设备细节,同时也不需要一大堆针对不同设备的系统调用
状态切换
user space to kernel space
当发生 系统调用, 产生异常 , 外设产生中断 等事件时,会发生用户态到内核态的切换,具体的过程为:
通过
swapgs
切换gs
段寄存器,将gs
寄存器值和一个特定位置的值进行交换,目的是保存gs
值,同时将该位置的值作为内核执行时的gs
值使用将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里,将 CPU 独占区域里记录的内核栈顶放入
rsp/esp
通过
push
保存各寄存器值,具体的代码 如下:ENTRY(entry_SYSCALL_64) /* SWAPGS_UNSAFE_STACK是一个宏,x86直接定义为swapgs指令 */ SWAPGS_UNSAFE_STACK /* 保存栈值,并设置内核栈 */ movq %rsp, PER_CPU_VAR(rsp_scratch) movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp /* 通过push保存寄存器值,形成一个pt_regs结构 */ /* Construct struct pt_regs on stack */ pushq $__USER_DS /* pt_regs->ss */ pushq PER_CPU_VAR(rsp_scratch) /* pt_regs->sp */ pushq %r11 /* pt_regs->flags */ pushq $__USER_CS /* pt_regs->cs */ pushq %rcx /* pt_regs->ip */ pushq %rax /* pt_regs->orig_ax */ pushq %rdi /* pt_regs->di */ pushq %rsi /* pt_regs->si */ pushq %rdx /* pt_regs->dx */ pushq %rcx tuichu /* pt_regs->cx */ pushq $-ENOSYS /* pt_regs->ax */ pushq %r8 /* pt_regs->r8 */ pushq %r9 /* pt_regs->r9 */ pushq %r10 /* pt_regs->r10 */ pushq %r11 /* pt_regs->r11 */ sub $(6*8), %rsp /* pt_regs->bp, bx, r12-15 not saved */
通过汇编指令判断是否为
x32_abi
通过系统调用号,跳到全局变量
sys_call_table
相应位置继续执行系统调用
kernel space to user space
退出时,流程如下:
- 通过
swapgs
恢复gs
值 - 通过
sysretq
或者iretq
恢复到用户控件继续执行。如果使用iretq
还需要给出用户空间的一些信息(CS
,eflags/rflags
,esp/rsp
等)
struct cred
kernel
用 cred
结构体记录了进程的权限等信息(uid
,gid
等),每个进程中都有一个 cred
结构,如果能修改某个进程的 cred
,那么也就修改了这个进程的权限
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
} __randomize_layout;
内核态函数
相比用户态库函数,内核态的函数有了一些变化
printf() -> printk()
,但需要注意的是printk()
不一定会把内容显示到终端上,但一定在内核缓冲区里,可以通过dmesg
查看效果memcpy() -> copy_from_user()/copy_to_user()
copy_from_user()
将用户空间的数据传送到内核空间copy_to_user()
将内核空间的数据传送到用户空间
malloc() -> kmalloc()
,内核态的内存分配函数,和malloc()
相似,但使用的是slab/slub 分配器
free() -> kfree()
,同kmalloc()
另外要注意的是,kernel 管理进程,也记录了进程的权限
,kernel
中改变权限的函数:
- int commit_creds(struct cred *new)
- struct cred* prepare_kernel_cred(struct task_struct* daemon)
从函数名也可以看出,执行 commit_creds(prepare_kernel_cred(0))
即可获得 root
权限,0
表示 以 0
号进程作为参考准备新的 credentials
执行 commit_creds(prepare_kernel_cred(0))
也是最常用的提权手段,两个函数的地址都可以在 /proc/kallsyms
中查看(较老的内核版本中是 /proc/ksyms
)。
$ sudo grep commit_creds /proc/kallsyms
ffffffffbb6af9e0 T commit_creds
ffffffffbc7cb3d0 r __ksymtab_commit_creds
ffffffffbc7f06fe r __kstrtab_commit_creds
$ sudo grep prepare_kernel_cred /proc/kallsyms
ffffffffbb6afd90 T prepare_kernel_cred
ffffffffbc7d4f20 r __ksymtab_prepare_kernel_cred
ffffffffbc7f06b7 r __kstrtab_prepare_kernel_cred
一般情况下,
/proc/kallsyms
的内容需要root
权限才能查看
Mitigation
canary
,dep
,PIE
,RELRO
等保护与用户态原理和作用相同
smep
:Supervisor Mode Execution Protection
,当处理器处于ring0
模式,执行用户空间的代码会触发页错误。(在arm
中该保护称为PXN
)smap
:Superivisor Mode Access Protection
,类似于smep
,通常是在访问数据时mmap_min_addr
CTF kernel pwn 相关
题目文件
boot.sh
: 一个用于启动kernel
的shell
的脚本,多用qemu
,保护措施与qemu
不同的启动参数有关bzImage
:kernel binary
rootfs.cpio
: 文件系统映像$x babydriver.tar boot.sh bzImage rootfs.cpio $file bzImage bzImage: Linux kernel x86 boot executable bzImage, version 4.4.72 (atum@ubuntu) #1 SMP Thu Jun 15 19:52:50 PDT 2017, RO-rootFS, swap_dev 0x6, Normal VGA $file rootfs.cpio rootfs.cpio: gzip compressed data, last modified: Tue Jul 4 08:39:15 2017, max compression, from Unix, original size 2844672 $file boot.sh boot.sh: Bourne-Again shell script, ASCII text executable $bat boot.sh ───────┬──────────────────────────────────────────────────────────── │ File: boot.sh ───────┼─────────────────────────────────────────────────────────── #!/bin/bash qemu-system-x86_64 \ -initrd rootfs.cpio \ -kernel bzImage \ -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' \ -enable-kvm -monitor /dev/null \ -m 64M \ --nographic \ -smp cores=1,threads=1 \ -cpu kvm64,+smep
-initrd rootfs.cpio
,使用rootfs.cpio
作为内核启动的文件系统-kernel bzImage
,使用bzImage
作为kernel
映像-cpu kvm64,+smep
,设置CPU
的安全选项,这里开启了smep
-m 64M
,设置虚拟RAM
为64M
,默认为128M
保护机制
KASLR
:内核地址随机化,相当于ASLR
(并非默认启用,需要在内核命令行中加入kaslr
开启)SMEP/SMAP
:[SMEP
管理模式执行保护,禁止内核访问用户空间的数据],[SMAP
管理模式访问保护,类似于NX
,即内核态无法执行shellcode
]Stack Protector
:(canary
)在编译内核时设置CONFIG_CC_STACKPROTECTOR
,开了这个保护再编译驱动会有canary
KPTI
:KPTI
即内核页表隔离(Kernel page-table isolation
),内核空间与用户空间分别使用两组不同的页表集,这对于内核的内存管理产生了根本性的变化攻击流程
- 解包
rootfs
,检查保护
$ mkdir File_system $ mv rootfs.cpio ./File_system/rootfs.cpio.gz $ cd File_system $ gunzip rootfs.cpio.gz $ cpio -idmv < rootfs.cpio $ file babydriver.ko $ checksec --file=babydriver.ko
2.拖进
IDA
在内核代码中找到漏洞3.利用
Shellcode
,ROP
, 等攻击方式实现代码执行4.提权
5.本地写好
exploit
后,可以通过base64
编码等方式把编译好的二进制文件保存到远程目录下,进而拿到flag
,同时可以使用musl
,uclibc
等方法减小exploit
的体积方便传输测试
exp
$ cp ./exp ./fs && cd fs $ find . | cpio -o --format=newc > ../rootfs.cpio # 重新打包文件系统 $ ./boot.sh # 启动&测试exp
目标
提权
即获取到 root
权限
改变自身
通过改变自身进程的权限,使其具有 root 权限,内核会通过进程的 task_struct
结构体中的 cred
指针来索引 cred
结构体,cred
结构体成员中的 uid-fsgid
都为 0
则具有 root
权限,这种提权方法分为定位和修改两个步骤,cred
结构体在include/linux/cred.h
直接改cred结构体的内容
定位cred具体位置
直接定位
cred
结构体的最前面记录了各种id
信息,对于一个普通的进程而言,uid-fsgid
都是执行进程的用户的身份,因此可以通过扫描内存来定位cred
在实际定位的过程中,我们可能会发现很多满足要求的 cred,这主要是因为 cred 结构体可能会被拷贝、释放。可以利用
usage
不为0
来筛除掉一些cred
,但仍然会发现一些usage
为0
的cred
,这是因为cred
从usage
为0
, 到释放有一定的时间,此外,cred
是使用rcu
延迟释放的间接定位
task_struct
进程的
task_struct
结构体中会存放指向cred
的指针,因此我们可以- 定位当前进程
task_struct
结构体的地址 - 根据
cred
指针相对于task_struct
结构体的偏移计算得出cred
指针存储的地址 - 获取
cred
具体的地址
- 定位当前进程
comm
comm
用来标记可执行文件的名字,位于进程的task_struct
结构体中,comm
在cred
的正下方,所以可以先定位comm
,然后定位cred
的地址const struct cred __rcu *ptracer_cred; const struct cred __rcu *real_cred; const struct cred __rcu *cred; #ifdef CONFIG_KEYS struct key *cached_requested_key; #endif char comm[TASK_COMM_LEN];
进程名字并不特殊的情况下可能会有多个同样的字符串,,可以使用
prctl
设置进程的comm
为一个特殊的字符串,然后再开始定位comm
修改
直接将
cred
中的uid-fsgid
都修改为0
,当然修改的方式有很多种在我们具有任意地址读写后,可以直接修改
cred
在我们可以
ROP
执行代码后,可以利用ROP gadget
修改cred
间接定位
不一定非得知道 cred
的具体位置,只需要能够修改 cred
即可
UAF 使用同样堆块-过时
如果我们在进程初始化时能控制 cred
结构体的位置,并且我们可以在初始化后修改该部分的内容,那么我们就可以很容易地达到提权的目的,且不需要任何信息泄露,例子:
- 申请一块与
cred
结构体大小一样的堆块 - 释放该堆块
fork
出新进程,恰好使用刚刚释放的堆块- 修改
cred
结构体特定内存,从而提权
此种方法在较新版本内核中已不再可行,我们已无法直接分配到 cred_jar 中的 object,这是因为 cred_jar
在创建时设置了 SLAB_ACCOUNT
标记,在 CONFIG_MEMCG_KMEM=y
时(默认开启)cred_jar 不会再与相同大小的 kmalloc-192 进行合并
void __init cred_init(void)
{
/* allocate a slab in which we can store credentials */
cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred), 0,
SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, NULL);
}
修改 task_struct 结构体中的 cred 指针
定位cred结构体指针具体位置
间接定位
task_struct
task_struct
结构体中会存放指向cred
的指针,可以定位task_struct
结构体的地址,再根据cred
指针的偏移计算得出cred
指针地址comm
在
cred
指针正下方,可以使用prctl
设置进程的comm
为一个特殊的字符串,然后再定位comm
const struct cred __rcu *ptracer_cred; const struct cred __rcu *real_cred; const struct cred __rcu *cred; #ifdef CONFIG_KEYS struct key *cached_requested_key; #endif char comm[TASK_COMM_LEN];
修改
- 修改
cred
指针为内核镜像中已有的init_cred
的地址。这种方法适合于我们能够直接修改 cred 指针以及知道 init_cred 地址的情况 - 伪造一个
cred
,然后修改cred
指针指向该地址即可。比较麻烦,一般不使用
- 修改
间接定位
commit_creds(&init_cred)
commit_creds()
:将一个新的cred
设为当前进程task_struct
的real_cred
与cred
字段,调用该函数并传入一个具有root
权限的cred
,则能直接完成对当前进程的提权工作int commit_creds(struct cred *new) { struct task_struct *task = current;//内核宏,用以从 percpu 段获取当前进程的 PCB const struct cred *old = task->real_cred; //... rcu_assign_pointer(task->real_cred, new); rcu_assign_pointer(task->cred, new);
在内核初始化过程当中会以
root
权限启动init
进程,其cred
结构体为静态定义的init_cred
,可以通过commit_creds(&init_cred)
来完成提权的工作struct cred init_cred = { .usage = ATOMIC_INIT(4), #ifdef CONFIG_DEBUG_CREDENTIALS .subscribers = ATOMIC_INIT(2), .magic = CRED_MAGIC, #endif .uid = GLOBAL_ROOT_UID, .gid = GLOBAL_ROOT_GID, .suid = GLOBAL_ROOT_UID, .sgid = GLOBAL_ROOT_GID, .euid = GLOBAL_ROOT_UID, .egid = GLOBAL_ROOT_GID, .fsuid = GLOBAL_ROOT_UID, .fsgid = GLOBAL_ROOT_GID, .securebits = SECUREBITS_DEFAULT, .cap_inheritable = CAP_EMPTY_SET, .cap_permitted = CAP_FULL_SET, .cap_effective = CAP_FULL_SET, .cap_bset = CAP_FULL_SET, .user = INIT_USER, .user_ns = &init_user_ns, .group_info = &init_groups, .ucounts = &init_ucounts, };
commit_creds(prepare_kernel_cred(0))
-6.2
之后失效prepare_kernel_cred()
函数用以拷贝指定进程的cred
结构体,参数为NULL
时,该函数会拷贝init_cred
并返回一个有着root
权限的cred
:struct cred *prepare_kernel_cred(struct task_struct *daemon) { const struct cred *old; struct cred *new; new = kmem_cache_alloc(cred_jar, GFP_KERNEL); if (!new) return NULL; kdebug("prepare_kernel_cred() alloc %p", new); if (daemon) old = get_task_cred(daemon); else old = get_cred(&init_cred);
调用
commit_creds(prepare_kernel_cred(NULL))
能直接完成提权的工作不过自从内核版本
6.2
起,prepare_kernel_cred(NULL)
将不再拷贝 init_cred,而是将其视为一个运行时错误并返回 NULL,因此内核6.2
之后失效struct cred *prepare_kernel_cred(struct task_struct *daemon) { const struct cred *old; struct cred *new; if (WARN_ON_ONCE(!daemon)) return NULL; new = kmem_cache_alloc(cred_jar, GFP_KERNEL); if (!new) return NULL;
改变别人
通过影响高权限进程的执行,使其完成我们想要的功能
改数据
符号链接
如果一个 root
权限的进程会执行一个符号链接的程序,并且该符号链接或者符号链接指向的程序可以由攻击者控制,攻击者就可以实现提权
call_usermodehelper
一种内核线程执行用户态应用的方式,并且启动的进程具有
root
权限,通过改变某个变量指定的具体要执行的应用提权修改
modprobe_path
- 获取
modprobe_path
地址- 直接定位:取值确定,
modprobe_path
扫描内存寻找字符串 - 间接定位:相对内核基址偏移固定,先获取内核基址再通过偏移得到地址
- 直接定位:取值确定,
- 修改
modprobe_path
为指定的程序 - 触发
call_modprobe
- 执行满足相应要求的非法可执行文件
- 使用未知协议触发
模板如下:
// step 1. modify modprobe_path to the target value // step 2. create related file system("echo -ne '#!/bin/sh\n/bin/cp /flag /home/pwn/flag\n/bin/chmod 777 /home/pwn/flag\ncat flag' > /home/pwn/catflag.sh"); system("chmod +x /home/pwn/catflag.sh"); // step 3. trigger it using unknown executable system("echo -ne '\\xff\\xff\\xff\\xff' > /home/pwn/dummy"); system("chmod +x /home/pwn/dummy"); system("/home/pwn/dummy"); // step 3. trigger it using unknown protocol socket(AF_INET,SOCK_STREAM,132);
- 获取
修改
poweroff_cmd
poweroff_cmd
- 获取
poweroff_cmd
地址,方法同上 - 修改
poweroff_cmd
为指定的程序 - 劫持控制流执行
__orderly_poweroff
- 获取
改代码
修改root
权限执行的代码,内核中 vDSO
的代码会被映射到所有的用户态进程中,高特权的进程周期性地调用 vDSO
中的函数时可以考虑把 vDSO
中相应的函数修改为特定的 shellcode
,早期vDSO
是可写的,后来引入 post-init read-only
的数据,即将初始化后不再被写的数据标记为只读,来防御这样的利用
定位
vDSO
IDA
在vmlinux
里定位vDSO
点击
init_vdso函数 -> vdso_image_64 / vdso_image_x32 -> raw_data
或直接使用
raw_data
来寻找vDSO
内存中定位
直接定位:
vDSO
是一个ELF
文件,有ELF
文件头,vDSO
特定位置存储导出函数的字符串,根据这两个特征扫描内存vDSO
相对内核基址偏移固定,先获取内核基址再根据偏移得到地址
修改
vDSO
的特定函数为指定的shellcode
等待触发执行
shellcode
总结
改变自身
- 改
cred
结构体:扫描内存定位/task_struct
结构体存放cred
结构体指针/comm
偏移定位cred
结构体位置,通过任意地址写/rop
来改cred
中的uid-fsgid
为0
- 改
cred
结构体指针:task_struct
结构体存放cred
结构体指针/comm
偏移定位cred
结构体指针位置,能够直接修改cred
指针以及知道有root
权限的init_cred
地址时,修改cred
指针为内核镜像中已有的init_cred
的地址 UAF
使用同样堆块控制cred
结构体-过时commit_creds(&init_cred)
,传入有root
权限的init_cred
的地址commit_creds(prepare_kernel_cred(0))
-6.2
之后失效
改变别人
- 修改
modprobe_path
为指定程序之后触发call_modprobe
- 修改
poweroff_cmd
为指定的程序之后触发__orderly_poweroff
- 写
vDSO
为shellcode
泄露敏感信息
需要我们具有读取内核数据的能力,具体想要泄漏什么数据与利用场景紧密相关
DoS
即使得内核崩溃
- 触发内核中的某个漏洞让内核崩溃
- 触发内核中的死锁
- 触发大量的内核内存泄漏,即存在大量的内存被申请但是没有被释放
内核防御机制
隔离
内核态和用户态的隔离
默认
用户态不可直接访问内核态的数据、执行内核态的代码
SMEP
内核态不可执行用户态的代码/用户代码不可执行,位于cr4
的第20
位,不开的话会导致攻击者控制了内核中的执行流,就可以执行处于用户态的代码,由于用户态的代码是攻击者可控的,所以更容易实施攻击
开启:默认开启,qemu
开启在启动时加-append +smep
关闭:/etc/default/grub
中如下两行添加nosmep
,运行update-grub
并重启系统,qemu
中加-append nosmep
GRUB_CMDLINE_LINUX_DEFAULT="quiet"
GRUB_CMDLINE_LINUX="initrd=/install/initrd.gz"
状态查看:存在smep
字符即开启了smep
保护
grep smep /proc/cpuinfo
攻击:内核中存在固定的修改cr4
的代码,比如refresh_pce
函数、set_tsc_mode
函数,或者控制程序流后执行内核中的gadget
修改,将CR4
寄存器第20
位置0
后即关闭smep
,一般将CR4
设置位0x6f0
,这样同时也关闭了smap
SMAP
内核态不可访问用户态的数据/用户数据不可访问,位于cr4
的第21
位,不开smap
可能导致攻击者可以通过栈迁移将栈迁移到用户态,然后进行 ROP
开启/关闭/状态查看:同上
攻击:设置rc4
同上,劫持程序流之后可以调用 copy_from_user
和 copy_to_user
来访问用户态的内存。这两个函数会临时清空禁止访问用户态内存的标志
KPTI-Kernel Page Table Isolation
用户态不可看到内核态的页表;内核态不可执行用户态的代码,进一步增强了用户态和内核态内存的隔离,在开启 KPTI
机制后,用户态进入内核态或内核态进入用户态时都会进行页表切换
- 内核态页表:用户空间内存的页表(标记为不可执行,类似
smep
,未开启smap
则仍然可以访问用户态空间内存,只是不能执行shellcode
)+内核空间内存的页表 - 用户态的页表:用户空间内存的页表+必要的内核空间内存的页表(如用于处理系统调用、中断等信息的内存
Linux 4.15
中引入了 KPTI
机制,并且该机制被反向移植到了 Linux 4.14.11
,4.9.75,4.4.110
开启:-append kpti=1
关闭:-append nopti
状态查看:dmesg | grep 'page table'
攻击:KPTI
机制和 SMAP 、SMEP
不太一样,由于与源码紧密结合,似乎没有办法在运行时刻关闭
修改页表
用户态空间所有数据被标记了
NX
,可以修改对应页表权限,未开smep
修改后可以返回到用户态执行用户态代码SWITCH_TO_USER_CR3_STACK
控制内核执行返回用户态时执行的切换页表代码判断也可以正常返回到用户态,页表切换主要靠
SWITCH_TO_USER_CR3_STACK
汇编宏,只要能调用这部分代码即可进行页表切换.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
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
就可以切换页表并且返回至用户态fake rax fake rdi RIP CS EFLAGS RSP SS
sysret
rcx
、r11
取值rcx, save the rip of the code to be executed when returning to userspace r11, save eflags
构造栈
fake rdi rsp, the stack of the userspace
跳转
entry_SYSCALL_64
的如下部分即可返回到用户态SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi popq %rdi popq %rsp swapgs sysretq
signal handler
在用户态注册
signal handler
来执行用户态的代码,无需切换页表
内核自身内部不同对象间的隔离
堆块隔离
GFP_KERNEL & GFP_KERNEL_ACCOUNT 的隔离
GFP_KERNEL
与 GFP_KERNEL_ACCOUNT
是内核中最为常见与通用的分配 flag,常规情况下他们的分配都来自同一个 kmem_cache
——即通用的 kmalloc-xx
。
在 5.9
版本之前GFP_KERNEL
与 GFP_KERNEL_ACCOUNT
存在隔离机制,自内核版本 5.14 起又重新引入:
- 对于开启了
CONFIG_MEMCG_KMEM
编译选项的kernel
而言(默认开启),其会为使用GFP_KERNEL_ACCOUNT
进行分配的通用对象创建一组独立的kmem_cache
——名为kmalloc-cg-\*
,从而导致使用这两种 flag 的 object 之间的隔离。
SLAB_ACCOUNT
如果在使用 kmem_cache_create
创建一个 cache
时,传递了 SLAB_ACCOUNT
标记,那么这个 cache
就会单独存在,不会与其它相同大小的 cache
合并。在早期,许多结构体(如 cred
结构体)对应的堆块并不单独存在,会和相同大小的堆块使用相同的 cache
。在 Linux 4.5
版本引入了这个 flag
后,许多结构体就单独使用了自己的 cache
访问控制
使得内核中相应的对象具有一定的访问控制要求,比如不可写,或者不可读
信息泄露
dmesg_restrict
内核日志有一些地址信息或者敏感信息,因此对内核日志的访问进行限制
该选项用于控制是否可以使用 dmesg
来查看内核日志。当 dmesg_restrict
为0
时,没有任何限制;当该选项为 1 时,只有具有 CAP_SYSLOG
权限的用户才可以通过 dmesg
命令来查看内核日志
kptr_restrict
在输出内核地址时施加的限制,主要限制通过 /proc
获取的内核地址
选项配置:
0
:默认情况下,没有任何限制1
:使用%pK
输出的内核指针地址将被替换为0
,除非用户具有CAP_ SYSLOG
特权,并且group id
和真正的id
相等2
:使用%pK
输出的内核指针都将被替换为0
,与权限无关
当开启该保护后,攻击者就不能通过 /proc/kallsyms
来获取内核中某些敏感的地址了,如 commit_creds
、prepare_kernel_cred
Misc
__ro_after_init
使用 __ro_after_init
标记在 __init
阶段被初始化且初始化后就不会改变的内存,在 init
阶段结束后,不能够被再次修改
我们可以使用 set_memory_rw(unsigned long addr, int numpages)
来修改对应页的权限
mmap_min_addr
mmap_min_addr
是用来对抗 NULL Pointer Dereference
的,指定用户进程通过 mmap
可以使用的最低的虚拟内存地址
异常检测
Kernel Stack Canary
开启:开启编译选项CONFIG_CC_STACKPROTECTOR
关闭:重新编译内核,并关闭编译选项
状态检查:checksec
特点:x86
架构下同一个 task
共享 Canary
攻击:只要泄露一次系统调用中的canary
,同一task
的其他系统调用中的canary
就都被泄露了
随机化
KASLR
内核的代码段基地址等地址会整体偏移
开启:-append kaslr
关闭:-append nokaslr
攻击:泄露某个段的地址得到该段的基址
FGKASLR
在KASLR
基础上,在加载时刻以函数粒度重新排布内核代码
特点:.text
参与随机化,.data
不参与随机化
开启:开启CONFIG_FG_KASLR=y
选项
关闭:nokaslr
会同时关闭FGKASLR
,也可以单独使用nofgkaslr
缺点:
- 函数粒度随机化,如果函数内的某个地址知道了,函数内部的相对地址也就知道了
.text
节区不参与函数随机化,一旦知道其中的某个地址,就可以获取该节区所有的地址。系统调用的入口代码都在该节区内,且该节区具有以下gadget
swapgs_restore_regs_and_return_to_usermode
,用于绕过 KPTI 防护memcpy
内存拷贝sync_regs
,可以把RAX
放到RDI
中
__ksymtab
相对于内核镜像的偏移是固定的,如果我们可以泄露数据,那就可以泄露出其它的符号地址,如prepare_kernel_cred
、commit_creds
- 基于内核镜像地址获取
__ksymtab
地址 - 基于
__ksymtab
获取对应符号记录项的地址 - 根据符号记录项中具体的内容来获取对应符号的地址
- 基于内核镜像地址获取
data
节区相对于内核镜像的偏移是固定的。因此在获取了内核镜像的基地址后,就可以计算出数据区数据的地址,这个节区有一些重要数据比如modprobe_path
__ksymtab
格式
__ksymtab
中每个记录项的名字的格式为 __ksymtab_func_name
,以 prepare_kernel_cred
为例,对应的记录项的名字为__ksymtab_prepare_kernel_cred
,因此,我们可以直接通过该名字在 IDA 里找到对应的位置,如下
__ksymtab:FFFFFFFF81F8D4FC __ksymtab_prepare_kernel_cred dd 0FF5392F4h
__ksymtab:FFFFFFFF81F8D500 dd 134B2h
__ksymtab:FFFFFFFF81F8D504 dd 1783Eh
__ksymtab
每一项的结构为
struct kernel_symbol {
int value_offset;
int name_offset;
int namespace_offset;
};
第一个表项记录了重定位表项相对于当前地址的偏移。那么,prepare_kernel_cred
的地址应该为 0xFFFFFFFF81F8D4FC-(2**32-0xFF5392F4)=0xffffffff814c67f0
即__ksymtab_prepare_kernel_cred addr - (2 ** 32 - __ksymtab_prepare_kernel_cred)
.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