0
点赞
收藏
分享

微信扫一扫

Linux kernel pwn 基础知识

小磊z 2022-05-06 阅读 98

linux 内核

kernel 最主要的功能有两点:

  • 控制并与硬件进行交互
  • 提供 application 能运行的环境

Linux内核包含了哪些内容:

  • 系统调用接口SCI层提供了某些机制执行从用户空间到内核的函数调用。这个接口依赖于体系结构。
  • 进程管理:进程的执行。在内核中,这些进程称为线程,代表了单独的处理器虚拟化(线程代码、数据、堆栈、CPU寄存器)。用户空间使用进程这个术语,但是Linux实现并没有区分这两个的概念(进程和线程),内核通过SCI提供了一个应用程序编程接口API,来创建一个新进程,停止进程,并在他们之间进行通信和同步。进程管理还包括处理获得进程之间共享CPU的需求。内核实现了一种新型的调度算法,不管有多少个线程在竞争CPU,这种算法都可以在固定时间内进行操作。调度程序也可以支持处理器(称为对称多处理器或SMP);
  • 内存管理:如果由硬件管理虚拟内存,内存是按照所谓的内存页方式进行管理的(4KB),Linux包括了管理可用内存的方式,以及物理和虚拟映射所使用的硬件机制。但是内存管理要管理的可不止4KB缓冲区。Linux提供了对4KB缓冲区的抽象,例如slab分配器。这种内存管理模式使用的是4KB缓冲区为基数,然后从中分配结构,并跟踪内存页使用情况,比如哪些内存是满的,哪些页面没有完全使用,哪些页面为空。由于这个原因,页面可以移除内存并放入磁盘中。这个过程叫交换,因为页面会被从内存交换到硬盘上。Linux系统中,被用于交换的分区叫swap分区,在windows下叫做虚拟内存。
  • 文件系统:虚拟文件系统(VFS)是Linux内核中非常有用的一个方面,因为它为文件系统提供了一个通用的接口抽象。VFSSCI和内核所支持的文件系统做了一个交换层。在VFS上面,是对oepn,close之类的函数的一个通用API抽象。在VFS下面是文件系统抽象,它定义了上层函数的实现方式。
  • 网络管理:网络堆栈在设计上遵循模拟协议本身的分层体系结构
  • 设备驱动:Linux内核中有大量代码都在设备驱动程序中,它们能够运转特定的硬件设备。Linux源码提供了一个驱动程序子目录,这个目录又进一步划分为各种支持设备,例如Bluetooth,I2C,serial等。

不同于Windows NT内核和Mach(Mac OS X 的组成部分)的微内核结构,linux内核采用的是单内核结构,效率高,但是体积大。
在这里插入图片描述

Ring Model

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

大多数的现代操作系统只使用了 Ring 0Ring 3
在这里插入图片描述

  • 内核空间运行在Ring 0特权等级,拥有自己的空间,位于内存的高地址。
  • 用户空间则是我们平时应用程序运行的空间,运行在Ring 3特权等级,使用较低地址。

Model Change

中断

中断即硬件/软件向 CPU 发送的特殊信号,CPU 接收到中断后会停下当前工作转而执行中断处理程序,完成后恢复原工作流程

中断向量表(interrupt vector table)类似一个虚表,该表通常位于物理地址 0~1k处,其中存放着不同中断号对应的中断处理程序的地址

自保护模式起引入中断描述符表(Interrupt Descriptor Table)用以存放 「门描述符」(gate descriptor),中断描述符表地址存放在 IDTR 寄存器中,CPU 通过中断描述符表访问对应门

「门」(gate)可以理解为中断的前置检查物件,当中断发生时会先通过这些「门」,主要有如下三种门:

  • 中断门(Interrupt gate):用以进行中断处理,其类型码为 110;中断门的 DPL(Descriptor Priviledge Level)为 0,故只能在内核态下访问,即中断处理程序应当由内核激活;进入中断门会清除 IF 标志位以关闭中断,防止中断嵌套的发生
  • 陷阱门(Trap gate):类型码为 111,类似于中断门,主要用以处理 CPU 异常,但不会清除 IF 标志位
  • 系统门System gate):Linux 特有门,类型码为 3、4、5、128;其 DPL 为 3,用以供用户进程访问,主要用以进行系统调用(int 0x80)

