Linux本地内核提权漏洞(CVE-2017-7184) – 作者:ellenZ

实验内容

此漏洞主要利用了linux内核IPSEC框架(自linux2.6开始支持)中的一个内存越界漏洞,CVE编号为CVE-2017-7184。

IPSEC协议简介

IPSEC是一个协议组合,它包含AH、ESP、IKE协议,提供对数据包的认证和加密功能。

为了帮助更好的理解漏洞成因,下面有几个概念需要简单介绍一下

1、SA(Security Associstion)

SA由spi、ip、安全协议标识(AH或ESP)这三个参数唯一确定。SA定义了ipsec双方的ip地址、ipsec协议、加密算法、密钥、模式、抗重放窗口等。

2、AH(Authentication Header)

AH为ip包提供数据完整性校验和身份认证功能,提供抗重放能力,验证算法由SA指定。

3、ESP(Encapsulating security payload)

ESP为ip数据包提供完整性检查、认证和加密。

实验工具

qemu虚拟机:QEMU是一款开源的模拟器及虚拟机监管器。QEMU主要提供两种功能给用户使用。一是作为用户态模拟器,利用动态代码翻译机制来执行不同于主机架构的代码。二是作为虚拟机监管器,模拟全系统,利用其他VMM来使用硬件提供的虚拟化支持,创建接近于主机性能的虚拟机。

gdb调试器:GDB是一个由GNU开源组织发布的、UNIX/LINUX操作系统下的、基于命令行的、功能强大的程序调试工具。

实验步骤

步骤一、漏洞分析

1、补丁分析

static inline int xfrm_replay_verify_len(struct xfrm_replay_state_esn *replay_es
    if (nla_len(rp) < ulen || xfrm_replay_state_esn_len(replay_esn) != ulen)
        return -EINVAL;
    if (up->replay_window > up->bmp_len * sizeof(__u32) * 8)
        return -EINVAL;
    return 0;
}

从补丁可以看到代码添加了对replay_windowbmp_len的大小检测,当replay_window > bmp_len的时候退出。

而这两个成员都来自同一个结构体:

struct xfrm_replay_state_esn {
    unsigned int    bmp_len;
    __u32       oseq;
    __u32       seq;
    __u32       oseq_hi;
    __u32       seq_hi;
    __u32       replay_window;
    __u32       bmp[0];
};

bmp_len决定整个结构体的具体大小. 而replay_window则决定了bmp数组的索引范围。

通过上述信息可以得知:如果没有对replay_windowbmp_len的大小进行检测,则可能出现索引到结构体之外的情况。

2、成因分析

问题出在xfrm_replay_state_esn结构体的replay_windowbmp_len

当我们发送一个XFRM_MSG_NEWAE类型的消息时,即可调用xfrm_new_ae函数来更新一个已存在的 SA。

这里有检测函数如下所示:

static inline int xfrm_replay_verify_len(
        struct xfrm_replay_state_esn *replay_esn,
        struct nlattr *rp)
{
    struct xfrm_replay_state_esn *up;
    int ulen;
    if (!replay_esn || !rp)
        return 0;
    up = nla_data(rp);
    ulen = xfrm_replay_state_esn_len(up);
    if (nla_len(rp) < ulen || xfrm_replay_state_esn_len(replay_esn) != ulen)
        return -EINVAL;
    return 0;
}

可以看到,这个函数没有对replay_window做任何检测,所以这个时候我们就可以控制replay_window超过bmp_len。这样,在后面流程的读写操作中,很可能就会触发越界。

触发越界的函数有两个,xfrm_replay_checkxfrm_replay_advance,其中xfrm_replay_advance有越界写操作:

static void xfrm_replay_advance_esn(struct xfrm_state *x, __be32 net_seq)
{
    unsigned int bitnr, nr, i;
    int wrap;
    u32 diff, pos, seq, seq_hi;
    struct xfrm_replay_state_esn *replay_esn = x->replay_esn;
    if (!replay_esn->replay_window)
        return;
    seq = ntohl(net_seq); 
    pos = (replay_esn->seq - 1) % replay_esn->replay_window;
    seq_hi = xfrm_replay_seqhi(x, net_seq);
    wrap = seq_hi - replay_esn->seq_hi;
    if ((!wrap && seq > replay_esn->seq) || wrap > 0) {
        if (likely(!wrap))
            diff = seq - replay_esn->seq;
        else
            diff = ~replay_esn->seq + seq + 1;
        if (diff < replay_esn->replay_window) {
            for (i = 1; i < diff; i++) {
                bitnr = (pos + i) % replay_esn->replay_window;
                nr = bitnr >> 5;
                bitnr = bitnr & 0x1F;
                replay_esn->bmp[nr] &=  ~(1U << bitnr);
            }
        } else {
            nr = (replay_esn->replay_window - 1) >> 5;
            for (i = 0; i <= nr; i++)
                replay_esn->bmp[i] = 0;
        }
        bitnr = (pos + diff) % replay_esn->replay_window;
        replay_esn->seq = seq;
        if (unlikely(wrap > 0))
            replay_esn->seq_hi++;
    } else {
        diff = replay_esn->seq - seq;
        if (pos >= diff)
            bitnr = (pos - diff) % replay_esn->replay_window;
        else
            bitnr = replay_esn->replay_window - (diff - pos);
    }
    nr = bitnr >> 5;
    bitnr = bitnr & 0x1F;
    replay_esn->bmp[nr] |= (1U << bitnr);
    if (xfrm_aevent_is_on(xs_net(x)))
        x->repl->notify(x, XFRM_REPLAY_UPDATE);
}

这里我们可以看到,有多处都能触发越界写操作。

