漏洞分析(CVE-2018-10115)存在问题的代码位于solid compression处理流程中。solid compression的原理很简单:给定一组文件(比如来自于某个文件夹的一组文件),我们可以将这些文件当成一个整体,即单独的一个数据块,然后对整个数据块进行压缩(而不是单独压缩每一个文件)。这样可以达到较高的压缩率,特别是文件数非常多或类似情况时压缩率会更高。 在(版本5之前的)RAR格式中,solid compression的用法非常灵活:压缩文档中每个文件(item)都可以打上 显而易见的是,程序需要确保解码器对象在一开始时(从解码第一个item开始)就初始化其状态。我们来看一下7-Zip中的具体实现。RAR处理器中包含NArchive::NRar::CHandler::Extract这样一个方法,该方法在循环中通过一个变量索引遍历所有item。在这个循环中,我们可以找到如下代码: 这段代码的主要原理是使用 这个逻辑看上去没有问题,对吧?好吧,其实问题在于RAR支持3种不同的编码方法(版本5除外),每个item都可以使用不同的方法进行编码。更具体一点,这3种编码方法中每一种都存在不同的解码器对象。有趣的是,3种解码器对象的构造函数中并没有对一大部分成员进行初始化处理。这是因为对于非solid的item,其状态总是需要重新进行初始化,并且有一个隐含的前提,那就是解码器的调用者会确保首次调用解码器时使用 1、第一个item使用的是v1编码方法; 2、第二个item使用的是v2(或者v3)编码方法,并且设置了 第一个item会导致 乍看之下,这可能不是个大问题。然而,许多数据没经过初始化处理可能会被恶意利用,导致出现内存损坏: 1、保存堆上缓存大小的成员变量。这些变量现在保存的大小值可能比真实的缓冲区还要大,就会出现堆缓冲区溢出现象。 2、带有索引的数组,这些数组用来索引其他数组的读写操作。 3、PPMd状态。这些代码很大程度上依赖于模型状态的正确性,然而现在这个正确性很容易就会被破坏。 很显然,以上并没有覆盖所有的利用场景。
修复措施实际上这个漏洞的本质是程序无法确保在第一次使用解码器类之前正确初始化解码器类的状态。相反,在解码第一个item前,程序需要依赖调用者使用 解决这个漏洞可以采用两种不同的方法: 1、在解码器类的构造函数中正确初始化所有的状态。 2、在每个解码器类中添加一个额外的boolean成员变量: UnRAR貌似使用的是第一种方法,而Igor Pavlov选择使用第二种方法来修复7-Zip。
缓解漏洞利用Igor Pavlov 发布了7-Zip 18.01,该版本带有 然而,所有的主执行文件( 显然,只有当所有的模块都正确随机化后,ASLR才能发挥作用。他在新版的7-Zip 18.05中,让主执行程序使用 另外我想指出一点,7-Zip并不会分配或者映射其他可执行内存空间,因此可以作为Windows ACG(Arbitrary Code Guard)机制的保护目标。如果你使用的是Windows 10,我们可以在Windows Defender Security Center中添加7-Zip的主执行文件(
六、编写代码执行利用载荷通常情况下,我并不会花太多事件来思考如何开发武器化的利用技术。然而,如果我们想知道在给定条件下,编写漏洞利用代码需要花费多少精力,那么此时我们可以考虑实际动手试一下。 我们的目标平台为打上完整更新补丁的Windows 10 Redstone 4(RS4,Build 17134.1),64位操作系统,上面运行着64位版本的7-Zip 18.01。 挑选合适的利用场景使用7-Zip来解压归档文件时,我们主要可以采用3种方法: 1、通过GUI界面打开压缩文档,分别提取其中的文件(比如使用拖放操作)或者使用 2、右键压缩文件,在弹出的菜单种选择“7-Zip->Extract Here”或者“7-Zip->Extract to subfolder”。 3、使用命令行版本的7-Zip进行解压。 这三种方法都要调用不同的可执行文件( 第二种方法(通过上下文菜单解压文件)看起来吸引力最大,原因在于这可能是人们最常使用的方法,并且通过这种方法我们可以较为精确地预测用户的行为(不像第一种方法那样,人们会打开压缩文档,但选择提取“错误”的文件)。因此,我们选择第二种方法作为目标。 利用策略利用前面介绍的那个问题,我们可以创建一个Rar解码器,针对(大部分)未初始化的状态执行处理过程。我们来看一下哪个Rar解码器可以让我们以攻击者期望看到的效果来破坏内存。 一种可能的方法是选择使用Rar1解码器,其NCompress::NRar1::CDecoder::HuffDecode方法包含如下代码: 这一点非常有用,因为Rar1解码器的未初始化状态中包含 这样就让我们具有很好的读写利用条件。但是请注意,我们正处于64位地址空间中,所以我们不可能通过 为此我们可以使用一个Rar3解码器对象,与此同时我们也会使用该对象来保存我们的载荷。更具体一点,我们利用前面得到的读写条件将 我们将使用stack pivot技术( 在堆上放置一个替代对象为了成功实现既定策略,我们需要完全控制解码器的未经初始化的内存空间。大致做法就是分配大小为Rar1解码器对象大小的一段内存空间,将所需数据写入其中,然后在程序真正分配Rar1解码器空间之前先行释放掉这块内存。 显然,我们需要确保Rar1解码器所分配的空间的确重用了我们先前释放的同一块内存区域。想实现这个目标的一种直接方法就是激活相同大小的低碎片堆(Low Fragmentation Heap,LFH),然后使用多个替代对象来喷射LFH。这种方法的确行之有效,然而由于从Windows 8开始,在LFH分配空间会被随机化处理,因此使用这种方法再也不能让Rar1解码器对象与任何其他对象保持恒定的距离。因此,我们会尽量避免使用LFH,将我们的对象放置在常规堆上。整个空间分配策略大概如下所示: 1、创建大约18个待分配的空间,其大小小于Rar1解码器对象的大小。这样就会激活LFH,避免这类小空间分配操作摧毁我们干净的堆结构。 2、分配替代对象然后释放这个对象,确保该对象被我们前面分配的空间所包围(因此不会与其他空闲块合并)。 3、分配Rar3解码器(替代对象并没有被重用,因为Rar3解码器比Rar1解码器要大)。 4、分配Rar1解码器(重用替代对象)。 需要注意的是,在为Rar1解码器分配空间时,我们无法避免先分配一些解码器,这是因为只有通过这种方式, 如果一切按计划运行,Rar1解码器就会重用我们的替代对象,Rar3解码器对象在堆上将位于Rar1解码器对象之后,并且保持某个恒定的偏移距离。 在堆上分配并释放显然,如上分配策略需要我们能够以合理可控的方式在堆上分配空间。翻遍了RAR处理函数的所有源码,我无法找到很多较好的方法来对默认进程堆动态分配空间,以满足攻击者所需的大小要求并往其中存储攻击者可控的数据。事实上,完成这种动态分配任务的貌似只能通过压缩文档item的名称来实现。接下来我们看一下具体方法。 当程序打开某个压缩文档时,NArchive::NRar::CHandler::Open2方法就会读取压缩文档的所有item,具体代码如下(经过适当简化):
不幸的是,item的名称通过NArchive::NRar::CInArchive::ReadName来设置,代码如下: 这里我看到了一些困难,因为这意味着我们无法将任意字节为所欲为地写入缓冲区中。更具体一点,我们似乎无法写入null(空)字节。这一点非常糟糕,因为我们想放在堆上的替代对象中包含若干个0字节。那么我们该怎么办?让我们来看看AString::SetFrom: 如你所见,这个方法总是会以一个null字节来结束字符串。此外,我们发现只要字符串大小大于一定值, |