向左,向右,向静态代码分析看

“一棵是枣树,另一棵还是枣树。”

今天是周六,今天也是周五...

大家好,这里是沉迷静态代码分析的超级牛

在愉快的“周五”,牛牛拿着鞭子找上了Yak的灵魂人物——

姬哥!

并邀请他教牛牛进行高级静态代码分析

在古早的教材和中文互联网材料中,数据流的自顶向下传播和影响,往往被认为是另一个概念“污点传播”,一般被定义为用户输入从某一个输入点,会逐步影响后续的函数执行,并且传播数据,进入 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 代码主要用于从数据库中检索数据,其具体功能和流程如下:

  1. 获取 URL参数$link=trim($_GET['query'])这行代码从 URL 的查询字符串中获取名为query的参数值,并使用trim()函数去除其前后的空白字符。这个值被存储在变量$llink中。

  2. 构建 SQL 查询$query = "SELECT * FROM nav WHERE link='$llink'";这里使用了一个 SQL 查询语句,目的是从nav表中选取所有列,条件是其link列的值等于$llink。这个 SQL 语句将被用于查询数据库。

  3. 执行 SQL 查询$result = mysql_query($query) or die('SQL语句有误:'.mysql_error());使用mysql_query()函数执行前面构建的 SQL 查询。如果查询失败(例如,因为 SQL 语法错误或数据库连接问题),则脚本执行将终止,并显示错误信息SQL语句有误:后跟具体的错误原因。

  4. 处理查询结果$navs = mysql_fetch_array($result);使用mysql_fetch_array()函数从结果集中获取一行数据,并将其保存在数组$navs中。这个数组将包含nav表中对应行的所有列数据。

自顶向下的数据流我们发现,基本就是污点传播的路径问题,从_GET[query]开始,经过trim,经过mysql_query,最后查询出来的结果放在了$navs中。从数据传播的角度上来看,用户输入的参数,直接影响到了$navs变量的结果。中间经过了数据库查询。

这是数据流的最基本研究方法,也是广为人知的“污点传播”的理论基础。然而我们很容易发现一些基本事实:

  1. 我们关心的实际上并不是$navs而是mysql_query这个函数,因为他可能会导致 SQL 注入;

  2. 我们不关心“污点”最后是谁,只要他经过了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 mysqli_real_escape_string的节点(指令)。

最简单的思路是我们仍然需要找到数据流。并且把在数据流路径过程所有的指令都整理出来,检查一下这个指令是否包含一些过滤函数,如果包含过滤函数,说明这个 SQL 注入增加了过滤特殊字符的函数调用,就应该调整它的漏洞级别。我们如何表示这种过程?

在前面的描述中,我们极力避免出现除了 PHP 之外的代码,为了主要是讲解思路和基本研究过程。那么当基本思路都清楚之后,我们将会讲解一下如何利用我们的技术和基础设施完成这种漏洞的识别。

在 SyntaxFlow 的基本教程中,我们知道了数据流分析的 API,其实非常简单,在这里大家回顾一下用法:在 SyntaxFlow 中,对变量的使用(Use)和定义(Def)链的追踪是通过特定的运算符实现的,这些运算符使得在静态单赋值(SSA)形式的代码中追踪数据流变得异常精确和高效。这一部分教程将解释如何使用这些关键的符号来追踪变量和函数的使用和定义链。

数据流 Use-Def 链运算符概览

    1. ->和 -->(使用链追踪)

      1. ->:追踪到下一个使用该变量或函数的地方。这是追踪变量在代码中“一级”使用的基本方式。

      2. -->:追踪直到使用链的结束。这个运算符将继续追踪,穿过所有使用点,直到没有更多的使用,即完全展开整个使用链。

    2. #> 和 #->(定义链追踪)

      1. #>:追踪到变量或函数的直接定义点。这通常是变量被赋值或函数被声明的地方。

      2. #->:追踪直到定义链的开始。使用这个运算符可以追踪到变量或函数的最初定义,穿过所有中间的定义点。

    3. -{}和 #{}内的设置(定制追踪深度或上下文)

-{}->:允许你定义追踪的深度或其他参数。例如,-{depth: 5}->表示追踪使用链,但追踪的深度限制为5层。

实战案例:在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 变量。那么光得到这一步,我们还不能说可以过滤出想要的结果,那么如何在规则中编写过滤语句呢?

图片

我们经过上述操作可以得到数据流最终的点,但是需要检查过滤的步骤,这个时候应该怎么做呢?我们在 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 注入的规则解释

这行代码的意图是从 PHP 的全局 $_GET 数组中获取所有参数,并将这些参数存储到一个名为 $params 的变量中。在实际的 PHP 代码中,这通常是通过遍历 $_GET 数组完成的。

这条指令可能意味着将 $params 中的数据流向任何可能的“汇点”(sink),这里的“汇点”指的是数据可能被用于敏感操作的地方,如数据库查询。$sink 变量代表这些潜在的危险使用点。

