本文來源:長亭技術專欄
作者:趙漢青

0.前言

在2017年PWN2OWN大賽中,長亭安全研究實驗室(Chaitin Security Research Lab)成功演示了Ubuntu 16.10 Desktop的本地提權。本次攻擊主要利用了linux內核IPSEC框架(自linux2.6開始支持)中的一個內存越界漏洞,CVE編號為CVE-2017-7184。

眾所周知,Linux的應用范圍甚廣,我們經常使用的Android、Redhat、CentOS、Ubuntu、Fedora等都使用了Linux操作系統。在PWN2OWN之后,Google、Redhat也針對相應的產品發出了漏洞公告或補丁(見參考資料)。并表示了對長亭安全研究實驗室的致謝,在此也建議還沒有升級服務器內核的小伙伴們及時更新內核到最新版本:P

不同于通常的情況,為了增加比賽難度,本次PWN2OWN大賽使用的Linux版本開啟了諸多漏洞緩解措施,kASLR、SMEP、SMAP都默認開啟,在這種情況下,漏洞變得極難利用,很多漏洞可能僅僅在這些緩解措施面前就會敗下陣來。

另外值得一提的是,本次利用的漏洞隱蔽性極高,在linux內核中存在的時間也非常長。因為觸發這個漏洞不僅需要排布內核數據結構,而且需要使內核處理攻擊者精心構造的數據包,使用傳統的fuzz方式幾乎是不可能發現此漏洞的。

最終,長亭安全研究實驗室成功利用這個漏洞在PWN2OWN的賽場上彈出了PWN2OWN歷史上的第一個xcalc, ZDI的工作人員們看到了之后也表示驚喜不已。

下面一起來看一下整個漏洞的發現和利用過程。

1.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數據包提供完整性檢查、認證和加密。

2.Linux內核的IPSEC實現

在linux內核中的IPSEC實現即是xfrm這個框架,關于xfrm的代碼主要在net/xfrm以及net/ipv4下。

以下是/net/xfrm下的代碼的大概功能

xfrm_state.c     狀態管理
xfrm_policy.c    xfrm策略管理
xfrm_algo.c      算法管理
xfrm_hash.c      哈希計算函數
xfrm_input.c     安全路徑(sec_path)處理, 用于處理進入的ipsec包
xfrm_user.c      netlink接口的SA和SP(安全策略)管理

其中xfrm_user.c中的代碼允許我們向內核發送netlink消息來調用相關handler實現對SA和SP的配置,其中涉及處理函數如下。

xfrm_dispatch[XFRM_NR_MSGTYPES] = {
[XFRM_MSG_NEWSA       - XFRM_MSG_BASE] = { .doit = xfrm_add_sa        },
[XFRM_MSG_DELSA       - XFRM_MSG_BASE] = { .doit = xfrm_del_sa        },
[XFRM_MSG_GETSA       - XFRM_MSG_BASE] = { .doit = xfrm_get_sa,
    .dump = xfrm_dump_sa,
    .done = xfrm_dump_sa_done  },
[XFRM_MSG_NEWPOLICY   - XFRM_MSG_BASE] = { .doit = xfrm_add_policy    },
[XFRM_MSG_DELPOLICY   - XFRM_MSG_BASE] = { .doit = xfrm_get_policy    },
[XFRM_MSG_GETPOLICY   - XFRM_MSG_BASE] = { .doit = xfrm_get_policy,
                                       .dump = xfrm_dump_policy,
                                       .done = xfrm_dump_policy_done },
[XFRM_MSG_ALLOCSPI    - XFRM_MSG_BASE] = { .doit = xfrm_alloc_userspi },
[XFRM_MSG_ACQUIRE     - XFRM_MSG_BASE] = { .doit = xfrm_add_acquire   },
[XFRM_MSG_EXPIRE      - XFRM_MSG_BASE] = { .doit = xfrm_add_sa_expire },
[XFRM_MSG_UPDPOLICY   - XFRM_MSG_BASE] = { .doit = xfrm_add_policy    },
[XFRM_MSG_UPDSA       - XFRM_MSG_BASE] = { .doit = xfrm_add_sa        },
[XFRM_MSG_POLEXPIRE   - XFRM_MSG_BASE] = { .doit = xfrm_add_pol_expire},
[XFRM_MSG_FLUSHSA     - XFRM_MSG_BASE] = { .doit = xfrm_flush_sa      },
[XFRM_MSG_FLUSHPOLICY - XFRM_MSG_BASE] = { .doit = xfrm_flush_policy  },
[XFRM_MSG_NEWAE       - XFRM_MSG_BASE] = { .doit = xfrm_new_ae  },
[XFRM_MSG_GETAE       - XFRM_MSG_BASE] = { .doit = xfrm_get_ae  },
[XFRM_MSG_MIGRATE     - XFRM_MSG_BASE] = { .doit = xfrm_do_migrate    },
[XFRM_MSG_GETSADINFO  - XFRM_MSG_BASE] = { .doit = xfrm_get_sadinfo   },
[XFRM_MSG_NEWSPDINFO  - XFRM_MSG_BASE] = { .doit = xfrm_set_spdinfo,
                                   .nla_pol = xfrma_spd_policy,
                           .nla_max = XFRMA_SPD_MAX },
[XFRM_MSG_GETSPDINFO  - XFRM_MSG_BASE] = { .doit = xfrm_get_spdinfo   },
};

