作者: 啟明星辰ADLab

1. About“Phoenix Talon”

2017年5月9日,啟明星辰ADLab發現 Linux 內核存在遠程漏洞 “Phoenix Talon”(取鳳凰爪四趾之意),涉及 CVE-2017-8890、CVE-2017-9075、CVE-2017-9076、CVE-2017-9077,可影響幾乎所有 Linux kernel 2.5.69 ~Linux kernel 4.11 的內核版本、對應的發行版本以及相關國產系統。可導致遠程 DOS,且在符合一定利用條件下可導致 RCE,包括傳輸層的 TCP、DCCP、SCTP 以及網絡層的 IPv4 和 IPv6 協議均受影響。實際上該漏洞在 Linux 4.11-rc8 版本中已經被啟明星辰ADLab發現,且后來的 Linux 4.11 stable 版同樣存在此問題。經研究這些漏洞在 Linux 內核中至少已經潛伏了11年之久,影響極為深遠。

啟明星辰ADLab已第一時間將 “Phoenix Talon” 漏洞反饋給了 Linux 內核社區,漏洞上報后 Linux 社區在 Linux 4.12-rc1 中合并了修復該問題的補丁。

這些漏洞中以 CVE-2017-8890 最為嚴重(達到 Linux 內核漏洞兩個評分標準的歷史最高分,CVSS V2 評分達到滿分 10.0,CVSS V3 評分是歷史最高分9.8,NVD 上搜索歷史上涉及 Linux 內核漏洞這樣評分的漏洞不超過 20 個),以下分析以該漏洞為例,引用官方描述如下:

“The inet_csk_clone_lock function in net/ipv4/inet_connection_sock.c in the Linux kernel through 4.10.15 allows attackers to cause a denial of service (double free) or possibly have unspecified other impact by leveraging use of the accept system call.”

2. The Vulnerability

CVE-2017-8890 本身是一個 double free 的問題,使用 setsockopt() 函數中 MCAST\_JOIN\_GROUP選項,并調用 accept() 函數即可觸發該漏洞。

接著先看看幾個組播相關的數據結構:

include/uapi/linux/in.h
struct ip_mreq  {
        struct in_addr imr_multiaddr;   /* IP multicast address of group */
        struct in_addr imr_interface;   /* local IP address of interface */
};

該結構體的兩個成員分別用于指定所加入的多播組的組IP地址和所要加入組的本地接口IP地址。 ip_setsockopt() 實現了該功能,它通過調用 ip_mc_join_group() 把 socket 加入到多播組。

include/net/inet_sock.h
struct inet_sock {
    /* sk and pinet6 has to be the first two members of inet_sock */
    struct sock     sk;
#if IS_ENABLED(CONFIG_IPV6)
    struct ipv6_pinfo   *pinet6;
#endif
    /* Socket demultiplex comparisons on incoming packets. */
#define inet_daddr      sk.__sk_common.skc_daddr
#define inet_rcv_saddr  sk.__sk_common.skc_rcv_saddr   
#define inet_dport      sk.__sk_common.skc_dport
#define inet_num        sk.__sk_common.skc_num
[...]
    __u8            tos;
    __u8            min_ttl;
    __u8            mc_ttl;                 
    __u8            pmtudisc;
    __u8            recverr:1,
                is_icsk:1,
                freebind:1,
                hdrincl:1,                   
                mc_loop:1,                   
[...]
    int         uc_index;                     
    int         mc_index;                     
    __be32          mc_addr;                 
    struct ip_mc_socklist __rcu *mc_list;     
    struct inet_cork_full   cork;
};

其中 sk.__sk_common.skc_rcv_saddr 對于組播而言,只接收該地址發來的組播數據,對于單播而言,只從該地址所代表的網卡接收數據;mc_ttl 為組播的 ttlmc_loop 表示組播是否發向回路;mc_index 表示組播使用的本地設備接口的索引;mc_addr 表示組播源地址;mc_list 為組播列表。

include/linux/igmp.h
/* ip_mc_socklist is real list now. Speed is not argument;
   this list never used in fast path code
 */