用户态->内核态

当发生 系统调用产生异常外设产生中断等事件时,会发生用户态到内核态的切换,具体的过程为:

  • 通过swapgs切换GS段寄存器,将GS寄存器值和一个特定位置的值进行交换,目的是为了保存GS值,同时将该位置的值作为内核执行时的GS值使用
    • GS寄存器的目的是访问CPU特定的内存。
  • 将当前栈顶(用户空间栈顶)记录在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。(linux 32位内核)
  • 通过系统调用号,跳到全局变量 sys_call_table 相应位置继续执行系统调用。

内核态->用户态

具体流程如下:

  • 通过swapgs恢复GS
  • 通过 sysretq 或者 iretq 恢复到用户控件继续执行。如果使用 iretq 还需要给出用户空间的一些信息(CS, eflags/rflags, esp/rsp 等)

关于syscall

系统调用,指的是用户空间的程序向操作系统内核请求需要更高权限的服务,比如 IO 操作或者进程间通信。系统调用提供用户程序与操作系统间的接口,部分库函数(如 scanf,puts 等 IO 相关的函数实际上是对系统调用的封装 (read 和 write))。
Int $0x80指令的目的是产生一个编号为128的编程异常,这个编程异常对应的是中断描述符表IDT中的第128项——也就是对应的系统门描述符。门描述符中含有一个预设的内核空间地址,它指向系统调用处理程程序:system_call().

关于ioctl

man手册中,关于这个函数的说明如下

NAME
       ioctl - control device
SYNOPSIS
       #include <sys/ioctl.h>
       int ioctl(int fd, unsigned long request, ...);

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>.

int ioctl(int fd, unsigned long request, ...)的第一个参数为打开设备 (open) 返回的 文件描述符,第二个参数为用户程序对设备的控制命令,再后边的参数则是一些补充参数,与设备有关。

进程权限管理

在这里插入图片描述
注意到task_struct的源码中有如下代码:

/* Process credentials: */

/* Tracer's credentials at attach: */
const struct cred __rcu		*ptracer_cred;

/* Objective and real subjective task credentials (COW): */
const struct cred __rcu		*real_cred;

/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu		*cred;

Process credentials 是 kernel 用以判断一个进程权限的凭证,在 kernel 中使用 cred 结构体进行标识,对于一个进程而言应当有三个 cred:

  • **ptracer_cred:**使用ptrace系统调用跟踪该进程的上级进程的cred(gdb调试便是使用了这个系统调用,常见的反调试机制的原理便是提前占用了这个位置)
  • real_cred:客体凭证objective cred),通常是一个进程最初启动时所具有的权限
  • cred:主体凭证subjective cred),该进程的有效cred,kernel以此作为进程权限的凭证

进程权限凭证: cred结构体

对于一个进程,在内核当中使用一个结构体cred管理其权限,该结构体定义于内核源码include/linux/cred.h中,如下:

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	*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 */
	/* RCU deletion */
	union {
		int non_rcu;			/* Can we skip RCU deletion? */
		struct rcu_head	rcu;		/* RCU deletion hook */
	};
} __randomize_layout;

我们主要关注cred结构体中管理权限的变量

用户ID和组ID

一个cred结构体中记载了一个进程四种不同的用户ID

  • 用户真实IDreal UID):标识一个进程启动时的用户ID
  • 保存用户IDsaved UID):标识一个进程最初的有效用户ID
  • 有效用户IDeffective UID):标识一个进程正在运行时所属的用户ID
  • 文件系统用户IDUID for VFS ops):标识一个进程创建文件时进行标识的用户ID

通常情况下这四个值都是相同的。

用户组ID同样分为四个:真实组保存组有效组文件系统组与上面类似。

提权

