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

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