作者:Zhuo Liang of Qihoo 360 Nirvan Team
博客:https://blogs.projectmoon.pw/2018/11/30/A-Late-Kernel-Bug-Type-Confusion-in-NECP/

1 介紹

Apple 對于代碼開放的態度一直為廣大的安全研究員們所詬病,代碼更新總是扭扭捏捏滯后很多于系統發版時間。比如當前 macOS 的最新版本為 Mojave 10.14.1,而最新的代碼還停留在 10.13.6。本文將要提到的是筆者在審計 10.13.6 的代碼時發現的一個內核漏洞,而在驗證的時候發現在最新版本已經無法觸發,經過分析發現這不是一個 0-day,而是一個已經在 macOS 10.14 和 iOS 12.0 上已經修補了的問題。由于這個漏洞的觸發沒有權限限制和強制訪問控制,漏洞本身也有一定代表性,特記錄分享一下。

2 背景知識

2.1 一切皆文件

*nix 世界眾多的優秀品質(所謂設計哲學)最廣為人知的可能就是“一切皆文件”了。在這種設計理念下,在 *nix 下特別是在 Linux 里,大部分內核對象,比如普通文件、socket、共享內存和信號量等,都是由內核給用戶態暴露一個文件描述符,并提供對于文件描述符的統一操作,常見的比如 read、write、close 和 select 等。 很顯然,內核對于這些文件描述符本身所代表的對象類型都是有標記的,通過類型將這些同樣的系統調用分配到各自的回調函數中,這就是 C 語言里的多態。 Linux 里另外一個重度依賴多態的模塊就是 VFS,與本文要談的漏洞無關,所以此處不談。

macOS 和 iOS 內核(以下統一稱 XNU)的部分代碼也遵循“一切皆文件”這條原則。在 XNU 里,內核為每個進程維護一張文件描述符與文件表項的映射表struct filedesc * p_fd,每一個文件表項即為每一個與文件有關操作的第一步需要獲取的對象,其定義如下:

// bsd/sys/file_internal.h
/*
 * Kernel descriptor table.
 * One entry for each open kernel vnode and socket.
 */

struct fileproc {
    unsigned int f_flags;
    int32_t f_iocount;
    struct fileglob * f_fglob;
    void * f_wset;
};

在這個結構體的成員中,最重要的一項即為 struct fileglob * f_fglob,其定義如下:

// bsd/sys/file_internal.h

struct fileglob {
    LIST_ENTRY(fileglob) f_msglist; /* list of active files */
    int32_t fg_flag;                /* see fcntl.h */
    int32_t fg_count;               /* reference count */
    int32_t fg_msgcount;            /* references from message queue */
    int32_t fg_lflags;              /* file global flags */
    kauth_cred_t fg_cred;           /* credentials associated with descriptor */
    const struct fileops {
        file_type_t fo_type; /* descriptor type */
        int (*fo_read)(struct fileproc * fp, struct uio * uio, int flags, 
                       vfs_context_t ctx);
        int (*fo_write)(struct fileproc * fp, struct uio * uio, int flags, 
                        vfs_context_t ctx);
#define FOF_OFFSET 0x00000001 /* offset supplied to vn_write */
#define FOF_PCRED 0x00000002  /* cred from proc, not current thread */
        int (*fo_ioctl)(struct fileproc * fp, u_long com, caddr_t data, 
                        vfs_context_t ctx);
        int (*fo_select)(struct fileproc * fp, int which, void * wql, 
                         vfs_context_t ctx);
        int (*fo_close)(struct fileglob * fg, vfs_context_t ctx);
        int (*fo_kqfilter)(struct fileproc * fp, struct knote * kn, 
                           struct kevent_internal_s * kev, 
                           vfs_context_t ctx);
        int (*fo_drain)(struct fileproc * fp, vfs_context_t ctx);
    } * fg_ops;
    off_t fg_offset;
    void * fg_data;    /* vnode or socket or SHM or semaphore */
    void * fg_vn_data; /* Per fd vnode data, used for directories */
    lck_mtx_t fg_lock;
#if CONFIG_MACF
    struct label * fg_label; /* JMM - use the one in the cred? */
#endif
};