通过前面我们可以知道,只要我们改变一个进程的cred结构体,就能改变其执行权限。

内核空间下面有两个函数,都位于kernel/cred.c中:

  • struct cred* prepare_kernel_cred(struct task_struct* daemon):该函数用以拷贝一个进程的cred结构体,并返回一个新的cred结构体,需要注意的是daemon参数应为有效的进程描述符地址或者NULL
  • int commit_creds(struct cred *new):该函数用以将一个新的cred结构体应用到进程。

查看prepare_kernel_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);
...

prepare_kernel_cred()函数中,若传入的参数为NULL,则会缺省使用init进程的cred作为模板进行拷贝,即可以直接获得一个标识着root权限的cred结构体。那么我们不难想到,只要我们能够在内核空间执行commit_creds(prepare_kernel_cred(NULL)),那么就能够将进程的权限提升到root

IO

NIX/Linux追求高层次抽象上的统一,其设计哲学之一便是万物皆文件

万物皆文件

NIX/Linux设计的哲学之一 —— 万物皆文件,在Linux系统的视角下,无论是文件、设备、管道、还是目录,进程,甚至是磁盘,套接字等等,一切都可以被抽象成文件,一切都可以使用访问文件的方式进行操作:

通过这种哲学,Linux提供了操作的同一性:

  • 所有的读去操作都可以通过read进行
  • 所有的更改操作都可以通过write进行

我们还可以cat /dev/unrandom > /dev/dsp

进程文件系统

用以描述一个进程,其中包括一个该进程所打开的文件描述符、堆栈内存布局、环境变量等

进程文件系统本身是一个伪文件系统,通常被挂载到/proc目录下,并不真正占用存储内存,而是占用一定的内存

当一个进程被建立起来时,其进程文件系统便会挂载到/proc/[PID]下,我们可以在该目录下查看其相关信息。

文件描述符

进程通过文件描述符来完成对文件的访问,其在形式上是一个非负整数,本质上是对文件的索引值,进程所有执行I/O操作的系统调用都会通过文件描述符。

每个进程都独立有着一个文件描述符表,存放着该进程所打开的文件索引,每当进程成功打开一个现有文件/创建一个新文件时(通过系统调用open进行操作),内核会向进程返回一个文件描述符

kernel中有着一个文件表,由所有的进程共享。

每个*NIX进程都应当有着三个标准的POSIX文件描述符,对应着三个标准文件流:

  • stdin:标准输入 = 0
  • stdout:标准输出 = 1
  • stderr:标准错误 = 2

后面打开的文件描述符应当从标号3起始。

系统调用:ioctl

*NIX中一切都可以被视为文件,因为一切都可以访问文件的方式进行操作,Linux定义了系统调用ioctl供进程与设备之间进行通信

系统调用ioctl是一个用于设备输入输出操作的一个系统调用,调用方式如下:

int ioctl(int fd,unsigned long request, ...)
  • fd:设备的文件描述符
  • request:请求码
  • 其他参数

对于一个提供了ioctl通信方式的设备而言,我们可以通过其文件描述符、使用不同的请求码及其他请求参数通过ioctl系统调用完成不同的对设备的I/O操作

内核态函数调用

  1. printf变更为printk(),但需要注意的是printtk()不一定会把内容显示在终端上,当一定是在内核缓冲区里,可以通过dmesg查看效果
  2. memcpy变更为copy_from_user()/copy_to_user()
    • copy_from_user()将用户空间的数据传送到内核空间
    • copy_to_user()实现将内核空间的数据传送到用户空间
  3. malloc()变更为kmalloc(),内核态的内存分配函数,和malloc相似,但使层用slab,slub分配器.这个分配器通过一个多级的结构进行管理。
    1. 首先有cache层,cache是一个结构,也就是用来分配或者已经分配的一部分内核空间。kmalloc使用多个cache,一个cache对应一个2的幂的大小的一组内存对象。slab分配器严格按照cache去区分,不同的cache无法分配在一页内,slub分配器则较为宽松,不同的cache如果分配相同大小,可能会在一页内。
  4. free变更为kfree(),同kmalloc()

