双非本直博自救计划,pwn新手的学习之路

开始之前

笔者是双非本,本科前两年其实挺摆的,所以也没啥科研成果,均绩也不太好,尽管过程很曲折但是最后还是保研成功。笔者受家庭影响最开始对直博的看法还不错,所以推免时笔者也经常套瓷直博,最后来这所学校直博是因为离家近,没想到这竟然是噩梦的开始。

说实话,直博到现在感觉到的只有痛苦,课题组毫无进展、导师毫无实质性帮助只会不停push进度以及组内还需要做横向都让我无比失落,与读博之前的畅想完全不同。在这提前进组的一年,学习的东西都是浅尝辄止,因为之前一直根据课题组的需要来学习的,但是课题组根本没有一个确切的方向,学的东西也都不确定能不能用上,因此研究的对象也一直在换,就这样白白耽误了一年时间。

在这种情况下笔者一度产生提桶跑路的想法,不过最后还是放弃了,可能是因为笔者性格本身就不够勇敢,也可能是看了网上一些直博的心得发现大家都差不多(国内起码都差不多)。这期间笔者看到很应景的一句话,“读博就像是在青楼当妓女,你需要做的不是争做头牌,而是尽快赎身”。那时候我大概明白了,既然组里和导师都靠不住,那就靠自己吧,做自己想做的,组内的东西就不管了,导师骂归骂,自己做归做,只要达到毕业条件自然不会卡毕业。

想通之后笔者就释然了,组里的方向都没定我也不想帮忙了,想到之前很早就想研究的内核安全,遂来学习,争取在我自己感兴趣的方向做出点成果出来。虽然以这个方向毕业确实很难,但是起码是我愿意一直做下去的且是一个明确的目标。

有感而发,废话就说到这么多,由于笔者太菜可能存在一些术语使用不标准,如果有读者发现错误请不吝赐教,同时笔者借鉴了很多ctf-wiki和其他大佬的内容,都标注在文后了,如有侵权请联系yzsandw@gmail.com。

Operation System Kernel

操作系统内核(Operation System Kernel)本质上也是一种软件,可以看作是普通应用程式与硬件之间的一层中间层,其主要作用便是调度系统资源、控制 IO 设备、操作网络与文件系统等,并为上层应用提供便捷、抽象的应用接口。

操作系统内核其实是一个抽象出来的概念,实际上内核和普通进程是一样在内存中并且由cpu执行,不同之处在于cpu执行内核代码时通常处于高权限,拥有完全的硬件访问能力,而用户代码往往执行在低权限环境。

不同的权限状态其实是通过硬件来实现的,硬件提供的分级保护环实现这种区分。

hierarchical protection domains