下面簡單介紹一下其中幾個函數的功能:

xfrm_add_sa

創建一個新的SA,并可以指定相關attr,在內核中,是用一個xfrm_state結構來表示一個SA的。

xfrm_del_sa

刪除一個SA,也即刪除一個指定的xfrm_state。

xfrm_new_ae

根據傳入參數,更新指定xfrm_state結構中的內容。

xfrm_get_ae

根據傳入參數,查詢指定xfrm_state結構中的內容(包括attr)。

3.漏洞成因

當我們發送一個XFRM_MSG_NEWSA類型的消息時,即可調用xfrm_add_sa函數來創建一個新的SA,一個新的xfrm_state也會被創建。在內核中,其實SA就是使用xfrm_state這個結構來表示的。

若在netlink消息里面使用XFRMA_REPLAY_ESN_VAL這個attr,一個replay_state_esn結構也會被創建。它的結構如下所示,可以看到它包含了一個bitmap,這個bitmap的長度是由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];
};

內核對這個結構的檢查主要有以下幾種情況:

首先,xfrm_add_sa函數在調用verify_newsa_info檢查從用戶態傳入的數據時,會調用verify_replay來檢查傳入的replay_state_esn結構。

static inline int verify_replay(struct xfrm_usersa_info *p,
                struct nlattr **attrs)
{
    struct nlattr *rt = attrs[XFRMA_REPLAY_ESN_VAL];
    struct xfrm_replay_state_esn *rs;

    if (p->flags & XFRM_STATE_ESN) {
        if (!rt)
            return -EINVAL;

        rs = nla_data(rt);

        if (rs->bmp_len > XFRMA_REPLAY_ESN_MAX / sizeof(rs->bmp[0]) / 8)
            return -EINVAL;

        if (nla_len(rt) < xfrm_replay_state_esn_len(rs) &&
            nla_len(rt) != sizeof(*rs))
            return -EINVAL;
    }

    if (!rt)
        return 0;

    /* As only ESP and AH support ESN feature. */
    if ((p->id.proto != IPPROTO_ESP) && (p->id.proto != IPPROTO_AH))
        return -EINVAL;

    if (p->replay_window != 0)
        return -EINVAL;

    return 0;
}

這個函數要求replay_state_esn結構的bmp_len不可以超過最大限制XFRMA_REPLAY_ESN_MAX。

另外,在這個創建xfrm_state的過程中,如果檢查到成員中有xfrm_replay_state_esn結構,如下函數中的檢查便會被執行。

int xfrm_init_replay(struct xfrm_state *x)
{
    struct xfrm_replay_state_esn *replay_esn = x->replay_esn;

    if (replay_esn) {
        if (replay_esn->replay_window >
            replay_esn->bmp_len * sizeof(__u32) * 8) <-----檢查replay_window
            return -EINVAL;

        if (x->props.flags & XFRM_STATE_ESN) {
            if (replay_esn->replay_window == 0)
                return -EINVAL;
            x->repl = &xfrm_replay_esn;
        } else
            x->repl = &xfrm_replay_bmp;
    } else
        x->repl = &xfrm_replay_legacy;

    return 0;
}

