使用QL和LGTM进行变异分析

lucywang Web安全 2019年9月18日发布
Favorite收藏

导语:在软件开发中,我们经常看到相同的代码错误在项目的生命周期中反复出现。这些相同的错误甚至会出现在多个项目中。有时,这些错误同时有多个活动实例,有时一次只有一个活动实例,但是它们不断地重新出现。当这些错误导致安全漏洞时,后果可能相当严重。

在软件开发中,我们经常看到相同的代码错误在项目的生命周期中反复出现。这些相同的错误甚至会出现在多个项目中。有时,这些错误同时有多个活动实例,有时一次只有一个活动实例,但是它们不断地重新出现。当这些错误导致安全漏洞时,后果可能相当严重。

在本文中,我将解释如何使用QL的变异分析(variant analysis)来解决这个问题,并演示如何使用LGTM对你自己的项目进行变异分析。注意:本文使用的是真实存在的systemd示例。

相同的错误重复出现是一种正常现象吗?

在进行变异分析之前,我想先举个例子。在有很多引人注目的案例中,相同的漏洞一再出现。以Google的Project Zero的Tavis Ormandy发现的漏洞为例:

1.png

我们可以从这篇评论中看到,这不是Tavis在Ghostscript中发现的第一个漏洞。Ghostscript是一套建基于Adobe、PostScript及可移植文档格式(PDF)的页面描述语言等而编译成的免费软件。上图是他正在检查之前报告的一个漏洞的修复程序,发现其并没有被正确地修复。

现在让我们按着上面的思路找到issue 1690

2.png

我们看到的又是一条关于先前漏洞的注释!

如果我们按着这个思路继续往下,漏洞则会越来越多!

3.png

同样的错误在代码库的不同部分一次又一次的重复被发现。每次修复时,都会发现有新的变异出现。

这意味着,许多漏洞的原始状态都是出不多的,只不过是随着时间的推移,它们通常以稍微修改过的形式或变异形式,出现在各个开发项目中,给人感觉漏洞有很多类,其实不然。

什么是变异分析?

每当发现安全漏洞时,无论是通过安全研究、代码评审、漏洞赏金计划还是通过其他流程,我们都应该先调查漏洞重复的频率,确定代码库中是否存在其他类似的漏洞,并建立一个流程机制来防止漏洞再次发生。

4.png

第一步是诊断哪里出了问题,以便我们能够找到并修复漏洞。但是,如上所述,这通常是不够的,因为大多数漏洞并不是以一成不变的形式出现的。这时,就需要变异分析发挥作用了。

变异分析是获取现有漏洞、查找根本原因以及在代码库中搜索变异漏洞的过程。按着传统观念,这个过程应该是由安全研究人员而不是开发人员完成的,但正如我将要在下面解释的那样,不一定总是这样。重要的是找到所有这些变异并同时修补它们,否则这些漏洞可能会在野外被利用。一旦找到所有的变异,最后一步是确保类似的漏洞不再出现。

我们如何找到变异?

目前使用的最常见的自动化安全研究技术通常包括回归测试、单元测试和模糊测试。这些测试方法可以帮助保护代码库,但这些方法在查找已知漏洞的变异对象时,却不是最优的。除了自动化之外,你还可以手动检查变异代码。然而,对于任何合理大小的项目(或项目的集合),手动梳理整个代码库是完全不可行的。此外,手动过程不会让我们深入了解可能存在的漏洞,同时还容易出现人为错误。

开发人员在尝试保护代码时面临的另一个问题是,他们通常缺乏某些安全知识,或者不了解漏洞如何被利用。而这种专业知识和工具通常只能在专业公司的安全团队的成员中找到,因此开发人员不得不完全依赖专业公司的安全团队来获取这些信息,这也造成了对项目的安全状态无法及时有效的控制。例如,在处理开源项目时,开发人员可能没有对整个代码库的背景进行完整的了解,很容易无意中引入导致安全漏洞的代码。

幸运的是,有一种可用的资源可以自动完成许多繁重的变异分析工作,无需手动梳理源代码,就可以共享安全知识和最佳实战经验。更重要的是,这种技术对于开源项目是完全免费的。

