LoRexxar's Blog | 信息技术分享

人与代码的桥梁-聊聊SAST

2023/12/18

自从人类发明了工具开始,人类就在不断为探索如何更方便快捷的做任何事情,在科技发展的过程中,人类不断地试错,不断地思考,于是才有了现代伟大的科技时代。

在安全领域里,每个安全研究人员在研究的过程中,也同样的不断地探索着如何能够自动化的解决各个领域的安全问题。其中自动化代码审计就是安全自动化绕不过去的坎。

而SAST作为自动化代码分析的一种,有着其特有的定位以及作用,这篇文章我们就来聊聊静态分析的一些发展历程和思路。

本篇文章其实在2年前曾经写过一次,在2年后的今天处于偶然的契机正好要写一篇相关的文章,所以重写了这部分内容,其中修正了不少在这期间内探索获取的新理念和思路,希望有价值。

静态代码分析工具

静态代码分析主要是通过分析目标代码,通过纯静态的手段进行分析处理,并挖掘相应的漏洞/Bug.

再过去的十几年里,静态代码分析工具经历了长期的发展与演变过程,下面我们就一起回顾一下(下面的每个时期主要代表的相对的发展期,并不是比较绝对的诞生前后):

上古时期 - 关键字匹配

如果我问你“如果让你设计一个自动化代码审计工具,你会怎么设计?”,我相信,你一定会回答我,可以尝试通过匹配关键字。紧接着你也会迅速意识到通过关键字匹配的问题

这里我们拿PHP做个简单的例子。

img

虽然我们匹配到了这个简单的漏洞,但是很快发现,事情并没有那么简单。

img

也许你说你可以通过简单的关键字重新匹配到这个问题