struct ip_mc_socklist {
        struct ip_mc_socklist __rcu *next_rcu;
        struct ip_mreqn         multi;
        unsigned int            sfmode;         /* MCAST_{INCLUDE,EXCLUDE} */
        struct ip_sf_socklist __rcu     *sflist;
        struct rcu_head         rcu;
};

next_rcu 指向鏈表的下一個節點;multi 表示組信息,即在哪一個本地接口上,加入到哪一個多播組;sfmode 是過濾模式,取值為 MCAST_INCLUDEMCAST_EXCLUDE ,分別表示只接收 sflist 所列出的那些源的多播數據報和不接收 sflist 所列出的那些源的多播數據報;sflist 是源列表。

下面分別從該漏洞內存分配的關鍵代碼及二次釋放的關鍵代碼進行分析。

  • The Allocate

內存分配調用鏈:

1.用戶態

setsockopt() -> 

            …

2.內核態:

-> entry_SYSCALL_64_fastpath() -> SyS_setsockopt() -> SYSC_setsockopt() -> sock_common_setsockopt() -> tcp_setsockopt() -> ip_setsockopt() -> do_ip_setsockopt() -> do_ip_setsockopt() -> ip_mc_join_group() -> sock_kmalloc() -> [...]

使用 setsockopt() 函數中的 MCAST_JOIN_GROUP 選項。

net/socket.c

1777 SYSCALL_DEFINE5(setsockopt, int, fd, int, level, int, optname,
1778                 char __user *, optval, int, optlen)
1779 {
1780         int err, fput_needed;
1781         struct socket *sock;
1782
1783         if (optlen < 0)
1784                 return -EINVAL;
1785
1786         sock = sockfd_lookup_light(fd, &err, &fput_needed);
1787         if (sock != NULL) {
1788                 err = security_socket_setsockopt(sock, level, optname);
1789                 if (err)
1790                         goto out_put;
1791
1792                 if (level == SOL_SOCKET)
1793                         err =
1794                             sock_setsockopt(sock, level, optname, optval,
1795                                             optlen);
1796                 else
1797                         err =
1798                             sock->ops->setsockopt(sock, level, optname, optval,
1799                                                   optlen);
1800 out_put:
1801                 fput_light(sock->file, fput_needed);
1802         }
1803         return err;
1804 }

進入內核調用 SyS_setsockopt() 函數,level 設置的不為 SOL_SOCKET 即可,一般設置為 SOL_IP ,在1798 行處被調用。緊接著調用 sock_common_setsockopt() 函數。

net/ipv4/ip_sockglue.c

1256 int ip_setsockopt(struct sock *sk, int level,
1257                 int optname, char __user *optval, unsigned int optlen)
1258 {
1259         int err;
1260
1261         if (level != SOL_IP)
1262                 return -ENOPROTOOPT;
1263
1264         err = do_ip_setsockopt(sk, level, optname, optval, optlen);
1265 #ifdef CONFIG_NETFILTER
1266         /* we need to exclude all possible ENOPROTOOPTs except default case */
1267         if (err == -ENOPROTOOPT && optname != IP_HDRINCL &&
1268                         optname != IP_IPSEC_POLICY &&
1269                         optname != IP_XFRM_POLICY &&
1270                         !ip_mroute_opt(optname)) {
1271                 lock_sock(sk);
1272                 err = nf_setsockopt(sk, PF_INET, optname, optval, optlen);
1273                 release_sock(sk);
1274         }
1275 #endif
1276         return err;
1277 }

然后進入 ip_setsockopt() 函數,調用 do_ip_setsockopt() 函數(1264行代碼)。

net/ipv4/ip_sockglue.c