先来看看LGTM背后的技术QL。QL采用了一种全新的方法来分析代码,该方法会将代码视为数据。首先,将这些代码放入一个特殊的关系数据库,分析代码之间的关系。你可以查询这个数据库,查到这些代码从基本语法模式到数据流的全部分析情况,而这些代码的关联关系以前是不可能被自动检测到的。这就允许开发人员利用QL自动化和扩展变异分析。基本分析完成之后,你就可以通过再次编写和修改查询,以发现语义上与原始漏洞相似的代码模式。搜寻出来的任何结果都会被进行分类,并提供给开发团队来实现修复。除此之外,每个查询都可以放在一个中央存储库中,以便与组织内外的其他人共享。然后,这些查询将连续运行,以便在新变异可能导致漏洞之前捕获它们。

如何在LGTM上使用QL ?

如果你使用的是LGTM,那么这意味着你已经在使用QL了! LGTM提供了项目源代码最新版本的警报视图,该视图是通过对该代码库运行QL的标准查询生成的。这些查询都是开源的,并且不断更新,这样你就可以从客户、内部专家和QL的其他用户的安全团队获取共享的知识。你还可以使用LGTM的自动代码审查集成来为每个pull请求运行这些查询,并在合并之前捕获问题。除此之外,你还可以将自己的特殊查询添加到存储库中,并让LGTM将它们与标准查询一起运行。

让我们看一个例子,看看一个著名的开源项目如何利用所有这些功能,让LGTM作为变异分析平台的。

在2018年10月中旬,谷歌的Jann Horn发现了systemd的一个漏洞,systemd是许多Linux发行版的核心部分。

5.png

研究发现,当通过fgets()进行行分割时,systemd很容易受到状态注入攻击(state injection attack)。由于systemd是一个开源项目,主要开发人员需要一种方法来警告开源代码的使用者不要使用fgets()的任何功能。为此,他们在CODING_STYLE文档的末尾添加了一段说明。然而……

6.png

正如systemd的维护者所指出的,他们的CODING_STYLE文档末尾的注释说明不足以阻止贡献者对fgets()的使用。为了改善这种情况,systemd团队决定在LGTM上的项目中添加一个定制QL查询,以捕获对fget()的任何使用。

import cpp

predicate dangerousFunction(Function function) {
  exists (string name | name = function.getQualifiedName() |
    name = "fgets")
}

from FunctionCall call, Function target
where call.getTarget() = target
  and dangerousFunction(target)
select call, target.getQualifiedName() + " is potentially dangerous"

该查询会检查是否有人对fgets()进行调用,然后为每个调用显示一个LGTM警告,说明这些函数有潜在的危险。

通过编写自定义查询,systemd的开发人员能够轻松地跟踪代码库中fgets()的所有用法。首先,他们在LGTM的交互查询控制台中运行查询以检查结果。然后,他们将查询添加到存储库中,以便LGTM将其与所有标准查询一起运行,从而允许他们持续监控对systemd代码库中fgets()的调用。在创建此查询后不久,如果我们在其中一个提交中查看src/udev/udev-rules.c,就可以看到这些警报正在启动。

8.png

通过使用LGTM作为一个持续集成工具,在将请求发送到主分支之前自动审查其中的代码,systemd团队能够阻止其他开发人员在他们的代码中意外地重新引入对fgets()的调用。这就是QL和LGTM真正的价值所在,即使你没有自己的专用安全工程师团队,你也可以利用它们,为项目提供持续的监控和可扩展的变异分析。

虽然这个查询非常简单,但事实证明它对systemd团队非常有帮助。使用QL,可以编写更高级的查询来解决更具挑战性的问题。接下来,我们将更深入地研究QL,带你从头开始编写你自己的查询,并演示如何使用它来修改你自己项目的现有查询。

变异分析是获取一个已知问题或漏洞样本,并在代码库中找到该漏洞的其他实例的过程。接下来,我将展示如何使用既有的漏洞样本来编写和改进QL查询。我们将试图找到的这类漏洞是snprintf的潜在使用漏洞,snprintf已成为热门项目中许多CVE的来源,包括rsyslog (CVE-2018-1000140)和icecast (CVE-2018-18820)。在开始编写查询之前,让我们先了解一下要使用的技术。

QL