其中 net_seqreplay_esn->seqreplay_esn->replay_windowreplay_esn->seq_hi 是我们可控的变量,所以我们可以通过控制这些变量让程序执行到我们想要到的流程,从而达到越界的目的。

这个函数在xfrm_input中被调用,如果采用IPPROTO_AH协议的话, 调用链如下所示:

xfrm4_ah_rcv -> xfrm4_rcv -> xfrm4_rcv_spi -> xfrm_input

这里通过如下伪代码测试是否能够成功触发越界写:

/* 用于向内核发送信息, 然后生成/更新xfrm_state结构体的套接字 */
xfrm_state_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_XFRM);
/* 用于接收IPPROTO_AH信息, 触发xfrm_input的接收套接字 */
recvfd = socket(AF_INET, SOCK_RAW, IPPROTO_AH);
/* 用于发送自定义数据包的发送套接字 */
sendfd = socket(AF_INET, SOCK_RAW, IPPROTO_AH);
/* 触发漏洞 */
trigger_oob(...);

步骤二、漏洞调试过程

1、开启qemu虚拟机以及远程调试

首先开启桌面上的xshell,新建连接,连接信息如下:

ip:172.16.12.2,user:ichunqiu,password:ichunqiu

进入目标机后,使用su命令切换至root权限,然后进入/home/ichunqiu目录下

复制当前会话窗口,右键->复制SSH渠道,新窗口为了运行qemu虚拟机系统

为了方便理解,新窗口简称为虚拟机,旧窗口命名为宿主机

12.png

在虚拟机窗口中输入如下命令开启qemu虚拟机:

qemu-system-x86_64 -kernel bzImage -hda wheezy.img -m 512M -nographic -append "console=ttyS0 root=/dev/sda" -net user,hostfwd=tcp::10011-:22 -net nic -s

过程需要几分钟,启动后使用root用户登陆,密码为ichunqiu

13.png

回到宿主机窗口,进入/home/ichunqiu/Tools目录下,运行./upload setcap./upload exp

root@localhost’s password:ichunqiu

16.png

之后在虚拟机窗口中,进入/tmp目录下,执行命令

./setcap cap_net_raw,cap_net_admin=eip ./exp
su nobody

18.png

回到宿主机窗口,进入/home/ichunqiu/linux-4.12目录下,输入命令:gdb vmlinux

然后启动远程调试,输入命令:target remote:1234

17.png

2、调试分析

首先我们确定xfrm_alloc_replay_state_esn的创建,因为我们传入了XFRMA_REPLAY_ESN_VAL,所以最终会进入函数xfrm_alloc_replay_state_esn,创建xfrm_replay_state_esn结构体

因此,我们需要在以下四个函数处下断点,执行命令如下:

b xfrm_alloc_replay_state_esn
b xfrm_init_replay
b xfrm_replay_verify_len
b xfrm_replay_advance_esn

19.png

之后,输入命令c,使虚拟机系统继续运行,来到虚拟机窗口中,输入命令./exp 123来运行exp

20.png

回到宿主机窗口,此时gdb会第一次中断到xfrm_replay_state_esn结构体生成的函数

21.png

继续输入命令c,gdb会中断到SA结构体的检测函数处

22.png

继续输入命令n,单步运行到第743行处,输入命令x replay_esnx/128 0xffff88001a6e7e00(此值可能会变化,需要根据实际情况填写)

23.png

24.png

可以看到,这个时候replay_windowbmp_len的大小是一样的,因为这个时候刚创建结构体,并没有绕过验证传入一个大于 bmp_lenreplay_window

另外根据我们前面的分析,在add_sa的过程中会有两次检测,这两次检测都无法绕过,所以在add_sa的整个过程中,xfrm_replay_state_esn结构体并不会出错。

继续输入命令c,第三次中断来到调用xfrm_replay_verify_len函数的位置

25.png

输入命令si,进入这个xfrm_replay_verify_len函数里

26.png

通过对这个函数的调试分析可以知道,没有对replay_windowbmp_len的大小进行检测,导致我们可以传入一个比bmp_len大的replay_window

通过上面的分析我们可以知道,内核的流程为:xfrm_add_sa -> xfrm_new_ae -> xfrm_replay_advance_esn,所以我们这里直接定位到xfrm_replay_advance_esn,因为如果成功更改了replay_window,在这个函数中一定是已经更新过后的xfrm_replay_state_esn

我们继续输入命令c,中断到如下位置

27.png

这里便是溢出点,通过输入命令n,来到第513行处

29.png

然后输入命令p replay_esn来查看xfrm_replay_state_esn结构体地址,并且通过这个地址来查看replay_windowbmp_len的值

29.png

这里可以看到,replay_window的值0xc01已经远远大于bmp_len的值0x24了。

继续执行命令n,来到第536行的位置

30.png

接下来在net/xfrm/xfrm_replay.c:541位置处下断点,执行命令b net/xfrm/xfrm_replay.c:541,然后输入命令c使程序运行到该位置。

这个时候已经清零完成,通过之前查看内存的命令,来看下内存现在是什么情况

31.png

可以看到,已经把xfrm_replay_state_esn结构体之后的固定位给清零了。

至此,cred结构体已经通过喷射到了xfrm_replay_state_esn结构体之后的位置,通过清零操作可以将其中的关键设置为0,从而拥有该cred的线程已经为root,再通过该线程去修改主进程的权限,从而达到提取的目的。

思考

1、造成漏洞的点在哪儿?

2、造成越界访问的那些参数是可控的,如何控制?

3、还有其他的利用思路吗?

来源:freebuf.com 2018-04-10 14:28:32 by: ellenZ

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论