linux内核递归漏洞——翻译自P0文章

  linux内核递归漏洞——翻译自P0文章
  

Linux给每一个用户进程分配了8M大小的栈,如果程序耗尽了这个栈的话,比如用了无限递归,就会触发栈后面的页保护。
  

但是Linux的内核栈就很不一样了,尤其是处理系统调用的时候。内核栈相对较短,32位系统上4096bytes,64位系统上bytes。内核栈由linux的伙伴系统来分配的,这也是linux系统中常规的页分配机制,并且伙伴系统并不提供页保护。这就意味着内核栈溢出的时候可写入正常数据。由于这个原因,内核代码必须非常小心,尽量不要申请大块内存,尽量避免过多的递归。
  

Linux系统中的绝大部分文件系统不会使用基础设备或者块设备作为后备设备。但有两种文件系统,ecryptfs和overlayfs是例外:它们复用文件系统,即利用一个存在于其他文件系统里的文件夹来作为后备设备。复用文件系统的作用是在转接对于底层文件系统的访问的时候,对交换的数据做一些修改。这样做的结果,overlayfs文件系统将多个文件系统整合到一起,而ecryptfs被用来做透明加密。
  

stacking filesystems的一个潜在问题是,因为虚拟文件系统的句柄经常调用其下层实际文件系统的句柄,这样比单纯和真实的文件系统交互麻烦很多,增加了内核栈的负荷。如果可能的话,将上层的虚拟系统当做另外一个虚拟系统的实际的底层系统,以此类推,无限递归,内核栈就会被耗荆当然,这可以通过设定FILESYSTEM_MAX_STACK_DEPTH变量的值来限定嵌套的层数——在一个没有被嵌套的文件系统上只可以嵌套两层。
  

procfs伪文件系统给每一个正在运行的进程准备了一个目录,每一个目录里面都包含了这个进程的描述文件。我们这里感兴趣的只是“mem”,“environ”和“cmdline”这三个文件,因为访问它们就是同步访问这个进程的虚拟内存。这些文件暴露了虚拟内存地址范围:
  

"mem"暴露了虚拟地址的整个范围
  

"environ"暴露了从PTRACE_MODE_ATTACH到mm->env_end的内存范围
  

"cmdline",如果mm->arg_end之前是0字节的话,会暴露从mm->arg_start到mm->arg_end的内存范围;它有点复杂
  

如果可以mmap这个"mem"文件的话,你可以这样设置页:
  

假设/proc/$pid/mem页面需要被载入,这个在进程C里的缺页异常需要从进程B里载入页面,这可以造成进程B的缺页异常,需要从进程A里面载入页面——递归缺页
  

这种情况实际上不会发生,mem、environ和cmdline文件只有VFS句柄,和正常的读写权限,并不能mmap:
  