QL语言是一种高级的、面向对象的逻辑语言,它支持Semmle的所有库和分析。使用QL,你可以快速执行变异分析,以发现以前未知的安全漏洞。

Semmle QL附带了大量的库来执行控制和数据流分析、污染跟踪(taint tracking)和探索已知的威胁模型。使用QL,你可以在多个代码库上运行开箱即用或自定义查询,以获得准确且相关的安全性分析,从而使你能够关注最关键的问题。

QL将代码视为数据,允许你编写定制查询来研究代码,甚至识别最复杂的语义模式。你可以使用你喜欢的IDE的QL插件在本地编写和执行QL查询。你还可以使用LGTM查询控制台在web浏览器中直接编写QL,并查询其中的安全漏洞。

Semmle QL的工作原理

1.png

QL的工作原理是创建或提取源代码的可查询数据库,然后允许你运行查询来探索代码,或查找已知漏洞的变异对象。每次调用编译器时,Semmle都会“拦截”编译器调用,并使用相同的参数调用提取程序,这使得提取程序精确地查看编译以构建程序的相同源代码。提取程序收集有关源代码的所有相关信息,比如文件名、AST的表示形式、类型信息、关于预处理器操作的信息等,并将其存储在关系数据库中。对于没有编译器的解释语言,提取程序通过直接在源代码上运行来收集类似的信息。

提取完成后,项目的所有相关信息都包含在一个快照数据库中,然后可以在另一台机器上查询快照数据库。快照中还包括创建数据库时生成的源文件的副本,以便分析结果可以显示在代码中的正确位置。

查询是用QL语言编写的,通常依赖于一个或多个标准QL库。当然,你也可以编写自己的定制库。它们由QL编译器编译为高效的可执行格式,然后由QL评估程序在远程工作计算机上或本地开发人员计算机上的快照数据库上运行。

查询结果可以用多种方式解释和显示,包括在IDE插件(如Eclipse的QL)或web仪表板(如LGTM)中显示查询结果。

样本漏洞

众所周知,sprintf是不安全的,因为它不能防止缓冲区溢出。一个常见的错误是程序员假定snprintf总是返回写入缓冲区的字节数。在开源流媒体服务器Icecast中,有种假设(如果缓冲区的大小不受限制,它总是返回它写入缓冲区的字节数)会导致漏洞,它允许攻击者编写覆盖服务器堆栈内容的HTTP标头文件,并允许远程代码执行。我们将开发一个查询来发现snprintf的这些不安全使用。

下面是Icecast CVE-2018-18820中易受攻击代码的简化版本。此代码用于循环中,将每个HTTP标头(cur_header)从用户请求复制到一个新缓冲区(post),并在其中构造要发送到另一个服务器的post请求的主体。此时的变异对象就是post_offset,它会跟踪每次循环迭代时我们需要写入的位置。

post_offset += snprintf(post + post_offset,
                        sizeof(post) - post_offset,
                        "%s",
                        cur_header);

由于post_offset的值没有进行边界检查,并且假设snprintf返回它将写入的数据的长度,这将允许用户发送一个将被截断的长HTTP标头。不过这部分长度将允许我们定位post_offset。然后我们可以发送第二个HTTP标头,其内容将被写入被定位的位置。

我们将使用这个已知的漏洞样本来编写一个简单的QL查询,以捕获另一个代码库中的其他变异对象。查询可以在LGTM的查询控制台中运行,也可以在IDE中运行。

目标代码库

现在我们已经有了一个漏洞样本,需要选择一个代码库来运行变异分析。本文,我们将在rsyslog上运行查询。

我们现在已经知道rsyslog/librelp有一个漏洞的变异对象,不过它在commit 2cfe657中得到了修复,在修复之前和之后对快照运行查询将非常有用,这样我们就可以确认我们是否正确捕获了变异对象。

我们将对以下内容进行查询:

· 最新版本的rsyslog;

· 最新版本的rsyslog / librelp;

· rsyslog / librelp的版本5b81b1f(在修复CVE-2018-1000140之前);

· rsyslog / librelp的版本2cfe657(在修复CVE-2018-1000140之后);

一个简单的查询

我们首先编写一个简单的查询来查找所有对snprintf的调用,QL查询由一个select子句组成,该子句指示应该返回的结果。

import cpp

