• CodeQL数据库构建原理分析


    CodeQL是一个帮助开发者自动完成安全检查、帮助安全研究者进行变异分析的分析引擎。它由代码数据库和代码语义分析引擎组成,通过将代码抽象为数据查询表保存到代码数据库中,可以方便地运行代码查询。本文的关注点在于CodeQL是如何生成代码数据库。

    这里以 java 作为示例语言进行分析

    在配置好CodeQL以后,用户目录下的 codeql-home/codeql 文件夹保存了CodeQL的 CLI 部分,它的目录结构如下,这里省略了部分无关文件

    ├── codeql
    ├── java
    │   ├── codeql-extractor.yml
    │   ├── semmlecode.dbscheme
    │   ├── semmlecode.dbscheme.stats
    │   └── tools
    │         ├── autobuild-fat.jar
    │         ├── autobuild.cmd
    │         ├── autobuild.sh
    │         ├── codeql-java-agent.jar
    │         ├── compiler-tracing.spec
    │         ├── macos
    │         ├── pre-finalize.sh
    │         ├── semmle-extractor-java.jar
    │         └── tracing-config.lua
    └──── tools
        ├── codeql.jar
        ├── osx64
        ├── test
        └── tracer
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    CodeQL的入口文件为 codeql ,这是一个 shell 脚本,主要目的就是为调用 codeql.jar 做准备,包括检查环境和配置环境变量。 codeql.jar 是CodeQL的核心文件,包含了命令行解析、数据库创建和查询引擎相关的代码。

    这里以创建数据库的指令为例。创建数据库要经过下面三步

    `initialize  初始化数据库,用到codeql.jar
    build       生成trap文件,用到codeql-java-agent.jar,semmle-extractor-java.jar            
    finalize    将trap文件导入数据库,用到pre-finalize.sh,codeql.jar` 
    
    • 1
    • 2
    • 3

    我们按照这个流程,分成三步进行分析

    我们新建一个IDEA工程,将 codeql.jar 导入为依赖库,然后编写如下代码

    `package cokeBeer;
    
    import com.semmle.cli2.CodeQL;
    import java.io.File;
    
    public class RunCreate {
        public static void main(String[] args) {
            //参数部分可以自由配置,只要能正常运行database create的参数即可
            String UserHome=System.getProperty("user.home");
            String language="java";
            String command="mvn clean package";
            String ProjectName="java-sec-code";
            String CodeQLHome=String.join(File.separator,UserHome,"codeql-home");
            String SourceRoot=String.join(File.separator,CodeQLHome,"source","java-source");
            String DatabaseRoot=String.join(File.separator,CodeQLHome,"database","java-database");
            String source=String.join(File.separator,SourceRoot,ProjectName);
            String database=String.join(File.separator,DatabaseRoot,ProjectName);
            String[] QLArgs=new String[]{"database","create","-v","--overwrite","-l",language,"-s",source,"-c",command,database};
            //调用CodeQL的入口方法,可以在这里下断点
            CodeQL.main(QLArgs);
        }
    }` 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    这里选择 java-sec-code 这个项目作为测试项目。具体选择的项目内容对分析过程没有影响,编译指令正确即可。

    在入口方法处打上断点,开始调试,接下来的方法调用过程如下

    `com.semmle.cli2.CodeQL#main
    com.semmle.cli2.picocli.SubcommandMaker#runMain(java.lang.String[])
    com.semmle.cli2.picocli.SubcommandMaker#runMain(java.lang.String[], java.util.function.Function, boolean)
    java.util.function.Function#apply
    com.semmle.cli2.picocli.SubcommandCommon#call
    com.semmle.cli2.database.CreateCommand#executeSubcommand` 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    最后是进入到了 CreataeCommmand 类,这个类处理创建数据库相关的操作,这里简化了部分代码,方法逻辑流程如下

    `protected void executeSubcommand() throws SubcommandDone {
        // 初始化数据库
            this.runPlumbingInProcess(InitCommand.class, new Object[]{this.initOptions, "--source-root=" + this.sourceRoot, "--allow-missing-source-root=" + this.traceCommandOptions.hasWorkingDir(), "--allow-already-existing", "--", this.initOptions.directory});
            // 运行编译指令
        this.runPlumbingInProcess(TraceCommandCommand.class, new Object[]{threadsOption(this.threads), ramOption(this.ram), this.tracingOptions, this.traceCommandOptions, this.extractorOptionsOptions, indexTracelessOption, multispec, "--", multispec.directory, commandLine});
        // finalize
            this.runPlumbingInProcess(FinalizeCommand.class, new Object[]{threadsOption(this.threads), ramOption(this.ram), this.finalizeParams, multispec, "--", multispec.directory});
            }
    }` 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    我们进入初始化数据库的代码,调用链如下

    `com.semmle.cli2.picocli.SubcommandCommon#runPlumbingInProcess
    com.semmle.cli2.picocli.PlumbingRunner#run
    com.semmle.cli2.database.InitCommand#executeSubcommand
    com.semmle.cli2.database.InitCommand#initOneDatabase` 
    
    • 1
    • 2
    • 3
    • 4

    最后是进入了 InitCommand 类,这个类负责初始化数据库。 initOneDatabase 的代码简化后如下

    private void initOneDatabase(String language, Path databaseDir, long linesOfCode, Optional shaAnalyzed) {
        // 搜索extractor
        Map> allExtractors = ((ResolveLanguagesResult)this.callPlumbingInProcess(ResolveLanguagesCommand.class, new Object[]{this.options.extractorOptions})).getExtractorRoots();
        List found = (List)allExtractors.get(language);
        Path packRoot = (Path)found.get(0);
        // 创建extractor对象
        CodeQLExtractor extractor = new CodeQLExtractor(packRoot);
        DbInfo dbInfo = new DbInfo(this.sourceRoot.toString(), extractor.usesUnicodeNewlines(), extractor.getColumnKind(), language, allExtractors, linesOfCode, (String)shaAnalyzed.orElse((Object)null), CodeQLVersion.currentVersion().version);
        // 创建 skeleton
        DatabaseLayout layout = DatabaseLayout.create(databaseDir, dbInfo);
    }` 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    运行完成后,数据库目录下会出现 codeql-database.yml 文件

    `java-sec-code $ tree -L 1
    .
    ├── codeql-database.yml
    └── log` 
    
    • 1
    • 2
    • 3
    • 4

    initalize 部分返回以后,就进入了 build 部分,这里我们先调试几步,调用链如下

    com.semmle.cli2.picocli.SubcommandCommon#runPlumbingInProcess
    com.semmle.cli2.picocli.PlumbingRunner#run
    com.semmle.cli2.database.TraceCommandCommand#executeSubcommand
    com.semmle.cli2.database.DatabaseProcessCommandCommon#executeSubcommand` 
    
    • 1
    • 2
    • 3
    • 4

    这个 executeSubcommand 方法很长,我们关注他进行的两个关键操作。

    一是读取 compile.spec 文件,创建 Tracer ,对应代码如下

    TracerSetup tracerSetup = this.getTracerSetup(this.logger(), databases, scratchFolder, logFolder, extractors);` 
    
    • 1

    getTracerSetup 里面又调用了 getTracingSpec

    `extractor.getTracingSpec().get()` 
    
    • 1

    内容如下,这里 getTracingSpec 会去找 extractor 根目录下的 tools/compile.spec 文件并读取

    `public Optional getTracingSpec() {
        Path tools = this.extractorRoot.resolve("tools");
        Path platformTools = tools.resolve(CodeQLDist.currentPlatform().name());
        Iterator var3 = Arrays.asList(platformTools.resolve("compiler-tracing.spec"), tools.resolve("compiler-tracing.spec")).iterator();
    
        Path candidate;
        do {
            if (!var3.hasNext()) {
                return Optional.empty();
            }
    
            candidate = (Path)var3.next();
        } while(!Files.isRegularFile(candidate, new LinkOption[0]) || !Files.isReadable(candidate));
    
        return Optional.of(candidate);
    }` 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    用于示例的是 javaextractor ,我们很容易找到对应的 compile.spec ,内容如下

    `jvm_prepend_arg -javaagent:${config_dir}/codeql-java-agent.jar=ignore-project,java
    jvm_prepend_arg -Xbootclasspath/a:${config_dir}/codeql-java-agent.jar`
    
    • 1
    • 2

    可见CodeQL会在build前准备好调用 code-java-agent.jar 相关的参数

    二是创建进程,运行build指令。

    `Builder8 p = new Builder8(cmdArgs, LogbackUtils.streamFor(this.logger(), "build-stdout", true), LogbackUtils.streamFor(this.logger(), "build-stderr", true), Env.systemEnv().getenv(), workingDir.toFile());
    this.env.addToProcess(p);
    List cmdProcessor = new ArrayList();
    CommandLine.addCommandProcessor(cmdProcessor, this.env.expander);
    p.prependArgs(cmdProcessor);
    tracerSetup.enableTracing(p);
    StreamAppender streamOutAppender = new StreamAppender(Streams.out());
    
    int result;
    try {
            LogbackUtils.addAppender(streamOutAppender);
        result = p.execute();
    } finally {
        LogbackUtils.removeAppender(streamOutAppender);
    }` 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    经过一番设置,进程运行时的命令行如下

    `codeql-home/codeql/tools/osx64/preload_tracer mvn clean package` 
    
    • 1

    关键环境变量如下

    `CODEQL_EXTRACTOR_JAVA_ROOT -> codeql-home/codeql/java
    CODEQL_SCRATCH_DIR -> codeql-home/database/java-database/java-sec-code/working
    CODEQL_EXTRACTOR_JAVA_LOG_DIR -> codeql-home/database/java-database/java-sec-code/log
    CODEQL_EXTRACTOR_JAVA_SOURCE_ARCHIVE_DIR -> codeql-home/database/java-database/java-sec-code/src
    CODEQL_EXTRACTOR_JAVA_TRAP_DIR -> codeql-home/database/java-database/java-sec-code/trap/java
    SEMMLE_JAVA_TOOL_OPTIONS -> '-javaagent:codeql-home/codeql/java/tools/codeql-java-agent.jar=ignore-project,java' '-Xbootclasspath/a:codeql-home/codeql/java/tools/codeql-java-agent.jar'` 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    因为这里调用的 preload_tracer 为二进制文件,所以直接分析它的具体行为较为困难。

    但是我们可以推测出, preload_tracer 会监控编译的过程。当需要运行 JVM 时, preload_tracer 会添加准备好的 -javaagent 参数,使得 codeql-java-agent.jar 参与到编译过程中去。

    所以我们接下来的任务是分析 codeql-java-agent.jar 的行为

    1.3 codeql-java-agent.jar

    这一部分需要读者对于 java-agent 技术和 ASM 技术有一定了解

    java 源文件文件一般使用 javac 作为编译程序,生成类文件。但是 javac 仅仅是一个封装程序,其实际的编译操作是调用 com.sun.tools.javac 包下的类来完成的。如果使用 java-agent 技术,劫持 com.sun.tools.javac 包下的关键方法,就能自定义编译行为。

    我们编写如下代码来调试 codeql-java-agent.jar

    `package cokeBeer;
    import com.sun.tools.javac.main.Main;
    import com.sun.tools.javac.util.Context;
    
    public class  RunAgent  {
        public  static  void  main(String[]  args)  throws  Exception{
            Main main=new Main("");
            String[] arg=new String[]{"Test.java"};
            main.compile(arg,new Context());
            System.out.println("run agent");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    为了调试 codeql-java-agent.jar ,首先将其作为库文件导入IDEA,然后在运行配置中添加 vmoptions 如下

    `-javaagent:your-codeql-home/codeql/java/tools/codeql-java-agent.jar=ignore-project,java` 
    
    • 1

    同时在运行配置中添加环境变量如下

    `CODEQL_EXTRACTOR_JAVA_ROO
    • 相关阅读:
      【MySQL】数据类型——MySQL的数据类型分类、数值类型、小数类型、字符串类型
      4.14每日一题(二元函数求极值:常规方法、先代后求法)
      迎战秋招计划
      Android的本地数据
      pytorch环境配置教程(基于Anaconda)
      Flink动态业务规则的实现
      力扣 1662. 检查两个字符串数组是否相等
      mysql集群的主从复制搭建
      【AI语言大模型】文心一言功能使用介绍
      基于nodejs+vue语言的酒店管理系统
    • 原文地址:https://blog.csdn.net/band_mmbx/article/details/126406879