1
\beval\(\$

但是可惜的是,作为安全研究员,你永远没办法知道开发人员是怎么写代码的。于是选择用关键字匹配的你面临着两种选择:

  • 高覆盖性 – 宁错杀不放过

这类工具最经典的就是Seay,通过简单的关键字来匹配经可能多的目标,之后使用者可以通过人工审计的方式进一步确认。

1
\beval\b\(
  • 高可用性 – 宁放过不错杀

这类工具最经典的是Rips免费版

1
\beval\b\(\$_(GET|POST)

用更多的正则来约束,用更多的规则来覆盖多种情况。这也是早期静态自动化代码审计工具普遍的实现方法。

但问题显而易见,高覆盖性和高可用性是这种实现方法永远无法解决的硬伤,不但维护成本巨大,而且误报率和漏报率也是居高不下。所以被时代所淘汰也是历史的必然。

近代时期 - 基于AST的代码分析

有人忽略问题,也有人解决问题。关键字匹配最大的问题是在于你永远没办法保证开发人员的习惯,你也就没办法通过任何制式的匹配来确认漏洞,那么基于AST的代码分析方式就诞生了,开发人员是不同的,但编译器是相同的。

在分享这种原理之前,我们首先可以复习一下编译原理。拿PHP代码举例子:

img

随着PHP7的诞生,AST也作为PHP解释执行的中间层出现在了编译过程的一环。

img

通过词法分析和语法分析,我们可以将任意一份代码转化为AST语法树PHP常见的语义分析库可以参考:

当我们得到了一份AST语法树之后,我们就解决了前面提到的关键字匹配最大的问题,至少我们现在对于不同的代码,都有了统一的AST语法树。如何对AST语法树做分析也就成了这类工具最大的问题。

在理解如何分析AST语法树之前,我们首先要明白infomation flow、source、sink三个概念,

  • source: 我们可以简单的称之为输入,也就是infomation flow的起点
  • sink: 我们可以称之为输出,也就是infomation flow的终点
  • infomation flow,则是指数据流动的过程。

把这个概念放在PHP代码审计过程中,Source就是指用户可控的输入,比如$_GET、$_POST等,而Sink就是指我们要找到的敏感函数,比如echo、eval,如果某一个Source到Sink存在一个完整的流,那么我们就可以认为存在一个可控的漏洞,这也就是基于infomation flow的代码审计原理

在明白了基础原理的基础上,我举几个简单的例子:

img

在上面的分析过程中,Sink就是eval函数,Source就是$_GET,通过回溯Sink的来源,我们成功找到了一条流向Source的infomation flow,也就成功发现了这个漏洞。

在分析infomation flow的过程中,明确作用域是基础中的基础.这也是分析infomation flow的关键,我们可以一起看看一段简单的代码

img

如果我们很简单的跟踪赋值关系去回溯,而没有考虑到函数定义的话,我们很容易将流定义为:
img

这样我们就错误的把这段代码定义成了存在漏洞,但很显然并不是,而正确的分析流程应该是这样的:
img

在这段代码中,从主语法树的作用域跟到Get函数的作用域,如何维持作用域的变动,就是基于AST语法树分析的一大难点,当我们在代码中不可避免的使用递归来控制作用域时,在多层递归中的统一标准也就成了分析的基础核心问题。

事实上,即便你做好了这个最简单的基础核心问题,你也会遇到层出不穷的问题。这里我举两个简单的例子

(1) 新函数封装

img

这是一段很经典的代码,敏感函数被封装成了新的敏感函数,参数是被二次传递的。为了解决,这样infomation flow的方向从逆向->正向的问题。

img

(2) 多重调用链

img

这是一段有漏洞的JS代码,人工的话很容易看出来问题。但是如果通过自动化的方式回溯参数的话就会发现整个流程中涉及到了多种流向

img

这里我用红色和黄色代表了流的两种流向。要解决这个问题只能通过针对类/字典变量的特殊回溯才能解决。

如果说,前面的两个问题是可以被解决的话,还有很多问题是很难被解决的,这里举一个简单的例子。

img

这是一个典型的全局过滤,人工审计可以很容易看出这里被过滤了。但是如果在自动化分析过程中,当回溯到Source为$_GET[‘a’]时,已经满足了从Source到sink的infomation flow已经被识别为漏洞。一个典型的误报就出现了。

基于AST的自动化代码审计工具也正是在与这样的问题做博弈,对于基于AST的代码分析来说,最大的挑战在于没人能保证自己完美的处理所有的AST结构,再加上基于单向流的分析方式,无法应对100%的场景

近代时期 - 基于IR/CFG的代码分析

如果深度了解过基于AST的代码分析原理的话,不难发现许多弊端。首先AST是编译原理中IR/CFG的更上层,其内容更接近源代码

也就是说,分析AST更接近分析代码,换句话就是说基于AST的分析得到的流,更接近脑子里对代码执行里的流程,忽略了大多数的分支、跳转、循环这类影响执行过程顺序的条件,这也是基于AST的代码分析的普遍解决方案,当然,从结果论上很难辨别忽略带来的后果。而基于IR/CFG这类带有控制流的解决方案,则是另一种解决思路。

首先我们得知道什么是IR/CFG。

  • IR:是一种类似于汇编语言的线性代码,其中各个指令按照顺序执行。其中现在主流的IR是三地址码(四元组)
  • CFG: (Control flow graph)控制流图,在程序中最简单的控制流单位是一个基本块,在CFG中,每一个节点代表一个基本块,每一个边代表一个可控的控制转移,整个CFG代表了整个代码的的控制流程图。

一般来说,我们需要遍历IR来生成CFG,当然,你也可以用AST来生成CFG,毕竟AST是比较高的层级。

而基于CFG的代码分析思路优势在于,对于一份代码来说,你首先有了一份控制流图(或者说是执行顺序),然后才到漏洞挖掘这一步。比起基于AST的代码分析来说,你只需要专注于从Source到Sink的过程即可。

但其实无论是基于哪种底层,后续的分析流程与AST其实别无太大的差别,挑战的核心仍然维持在如何控制流,维持作用域,处理程序逻辑的分支过程,确认Source与Sink。上文中提到的是静态分析当中比较常见的一种作用域数据流回溯分析的思路,其实正向的污点分析,亦或者后来被人提到比较多的指针分析,核心思路大同小异。

而代码分析的基础方面,既然存在基于AST的代码分析,又存在基于CFG的代码分析,自然也存在其他的种类。比如现在市场上主流的fortify,Checkmarx,Coverity包括最新的Rips都使用了自己构造的语言的某一个中间部分,比如fortify和Coverity就需要对源码编译的某一个中间语言进行分析,又比如源伞实现了多种语言生成统一的IR,Joern使用了基于AST生成的CPG图结构进行分析。

事实上,无论是基于某种基础结构的代码分析,技术手段本身只有适应场景的不同,对于技术选型这件事情本身来说更重要的是你想要构建一个什么样的代码分析工具

未来 - 通用化代码分析框架

基于QL概念的框架 - CodeQL

QL指的是一种面向对象的查询语言,用于从关系数据库中查询数据的语言。我们常见的SQL就属于一种QL,一般用于查询存储在数据库中的数据。

而在代码分析领域,Semmle QL是最早诞生的QL语言,他最早被应用于LGTM,并被用于Github内置的安全扫描为大众免费提供。紧接着,CodeQL也被开发出来,作为稳定的代码分析框架在github社区化。

那么什么是QL呢?QL又和代码分析有什么关系呢?

首先我们回顾一下基于AST、CFG这类代码分析最大的特点是什么?无论是基于哪种中间件建立的代码分析流程,都离不开3个概念,流、Source、Sink,这类代码分析的原理无论是正向还是逆向,都是通过在Source和Sink中寻找一条流。而这条流的建立围绕的是代码执行的流程,就好像编译器编译运行一样,程序总是流式运行的。这种分析的方式就是数据流分析(Data Flow)

QL就是把这个流的每一个环节具象化,把每个节点的操作具像成状态的变化,并且储存到数据库中。

这样一来,通过构造QL语言,我们就能找到满足条件的节点,并构造成流。下面我举一个简单的例子来说:

1
2
3
4
5
6
<?php

$a = $_GET['a'];
$b = htmlspecialchars($a);

echo $b;

我们简单的把前面的流写成一个表达式

1
echo => $_GET.is_filterxss

这里is_filterxss被认为是输入$_GET的一个标记,在分析这类漏洞的时候,我们就可以直接用QL表达

1
2
3
4
5
select * where {
Source : $_GET,
Sink : echo,
is_filterxss : False,
}

通过构造满足条件的语句,我们就可以找到这个漏洞(上面的代码仅为伪代码),从这样的一个例子我们不难发现,QL其实更接近一个概念,他鼓励将信息流具象化,这样我们就可以用更通用的方式去写规则筛选。

CodeQL类的工具(包括CheckMarx等等)其实就是类似的一个基础理念,通过封装底层的代码处理逻辑,并提供一个非常易用的上层平台给用户,用户可以不用了解复杂的编译原理就可以编写漏洞的规则。

但其实说到底这只是一个理念,并不是结果,以CodeQL为例子,其构建的一套语法规则并不能算是一套门槛很低的东西,反而其黑盒的底层阻止了安全研究人员进一步研究和使用CodeQL。

基于工具化的框架 - Joern

如果说CodeQL类的工具是探索做一个通用化的代码分析框架,来解决代码分析的场景。那Joern就走了另一条路,就是工具化

Joern的底层原理是一套基于AST生成的通用CPG(Code Property Graph)图,在图的上层实现了一套基于OverflowDb的查询语言以供使用者可以在不需要知晓底层原理的基础上查询分析。

img

但我们这里想讨论的并不是Joern的原理,而是理念。Joern把自己定位成了安全研究员用于代码分析的一个工具,而不是执着于用一个按钮一个规则扫描漏洞,而是提供了人和代码的桥梁

在Joern里,我用的比较多,也是比较常见的一个场景就是寻找某个方法的调用关系。在Joern shell当中,你可以用非常简单的方法获取某个函数的调用位置已经调用了该函数的函数

img

所以在Joern当中,你可以忽略数据流分析,而是用一些非常简单的交互式命令来辅助,在Joern你可以非常简单的获取“调用了A方法的路由入口”,而更实际的利用链完全可以有人来判定,省去为了上下文分析花去的力气。

当然,Joern在某些方面是成也工具化败也工具化cpg本身强调调用关系和引用关系,Joern shell后端引用scala易用性有余实用性不足,你几乎很难在Joern的上层做数据流分析层面的分析

后话

其实相较第一版本的内容来说,我没有对文章内容做太多的更改,因为对于静态分析的底层原理来说用什么技术已经没什么很大的意义了,说到底原理都是差不多的

商业代码分析的软件包括Checkmarx、fortify等等,再到后来的CodeQL说到底其实都是技术长期积累的技术壁垒,很多问题也不是学术上的什么难点攻破。

而近几年越来越多的相关东西也如雨后春笋冒了出来,开源社区比较火的Joern、tabby、tai-e,商业公司比较火的蜚语的corax,梅子酒创业的寻臻科技,其实技术原理上的东西大同小异,说到底就是还没有足够好用的产品出来大多代码分析的软件还停留在某个底层技术的应用上

而代码分析工具本身的易用性和场景化遇到的问题在我看来问题更大,即便是商业化程度非常高的Checkmarx这种软件也没法非常简单直白的接入到devsecops流程当中,很多工具甚至都解决不了高误报率和扫描效率低的问题,更谈不上实用了。在我看来,一款实用的好的代码分析软件还有很长的路要走~

CATALOG
  1. 1. 静态代码分析工具
    1. 1.1. 上古时期 - 关键字匹配
    2. 1.2. 近代时期 - 基于AST的代码分析
    3. 1.3. 近代时期 - 基于IR/CFG的代码分析
    4. 1.4. 未来 - 通用化代码分析框架
      1. 1.4.1. 基于QL概念的框架 - CodeQL
      2. 1.4.2. 基于工具化的框架 - Joern
  2. 2. 后话