在本文中,我们将与读者一起深入考察解释型语言背后隐藏的攻击面。
简介
攻击面就像一层蛋糕。我们通常将软件攻击面定义为任何对攻击者控制的输入做出反应或受其影响的东西。在开发高级解释型语言应用程序时,很容易做出这样的假设:语言本身的运行时系统或代码库中的底层代码是可靠的。
经验证明,这种假设是错误的。
通常情况下,在高级语言的内存管理功能的实现代码中,往往存在着相对脆弱的基于C/C++的攻击面。这种问题可能存在于语言本身的核心实现中,也可能存在于将向高级语言提供基于C/C++的库的第三方语言生态系统中。
这些第三方库通常是通过显式外部函数接口(FFI)或其他形式的API转换包装器来提供其功能的,因为这些API转换包装器便于从较低级别代码中使用较高层的对象,或反之。它们通常被称为本机模块、扩展或FFI首字母缩写的某种形式,这样的攻击面的特点在于,在高级应用程序的安全上下文中,将与C/C++代码相关的各种内存管理漏洞都暴露给了攻击者。
在本系列文章中,我们将探讨在高级语言应用程序上下文中有时被忽略的C/C++攻击面的例子,其中,有些示例是很久之前的,有些是当前发现的。在第一部分中,我们将为解释语言的低级别攻击面提供相应的背景知识,并展示一些跨语言的安全漏洞。在以后的文章中,我们将介绍针对现代解释语言生态系统的新型攻击技术,以及哪些特征倾向于使这些表面上看是轻微的编程错误成为实际可利用的安全漏洞。
本系列文章主要面向希望在如何、为何以及在何处将攻击面暴露给潜在的恶意输入等方面做出明智决定的开发人员,因此,我们将在需要时对软件漏洞的利用理论给出相应的解释。
为了便于讨论,我们对软件漏洞利用的定义大致如下:利用输入内容的影响,将目标进程从其预期的状态空间转移到非预期的状态空间的过程。
摆放餐具
在判断某个代码问题“只是”一个软件bug,还是一个安全漏洞时,完全取决于相应的上下文。一个bug被判定为安全漏洞,应满足下列条件:它们应以某种状态、方式或形式帮助攻击者发动进攻。这本身是高度依赖于上下文的,尤其是在处理核心语言问题时。受影响的API如何以及在何处暴露于攻击者的输入,决定了是否可以将其视为安全漏洞。
在解释型语言的上下文中,存在两种主要的攻击方案。在第一种情况下,攻击者可以在目标解释器上运行其自己的程序,通常,他们的目标是破坏解释器本身的安全措施,以诱使托管解释器的进程跨越某种安全边界。
这样的示例包括Web浏览器使用的Javascript解释器中的安全漏洞。这个主题的变体包括以下场景:攻击者已在攻击的第一阶段获得了运行任意解释型代码的能力,但解释器本身实施了某种限制(例如:由于存在严格的PHP配置,使其无法使用命令执行功能),从而限制了他们进一步开展攻击的能力。
当攻击者对目标解释器拥有完全访问权限时,通常会在核心解释器的实现中查找漏洞。例如,在各种Javascript解释器中存在内存管理漏洞,通常能演化成Web浏览器中可利用的客户端漏洞。由于攻击者完全控制了解释器的状态,因此,从攻击者的角度来看,解释器本身的任何bug都可能是非常有用的。
在第二种情况下,攻击者可以利用解释型语言实现中的某些逻辑,向其提供输入,但无法直接与解释器进行交互。在这些情形中,攻击者的攻击范围受到他们实际可以直接或间接向其传递数据的API的限制。
当攻击者只能控制用高级语言实现的目标进程的输入时,他们就只能影响接收这些输入的逻辑,从而限制了他们的选择余地。在这些情况下,通过深入挖掘找出处理输入的底层攻击面,可以发现从较高级别逻辑角度看不到的漏洞。
从攻击者的角度来看,我们的主要目标是增加攻击面的深度。不要横着挖,而是往下挖!
剥开糖衣
关于解释型语言可以在较低级别进行利用的漏洞,已经由来已久。在本文中,我们不会完整的介绍这些漏洞的历史,但是,我们将深入研究一些有趣的例子——它们为我们展示了高级编程语言的bug是如何转化为底层编程语言的安全漏洞的——希望这些“老洞”能够激发读者新的灵感。
历史上的示例:当Perl格式化出错时
2005年的Perl代码中存在的格式字符串漏洞是一个有趣的案例。为了充分理解这个安全问题,我们首先要快速回顾一下C程序中格式字符串漏洞利用方面的基础知识。
C格式字符串漏洞简介
虽然许多人认为格式字符串错误由于易于检测而在很大程度上已被根除,但它们仍然不时出现在意想不到的地方。
从解释型语言与低级代码交互的上下文中考虑格式字符串错误也很有趣,因为它们可能会在较高级别上预处理攻击者控制下的格式字符串,然后直接传递给较低级别的格式化函数。这种延迟型格式化问题并不少见,尤其是当格式串的源和所述格式串的目的地之间存在强烈的逻辑分离时,特别容易出现这种问题。
简单地说,格式字符串错误是一类错误,其中攻击者将自己的格式字符串数据提供到格式化函数中,例如printf(attacker_controll)。然后,他们可以滥用对受控格式说明符的处理,以实现对目标进程空间的读写原语。
对此类漏洞的实际攻击主要依赖于滥用%n和%hn类格式说明符的能力。这些格式符会命令格式化函数将打印字符的当前运行计数分别写入整型(%n)或短整型(%hn)变量中,例如printf(“abcd%n”,&count)将通过指针参数将值4写入整型变量count中。
同样,在格式化函数的输出对攻击者可见的情况下,攻击者只需提供期望打印变量值(例如printf("%x%x%x%x"))的格式标识符,即可转储内存内容。当将预期的目标指针值与其%n个对应值对齐时,这种“吃掉”堆栈的能力也变得非常重要。
如果攻击者能够向格式化函数的调用堆栈提供受控数据(通常是通过恶意格式字符串本身来实现的),并禁用所有编译器缓解措施,则攻击者可以将对写入字符计数器的控制与对%n/%hn将写入的指针值的控制相结合,这样的话,他们就可以将自己控制的值写入指定的内存位置了。
通过使用诸如在格式说明符上设置精度/宽度等技巧,将写入字符计数器设置为特定值,并在支持的情况下设置直接参数访问索引,即使是少量的格式的字符串输入也能转化为攻击者强大的攻击原语。
C语言格式化函数中的直接参数访问(DPA)特性,允许我们指定用于格式标识符的参数的索引。例如printf("%2$s %1$s\n", "first", "second") 将打印“ second first”,因为第一个字符串格式标识符指定了参数 2 (2$) ,第二个字符串格式标识符指定了参数1(1$)。同样,从攻击者的角度来看,使用DPA可使您直接偏移到存放给定%n/%hn写入所需的目标指针值的堆栈位置。理解DPA的用途对于回顾历史上的Perl示例非常重要。
小结
通常情况下,在高级语言的内存管理功能的实现代码中,往往存在着相对脆弱的基于C/C++的攻击面。这种问题可能存在于语言本身的核心实现中,也可能存在于将向高级语言提供基于C/C++的库的第三方语言生态系统中。本文中,我们为读者介绍了与此紧密相关的C格式字符串漏洞方面的知识,在下一篇文章中,我们将为读者介绍这些底层实现是如何影响解释型语言的安全性的。