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);
}
用于示例的是 java
的 extractor
,我们很容易找到对应的 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_ROOT=your-codeql-home/codeql/java
CODEQL_EXTRACTOR_JAVA_LOG_DIR=your-test-dir/log
再找到入口方法 com.semmle.extractor.java.InterceptingAgent#premain
打上断点&#