struct fileglob 中,需要關注的幾個成員如下:

a) fg_flag 對于文件操作的權限,比如 FWRITE和FREAD,讀寫操作對應的不同類型的對象有不同的解釋。

b) fileops 文件操作定義,其中需要關注 fo_type 也就是對象類型,在 XNU 中目前存在的幾種類型如下:

// bsd/sys/file_internal.h
/* file types */
typedef enum {
    DTYPE_VNODE = 1, /* file */
    DTYPE_SOCKET,    /* communications endpoint */
    DTYPE_PSXSHM,    /* POSIX Shared memory */
    DTYPE_PSXSEM,    /* POSIX Semaphores */
    DTYPE_KQUEUE,    /* kqueue */
    DTYPE_PIPE,      /* pipe */
    DTYPE_FSEVENTS,  /* fsevents */
    DTYPE_ATALK,     /* (obsolete) */
    DTYPE_NETPOLICY, /* networking policy */
} file_type_t;

c) fg_data 代表真正的對象以及上下文信息,fileops 里的 fo_* 回調函數最終都是操作對應的 fg_data 對象。

下面以 socket 的創建為例說明上述大致流程。

// bsd/kern/uipc_syscall.c 

static int
socket_common(struct proc * p, int domain, int type, int protocol, pid_t epid, 
    int32_t * retval, int delegate)
{
    ...
    error = falloc(p, &fp, &fd, vfs_context_current());
    if (error) {
        return (error);
    }
    fp->f_flag = FREAD | 
        FWRITE; // [a],這里的 f_flag 實際上是指向 fileops 的 fg_flag,下同。
    fp->f_ops  = &socketops; // [b]

    if (delegate)
        error = socreate_delegate(domain, &so, type, protocol, epid);
    else
        error = socreate(domain, &so, type, protocol); // [c]

    if (error) {
        fp_free(p, fd, fp);
    } else {
        fp->f_data = (caddr_t)so; // [d]
        ...
    }
    return (error);
}

在用戶態調用 socket(AF_INET, SOCK_STREAM, 0) 后,內核代碼將進入如上流程,首先分配文件表項和 fileglob 對象,然后在 [a] 處將 fg_flag 設置為可讀可寫,表示可以對這個 socket 進行發送和接收數據相關操作。在 [b] 處,將 fileops 設置為 socketops,對于該變量的定義如下:

// bsd/kern/sys_socket.c

const struct fileops socketops = {
    .fo_type     = DTYPE_SOCKET,
    .fo_read     = soo_read,
    .fo_write    = soo_write,
    .fo_ioctl    = soo_ioctl,
    .fo_select   = soo_select,
    .fo_close    = soo_close,
    .fo_kqfilter = soo_kqfilter,
    .fo_drain    = soo_drain,
};

該變量里設置的成員回調函數即為用戶態的系統調用將會真正觸發的函數。在 [c] 處, socreate 函數會根據 domain、type 和 protocol 創建 struct socket 對象, 并在 [d] 處賦給 fg_data,即為真正的 backend object。

2.2 NECP

NECP, Network Extension Control Policy,顧名思義是一種網絡控制策略,下面是內核對其作出的解釋:

// bsd/net/necp.c

/*
 * NECP - Network Extension Control Policy database
 * ------------------------------------------------
 * The goal of this module is to allow clients connecting via a
 * kernel control socket to create high-level policy sessions, which
 * are ingested into low-level kernel policies that control and tag
 * traffic at the application, socket, and IP layers.
 */

簡單的說就是用戶態程序可以通過 NECP 來創建一些策略并將其注入到內核網絡流量處理的模塊中,對于應用層、socket 層和 IP 層的流量進行控制以及標記。本文不對與本漏洞無關的業務邏輯進行闡述,感興趣的讀者可以自行閱讀內核代碼。