from FunctionCall call
where call.getTarget().getName() = "snprintf"
select call, "potentially dangerous call to snprintf."

查询的第一行导入C/ c++标准QL库,该库定义了FunctionCall之类的概念。我们使用where子句指定一个条件,即我们只对调用函数的目标的名称等于(没有分配!)snprintf的行感兴趣。不过,getname()操作可用于任何函数调用。

最后,我们选择call(具有FunctionCall类型),返回目标名为snprintf的每个FunctionCall,并显示注释信息。

优化查询过程

一个常见的工作流程是从一个简单的查询开始,比如查找对snprintf的调用的查询,并根据出现的任何模式改进查询。

我们的第一个查询找到173个结果,手动检查这些结果是不现实的。我们可以根据观察结果来优化我们的查询,即只有在格式说明符中使用%s调用snprintf可能容易受到的攻击。

import cpp

from FunctionCall call
where call.getTarget().getName() = "snprintf"
  and call.getArgument(2).getValue().regexpMatch("(?s).*%s.*")
select call, "potentially dangerous snprintf."

这个改进后的查询只找到包含格式字符串%s的snprintf调用。每次优化查询时,我们都会删除潜在的误报。这样,现在的查询结果就只有103个结果。

接下来,我们将使用污染跟踪来查找对snprintf的调用,其返回值将返回它们的大小参数。

import cpp
import semmle.code.cpp.dataflow.TaintTracking

from FunctionCall call
where call.getTarget().getName() = "snprintf"
  and call.getArgument(2).getValue().regexpMatch("(?s).*%s.*")
  and TaintTracking::localTaint(DataFlow::exprNode(call), DataFlow::exprNode(call.getArgument(1)))
select call, "potentially dangerous call to snprintf."

当存在从源节点到汇聚节点的数据流路径时,TaintTracking :: localTaint(source,sink)为真。在上面的查询中,我们使用(DataFlow::exprNode(call)作为源,它返回与snprintf调用对应的数据流图中的节点。Sink是我们使用调用的第一个参数,它对应snprintf的size参数。

如果研究一下这个查询生成的结果,就会发现rsyslog中只有一个结果,即易受攻击的librelp版本。对rsyslog结果的手动检查显示它实际上是一个误报:

if (offset + len + 1 >= option_str_len) {
        break;
}
int bytes = snprintf((char*)option_str + offset,
                (option_str_len - offset), "%s&", token);

我们可以进一步改进查询,排除已经进行了类似检查的情况。

import cppimport semmle.code.cpp.dataflow.TaintTrackingimport semmle.code.cpp.controlflow.Guardsfrom FunctionCall callwhere call.getTarget().getName() = "snprintf"
  and call.getArgument(2).getValue().regexpMatch("(?s).*%s.*")
  and TaintTracking::localTaint(DataFlow::exprNode(call), DataFlow::exprNode(call.getArgument(1)))
  // Exclude cases where it seems there is a check in place
  and not exists(GuardCondition guard, Expr operand |
      // Whether or not call is called is controlled by this guard 
      guard.controls(call.getBasicBlock(), _) and
      // operand is one of the values compared in the guard
      guard.(ComparisonOperation).getAnOperand() = operand and
      // the operand is derrived from the return value of the call to snprintf 
      TaintTracking::localTaint(DataFlow::exprNode(call), DataFlow::exprNode(operand))
  )select call

这个经过改进的查询只产生一个结果,经过分析,是漏洞CVE-2018-1000140。在Semmle团队发现这个漏洞之后,rsyslog的首席开发人员修复了这个漏洞,从他们的代码库中删除了对snprintf的危险调用。

本文翻译自:https://lgtm.com/blog/intro_to_variant_analysis_part_1?__hstc=70225743.b4b730331d0cb051d0c23ccdabf49c3a.1558406060018.1558406060018.1558406060018.1&__hssc=70225743.1.1558406060018&__hsfp=821324651&__hstc=70225743.b4b730331d0cb051d0c23ccdabf49c3a.1558406060018.1558406060018.1558406060018.1&__hssc=70225743.1.1558406060018&__hsfp=821324651https://blog.semmle.com/introduction-to-variant-analysis-part-2/如若转载,请注明原文地址: https://www.4hou.com/web/18266.html
点赞 4
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论