8

深入考察解释型语言背后隐藏的攻击面,Part 2(一)

 3 years ago
source link: http://netsecurity.51cto.com/art/202012/637978.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

接上文:

在本系列关于解释型语言底层攻击面的第一篇文章中,我们了解到,即使在Javascript、Python和Perl等解释型语言的核心实现中,内存安全也不是无懈可击的。

m6JFf2v.jpg!mobile

在本文中,我们将更加深入地探讨,在通过外部函数接口(Foreign Function Interface,FFI)将基于C/C++的库“粘合”到解释语言的过程中,安全漏洞是如何产生的。正如我们之前所讨论的,FFI充当用两种不同语言编写的代码之间的接口。例如,使一个基于C语言的库可用于Javascript程序。

FFI负责将编程语言A的对象翻译成编程语言B可以使用的东西,反之亦然。为了实现这种翻译,开发人员必须编写特定于语言API的代码,以实现两种语言之间的来回转换。这通常也被称为编写语言绑定。

从攻击者的角度来看,外部语言绑定代表了一个可能的攻击面。当处理一个从内存安全语言翻译成内存不安全语言(如C/C++)的FFI时,开发者就有可能引入内存安全漏洞。

即使高层的语言被认为是内存安全的,同时目标外部代码也经过了严格的安全审查,但是,在两种语言之间架起桥梁的代码中,仍可能潜伏着可利用的漏洞。

在这篇文章中,我们将仔细研究两个这样的漏洞,我们将一步步地了解攻击者如何评估你的代码的可利用性的。本文的目的是提高读者对exploit开发过程的理解,而不仅仅是针对一个具体的案例,而是从概念的角度来理解。通过了解exploit开发人员如何思考您的代码,帮您建立防御性的编程习惯,从而编写出更安全的代码。

在我们的案例研究中,我们将考察两个看起来非常相似的bug,然而只有一个是bug,而另一个则是一个安全漏洞。两者都存在于绑定Node.js包的C/C++代码中。

node-sass

Node-sass是一个库,它将Node.js绑定到LibSass(一款流行的样式表预处理器Sass的C版本)。虽然node-sass最近被弃用了,但它每周仍有500万次以上的下载量,所以,它是一个非常有价值的审计对象。

当阅读node-sass绑定时,我们注意到以下代码模式:

 int indent_len = Nan::To 
    Nan::Get( 
        options, 
        Nan::New("indentWidth").ToLocalChecked() 
    ).ToLocalChecked()).FromJust(); 
  
  