在談 NECP 的同時可以簡單的介紹一下 Kernrl Control,通過官網對 Kernel Control 的介紹可以知道,Kernel Control 的主要作用就是用來使用戶態程序有能力配置和控制內核以及內核擴展,這就是 NECP 最開始提供給用戶態訪問的最原始的形式。具體而言,內核首先通過 Kernel Control 提供的 KPI 注冊一種 socket 類型,使用戶態可以通過諸如 socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL) 訪問到。注冊部分的代碼如下:

// bsd/net/necp.c

static errno_t
necp_register_control(void)
{
    struct kern_ctl_reg kern_ctl;
    errno_t result = 0;

    // Create a tag to allocate memory
    necp_malloc_tag = OSMalloc_Tagalloc(NECP_CONTROL_NAME, OSMT_DEFAULT);

    // Find a unique value for our interface family
    result = mbuf_tag_id_find(NECP_CONTROL_NAME, &necp_family);
    if (result != 0) {
        NECPLOG(LOG_ERR, "mbuf_tag_id_find_internal failed: %d", result);
        return (result);
    }

    bzero(&kern_ctl, sizeof(kern_ctl));
    strlcpy(kern_ctl.ctl_name, NECP_CONTROL_NAME, sizeof(kern_ctl.ctl_name));
    kern_ctl.ctl_name[sizeof(kern_ctl.ctl_name) - 1] = 0;
    kern_ctl.ctl_flags                               = CTL_FLAG_PRIVILEGED; // Require root
    kern_ctl.ctl_sendsize                            = 64 * 1024;
    kern_ctl.ctl_recvsize                            = 64 * 1024;
    kern_ctl.ctl_connect                             = necp_ctl_connect;
    kern_ctl.ctl_disconnect                          = necp_ctl_disconnect;
    kern_ctl.ctl_send                                = necp_ctl_send;
    kern_ctl.ctl_rcvd                                = necp_ctl_rcvd;
    kern_ctl.ctl_setopt                              = necp_ctl_setopt;
    kern_ctl.ctl_getopt                              = necp_ctl_getopt;

    result = ctl_register(&kern_ctl, &necp_kctlref);
    if (result != 0) {
        NECPLOG(LOG_ERR, "ctl_register failed: %d", result);
        return (result);
    }

    return (0);
}

內核使用 ctl_register 函數將一個 kern_ctl 的對象注冊到一個全局的數據集中。同樣這里也有幾點需要關注:

a) NECP_CONTROL_NAME 內核定義的宏,定義為字符串 com.apple.net.necp_control

b) kern_ctl.ctl_flags 標記為需要 root 才能訪問,后面會有提到。

c) necp_ctl_* 系列函數,作為回調函數會被對 NECP Kernel Control 的套接字的操作觸發。

用戶態在創建相關套接字后,通過 connect 系統調用可以創建與內核 NECP 模塊交互的會話,并通過 write 的方式配置網絡策略,通過 read 的方式讀取內核通知。

2.3 代碼審計的一點思考

至此,對于本漏洞的基本知識已介紹完畢,本小結作為筆者審計代碼的一點小感受,與本文主題無關,不感興趣的讀者可以直接閱讀第3部分。

本文提到的代碼廣義上都是多態,而在 C 語言里多態的實現基本要依賴回調函數。對于更加復雜的諸如此類的回調函數系統實際上是很容易出問題的,閱讀理解困難、調試不方便,這點筆者在曾經作為開發者參與開發維護一個回調滿天飛的軟件時深有體會,很顯然會出現的問題有如下兩點:

a) 資源管理 底層語言程序員們肯定會聽過“誰開發,誰保護;誰污染,誰治理”的資源管理原則,但是事情總是這樣嗎?在實際的案例中,再優美的設計也有可能被歷史包袱和奇葩的需求所打敗,最后落得一地雞毛。當然,遵循一種統一的資源管理原則肯定是值得提倡的,問題是軟件開發初期肯定會有考慮欠周的地方,加上開發中后期人員的變動,后來參與的成員可能會因為不能熟悉該軟件中的統一規范而導致寫了危險的代碼。

b) 處理邏輯 這里提到的處理邏輯是指回調函數設計之初所期望的開發者對于這些回調函數的參數、返回值的處理以及實際邏輯所能訪問的邊界有足夠的意見一致性,這些問題較多出現在擴展性質的程序中。而對這些約定的東西處理不當又極易導致資源管理的問題。

篇幅有限,這里不進行展開,觀點僅作為一點不成熟的小建議。

3 類型混淆

目前為止所提到的內容都是沒問題的,問題出在 Apple 在2017年7月份一次更新(沒有仔細看,感興趣的讀者可以自行查證)中添加的關于 necp 模塊的幾個系統調用里面。這些系統調用作為 2.2 中提到的用 socket 的方式操作 NECP 的一種替代品。具體來說就是 necp_session_*necp_client_* 這兩類函數。這些函數是怎么實現相關功能的這里不討論,只談與漏洞相關的地方。

內核提供 necp_opennecp_session_open 這兩個系統調用,并且兩個系統調用都返回文件描述符,根據之前提到的,文件描述符所對應的內核真正的對象的類型應該是不同的。通過查看代碼發現,確實不同。兩個函數的代碼如下:

// bsd/net/necp.c 
int necp_session_open(struct proc * p, struct necp_session_open_args * uap, int * retval)
{
    int error                     = 0;
    struct necp_session * session = NULL;
    struct fileproc * fp          = NULL;
    int fd                        = -1;

    uid_t uid = kauth_cred_getuid(proc_ucred(p));
    if (uid != 0 && priv_check_cred(kauth_cred_get(), 
        PRIV_NET_PRIVILEGED_NECP_POLICIES, 0) != 0) { // [a]
        NECPLOG0(LOG_ERR, "Process does not hold necessary entitlement to open NECP session");
        error = EACCES;
        goto done;
    }

    error = falloc(p, &fp, &fd, vfs_context_current());
    if (error != 0) {
        goto done;
    }

    session = necp_create_session(); // [b]
    if (session == NULL) {
        error = ENOMEM;
        goto done;
    }

    fp->f_fglob->fg_flag = 0;
    fp->f_fglob->fg_ops  = &necp_session_fd_ops; // [c]
    fp->f_fglob->fg_data = session; // [d]

    proc_fdlock(p);
    FDFLAGS_SET(p, fd, (UF_EXCLOSE | UF_FORKCLOSE));
    procfdtbl_releasefd(p, fd, NULL);
    fp_drop(p, fd, fp, 1);
    proc_fdunlock(p);

    *retval = fd;
done:
    if (error != 0) {
        if (fp != NULL) {
            fp_free(p, fd, fp);
            fp = NULL;
        }
    }

    return (error);
}
// bsd/net/necp_client.c
int necp_open(struct proc * p, struct necp_open_args * uap, int * retval)
{
    int error                     = 0;
    struct necp_fd_data * fd_data = NULL;
    struct fileproc * fp          = NULL;
    int fd                        = -1;
    ...
    error = falloc(p, &fp, &fd, vfs_context_current());
    if (error != 0) {
        goto done;
    }

    if ((fd_data = zalloc(necp_client_fd_zone)) == NULL) { // [f]
        error = ENOMEM;
        goto done;
    }

    memset(fd_data, 0, sizeof(*fd_data));

    fd_data->necp_fd_type = necp_fd_type_client;
    fd_data->flags        = uap->flags;
    RB_INIT(&fd_data->clients);
    TAILQ_INIT(&fd_data->update_list);
    lck_mtx_init(&fd_data->fd_lock, necp_fd_mtx_grp, necp_fd_mtx_attr);
    klist_init(&fd_data->si.si_note);
    fd_data->proc_pid = proc_pid(p);

    fp->f_fglob->fg_flag = FREAD;
    fp->f_fglob->fg_ops  = &necp_fd_ops; // [g]
    fp->f_fglob->fg_data = fd_data; // [h]

    proc_fdlock(p);

    *fdflags(p, fd) |= (UF_EXCLOSE | UF_FORKCLOSE);
    procfdtbl_releasefd(p, fd, NULL);
    fp_drop(p, fd, fp, 1);
    ...
    return (error);
}

