CodeQL与Semgrep规则编写深度对比:静态代码分析工具的技术解析

本文深入对比CodeQL与Semgrep两种主流静态代码分析工具的规则编写方式,涵盖语法结构、数据流分析、污点追踪实现机制及开发环境差异,帮助开发者根据实际需求选择合适的工具。

Rule Writing for CodeQL and Semgrep

Apr 8, 2023 · 1785 words · 9 minute read

一种普遍看法是,为Semgrep编写规则比CodeQL更容易。在广泛使用这两种静态代码分析工具约一年后,我有一些想法。作为实践者,我不需要了解这些工具的确切工作原理,但最近对其理论基础的深入探讨促使我在此整合我的思考。

语法和数据结构 🔗

CodeQL和Semgrep OSS是支持自定义规则的主要免费代码分析工具。除非你将编写XPATH查询和Java插件视为“自定义规则支持”。然而,它们在规则语法方面差异很大,这也是CodeQL学习曲线的主要部分。

CodeQL使用QL语言,看起来像典型的SQL查询,带有select和where子句:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/* github/codeql/blob/main/javascript/ql/src/Electron/AllowRunningInsecureContent.ql */
/**
 * @name Enabling Electron allowRunningInsecureContent
 * @description Enabling allowRunningInsecureContent can allow remote code execution.
 * @kind problem
 * @problem.severity error
 * @security-severity 8.8
 * @precision very-high
 * @tags security
 *       frameworks/electron
 *       external/cwe/cwe-494
 * @id js/enabling-electron-insecure-content
 */

import javascript

from DataFlow::PropWrite allowRunningInsecureContent, Electron::WebPreferences preferences
where
  allowRunningInsecureContent = preferences.getAPropertyWrite("allowRunningInsecureContent") and
  allowRunningInsecureContent.getRhs().mayHaveBooleanValue(true)
select allowRunningInsecureContent, "Enabling allowRunningInsecureContent is strongly discouraged."

然而,这是一个陷阱:将QL视为数据库查询语言只会导致失败。正如其文档所述,QL基于Datalog,“一种声明性逻辑编程语言,常用作查询语言。”我的看法是,QL是一种用于进行查询的面向对象编程语言。为了编写好的QL,你需要非常熟悉CodeQL标准库中的各种类及其谓词(即方法)。例如,在上面的查询中,你需要确保preferences.getAPropertyWrite(“allowRunningInsecureContent”)返回一个DataFlow::PropWrite实例,以便等式比较能够工作。

相比之下,Semgrep规则语法是面向模式的。为了可视化这一点,考虑以下Semgrep规则,它检查与CodeQL示例相同的漏洞:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# ajinabraham/njsscan/blob/master/njsscan/rules/semantic_grep/electronjs/security_electron.yaml
rules:
  - id: electron_allow_http
    patterns:
      - pattern-either:
          - pattern: >
              new BrowserWindow({webPreferences: {allowRunningInsecureContent:
              true}})              
          - pattern: |
                            var $X = {webPreferences: {allowRunningInsecureContent: true}};
    message: >-
      Application can load content over HTTP and that makes the app vulnerable
      to Man in the middle attacks.      
    languages:
      - javascript
    severity: ERROR
    metadata:
      owasp-web: a6
      cwe: cwe-319

总体而言,对于未经训练的眼睛来说,似乎更容易理解规则在做什么。当然,这是在Semgrep的主场上;事实上,甚至正则表达式搜索也可以实现这一点(因此Semgrep中有grep)。识别真实漏洞的高质量规则的试验场是污点追踪。虽然Semgrep仅在付费的Semgrep Pro产品中提供全局污点追踪,但CodeQL可以开箱即用地做到这一点。

这是因为两种工具对源代码的建模方式不同。Semgrep将代码解析并表示为一个通用的抽象语法树。这可以更准确地标记为抽象语义树,因为它还折叠了语义上等效的代码。接下来,它将此AST转换为中间语言(IL),可以与Semgrep规则语法中的模式匹配。

这种解析到通用AST然后转换为IL的方法使得添加语言支持变得更加容易,因为你只需要担心解析步骤。然而,AST对于污点追踪并不是最优的。树结构不是有向图,因此不能直接提供关于数据或控制流的信息;你需要数据流图(DFG)和控制流图(CFG)来实现这一点。解析和转换步骤还会丢失关于语言特定行为的细节,例如类继承和全局变量传播。这些都反映在Semgrep污点追踪的设计权衡中:

  • 无路径敏感性:考虑所有可能的执行路径,尽管有些可能不可行。
  • 无指针或形状分析:可能无法检测到以非平凡方式发生的别名,例如通过数组或指针。不跟踪数组或其他数据结构中的单个元素。数据流引擎支持有限的字段敏感性用于污点追踪,但尚未用于常量传播。
  • 无健全性保证:Semgrep忽略类似eval函数对程序状态的影响。它不做最坏情况的健全假设,而是做“合理”的假设。

相比之下,CodeQL尝试在每个语言基础上提取尽可能多的关系数据,包括数据和控制流。它不是将每种语言解析为通用AST,而是为每种语言运行自定义提取器。对于编译语言,它甚至检测编译器以提取标准库等额外信息。快速浏览一下CodeQL的go提取器。每种编程语言都有自己特定的类集,可以在QL查询中使用。例如,Python CodeQL库使用Call类进行函数调用,而Javascript有CallExpression。这种差异源于每种语言的语法命名约定。

