转:https://zhuanlan.zhihu.com/p/26271959
背景知识:
1.snprintf(),为函数原型int snprintf(char *str, size_t size, const char *format, ...),将可变个参数(...)按照format格式化成字符串,然后将其复制到str中。
函数返回值:若成功则返回欲写入的字符串长度,若出错则返回负值。与snprintf的返回值不同,sprintf的返回值是成功写入的字符串长度,此处需要谨慎处理!
2.strcpy() sprintf() strcat() 存在安全隐患, 其对应的安全版为:strncpy() snprintf() strncat()
很多技术宅们都喜欢折腾自己的路由器,例如在上面搭建NAS、配置远程下载和使用代理上网,这些技术和相关软件能帮助大家在硬盘里搜集大量的娱乐、教育资源。
不过大家在网上下载和使用第三方软件时要小心啦,要多关注软件的安全更新。长亭安全研究实验室在2016年5月通过GeekPwn向华为PSIRT报告了迅雷固件Xware的多个漏洞,这些漏洞不仅存在于华为荣耀路由默认支持的远程下载功能中,也会影响使用Xware的其他路由器或Linux服务器。在收到漏洞报告后,华为官方迅速给出了修复,华为荣耀路由早已不受影响。不过,迅雷官方在2016年2月就已经宣布停止维护该固件,github上基于Xware的一些开源项目也因此弃坑(例如Xinkai/XwareDesktop和PointTeam/PointDownload)。目前散落在网上的软件版本很可能是未得到修复的,在这里建议大家尽量避免使用已过期且无官方支持的Xware软件。
在本文中,笔者会给大家分享一下漏洞的细节和利用思路。阅读本文不需要任何安全研究方面的经验,只需要一点点栈溢出的基本知识。还不知道栈溢出的读者请移步长亭技术专栏的《手把手教你栈溢出从入门到放弃》上下两篇文章,如果您读完后真的准备放弃,那不妨试试本文,或许本文能给你一次重新选择的机会。
说点历史
栈溢出攻击的相关概念最早要追述到1972年美国空军发表的一份研究报告《Computer Security Technology Planning Study》。在这份报告中,通过溢出缓冲区来注入代码这一想法首次被提了出来。大家来感受一下最早的描述原文:
栈溢出的概念虽然早就提出,但直到1988年才出现了首次真实的攻击,Morris蠕虫病毒利用了Unix操作系统中fingerd程序的gets()函数导致的栈溢出来实现远程代码执行。
1996年,Elias Levy (a.k.a Aleph One)在大名鼎鼎的Phrack杂志上发表了文章《Smashing the Stack for Fun and Profit》,从此栈溢出漏洞的利用技术被广泛知晓。
也许有读者会疑惑,栈溢出这样的低级错误现在还存在吗?本文要介绍的几个2016年发现的漏洞中,最为关键的一个漏洞就是栈溢出。从最早提出这一概念的1972年到现在已经有四十多年的历史,经历了将近半个世纪,程序员们依然会在这一看似简单的问题中跌倒。这一情况并非个案,在路由器这类嵌入式设备中依然普遍存在,长亭安全研究实验室在2016年GeekPwn中攻破了10款路由器,利用的漏洞中大多数还是栈溢出。而即便是经历了多年发展PC端操作系统,也同样存在栈溢出:在Pwn2Own 2017上,来自美国的Richard Zhu找到了Mac操作系统中的栈溢出漏洞。
来到2017年的今天,面对栈溢出,我们依然不能说放弃。
路由器被搞,有哪些危害?
路由器作为家庭上网的入口,其安全重要性不言而喻。家里所有智能设备、电脑都需要通过连接路由器上网,一旦路由器沦陷,攻击者就可以看到所有明文上传和下载的流量。还记得2016年的央视315晚会吗?节目现场就演示了在WiFi端截获手机App的上网流量,其中包含姓名、电话、生日、家庭地址、订单等隐私信息。更严重的是,攻击者还可能通过篡改流量进一步入侵连接这台路由器的设备,例如在你下载某一个Windows安装包或者安卓应用APK文件时,偷偷将其替换成植入后门的版本。
不开防火墙,后果很严重
路由器有哪些可能被入侵的途径呢?一般来说,家用路由器用于组建家庭局域网(LAN)。对于公网连接的部分(WAN口),路由器往往都配置了防火墙,禁止了公网对路由器自身服务的访问,这样即便路由器存在漏洞,也不至于暴露在“大庭广众”之下。长亭安全研究实验室就在GeekPwn上披露过一款在此问题上出现疏忽的路由器,这就导致攻击者可以直接在公网上利用路由器的漏洞来入侵成千上万个目标。
入侵路由两步走
对于大多数开启防火墙的路由器来说,入侵的第一步就是接入路由器局域网络(LAN),这一步有好多种方法可以尝试:Wifi万能钥匙、破解WEP加密、破解WPS PIN码、使用字典爆破Wifi密码等等。而对于公共场合的路由器来说,这一步就不是问题了,Wifi密码是公开的,任何人都可以直接接入。
接入路由器网络后,第二步就是利用路由器自身的缺陷来取得路由器的完全控制权,本文介绍的案例漏洞就是用在这一步。路由器的漏洞主要存在于自身开启的软件服务当中,例如几乎每个路由器都会有一个开启在80端口的Web管理界面,还有其他常见服务例如用于分配IP地址的服务DHCP、即插即用服务UPnP等,这些服务会监听在某个TCP/UDP端口,接入路由器网络的攻击者可以通过向这些端口发送特定数据包来实施各种类型的攻击,例如权限绕过、命令注入、内存破坏等。
说完了攻击的场景,让我们回到本文的目标上来。如果路由器自带或者手动配置了迅雷远程下载功能,Xware软件会监听一些端口,其中包含一个处理HTTP协议的端口,在某款路由器上为9000,本文介绍的漏洞就是跟这个服务有关。大家可以在这里下载官方停止维护之前放出的最后一个版本。
一串漏洞来袭
官方提供的Xware软件以及路由器固件中自带的Xware软件都只有编译好的二进制文件,通过逆向分析,我们一共发现了三个问题,每个问题单独来看都无法造成严重影响,但是三个漏洞经过组合利用便可以达到远程任意代码执行的效果。
漏洞一:你真的会用snprintf吗:信息泄漏
学过C语言的同学都知道snprintf函数的用法,这是最基本的字符串处理函数之一。基本形式如下:
int snprintf(char *str, size_t size, const char *format, ...);
众所周知,我们可以通过指定snprintf的第二个参数size来防止缓冲区溢出的发生,然而你是否真正理解snprintf返回值的含义?大家先来看看下面几行代码,猜猜代码的输出是什么:
int main() {
char buf[8];
int n = snprintf(buf, 8, "%s", "1234567890");
printf("buf: %s\n", buf);
printf("n: %d\n", n);
}
相信第一个buf的输出难不倒大多数人,而返回值n的输出一定会让一部分人吃惊:
buf: 1234567
n: 10
难道snprintf函数返回的不是打印字符的个数吗?让我们来查一查文档,下面是摘录自man中的一段解释:
The functions snprintf() and vsnprintf() do not write more than size bytes (including the terminating null byte (’\0’)). If the output was truncated due to this limit, then the return value is the number of characters which would have been written to the final string if enough space had been available. Thus, a return value of size or more means that the output was truncated.
确实,snprintf返回的是打印字符的个数,但是这个数字是在假设没有第二个参数size限制的情况下统计的。这一细节同样困惑了Xware开发人员,我们来看一看存在漏洞的代码(下图为IDA的反编译结果):
在上面这段代码中,snprintf的返回值v19被用作HTTP响应包的实际长度传入em_comm_send函数。HTTP响应包的实际长度实际上会受到snprintf的第二个参数0x100的限制,但返回的长度v19实际上没有这个限制,因此http响应在有些情况下会输出超过0x100的字符,buf缓冲区后面的数据会被返回。buf缓冲区实际分配在堆上,因此这个漏洞能够用来泄露堆上的数据。
触发漏洞时返回的HTTP响应数据如下所示:
这个漏洞的CVE编号是CVE-2016-5367。
漏洞二:INI配置注入漏洞
INI是一种常见的初始化配置文件格式,INI就是Initialization的前三个字母。INI文件的格式非常简单,由多个节(Section)组成,每个节由一行行的键值对组成,如下所示:
[section1]
key1=value1
key2=value2
[section2]
key3=value3
Xware的HTTP服务存在一个登录接口login,该接口会解析HTTP请求中cookie里的一些参数,并将值保存入ini配置文件中。例如cookie = "isvip=0; jumpkey=A; usernick=B; userid=1"时,ini配置文件中会写入用户相关的参数,样例配置文件如下:
[license]
server_addr=X.X.X.X
[huiyuan]
userid=1
username=B
vip_level=0
jumpkey=A
细心的话会发现在ini配置文件中,不同的键值对都是通过换行来区分。如果键或值中存在换行符怎么办?假如我们尝试给出这样的cookie = “isvip=0; jumpkey=A\n\n[license]\nserver_addr=1.3.3.7; usernick=B; userid=1”,那么写入配置文件的就是:
[license]
server_addr=X.X.X.X
[huiyuan]
userid=1
username=B
vip_level=0
jumpkey=A
[license]
server_addr=1.3.3.7
这样我们就可以在ini文件后面的位置插入一个新的配置,来修改文件前面的默认配置,此例中我们修改的是server_addr的值。
本身通过cookie来设置的配置项只能是huiyuan这个节中的指定键,通过在值中插入换行符,我们就实现了任意配置选项的注入。
这个漏洞的CVE编号是CVE-2016-5366。
漏洞三:发生在2016年的栈溢出
利用上述漏洞,在配置文件中把默认的license server改掉能有什么用呢?
幸运的是,Xware的这个HTTP服务器还暴露了一个接口可以重启Xware软件,攻击者可以随时调用它,来让程序在重启时解析被我们注入过的INI配置文件。所以让我们看看程序在初始化过程中对配置文件中的license server做了什么。
通过逆向分析,我们找到了解析license server的相关代码:
解析server地址和端口的代码在parse_server_addr函数中:
此处代码明显存在溢出,首先memcpy函数在使用时指定的拷贝长度只与源字符串有关,其次在另个分支中直接使用了危险函数strcpy。
两处拷贝的目标缓冲区v4,即传入parse_server_addr的第二个参数,实际是在上层函数中栈上的局部buffer,因此这里的溢出是典型的栈溢出。
这个漏洞的CVE编号是CVE-2016-5365。
漏洞利用:“组合拳”
上面三个漏洞每个漏洞单独看都无法造成严重影响,即便是栈溢出漏洞也是发生在解析初始化INI配置文件的过程中,一般人都会认为配置文件中的选项都是写死的,例如这里的license server地址,就算用strcpy也不必担心。然而如果我们把这几个小问题组合在一起使用时,会出现怎样的威力呢?
首先我们可以通过堆内存的泄露找到libc库加载的地址,因为通常linux采用的是dlmalloc/ptmalloc,堆上空闲的块中会包含指向libc全局变量的指针(具体参考堆的实现,这里不作展开)。目前大多MIPS/ARM架构的路由器都没有开启地址随机化保护(ASLR),泄露的这个地址往往是不变的。
接下来我们可以利用INI配置注入漏洞,往INI配置文件中注入超长的license server地址,并在其中植入ROP payload。由于我们已经知道libc的地址,我们就能够使用libc中的gadget来组ROP。
最终,只要我们调用重启Xware的接口,Xware就会重新解析INI配置文件,并在这个过程中触发栈溢出,从而执行我们的ROP代码。
我们用下面的流程图来总结这三个漏洞的组合利用过程:
后记
本文以一个真实案例给大家介绍了栈溢出漏洞造成的危害。根据长亭安全研究实验室的研究经验,像这样的栈溢出的案例在智能设备上还有很多很多。目前市面上的智能设备种类和品牌可谓百花齐放,这些产品在安全性的设计和实现上参差不齐。如果单从安全性考量,笔者建议读者在选购时选择大厂商自主研发的产品,因为大品牌厂商不仅在研发经验上有更多的积累,而且在对待安全问题的态度上也是积极正面的。也建议大家在使用智能设备时,多关注官方的动态和安全补丁的发布,及时更新固件。