CVE-2016-1503 漏洞分析

CVE-2016-1503 漏洞分析

一、漏洞成因

首先来看一下官方的描述:
“A vulnerability in the Dynamic Host Configuration Protocol service could enable an attacker to cause memory corruption, which could lead to remote code execution.”
攻击者可能会通过动态主机配置协议服务中的漏洞破坏内存,从而执行远程代码。

Diff:
这里写图片描述

从官方打下的Patch来看,主要的区别是一个连续的if语句改成了互斥的if else语句,另外一个关键就是之前可以返回除了0或者-1之外的其他值,而patch之后只能返回0或者-1.
通过以上的分析,可以确定了具体的漏洞原因就是dhcpcd在解析options的时候长度dl的校验出现了问题,导致了后续的远程执行漏洞。

二、函数追踪

定位一下该patch的地方可以得知该Patch的位置在文件dhcp.c中的valid_length(uint8_t option, int dl, int *type)函数里,查找一下valid_length 的引用有一处,是在文件dhcp.c中的get_option(const struct dhcp_message *dhcp, uint8_t opt, int *len, int *type)函数,如下:
get_option(const struct dhcp_message *dhcp, uint8_t opt, int *len, int *type)
{
    const uint8_t *p = dhcp->options;
    const uint8_t *e = p + sizeof(dhcp->options);
    uint8_t l, ol = 0;
    uint8_t o = 0;
    uint8_t overl = 0;
    uint8_t *bp = NULL;
    const uint8_t *op = NULL;
    int bl = 0;

    …...


    if (valid_length(opt, bl, type) == -1) {
        errno = EINVAL;
        return NULL;
    }
    if (len)
        *len = bl;
    if (bp) {
        memcpy(bp, op, ol);
        return (const uint8_t *)opt_buffer;
    }
    if (op)
        return op;
    errno = ENOENT;
    return NULL;
}

从代码可以看到,只要返回值不为-1,即可通过options的长度校验,也就是说,只要dl % sz 不等于-1即可。sz是根据option的类型来确定,根据不同的类型来取不同的值,如果是UINT32,sz则为4;如果是UINT16,sz则为2,如果是UINIT8,sz则为1。而在valid_length函数中的dl其实就是get_option函数中的bl,这个值是服务器发出来的数据包中单个option的长度。在校验完bl的长度后,将会把这个bl的值赋给get_option函数中的第三个参数*len。
接下来我们继续追踪这个指针len,查找get_option函数的引用,在dhcp.c文件中一共有7处,排除之后找到configure_env(char **env, const char *prefix, const struct dhcp_message *dhcp, const struct if_options *ifo)函数如下:

configure_env(char **env, const char *prefix, const struct dhcp_message *dhcp,
    const struct if_options *ifo)
{
    unsigned int i;
    const uint8_t *p;
    int pl;
    struct in_addr addr;
    struct in_addr net;
    struct in_addr brd;
    char *val, *v;
    const struct dhcp_opt *opt;
    ssize_t len, e = 0;
    char **ep;
    char cidr[4];
    uint8_t overl = 0;

    …...

    //循环读取options
    for (opt = dhcp_opts; opt->option; opt++) {
        if (!opt->var)
            continue;
        if (has_option_mask(ifo->nomask, opt->option))
            continue;
        val = NULL;
        p = get_option(dhcp, opt->option, &pl, NULL);
        if (!p)
            continue;
        /* We only want the FQDN name */
        if (opt->option == DHO_FQDN) {
            p += 3;
            pl -= 3;
        }
        len = print_option(NULL, 0, opt->type, pl, p);
        if (len < 0)
            return -1;
        e = strlen(prefix) + strlen(opt->var) + len + 4;
        v = val = *ep++ = xmalloc(e);
        v += snprintf(val, e, "%s_%s=", prefix, opt->var);
        if (len != 0)
            print_option(v, len, opt->type, pl, p);
    }

    return ep - env;
}

从以上代码可以看到,之前的校验出现了问题的bl,其实赋值给了pl,而pl在configure_env函数中为一个局部变量,在调用get_option 函数给pl赋值后,后来又2次调用print_option函数,并传入了pl参数。接下来就是来跟踪一下这个pl的值,查看print_option(char *s, ssize_t len, int type, int dl, const uint8_t *data)函数如下:

print_option(char *s, ssize_t len, int type, int dl, const uint8_t *data)
{
    const uint8_t *e, *t;
    uint16_t u16;
    int16_t s16;
    uint32_t u32;
    int32_t s32;
    struct in_addr addr;
    ssize_t bytes = 0;
    ssize_t l;
    char *tmp;

    …...


    if (!s) {
        if (type & UINT8)
            l = 3;
        else if (type & UINT16) {
            l = 5;
            dl /= 2;
        } else if (type & SINT16) {
            l = 6;
            dl /= 2;
        } else if (type & UINT32) {
            l = 10;
            dl /= 4;
        } else if (type & SINT32) {
            l = 11;
            dl /= 4;
        } else if (type & IPV4) {
            l = 16;
            dl /= 4;
        } else {
            errno = EINVAL;
            return -1;
        }
        return (l + 1) * dl; //第一次调用 print_option在这里返回
    }
//第二次调用 print_option函数才可以到这里
    t = data;
    e = data + dl;
    while (data < e) {
        if (data != t) {
            *s++ = ' ';
            bytes++;
            len--;
        }
        if (type & UINT8) {
            l = snprintf(s, len, "%d", *data);
            data++;
        } else if (type & UINT16) {
            memcpy(&u16, data, sizeof(u16));
            u16 = ntohs(u16);
            l = snprintf(s, len, "%d", u16);
            data += sizeof(u16);
        } else if (type & SINT16) {
            memcpy(&s16, data, sizeof(s16));
            s16 = ntohs(s16);
            l = snprintf(s, len, "%d", s16);
            data += sizeof(s16);
        } else if (type & UINT32) {
            memcpy(&u32, data, sizeof(u32));
            u32 = ntohl(u32);
            l = snprintf(s, len, "%d", u32);
            data += sizeof(u32);
        } else if (type & SINT32) {
            memcpy(&s32, data, sizeof(s32));
            s32 = ntohl(s32);
            l = snprintf(s, len, "%d", s32);
            data += sizeof(s32);
        } else if (type & IPV4) {
            memcpy(&addr.s_addr, data, sizeof(addr.s_addr));
            l = snprintf(s, len, "%s", inet_ntoa(addr));
            data += sizeof(addr.s_addr);
        } else
            l = 0;
        if (len <= l) {
            bytes += len;
            break;
        }
        len -= l;
        bytes += l;
        s += l;
    }
    return bytes;
}

从上述的代码可以看到,第一次的调用将会在”return (l + 1) * dl ”这里返回,并且会返回一个len作为第二次调用的第二个参数再次传入print_option函数。第二次调用print_option函数的时候,dl的值影响了while循环,这个循环在第一次调用print_option函数是无法进入的。
分析到这里,可以推断之前在valid_length函数中未合理校验的dl值传入到了此while循环中,正是由于之前的校验不够完整使得在此while循环中dl的值不合法,最终导致了该漏洞的形成。
整个在客户端的过程如图所示:

这里写图片描述

三、测试环境的搭建

由于该漏洞的特殊性,需要开启一个热点,搭建一个Dhcpd服务器以及一个带log的dhcpcd的客户端来验证此漏洞,这里选用的系统为Ubuntu14.04。

3.1 Hostapd热点的搭建

安装Hostapd:
sudo apt-get install hostapd

安装了软件以后,在/etc/hostapd文件夹中建立一个hostapd.conf的文件,在里面写入接入点的信息。
配置Hostapd:
sudo nano /etc/hostapd/hostapd.conf

hostapd.conf文件改成如下:


interface=wlan0//改成对应的网卡
driver=nl80211//这个driver一定得是这个
ssid=baobaonihao
hw_mode=g
channel=10
macaddr_acl=0
auth_algs=3
wpa=2
wpa_passphrase=qqqq1111
wpa_key_mgmt=WPA-PSK
wpa_pairwise=TKIP CCMP
rsn_pairwise=TKIP CCMP


注意要自己设置其中的无线热点名称ssid和认证密码wpa_passphrase。

上述配置完成以后,在终端执行sudo hostapd /etc/hostapd/hostapd.conf -B(-B是需要在后台运行的时候添加),到这里,就表明Hostapd的安装和配置结束了,现在已经可以在手机终端上可以搜索到这个baobaonihao 的热点了,但是无法连接到这个热点,此时应该出现的情况是:正在获取IP地址,但是一直获取不到。这是由于dhcpd服务器没搭建好的原因,接下来就是dhcpd服务器的搭建。

3.2 Dhcpd服务器的编译与搭建
这个dhcpd服务器不能直接用apt-get来安装,可以在官网https://www.isc.org/ 里面找到源码并且下载,进行编译安装。
下载之后为一个dhcp-4.3.4.tar.gz包。
安装官方原始版本如下:
tar zxvf dhcp-4.3.4.tar.gz

cd dhcp-4.3.4