同时kernel负责管理进程,因此kernel也记录了进程的权限。kernel中有两个可以方便的改变权限的函数:

  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的内容需要root权限才能查看,如果以非root用户权限查看将显示地址为0
在这里插入图片描述

内核保护机制

smep

管理模式执行保护(Superivisor Mode Access Protection),禁止内核执行用户空间代码。当处理器处于ring 0模式,执行用户空间的代码会触发页错误。Linux下叫做PXN

smap

管理模式访问保护(Supervisor Mode Access Prevention),禁止内核访问用户地址空间,类似于smep。当处理器处于ring 0模式,访问用户空间的数据会触发页错误。ARM下叫做PAN(Privileged Access Never)

  • 对于没有SMAP/SMEP的情况下把内核指针重定向到用户空间的利用方式称为ret2usr
  • 上面两种保护的绕过方法:
    1. physmap是内核管理的一块非常大的连续的虚拟地址空间,为了提高效率,该地址空间和内存地址直接映射。内存地址相对physmap要小的多,导致了任何一个内存地址可以在physmap中找到对应的虚拟内存地址。我们知道用户空间的虚拟内存也会映射到内存地址,这就存在了连续虚拟内存地址映射到了同一个内存地址的情况。也就是说,我们在用户空间里创建的数据,代码就很有可能映射到physmap空间。那么在用户空间用mmap()将提权代码映射到内存,然后再在内核空间里找到其对应的副本,修改IP调到副本执行就可以了。因为physmap本身就在内核空间里,这种漏洞利用方式叫做ret2dir
    2. Intel下系统根据CR4控制寄存器的第20位标识是否开启SMEP保护,若能够通过kernel ROP改变CR4寄存器的值便能够关闭SMEP保护,完成SMEP-bypass,接下来就可以重新ret2usr
  • 关闭SMEP方法:修改/etc/default/grub文件中的GRUB_CMDLINE_LINUX=,加上nosmep/nosmap/nokaslr,然后update-grub就可以。

MMAP_MIN_ADDR

内核空间和用户空间共享虚拟内存地址,因此需要防止用户空间mmap的内存从0开始,从而缓解空指针引用攻击。windows系统从win8开始禁止在零页分配内存。从linux内核2.6.22开始可以使用sysctl设置mmap_min_addr来实现这一保护。

KASLR

内核地址空间布局随机化(Kernel Address Space Layout Randomization),开启后,允许kernel image加载到VMALLOC区域的任何位置。在未开启KASLR保护机制时,内核的基址为0xffffffff80000000, 内核会占用0xffffffff80000000~0xffffffffC0000000这1G虚拟地址空间

Dmesg Restrictions

通过设置/proc/sys/kernel/dmesg_restrict为1,可以将dmesg输出的信息视为敏感信息(默认为0);

Kernel Address Display Restriction

/proc/sys/kernel/kptr_restrict被设置1,导致无法通过/proc/kallsyms获取内核地址。

Kernel PageTable Isolation

KPTI,内核页表隔离,进程页表隔离。进程地址空间被分成了内核地址空间和用户地址空间,内核地址都是共享的,用户空间只能单独使用。为了防止用户程序获取内核数据,可以让用户地址空间和内核地址空间使用两组页表集。Windows称为KVA Shadow

由于有KPTI保护,即使关闭了smepsmap,也不能执行用户区间的代码,只能读,原因如下:
不隔离不意味着完全相同,填充内核态页表项时,KPTI 会给页表项加上_PAGE_NX 标志,以阻止执行内核态页表所映射用户地址空间的代码。在 KAISER patch 里把这一步骤叫 毒化(poison)。

STACK PROTECTOR

类似于用户态程序的canary,通常又被称作是stack cookie,用以检测是否发生内核堆栈溢出,若是发生内核堆栈溢出则会产生kernel panic内核中的canary的值通常取自gs段寄存器某个固定偏移处的值,可以直接绕过。

举报

相关推荐

0 条评论