tabby 是一款基于 soot 实现的java静态代码分析工具,用于分析jar包,生成代码属性图。结合手工可以半自动地完成java反序列化链挖掘工作。
我们从tabby的源代码进行分析,根据源代码的逻辑结构,可以看出其工作流程分成下面几个部分
加载配置文件,设置分析选项
分析类、方法和关联信息
使用soot进行污点分析、生成调用边
保存代码属性图`
其中使用soot进行污点分析的部分是本文的重点
1.1 加载配置文件,设置分析选项
tabby的入口方法为 tabby.App#run
,代码如下
@Bean
CommandLineRunner run(){
return args -> {
try{
if(!JavaVersion.isJDK8()){
throw new JDKVersionErrorException("Error JDK version. Please using JDK8.");
}
loadProperties("config/settings.properties");
applyOptions();
analyser.run(props);
log.info("Done. Bye!");
System.exit(0);
}catch (IllegalArgumentException e){
log.error(e.getMessage() +
"\nPlease use java -jar tabby target_directory [--isJDKOnly|--isJDKProcess|--isSaveOnly|--excludeJDK] !" +
"\ntarget_directory 为相对路径" +
"\n--isJDKOnly出现时,仅处理JDK的内容" +
"\n--excludeJDK出现时,不添加当前运行jre环境" +
"\n--isJDKProcess出现时,将处理当前运行jre环境的分析" +
"\nExample: java -jar tabby cases/jars --isJDKProcess" +
"\nOthers: https://github.com/wh1t3p1g/tabby/wiki/Tabby%E9%A3%9F%E7%94%A8%E6%8C%87%E5%8C%97");
}catch (JDKVersionErrorException e){
log.error(e.getMessage());
}
};
}`
这里tabby通过一个 .properties
文件来加载配置,配置选项如下
`# build code property graph
tabby.build.enable = true
# jdk settings
tabby.build.isJDKProcess = true
tabby.build.withAllJDK = false
tabby.build.excludeJDK = false
tabby.build.isJDKOnly = false
# dealing fatjar
tabby.build.checkFatJar = true
# default pointed-to analysis
tabby.build.isFullCallGraphCreate = true
# targets to analyse
tabby.build.target = path/to/target
tabby.build.libraries = path/to/lib
# load to neo4j
tabby.load.enable = false
# debug
tabby.debug.details = false
tabby.debug.inner.details = false`
这里关注一下 isFullCallGraphCreate
这个选项。如果为 true
,那么分析时使用 FullCallGraphScanner
类,如果为 false
那么使用 CallGraphScanner
类。因为 FullCallGraphScanner
类只是简单的生成调用图,而 CallGraphScanner
类会对输入进行污点分析,所以后续分析时主要针对 CallGraphScanner
类。
加载配置完成以后,代码进入 tabby.core.Analyser#run
方法,先配置soot分析目标所在的 classpath
`public void run(Properties props) throws IOException {
if("true".equals(props.getProperty(ArgumentEnum.BUILD_ENABLE.toString(), "false"))){
Map<String, String> dependencies = getJdkDependencies(
props.getProperty(ArgumentEnum.WITH_ALL_JDK.toString(), "false"));
log.info("Get {} JDK dependencies", dependencies.size());
Map<String, String> cps = "true".equals(props.getProperty(ArgumentEnum.EXCLUDE_JDK.toString(), "false"))?
new HashMap<>():new HashMap<>(dependencies);
Map<String, String> targets = new HashMap<>();
// 收集目标
if("false".equals(props.getProperty(ArgumentEnum.IS_JDK_ONLY.toString(), "false"))){
String target = props.getProperty(ArgumentEnum.TARGET.toString());
boolean checkFatJar = "true".equals(props.getProperty(ArgumentEnum.CHECK_FAT_JAR.toString(), "false"));
Map<String, String> files = FileUtils.getTargetDirectoryJarFiles(target, checkFatJar);
cps.putAll(files);
targets.putAll(files);
}
if("true".equals(props.getProperty(ArgumentEnum.IS_JDK_ONLY.toString(), "false"))
|| "true".equals(props.getProperty(ArgumentEnum.IS_JDK_PROCESS.toString(), "false"))){
targets.putAll(dependencies);
}
// 添加必要的依赖,防止信息缺失,比如servlet依赖
if(FileUtils.fileExists(GlobalConfiguration.LIBS_PATH)){
Map<String, String> files = FileUtils
.getTargetDirectoryJarFiles(GlobalConfiguration.LIBS_PATH, false);
for(Map.Entry<String, String> entry:files.entrySet()){
cps.putIfAbsent(entry.getKey(), entry.getValue());
}
}
runSootAnalysis(targets, new ArrayList<>(cps.values()));
}`
然后进入 tabby.core.Analyser#runSootAnalysis
方法
public void runSootAnalysis(Map<String, String> targets, List<String> classpaths){
try{
SootConfiguration.initSootOption();
addBasicClasses();
// set class paths
Scene.v().setSootClassPath(String.join(File.pathSeparator, new HashSet<>(classpaths)));
// get target filepath
List<String> realTargets = getTargets(targets);
if(realTargets.isEmpty()){
log.info("Nothing to analysis!");
return;
}
Main.v().autoSetOptions();
// 类信息抽取
classInfoScanner.run(realTargets);
// 函数调用分析
if(GlobalConfiguration.IS_FULL_CALL_GRAPH_CONSTRUCT){
fullCallGraphScanner.run();
}else{
callGraphScanner.run();
}
rulesContainer.saveStatus();
}catch (CompilationDeathException e){
if (e.getStatus() != CompilationDeathException.COMPILATION_SUCCEEDED) {
throw e;
}
}`
它先调用了 addBasicClasses
方法,内部调用soot提供的API,加载 rules/basicClasses.json
中配置的类文件
`public void addBasicClasses(){
List<String> basicClasses = rulesContainer.getBasicClasses();
for(String cls:basicClasses){
Scene.v().addBasicClass(cls ,HIERARCHY);
}
}`
rules/basicClasses.json
默认的内容如下
`[
"io.netty.channel.ChannelFutureListener",
"scala.runtime.java8.JFunction2$mcIII$sp",
"scala.runtime.java8.JFunction1$mcII$sp",
"scala.runtime.java8.JFunction0$mcV$sp",
"scala.runtime.java8.JFunction0$mcZ$sp",
"scala.runtime.java8.JFunction0$mcJ$sp",
"scala.runtime.java8.JFunction0$mcI$sp",
"scala.runtime.java8.JFunction1$mcZJ$sp",
"scala.runtime.java8.JFunction1$mcZI$sp",
"scala.runtime.java8.JFunction1$mcVI$sp",
"scala.runtime.java8.JFunction0$mcD$sp",
"scala.runtime.java8.JFunction0$mcF$sp",
"scala.runtime.java8.JFunction0$mcS$sp",
"scala.runtime.java8.JFunction0$mcB$sp",
"com.codahale.metrics.Gauge"
]`
然后调用 Scene.v().setSootClassPath()
,设置soot加载目标文件用到的 classpath
,之后soot加载 target
时,会在这里设置的 classpath
范围内进行搜索
`public void setSootClassPath(String p) {
sootClassPath = p;
SourceLocator.v().invalidateClassPath();
}`
然后调用 getTargets
方法,获取目标类文件的绝对路径,保存到 realTargets
变量中
`public List<String> getTargets(Map<String, String> targets){
Set<String> stuff = new HashSet<>();
List<String> newIgnore = new ArrayList<>();
targets.forEach((filename, filepath) -> { if(!rulesContainer.isIgnore(filename)){ stuff.add(filepath); newIgnore.add(filename); } }); rulesContainer.getIgnored().addAll(newIgnore); log.info("Total analyse {} targets.", stuff.size()); Options.v().set_process_dir(new ArrayList<>(stuff)); return new ArrayList<>(stuff); }`
到这里分析需要的soot选项、 classpath
参数和 target
参数就配置好了
完成了上面的配置以后,tabby使用 ClassInfoScanner
来分析类信息和方法信息。
这里定位到 tabby.core.scanner.ClassInfoScanner#run
方法,代码如下,传入的参数为刚才配置的 target
,也就是分析目标类的路径
`public void run(List<String> paths){
// 多线程提取基础信息
Map<String, CompletableFuture<ClassReference>> classes = loadAndExtract(paths);
transform(classes.values()); // 等待收集结束,并保存classRef
List<String> runtimeClasses = new ArrayList<>(classes.keySet());
classes.clear();
// 单线程提取关联信息
buildClassEdges(runtimeClasses);
save();
}`
首先看 loadAndExtract
方法,它接收类文件的路径作为参数。里面先调用soot提供的API loadBasicClasses
和 loadDynamicClasses
,加载和项目无关,但是必要的类文件。然后使用 getClassesUnder
方法,根据项目文件的路径获取项目中类文件的路径,并使用 loadClassAndSupport
方法将其加载为 SootClass
类型的对象。然后对于每一个 SootClass
类型的对象,调用 collector.collect
方法,收集保存类、方法信息。
`public Map<String, CompletableFuture<ClassReference>> loadAndExtract(List<String> targets){
Map<String, CompletableFuture<ClassReference>> results = new HashMap<>();
Scene.v().loadBasicClasses();
Scene.v().loadDynamicClasses();
int counter = 0;
log.info("Start to collect {} targets' class information.", targets.size());
for (final String path : targets) {
for (String cl : SourceLocator.v().getClassesUnder(path)) {
try{
SootClass theClass = Scene.v().loadClassAndSupport(cl);
if (!theClass.isPhantom()) {
// 这里存在类数量不一致的情况,是因为存在重复的对象