注意代碼中標記字母的地方。在 [d] 和 [h] 處對應賦值的兩個對象的類型分別為 struct necp_sessionstruct necp_fd_data 類型。再注意 [c] 和 [g] 處, 給 fileops 賦值的值分別為:

// bsd/net/necp.c
static const struct fileops necp_session_fd_ops = {
    .fo_type     = DTYPE_NETPOLICY,
    .fo_read     = noop_read,
    .fo_write    = noop_write,
    .fo_ioctl    = noop_ioctl,
    .fo_select   = noop_select,
    .fo_close    = necp_session_op_close,
    .fo_kqfilter = noop_kqfilter,
    .fo_drain    = NULL,
};
// bsd/net/necp_client.c
static const struct fileops necp_fd_ops = {
    .fo_type     = DTYPE_NETPOLICY,
    .fo_read     = noop_read,
    .fo_write    = noop_write,
    .fo_ioctl    = noop_ioctl,
    .fo_select   = necpop_select,
    .fo_close    = necpop_close,
    .fo_kqfilter = necpop_kqfilter,
    .fo_drain    = NULL,
};

fo_type 都是 DTYPE_NETPOLICY,類型居然一樣!再看從文件描述符到具體對象轉換的函數:

// bsd/net/necp.c

static int
necp_session_find_from_fd(int fd, struct necp_session ** session)
{
    proc_t p             = current_proc();
    struct fileproc * fp = NULL;
    int error            = 0;

    proc_fdlock_spin(p);
    if ((error = fp_lookup(p, fd, &fp, 1)) != 0) {
        goto done;
    }
    if (fp->f_fglob->fg_ops->fo_type != DTYPE_NETPOLICY) { // [a]
        fp_drop(p, fd, fp, 1);
        error = ENODEV;
        goto done;
    }
    *session = (struct necp_session *)fp->f_fglob->fg_data; // [b]

done:
    proc_fdunlock(p);
    return (error);
}

另外一個函數對應也一樣,可自行查閱。在這里,[a] 處先判斷該類型是否為 DTYPE_NETPOLICY,[b] 處,直接就轉換成 struct necp_session 對象。單個的看是沒有問題的,但如果傳進來的是一個代表了 struct necp_fd_data 的文件描述符呢,此時在 [a] 處, CHECK![b] 處,TYPE CONFUSION。下載 PoC 可驗證這一猜想。

4 權限與沙箱

在 PoC 中,使用的是 necp_open 創建 necp_fd_data 對象, 然后以把其當做 necp_session 對象。反過來其實也行,但是由于 necp_session_open 函數因為 PRIV_NET_PRIVILEGED_NECP_POLICIES 的檢查是普通用戶無法成功調用的,所以最好是以 PoC 中的方式觸發。同時,在這幾個函數中,是沒有沙盒限制的,意味著這個類型混淆漏洞可以用來繞過任意沙盒。

5 漏洞修復

查證了內核最新代碼(沒有源代碼,只有二進制),修復的方式是加了一個子類型的檢查。

necp_session_find_from_fd 函數:

necp_find_fd_data 函數:

在 fg_data 的第一個字節存儲的就是這個類型信息,這個在漏洞修復之前就存在,只是沒有利用起來。

6 One More

在驗證漏洞失敗后的失望之余,在蘋果公告上找到了可能的漏洞致謝信息。

隨后又去 ZDI 上證實了這個信息,編號為 CVE-2018-4425。

行文倉促,難免會有不嚴謹的地方,歡迎指出


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