Featured image of post CodeQL查询调试全攻略:从零到精通的实用技巧

CodeQL查询调试全攻略:从零到精通的实用技巧

本文详细介绍了CodeQL查询调试的完整流程,包括创建最小代码示例、简化查询、快速评估、AST查看器使用、部分路径图分析以及编写污点步骤等实用技术,帮助开发者有效解决CodeQL查询中的问题。

CodeQL从零到精通第五部分:调试查询

当你刚开始使用CodeQL时,可能会遇到查询没有返回预期结果的情况。调试这些查询可能很棘手,因为CodeQL是一种类似Prolog的语言,其评估模型与Python等主流语言有很大不同。这意味着你无法"单步执行"代码,附加gdb或添加print语句等技术也不适用。幸运的是,CodeQL提供了各种内置功能来帮助你诊断和解决查询中的问题。

下面,我们将深入探讨这些功能——从抽象语法树(AST)到部分路径图——使用CodeQL用户的问题作为示例。如果你有自己的问题,可以访问GitHub Security Lab的公共Slack实例并提问,CodeQL工程师会进行监控。

最小代码示例

我们将使用用户NgocKhanhC31提出的问题,后来zhou noel也提出了类似的问题。两人都在编写CodeQL查询以检测使用Gradio框架的项目中的漏洞时遇到了困难。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import pickle
import gradio as gr

def load_config_from_file(config_file):
    """Load settings from a UUID.pkl file."""
    try:
        with open(config_file.name, 'rb') as f:
            settings = pickle.load(f)
        return settings
    except Exception as e:
        return f"Error loading configuration: {str(e)}"

with gr.Blocks(title="Configuration Loader") as demo:
    config_file_input = gr.File(label="Load Config File")
    load_config_button = gr.Button("Load Existing Config From File", variant="primary")
    config_status = gr.Textbox(label="Status")
    
    load_config_button.click(
        fn=load_config_from_file,
        inputs=[config_file_input],
        outputs=[config_status]
    )

demo.launch()

这里的漏洞更像是一个"二阶"漏洞。首先,攻击者上传恶意文件,然后应用程序使用pickle加载它。

用户编写了一个CodeQL污点跟踪查询,乍一看应该能找到这个漏洞:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
 * @name Gradio unsafe deserialization
 * @description This query tracks data flow from inputs passed to a Gradio's Button component to any sink.
 * @kind path-problem
 * @problem.severity warning
 * @id 5/1
 */
import python
import semmle.python.ApiGraphs
import semmle.python.Concepts
import semmle.python.dataflow.new.RemoteFlowSources
import semmle.python.dataflow.new.TaintTracking

import MyFlow::PathGraph

class GradioButton extends RemoteFlowSource::Range {
    GradioButton() {
        exists(API::CallNode n |
        n = API::moduleImport("gradio").getMember("Button").getReturn()
        .getMember("click").getACall() |
        this = n.getParameter(0, "fn").getParameter(_).asSource())
    }

    override string getSourceType() { result = "Gradio untrusted input" }
}

private module MyConfig implements DataFlow::ConfigSig {
    predicate isSource(DataFlow::Node source) { source instanceof GradioButton }
    predicate isSink(DataFlow::Node sink) { exists(Decoding d | sink = d) }
}

module MyFlow = TaintTracking::Global<MyConfig>;

from MyFlow::PathNode source, MyFlow::PathNode sink
where MyFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "Data Flow from a Gradio source to decoding"

如果在数据库上运行此查询,你不会得到任何结果。

创建CodeQL数据库

使用我们的最小代码示例,我们将创建一个CodeQL数据库:

1
codeql database create codeql-zth5 --language=python

此命令将创建一个新目录codeql-zth5,其中包含CodeQL数据库。

简化查询和快速评估

查询已经简化为谓词和类,因此我们可以使用"快速评估"按钮快速评估它。

抽象语法树(AST)查看器

如果你在识别源节点或汇节点时遇到问题,检查代码的抽象语法树(AST)以确定特定代码元素的类型会很有帮助。

运行快速评估后,你将看到CodeQL识别汇节点的文件。要查看文件的抽象语法树,右键单击你感兴趣的代码元素并选择"CodeQL: View AST"。

getAQlClass谓词

找出你感兴趣的代码元素类型的另一个好策略是使用getAQlClass谓词。

部分路径图:正向

现在我们已经确定在连接源和汇时存在问题,我们应该验证污点流停止的位置。我们可以使用部分路径图来实现这一点,它显示源流向的所有汇以及这些流停止的位置。

污点步骤

最快的方法(虽然不太优雅)是编写一个污点步骤,从任何对象传播到该对象的name属性。

1
2
3
4
5
6
predicate isAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
    exists(DataFlow::AttrRead attr |
        attr.accesses(nodeFrom, "name")
        and nodeTo = attr
    )
}

再次使用污点步骤?

特定的代码片段有点特殊。我之前提到这个漏洞本质上是一个"二阶"漏洞——我们首先上传恶意文件,然后加载本地存储的文件。通常在这些情况下,我们认为文件的路径是被污染的,而不是文件本身的内容,因此CodeQL通常不会在这里传播。

这就是为什么我们需要另一个污点步骤来从config_file.name传播到open(config_file.name, ‘rb’)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
predicate osOpenStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
    // Connects the argument to `open()` to the result of `open()`
    // And argument to `os.open()` to the result of `os.open()`
    exists(API::CallNode call |
        call = API::moduleImport("os").getMember("open").getACall() and
        nodeFrom = call.getArg(0) and
        nodeTo = call)
    or
    exists(API::CallNode call |
        call = API::builtin("open").getACall() and
        nodeFrom = call.getArg(0) and
        nodeTo = call)
}

更优雅的污点步骤

本节中编写的CodeQL非常特定于Gradio,你在其他框架中不太可能遇到类似的建模。以下部分是先前污点步骤的更高级版本,适用于那些想要更深入研究编写更可维护解决方案的人。

结论

我们在GHSL Slack上遇到的一些关于跟踪污点的问题可能具有挑战性。像这样的情况不常发生,但当它们发生时,它们成为分享经验教训和撰写博客文章的好候选者。

我希望我追踪污点的故事能帮助你调试查询。如果在尝试了本博客中的提示后,你的查询仍然存在问题,请随时在我们的公共GitHub Security Lab Slack实例或github/codeql讨论中寻求帮助。

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