这部分开始定义一个数据流查询,用来跟踪 $sink 中的数据流.<<<CODE 表示接下来是一段内嵌的代码或查询。

这表示上述代码块的结束。查询的结果,即所有经过 mysqli_real_escape_string 处理的数据点,被存储在变量 $filtered 中。

这行代码从 $sink 中排除那些已经被标记为 $filtered 的数据点。换句话说,它寻找那些潜在未经过滤直接用于敏感操作的数据点,并将这些点存储在$vuln 中。

  1. _GET.* as $params;

  2. $params --> * as $sink:

  3. $sink<dataflow(<<<CODE

  4. *?{opcode: call && <getCaller><name>?{have: mysqli_real_escape_string }} as $__next__

    这段查询检测任何函数调用(opcode:call),并且特别查找调用者名称中包含 mysqli_real_escape_string 的情况。如果存在这样的函数调用,这个调用点被存储为 $__next__

  5. CODE)> as $filtered;

  6. $sink - $filtered as $vuln

  7. 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> 支持路径检测

<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 语法。它用于定义一个多行的字符串或代码块,在 <dataflow> 的上下文中,这种语法用于编写复杂的查询逻辑。<<<CODE 标记代码块的开始,而 CODE 标记代码块的结束。这允许开发者在 NativeCall 函数中嵌入较长或较复杂的代码片段。

$__next__ 变量的作用

在 <<<CODE 块中定义的查询逻辑中,$__next__ 是一个特殊的变量,用于存储满足查询条件的代码点。在上述示例中,任何调用 mysqli_real_escape_string 函数的地方都会被捕获并赋值给 $__next__。如果 $__next__ 不为空,即存在至少一个满足条件的点,那么当前路径对应的点将会被保留。这种机制允许 <dataflow> 分析函数细致地筛选和确定哪些数据流路径是安全的,哪些可能包含潜在的安全风险。

我们通过数据流路径敏感技术分析了 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 注入风险。以下是对这些规则的详细解释,帮助用户和读者更好地理解其工作原理和应用场景:

<include('php-param')> as $params; 

和 

<include('php-filter-function')> as $filter; 

这两行代码分别用于引入 PHP 参数和过滤函数的定义。这意味着规则可以自动识别 PHP 代码中的输入参数和常见的过滤函数,如 mysqli_real_escape_string。

mysql_query(* as $query); 

这行代码标记了对 mysql_query 函数的调用点,这是执行 SQL 语句的关键位置。

$query #{ until: * & $params, }-> as $root; 

这行代码定义了一个从 SQL 查询中追溯到参数输入的数据流路径。这里的 until 关键字用于限定追踪到包含参数的点为止,确保数据流的起点是用户输入。

$root?{!<dataflow(<<<CODE *?{opcode: call} as $__next__; CODE)>} as $result; 

这段代码检测是否有直接从用户输入到 SQL 查询的数据流,而中间没有任何函数调用。如果存在这样的数据流,即认为存在高风险的直接 SQL 注入。

$root?{<dataflow(<<<CODE *?{opcode: call && <self> & $filter} as $__next__; CODE)>} as $filter_result; 

这段代码用于检测数据流中是否存在过滤函数的调用。如果数据流中包含了过滤函数,那么认为 SQL 注入的风险较低。

$root?{<dataflow(<<<CODE *?{opcode: call && !<self> & $filter} as $__next__; CODE)>} as $seem_filter; 

这段代码检测数据流中是否虽然有函数调用,但并非是已知的过滤函数。如果是这种情况,那么将其认为是中等风险的 SQL 注入。

  1. 参数识别与过滤函数检测

  2. 基本 SQL 注入检测

  3. 直接 SQL 注入检测

  4. 过滤函数检测

  5. 过滤函数未检测到的情况

通过这样的规则设置,SyntaxFlow 能够在单一的规则文件中,根据数据流的不同特点,区分出不同级别的 SQL 注入风险。这种细致的风险分级有助于开发者更精确地定位潜在的安全问题,并采取相应的防护措施。这不仅提高了代码的安全性,也优化了安全审计的效率。

我们深入探讨了静态代码分析的关键概念和数据流分析的实践应用,特别是通过 SyntaxFlow 工具展示了如何有效地检测和评估安全漏洞。通过结合理论与实践,以及使用直观的 PHP 示例,我们旨在使开发者更容易理解和采用这些高级技术,从而提升代码的安全性和质量。希望这些讨论能够激发更多开发者的兴趣,推动静态代码分析技术的普及和发展。

END

YAK官方资源

Yak 语言官方教程:
https://yaklang.com/docs/intro/

Yakit 视频教程:
https://space.bilibili.com/437503777

Github下载地址:
https://github.com/yaklang/yakit
https://github.com/yaklang/yaklang

Yakit官网下载地址:
https://yaklang.com/

Yakit安装文档:
https://yaklang.com/products/download_and_install

Yakit使用文档:
https://yaklang.com/products/intro/

常见问题速查:
https://yaklang.com/products/FAQ

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值