chmod 777 configure

sudo ./configure

sudo make

sudo make install

安装debug版本如下,debug版本能输出log,在之后的构建package中方便查看调试以及log信息:

tar zxvf dhcp-4.3.4.tar.gz

cd dhcp-4.3.4

chmod 777 configure

sudo ./configure –enable-debug

sudo make

sudo make install

这样就可以编译安装一个调试版本了。

同样的,安装了软件以后,在/etc/dhcp文件夹中建立一个dhcpd.conf的配置文件,在里面写入dhcpd的配置信息。

dhcpd.conf文件改成如下:

************************
ddns-update-style none;
log-facility local7;

subnet 172.20.94.0 netmask 255.255.255.0 {
        option routers                  172.20.94.1;
        option subnet-mask              255.255.255.0;
        option broadcast-address        172.20.94.255;
    option domain-name "internal.baidu.com";
        option domain-name-servers      172.22.1.253,172.22.1.254;
        option ntp-servers              172.20.94.1;
        option netbios-name-servers     172.20.94.1;
        option netbios-node-type 2;
        default-lease-time 86400;
        max-lease-time 86400;
    range 172.20.94.0 172.20.94.100;
}

在上述配置完成以后我们需要手动给wlan0配置IP地址并且启动它,在终端输入:

sudo ifconfig wlan0 172.20.94.1

sudo dhcpd /etc/dhcp/dhcpd.conf

就可以启动dhcpd了,此时可以获取到ip地址了,已经可以成功发包了,如果要上网,则还要输入以下命令:

开启内核IP转发

bash -c “echo 1 > /proc/sys/net/ipv4/ip_forward”
开启防火墙NAT转发(如果本机使用eth0上网,则把ppp0改为eth0)

iptables -t nat -A POSTROUTING -o ppp0 -j MASQUERADE

这样Dhcpd服务器的编译与搭建到此就完成了,以及可以修改源码取任意构造数据包了。

3.3 Dhcpcd客户端增加log编译。
采用4.4.4版本的源码来编译
log函数如下:

void MYLOG(const char* ms, ...);
void MYLOG(const char* ms, ...)  
{  
    char wzLog[1024] = {0};  
    char buffer[1024] = {0};  
    va_list args;  
    va_start(args, ms);  
    vsprintf( wzLog ,ms,args);  
    va_end(args);  

    time_t now;  
    time(&now);  
    struct tm *local;  
    local = localtime(&now);  
    sprintf(buffer,"%04d-%02d-%02d %02d:%02d:%02d %s\n", local->tm_year+1900, local->tm_mon,  
                local->tm_mday, local->tm_hour, local->tm_min, local->tm_sec,  
                wzLog);  
    FILE* file = fopen("/data/local/tmp/dhcplog","a+");  
    fwrite(buffer,1,strlen(buffer),file);  
    fclose(file);  

  // syslog(LOG_INFO,wzLog);  
    return ;  
}

编译的话直接make即可编译

四、dhcp发包交互过程

要想触发该漏洞,当然得了解dhcp服务器与客户端之间的交互。那它们之间是怎么样交互的呢?    DHCP协议采用UDP作为传输协议,主机发送请求消息到DHCP服务器的67号端口,DHCP服务器回应应答消息给主机的68号端口。

DHCP Client以广播的方式发出DHCP Discover报文。

所有的DHCP Server都能够接收到DHCP Client发送的DHCP Discover报文,所有的DHCP Server都会给出响应,向DHCP Client发送一个DHCP Offer报文。
DHCP Offer报文中“Your(Client) IP Address”字段就是DHCP Server能够提供给DHCP Client使用的IP地址,且DHCP Server会将自己的IP地址放在“option”字段中以便DHCP Client区分不同的DHCP Server。DHCP Server在发出此报文后会存在一个已分配IP地址的纪录。

DHCP Client只能处理其中的一个DHCP Offer报文,一般的原则是DHCP Client处理最先收到的DHCP Offer报文。
DHCP Client会发出一个广播的DHCP Request报文,在选项字段中会加入选中的DHCP Server的IP地址和需要的IP地址。

DHCP Server收到DHCP Request报文后,判断选项字段中的IP地址是否与自己的地址相同。如果不相同,DHCP Server不做任何处理只清除相应IP地址分配记录;如果相同,DHCP Server就会向DHCP Client响应一个DHCP ACK报文,并在选项字段中增加IP地址的使用租期信息。

