Kernel基础
2022-03-23 20:15:06

Kernel基础知识

kernel介绍:

kernel也是一个程序,用来管理软件发出的数据I/O要求,交给CPU和计算机中的其他组件处理,kernel是现代操作系统中最基础的部分

kernel主要功能:

1、控制并与硬件交互

2、提供applications能运行的环境

注意点:kernel的crash通常会引起重启

Ring Model

Intel CPU将CPU的特权级别分为4个等级:Ring 0,Ring 1,Ring 2,Ring 3

Ring 0只提供给OS使用,Ring 3可供所有程序使用

内层Ring 可以随便使用外层Ring资源===》提高系统安全

大多数现代操作系统只使用了Ring 0和Ring 3

LKMS

Loadable Kernel Modules

可加载核心模块/内核模块==》运行在内核空间的可执行程序

包含:

驱动程序(Device drivers):设备驱动,文件系统驱动……

内核扩展模块(modules)

LKMS的文件格式与用户态的可执行程序相同==》

Linux下为ELF,Windows下为exe/dll,mac下为MACH-O

模块可以被单独编译,但是不能单独运行,它在运行时被链接到内核作为内核的一部分在内核空间运行==》

与运行在用户空间的进程不同

模块通常用来实现一种文件系统,一个驱动程序或者其他内核上层的功能

Linux内核提供模块机制原因:

它本身是一个单向核,单向核的优点是效率高==》因为所有的内容都集合到一起

缺点:可扩展性和可维护性相对较差,模块机制就是为了弥补这一缺陷

模块相关指令

insmod:将指定模块加载到内核中

rmmod:从内核中卸载指定模块

lsmod:列出已经加载的模块

modpprobe:添加或删除模块,modprebe在加载模块时会查找依赖关系

内核系统调用

syscall:指的是用户空间向操作系统内核请求需要更高权限的服务

例:IO操作或者进程间通信

系统调用提供用户程序与操作系统之间的接口,部分库函数

设备通信ioctl

NAME
       ioctl - control device

SYNOPSIS
       #include <sys/ioctl.h>

       int ioctl(int fd, unsigned long request, ...);
//ioctl也是一个系统调用,用于设备通信
//第一个参数为打开设备(open)返回的文件描述符
//第二个参数为用户程序对设备的控制命令
//再后面的参数都为补充参数,与设备有关

DESCRIPTION
       The ioctl() system call manipulates the underlying device parameters of special
       files.  In particular, many  operating  characteristics  of  character  special
       files  (e.g., terminals) may be controlled with ioctl() requests.  The argument
       fd must be an open file descriptor.

       The second argument is a device-dependent request code.  The third argument  is
       an  untyped  pointer  to  memory.  It's traditionally char *argp (from the days
       before void * was valid C), and will be so named for this discussion.

       An ioctl() request has encoded in it whether the argument is an in parameter or
       out  parameter, and the size of the argument argp in bytes.  Macros and defines
       used in specifying an ioctl() request are located in the file <sys/ioctl.h>.

状态切换

用户态==》内核态

user space to kernel space

当发生系统调用,产生异常,外设产生中断等==》发生用户态到内核态的切换==》

1、通过swapgs切换GS段寄存器,将GS寄存器值和一个特定位置的值进行交换==》保存GS值

同时将该位置的值作为内核执行时的GS使用

2、将当前栈顶(用户空间栈顶)记录在CPU独占变量区域里,将CPU独占区域里记录的内核栈顶放入rsp/esp

3、通过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 */

4、通过汇编指令判断是否为x32_abi

5、通过系统调用号,跳转至全局变量sys_call_table相应位置继续执行系统调用

内核态==》用户态

kernel space to user space

1、通过swapgs恢复GS值

2、通过sysretq或者iretq恢复至用户控件继续执行,如果使用iretq还需要给出用户空间的一些信息

(CS,eflags/rflags,esp/rsp等)

struct cred

记录kernel进程的权限

每个进程中都有一个cred结构,该结构保存了该进程的权限信息(uid,gid……)

如果可以修改某个进程的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类似。但是使用的是slan/slub分配器

free()==>kfree(),与 kmalloc()类似

权限相关函数

1、int commit_creds(struct cred *new)

2、struct cred* prepare_kernel_cred(struct task_struct* daemon)

==》执行commit_creds(prepare_kernel_cred(0))==》获得root权限

0:以0号进程作为参考准备新的credentials

==》该函数地址==》**/proc/kallsyms**

保护 Mitigation

canary,dep,PIE,RELRO等保护与用户态原理和作用相同

smep:当处理器处于Ring 0,执行用户空间的代码会触发页错误(在ARM中该保护称为PXN)

smap:类似smep,通常是访问数据时的保护

mmap_min_addr:指定用户进程通过mmap可使用的最小虚拟内存地址,避免其在低地址空间产生映射导致安全问题

隔离 Insroduction

User and kernel

内核态与用户态的隔离

1、默认:用户态不可直接访问内核态的数据,执行内核态的代码

2、SMEP:内核态不可执行用户态的代码

CR4寄存器中的第20位用来标记是否开启SMEP,默认是开启状态

开启:qemu==》在-append选项中添加+smep

关闭:qemu==》在append选项中添加nosmep

查看:

grep smep /proc/cpuinfo

**bypass:**将CR4寄存器的第20位置设置为0,就可以执行用户态的代码

==》使用0x6f0来设置==》SMAP与SMEP都会关闭

==》修改RC4最终都会调用native_write_cr4==》劫持控制流

3、SMAP:内核态不可访问用户态的数据

CR4寄存器中的第21位用来标记是否开启SMAP保护,默认开启

开启:qemu==》在-append选项中添加+smap

关闭:qemu==》在append选项中添加nosmap

查看:

grep smap /proc/cpuinfo

bypass:

1、CR4设置0x6f0

2、攻击者可以调用copy_from_user和copy_to_user来访问用户态的内存==》

这两个函数会临时清空禁止访问用户态内存的标志

4、KPTI:用户态不可看到内核态的页表,内核态不可执行用户态的代码

内核态中的页表包括用户空间内存的页表和内核空间内存的页表

用户态的页表只包括用户空间内存的页表以及必要的内核空间内存的页表

例:用于处理系统调用,中断等信息的内存

==》KPTI将内核态的用户空间的内存映射部分全部标记为不可执行==》

没有SMEP特性的硬件,开启了KPTI,也会具有类似SMEP==》具体表现:

内核仍然可以访问用户态空间的内存,只是不能跳转到用户态空间执行shellcode

开启:qemu==》在-append选项中添加Kpti=1

关闭:qemu==》在-append选项中添加nopti

查看:

dmesg |grep 'page table'
//
cat /proc/cpuinfo |grep pti

bypass:

修改页表:开启了KPTI后,用户态空间所有数据被标记为NX权限==》考虑修改对应的页表权限

==》当内核没有开启smep==》在修改了页表权限后就可以返回用户态,并执行用户态的代码

SWITCH_TO_USER_CR3_STACK:在开启了KPTI,用户态进入内核态会进行页表转换,当从内核态恢复为用户态时,也会进行页表转换

==》页表转换主要靠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

==》

还需要返回用户态==》tret或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

==》fake_stack==》

fake rax
fake rdi
RIP
CS
EFLAGS
RSP
SS

===》跳转至movq %rsp, %rdi==》实现同时切换页表和返回用户态

sysret

首先确保rcx和r11为

rcx, save the rip of the code to be executed when returning to userspace
//保存返回用户空间时要执行的代码
r11, save eflags

fake_stack==》

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来执行位于用户态的代码==》无需切换页表

Inside kernel

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

在使用kmem_cache_create创建一个cache时,传递了SLAB_ACCOUNT标记==》

cache会单独存在,不与其他相同大小的cache合并

访问控制

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

信息泄漏

1、dmesg_restrict:

用于控制是否可以使用dmesg来查看日志==》

dmesg_restrict为0==》没有任何限制

dmesg_restrict为1==》只有具有CAP_SYSLOG权限的用户才可以通过dmesg命令来查看内核日志

2、Kptr_restrict:

用于控制在输出内核地址时施加的限制==》

限制:

通过/proc获取内核地址

通过其他接口获取地址

Kptr_restrict=0==》没有限制

Kptr_restrict=1==》使用%pk输出的内核指针地址将被替换为0,除非用户具有CAP_SYSLOG特权,并且group id和真正的id相等

Kptr_restrict=2==》使用%pk输出的内核指针将被替换为0,与权限无关

权限相关

Linux内核中有很多数据都只会在__init阶段初始化,之后不会改变==》

使用__ro_after_init标价的内存在init阶段结束后不能再次被修改

====》使用set_memory_rw(unsigned long addr, int numpages)修改对应页的权限

mmap_min_addr:

对抗空指针==》指定用户进程通过mmap可以使用最低的虚拟内存地址