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

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` 

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

我们新建一个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);
    }
}` 

这里选择 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<com.semmle.cli2.picocli.SubcommandCommon,java.lang.Integer>, boolean)
java.util.function.Function#apply
com.semmle.cli2.picocli.SubcommandCommon#call
com.semmle.cli2.database.CreateCommand#executeSubcommand` 

最后是进入到了 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});
        }
}` 

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

`com.semmle.cli2.picocli.SubcommandCommon#runPlumbingInProcess
com.semmle.cli2.picocli.PlumbingRunner#run
com.semmle.cli2.database.InitCommand#executeSubcommand
com.semmle.cli2.database.InitCommand#initOneDatabase` 

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

private void initOneDatabase(String language, Path databaseDir, long linesOfCode, Optional<String> shaAnalyzed) {
    // 搜索extractor
    Map<String, List<Path>> allExtractors = ((ResolveLanguagesResult)this.callPlumbingInProcess(ResolveLanguagesCommand.class, new Object[]{this.options.extractorOptions})).getExtractorRoots();
    List<Path> 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);
}` 

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

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

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` 

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

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

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

getTracerSetup 里面又调用了 getTracingSpec

`extractor.getTracingSpec().get()` 

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

`public Optional<Path> 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);
}` 

用于示例的是 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`

可见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<String> 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);
}` 

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

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

关键环境变量如下

`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'` 

因为这里调用的 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");
    }
}

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

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

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

`CODEQL_EXTRACTOR_JAVA_ROO
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值