DHCP Client接收到DHCP ACK报文后,检查DHCP Server分配的IP地址是否能够使用。如果可以使用,则DHCP Client成功获得IP地址并根据IP地址使用租期自动启动续延过程;如果DHCP Client发现分配的IP地址已经被使用,则DHCP Client向DHCPServer发出DHCP Decline报文,通知DHCP Server禁用这个IP地址,然后DHCP Client开始新的地址申请过程。

DHCP Client在成功获取IP地址后,随时可以通过发送DHCP Release报文释放自己的IP地址,DHCP Server收到DHCP Release报文后,会回收相应的IP地址并重新分配。

可以得知DHCP  Server会向DHCP Client响应一个DHCP ACK报文,而这个ACK报文能触发到漏洞代码片段,而需要知道的就是如何构建自己的DHCP ACK报文。查看Server端的代码,找到处理客户端报文的函数为void dhcp (struct packet *packet) ,代码片段如下:

这里写图片描述

对应的ACK 报文当然是DHCPREQUEST,继续查看dhcprequest函数,在结尾处找到:
这里写图片描述

可以发现,是ack_lease (packet, lease, DHCPACK, 0, msgbuf, ms_nulltp,(struct host_decl *)0);这个函数,继续追踪,还是在尾部:

这里写图片描述

进入dhcp_reply(lease),继续追踪:

这里写图片描述

进入cons_options函数,还是在函数的尾部发现:
memcpy(outpacket->options, buffer, index);

length = DHCP_FIXED_NON_UDP + index;

return length;


这里的buffer就是储存数据包中options字段的地方了,在这个memcpy之前改写这个buffer,再对应的把index改成数据包中options字段实际的长度就可以了。

五、发包验证

可以先发个包熟悉一下格式是什么样的(Hex格式):

这里写图片描述

表示的是一个完整的DHCP ACK 数据包,这个数据包的格式如下:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| op (1) | htype (1) | hlen (1) | hops (1) |
+—————+—————+—————+—————+
| xid (4) |
+——————————-+——————————-+
| secs (2) | flags (2) |
+——————————-+——————————-+
| ciaddr (4) |
+—————————————————————+
| yiaddr (4) |
+—————————————————————+
| siaddr (4) |
+—————————————————————+
| giaddr (4) |
+—————————————————————+
| |
| chaddr (16) |
| |
| |
+—————————————————————+
| |
| sname (64) |
+—————————————————————+
| |
| file (128) |
+—————————————————————+
| |
| options (variable) |
+—————————————————————+

具体参数含义请参考rfc2131[1]文档,这里关注的是最后一个字段options,这个字段就是导致漏洞触发的关键点。

为了顺利的利用长度校验不合理的这个缺陷,可以把opt->type设置成UINT16和ARRAY,查找一下这种type的option在客户端的源码对应的option号码,如下:
这里写图片描述

option的号码为25,为了清楚25这个option的格式,查看rfc2132[2]文档找到描述如下:

4.7. Path MTU Plateau Table Option

This option specifies a table of MTU sizes to use when performing
Path MTU Discovery as defined in RFC 1191. The table is formatted as
a list of 16-bit unsigned integers, ordered from smallest to largest.
The minimum MTU value cannot be smaller than 68.

The code for this option is 25. Its minimum length is 2, and the
length MUST be a multiple of 2.

Code   Len     Size 1      Size 2

+—–+—–+—–+—–+—–+—–+—
| 25 | n | s1 | s2 | s1 | s2 | …
+—–+—–+—–+—–+—–+—–+—
从上述的描述可以得知最小长度n规定为2,且长度为2的整数倍。但是可以构建一个option为25长度为3的数据包,既可以在长度校验函数中返回1从而通过校验,又能进入print_option函数中的while循环,当进入while循环后如下:

t = data;
    e = data + dl;
    while (data < e) {

    …...

    else if (type & UINT16) {
            memcpy(&u16, data, sizeof(u16));
            u16 = ntohs(u16);
            l = snprintf(s, len, "%d", u16);
            data += sizeof(u16);
        } 

    …...


    }

dl为3,而data每次循环后只加了2,导致了此while循环多循环了一次,memcpy多copy了一次2字节,造成了越界。至此整个漏洞就分析完毕。

                                    By Ericky

                                    2016.06.06

参考链接:

[1] https://tools.ietf.org/html/rfc2131

[2] https://tools.ietf.org/html/rfc2132

[3] https://android.googlesource.com/platform/external/dhcpcd/+/1390ace71179f04a09c300ee8d0300aa69d9db09

[4] http://source.android.com/security/bulletin/2016-04-02.html

[5] http://www.isc.org/downloads/

[6] https://help.ubuntu.com/community/isc-dhcp-server

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章