static const struct file_operations proc_pid_cmdline_ops = {
  

.read = proc_pid_cmdline_read,
  

.llseek = generic_file_llseek,
  

static const struct file_operations proc_mem_operations = {
  
linux内核递归漏洞——翻译自P0文章
  

static const struct file_operations proc_environ_operations = {
  

.llseek = generic_file_llseek,
  

有趣的是,ecryptfs文件系统支持mmap。因为底层实际系统加密的页面,展示给用户的时候需要解密,ecryptfs不能把mmap操作直接交给底层实际文件系统的mmap。所以ecryptfs需要有自己的页面缓存。
  

当ecryptfs处理一个缺页异常的时候,它必须从底层文件系统中读取一个加密的页面。这可以通过读取底层文件系统的页面缓存实现,但这样做会浪费内存。于是,ecryptfs只是简单地利用底层文件系统的VFS句柄)。这高效又直接多了,但是也有副作用——有可能mmap到原本不可能被分页到的解密后的文件。
  

于是,我们可以把前面提到的这些点串联起来。首先创建进程A,PID是$A。然后在ecryptfs文件系统里创建了/tmp/$A,在真实的底层文件系统创建了/proc/$A。现在,如果对应的/proc/$A包含了有效的ecryptfs数据结构头部的话,/tmp/$A/mem,/tmp/$A/environ和/tmp/$A/cmdline都是可被分页到的。除非有root权限,否则无法在进程A中分配0地址,对应着/proc/$A/mem的偏移0处。所以访问/proc/$A/mem的开头永远返回-EIO,并且/proc/$A/mem也永远不可能有一个有效的加密数据头。因此,environ和cmdline的潜在威胁更大一些。
  

在那些用参数CONFIG_CHECKPOINT_RESTORE编译的linux内核上,mm_struct结构的成员arg_start,arg_end,env_start和env_end可以轻易地被非特权进程利用prctl修改。这可以在任意虚拟机制上映射/proc/$A/environ和/proc/$A/cmdline。
  

如果一个有效的加密过的ecryptfs文件被载入了进程A的内存空间,然后这个进程的运行环境被配置成指向这块内存,这个运行环境里解密后的数据/tmp/$A/environ就变得可访问了。然后,这个文件可以被映射进另一个进程,进程B。为确保上面的过程能够重复进行,一些数据需要被ecryptfs反复加密,在进程A里面创建了ecryptfs套娃。现在,一个互相指向对方解密后的运行环境内存的进程链表如下:
  

如果在进程C和进程B相应的内存页面没有数据的话,C进程里产生的缺页)引起ecryptfs文件系统去读取/proc/$B/environ,又引发了进程B的缺页,又通过ecryptfs去读/proc/$A/environ,再引发进程A的缺页。这个过程可以任意循环下去,造成内核栈溢出:
  

handle_mm_fault+0xf8b/0x1820
  

__get_user_pages+0x135/0x620
  

? alloc_pages_current+0x8c/0x110
  

? security_file_permission+0xa0/0xc0
  

ecryptfs_read_lower+0x23/0x30
  

ecryptfs_decrypt_page+0x82/0x130
  

ecryptfs_readpage+0xcd/0x110
  

handle_mm_fault+0xf8b/0x1820
  

__get_user_pages+0x135/0x620
  

? alloc_pages_current+0x8c/0x110
  

有关怎么触发这个洞:需要ecryptfs文件系统作为源挂载/proc/$pid。ecryptfs软件包安装好之后,通过/sbin/_private来完成挂载。
  

曾经攻击这样的漏洞很简单:Jon Oberheide's "The Stack is Back" slides讨论过,曾经还是可以溢出覆盖到栈底的thread_info结构,用适当的值覆盖restart_block或者addr_limit,取决于你选择攻击哪个,利用用户态可执行页面的代码或者利用copy_from_user和copy_to_user读写内核数据。
  

然而,restart_block被从thread_info结构中移除,而且触发栈溢出漏洞的时候有包含kernel_read的栈帧,所以addr_limit已经是KERNEL_DS,并且在返回时被置成USER_DS。另外,Ubuntu Xenial发行版内核开启了CONFIG_SCHED_STACK_END_CHECK配置选项,导致不论是否被调用,thread_info结构前面的canary都会被检查;如果canary不正确的话,内核会抛出panic。
  

由于在thread_info结构中找不到任何有价值的目标,我选择了一个不同的策略:仅仅溢出覆盖栈的前部,即攻击本栈帧本身和其他的栈数据。这种方法的问题是,canary和一些其他的thread_info结构内容不能被覆盖。栈看起来如下:
  

幸运的是,栈帧里有洞——如果递归的底部用cmdline而不是environ的话,会有一个5qword的洞在递归中没有被用到,可以避开从STACK_END_MAGIC到flags之间的所有数据。这些洞可以通过安全递归和内核调试模块观察到:
  

0xffffd: 0xffffeafeb0 0xffffd
  

0xffffd: 0xffffffff811bfc2b 0xdead505cdead5058
  

0xffffd: 0xdead5064dead5060 0xdead506cdead5068
  

0xffffd: 0xffffe3dff70 0xffffd1150d8
  

0xffffd: 0xffffffff811bacd5 0xdead512cdead5128
  

0xffffd: 0xdead5134dead5130 0xdead513cdead5138
  

0xffffd: 0xdead5144dead5140 0xdead514cdead5148
  

0xffffd: 0xffff8800d8364b00 0xffffd
  

0xffffd1154d0: 0xffffffff811bfc2b 0xdead54dcdead54d8
  

0xffffd1155a0: 0xffffffff811bacd5 0xdead55acdead55a8
  

0xffffd1155b0: 0xdead55b4dead55b0 0xdead55bcdead55b8
  

0xffffd1155c0: 0xdead55c4dead55c0 0xdead5dead55c8
  

0xffffd: 0xffffffff811bfc2b 0xdead595cdead5958
  

0xffffd: 0xdead5964dead5960 0xdead596cdead5968
  

0xffffd115a20: 0xffffffff811bacd5 0xdead5a2cdead5a28
  

0xffffd115a30: 0xdead5a34dead5a30 0xdead5a3cdead5a38
  

0xffffd115a40: 0xdead5a44dead5a40 0xdead5a4cdead5a48
  

下一个问题是这些洞只出现在特定深的栈帧中,要攻击成功,必须让它们出现在恰当的位置。这里有几个技巧可以利用:
  

在每次递归的时候,都可以选择利用environ或者是cmdline文件,它们产生的栈帧大小和洞的分布是不一样的
  

任意一个copy_from_user都是有效的引起缺页的入口。并且也可以把任意写地址的系统调用和VFS句柄结合起来,这两者共同影响栈的深度
  

最后,我把environ文件,cmdline文件,write系统调用和uid_map的VFS句柄结合起来。
  

现在我们可以递归覆盖到栈的前部并且不影响任何危险的栈内区域。当被溢出的栈返回的时候,内核线程必须被暂停,返回地址需要被覆盖,并且指向一个新的构造出来的栈,然后再恢复内核线程的运行。
  

为了在递归中终止内核线程,设定好前面的页链表之后,这个链表的最后一页可设置为FUSE页面
  

有关栈前部分填充的内容,我用了pipe。向一个新分配的空pipe写入数据的时候,linux的伙伴系统就会分配一个页面给它。exploit只是简单地用clone创建进程然后用pipe垃圾数据填充内存,这会触发缺页异常。用clone而不是fork因为需要更少的设置参数,减少了递归中的噪音。在clone的时候,所有的pipe页面都被填满,但除了在FUSE暂停递归进程的时候,一开始保存的在RSP之后的RIP没有被改写。写入得更少会导致第二个pipe在RIP被控制之前改写栈里边的内容,这可能使内核崩溃。一旦递归在FUSE里面停下,二次写入所有pipe,覆盖RIP和其后面的数据,并用新的攻击者控制的假栈帧代替。
  

最后一道防线就是ASLR了。正如ubuntu的官方描述Security Features page,x86和amd64上都支持ASLR,但是需要用户手动开启。这个bug已经被很快修复,现在所有的发行版应该默认开启了ASLR。由于绝大多数的机器上的系统并没有加入任何特殊的内核参数,所以这里假设KASLR并没有被编入内核,攻击者知道内核text和静态数据的地址。
  

然后你可以选择利用ROP来完砿it_creds),而我选择了另一条路。请注意下,在栈溢出发生的时候,addr_limit结构里面的KERNEL_DS变量的值,在最后返回的时候被设置成了USER_DS,但在这里我们是直接返回到的用户态地址空间,addr_limit结构里仍然是KERNEL_DS,所以我的exploit填充了一个新的栈:
  

