攻击面是一层蛋糕。我们通常将软件攻击面定义为对攻击者控制的输入做出反应或受其影响的任何东西。在开发高级解释语言应用程序时,很容易假设运行时系统或语言本身的库中的低级代码是健全的。
通常情况是,在高级语言的内存管理奢侈品之下,存在相对脆弱的基于C/C++的攻击面。这些问题可能存在于语言本身的核心实现中,也可能存在于将基于C/C++的库功能暴露给高级语言的第三方语言生态系统中。
这样的第三方表面通常通过显式外部函数接口(FFI)或某种其他形式的API转换包装器来公开,这些API转换包装器便于向低级代码和从低级代码使用高级对象。通常称为本机模块、扩展或FFI首字母缩写的某种形式,这样的攻击面暴露了与更高级别应用程序的安全上下文中的C/C++代码相关联的所有内存管理不善漏洞。
在这一系列帖子中,我们将探索一些在高级语言应用程序环境中有时被忽视的C/C++攻击面的历史和当前示例。在第一部分中,我们将提供解释型语言的低级攻击面的历史背景,并演示其许多漏洞主题跨越语言。在以后的文章中,我们将介绍针对现代解释型语言生态系统的新攻击,以及哪些特征倾向于使这些表面上的bug成为实际可利用的漏洞。
本系列主要面向那些希望做出明智决策的开发人员,他们如何、为什么以及在哪里将受攻击面暴露给潜在的恶意输入,因此,它将在需要时包括软件利用理论的解释。
出于本讨论的目的,我们广义地定义软件开发如下:利用输入作为影响,将目标进程从其预期状态空间旋转到非预期状态空间。
在决定某事是错误还是可以被认为是漏洞时,上下文决定一切。要被认为是漏洞,缺陷应该以某种形式、方式或形式推进攻击者的议程。这本身高度依赖于上下文,特别是在处理核心语言问题时。受影响的API向攻击者输入公开的方式和地点主要决定了它是否可以被视为漏洞。
在解释型语言的上下文中,存在两个主要的攻击场景。在第一种情况下,攻击者能够在目标解释器上运行他们自己的程序,通常他们的目的是破坏解释器本身的安全保证,以诱使承载解释器的进程越过某种安全边界。
这方面的示例包括Web浏览器使用的Javascript解释器中的漏洞。此主题的变体包括这样的场景:攻击者在攻击的第一阶段实现了运行任意解释代码的能力,但解释器本身实施了某种限制,限制了攻击者进一步攻击的能力,例如不允许使用命令执行函数的限制性PHP配置。
当攻击者拥有对目标解释器的完全访问权限时,通常会在核心解释器实现中查找漏洞。在各种Javascript解释器中存在导致Web浏览器中可被利用的客户端漏洞的内存管理错误的历史由来已久。由于攻击者完全控制解释器状态,因此从攻击者的角度来看,解释器本身中的任何错误都可能是有益的。
在第二个场景中,存在一些用解释语言实现的逻辑,攻击者可以向这些逻辑提供输入,但不能直接与解释器交互。在这些情况下,攻击者的表面覆盖范围受到他们实际可以直接或间接传递数据的API的限制。
当我们只控制对用高级语言实现的目标进程的输入时,我们受到输入可以影响的逻辑的限制,这通常会限制攻击者的选择。在这些场景中,深入挖掘以公开处理您的输入的任何较低级别的表面,可以发现从较高级别的逻辑角度看不到的漏洞。
从攻击者的角度来看,我们的主要目标是增加攻击面的垂直度。如果你不能走得更宽,就走得更深!
解释语言漏洞有着悠久而有趣的历史,这些漏洞在较低级别上是可以利用的。完整的历史不在这篇文章的讨论范围之内,但我们将深入研究一些有趣的例子的细节,这些例子突出了高级错误是如何变成低级漏洞的,因为对旧的理解通常可以启发新的。
与Perl代码anno2005相关的格式字符串漏洞案例是一个有趣的案例。要充分理解存在的问题,我们必须首先快速回顾一下在C程序中利用格式字符串的基础知识。
虽然许多人认为格式字符串错误由于易于检测而在很大程度上已被根除,但它们仍然倾向于不时出现在意想不到的地方。
从解释语言与低级代码交互的上下文中考虑格式字符串错误也很有趣,因为它们可能会在较高级别对受攻击者影响的格式字符串进行预处理,然后将其直接传递给低级格式化函数。这种延迟格式化问题并不少见,尤其是当格式串的源和所述格式串的目的地之间存在强烈的逻辑分离时,特别具有悄悄进入的习惯。
简而言之,格式字符串错误是攻击者将其自己的格式字符串数据提供给格式化函数(例如printf(Attner_Control))的一类错误。然后,它们可以滥用对其受控格式说明符的处理来实现对目标进程空间的读写原语。
对此类漏洞的实际攻击主要依赖于滥用%n和%hn类格式说明符的能力。这些格式化程序指示格式化函数将打印字符的当前运行计数分别写入整型(%n)或短型(%hn),例如printf(";abcd%n";,&;count)将通过指针参数将值4写入整数计数。
同样,在格式化函数的输出对攻击者可见的情况下,攻击者只需提供期望打印变量值的格式化程序,例如printf(";%x%x";),即可转储内存内容。当将预期的目标指针值与其%n对应值对齐时,这种吃掉堆栈的能力也变得很重要。
如果攻击者能够向格式化函数的调用堆栈提供受控数据(通常通过恶意格式字符串本身实现),并阻止任何编译器缓解,则攻击者可以将对写入字符计数器的控制与对%n/%hn将写入的指针值的控制相结合。这将导致能够将攻击者控制的值写入攻击者指定的内存位置。
通过使用诸如在格式说明符上设置精度/宽度等技巧将写入字符计数器设置为特定值,并在支持的情况下设置直接参数访问索引,即使是小格式字符串输入也可能成为攻击者强大的攻击基元。
C格式化函数中的直接参数访问(DPA)允许您指定要用于格式化程序的参数索引。例如,printf(";%2$s%1$s\n";,";First";,";Second";)将打印";第二个第一";,因为第一个字符串格式化程序指定参数2(2$),第二个字符串格式化程序指定参数1(1$)。同样,从攻击者的角度来看,使用DPA允许您直接偏移到包含给定%n/%hN写入所需目标指针值的堆栈位置。了解DPA的目的非常重要,因为我们将重点放在历史悠久的Perl示例中。
解释型语言提供自己的格式化函数并不少见,具体地说,Perl通过其较低级别的perl_sv_vcatpvfn函数提供格式化支持。这个低级C API为高级Perl API提供了大部分核心格式化支持。它的格式化支持在语法上有点类似于C格式化支持,因为它支持直接参数访问的概念,在Perl中称为精确格式化索引,以及%n类格式化程序。
当您考虑到存在明显易受格式字符串错误攻击的基于Perl的远程服务应用程序时,理解Perl的内置格式化支持变得很有趣。然而,由于没有办法在Perl级别直接利用这些漏洞,安全研究团体并没有投入太多精力来试图利用这些问题,它们通常被认为只是一个漏洞。
大约在2005年,在CVE-2005-3962(Jack Louis)的作者请求建立可利用性之后,我对Perl格式字符串利用进行了更深入的研究。在Webmin中测试他的Perl格式字符串错误发现之一时,他在Perl解释器中遇到了一些明显的崩溃。
事实证明,是的,您确实可以通过其在Perl_SV_vcatpvfn中的格式化支持的C级实现来利用基于Perl的格式字符串错误。
Perl的格式字符串的参数存储在参数结构指针数组(称为svargs)中,当提供格式说明符的精确格式索引(例如%1$n)时,该索引随后用于从参数数组中检索适当的参数结构指针。当从数组中检索关联的参数结构指针时,Perl将根据格式字符串可用的参数数量,确保提供的索引不会超过数组的上限。此参数计数维护为带符号整数svmax。即,如果向格式字符串传递了1个参数,则svmax将具有值1,并且精确的格式索引值被检查为不超过1。在攻击者提供的格式字符串的情况下,不存在参数,并且svmax为0。
但是,精确的格式索引也保留为带符号的32位整数,其值完全由攻击者提供的格式字符串控制。这意味着您可以将此参数数组索引设置为负值,这将针对svmax通过带符号的上限检查。
有了这一认识,剥削变得相当简单,特别是在2005年。您可以简单地在svargs数组下面编制索引,指向任何指向攻击者控制的数据的指针。该攻击者控制的数据将被解释为包含指向值字段的指针的参数结构。与熟悉的%n格式化程序相结合,这将导致对受控位置的受控写入。使用这样的写原语,然后可以重写任何可写进程存储器的内容,这些内容可以以各种方式被利用到完全进程控制中。
这是一个很好的例子,说明了在Perl格式化实现的掩护下相对简单地将bug变成漏洞的一个很好的例子。再加上Webmin中的格式字符串漏洞,这会导致针对Webmin的完全远程代码执行(RCE)攻击。
我们的结论是,任何时候,只要在较高级别的语言级别上看起来像只是一个bug,评估对错误输入的较低级别的处理是有益的,因为可能存在升级到充分利用的直接途径,即使之前已有共识认为这样的问题实际上是不可利用的。
从攻击者的角度来看,PHP解释器在其许多版本中都有一段有趣的历史。通常会从完全解释器控制角度(攻击者能够执行任意PHP代码)和远程API输入角度(攻击者可以向潜在易受攻击的PHP API提供恶意输入)进行攻击。
利用PHP解释器的一个更有趣的示例是非序列化攻击类。我们的讨论很有趣,因为攻击者已经在PHP逻辑级和核心解释器级攻击了PHP的非序列化API。
通常认为,取消序列化不受信任的用户提供的数据不是一个好主意。远程应用程序上下文中的任意对象膨胀可能会导致相对简单的任意PHP执行,具体取决于应用程序名称空间中可用和允许的类。这个主题超越了语言界限,我们看到几乎所有支持反序列化的语言和应用程序框架都在积极利用相同的概念。
在攻击者实现任意PHP执行后,他们可能会发现自己受到受限解释器配置的限制,在这一点上,他们通常会探索解除这些限制的方法。历史上流行的一种选择是滥用PHP解释器本身中的错误。这类攻击的最新示例可在https://bugs.php.net/bug.php?id=76047中找到,其中可利用DEBUG_BACKTRACE()函数中的释放后使用(Uaf)漏洞完全控制PHP解释器本身并取消任何配置限制。
有时,即使使用受控的PHP反序列化原语,攻击者也可能无法将其转换为任意PHP执行,因为缺乏可用的类洞察力或对应用程序命名空间的其他限制。这就是深入底层代码层再次成为可行策略的地方。
PHP的非序列化API中存在大量的内存管理不善问题,通常它是模糊化和研究解释器漏洞的热门目标。
通过在非序列化API的解释器实现级别利用此类内存管理不善问题,决心坚定的攻击者能够将原本无法利用的漏洞转变为完全可利用的漏洞。存在许多利用该表面远程利用PHP应用程序的实际攻击和现实世界攻击的示例。
一个相对较新和相关的例子是Ruslan Habolov写的一篇出色的文章,描述了他们如何利用低级PHP解释器错误和高级PHPAPI交互的组合,针对一个备受瞩目的现实世界目标进行全面的RCE。
PHP反序列化在较高和较低级别实现的混合攻击面的情况是解释语言垂直攻击面的另一个很好的例子,以及它是如何被坚定的攻击者协同滥用的。
就本文而言,我们的第三个也是最后一个例子是CVE-2014-1912年的案例。此漏洞存在于Python的socket.recvfrom_into函数中。
在Python2.5中引入的socket.recvfrom_into的目标用途是将数据接收到指定的Python字节数组中。但是,它缺乏明确的检查来确保接收数据的目标缓冲区实际上足够大,可以容纳指定数量的传入数据。
Diff-r e6358103fe4f Modules/socketmodule e.c-a/Modules/socketmodule.c Wed Jan 08 20:44:37 2014-0800+b/Modules/socketmodule.c Sun Jan 12 13:21:19 2014-0800@@-2877,6+2877,14@@recvlen=buflen;}+/*检查缓冲区是否足够大*/+(buflen&l。+}+readlen=sock_recvfrom_guts(s,buf,recvlen,flag,&;addr);if(readlen<;0){PyBuffer_Release(&;pbuf);
要真正易受攻击,应用程序必须显式尝试读取比通过大于目标字节数组的大小参数分配给它的数据更多的数据到字节数组中。如果未提供size参数,则该函数将默认为bytearray本身的大小,并且不会发生内存损坏。
如果您的开发背景是您自己负责内存管理的语言,那么这听起来可能很熟悉。你甚至可能认为,任何一个头脑正常的人都不会做这样的蠢事。显然,您不应该读取比目标缓冲区中可用的数据更多的数据,对吗?
但这正是为什么这是一个有趣的案例。这并不是因为这种脆弱性在现实世界中普遍存在。这很有趣,因为内存管理语言的开发人员倾向于显式地信任语言实现来保证它们的安全。当语言中存在诸如CVE-2014-1912之类的问题时,可能会出现认知不和谐。
Python开发人员可能完全期望能够将s.recvfrom_into(bytearray(256),512)转到(bytearray(256),512)中,而他们的Python解释器不会受到内存损坏的影响。事实上,如果您尝试这个补丁后版本,它现在的行为可能与人们预期的一样:
回溯(最近一次调用):文件";<;stdin>;";,第1行,<;模块>;ValueError:n字节大于>;>;>;缓冲区的长度。
因此,本质上的漏洞是,在一种在很大程度上假定内存安全的语言中,该函数不是内存安全的。但对于一名C程序员来说,CVE-2014-1912主要读起来像是这样的,是的,这就是它的工作原理,不是吗?
这给我们上了一课,即使在宣传内存安全的高级语言中,安全内存管理语义的假设也从来都不是给定的。在处理显式操作静态大小可变缓冲区的API时,确保您的大小与您的缓冲区匹配是不会有什么坏处的,即使是在假定不这样做是安全的情况下,因为语言本身的原因也是如此。
从攻击者的角度来看,对于通常认为是内存安全的API实际上根本不是内存安全的情况进行审计是很有用的。
在我们关于隐藏的C/C++攻击面的系列文章的第一部分中,我们探索了几个实际示例,说明内存安全假象如何欺骗开发人员轻而易举地处理他们接受到应用程序中的输入。
这个主题的变体可以并且确实存在于任何解释器API中,这些解释器API可以从更高级别访问,其核心是用内存不安全语言实现的。这些问题实际上是否可利用通常取决于开发人员在其输入方面为攻击者提供了多大的回旋余地。
开发人员对其输入类型、大小和值范围提出严格要求的开发人员通常会挫败利用漏洞-例如,当接收整数值时,将该值的范围显式限制到应用程序上下文中有意义的值,而不是将其保留在变量类型本身的值范围内,这是一种防御性的编程习惯,对您很有帮助。
在本系列的下一篇文章中,我们将更深入地研究现代C/C++解释型语言的攻击面,重点关注流行的解释型语言框架的第三方库生态系统,并介绍该领域的一些新攻击。