原理剖析:静态代码分析中的数据流与敏感性
在静态代码分析领域,我们经常遇到一系列复杂的概念,如指针敏感性(pointer sensitivity)、数据流敏感性(data flow sensitivity)和路径敏感性(path sensitivity)等。大多数相关文献充斥着晦涩难懂的数学公式和LaTeX算法,这些表述方式往往给人以高深莫测的印象。令人遗憾的是,这些所谓的“高级技术”在实际工程实践和漏洞挖掘中的应用效果往往难以体现。更有甚者,相关的实现代码常常被刻意或无意地隐藏在文献的角落,使得这些技术与实际应用之间产生了巨大的鸿沟。
过分依赖理论推导和数学模型,而忽视了实际应用的重要性。这种做法不仅使得相关知识难以被广大工程师所理解和应用,更是阻碍了整个领域的发展。我们需要的是更加实用、直观、易于理解的教学和研究方法。
数据流分析
在古早的教材和中文互联网材料中,数据流的自顶向下传播和影响,往往被认为是另一个概念“污点传播”,一般被定义为用户输入从某一个输入点,会逐步影响后续的函数执行,并且传播数据,进入 Sink(污染槽)之类的概念。
实际上污点传播就是一个非常狭义的数据流的描述。如果以它来概括代码中的数据流就过分以偏概全了。实际上数据流我们可以大致分为自顶向下,或者自底向上的两种流动方式。这两种流动方式基本可以适配常见可以通过数据流发现的程序行为。
Java 大家都看腻了,我们以大家更容易读得懂的 PHP 来为大家介绍这个概念,可能会更加容易让人理解:
<?php $llink=trim($_GET['query']); $query = "SELECT * FROM nav WHERE link='$llink'"; $result = mysql_query($query) or die('SQL语句有误:'.mysql_error()); $navs = mysql_fetch_array($result);
这段 PHP 代码主要用于从数据库中检索数据,其具体功能和流程如下:
- 获取 URL 参数:$link=trim($_GET['query'])**这行代码从 URL 的查询字符串中获取名为query的参数值,并使用trim() 函数去除其前后的空白字符。这个值被存储在变量$llink 中。
- 构建 SQL 查询:$query = "SELECT * FROM nav WHERE link='$llink'";**这里使用了一个 SQL 查询语句,目的是从 nav 表中选取所有列,条件是其 link 列的值等于 $llink。这个 SQL 语句将被用于查询数据库。
- 执行 SQL 查询:$result = mysqlquery($query) or die('SQL语句有误:'.mysqlerror());**使用mysql_query() 函数执行前面构建的 SQL 查询。如果查询失败(例如,因为 SQL 语法错误或数据库连接问题),则脚本执行将终止,并显示错误信息SQL语句有误:后跟具体的错误原因。
- 处理查询结果:**$navs = mysqlfetcharray($result);**使用mysqlfetcharray()函数从结果集中获取一行数据,并将其保存在数组$navs中。这个数组将包含 nav 表中对应行的所有列数据。
自顶向下的数据流
自顶向下的数据流我们发现,基本就是污点传播的路径问题,从 GET[query] 开始,经过 trim,经过 mysqlquery ,最后查询出来的结果放在了 $navs 中。从数据传播的角度上来看,用户输入的参数,直接影响到了 $navs 变量的结果。中间经过了数据库查询。
这是数据流的最基本研究方法,也是广为人知的“污点传播”的理论基础。然而我们很容易发现一些基本事实:
- 我们关心的实际上并不是 $navs 而是 mysql_query 这个函数,因为他可能会导致 SQL 注入;
- 我们不关心“污点”最后是谁,只要他经过了 mysql_query 按理说就会有问题;
因此,我们需要一整套的算法或者机制让数据流在自顶向下的过程中可以根据一定条件停止,或筛选出我们想要的值。这个意义十分重要。
自底向上的数据流
在我们的理念中,数据流是可以自底向上进行探索传播的,意味着,我们可以直接找到 mysql_query 函数,直接去探索它的参数的向上的数据流有没有经过用户输入的控制。
上图表示了自底向上的数据流分析的实际效果,我们发现通过这种方式反而更容易的定位问题。
这些基本思路和基本研究方法我们早在之前的文章中早已给大家介绍,接下来将讨论如何利用数据流分析的结果进行漏洞分析和行为限制。
数据流路径妙用:识别过滤函数
在上面的代码中,我们似乎能识别出这是一个 SQL 注入漏洞了,不需要任何限制,只需要分析数据流就可以得到,那么我们再对上述例子进行修改,增加一个 escape 函数来过滤一些危险字符,一般遇到这种情况,注入将会变的困难:
<?php $llink=mysqli_real_escape_string(trim($_GET['query'])); $query = "SELECT * FROM nav WHERE link='$llink'"; $result = mysql_query($query) or die('SQL语句有误:'.mysql_error()); $navs = mysql_fetch_array($result);
那么,从数据流的角度上来看,增加一个过滤函数并不会破坏数据流的流动,数据流的路径上会增加一个 call mysqlirealescape_string 的节点(指令)。
最简单的思路是我们仍然需要找到数据流。并且把在数据流路径过程所有的指令都整理出来,检查一下这个指令是否包含一些过滤函数,如果包含过滤函数,说明这个 SQL 注入增加了过滤特殊字符的函数调用,就应该调整它的漏洞级别。我们如何表示这种过程?
过程展示
如何使用规则自动识别这种漏洞?
在前面的描述中,我们极力避免出现除了 PHP 之外的代码,为了主要是讲解思路和基本研究过程。那么当基本思路都清楚之后,我们将会讲解一下如何利用我们的技术和基础设施完成这种漏洞的识别。
使用 SyntaxFlow 进行数据流分析
在 SyntaxFlow 的基本教程中,我们知道了数据流分析的 API,其实非常简单,在这里大家回顾一下用法:在 SyntaxFlow 中,对变量的使用(Use)和定义(Def)链的追踪是通过特定的运算符实现的,这些运算符使得在静态单赋值(SSA)形式的代码中追踪数据流变得异常精确和高效。这一部分教程将解释如何使用这些关键的符号来追踪变量和函数的使用和定义链。
数据流 Use-Def 链运算符概览
->和-->(使用链追踪)#>和#->(定义链追踪)**- **
-{}和#{}内的设置(定制追踪深度或上下文)
实战案例:在PHP中使用-->识别参数最终影响的数据流位置**
我们还是针对这一段代码:
<?php $llink=mysqli_real_escape_string(trim($_GET['query'])); $query = "SELECT * FROM nav WHERE link='$llink'"; $result = mysql_query($query) or die('SQL语句有误:'.mysql_error()); $navs = mysql_fetch_array($result);
保存为 code.php 之后,执行 yak ssa -l php -t . -p dataflow 命令进行编译,编译后我们就可以对 dataflow 这个程序名对应的程序进行测试并且,编写 SyntaxFlow 规则。在编译过程中会有日志输出,当你看到:...``...``...``...``[INFO] 2024-09-26 13:43:54 [language_parser:72] parsed file: [code.php]``[INFO] 2024-09-26 13:43:54 [language_parser:77] program dataflow finish``[INFO] 2024-09-26 13:43:54 [ssacli:189] finished compiling..., results: 1``[INFO] 2024-09-26 13:43:54 [database_profile:26] SSA Database SaveIrCode Cost: 23.121462ms``[INFO] 2024-09-26 13:43:54 [database_profile:27] SSA Database SaveIndex Cost: 5.212542ms``[INFO] 2024-09-26 13:43:54 [database_profile:28] SSA Database SaveSourceCode Cost: 359.583µs``[INFO] 2024-09-26 13:43:54 [database_profile:29] SSA Database SaveType Cost: 4.127001ms``[INFO] 2024-09-26 13:43:54 [database_profile:30] SSA Database CacheToDatabase Cost: 23.195708ms之类的内容输出的时候意味着编译完成了,随后创建扫描规则文件内容:_GET.* as $params; ``$params --> * as $sink; ``alert $sink;把上述规则保存为:rule.sf 然后再命令行中执行 yak sf rule.sf -p dataflow 将会看到输出为:[INFO] 2024-09-26 13:48:03 [ssacli:539] start to use SyntaxFlow rule: rule.sf``...``...``...``[INFO] 2024-09-26 13:48:03 [ssacli:688] syntax flow query result:``rule md5 hash: 468a9fc888219cc95c0771b76b4ee88a``rule preview: _GET.* as $params; $params --> * as $sink; alert $sink;``description: {"desc":"","lang":"","level":"","title":"","title_zh":"","type":""}``Result Vars: `` $sink:`` t190614: eq(Undefined-mysql_query(add(add...ET.query(valid)))), "'")), true)`` code.php:4:5 - 4:67`` t190631: Undefined-mysql_fetch_array(Unde...ned-_GET.query(valid)))), "'")))`` code.php:5:13 - 5:39看到上述数据,基本说明我们获取到了数据流的两个关键最终点,一个是mysql_fetch_array 的调用结果,另一个是 mysql_query 执行结果和 true 的比较函数(这是由 or 运算来产生的,不要惊慌)。我们在这里已经成功获取到了数据流最终终结的位置,在这里看他还是相当准确的,把上述结果整理起来就可以看到真正的结果所有数据流相关的结果:
在途中我们可以很清楚的发现,最终的结果其实是 eq 和一个 $navs 变量。那么光得到这一步,我们还不能说可以过滤出想要的结果,那么如何在规则中编写过滤语句呢?
获取数据流路径并过滤结果:检查 SQL 注入
我们经过上述操作可以得到数据流最终的点,但是需要检查过滤的步骤,这个时候应该怎么做呢?我们在 SyntaxFlow 中可以通过 <dataflow()> 这个功能来实现:我们把这个复杂的数据流捋清的过程封装成了一个叫 <dataflow> 的NativeCall。用户可以调用这个指令,把数据流的所有路径整理成在一起,一起进行检查,直到过滤出自己想要的数据。继续案例中 PHP 的程序规则:_GET.* as $params; ``$params --> * as $sink;``$sink<dataflow(<<<CODE``*?{opcode: call && <getCaller><name>?{have: mysqli_real_escape_string }} as $__next__``CODE)> as $filtered;``$sink - $filtered as $vuln;``alert $vuln;这段审计代码看起来就复杂了不少,我们直接把这段代码执行一下:yak sf -p dataflow rule.sf 发现他无法输出原来的信息了,因为我们过滤了mysqli_real_escape_string 这个函数。我们创建一个有漏洞的代码:<?php`` $llink=trim($_GET['query']);`` $query = "SELECT * FROM nav WHERE link='$llink'";`` $result = mysql_query($query) or die('SQL语句有误:'.mysql_error());`` $navs = mysql_fetch_array($result);在上述代码中移除了 mysqlirealescape_string 函数,发现重新执行规则将会检查出来,那就发生了什么?接下来我们将解释一下这段代码究竟怎么回事儿:
SyntaxFlow 检测 SQL 注入的规则解释
- _GET. as $params;*
$params --> * as $sink:- $sink<dataflow(<<<CODE
- **
*?{opcode: call && <getCaller><name>?{have: mysqli_real_escape_string }} as $__next__**这段查询检测任何函数调用(opcode:call),并且特别查找调用者名称中包含mysqli_real_escape_string的情况。如果存在这样的函数调用,这个调用点被存储为$__next__。 - CODE)> as $filtered;
$sink - $filtered as $vuln- alert $vuln最后,这行代码发出警告或报告关于 $vuln 的信息,即所有潜在的未经过滤的危险数据使用点。
这段代码是用于静态代码分析,特别是用来检测和报告潜在的 SQL 注入漏洞。它通过跟踪从用户输入到可能的敏感操作的数据流,并检查这些数据是否经过了适当的过滤(如使用 mysqli_real_escape_string),来帮助识别和预防安全风险。这种分析对于确保 Web 应用的安全性至关重要。
**SyntaxFlow 的新语法:NativeCall 与 <dataflow>**
SyntaxFlow 的高级关键特性之一是使用 NativeCall 函数,这些函数是预先定义的,可在语言内部提供各种实用功能。本教程将介绍 NativeCall 函数的概念,解释其用法,并提供可用函数的完整列表及其描述。
NativeCall 是预封装的函数,可调用以对代码中的值执行特定操作。这些函数用于操作、检查和转换数据结构,促进高级代码分析和转换任务。
SPEC: NativeCall 语法定义
<nativeCallName(arg1, argName="value", ...)>
其中:
<:标记 NativeCall 的开始。nativeCallName:要使用的 NativeCall 函数名称。(...):包含函数参数的圆括号。>:标记 NativeCall 的结束。
其完整的 eBNF 描述为:
nativeCall
: '<' useNativeCall '>'
;
useNativeCall
: identifier useDefCalcParams?
;
useDefCalcParams
: '{' nativeCallActualParams? '}'
| '(' nativeCallActualParams? ')'
;
nativeCallActualParams
: lines? nativeCallActualParam (',' lines? nativeCallActualParam)* ','? lines?
;
nativeCallActualParam
: (nativeCallActualParamKey (':' | '='))? nativeCallActualParamValue
;
nativeCallActualParamKey
: identifier
;
nativeCallActualParamValue
: identifier | numberLiteral | '`' ~'`'* '`' | '$' identifier | hereDoc
;
NativeCall 中的 支持路径检测
<dataflow> 是一个高级的 NativeCall 函数,用于在代码中进行数据流分析。它的参数是一段专门用于检查特定变量的数据流路径的代码。这段代码帮助识别和过滤数据流路径,从而发现潜在的安全漏洞。为了确保分析的准确性,进入 <dataflow> 的变量必须至少经过 Use-Def 运算符的处理,这样可以确保变量的来源和使用都被妥善追踪和记录。
示例代码解释
_GET.* as $params;$params --> * as $sink;$sink<dataflow(<<<CODE*?{opcode: call && <getCaller><name>?{have: mysqli_real_escape_string }} as $__next__CODE)> as $filtered;$sink - $filtered as $vuln;alert $vuln;
<<<CODE 与 CODE 语法
<<<CODE 到 CODE 的语法类似于 PHP 中的HereDoc 语法。它用于定义一个多行的字符串或代码块,在
$next 变量的作用
在 <<<CODE 块中定义的查询逻辑中,$next 是一个特殊的变量,用于存储满足查询条件的代码点。在上述示例中,任何调用 mysqli_real_escape_string 函数的地方都会被捕获并赋值给 $next。如果 $next 不为空,即存在至少一个满足条件的点,那么当前路径对应的点将会被保留。这种机制允许
产品化工程化的 SyntaxFlow 规则
我们通过数据流路径敏感技术分析了 SQL 注入漏洞的检测,并介绍了高级的 SyntaxFlow 规则。虽然我们探索了基础的数据流分析方法,这些方法有效地帮助我们识别和防范常见的安全漏洞如 SQL 注入,然而我们所展示的仅是冰山一角,SyntaxFlow 的能力远超这些基础示例,它能够应对更加复杂和隐蔽的代码场景。
为了激发读者对学习更多高级 SyntaxFlow 规则的兴趣,我们提供了一个更为复杂的规则示例,这个示例使用 SyntaxFlow 内置的输入点识别并且可以检测常见的 php 过滤函数,可以直接按不同级别找到不同风险的 SQL 注入漏洞,可以在一条规则中识别多种模式的 SQL 注入情况:
desc(
title: "mysql inject",
type: audit,
level: low,
)
<include('php-param')> as $params;
<include('php-filter-function')> as $filter;
mysql_query(* as $query);
$query #{
until: `* & $params`,
}-> as $root;
$root?{!<dataflow(<<<CODE
*?{opcode: call} as $__next__;
CODE)>} as $result;
alert $result for {
title: "Direct mysql injection",
title_zh: "直接的mysql注入不经过任何过滤",
type: 'vuln',
level: 'high',
};
$root?{<dataflow(<<<CODE
*?{opcode: call && <self> & $filter} as $__next__;
CODE)>} as $filter_result;
alert $filter_result for {
title: 'Filtered sql injection, filter function detected',
title_zh: '经过过滤的sql注入,检测到过滤函数',
type: 'low',
level: 'low'
};
$root?{<dataflow(<<<CODE
*?{opcode: call && !<self> & $filter} as $__next__;
CODE)>} as $seem_filter;
alert $seem_filter for {
title: 'Filtered sql injection, but no filter function detected',
title_zh: '经过过滤的sql注入,但未检测到过滤函数',
type: 'mid',
level: 'mid'
};
这段 SyntaxFlow 规则的精妙之处在于其能够通过高级的数据流分析技术,综合利用路径敏感性、函数调用分析和条件判断,来识别和区分不同级别的 SQL 注入风险。以下是对这些规则的详细解释,帮助用户和读者更好地理解其工作原理和应用场景:
- 参数识别与过滤函数检测
- 基本 SQL 注入检测
- 直接 SQL 注入检测
- 过滤函数检测
- 过滤函数未检测到的情况
通过这样的规则设置,SyntaxFlow 能够在单一的规则文件中,根据数据流的不同特点,区分出不同级别的 SQL 注入风险。这种细致的风险分级有助于开发者更精确地定位潜在的安全问题,并采取相应的防护措施。这不仅提高了代码的安全性,也优化了安全审计的效率。
总结
我们深入探讨了静态代码分析的关键概念和数据流分析的实践应用,特别是通过 SyntaxFlow 工具展示了如何有效地检测和评估安全漏洞。通过结合理论与实践,以及使用直观的 PHP 示例,我们旨在使开发者更容易理解和采用这些高级技术,从而提升代码的安全性和质量。希望这些讨论能够激发更多开发者的兴趣,推动静态代码分析技术的普及和发展。
本文首发于 Yak Project 公众号,阅读原文。