[1] 
  ctx_w->indent = (char*)malloc(indent_len + 1); 
  
  
  strcpy(ctx_w->indent, std::string( 
[2] 
    indent_len, 
    Nan::To 
        Nan::Get( 
            options, 
            Nan::New("indentType").ToLocalChecked() 
        ).ToLocalChecked()).FromJust() == 1 ? '\t' : ' ' 

在[1]处,我们注意到一个受控于用户输入的32位整数值被用于内存分配。如果该用户提供的整数为-1,则整数算术表达式indent_len + 1的值将变成0。在[2]处,原始负值用于创建由indent_len字符组成的制表符或空格字符串,其中indent_len值为负,现在将变成一个相当大的正值,因为std::string构造函数期望接收无符号的长度参数,其类型为size_t。

在JS API级别,我们注意到indentWidth的检索方式如下所示:

/** 
 * Get indent width 
 * 
 * @param {Object} options 
 * @api private 
 */ 
  
  
function getIndentWidth(options) { 
  var width = parseInt(options.indentWidth) || 2; 
  
  
  return width > 10 ? 2 : width; 
} 

此处的目的是确保indentWidth >= 2或 <= 10,但实际上这里仅检查了上界,并且parseInt允许我们提供负值,例如:

var sass = require('node-sass') 
var result = sass.renderSync({ 
        data: `h1 { font-size: 40px; }`, 
        indentWidth: -1 
}); 

这将触发一个整数溢出,从而导致分配的内存不足,并进一步导致后续的内存被破坏。

为了解决这个问题,node-sass应该确保在将用户提供的indentWidth值传递给底层绑定之前,先检查该值的下界和上界。

全面地检查输入,并明确地将它们的取值范围限制在对程序逻辑有意义的范围内,这将很好地帮助您养成一种通用的防御性编程习惯。

所以我们来总结一下。这里的bug模式是什么?整数溢出,导致堆分配不足,其后的内存填充可能会破坏相邻的堆内存。听起来确实值得分配CVE,不是吗?

然而,虽然这个整数溢出确实会导致堆内存分配不足,但这个bug并不代表就是一个漏洞,因为这个样式表输入很可能不是攻击者控制的,并且在任何堆破坏发生之前,都会抛出std::string异常。即使发生了堆损坏,也只是一个非常有限的控制覆盖(借助于一个非常大的indent_len的制表符或空格字符),所以,实际被利用的可能性很低。

anticomputer@dc1:~$ node sass.js 
terminate called after throwing an instance of 'std::length_error' 
  what():  basic_string::_S_create 
Aborted (core dumped) 

结论:只是一个bug。

那么,什么情况下攻击者才会对这样的bug感兴趣呢?攻击者能够对触发bug的输入施加影响。在这种情况下,不太可能有人为node-sass绑定提供受控于攻击者的输入。同时,内存破坏原语本身的控制能力也会非常有限。虽然确实存在这样的情况:即使是非常有限的堆损坏也足以充分利用某个缺陷,但通常攻击者会更乐于寻求具有某些控制权的情形,比如可以控制用于破坏内存的东西,或者可以控制覆盖的内存数量。最好是两者兼而有之。

在这种情况下,即使std::string构造函数没有退出,攻击者也必须用空格或制表符进行大规模的覆盖,以控制进程。虽然这并非完全不可能,但考虑到对周围内存布局的足够影响和控制,可能性仍然偏低。

在这种情况下,我们通常可以通过回答下面的三个问题,来进行一个简单的可利用性“嗅觉测试”:

  • 攻击者是如何触发这个bug的?
  • 攻击者控制了哪些数据,控制到什么程度?
  • 哪些算法受到攻击者控制的影响?

除此之外,可利用性主要取决于攻击者的目标、经验和资源。这些我们可能一无所知。除非您花了很多时间实际编写exploit,否则很难确定某个问题是否可利用。特别是当您的代码被其他软件使用时,即您编写的是库代码,或者是一个更大系统中的一个组件。在一个孤立的环境中,某个错误看起来只是bug,在更大的范围内可能就是安全漏洞。

虽然常识对于确定可利用性有很大的帮助,但在时间和资源允许的情况下,任何可以由用户控制(或影响)的输入触发的bug都是潜在的安全漏洞,因此,将其视为安全漏洞是非常明智的做法。

png-img

对于我们的第二个案例研究,我们将考察GHSL-2020-142。这个bug存在于提供libpng绑定的node.js png-img包中。

当加载PNG图像进行处理时,png-img绑定将使用PNGIMG::InitStorage函数来分配用户提供的PNG数据所需的初始内存。

void PngImg::InitStorage_() { 
    rowPtrs_.resize(info_.height, nullptr); 
[1] 
    data_ = new png_byte[info_.height * info_.rowbytes]; 
  
  
[2] 
    for(size_t i = 0; i < info_.height; ++i) { 
        rowPtrs_[i] = data_ + i * info_.rowbytes; 
    } 
} 

在[1]处,我们观察到为一个大小为info_.height * info_.rowbytes的png_byte数组分配了相应的内存。其中,结构体成员height和rowbytes的类型都是png_uint_32,这意味着这里的整数算术表达式肯定是无符号32位整数运算。

info_.height可以直接作为32位整数从PNG文件提供,info_.rowbytes也可以从PNG数据派生。

这种乘法运算可能会触发整数溢出,导致data_内存区域分配不足。

例如,如果我们将info_.height设置为0x01000001,而info_.rowbytes的值为0x100,那么生成的表达式将是(0x01000001 * 0x100) & 0xffffffff ,其值为0x100。这样的话,data_将作为一个0x100大小的png_byte数组来分配内存,这明显不够用。

随后,在[2]处,将使用行数据指针填充rowPtrs_array,这些指针指向所分配的内存区的边界之外,因为for循环条件是对原始的info_.height值进行操作的。

一旦实际的行数据被从PNG文件中读取,任何与data_区域相邻的内存都可能被攻击者控制的行数据覆盖,最高可达info_.height * info_.rowbytes字节,这给任何潜在的攻击者提供了大量可控的进程内存。

需要注意的是,根据攻击者的意愿,可以通过不从PNG本身提供足够数量的行数据来提前停止覆盖,这时libpng错误例程就会启动。任何后续处理错误路径的程序逻辑都会在被破坏的堆内存上运行。

这很有可能导致一个高度受控(无论是内容还是大小)的堆溢出漏洞,我们的直觉是,这个bug可能是一个可利用的安全漏洞。

下面,让我们来回答可利用性问题,以确定这个bug是否对攻击者具有足够的吸引力。

攻击者是如何触发该bug的?

这个bug是由攻击者提供的PNG文件触发的。攻击者可以完全控制在png-img绑定中作用于PNG的任何数据,并废除文件格式完整性检查所施加的任何限制。

因为攻击者必须依赖于加载的恶意PNG文件,我们可以假设任何利用逻辑都可能必须包含在这个单一的PNG文件中。这意味着,攻击者与目标Node.js进程反复交互的机“可能”更少,例如,实施信息泄露,以帮助后续的漏洞利用过程绕过任何系统级别的缓解措施,如地址空间布局随机化(ASLR)。

我们说“可能”,是因为我们无法预测png-img的实际使用情况。换句话说,也可能存在这样的使用情况:存在可重复的交互机会,来触发该bug或进一步帮助利用该bug。

攻击者能够控制哪些数据,控制到什么程度?

攻击者可以提供所需的height和rowbytes变量,以便对整数运算和后续的整数封装(integer wrap)进行精细控制。被封装的值用于确定data_数组的最终分配内存的大小。它们也可以通过PNG图像本身提供完全受控的行数据,这些数据通过rowPtrs数组中的越界指针值填充到越界内存中。他们可以通过提前终止提供的行数据,精细控制这个攻击者提供的行数据有多少被填充到内存中。

简而言之,攻击者可以通过精细控制内容和长度来覆盖任何与data_相邻的堆内存。

哪些算法会受到攻击者控制的影响?

由于我们处理的是堆溢出,攻击者的影响扩展到任何涉及被破坏的堆内存的算法。这可能涉及Node.js解释器代码、系统库代码,当然还有绑定代码和任何相关库代码本身。

小结

在本文中,我们将深入地探讨,在通过外部函数接口(Foreign Function Interface,FFI)将基于C/C++的库“粘合”到解释语言的过程中,安全漏洞是如何产生的。由于篇幅过长,我们将分为多篇进行介绍,更多精彩内容,敬请期待!

本文翻译自:​https://securitylab.github.com/research/now-you-c-me-part-two

【责任编辑:赵宁宁 TEL:(010)68476606】


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK