译者:興趣使然的小胃
预估稿费:200RMB
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
一、前言
最近我在Alpine Linux包管理器中发现了两个严重漏洞,漏洞编号分别为CVE-2017-9669以及CVE-2017-9671。如果你正在使用Alpine,攻击者有可能利用这两个漏洞在你的主机上执行恶意代码。
Alpine Linux是一款轻量级的Linux发行版,在过去的几年中越来越受人们欢迎,主要原因可能是它在容器(特别是Dokcer)方面的巨大优势。官方Docker仓库中大多数都有与alpine对应的版本号,并且alpine仓库的pull量级已经超过1千万次。
Alpine的设计理念就是面向安全的发行版,相关的开发者也正在投入大量精力来实现这一理念,他们所采用的方法包括在内核中添加防御机制,以及使用现代的二进制保护机制来编译用户空间中的软件包(packet)。因为这些原因,我们在Twistlock中使用了Alpine。作为一名安全研究员,我决定深入研究下alpine的内部机理,寻找能够危及所有alpine用户的重大安全缺陷。
二、以apk为目标查找漏洞
许多工具共同组成了alpine发行版,其中我想好好研究一下的是apk,这也是alpine的软件包管理器。如果我能在软件包安装前就篡改它们的内容,或者让管理器降级某个软件包,那么我就能在目标系统上执行代码。
经过一些初步的研究,我最终决定尝试对apk中的某些部位进行模糊测试(fuzzing)。我对apk做了些修改,编译apk使其适用于afl工具(准确说来,我自己写了一个小程序,使用文件作为程序输入)。过去我曾使用afl成功挖掘出了一些零日漏洞,因此我觉得通过fuzz某些可能存在漏洞的函数,我也能找出apk中存在的漏洞。
我写的小程序主要侧重点在tar的解析代码上。Apk以gz压缩包(tar.gz)的形式接受更新文件,因此我抽出了接收tar流的那份代码,直接给它一个输入文件。这是一个完美的fuzz点,因为这个程序有很多代码用来解析用户的输入,并且我发现的所有崩溃点都会先于任何签名过程。相对比而言,在解析特定包时的崩溃点就不是特别重要,因为apk会对文件签名进行校验。
我的猜测是正确的。不到一天的时间,afl就发现了这个小程序中的大量崩溃点。我对这些崩溃点进行归类,然后通过调试手段定位具体漏洞。经过某些痛苦的尝试、解决某些错误之后,我在tar解析函数中找到两段可能导致堆溢出的代码。
三、漏洞分析
溢出漏洞存在于如下代码中(位于archive.c源文件中):
case 'L': /* GNU long name extension */
if (blob_realloc(&longname, entry.size+1)) goto err_nomem;
entry.name = longname.ptr;
is->read(is, entry.name, entry.size);
entry.name[entry.size] = 0;
offset += entry.size;
entry.size = 0;
break;
先来理解一下这段代码。这段代码来自于archive.c中的apk_parse_tar函数。这个函数接收来自于apk_istream的tar数据流,解析这段数据流,在解析出的每个数据段上调用一个回调函数。
通常情况下,一个tar数据流包含多个512字节的数据块,数据流开头为tar头部数据块,然后跟着若干个文件数据块。头部字段中有个字段为typeflag,用来表示数据流所包含的文件的类型。这个字段也用来表示某些数据块的用途,比如longname(或者“GNU长名扩展”)标识表示接下来的字节会包含所含文件的名称。如果文件名长度超过100字节就会使用这个标识。
因此,当解析器遇到一个longname数据块时,它会根据所给的大小分配缓冲区,然后从数据流中拷贝名字信息到缓冲区中。这个过程对应的缓冲区名称为longname,如果需要扩充缓冲区的大小,程序所使用的函数为blob_realloc。
blob_realloc函数的代码如下:
static int blob_realloc(apk_blob_t *b, int newsize)
{
char *tmp;
if (b->len >= newsize) return 0;
tmp = realloc(b->ptr, newsize);
if (!tmp) return -ENOMEM;
b->ptr = tmp;
b->len = newsize;
return 0;
}
问题在于这个函数会接受一个int类型的size参数,而int类型本来就是有符号类型。b->len为long类型,这表明这个变量也是有符号类型。因此,这两个变量的比较结果也是有符号类型。
你可能会好奇这样处理会有什么问题。你可以尝试一下当size超过0x80000000时的程序处理场景。对无符号整数(unsigned int)而言,0x80000000用十进制表示为2147483648,如果是有符号整数,这个值所对应的十进制数为-2147483648(即便在64位主机上使用gcc进行编译,int以及long通常情况下都会被编译为32位数值)。换句话说,当size比有符号整数的最大值还大时,程序会认为size是个负数,因此blob_realloc会返回0,没有修改缓冲区的内容。
随后程序会调用is->read,将大量字节拷贝到缓冲区中,这些字节大小超过了缓冲区所分配的大小,会覆盖堆上的后续数据。read函数认为size的类型为无符号类型,而对处理tar.gz的gzi_read函数而言(这个函数位于gunzip.c中),它期望得到一个size_t参数(无符号类型)。
简单地修改blob_realloc的定义,将参数类型从int改为size_t并不足以解决这个问题,因为当变量值比entry.size的最大值还大时(比如entry.size+1),还是会出现整数溢出问题,最终还是会导致缓冲区溢出,与之前的情况一样。
如果攻击者能够预测程序执行时的内存布局,那么他就可以利用这个缓冲区溢出漏洞来达到代码执行目的,具体来说,他可以覆盖堆上的函数偏移量,将其指向他所设置的任意函数来做到这一点。此时此刻,我并不想放出这个漏洞的PoC代码,在不久的将来,如果这个漏洞被修复,并且攻击者很难利用这个漏洞时,我就会放出PoC代码,同时介绍整个利用过程。
MITRE将这个漏洞编号为CVE-2017-9669。我们可以在代码中找到blob_realloc的另一处调用,这部分代码负责解析tar文件的pax头部信息,也存在类似的缓冲区溢出漏洞,这个漏洞的编号为CVE-2017-9671。
四、漏洞影响
攻击者可以通过各种方式利用这类缓冲区溢出漏洞。最直接的一种方法就是尝试利用这个漏洞实现目标系统上的代码执行。漏洞利用的唯一前提就是需要知道程序的内存布局信息。诸如ASLR(地址空间布局随机化)之类的保护机制可能会阻止攻击者利用这种漏洞,但攻击者还是有可能绕过这类保护机制完成代码执行。我会在后续的文章中介绍这方面内容。
在实际的攻击场景中,攻击者可能会搭建一个中间人形式的Alpine更新服务器来实施攻击。攻击者可以构造一个恶意的APKINDEX.tar.gz文件(Alpine的更新文件),托管于他自己的HTTP服务器上。如果网络中的某个用户在任何主机(包括容器)上运行“apk update”命令或者构建一个基于容器镜像的alpine版本(这需要调用其他命令),这些行为会引入攻击者的恶意文件,最终导致攻击者的恶意代码在受害者的主机上执行。攻击者可以隐藏他们的攻击行为,受害者可能永远都无法知道自己的主机已被攻陷。
最后谈谈版本信息。2.5.0_rc1之后所有版本的apk都会受到这两个漏洞的影响。我查看过老版本的代码,貌似老版本依然会受到类似问题的影响。因此我想说的是,所有版本的apk都会存在这类漏洞,但真正去检查非常老的那些版本是否存在漏洞意义不大。
五、尾声
我私底下将这个问题报告给了Alpine的开发者。Apk的经理Timo Teräs(传说中的一个人物)及时回复了我的邮件,与我一起讨论了这个问题,共同发布了一个快速补丁。
Timo还提到我们可以添加其他安全加固机制(如控制流完整性)来强化apk的安全等级,进一步限制攻击者利用这类漏洞的途径。
apk-tools 2.7.2、2.6.9版中包含了修复补丁,3.2-stable版之后的所有alpine版本做了相应的更新(3.3-stable版实际上是打上补丁的最新版本,但Timo还是更新了3.2版)。
你可以参考此链接了解我最初发布的安全公告,欢迎多提意见及建议。