CodeQL的方法允许它进行更深入、更具体的查询。虽然从局部分析到全局分析以及从数据流到污点追踪会有一些损失,但它不会像Semgrep那样牺牲太多能力(由于缺乏更好的词)。它确实放弃了Semgrep快速支持新编程语言和随处运行的能力;如果你正在扫描编译语言代码库并且构建步骤失败,你将面临数小时的调试。

然而,作为在漏洞研究和DevSecOps环境中实际编写自定义规则的人,我主要关心的是前端,即规则语法和规则编写体验。

上下文切换 🔗

就规则编写而言,CodeQL的QL是一种面向对象的编程语言。你可以在阅读他们的入门教程时尽可能长时间地避免这个事实,但一旦你开始实际编写生产就绪的规则,就是面对现实的时候了。你将花费大量时间阅读CodeQL库文档,这些文档通常缺乏使用示例,并且是子类型、(直接和间接)超类型、分支类型、联合类型等的混乱。在你意识到之前,已经是凌晨3点,你正在尝试理解semmle.javascript.security.dataflow.ShellCommandInjectionFromEnvironmentCustomizations中的FileNameSourceAsSource是如何工作的。CodeQL试图通过备忘单和样板模板来帮助,但事实是编写QL需要从阅读代码中进行巨大的上下文切换。

相比之下,Semgrep的模式语法看起来非常像你正在扫描的实际代码。你可以从精确匹配开始,然后逐渐抽象出通用项,如变量名和序列,以泛化你的规则。你不需要担心编程,可以专注于匹配。然而,我担心随着Semgrep试图赶上CodeQL的污点追踪能力,它越来越依赖其规则语法来表达复杂的关系。例如:

1
2
3
4
5
6
7
8
pattern-propagators:
  - pattern: |
            $TO.foo($FROM)
    from: $FROM
    to: $TO
    requires: A
    replace-labels: [A, C]
    label: B

标签的作用在这里并不立即明显。CodeQL的优势在于许多污点追踪的复杂性在数据库构建步骤中预先加载,而Semgrep由于引擎的工作方式,必须将这种复杂性转移到规则语法中。还有查询导向与模式导向语法的表达力。

迭代 🔗

CodeQL的杀手功能,一旦你通过了扩展的设置过程,就是VS Code扩展。它很好地集成到类似CodeQL规则的IDE中,允许你从用户界面而不是CLI重建数据库和可视化规则发现。我发现这对于调试污点追踪查询特别有帮助,因为你可以直接点击每个污点步骤结果在代码中的位置。它还有其他功能,如单元测试、性能监控和AST可视化。

[来自CodeQL文档的图像]

Semgrep的迭代杀手功能是Semgrep Playground,一个Web应用程序,允许你在同一窗口中编写测试规则。它有助于高亮显示你的规则匹配的代码行,以便你可以快速纠正错误。一个缺点是它将所有代码发送到执行Semgrep的API服务器,因此不如在本地运行Semgrep快,并且必须匿名化敏感代码。幸运的是,Semgrep最近宣布了一种Turbo模式,使用WebAssembly在浏览器中运行Semgrep,但尚未发布。同时,Semgrep的VS Code扩展更侧重于开发者和SaaS集成,缺乏CodeQL VS Code扩展的许多功能,这些功能使得在本地测试规则更容易。

开发环境 🔗

虽然Semgrep可以轻松安装为pip包,但它不能在Windows上运行。当然,你可以在Windows子系统 for Linux上运行,但WSL2在从Linux VM访问挂载的Windows文件系统时,文件I/O性能会受到巨大影响。是的,你可以将源代码复制到WSL2文件系统,但在企业环境中,你需要处理网络共享和VPN,这些与WSL2不太兼容。如果你的机器运行Windows,它会增加一层不必要的摩擦。

虽然CodeQL的二进制文件可以在Windows上运行,但设置有点复杂,因为你需要正确配置工作空间以编写自定义查询。如果你尝试在桌面上创建QL规则文件并在数据库上运行,它很可能会失败,并显示ERROR: Could not resolve module。这是另一个提醒,QL实际上是一种编程语言,因为它需要首先通过qlpack.yml文件加载依赖项。你不能随便在任何地方编写规则;你需要一个“CodeQL工作空间”。我希望有某种create-react-app for CodeQL规则,但它已经感觉像太多的开销。

最终想法 🔗

总体而言,我发现Semgrep规则编写更容易上手,这主要归功于其查询代码库的方法。我可以快速编写一个简单的规则,迭代,然后转向结果,而CodeQL可能导致文档和调试的兔子洞。然而,当你转向更复杂的污点追踪查询时,你可能需要开始使用CodeQL(或付费购买Semgrep Pro)。

很大程度上还取决于你的代码扫描需求。如果你正在为组织的DevSecOps管道构建一组规则,你希望扫描快速且兼容大量多样化的代码库,而不必担心某些东西会中断(如构建步骤)。你希望最小化误报,因为没有人有时间分类警报。理想情况下,你希望分发规则编写职责,并使培训他人变得容易。

另一方面,渗透测试员或漏洞研究员可以处理误报;你更希望减少漏报。速度不是问题,因为你在本地运行扫描而不是在CI管道中。你可能还专注于较小的目标集,甚至单个代码库。你可以投入更多时间学习复杂的规则语法。

我希望这有助于解释为什么编写CodeQL与Semgrep规则如此不同。我建议查看以下详细讨论其后端的材料。

https://semgrep.dev/blog/2021/semgrep-a-static-analysis-journey https://codeql.github.com/publications/ https://github.blog/2023-03-31-codeql-zero-to-hero-part-1-the-fundamentals-of-static-analysis-for-vulnerability-research/

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计