0x00漏洞背景
2017年6月29日,思科在安全更新中修复了在IOS和IOS XE软件中SNMP子系统的9个严重远程代码执行漏洞(CVE-2017-6736—–CVE-2017-6744)。这些漏洞影响了多个Cisco IOS和Cisco IOS XE的主流版本。其中,CVE-2017-6736漏洞允许攻击者通过发送特定的SNMP数据包,使目标系统重新加载或执行代码。2018年1月,研究人员Artem Kondratenko公开了CVE-2017-6736的PoC脚本代码,由于思科网络设备有极高的市场占有率,所以很多没有及时更新补丁或按照思科官方处置建议进行配置的网络设备,增大了被攻击者利用漏洞进行攻击的风险。
从官方给出的信息来看,这个漏洞与2016年影子经纪人披露的NSA武器库中Cisco ASA设备SNMP远程代码执行漏洞(CVE-2016-6366)在受影响系统版本,利用条件限制等很多信息上颇为相似。出于探究两个漏洞原理和细节关系,并通过分析过程深入了解基于RISC指令集的IOT设备漏洞分析和调试方法的想法,本文从思科IOS处理SNMP请求数据包的过程入手,分析了漏洞产生的原因,并将分析过程和技术细节予以呈现。
0x01 漏洞简要介绍
1. 漏洞原理
CVE-2017-6736从本质上来说,是一个缓冲区溢出漏洞。从漏洞利用的角度来讲,攻击者可以向系统发送精心构造的SNMP数据包来造成溢出,当漏洞利用成功时,攻击者即可在设备上执行shellcode。
2. 漏洞影响范围
该漏洞可以影响此前所有Cisco IOS 和IOS XE软件的发行版,且影响所有的SNMP版本(1, 2c 和3)。具体发行版包括Cisco IOS 12.0版本至12.4版本、15.0版本至15.6版本和IOS XE 2.2版本至3.17版本。其中,运行SNMP 2c或更低版本的系统只有在攻击者知道系统SNMP只读社区(Readonly Community)字符串时才能成功利用,对于运行SNMP v3的系统,攻击者必须拥有系统的用户访问凭据才能进行攻击。
另外,Cisco官方给出了脆弱MIB的配置列表,如下所示:
l ADSL-LINE-MIB
l ALPS-MIB
l CISCO-ADSL-DMT-LINE-MIB
l CISCO-BSTUN-MIB
l CISCO-MAC-AUTH-BYPASS-MIB
l CISCO-SLB-EXT-MIB
l CISCO-VOICE-DNIS-MIB
l CISCO-VOICE-NUMBER-EXPANSION-MIB
l TN3270E-RT-MIB
上述MIB在个别SNMP系统上会有缺失,但是当列表中的MIB存在于SNMP系统时,会默认启用。
3. 利用条件限制
攻击者主机必须在设备的信任列表中才能向IOS发送SNMP数据包,可以通过IPv4或者IPv6发送SNMP数据包实现漏洞利用,但只有指向系统的流量才能利用漏洞。在运行SNMP 2c或更低版本的系统只有在攻击者知道系统SNMP只读社区(Readonly Community)字符串时才能成功利用,对于运行SNMP v3的系统,攻击者必须拥有系统的用户访问凭据才能进行攻击。
0x02 漏洞分析
1. 前期准备
由于没有真机作为调试环境,所以采用 IDA Pro + Qemu + Dynamips + GDB stub作为调试环境。而目前Dynamips最新版本支持的Cisco IOS镜像中,并没有c2800系列。经过模拟环境测试,PoC脚本用于c2600系列固件时可以触发漏洞造成溢出,所以我们选择c2600-bino3s3-mz.123-22.bin固件镜像作为调试和分析对象。
2. PoC代码分析
该漏洞PoC代码由python编写。主要功能是构造特定格式的数据包,造成SNMP处理流程溢出。从PoC代码公开的信息来看,漏洞可在Cisco Integrated Service Router 2811型号的设备上利用。固件和ROM支持型号如下:
最新固件型号 |
Cisco IOS Software, 2800 Software (C2800NM-ADVENTERPRISEK9-M), Version 15.1(4)M12a, RELEASE SOFTWARE (fc1) |
ROM型号 |
System Bootstrap, Version 12.4(13r)T, RELEASE SOFTWARE (fc1) |
核心代码如下:
alps_oid=’1.3.6.1.4.1.9.9.95.1.3.1.1.7.108.39.84.85.195.249.106.59.210.37.23.42.103.182.75.232.81{0}{1}{2}{3}{4}{5}{6}{7}.14.167.142.47.118.77.96.179.109.211.170.27.243.88.157.50{8}{9}.35.27.203.165.44.25.83.68.39.22.219.77.32.38.6.115{10}{11}.11.187.147.166.116.171.114.126.109.248.144.111.30′
for k, sh_dword in enumerate([sh_buf[i:i+4] for i in range(0, len(sh_buf), 4)]):
s0 = bin2oid(sh_dword) # shellcode dword
s1 = bin2oid(‘x00x00x00x00’)
s2 = bin2oid(‘xBFxC5xB7xDC’)
s3 = bin2oid(‘x00x00x00x00’)
s4 = bin2oid(‘x00x00x00x00’)
s5 = bin2oid(‘x00x00x00x00’)
s6 = bin2oid(‘x00x00x00x00’)
ra = bin2oid(‘xbfxc2x2fx60’) # return control flow jumping over 1 stack frame
s0_2 = bin2oid(shift(shellcode_start, k * 4))
ra_2 = bin2oid(‘xbfxc7x08x60’)
s0_3 = bin2oid(‘x00x00x00x00’)
ra_3 = bin2oid(‘xBFxC3x86xA0’)
payload = alps_oid.format(s0, s1, s2, s3, s4, s5, s6, ra, s0_2, ra_2, s0_3, ra_3)
send(IP(dst=args.host)/UDP(sport=161,dport=161)/SNMP(community=args.community,PDU=SNMPget(varbindlist=[SNMPvarbind(oid=payload)])))
其中payload为精心构造的SNMP数据包。这个数据包由3部分组成。前14个字节为OID = 1.3.6.1.4.1.9.9.95.1.3.1.1.7。这个OID代表alpsCktBaseNumActiveAscus,只读权限,可以返回当前配置状态下可连接ALPS电路的ASCU数量。OID后面的‘108’字段表示后面数据的字节数,但是‘108’后有109个字节。如此构造数据包的原因将在后面的代码分析给出解释。
我们可以看到,数据包中最为关键的两部分字段,为s0和ra。其中,s0为shellcode按照4字节大小拆分后发送给目标用来执行的指令,ra为栈帧溢出时构造的指令的执行地址。
3. 捕获数据包
上图为实际调试代码过程中捕获的数据包。
4. 漏洞代码静态分析
运行实验环境,并将IDA附加到远程进程,然后运行PoC代码,当第一个数据包发送完毕后,随即造成溢出。
此时查看系统堆栈,并尝试不同的断点进行调试,最后确定有漏洞的函数为sub_80f11864。
这个函数创建了v26这个局部变量,并在调用sub_80f09030时将其作为参数使用。
我们可以看到,sub_80f09030函数的具体功能为将参数a2作为原地址,将长度参数a3的数据经过一次运算后写入目的地址:参数result。
在这个copy过程中,copy长度受参数a3控制,而不是本地的局部变量,而目的地址则是上层函数的局部变量。由于PowerPC和Sparc架构在调用函数时,如果局部变量超过10个,则剩余的局部变量就储存在栈帧当中。在这种情况下,如果不对a3的值进行判断,则有可能因为数据操作长度过大而使上层函数的栈帧被破坏,从而造成缓冲区溢出。
5. 动态调试
系统启动以后,会有接受数据包的进程处理snmp 请求,并根据community等属性派发给不同的例程。经过多次断点调试,确定当Community 为public且snmp get请求的OID为1.3.6.1.4.1.9.9.95.1.3.1.1.7时,系统会将数据包信派发给sub_80f0d860。
经过分析,sub_80f0d860函数的参数如下:
A1 |
数据包结构体,成员包括数据包的长度,指向数据包内容的指针 |
A2 |
指向OID长度的指针 |
A3 |
当community为public时,该参数为160 |
A4 |
判断应该是一个标志位 |
A5 |
应为数据包完整性的校验值,1 |
该函数的主要代码如下所示:
下图为PoC代码第一次发送数据包的内容:前14个字节是snmp协议的OID值,第15个字节是一个长度的字节数,这里是108,而后面的payload有109个字节。
Sub_80A3D2E0的行为是取出packet中下标为0xE的数据,也就是那个长度值108。而后面的if语句则表明,当packet中oid字段后面的数字不为payload长度减一时,函数会return 0 。也就是如果要继续packet的处置过程,oid字段后面的数字必须为后面payload的长度减1。
在调用sub_80A3C414进行校验时,上层函数的局部变量v33被赋值为packet的最后一个字节。然后代码会申请一段内存,并将数据包中的payload内容由原来的双字大小转为一个字节。
Sub_80A3CBFC函数本身有5个参数,A1是数据包结构体,A2是指向OID长度的指针, A3 是一个局部变量,用来返回申请buffer的地址,A4是160, A5 是一个校验值,当父函数第四个参数为160,即community为public时为0。这个函数的行为是申请一段内存,并逆序将payload复制到buffer当中,将payload内容由双子转换为一个字节。逆序复制代码如下图所示:
然后代码执行到sub_80F11864处。此处函数形成栈帧的代码显示函数的栈帧大小为0x30个字节。IR的值存放在栈帧下方的4个字节中:
局部变量v26的位置是栈顶指针向下8个字节。
正如此前静态分析中提到的,sub_80F09030的参数中,result为上层函数的局部变量v26,a2是buffer的首地址,a3是0x6c,也就是108。这个函数的功能是将buffer中108个字节的内容复制到result为首地址的内存区域中,而sub_80F11864的栈帧只有0x30 也就是48个字节(0x80DCC3F0——0x80DCC420)。同时result局部变量在栈帧当中的位置为esp+8,也就是0x80DCC3F8。连续向栈帧写入108个字节,必然造成溢出。
上图为执行完内存拷贝函数后栈帧的内容。可以看到堆栈已经被破坏。sub_80F11864执行完毕时,PC寄存器的值将会变成0XBFC22F60,进而执行相应地址的代码。而我们可以看到,shellcode并没有布局在PC指针指向的内存区域,而是位于sub_80F11864函数的堆栈中,且只有4个字节。出于堆栈不可执行的因素,这个数据包在待分析固件上触发漏洞,并导致进程崩溃,无法处理新的攻击数据包。
至此,漏洞触发的原因和过程分析完毕。
0x03 总结
通过分析我们得知, CVE-2017-6736和CVE-2016-6366漏洞原理有一定的相似性,CVE-2016-6366具体可以参考我们以前的分析文章《揭开思科ASA防火墙网络军火的面纱(上)》,。两者漏洞触发的主要原因都是在内存拷贝过程中,由于上层函数局部变量控制拷贝长度而导致的栈溢出,而两者的不同之处在于内存布局的方法和执行shellcode前需要完成的跳转过程。
与CVE-2016-6366相似的是,CVE-2017-6736的代码逻辑中有对数据包长度的限制,会丢弃超过长度限制的UDP数据包,这使得在一次SNMP数据包请求过程中几乎不可能完成一次完整的shellcode执行。另外,该漏洞在不同版本固件上的利用,要根据固件的不同的内存分布对payload部分进行适配开发,以调整堆栈布局。
由于Cisco IOS系统软件主要应用于Cisco企业级的路由器和交换机中,很多大型网络基础设施都部署了相应Cisco IOS版本网络设备,而这些设备都可能会受到该漏洞的影响。同时,此类设备固件因为线上更新复杂往往得不到第一时间的更新,且为了方便SNMP远程管理网络设备,通常不做host限制,community串也多为默认口令或简单弱密码而易遭受到暴力破解攻击,再加上网络专有设备调试环境复杂,且多采用基于RISC指令集的架构而非x86架构的原因,漏洞披露,甚至于作者公开PoC时国内相关研究机构也少有跟进。考虑到上述种种原因给该漏洞造成的影响,相关运维人员应当尽早更新设备固件修复漏洞,特别是在互联网边界上的设备,及时消除这一安全隐患。
参考资料:
https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20170629-snmp