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
打上断点,就能够开始调试了
`public static void premain(String agentArgs, Instrumentation inst) {inst.addTransformer(new InterceptingAgent(agentArgs, new Interceptor[0]));
}`
这里咱们看到 premain
创立了一个 InterceptingAgent
类型的对象,而后增加为 Transformer
咱们先看 InterceptingAgent
的构造方法
`public InterceptingAgent(String agentArgs, Interceptor... extraInterceptors) {
// 略去局部无关代码
Set<String> args = new LinkedHashSet(Arrays.asList(agentArgs.split(",")));
Iterator var6 = args.iterator();
while(var6.hasNext()) {String arg = (String)var6.next();
if (!arg.equals("ignore-project")) {if (arg.equals("java")) {this.interceptors.add(new JavacMainInterceptor());
this.interceptors.add(new JavacToolInterceptor());
this.interceptors.add(new ECJInterceptor());
this.interceptors.add(new TakariLifecycleJdtInterceptor());
if (Boolean.parseBoolean(System.getenv("CODEQL_EXTRACTOR_JAVA_JSP"))) {this.interceptors.add(new JasperJdtInterceptor());
this.interceptors.add(new JasperJspcInterceptor());
}
} else if (arg.equals("kotlin")) {this.interceptors.add(new KotlinInterceptor());
} else {warn(1, "Unrecognized agent specification:" + arg);
}
}
}
}`
能够看出,依据输出参数的不同,会创立不同类型的 Interceptor
,插入到 this.interceptors
去。这里咱们的输出参数为 ignore-project,java
,所以会插入 JavacMainInterceptor
和 JavacToolInteceptor
而后咱们看 InterceptingAgent
的 tranform
办法,这个办法会在类加载时被零碎被动回调
`public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {if (loader == null && !bootstrapLoadableClasses.contains(className)) {info(2, "Skipping bootstrap-loaded class" + className);
return null;
} else if ((!className.startsWith("java/") || className.equals("java/lang/Shutdown")) && !className.startsWith("javax/") && !className.startsWith("sun/")) {if (className.startsWith("com/semmle/extractor/java/interceptors/")) {info(2, "Skipping intercept handler class" + className);
return null;
} else if (className.startsWith("jdk/internal/reflect/")) {info(2, "Skipping reflection class" + className);
return null;
} else if (className.startsWith("com/semmle/org/objectweb/asm/")) {info(2, "Skipping ASM class" + className);
return null;
} else {
boolean intercept = false;
Iterator var7 = this.interceptors.iterator();
while(var7.hasNext()) {Interceptor i = (Interceptor)var7.next();
if (i.interceptType(className)) {
intercept = true;
break;
}
}
// 对于须要拦挡的类,接下来应用 ASM 技术进行剖析
...
}
} else {info(2, "Skipping system class" + className);
return null;
}
}`
能够看到 if-else
判断过滤了 java
的内置类,以及 CodeQL
自身蕴含的类
而后遍历 this.interceptors
,调用 interceptType
办法进行判断。interceptType
办法要求输出的类名必须和 interceptor
内置的拦挡类名统一
例如 JavacMainInterceptor
,它的内置的拦挡类就是 com.sun.tools.javac.main.Main
`public boolean interceptType(String binaryTypeName) {return binaryTypeName.equals("com/sun/tools/javac/main/Main");
}`
当胜利匹配当前,就应用 ASM
技术,对这个类进行革新。调用 ASM
的代码如下
`if (!intercept) {info(2, "Skipping class with no interested interceptor:" + className);
return null;
} else {info(1, "Transforming" + className);
try {ClassReader reader = new ClassReader(classfileBuffer);
if ((reader.getAccess() & 512) != 0) {info(2, "Skipping interface" + className);
return null;
} else {ClassWriter writer = new ClassWriter(reader, 1);
reader.accept(new RewriteMethods(writer, className, this.collectMemberSignatures(classfileBuffer)), 0);
return writer.toByteArray();}
} catch (RuntimeException var9) {log("ERROR: Exception while processing" + className + ":" + var9);
var9.printStackTrace(System.out);
log("Current class loader:" + loader);
throw var9;
}
}`
这里是创立了一个 RewriteMethods
类型的对象,继承 ASM
中的 ClassVistor
,来重写类文件。这个 RewriteMethods
次要做两件事件,一是拦挡并革新特定类办法,这里须要看 visitMethod
办法,它创立了一个 InterceptMethod
类型的对象
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {return new InterceptMethod(super.visitMethod(access, name, desc, signature, exceptions), access, name, desc);
}`
InterceptMethod
又继承了 ASM
中的 MethodVistor
,它实现了 applyInterceptors
办法,外部会尝试遍历 this.interceptors
保留的 Interceptor
,而后调用他们的 intercept
办法。
`private void applyInterceptors(boolean before) {InterceptingAgent.info(3, "Considering method" + this.name + this.desc + "in" + RewriteMethods.this.binaryTypeName + ".");
Iterator var2 = InterceptingAgent.this.interceptors.iterator();
while(var2.hasNext()) {Interceptor i = (Interceptor)var2.next();
try {
// 这里调用了上面的 applyInterceptor
this.applyInterceptor(i, before);
} catch (Throwable var5) {InterceptingAgent.log("ERROR: Interceptor of type" + i.getClass() + "caused an exception:" + var5);
var5.printStackTrace(System.out);
}
}
}
private void applyInterceptor(Interceptor i, boolean before) {if (i.interceptType(RewriteMethods.this.binaryTypeName)) {Interceptor.Interception interception = i.intercept(RewriteMethods.this.binaryTypeName, RewriteMethods.this.classMembers, this.name, this.desc, before);
if (interception != null) {InterceptingAgent.info(1, "Interceptor" + i + "wants to call" + interception + "for" + RewriteMethods.this.binaryTypeName + "." + this.name + this.desc + ".");
this.instrument(interception);
} else {InterceptingAgent.info(2, "Interceptor" + i + "is not interested in" + RewriteMethods.this.binaryTypeName + "." + this.name + this.desc + ".");
}
}
}`
这里调试时会调用到 JavacMainInterceptor
的 intercept
办法,外面拦挡 com.sun.tools.java.main.Main
类型的两个 compile
办法,这两个办法都是负责编译源文件的办法
`public Interceptor.Interception intercept(String binaryTypeName, Set<String> classMembers, String methodName, String methodDescriptor, boolean before) {if (before) {return null;} else {if (methodName.equals("compile")) {if (methodDescriptor.equals("([Ljava/lang/String;Lcom/sun/tools/javac/util/Context;)Lcom/sun/tools/javac/main/Main$Result;")) {return new Interceptor.Interception("com/semmle/extractor/java/interceptors/JavacMainInterceptor", "void javacMainResult(Object,String[])", new Interceptor.CallWith[]{CallWith.STACK_TOP, CallWith.FIRST_ARG});
}
if (methodDescriptor.equals("([Ljava/lang/String;Lcom/sun/tools/javac/util/Context;)I")) {return new Interceptor.Interception("com/semmle/extractor/java/interceptors/JavacMainInterceptor", "int javacMainInt(int,String[])", new Interceptor.CallWith[]{CallWith.FIRST_ARG});
}
}
return null;
}
}`
而后创立对应的 Interception
类型的对象并返回,从 applyInterceptor
办法中看到返回值会被传递给 instrument
办法,这个办法的向类字节码中写入了一个办法调用 SEMMLE_INTERCEPT$0
`private void instrument(Interceptor.Interception interception) {Integer idx = (Integer)RewriteMethods.this.applicableInterceptions.get(interception);
if (idx == null) {idx = RewriteMethods.this.applicableInterceptions.size();
RewriteMethods.this.applicableInterceptions.put(interception, idx);
}
Interceptor.CallWith[] var3 = interception.callWith();
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {Interceptor.CallWith cw = var3[var5];
switch (cw) {
case ALL_ARGS:
this.loadArgs();
break;
case ALL_ARGS_AS_ARRAY:
this.loadArgArray();
break;
case FIRST_ARG:
this.loadArg(0);
break;
case CLASS:
this.visitLdcInsn(RewriteMethods.this.binaryTypeName);
break;
case METHOD_NAME_AND_DESC:
this.visitLdcInsn(this.name);
this.visitLdcInsn(this.desc);
break;
case STACK_TOP:
this.visitInsn(89);
break;
case THIS:
if (!this.isStatic && !this.name.equals("<init>")) {this.visitVarInsn(25, 0);
} else {this.visitInsn(1);
}
}
}
Method method = Method.getMethod(interception.methodDecl());
this.visitMethodInsn(184, RewriteMethods.this.binaryTypeName, "SEMMLE_INTERCEPT$" + idx, method.getDescriptor(), false);
}`
RewriteMethods
做的第二件事件是创立一个新办法,这个办法就是下面调用的办法 SEMMLE_INTERCEPT$0
这个一部分对应着它的 visitEnd
办法,外面应用 ASM
技术,结构了这个新办法。
为了直观展现,咱们间接获取最终转换好的字节码进行反编译。最终发生变化的局部如下
`public Result compile(String[] var1, Context var2) {Result var10000 = this.compile(var1, var2, List.nil(), (Iterable)null);
SEMMLE_INTERCEPT$0(var10000, var1);
return var10000;
}
private static void SEMMLE_INTERCEPT$0(Object var0, String[] var1) {
Object var10000 = var0;
String[] var10001 = var1;
try {JavacMainInterceptor.javacMainResult(var10000, var10001);
} catch (NoClassDefFoundError var2) {System.err.println("ERROR: Exception during invocation of Semmle Java compiler. Perhaps you need to put odasa-agent.jar on the boot classpath?");
var2.printStackTrace(System.err);
}
}`
能够看到,新的 compile
办法获取原 compile
办法的输出参数和编译返回值,而后交给 javacMainResult
解决
`@InterceptionMethod
public static void javacMainResult(Object result, String[] args) {info(1, "Intercepted javac Main.compile(String[],Context):" + Arrays.toString(args));
String resultName = result.toString();
int javacExitCode = getJavacExitCode(resultName);
int odasaJavacExitCode = Utils.invokeOdasaJavac(javacExitCode, args);
if (javacExitCode == 0 && odasaJavacExitCode != 0) {throw new Error("Fatal extractor error detected. Attempting to abort build commands.");
}
}`
外面调用 Utils.invoke0dasaJavac
,之后的调用链如下
`com.semmle.extractor.java.Utils#invokeOdasaJavac(int, java.lang.String[])
com.semmle.extractor.java.Utils#invokeOdasaJavac(int, java.lang.String[], boolean)
com.semmle.extractor.java.Utils#invokeOdasaJavac(int, java.lang.String[], boolean, java.util.Map<java.lang.String,java.lang.String>)`
最初一个 invoke0dasaJavac
办法外部首先配置一系列的环境变量、设置命令行参数,参数内容如下
`codeql-home/codeql/java/tools/macos/jdk-extractor-java/bin/java
-Dfile.encoding=UTF-8
-Xmx1024M
-Xms256M
--add-opens
java.base/sun.reflect.annotation=ALL-UNNAMED
-classpath
codeql-home/codeql/java/tools/semmle-extractor-java.jar
com.semmle.extractor.java.JavaExtractor
--jdk-version
-1
--javac-args
@@@/your-test-dir/log/ext/javac.args`
而后应用这些参数创立一个程序对象并执行
Builder b = new Builder(cmdLine, System.out, System.err);
b.removeEnvVar("JAVA_TOOL_OPTIONS");
Iterator var38 = addEnv.entrySet().iterator();
while(var38.hasNext()) {Map.Entry<String, String> entry = (Map.Entry)var38.next();
b.putEnvVar((String)entry.getKey(), (String)entry.getValue());
}
exitCode = b.execute();`
所以这里就是应用 CodeQL 内置的 java
命令行程序调用 semmle-extractor-java.jar
有了这些参数,咱们能够被动调用 semmle-extractor-java.jar
了
运行 semmle-extractor-java.jar
会解析我的项目源代码,生成 trap
文件
这里咱们将 semmle-extractor-java.jar
作为依赖库增加到 IDEA
并编写如下代码来调试 semmle-extractor-java.jar
,其中调用参数来自下面的剖析过程
`package cokeBeer;
import com.semmle.extractor.java.JavaExtractor;
import java.io.File;
public class RunExtractor {public static void main(String[] args) {String argPath="@@@/your-test-dir/log/ext/javac.args");
String[] ExtractorArgs=new String[]{"--jdk-version","-1","--javac-args",argPath};
JavaExtractor.main(ExtractorArgs);
}
}`
为了调试 semmle-extractor-java.jar
,首先将其作为库文件导入 IDEA,而后在运行配置中增加环境变量如下
`TRAP_FOLDER=your-test-dir/trap/java
SOURCE_ARCHIVE=your-test-dir/src`
在入口办法处打上断点,开始调试。JavaExtractor#main
首先创立一个 JavaExtractor
类型的对象
`public static void main(String[] args) {String allArgs = StringUtil.glue(" ", args);
JavaExtractor extractor = new JavaExtractor(args);
boolean hasJavacErrors = false;
try {hasJavacErrors = !extractor.runExtractor();
} catch (Throwable var8) {...} finally {extractor.close();
}
}`
而后运行 com.semmle.extractor.java.JavaExtractor#runExtractor
办法,外面应用 JavacCompiler
对源文件进行解析,而后利用解析信息生成 trap
文件
`boolean runExtractor() {
// 省略了局部日志相干代码
// 筹备编译环境
Context context = this.output.getContext();
JavacFileManager.preRegister(context, this.specialSourcepathHandling);
Arguments arguments = this.setupJavacOptions(context);
Options.instance(context).put("ignore.symbol.file", "ignore.symbol.file");
JavaFileManager jfm = (JavaFileManager)context.get(JavaFileManager.class);
JavaFileManager bfm = jfm instanceof DelegatingJavaFileManager ? ((DelegatingJavaFileManager)jfm).getBaseFileManager() : jfm;
JavacFileManager dfm = (JavacFileManager)bfm;
dfm.handleOptions(arguments.getDeferredFileManagerOptions());
arguments.validate();
if (jfm.isSupportedOption(Option.MULTIRELEASE.primaryName) == 1) {Target target = Target.instance(context);
List<String> list = List.of(target.multiReleaseValue());
jfm.handleOption(Option.MULTIRELEASE.primaryName, list.iterator());
}
JavaCompiler compiler = JavaCompiler.instance(context);
compiler.genEndPos = true;
Set<JavaFileObject> fileObjects = arguments.getFileObjects();
// 解析源文件
javac_extend.com.sun.tools.javac.util.List<JCTree.JCCompilationUnit> parsedFiles = compiler.parseFiles(fileObjects);
compiler.enterTrees(compiler.initModules(parsedFiles));
Queue<Queue<javac_extend.com.sun.tools.javac.comp.Env<AttrContext>>> groupedTodos = Todo.instance(context).groupByFile();
int prevErr = 0;
while(true) {while(true) {
JCTree.JCCompilationUnit cu;
while(true) {
Queue todo;
do {
cu = null;
Iterator var23 = todo.iterator();
while(var23.hasNext()) {javac_extend.com.sun.tools.javac.comp.Env<AttrContext> env = (javac_extend.com.sun.tools.javac.comp.Env)var23.next();
if (cu == null) {cu = env.toplevel;} else if (cu != env.toplevel) {throw new CatastrophicError("Not grouped by file: CUs" + cu + "and" + env.toplevel);
}
}
} while(cu == null);
try {Queue<javac_extend.com.sun.tools.javac.comp.Env<AttrContext>> queue = compiler.attribute(todo);
String envFlowChecks = System.getenv("CODEQL_EXTRACTOR_JAVA_FLOW_CHECKS");
if (envFlowChecks == null || Boolean.valueOf(envFlowChecks)) {compiler.flow(queue);
}
break;
} catch (StackOverflowError | Exception var36) {this.logThrowable(cu, var36);
}
}
try {CharSequence cachedContent = dfm.getCachedContent(cu.getSourceFile());
if (cachedContent == null) {
try {cachedContent = cu.getSourceFile().getCharContent(false);
} catch (IOException var37) {this.logThrowable(cu, var37);
continue;
}
}
String contents = ((CharSequence)cachedContent).toString();
// 抽取解析信息,创立 trap 文件
(new CompilationUnitExtractor(this.output, cu, this.dw)).process(contents);
} catch (StackOverflowError | Exception var38) {this.logThrowable(cu, var38);
}
break;
}
}
}`
咱们进入最初生成 trap
文件的办法 com.semmle.extractor.java.CompilationUnitExtractor#process
外面创立了 JavaTrapWriter
类型的对象,而后顺次调用各种 Extractor
,抽取信息写入 trap
文件
`public void process(String contents) {JavaFileObject sourceFile = this.compilationUnit.getSourceFile();
if (sourceFile.getKind() == Kind.SOURCE) {File file = PathTransformer.std().canonicalFile(sourceFile.getName());
String outputPath = ClassFileLocations.getClassFileLocation(sourceFile.getName()).getOutputPath();
File outputFile = PathTransformer.std().canonicalFile(outputPath);
this.output.setCurrentSourceFile(outputFile);
OdasaOutput.TrapLocker trapLocker = this.output.getTrapLockerForCurrentSourceFile();
try {
// 创立 writer
OdasaOutput.JavaTrapWriter writer = trapLocker.getTrapWriter();
try {if (writer != null) {OnDemandExtractor onDemand = new OnDemandExtractor(this.output, writer, this.dw);
TreeExtender treeExtender = new TreeExtender(file, contents, this.compilationUnit, this.dw);
// 抽取编译单元信息
this.extractCompilationUnit(contents, writer, onDemand, treeExtender);
Iterator var10 = this.compilationUnit.getTypeDecls().iterator();
while(var10.hasNext()) {JCTree aClass = (JCTree)var10.next();
if (aClass instanceof JCTree.JCClassDecl) {
// 抽取 AST 信息
(new ClassDeclExtractor(writer, treeExtender, onDemand, (JCTree.JCClassDecl)aClass, this.compilationUnit, this.dw)).process();}
}
treeExtender.writeCommentData(writer);
// 抽取类、办法的根本信息以及继承和隶属信息
onDemand.extract();
String rootUri = Env.systemEnv().get("CODEQL_EXTRACTOR_JAVA_JSP_ROOT_URI");
String destDir = Env.systemEnv().get("CODEQL_EXTRACTOR_JAVA_JSP_DEST_DIR");
if (rootUri != null && destDir != null) {String packge = this.compilationUnit.packge.getQualifiedName().toString();
String smapClassName = packge + "/" + FileUtil.basename(outputFile);
(new SmapExtractor(outputFile, smapClassName, destDir, rootUri, this.output, writer, this.dw)).extract();}
}
} catch (Throwable var16) {if (writer != null) {
try {writer.close();
} catch (Throwable var15) {var16.addSuppressed(var15);
}
}
throw var16;
}
if (writer != null) {writer.close();
}
} catch (Throwable var17) {if (trapLocker != null) {
try {trapLocker.close();
} catch (Throwable var14) {var17.addSuppressed(var14);
}
}
throw var17;
}
if (trapLocker != null) {trapLocker.close();
}
}
}`
先看 extractCompilationUnit
办法,它向 trap
文件写入包名称信息以及导入信息
`private void extractCompilationUnit(String contents, TrapWriter writer, OnDemandExtractor onDemand, TreeExtender treeExtender) {this.output.writeCurrentSourceFileToSourceArchive(contents);
TrapWriter.Label compilationUnitId = treeExtender.writeSourceFile(writer);
TrapWriter.Label packageId = onDemand.getPackageKey(this.compilationUnit.packge);
writer.addTuple(JavaTable.CuPackage, new Object[]{compilationUnitId, packageId});
Iterator var7 = this.compilationUnit.getImports().iterator();
while(var7.hasNext()) {JCTree.JCImport i = (JCTree.JCImport)var7.next();
classifyImport(treeExtender, writer, onDemand, i);
}
}`
而后是 com.semmle.extractor.java.ClassDeclExtractor#process
办法,它拜访整个语法树,向 trap
文件写入表达式和语句信息
`public void process() {this.log.info("Processing file" + this.compilationUnit.getSourceFile().getName());
this.visitTree(this.classToExtract);
}`
而后是 com.semmle.extractor.java.OnDemandExtractor#extract
办法,其外部会调用
`com.semmle.extractor.java.OnDemandExtractor#extractModules
com.semmle.extractor.java.OnDemandExtractor#extractJarInfo`
别离抽取模块信息和 jar
包清单信息
而后调用 com.semmle.extractor.java.OnDemandExtractor#extractMembersToCurrentWriter
办法,抽取成员变量和成员办法信息
实现剖析当前,之前设置的 trap
目录 your-test-dir/trap/java
下就会呈现多个 trap.gz
文件,这里咱们简略解压一个来剖析一下局部内容
源代码
`public static void main(String[] args) {System.out.println("hello");
}`
生成后果
#[[email protected]](https://tttang.com/cdn-cgi/l/email-protection)"callable;{#10007}.main({#10020}){#10009}"
#[[email protected]](https://tttang.com/cdn-cgi/l/email-protection)"loc,{#10000},2,24,2,27"
locations_default(#10029,#10000,2,24,2,27)
hasLocation(#10028,#10029)
numlines(#10028,3,3,0)
#10030=*
stmts(#10030,0,#10028,0,#10028)
#10031=*
locations_default(#10031,#10000,2,44,4,5)
hasLocation(#10030,#10031)
numlines(#10030,3,3,0)
#10032=*
exprs(#10032,62,#10009,#10028,-1)
callableEnclosingExpr(#10032,#10028)
#10033=*
locations_default(#10033,#10000,2,19,2,22)
hasLocation(#10032,#10033)
numlines(#10032,1,1,0)
#[[email protected]](https://tttang.com/cdn-cgi/l/email-protection)"params;{#10028};0"
params(#10034,#10020,0,#10028,#10034)
paramName(#10034,"args")
#[[email protected]](https://tttang.com/cdn-cgi/l/email-protection)"loc,{#10000},2,29,2,41"
locations_default(#10035,#10000,2,29,2,41)
hasLocation(#10034,#10035)
#10036=*
exprs(#10036,63,#10020,#10034,-1)
callableEnclosingExpr(#10036,#10028)
#10037=*
locations_default(#10037,#10000,2,29,2,36)
hasLocation(#10036,#10037)
numlines(#10036,1,1,0)
#10038=*
exprs(#10038,62,#10019,#10036,0)
callableEnclosingExpr(#10038,#10028)
#10039=*
locations_default(#10039,#10000,2,29,2,34)
hasLocation(#10038,#10039)
numlines(#10038,1,1,0)
#10040=*
stmts(#10040,14,#10030,0,#10028)
#10041=*
locations_default(#10041,#10000,3,9,3,36)
hasLocation(#10040,#10041)
numlines(#10040,1,1,0)
#10042=*
exprs(#10042,61,#10009,#10040,0)
callableEnclosingExpr(#10042,#10028)
statementEnclosingExpr(#10042,#10040)
#10043=*
locations_default(#10043,#10000,3,9,3,35)
hasLocation(#10042,#10043)
numlines(#10042,1,1,0)
#10044=*
#[[email protected]](https://tttang.com/cdn-cgi/l/email-protection)"class;java.io.PrintStream"
exprs(#10044,60,#10045,#10042,-1)
callableEnclosingExpr(#10044,#10028)
statementEnclosingExpr(#10044,#10040)
#10046=*
locations_default(#10046,#10000,3,9,3,18)
hasLocation(#10044,#10046)
numlines(#10044,1,1,0)
#[[email protected]](https://tttang.com/cdn-cgi/l/email-protection)"callable;{#10045}.println({#10019}){#10009}"
callableBinding(#10042,#10047)
#10048=*
exprs(#10048,22,#10019,#10042,0)
callableEnclosingExpr(#10048,#10028)
statementEnclosingExpr(#10048,#10040)
#10049=*
locations_default(#10049,#10000,3,28,3,34)
hasLocation(#10048,#10049)
numlines(#10048,1,1,0)
#10050=*
#[[email protected]](https://tttang.com/cdn-cgi/l/email-protection)"class;java.lang.System"
exprs(#10050,62,#10051,#10044,-1)
callableEnclosingExpr(#10050,#10028)
statementEnclosingExpr(#10050,#10040)
#10052=*
locations_default(#10052,#10000,3,9,3,14)
hasLocation(#10050,#10052)
numlines(#10050,1,1,0)
#[[email protected]](https://tttang.com/cdn-cgi/l/email-protection)"field;{#10051};out"
variableBinding(#10044,#10053)
namestrings("""hello""","hello",#10048)`
从最初面的 #10050=*
开始剖析,这里示意刷新标签,无具体含意,然而能够被其余变量绑定为 ID
接下来的 #[[email protected]](https://tttang.com/cdn-cgi/l/email-protection)"class;java.lang.System"
示意一个全局 gloablID
,其值为 10051
再下来的 exprs(#10050,62,#10051,#10044,-1)
示意向名为 exprs
的代码表中插入一条记录,具体记录的含意能够在下面工作流程概览局部外面列举到的文件 semmlecode.dbscheme
中找到
`#keyset[parent,idx]
exprs(
unique int id: @expr,
int kind: int ref,
int typeid: @type ref,
int parent: @exprparent ref,
int idx: int ref
);`
对应起来就是 id
为 10050
,kind
为 62
,typeid
为 10051
(也就是下面记录的 java.lang.System
类型 ),parent
为 10044
,idx
为 -1
通过了下面几步,trap
文件胜利地被生成了。接下来就是将 trap
文件导入到代码数据库中。
当初进入最初的 finalize 局部,调用链如下
`com.semmle.cli2.picocli.SubcommandCommon#runPlumbingInProcess
com.semmle.cli2.picocli.PlumbingRunner#run
com.semmle.cli2.database.FinalizeCommand#executeSubcommand
com.semmle.cli2.database.FinalizeCommand#finalizeOne`
咱们看 finalizeOne
办法的实现,它首先运行 pre-finalize.sh
文件,次要目标是为数据库建设索引。而后调用 doTrapImport
办法,导入 trap
文件
`private void finalizeOne(DatabaseLayout dbLayout) throws SubcommandDone {Path databaseDir = dbLayout.getDatabasePath();
if (dbLayout.isFinalized()) {throw new UserError("Database" + databaseDir + "is already finalized.");
} else if (!Files.exists(dbLayout.getSourceArchiveRoot(), new LinkOption[0])) {if (this.params.skipEmpty()) {this.printWarning(this.emptyDatabaseMessage(databaseDir), new Object[0]);
} else {this.printError(this.emptyDatabaseMessage(databaseDir), new Object[0]);
throw new SubcommandDone(32);
}
} else {
this.foundOneNonEmpty = true;
// 执行 pre-finalize.sh
if (!this.params.suppressPreFinalize()) {dbLayout.getExtractor().getPreFinalizeScript().ifPresent((script) -> {Path workingDir = Paths.get(dbLayout.getSourceLocationPrefix());
this.printProgress("Running pre-finalize script {} in {}.", new Object[]{script, workingDir});
int result = this.runPlumbingInProcess(TraceCommandCommand.class, new Object[]{"--working-dir=" + workingDir, "--no-tracing", threadsOption(this.importOptions.getThreads()), ramOption(this.importOptions.getRam()), "--", databaseDir, script});
if (result != 0) {throw new UserError("Failed to execute pre-finalize script in" + databaseDir + "[exit code:" + result + "].");
}
});
}
writeSourceLocationPrefixTrap(dbLayout);
List<Path> trapFolders = Collections.singletonList(dbLayout.getTrapFolder());
doTrapImport(this, dbLayout, this.importOptions, this.privateImportOptions, trapFolders);
dbLayout.markAsFinalized();
if (!this.params.suppressCleanup()) {this.runPlumbingInProcess(CleanupDatabaseCommand.class, new Object[]{this.params.cleanupParams, "--", databaseDir});
}
}
}`
接着看到 doTrapImport
办法,外面先获取数据库的 schmema
文件,而后持续调用 import
指令
`static void doTrapImport(SubcommandCommon owner, DatabaseLayout dbLayout, ImportOptions importOptions, PrivateImportOptions privateImportOptions, List<Path> trapPaths) {owner.printProgress("Running TRAP import for {}...", new Object[]{dbLayout});
SimpleTimer timer = new SimpleTimer();
Path dbscheme = importOptions.getDbscheme();
if (dbscheme == null) {Either<Path, String> detectedDbscheme = dbLayout.getExtractor().getDbscheme();
if (!detectedDbscheme.isLeft()) {throw new UserError((String)detectedDbscheme.getRight());
}
dbscheme = (Path)detectedDbscheme.getLeft();}
List<Object> importCommandArgs = new ArrayList(Arrays.asList(importOptions.getRam() != null ? ResolveRamCommand.createHeapSizeOption(importOptions.getRam()) : Collections.EMPTY_LIST, "--dbscheme=" + dbscheme, threadsOption(importOptions.getThreads()), privateImportOptions, "--", dbLayout.getDatasetPath()));
importCommandArgs.addAll(trapPaths);
int result;
if (importOptions.getRam() != null) {result = owner.spawnPlumbingAsChildProcess(ImportCommand.class, (RamOptions)null, importCommandArgs.toArray());
} else {result = owner.runPlumbingInProcess(ImportCommand.class, importCommandArgs.toArray());
}
if (result != 0) {throw new UserError("Dataset import for" + dbLayout.getDatasetPath() + "failed with code" + result + ".");
} else {owner.printProgress("TRAP import complete ({}).", new Object[]{timer});
}
}`
import
指令的调用栈如下
`com.semmle.cli2.picocli.SubcommandCommon#runPlumbingInProcess
com.semmle.cli2.picocli.PlumbingRunner#run
com.semmle.cli2.database.FinalizeCommand#executeSubcommand
com.semmle.cli2.ql.dataset.ImportCommand#executeSubcommand`
executeSubcommand
的实现如下,构建了一个 TrapImporter
类型对象,而后调用 run
办法
`protected void executeSubcommand() throws SubcommandDone {if (Files.exists(this.datasetDir, new LinkOption[0]) && !FileUtil8.isEmptyDirectory(this.datasetDir)) {if (!Files.isDirectory(this.datasetDir, new LinkOption[]{LinkOption.NOFOLLOW_LINKS})) {throw new UserError("Dataset" + this.datasetDir + "exists, but is not a directory.");
}
if (!Files.isDirectory((new IMBDiskLayout(this.datasetDir, new Context("default"))).getIdPoolDir(), new LinkOption[0])) {throw new UserError("Dataset" + this.datasetDir + "has been finalized and does not support further TRAP import.");
}
FileUtil8.strictRecursiveDelete(IMBDiskLayout.getCacheDir(this.datasetDir.resolve("default")));
}
AtomicBoolean hasErrors = new AtomicBoolean(false);
TrapImporter importer = new TrapImporter(new TRAPReaderConfig(this.privateOptions.checkUndefinedLabels(), this.privateOptions.checkUnusedLabels(), this.privateOptions.checkRepeatedLabels(), this.privateOptions.checkRedefinedLabels(), this.privateOptions.checkUseBeforeDefinition(), this.privateOptions.locationInStar(), (error) -> {hasErrors.set(true); this.printError(error, new Object[0]); }), this.datasetDir, "default", new CachingMode(), threadsOptionValue(this.threads)); try {importer.run(Arrays.asList(this.trapPaths), this.dbscheme); } catch (Throwable var6) {try { importer.close(); } catch (Throwable var5) {var6.addSuppressed(var5); } throw var6; } importer.close(); if (this.privateOptions.failOnErrors() && hasErrors.get()) {this.printError("Aborting as some errors occured during TRAP import.", new Object[0]); throw new SubcommandDone(2); } }`
run
办法外部最终实现了导入
`public void run(List<Path> trapRoots, Path targetDbscheme) {AtomicInteger totalNumTrapFilesCounter = new AtomicInteger(0);
CancellationToken cancelToken = new CancellationToken();
CompletableFuture<List<TrapTask<LinkTarget[]>>> tasks = this.scanAndLink(trapRoots, totalNumTrapFilesCounter, this.executor);
try {tasks.thenCompose((taskList) -> {return ImportTasksProcessor.importTrap(this.loadDbSchemeBinding(targetDbscheme), this.backend, this.executor, this.trapReaderConfig, new LogProgressTracker(totalNumTrapFilesCounter.get()), taskList, cancelToken); }).join();} catch (CompletionException var7) {logger.error("An exception occurred during TRAP import. The database may be partial.", var7); throw Exceptions.asUnchecked(var7.getCause()); } this.copyDbSchemeFile(targetDbscheme); logger.info("Finished importing trap files."); }`
从下面的剖析过程中能够看出,CodeQL 其实不须要 java
我的项目真的能够胜利编译,它只须要剖析源码获取语法树即可。那么咱们能够思考跳过编译这一步,间接利用 semmle-java-extractor.jar
生成数据库,这正好能够解决某些场景下,只有反编译出的 java
源代码,然而不能胜利编译的问题。
这里还是以 java-sec-code 这个我的项目为例子,为了模仿无源码的环境,上面咱们只应用编译好的 jar
包
上面一共用到三个要害文件夹
/Users/cokeBeer/decompiled
:反编译出的源代码的地位/Users/cokeBeer/test
:semmle-extractor-java
的工作文件夹/Users/coekBeer/nonsource
:最终生成数据库的地位
首先用 IDEA 提供的 fernflower 反编译工具对 java-sec-code-1.0.0.jar
进行反编译
`java -jar java-decompiler.jar -dgs=1 java-sec-code-1.0.0.jar <your-dst-dir>`
这里我应用的指标文件夹为 /Uesrs/cokeBeer/decompiled
而后在 /Uesrs/cokeBeer/decompiled
下找到反编译好的 java-sec-code-1.0.0.jar
,应用上面指令解压
`jar -xvf java-sec-code-1.0.0.jar`
而后在解压后的文件外面找到 BOOT-INF/classes
文件夹,这外面保留了反编译好的我的项目文件
`classes $ tree -L 1
.
├── application.properties
├── banner.txt
├── create_db.sql
├── logback-online.xml
├── mapper
├── org
├── static
├── templates
└── url`
上面咱们先进行 initialize 环节,新建一个文件夹 nonsource
,应用上面的指令初始化数据库
`codeql database init -l java --source-root org /Users/cokeBeer/nonsource`
这里的 source-root
参数就设置为下面解压出的源代码文件夹 org
回顾咱们之前调试的过程,在实现 codeql-java-agent.jar
的解决当前,test
文件夹外面应该呈现一个 log
文件夹,外面保留了调用 semmle-extractor-java.jar
须要的参数 javac.args
`log $ tree -L 2
.
├── ext
│ ├── javac.args
│ ├── javac.env
│ ├── javac.orig
│ └── javac.properties
└── javac-errors.log`
而后咱们能够利用这个文件作为输出,调用 semmle-extractor-java.jar
`package cokeBeer;
import com.semmle.extractor.java.JavaExtractor;
import java.io.File;
public class RunExtractor {public static void main(String[] args) {
// 在这里用到了
String argPath="@@@/Users/cokeBeer/test/log/ext/javac.args");
String[] ExtractorArgs=new String[]{"--jdk-version","-1","--javac-args",argPath};
JavaExtractor.main(ExtractorArgs);
}
}`
同时须要配置环境变量,阐明生成 trap
文件和 src
相干文件的地位,留神这里先输入到 test
文件夹
`TRAP_FOLDER=/Users/cokeBeer/test/trap/java
SOURCE_ARCHIVE=/Users/cokeBeer/test/src`
咱们看一下 javac.args
的内容
`-Xprefer:source
-source
1.8
-target
1.8
-classpath
...
-extdirs
...
-endorseddirs
...
-bootclasspath
...
Test.java`
它的最初一行传入了编译的指标,那么咱们只须要在这里替换了输出文件,就能正确调用
当初先回到 /Users/cokeBeer/decompiled/BOOT-INF/classes
文件夹,应用上面指令找到所有须要编译的 java
文件
`find org -name *.java > sources.txt`
sources.txt
可能是绝对目录,能够应用 vscode
批量替换为相对目录
`/Users/cokeBeer/decompiled/BOOT-INF/classes/org/joychou/imageConfig.java
/Users/cokeBeer/decompiled/BOOT-INF/classes/org/joychou/Application.java
/Users/cokeBeer/decompiled/BOOT-INF/classes/org/joychou/util/LoginUtils.java
/Users/cokeBeer/decompiled/BOOT-INF/classes/org/joychou/util/HttpUtils.java
/Users/cokeBeer/decompiled/BOOT-INF/classes/org/joychou/util/WebUtils.java
...`
而后将输出替换为 @/Users/cokeBeer/decompiled/BOOT-INF/classes/source.txt
`-Xprefer:source
-source
1.8
-target
1.8
-classpath
...
-extdirs
...
-endorseddirs
...
-bootclasspath
...
@/Users/cokeBeer/decompiled/BOOT-INF/classes/source.txt`
而后运行之前调试 semmle-extractor-java.jar
时配置好的代码,即可在 /Users/cokeBeer/test/trap/java
文件夹下找到 trap
文件
`org $ tree -L 2
.
└── joychou
├── Application.java.set
├── Application.java.trap.gz
├── RMI
├── config
├── controller
├── dao
├── filter
├── imageConfig.java.set
├── imageConfig.java.trap.gz
├── mapper
├── security
└── util`
当初 trap
文件曾经生成结束,最初就是 finalize 阶段
先将 /Users/cokeBeer/test
下生成的 trap
和 src
文件夹复制到 /Users/cokeBeer/nonsource
而后运行上面的指令生成数据库
`codeql database finalize '/Users/cokeBeer/nosource'`
实现当前 /Users/cokeBeer/nonsource
目录构造应该如下
`nonsource $ tree -L 1
.
├── codeql-database.yml
├── db-java
├── log
└── src.zip`
上面到 vscode
中导入数据库,而后编写一个命令注入的污点剖析查问
`import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking
abstract class CommandInjectionSink extends DataFlow::Node {}
private class DefaultCommandInjectionSink extends CommandInjectionSink{DefaultCommandInjectionSink(){exists(ConstructorCall cc |cc.getAnArgument()=this.asExpr()|cc.getCallee().getDeclaringType() instanceof TypeProcessBuilder)
}
}
class CommandInjectionConfiguration extends TaintTracking::Configuration {CommandInjectionConfiguration() {this = "CommandInjection"}
override predicate isSource(DataFlow::Node source) {source instanceof RemoteFlowSource}
override predicate isSink(DataFlow::Node sink) {sink instanceof CommandInjectionSink}
}
from DataFlow::PathNode source, DataFlow::PathNode sink, CommandInjectionConfiguration conf
where conf.hasFlowPath(source, sink)
select source, sink`
胜利查问出已知破绽
`commandInjection.ql on nonsource - finished in 0 seconds (1 results) [2022/8/11 18:52:16]`
本文从调试的角度剖析了 CodeQL 数据库构建原理,介绍了 CodeQL 数据库构建过程中用到的一系列文件和参数的作用和含意。同时也演示了一种绕过编译过程的无源码构建数据库的计划
1.CodeQL 构建数据库的理论过程?
应用 java
提供的 API
解析源代码,生成 trap
文件,导入数据库
2. 无源代码构建的适用范围
CodeQL 在构建 trap
文件时只须要解析源代码,不必真正生成类文件,这部分工作属于编译的前端。所以只须要 CodeQL 调用的 API
可能胜利解析源代码,就能实现无源代码构建。