分级保护域(hierarchical protection domains又被称作保护环,简称 Rings ,是一种将计算机不同的资源划分至不同权限的模型。

在一些硬件或者微代码级别上提供不同特权态模式的 CPU 架构上,保护环通常都是硬件强制的。Rings 是从最高特权级(通常被叫作 0 级)到最低特权级(通常对应最大的数字)排列的。

Intel 的 CPU 将权限分为四个等级:Ring0、Ring1、Ring2、Ring3,权限等级依次降低,现代操作系统模型中我们通常只会使用 ring0 和 ring3,对应操作系统内核与用户进程,即 CPU 在执行用户进程代码时处在 ring3 下。

  • Ring 0:权限最高,操作系统内核运行在这里,可以直接访问硬件、管理内存、切换进程等。
  • Ring 1/2:设计上是给驱动程序或中间层使用的,但在现代操作系统中几乎不用,常被忽略。
  • Ring 3:权限最低,用户程序运行在这里,不能直接访问硬件,只能通过系统调用进入内核。

这样分级的目的是为了保护内核和关键资源不被普通程序直接破坏

在其他架构(比如 ARM)里,也有类似的机制,不过叫做 异常等级 (Exception Levels, EL0–EL3),EL0 是用户程序,EL1 是内核,EL2 用于虚拟化,EL3 用于安全监控(TrustZone)。

Priv_rings.svg

现在我们给【用户态】与【内核态】这两个概念下定义:

  • 用户态:CPU 运行在 ring3 + 用户进程运行环境上下文。
  • 内核态:CPU 运行在 ring0 + 内核代码运行环境上下文。

状态切换

普通应用程序运行时处在用户态,只能执行受限指令。如果程序需要访问硬件或执行敏感操作(比如读写文件、分配内存、发送网络请求),就必须通过系统调用异常/中断的方式进入内核。CPU 接收到系统调用指令(如 int 0x80syscallsvc 等)后,会切换到内核态,跳转到内核定义的入口地址执行。内核完成操作后,再通过 iretsysret 之类的指令返回用户态,恢复程序继续运行。

user space to kernel space(系统调用)

当发生 系统调用产生异常外设产生中断 等事件时,会发生用户态到内核态的切换,进入到内核相对应的处理程序中进行处理。

系统调用是内核与用户通信的直接接口,因此我们主要关注用户空间比较常用的系统调用这一行为,其具体的过程为:

注:这部分代码都是在内核态中执行的

  1. 通过 swapgs 切换 GS 段寄存器,将 GS 寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用。
  2. 将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里,将 CPU 独占区域里记录的内核栈顶放入 rsp/esp。
  3. 通过 push 保存各寄存器值
  4. 通过汇编指令判断是否为 x32_abi
  5. 通过系统调用号,跳到全局变量 sys_call_table 相应位置继续执行系统调用。

kernel space to user space

退出时,流程如下:

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

内核漏洞利用的特点

在用户态漏洞利用时,利用的主要是进程本身的漏洞从而获取主机的控制权。而在内核态Pwn时主要会以“权限提升”作为主要目标,其前提看一看为在攻击者已经通过某种方式入侵了目标机器的情况下,攻击者利用内核漏洞进一步提升权限。因此“如何提升进程权限”成为一个问题,其中,我们需要知道进程权限是以什么样的形式存在的。

我们先来看一个进程的结构是怎么样的,在内核源码的include/linux/sched.h中定义了进程结构体task_struct,具体结构如下图所示。

image-20250826142629323

进程权限凭证(credential)

可以在task_struct的源码中看到进程权限凭证cred的相关定义:

1
2
3
4
5
6
7
8
9
10
/* 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;

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

  • 真实用户 ID(real UID):标识一个进程启动时的用户 ID
  • 保存用户 ID(saved UID):标识一个进程最初的有效用户 ID
  • 有效用户 ID(effective UID):标识一个进程正在运行时所属的用户 ID,一个进程在运行途中是可以改变自己所属用户的,因而权限机制也是通过有效用户 ID 进行认证的,内核通过 euid 来进行特权判断;为了防止用户一直使用高权限,当任务完成之后,euid 会与 suid 进行交换,恢复进程的有效权限
  • 文件系统用户 ID(UID for VFS ops):标识一个进程创建文件时进行标识的用户 ID

这几个ID可以这样理解:当你以用户态打开一个shell时,**真实用户ID(real UID)即为登录用户的ID,之后shell中想以root权限删除一个文件,使用sudo rm命令,这时候有效用户ID(effective UID)为root用户ID,这时候的删除进程实际上是普通用户创建的,但是其拥有root用户的权限。但是内核不会允许一个进程一直占据高权限,因此保存用户ID(saved UID)**将保存进程最开始的eUID,在之后euid 会与 suid 进行交换。

用户组 ID 同样分为四个:真实组 ID、保存组 ID、有效组 ID、文件系统组 ID,与用户 ID 是类似的,这里便不再赘叙。

进程权限改变

前面我们讲到,一个进程的权限是由位于内核空间的 cred 结构体进行管理的,那么我们不难想到:只要改变一个进程的 cred 结构体,就能改变其执行权限。

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

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

在内核版本6.2以前,prepare_kernel_cred()函数如果传入NULL,该函数会拷贝 init_cred 并返回一个有着 root 权限的 cred,不难想到,如果使用commit_creds(prepare_kernel_cred(NULL))就可以直接完成提权的操作。不过自从内核版本 6.2 起,prepare_kernel_cred(NULL)不再拷贝 init_cred,而是将其视为一个运行时错误并返回 NULL,这使得这种提权方法无法再应用于 6.2 及更高版本的内核。

Kernel安全机制

内核漏洞利用方式

Exploit编译与传输

参考文章

CTF Wiki

arttnba3佬的博客,是笔者很崇拜的大佬

Cyr1s佬的博客

Fernweh佬,也是笔者崇拜的大佬