kernel 也是一个程序,用来管理软件发出的数据 I/O 要求,将这些要求转义为指令,交给 CPU 和计算机中的其他组件处理,kernel 是现代操作系统最基本的部分。
以上便是ctf wiki原话 ,所以大家也不要太过于认为其很难,其实跟咱们用户态就是不同而已,也可能就涉及那么些底层知识罢了(师傅轻喷,我就口嗨一下)。
而kernel 最主要的功能有两点:
控制并与硬件进行交互
提供 application 能运行的环境
包括I/O,权限控制,系统调用,进程管理,内存管理等多项功能都可以归结到上边两点中。
需要注意的是,kernel 的crash 通常会引起重启。(所以咱们这点调试的时候就挺不方便的了,相比于用户态而言),不过这里也可能我刚开始学比较笨而已。
(1)intel CPU 将 CPU 的特权级别分为 4 个级别:Ring 0, Ring 1, Ring 2, Ring 3。
(2)Ring0 只给 OS 使用,Ring 3 所有程序都可以使用,内层 Ring 可以随便使用外层 Ring 的资源。
(3)使用 Ring Model 是为了提升系统安全性,例如某个间谍软件作为一个在 Ring 3 运行的用户程序,在不通知用户的时候打开摄像头会被阻止,因为访问硬件需要使用 being 驱动程序保留的 Ring 1 的方法。
注意大多数的现代操作系统只使用了 Ring 0 和 Ring 3。
也就是系统调用,指的是用户空间的程序向操作系统内核请求需要更高权限的服务,比如 IO 操作或者进程间通信。系统调用提供用户程序与操作系统间的接口,部分库函数(如scanf,puts 等 IO 相关的函数实际上是对系统调用的封装(read 和 write))。
user space to kernel space
当发生 系统调用,产生异常,外设产生中断等事件时,会发生用户态到内核态的切换,具体的过程为:
(1)通过swapgs切换 GS 段寄存器,将 GS 寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用。
(2)将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里,将 CPU 独占区域里记录的内核栈顶放入 rsp/esp。(这里我在调试的时候发现没整rbp,我最开始就发现这里怎么只保存了rsp,这个问题暂时还不是很了解)
(3)通过 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 */
(4)通过汇编指令判断是否为 x32_abi。
(5)通过系统调用号,跳到全局变量 sys_call_table 相应位置继续执行系统调用。
这里再给出保存栈的结构示意图,这里我就引用下别的师傅的图了。注意这是保存在内核栈中:
退出时,流程如下:
(1)通过 swapgs 恢复 GS 值
(2)通过 sysretq 或者 iretq 恢复到用户控件继续执行。如果使用 iretq 还需要给出用户空间的一些信息(CS, eflags/rflags, esp/rsp 等)
咱们要管理进程的权限,那么内核必定会维护一些数据结构来保存,他是用 cred 结构体记录的,每个进程中都有一个 cred 结构,这个结构保存了该进程的权限等信息(uid,gid 等),如果能修改某个进程的 cred,那么也就修改了这个进程的权限。
下面就是cred的数据结构源码:
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 __rcu *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 */
struct rcu_head rcu; /* RCU deletion hook */
} __randomize_layout;
基础知识介绍完毕,咱们开始介绍咱们内核pwn的最主要的目的。
二
借用arttnba3师傅的原话:“毫无疑问,对于内核漏洞进行利用,并最终提权到 root,在黑客界是一种最为 old school 的美学。
咱们在内核pwn中,最重要以及最广泛的那就是提权了,其他诸如dos攻击等也行,但是主要是把人家服务器搞崩之类的,并没有提权来的高效。
所谓提权,直译也即提升权限,是在咱们已经在得到一个shell之后,咱们进行深入攻击的操作,那么请问如何得到一个shell呢,那就请大伙好好学习用户模式下的pwn吧。
而与提权息息相关的那不外乎两个函数,不过咱们先不揭晓他们,咱们先介绍一个结构体:在内核中使用结构体 task_struct 表示一个进程,该结构体定义于内核源码include/linux/sched.h中,代码比较长就不在这里贴出了。
一个进程描述符的结构应当如下图所示:
注意到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;
看到熟悉的字眼没,对,那就是cred结构体指针。前面我们讲到,一个进程的权限是由位于内核空间的cred结构体进行管理的,那么我们不难想到:只要改变一个进程的cred结构体,就能改变其执行权限。
在内核空间有如下两个函数,都位于kernel/cred.c中:
这里我们也可以看到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);
三
保护措施
与用户态ASLR类似,在开启了 KASLR 的内核中,内核的代码段基地址等地址会整体偏移。
KASLR 虽然在一定程度上能够缓解攻击,但是若是攻击者通过一些信息泄露漏洞获取到内核中的某个地址,仍能够直接得知内核加载地址偏移从而得知整个内核地址布局,因此有研究者基于 KASLR 实现了 FGKASLR,以函数粒度重新排布内核代码。
类似于用户态程序的 canary,通常又被称作是 stack cookie,用以检测是否发生内核堆栈溢出,若是发生内核堆栈溢出则会产生 kernel panic
内核中的 canary 的值通常取自 gs 段寄存器某个固定偏移处的值。
SMAP即管理模式访问保护(Supervisor Mode Access Prevention),SMEP即管理模式执行保护(Supervisor Mode Execution Prevention),这两种保护通常是同时开启的,用以阻止内核空间直接访问/执行用户空间的数据,完全地将内核空间与用户空间相分隔开,用以防范ret2usr(return-to-user,将内核空间的指令指针重定向至用户空间上构造好的提权代码)攻击。
SMEP保护的绕过有以下两种方式:
四
环境利用
首先咱们拿到个ctf题目之后,咱们一般是先解包,会发现有这些个文件:
1.baby.ko
baby.ko是包含漏洞的程序,一般使用ida打开分析,可以根据init文件的路径去rootfs.cpio里面找。
2.bzImage
bzImage是打包的内核代码,一般通过它抽取出vmlinx,寻找gadget也是在这里。
3.initramfs.cpio
initramfs.cpio是内核采用的文件系统。
4.startvm.sh
startvm.sh是启动QEMU的脚本
5.vmlinux
静态编译,未压缩的内核文件,可以在里面找ROP
6.init文件
在rootfs.cpio文件解压可以看到,记录了系统初始化时的操作,一般在文件里insmod一个内核模块.ko文件,通常是有漏洞的文件
7..ko文件:需要拖到IDA里面分析找漏洞的文件,也即一般的漏洞出现的文件
---
之后咱们可以利用rootfs.cpio解压的文件中看到init脚本,此即为加载文件系统的脚本,在一般为boot.sh或start.sh脚本中也记录了qemu的启动参数
我的办法比较笨,那就是本地编译然后放到文件系统里面在压缩为cpio,这样再启动虚拟机的时候就会重新加载这个文件系统了。
全部0条评论
快来发表一下你的评论吧 !