作者: 啟明星辰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 為組播的 ttl ;mc_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_INCLUDE 或 MCAST_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
- 官方已經發布了修復該問題的補丁,可通過升級Linux內核修復“Phoenix Talon”相關漏洞。
- 使用 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安全研究、工控系統安全研究、云安全研究。研究成果應用于產品核心技術研究、國家重點科技項目攻關、專業安全服務等。

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