0xfffffffff2, /* return pointer of syscall handler */
  

0x, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  

post_corruption_user_code, /* user RIP */
  

0x246, /* EFLAGS: most importantly, turn interrupts on */
  

结束掉FUSE服务进程之后,递归进程会在post_corruption_user_code函数处恢复,这个函数可以通过pipe写任意内核地址,因为检查函数copy_to_user被禁掉了:
  

现在你可以在用户态任意读写地址了,如果想要一个root权限的shell,可以覆盖coredump句柄,它的位置处在一个固定地址,然后触发SIGSEGV信号:
  

char *core_handler = "|/tmp/crash_to_root";
  

这个bug在两个独立的补丁中修复:2f36db禁止cryptfs在不用mmap句柄的时候打开文件,e54ad7f1ee26禁止procfs上面嵌套任何东西,因为还有其他很多黑魔法来利用procfs,并且在procfs上嵌套任何东西都是不必要的。
  

我写这篇完整的exploit攻击文章只是想演示下linux栈溢出可以发生在很隐蔽的地方,就算现有的防护全部开启,仍然是可以攻击成功的。在我的报告中,问过linux security list,为什么不像其他操作系统一样在内核栈中增加页保护并且在栈底移除thread_info结构,Andy Lutomirski已经开始着手这项工作了

You may also like...

发表评论

电子邮件地址不会被公开。 必填项已用*标注