LoRexxar's Blog | 信息技术分享
LoRexxar's Blog
马上订阅 LoRexxar's Blog | 信息技术分享 RSS 更新: https://lorexxar.cn/atom.xml
人与代码的桥梁-聊聊SAST
自从人类发明了工具开始,人类就在不断为探索如何更方便快捷的做任何事情,在科技发展的过程中,人类不断地试错,不断地思考,于是才有了现代伟大的科技时代。
在安全领域里,每个安全研究人员在研究的过程中,也同样的不断地探索着如何能够自动化的解决各个领域的安全问题。其中自动化代码审计就是安全自动化绕不过去的坎。
而SAST作为自动化代码分析的一种,有着其特有的定位以及作用,这篇文章我们就来聊聊静态分析的一些发展历程和思路。
本篇文章其实在2年前曾经写过一次,在2年后的今天处于偶然的契机正好要写一篇相关的文章,所以重写了这部分内容,其中修正了不少在这期间内探索获取的新理念和思路,希望有价值。
静态代码分析工具
静态代码分析主要是通过分析目标代码,通过纯静态的手段进行分析处理,并挖掘相应的漏洞/Bug.
再过去的十几年里,静态代码分析工具经历了长期的发展与演变过程,下面我们就一起回顾一下(下面的每个时期主要代表的相对的发展期,并不是比较绝对的诞生前后):
上古时期 - 关键字匹配
如果我问你“如果让你设计一个自动化代码审计工具,你会怎么设计?”,我相信,你一定会回答我,可以尝试通过匹配关键字。紧接着你也会迅速意识到通过关键字匹配的问题。
这里我们拿PHP做个简单的例子。
虽然我们匹配到了这个简单的漏洞,但是很快发现,事情并没有那么简单。
也许你说你可以通过简单的关键字重新匹配到这个问题
1 | \beval\(\$ |
但是可惜的是,作为安全研究员,你永远没办法知道开发人员是怎么写代码的。于是选择用关键字匹配的你面临着两种选择:
- 高覆盖性 – 宁错杀不放过
这类工具最经典的就是Seay,通过简单的关键字来匹配经可能多的目标,之后使用者可以通过人工审计的方式进一步确认。
1 | \beval\b\( |
- 高可用性 – 宁放过不错杀
这类工具最经典的是Rips免费版
1 | \beval\b\(\$_(GET|POST) |
用更多的正则来约束,用更多的规则来覆盖多种情况。这也是早期静态自动化代码审计工具普遍的实现方法。
但问题显而易见,高覆盖性和高可用性是这种实现方法永远无法解决的硬伤,不但维护成本巨大,而且误报率和漏报率也是居高不下。所以被时代所淘汰也是历史的必然。
近代时期 - 基于AST的代码分析
有人忽略问题,也有人解决问题。关键字匹配最大的问题是在于你永远没办法保证开发人员的习惯,你也就没办法通过任何制式的匹配来确认漏洞,那么基于AST的代码分析方式就诞生了,开发人员是不同的,但编译器是相同的。
在分享这种原理之前,我们首先可以复习一下编译原理。拿PHP代码举例子:
随着PHP7的诞生,AST也作为PHP解释执行的中间层出现在了编译过程的一环。
通过词法分析和语法分析,我们可以将任意一份代码转化为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的代码审计原理。
在明白了基础原理的基础上,我举几个简单的例子:
在上面的分析过程中,Sink就是eval函数,Source就是$_GET,通过回溯Sink的来源,我们成功找到了一条流向Source的infomation flow,也就成功发现了这个漏洞。
在分析infomation flow的过程中,明确作用域是基础中的基础.这也是分析infomation flow的关键,我们可以一起看看一段简单的代码
如果我们很简单的跟踪赋值关系去回溯,而没有考虑到函数定义的话,我们很容易将流定义为:
这样我们就错误的把这段代码定义成了存在漏洞,但很显然并不是,而正确的分析流程应该是这样的:
在这段代码中,从主语法树的作用域跟到Get函数的作用域,如何维持作用域的变动,就是基于AST语法树分析的一大难点,当我们在代码中不可避免的使用递归来控制作用域时,在多层递归中的统一标准也就成了分析的基础核心问题。
事实上,即便你做好了这个最简单的基础核心问题,你也会遇到层出不穷的问题。这里我举两个简单的例子
(1) 新函数封装
这是一段很经典的代码,敏感函数被封装成了新的敏感函数,参数是被二次传递的。为了解决,这样infomation flow的方向从逆向->正向的问题。
(2) 多重调用链
这是一段有漏洞的JS代码,人工的话很容易看出来问题。但是如果通过自动化的方式回溯参数的话就会发现整个流程中涉及到了多种流向。
这里我用红色和黄色代表了流的两种流向。要解决这个问题只能通过针对类/字典变量的特殊回溯才能解决。
如果说,前面的两个问题是可以被解决的话,还有很多问题是很难被解决的,这里举一个简单的例子。
这是一个典型的全局过滤,人工审计可以很容易看出这里被过滤了。但是如果在自动化分析过程中,当回溯到Source为$_GET[‘a’]时,已经满足了从Source到sink的infomation flow,已经被识别为漏洞。一个典型的误报就出现了。
而基于AST的自动化代码审计工具也正是在与这样的问题做博弈,对于基于AST的代码分析来说,最大的挑战在于没人能保证自己完美的处理所有的AST结构,再加上基于单向流的分析方式,无法应对100%的场景。
近代时期 - 基于IR/CFG的代码分析
如果深度了解过基于AST的代码分析原理的话,不难发现许多弊端。首先AST是编译原理中IR/CFG的更上层,其内容更接近源代码。
也就是说,分析AST更接近分析代码,换句话就是说基于AST的分析得到的流,更接近脑子里对代码执行...
剩余内容已隐藏