這個函數確保了replay_window不會比bitmap的長度大,否則函數會直接退出。

下面再來看一下xfrm_new_ae這個函數,它首先會解析用戶態傳入的幾個attr,然后根據spi的哈希值以及ip找到指定的xfrm_state,之后xfrm_replay_verify_len中會對傳入的replay_state_esn結構做一個檢查,通過后即會調用xfrm_update_ae_params函數來更新對應的xfrm_state結構。下面我們來看一下xfrm_replay_verify_len這個函數。

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做任何的檢查,只需要提供的bmp_len與xfrm_state中原來的bmp_len一致就可以通過檢查。所以此時我們可以控制replay_window超過bmp_len。之后內核在處理相關IPSEC數據包進行重放檢測相關的操作時,對這個bitmap結構的讀寫操作都可能會越界。

4.漏洞利用

(1).權限不滿足

    /* All operations require privileges, even GET */
    if (!netlink_net_capable(skb, CAP_NET_ADMIN))
        return -EPERM;

xfrm_user_rcv_msg函數中,我們可以看到,對于相關的操作,其實都是需要CAP_NET_ADMIN權限的。那是不是我們就無法觸發這個漏洞了呢?

答案是否定的,在這里我們可以利用好linux的命名空間機制,在ubuntu,Fedora等發行版,User namespace是默認開啟的。非特權用戶可以創建用戶命名空間、網絡命名空間。在命名空間內部,我們就可以有相應的capability來觸發漏洞了。

(2).越界寫

當內核在收到ipsec的數據包時,最終會在xfrm_input解包并進行相關的一些操作。在xfrm_input中,找到對應的xfrm_state之后,根據數據包內容進行重放檢測的時候會執行x->repl->advance(x, seq);,即xfrm_replay_advance_esn這個函數。 這個函數會對bitmap進行如下操作

1.清除[last seq, current seq)的bit 2.設置bmp[current seq] = 1

我們可以指定好spi、seq等參數(內核是根據spi的哈希值以及ip地址來確定SA的),并讓內核來處理我們發出的ESP數據包,多次進行這個操作即可達到對越界任意長度進行寫入任意值。

(3).越界讀

我們的思路是使用越界寫,改大下一個replay_state_esn的結構中的bmp_len。之后我們就可以利用下一個bitmap結構進行越界讀。所以我們需要兩個相鄰的replay_state結構。我們可以使用defragment技巧來達到這個效果。即首先分配足夠多的同樣大小的replay_state結構把堆上原來的坑填滿,之后便可大概率保證連續分配的replay_state結構是相鄰的。

如上所述,使用越界寫的能力將下一個bitmap長度改大,即可使用這個bitmap結構做越界讀了。

圖中所示為被改掉bmp_len的bitmap結構。

(4).繞過kASLR

我們通過xfrm_del_sa接口把沒用的xfrm_state都給刪掉。這樣就可以在堆上留下很多的坑。之后我們可以向內核噴射很多struct file結構體填在這些坑里。

如下,利用上面已經構造出的越界讀能力,我們可以泄露一些內核里的指針來算出內核的加載地址和bitmap的位置。

5.內核任意地址讀寫及代碼執行

因為已經繞過了內核地址隨機化,這時我們可以進行內核ROP構造了。

1.在這個漏洞的利用當中,我們可以在bitmap中偽造一個file_operations結構。

2.之后通過越界寫可以改寫掉我們剛剛在內核中噴射的struct file結構體的file_operations指針,使其指向合適的ROPgadget。

3.調用llseek函數(實際上已經是rop gadget)來執行我們事先已經準備好的ROP鏈。

4.通過多次改寫file_operations結構中的llseek函數指針來實現多次執行ROPgadget實現提權。

如上所述,因為我們的數據都是偽造在內核里面,所以這種利用方式其實是可以同時繞過SMEP和SMAP的。

6.權限提升

下面是長亭安全研究實驗室在pwn2own2017上彈出xcalc的瞬間。

5.后記

非常感謝slipper老師的指導和講解 :P

感謝長亭安全研究實驗室的所有小伙伴:P

6.參考資料


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/293/