599 static int do_ip_setsockopt(struct sock *sk, int level,
600                             int optname, char __user *optval, unsigned int optlen)
601 {
602         struct inet_sock *inet = inet_sk(sk);
603         struct net *net = sock_net(sk);
604         int val = 0, err;
605         bool needs_rtnl = setsockopt_needs_rtnl(optname);
606
607         switch (optname) {
      [...]
1009         case MCAST_JOIN_GROUP:
1011         {
1012                 struct group_req greq;
1013                 struct sockaddr_in *psin;
1014                 struct ip_mreqn mreq;
1015
1016                 if (optlen < sizeof(struct group_req))
1017                         goto e_inval;
1018                 err = -EFAULT;
1019                 if (copy_from_user(&greq, optval, sizeof(greq)))
1020                         break;
1021                 psin = (struct sockaddr_in *)&greq.gr_group;
1022                 if (psin->sin_family != AF_INET)
1023                         goto e_inval;
1024                 memset(&mreq, 0, sizeof(mreq));
1025                 mreq.imr_multiaddr = psin->sin_addr;
1026                 mreq.imr_ifindex = greq.gr_interface;
1027
1028                 if (optname == MCAST_JOIN_GROUP)
1029                         err = ip_mc_join_group(sk, &mreq);
1030                 else
1031                         err = ip_mc_leave_group(sk, &mreq);
1032                 break;
1033         }
[...]

代碼 1019~1021 行調用 copy_from_user() 將用戶態的數據拷貝到內核態。之前已經將 option 設置為 MCAST_JOIN_GROUP,緊接著調用 ip_mc_join_group() 函數:

net/ipv4/igmp.c

2094 int ip_mc_join_group(struct sock *sk, struct ip_mreqn *imr)
2095 {
2096         __be32 addr = imr->imr_multiaddr.s_addr;
2097         struct ip_mc_socklist *iml, *i;
2098         struct in_device *in_dev;
2099         struct inet_sock *inet = inet_sk(sk);
2100         struct net *net = sock_net(sk);
2101         int ifindex;
2102         int count = 0;
2103         int err;
2104
2105         ASSERT_RTNL();
2106
2107         if (!ipv4_is_multicast(addr))
2108                 return -EINVAL;
2109
2110         in_dev = ip_mc_find_dev(net, imr);
2111
2112         if (!in_dev) {
2113                 err = -ENODEV;
2114                 goto done;
2115         }
2116
2117         err = -EADDRINUSE;
2118         ifindex = imr->imr_ifindex;
2119         for_each_pmc_rtnl(inet, i) {
2120                 if (i->multi.imr_multiaddr.s_addr == addr &&
2121                     i->multi.imr_ifindex == ifindex)
2122                         goto done;
2123                 count++;
2124         }
2125         err = -ENOBUFS;
2126         if (count >= net->ipv4.sysctl_igmp_max_memberships)
2127                 goto done;
2128         iml = sock_kmalloc(sk, sizeof(*iml), GFP_KERNEL);
2129         if (!iml)
2130                 goto done;
2131
2132         memcpy(&iml->multi, imr, sizeof(*imr));
2133         iml->next_rcu = inet->mc_list;
2134         iml->sflist = NULL;
2135         iml->sfmode = MCAST_EXCLUDE;
2136         rcu_assign_pointer(inet->mc_list, iml);
2137         ip_mc_inc_group(in_dev, addr);
2138         err = 0;
2139 done:
2140         return err;
2141 }

代碼2128行 sock_kmalloc() 進行了內存分配。

  • The first free

在內核里無時無刻都在產生軟中斷,而此次漏洞涉及的軟中斷是由 accept() 系統調用引起的,由于該函數本身作用于進程上下文,并不會產生軟中斷。但是調用 accept() 時,會在內核中誘發某種軟中斷產生,該軟中斷會調用 rcu_process_callbacks() 函數:

kernel/rcu/tree.c

3118 static __latent_entropy void rcu_process_callbacks(struct softirq_action *unused)
3119 {
3120         struct rcu_state *rsp;
3121
3122         if (cpu_is_offline(smp_processor_id()))
3123                 return;
3124         trace_rcu_utilization(TPS("Start RCU core"));
3125         for_each_rcu_flavor(rsp)
3126                 __rcu_process_callbacks(rsp);
3127         trace_rcu_utilization(TPS("End RCU core"));
3128 }

__rcu_process_callbacks 調用 rcu_do_batch() 函數,如下:

kernel/rcu/tree.c

2840 static void rcu_do_batch(struct rcu_state *rsp, struct rcu_data *rdp)
2841 {
2842         unsigned long flags;
2843         struct rcu_head *next, *list, **tail;
2844         long bl, count, count_lazy;
2845         int i;
2846
2847         /* If no callbacks are ready, just return. */
2848         if (!cpu_has_callbacks_ready_to_invoke(rdp)) {
2849                 trace_rcu_batch_start(rsp->name, rdp->qlen_lazy, rdp->qlen, 0);
2850                 trace_rcu_batch_end(rsp->name, 0, !!READ_ONCE(rdp->nxtlist),
2851                                     need_resched(), is_idle_task(current),
2852                                     rcu_is_callbacks_kthread());
2853                 return;
2854         }
[...]
2874         count = count_lazy = 0;
2875         while (list) {
2876                 next = list->next;
2877                 prefetch(next);
2878                 debug_rcu_head_unqueue(list);
2879                 if (__rcu_reclaim(rsp->name, list))
2880                         count_lazy++;
2881                 list = next;
2882                 /* Stop only if limit reached and CPU has something to do. */
2883                 if (++count >= bl &&
2884                     (need_resched() ||
2885                      (!is_idle_task(current) && !rcu_is_callbacks_kthread())))
2886                         break;
2887         }
[...]

注意代碼中第2879行,函數 __rcu_reclaim() 實現如下:

kernel/rcu/rcu.h

106 static inline bool __rcu_reclaim(const char *rn, struct rcu_head *head)
107 {
108         unsigned long offset = (unsigned long)head->func;
109
110         rcu_lock_acquire(&rcu_callback_map);
111         if (__is_kfree_rcu_offset(offset)) {
112                 RCU_TRACE(trace_rcu_invoke_kfree_callback(rn, head, offset));
113                 kfree((void *)head - offset);
114                 rcu_lock_release(&rcu_callback_map);
115                 return true;
116         } else {
117                 RCU_TRACE(trace_rcu_invoke_callback(rn, head));
118                 head->func(head);
119                 rcu_lock_release(&rcu_callback_map);
120                 return false;
121         }
122 }

在113行調用 kfree() 進行了第一次釋放。

  • The second free

當斷開 TCP 連接時,內核通過 sock\_close() 函數直接調用 sock\_release() 來實現斷開功能,該函數會清空 ops,更新全局 socket 數目,更新 inode 引用計數。隨后進入到 inet\_release() 函數調用 tcp\_close() 函數來最終關閉 sock。

net/ipv4/af_inet.c

403 int inet_release(struct socket *sock)
404 {
405         struct sock *sk = sock->sk;
406
407         if (sk) {
408                 long timeout;
409
410                 /* Applications forget to leave groups before exiting */
411                 ip_mc_drop_socket(sk);
412
413                 /* If linger is set, we don't return until the close
414                  * is complete.  Otherwise we return immediately. The
415                  * actually closing is done the same either way.
416                  *
417                  * If the close is due to the process exiting, we never
418                  * linger..
419                  */
420                 timeout = 0;
421                 if (sock_flag(sk, SOCK_LINGER) &&
422                     !(current->flags & PF_EXITING))
423                         timeout = sk->sk_lingertime;
424                 sock->sk = NULL;
425                 sk->sk_prot->close(sk, timeout);
426         }
427         return 0;
428 }

用戶程序斷開 TCP 連接時,內核里使用 ip\_mc\_drop\_socket() 函數進行回收。

net/ipv4/igmp.c

2592 void ip_mc_drop_socket(struct sock *sk)
2593 {
2594         struct inet_sock *inet = inet_sk(sk);
2595         struct ip_mc_socklist *iml;
2596         struct net *net = sock_net(sk);
2597
2598         if (!inet->mc_list)
2599                 return;
2600
2601         rtnl_lock();
2602         while ((iml = rtnl_dereference(inet->mc_list)) != NULL) {
2603                 struct in_device *in_dev;
2604
2605                 inet->mc_list = iml->next_rcu;
2606                 in_dev = inetdev_by_index(net, iml->multi.imr_ifindex);
2607                 (void) ip_mc_leave_src(sk, iml, in_dev);
2608                 if (in_dev)
2609                         ip_mc_dec_group(in_dev, iml->multi.imr_multiaddr.s_addr);
2610                 /* decrease mem now to avoid the memleak warning */
2611                 atomic_sub(sizeof(*iml), &sk->sk_omem_alloc);
2612                 kfree_rcu(iml, rcu);
2613         }
2614         rtnl_unlock();
2615 }

代碼2612行調用 kfree_rcu() 進行第二次釋放。

3.Affected

  • 受影響的內核版本

經研究,理論上 Linux kernel 2.5.69 ~ Linux kernel 4.11 的所有版本都受 “Phoenix Talon” 影響,且經開源社區驗證 “Phoenix Talon” 漏洞影響的 Linux 內核版本部分列表如下:

經啟明星辰ADLab測試 Linux kernel 4.11 亦受影響。

  • 受影響的發行版本

經開源社區驗證部分受影響發行版本(不完整列表)如下:

Red Hat Enterprise MRG 2 Red Hat Enterprise Linux 7 Red Hat Enterprise Linux 6 Red Hat Enterprise Linux 5 SUSE Linux Enterprise Desktop 12 SP1 SUSE Linux Enterprise Desktop 12 SP2 SUSE Linux Enterprise Server 11 SP3 LTSS SUSE Linux Enterprise Server 11 SP4 SUSE Linux Enterprise Server 12 GA SUSE Linux Enterprise Server 12 SP1 SUSE Linux Enterprise Server 12 SP2 SUSE Linux Enterprise Server for SAP 11 SP3 SUSE Linux Enterprise Server for SAP 11 SP4 SUSE Linux Enterprise Server for SAP 12 GA SUSE Linux Enterprise Server for SAP 12 SP1 SUSE Linux Enterprise Server for SAP 12 SP2

另外,啟明星辰ADLab對下列的部分發行版本做了測試,確認均受 “Phoenix Talon” 漏洞影響:

Ubuntu 14.04 LTS (Trusty Tahr) Ubuntu 16.04 LTS (Xenial Xerus) Ubuntu 16.10 (Yakkety Yak) Ubuntu 17.04 (Zesty Zapus) Ubuntu 17.10 (Artful Aardvark)

4. Solution

  1. 官方已經發布了修復該問題的補丁,可通過升級Linux內核修復“Phoenix Talon”相關漏洞。
  2. 使用 Grsecurity/PaX 對內核加固。

5. Timeline

May 09 - Report sent to Linux Kernel Community May 09 - Linux Kernel Community confirmed May 09 - Linux Kernel Community patched in linux upstream May 10 - Assgined CVE number

“Phoenix Talon”在 Linux 內核中潛伏長達11年之久,影響范圍非常廣泛(以上只是官方以及我們測試的部分結果,即使這些也足夠看出 “Phoenix Talon” 波及之深之廣),啟明星辰ADLab提醒廣大用戶盡快采取相應的修復措施,避免引發漏洞相關的網絡安全事件。

Reference:
[1] https://people.canonical.com/~ubuntu-security/cve/2017/CVE-2017-8890.html
[2] https://security-tracker.debian.org/tracker/CVE-2017-8890
[3] https://www.suse.com/security/cve/CVE-2017-8890/
[4] https://bugzilla.redhat.com/show_bug.cgi?id=1450973
[5] https://bugzilla.suse.com/show_bug.cgi?id=1038544
[6] https://www.mail-archive.com/netdev@vger.kernel.org/msg167626.html
[7] https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-8890
[8] https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-9075
[9] https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-9076
[10] https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-9077
[11] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=657831ffc38e30092a2d5f03d385d710eb88b09a
[12] http://www.securityfocus.com/bid/98562/info
[13] http://www.openwall.com/lists/oss-security/2017/05/30/24
[14] https://www.kernel.org
[15] Linux Kernel Documentation


啟明星辰積極防御實驗室(ADLab)

ADLab成立于1999年,是中國安全行業最早成立的攻防技術研究實驗室之一,微軟MAPP計劃核心成員。截止目前,ADLab通過CVE發布Windows、Linux、Unix等操作系統安全或軟件漏洞近300個,持續保持亞洲領先并確立了其在國際網絡安全領域的核心地位。實驗室研究方向涵蓋操作系統與應用系統安全研究、移動智能終端安全研究、物聯網智能設備安全研究、Web安全研究、工控系統安全研究、云安全研究。研究成果應用于產品核心技術研究、國家重點科技項目攻關、專業安全服務等。


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