0 简介
tabby是一款基于soot实现的java静态代码分析工具,用于分析jar包,生成代码属性图。结合手工可以半自动地完成java反序列化链挖掘工作。
1 原理分析
我们从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
参数就配置好了
1.2 分析类、方法信息
完成了上面的配置以后,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();
}
1.2.1 loadAndExtract
首先看loadAndExtract
方法,它接收类文件的路径作为参数。里面先调用soot提供的APIloadBasicClasses
和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()) {
// 这里存在类数量不一致的情况,是因为存在重复的对象
results.put(cl, collector.collect(theClass));
theClass.setApplicationClass();
if(counter % 10000 == 0){
log.info("Collected {} classes.", counter);
}
counter++;
}
}catch (Exception e){
log.error("Load Error: " + e.getMessage());
// e.printStackTrace();
}
}
}
log.info("Collected {} classes.", counter);
return results;
}
1.2.2 ClassInfoCollector
这里的collector
为作者自己实现的类ClassInfoCollector
,里面提供了一系列方法,将SootClass
类型的对象保存的信息,存放到作者自己实现的ClassReference
类型的对象中,方便后续使用。这个collect
方法会调用到collect0
方法,里面先抽取出SootClass
中的信息作为ClassReference
类型的对象。
public static ClassReference collect0(SootClass cls, DataContainer dataContainer){
ClassReference classRef = ClassReference.newInstance(cls);
Set<String> relatedClassnames = getAllFatherNodes(cls);
classRef.setSerializable(relatedClassnames.contains("java.io.Serializable"));
classRef.setStrutsAction(relatedClassnames.contains("com.opensymphony.xwork2.ActionSupport")
|| relatedClassnames.contains("com.opensymphony.xwork2.Action"));
// 提取类函数信息
if(cls.getMethodCount() > 0){
for (SootMethod method : cls.getMethods()) {
extractMethodInfo(method, classRef, relatedClassnames, dataContainer);
}
}
return classRef;
}
然后对于每个SootClass
类型的对象里面保存的SootMethod
类型的对象,调用extractMethodInfo
方法将SootMethod
类型的对象构建为作者自己实现的MethodReference
类型的对象
public static void extractMethodInfo(SootMethod method,
ClassReference ref,
Set<String> relatedClassnames,
DataContainer dataContainer
){
RulesContainer rulesContainer = dataContainer.getRulesContainer();
String classname = ref.getName();
MethodReference methodRef = MethodReference.newInstance(classname, method);
TabbyRule.Rule rule = rulesContainer.getRule(classname, methodRef.getName());
if (rule == null) { // 对于ignore类型,支持多级父类和接口的规则查找
for (String relatedClassname : relatedClassnames) {
TabbyRule.Rule tmpRule = rulesContainer.getRule(relatedClassname, methodRef.getName());
if (tmpRule != null && tmpRule.isIgnore()) {
rule = tmpRule;
break;
}
}
}
boolean isSink = false;
boolean isIgnore = false;
boolean isSource = false;
if(rule != null && (rule.isEmptySignaturesList() || rule.isContainsSignature(methodRef.getSignature()))){
// 当rule存在signatures时,该rule为精确匹配,否则为模糊匹配,仅匹配函数名是否符合
isSink = rule.isSink();
isIgnore = rule.isIgnore();
isSource = rule.isSource();
// 此处,对于sink、know、ignore类型的规则,直接选取先验知识
// 对于source类型 不赋予其actions和polluted
if (!isSource) {
Map<String, String> actions = rule.getActions();
List<Integer> polluted = rule.getPolluted();
if(isSink){
methodRef.setVul(rule.getVul());
}
methodRef.setActions(actions!=null?actions:new HashMap<>());
methodRef.setPollutedPosition(polluted!=null?polluted:new ArrayList<>());
methodRef.setActionInitialed(true);
if(isIgnore){// 不构建ignore的类型
methodRef.setInitialed(true);
}
}
}
methodRef.setSink(isSink);
methodRef.setIgnore(isIgnore);
methodRef.setSource(isSource);
methodRef.setEndpoint(ref.isStrutsAction() || isEndpoint(method, relatedClassnames));
methodRef.setNettyEndpoint(isNettyEndpoint(method, relatedClassnames));
methodRef.setGetter(isGetter(method));
methodRef.setSetter(isSetter(method));
methodRef.setSerializable(relatedClassnames.contains("java.io.Serializable"));
methodRef.setAbstract(method.isAbstract());
methodRef.setHasDefaultConstructor(ref.isHasDefaultConstructor());
methodRef.setFromAbstractClass(ref.isAbstract());
Has has = Has.newInstance(ref, methodRef);
ref.getHasEdge().add(has);
dataContainer.store(has);
dataContainer.store(methodRef);
}
然后根据rules/knowledges.json
中预定的规则,设置源点、汇点和其他一些附加属性。我们取出其中一个看一下
{"name":"java.lang.ClassLoader", "rules": [
{"function": "newInstance", "type": "sink", "vul": "CODE", "actions": {"return": "this"}, "polluted": [-1], "signatures": []}
]}
其中的参数含义如下
name: 表示类名称,这里是java.lang.ClassLoader
rules: 表示类具有的方法,可以有多个
function: 表示方法名称,这里是newInstance
type: 表示方法的类型,这里是sink类型
vul: 表示漏洞的类型,这里是代码执行
actions: 表示方法内部的污点操作,这里的return表示返回值的污点参数来自this
polluted: 表示sink方法的污点参数,这里-1表示污点来自类属性
signatures: 表示方法的签名,也就是描述信息
1.2.3 buildClassEdges
完成这一步以后tabby.core.scanner.ClassInfoScanner#run
方法会调用tabby.core.scanner.ClassInfoScanner#buildClassEdges
,进一步构建类和方法之间的关系
public void buildClassEdges(List<String> classes){
int counter = 0;
int total = classes.size();
log.info("Build {} classes' edges.", total);
for(String cls:classes){
if(counter%10000 == 0){
log.info("Build {}/{} classes.", counter, total);
}
counter++;
ClassReference clsRef = dataContainer.getClassRefByName(cls);
if(clsRef == null) continue;
extractRelationships(clsRef, dataContainer, 0);
}
log.info("Build {}/{} classes.", counter, total);
}
它的内部再调用tabby.core.scanner.ClassInfoScanner#extractRelationships
方法,其内部利用之前保存的类和方法信息,建立继承、接口和别名信息。
public static void extractRelationships(ClassReference clsRef, DataContainer dataContainer, int depth){
// 建立继承关系
if(clsRef.isHasSuperClass()){
ClassReference superClsRef = dataContainer.getClassRefByName(clsRef.getSuperClass());
if(superClsRef == null && depth < 10){ // 正常情况不会进入这个阶段
superClsRef = collect0(clsRef.getSuperClass(), null, dataContainer, depth+1);
}
if(superClsRef != null){
Extend extend = Extend.newInstance(clsRef, superClsRef);
clsRef.setExtendEdge(extend);
dataContainer.store(extend);
}
}
// 建立接口关系
if(clsRef.isHasInterfaces()){
List<String> infaces = clsRef.getInterfaces();
for(String inface:infaces){
ClassReference infaceClsRef = dataContainer.getClassRefByName(inface);
if(infaceClsRef == null && depth < 10){// 正常情况不会进入这个阶段
infaceClsRef = collect0(inface, null, dataContainer, depth+1);
}
if(infaceClsRef != null){
Interfaces interfaces = Interfaces.newInstance(clsRef, infaceClsRef);
clsRef.getInterfaceEdge().add(interfaces);
dataContainer.store(interfaces);
}
}
}
// 建立函数别名关系
makeAliasRelations(clsRef, dataContainer);
}
到这里基本的类、方法、继承、接口和别名信息就分析完成了,下面使用污点分析来进一步提取方法内部的信息。
1.3 使用soot进行污点分析
这里我们设置
isFullCallGraphCreate
选项为false
,也就是使用CallGraphScanner
类来进行污点分析。同时默认读者对于soot有一定的了解,如果没有,可以先学习最后附上的参考文献。
这里走进第else
分支
if(GlobalConfiguration.IS_FULL_CALL_GRAPH_CONSTRUCT){
fullCallGraphScanner.run();
}else{
callGraphScanner.run();
}
方法调用依次为
tabby.core.scanner.CallGraphScanner#run
tabby.core.scanner.CallGraphScanner#collect
tabby.core.collector.CallGraphCollector#collect
我们看最后一个方法collect
的实现。首先根据之前分析好的方法属性进行一些简单的判断,然后调用Switcher.doMethodAnalysis
进行污点分析
public void collect(MethodReference methodRef, DataContainer dataContainer){
try{
SootMethod method = methodRef.getMethod();
if(method == null) return; // 提取不出内容,不分析
if(method.isPhantom() || methodRef.isSink()
|| methodRef.isIgnore() || method.isAbstract()
|| Modifier.isNative(method.getModifiers())){
methodRef.setInitialed(true);
return; // sink点为不动点,无需分析该函数内的调用情况 native/抽象函数没有具体的body
}
if(method.isStatic() && method.getParameterCount() == 0){
// 静态函数 且 函数入参数量为0 此类函数
// 对于反序列化来说 均不可控 不进行分析
methodRef.setInitialed(true);
return;
}
Context context = Context.newInstance(method.getSignature(), methodRef);
PollutedVarsPointsToAnalysis pta =
Switcher.doMethodAnalysis(
context, dataContainer,
method, methodRef);
context.clear();
}catch (RuntimeException e){
e.printStackTrace();
}
}
我们看doMethodAnalysis
方法的实现,它调用了method.retrieveActiveBody
方法
public static PollutedVarsPointsToAnalysis doMethodAnalysis(Context context,
DataContainer dataContainer,
SootMethod method,
MethodReference methodRef){
try{
...
JimpleBody body = (JimpleBody) method.retrieveActiveBody();
UnitGraph graph = new BriefUnitGraph(body);
PollutedVarsPointsToAnalysis pta =
PollutedVarsPointsToAnalysis
.makeDefault(methodRef, body, graph,
dataContainer, context, !methodRef.isActionInitialed());
methodRef.setInitialed(true);
methodRef.setActionInitialed(true);
return pta;
}catch (RuntimeException e){
e.printStackTrace();
}
return null;
}
method.retrieveActiveBody
方法是soot提供的API。soot可以将java的字节码转化为jimple
语言的表达形式。
jimple语言是soot支持一种IR,即中间代码。jimple中所包含的语句种类数量远小于java字节码的操作码种类数量
通过将java字节码转化为jimple表示形式,可以大大简化污点分析代码实现的繁琐程度
得到Body
对象以后,再转化为BriefUnitGraph
类型的对象,准备进行数据流分析
1.3.1 PollutedVarsPointsToAnalysis
数据流分析调用了PollutedVarsPointsToAnalysis.makeDefault
,这是作者封装的一个静态方法。makeDefualt
的内容就是构建一个PollutedVarsPointsToAnalysis
类型的对象,并且调用doAnalysis
方法
public static PollutedVarsPointsToAnalysis makeDefault(MethodReference methodRef,
Body body,
DirectedGraph<Unit> graph,
DataContainer dataContainer,
Context context, boolean reset){
PollutedVarsPointsToAnalysis analysis = new PollutedVarsPointsToAnalysis(graph);
// 配置switchers
StmtSwitcher switcher = new SimpleStmtSwitcher();
SimpleLeftValueSwitcher leftSwitcher = new SimpleLeftValueSwitcher();
leftSwitcher.setReset(reset);
switcher.setReset(reset);
switcher.setMethodRef(methodRef);
switcher.setLeftValueSwitcher(leftSwitcher);
switcher.setRightValueSwitcher(new SimpleRightValueSwitcher());
// 配置pta依赖
analysis.setBody(body);
analysis.setDataContainer(dataContainer);
analysis.setStmtSwitcher(switcher);
analysis.setContext(context);
analysis.setMethodRef(methodRef);
// 进行分析
analysis.doAnalysis();
return analysis;
}
用到的类tabby.core.toolkit.PollutedVarsPointsToAnalysis
是作者自己实现的,它继承了soot提供的soot.toolkits.scalar.ForwardFlowAnalysis
类,用来进行数据流分析。
这里作者重写了下面几个方法
PollutedVarsPointsToAnalysis#PollutedVarsPointsToAnalysis 构造方法,用来自定义初始化
PollutedVarsPointsToAnalysis#doAnalysis 数据流分析的核心逻辑所在的方法
PollutedVarsPointsToAnalysis#flowThrough 说明当数据流通过一个单元时,应当如何变化
PollutedVarsPointsToAnalysis#newInitialFlow 如何初始化一个数据流
PollutedVarsPointsToAnalysis#copy 如何赋值一个数据流
PollutedVarsPointsToAnalysis#merge 两组数据流交汇时,如何合并
这里我们来看作者实现的doAnalysis
方法,它使用了getUseAndDefBoxes
方法,获取jimple
语言表示下的所有局部变量定义和调用情况,然后基于基础数据类型、局部变量、实例成员和数组四种数据类型的判断,分别处理以后保存到InitialMap
中。这样InitialMap
中就保存了方法调用过程中所有可能用到的局部变量。
public void doAnalysis(){
for(ValueBox box:body.getUseAndDefBoxes()){
Value value = box.getValue();
Type type = value.getType();
if(type instanceof PrimType){ // 对于基础数据类型 直接跳过
continue;
}
if(value instanceof Local && !initialMap.containsKey(value)){
initialMap.put((Local) value, TabbyVariable.makeLocalInstance((Local) value));
}else if(value instanceof InstanceFieldRef){
InstanceFieldRef ifr = (InstanceFieldRef) value;
SootField sootField = ifr.getField();
SootFieldRef sfr = ifr.getFieldRef();
String signature = null;
if(sootField != null){
signature = sootField.getSignature();
}else if(sfr != null){
signature = sfr.getSignature();
}
Value base = ifr.getBase();
if(base instanceof Local){
TabbyVariable baseVar = initialMap.get(base);
if(baseVar == null){
baseVar = TabbyVariable.makeLocalInstance((Local) base);
initialMap.put((Local) base, baseVar);
}
TabbyVariable fieldVar = baseVar.getField(signature);
if(fieldVar == null){
if(sootField != null){
fieldVar = TabbyVariable.makeFieldInstance(baseVar, sootField);
}else if(sfr != null){
fieldVar = TabbyVariable.makeFieldInstance(baseVar, sfr);
}
if(fieldVar != null && signature != null){
fieldVar.setOrigin(value);
baseVar.addField(signature, fieldVar);
}
}
}
}else if(value instanceof ArrayRef){
ArrayRef v = (ArrayRef) value;
Value base = v.getBase();
if(base instanceof Local){
TabbyVariable baseVar = initialMap.get(base);
if(baseVar == null){
baseVar = TabbyVariable.makeLocalInstance((Local) base);
initialMap.put((Local) base, baseVar);
}
}
}
}
super.doAnalysis();
}
方法的最后调用父亲类型的doAnalysis
方法,这个方法会触发上面重写的,包括flowThrough
方法在内的多个方法
soot框架将方法内部的代码抽象成图中的一个个单元,每个单元可以是一个jimple
语言的语句。
每个单元都接受它的前驱单元的输出集合,作为它自己的输入集合,然后向它的后继单元提供一个输出集合
flowThrough
方法,是污点分析中的核心方法,它说明了当数据流通过一个单元时,输出相对输入应当如何变化。
通过重写flowThrough
方法,可以自定义输出相对输入变化的逻辑。
在污点分析中,实际上要做的就是结合当前单元的信息,更新保存了污点信息的局部变量表。
作者在这里先构建了一个Context
类型的对象,保存了输入信息in
和初始化信息initialMap
,然后调用d.apply
@Override
protected void flowThrough(Map<Local, TabbyVariable> in, Unit d, Map<Local, TabbyVariable> out) {
Map<Local, TabbyVariable> newIn = new HashMap<>();
copy(in, newIn);
context.setLocalMap(newIn);
context.setInitialMap(initialMap);
stmtSwitcher.setContext(context);
stmtSwitcher.setDataContainer(dataContainer);
d.apply(stmtSwitcher);
out.putAll(clean(context.getLocalMap()));
// out.putAll(context.getLocalMap());
// TODO 去掉非污染的变量
// 影响 加快了分析速度
// 但是丢失了一部分的关系边(暂未找到这部分缺失的影响,还需要进行实验)
// 这里暂时为了效率舍弃了部分可控边
}
这里传入了一个参数stmtSwticher
,内部会自动根据Unit
类型对象d
的具体实现类来调用不同的方法。这种写法可以理解为基于类型的swtich case
结构
例如InovkeStmt
的实现类soot.jimple.internal.JInvokeStmt
内部的apply
方法就会自动调用传入的Switch
类型对象的caseInovkeStmt
方法
public void apply(Switch sw) {
((StmtSwitch) sw).caseInvokeStmt(this);
}
1.3.2 SimpleStmtSwitcher
StmtSwitcher
的实现类为SimpleStmtSwitcher
,代码中主要处理了InvokeStmt
、AssignStmt
、IdentityStmt
、ReturnStmt
四种类型的实现,对应着方法调用、赋值、定义、返回四种类型的jimple语句
public class SimpleStmtSwitcher extends StmtSwitcher {
@Override
public void caseInvokeStmt(InvokeStmt stmt) {
...//对应着方法调用语句的处理
}
@Override
public void caseAssignStmt(AssignStmt stmt) {
...//对应着赋值语句的处理
}
@Override
public void caseIdentityStmt(IdentityStmt stmt) {
...//对应着标志语句的处理
}
@Override
public void caseReturnStmt(ReturnStmt stmt) {
...//对应着返回语句的处理
}
}
1.3.2.1 caseInvokeStmt
caseInvokeStmt部分处理单个方法调用语句
@Override
public void caseInvokeStmt(InvokeStmt stmt) {
// extract baseVar and args
InvokeExpr ie = stmt.getInvokeExpr();
if("<java.lang.Object: void <init>()>".equals(ie.getMethodRef().getSignature())) return;
if(GlobalConfiguration.DEBUG){
log.debug("Analysis: "+ie.getMethodRef().getSignature() + "; "+context.getTopMethodSignature());
}
Switcher.doInvokeExprAnalysis(stmt, ie, dataContainer, context);
if(GlobalConfiguration.DEBUG) {
log.debug("Analysis: " + ie.getMethodRef().getName() + " done, return to" + context.getMethodSignature() + "; "+context.getTopMethodSignature());
}
}
我们看它调用的关键方法Switcher.doInvokeExprAnalysis
,它负责分析调用表达式。
里面先使用Switcher.extractBaseVarFromInvokeExpr
和Switcher.extractArgsFromInvokeExpr
抽取出方法的调用者和参数,然后根据这两个参数获取获取污点位置
public static TabbyVariable doInvokeExprAnalysis(
Unit unit,
InvokeExpr invokeExpr,
DataContainer dataContainer,
Context context){
// extract baseVar and args
TabbyVariable baseVar = Switcher.extractBaseVarFromInvokeExpr(invokeExpr, context); // 调用对象
Map<Integer, TabbyVariable> args = Switcher.extractArgsFromInvokeExpr(invokeExpr, context);
// 检查当前的调用 是否需要分析 看入参、baseVar是否可控
List<Integer> pollutedPosition = pollutedPositionAnalysis(baseVar, args, context);
TabbyVariable firstPollutedVar = null;
boolean flag = false;
int index = 0;
for(Integer pos:pollutedPosition){
if(pos != PositionHelper.NOT_POLLUTED_POSITION){
if(index == 0){
firstPollutedVar = baseVar;
}else{
firstPollutedVar = args.get(index-1);
}
flag=true;
break;
}
index++;
}
...
}
然后进入pollutedPositionAnalysis
方法,里面首先获取baseVar
中的污点,保存到postions
里面,然后获取args
中的污点,也保存到postions
里面
public static List<Integer> pollutedPositionAnalysis(TabbyVariable baseVar,
Map<Integer, TabbyVariable> args,
Context context){
List<Integer> positions = new ArrayList<>();
// baseVar
positions.add(getPollutedPosition(baseVar));
// args
for(TabbyVariable var: args.values()){
positions.add(getPollutedPosition(var));
}
return positions;
}
public static int getPollutedPosition(TabbyVariable var){
if(var != null){
String related = null;
if(var.isPolluted(-1)){ // var本身是pollted的情况
related = var.getValue().getRelatedType();
}else if(var.containsPollutedVar(new ArrayList<>())){ // 当前var的类属性,element元素是polluted的情况
related = var.getFirstPollutedVarRelatedType();
}
if(related != null){
return PositionHelper.getPosition(related);
}
}
return PositionHelper.NOT_POLLUTED_POSITION;
}
这里解释一下postions
中保存的污点位置的含义。当一个方法中调用了另一个方法,我们把调用者称为源方法,被调用者称为目标方法。postions
使用一个整数数组来描述源方法的参数向目的方法的参数传递的情况。例如tabby文档中的例子可以解释如下
[0,-1,-3,1]
数组的第0个位置表示方法的直接调用者,如果是实例方法就是调用的实例对象,0说明这个对象来自源方法的第1个参数
数组的第i(i>0)个位置表示目标方法的第i个参数
例如数组第1个位置上的-1就表示目的方法的第1个参数来自源方法的所在类的成员变量
数组第2个位置上的-3表示目的方法的第2个参数不可控
数组第3个位置上的1表示目的方法的第3个参数来自源方法的第2个参数
对应的代码例子如下
private Object f1;
public void SourceMethod(Object p1,Object p2){
p1.TargetMethod(f1,"constant",p2);
}
获取了positions
数组以后,我们仅仅知道了污点的在原方法参数和目标方法参数间的传递情况,但是如果需要将数据流分析继续下去,我们还需要知道污点参数如何通过数据流。具体到这里的InovkeStmt
,就是要知道污点参数通过这个方法调用以后的变化情况。所以作者添加这段代码,新建一个Context
类型的对象subContext
,在这个上下文中递归分析使用到的方法
// try to analysis this method
if((!methodRef.isInitialed() || !methodRef.isActionInitialed()) // never analysis with pta
&& !context.isInRecursion(methodRef.getSignature())){ // not recursion
// 分析interfaceInvoke时,
// 由于获取到的method是没有函数内容的,所以需要找到对应的具体实现来进行分析
// 这里继续进行简化,对于无返回的函数调用,可以仍然保持原状,也就是舍弃了函数参数在函数体内可能发生的变化
// 对于有返回的函数调用,则找到一个会影响返回值的具体实现
Context subContext = context.createSubContext(methodRef.getSignature(), methodRef);
Switcher.doMethodAnalysis(subContext, dataContainer, invokedMethod, methodRef);
}
这个
Switcher.doMethodAnalysis
上面已经出现过了,它在入口处会对方法的类型做判断,如果是抽象类型则会直接跳过。这里可能会导致接口类型的方法调用返回值被认为是空值,以至于污点分析断开。
分析完成子方法以后,再利用分析子方法得到的污点传播信息(也就是MethodReference
类型对象的actions
成员,后面会仔细介绍),更新本地变量表中的污点信息,并调用buildCallRelationship
方法,构建上面介绍的污点边
// 参数修正,将从子函数的分析结果套用到当前的localMap
// 修正 入参和baseVar
for (Map.Entry<String, String> entry : methodRef.getActions().entrySet()) {
String position = entry.getKey();
String newRelated = entry.getValue();
if("return".equals(position))continue; // return的修正 不进行处理,由assign的时候自己去处理
TabbyVariable oldVar = parsePosition(position, baseVar, args, true);
TabbyVariable newVar = null;
if (oldVar != null) {
if ("clear".equals(newRelated)) {
oldVar.clearVariableStatus();
} else {
boolean remain = false;
if(newRelated != null && newRelated.contains("&remain")){
remain = true;
}
newVar = parsePosition(newRelated, baseVar, args, false);
oldVar.assign(newVar, remain);
}
}
}
...
buildCallRelationship(cls.getName(), context, optimize,
methodRef, dataContainer, unit, invokeType,
pollutedPosition);
1.3.2.2 caseAssignmentStmt
caseAssignmentStmt处理赋值语句,它将右边的操作数和左边的操作数分别交给SimpleRightValueSwitcher
类型的对象和
SimpleLeftValueSwitcher
类型的对象处理。这两个Swicher
的工作模式和SimpleStmtSwticher
的工作模式大同小异,都是根据不同的Value
输入类型,进行不同的处理,这里不做过多介绍。
public void caseAssignStmt(AssignStmt stmt) {
Value lop = stmt.getLeftOp();
Value rop = stmt.getRightOp();
TabbyVariable rvar = null;
boolean unbind = false;
rightValueSwitcher.setUnit(stmt);
rightValueSwitcher.setContext(context);
rightValueSwitcher.setDataContainer(dataContainer);
rightValueSwitcher.setResult(null);
rop.apply(rightValueSwitcher);
Object result = rightValueSwitcher.getResult();
if(result instanceof TabbyVariable){
rvar = (TabbyVariable) result;
}
if(rop instanceof Constant && !(rop instanceof StringConstant)){
unbind = true;
}
if(rvar != null && rvar.getValue() != null && rvar.getValue().getType() instanceof PrimType){
rvar = null; // 剔除基础元素的污点传递,对于我们来说,这部分是无用的分析
}
// 处理左值
if(rvar != null || unbind){
leftValueSwitcher.setContext(context);
leftValueSwitcher.setMethodRef(methodRef);
leftValueSwitcher.setRvar(rvar);
leftValueSwitcher.setUnbind(unbind);
lop.apply(leftValueSwitcher);
}
}
可以看到SimpleRightValueSwitcher
处理完右边的表达式以后,会把结果构造为一个对象,保存到自己的成员字段里面,然后暴露出getResult
方法给外界,用来获取处理的结果。SimpleLeftValueSwticher
则获取这个结果,完成整个AssignmentStmt
的分析。
1.3.2.3 caseIdentityStmt
caseIdentityStmt负责将函数的参数与本地变量关联起来。这里的函数参数,就是污点分析中用到的污点源,而本地变量,就是在PollutedVarsPointsToAnalysis#doAnalysis
方法入口处枚举保存好的。
@Override
public void caseIdentityStmt(IdentityStmt stmt) {
Value lop = stmt.getLeftOp();
Value rop = stmt.getRightOp();
if(rop instanceof ThisRef){
context.bindThis(lop);
}else if(rop instanceof ParameterRef){
ParameterRef pr = (ParameterRef)rop;
context.bindArg((Local)lop, pr.getIndex());
}
}
在jimple语言中,函数的参数值通过IdentityStmt
显式地赋值给局部变量,所以可以通过分析IdentityStmt
来获取最基本的污点参数传递信息。
例如下面这个exec
方法,接收一个String
类型的参数
public class Son implements SonInterface, Serializable {
private TransformerInterface transformerInterface;
@Override
public void exec(String command) {
try{
Runtime.getRuntime().exec(command);
}catch (Exception e){
e.printStackTrace();
}
}
}
在转换为jimple语言后,就会生成将参数和本地变量绑定起来的语句,这些语句就是IdentityStmt
public void exec(java.lang.String)
{
java.lang.Runtime $r0;
java.lang.String r1;
java.lang.Exception $r3;
Son r5;
r5 := @this: Son;
r1 := @parameter0: java.lang.String;
...
}
在soot中我们可以使用getRightOp
方法获取右边的参数,然后和本地变量绑定。
1.3.2.4 caseReturnStmt
caseReturnStmt负责记录函数的返回值和函数参数的关系。也是先将返回表达式交给SimpleRightValueSwitcher
类型的对象处理,然后提取返回结果。
@Override
public void caseReturnStmt(ReturnStmt stmt) {
Value value = stmt.getOp();
TabbyVariable var = null;
// 近似处理 只要有一种return的情况是可控的,就认为函数返回是可控的
// 并结算当前的入参区别
if(context.getReturnVar() != null && context.getReturnVar().containsPollutedVar(new ArrayList<>())) return;
rightValueSwitcher.setUnit(stmt);
rightValueSwitcher.setContext(context);
rightValueSwitcher.setDataContainer(dataContainer);
rightValueSwitcher.setResult(null);
value.apply(rightValueSwitcher);
var = (TabbyVariable) rightValueSwitcher.getResult();
context.setReturnVar(var);
if(var != null && var.isPolluted(-1) && reset){
methodRef.addAction("return", var.getValue().getRelatedType());
}
}
上面四种语句是tabby考虑的主要单元,通过对这些单元进行定制化处理,可以完成数据流分析的核心方法flowThrough
。super.doAnalysis
会自动回调式地触发PollutedVarsPointsToAnalysis
重写的方法,完成数据流分析。
1.4 保存结果
当加载的所有方法都被PollutedVarsPointsToAnalysis
分析完成时,CallGraphScanner
通过save
方法来保存分析结果
public void save() {
log.info("Save remained data to graphdb. START!");
dataContainer.save("class");
dataContainer.save("method");
dataContainer.save("has");
dataContainer.save("call");
dataContainer.save("alias");
dataContainer.save("extend");
dataContainer.save("interfaces");
log.info("Save remained data to graphdb. DONE!");
}
其内部通过dataContainer
成员的save
方法来保存分析结果,统一地保存了之前分析获取的类、方法、调用、继承和重写信息
public void save(String type){
switch (type){
case "class":
if(!savedClassRefs.isEmpty()){
List<ClassReference> list = new ArrayList<>(savedClassRefs.values());
savedClassRefs.clear();
classRefService.save(list);
}
break;
case "method":
if(!savedMethodRefs.isEmpty()){
List<MethodReference> list = new ArrayList<>(savedMethodRefs.values());
savedMethodRefs.clear();
methodRefService.save(list);
}
break;
case "has":
if(!savedHasNodes.isEmpty()){
relationshipsService.saveAllHasEdges(savedHasNodes);
savedHasNodes.clear();
}
break;
case "call":
if(!savedCallNodes.isEmpty()){
relationshipsService.saveAllCallEdges(savedCallNodes);
savedCallNodes.clear();
}
break;
case "extend":
if(!savedExtendNodes.isEmpty()){
relationshipsService.saveAllExtendEdges(savedExtendNodes);
savedExtendNodes.clear();
}
break;
case "interfaces":
if(!savedInterfacesNodes.isEmpty()){
relationshipsService.saveAllInterfacesEdges(savedInterfacesNodes);
savedInterfacesNodes.clear();
}
break;
case "alias":
if(!savedAliasNodes.isEmpty()){
relationshipsService.saveAllAliasEdges(savedAliasNodes);
savedAliasNodes.clear();
}
break;
}
}
之后这些信息都会被持久化到数据库中
2 数据结构补充介绍
上面的部分概括描述了tabby的工作流程,下面再介绍一些关键的数据结构,主要用于理解污点分析。
2.1 MethodReference
作为代表方法分析结果的类型,MethodReference
提供了actions
成员来保存方法内部的一些信息
private Map<String, String> actions = new ConcurrentHashMap<>();
public void addAction(String key, String value){
actions.put(key, value);
}
action
主要的类型有下面几种
this,param-0
param-1,param-2
param-1,clear
param-1,return
其中this
和param
类型还可以添加后缀,用来表示数组索引,或者是键名称,或者是一些额外说明
this|0
param-0|name
param-1&remain
action
说明了,在一个方法的内部,哪些参数发生了变化,即被赋值为另一个参数,被赋值为一个非污染值,或者被返回。
例如下面的方法
public void assign(Object p0, Object p1, Object p2, Object p3){
p0=p1;
p2="constant"
return p3;
}
就会产生如下action
param-0,param-1 表示1位置的参数赋值给0位置的参数
param-2,clear 表示2位置的参数丢失污点
prarm-3,return 表示3位置的参数被返回
action
的构建方法位于SimpleLeftValueSwitcher
中,根据右值的类型生成不同的action
/**
* case a = rvar
* @param v
*/
public void caseLocal(Local v) {
if(v.getType() instanceof PrimType) return; // 提出无用的类属性传递
TabbyVariable var = context.getOrAdd(v);
generateAction(var, rvar, -1, unbind);
if(unbind){
var.clearVariableStatus();
}else{
var.assign(rvar, false);
}
}
/**
* case Class.field = rvar
* @param v
*/
public void caseStaticFieldRef(StaticFieldRef v) {
if(v.getField().getType() instanceof PrimType) return; // 提出无用的类属性传递
TabbyVariable var = context.getOrAdd(v);
if(unbind){
context.unbind(v);
} else {
var.assign(rvar, false);
}
}
/**
* case a[index] = rvar
* @param v
*/
public void caseArrayRef(ArrayRef v) {
Value baseValue = v.getBase();
Value indexValue = v.getIndex();
TabbyVariable baseVar = context.getOrAdd(baseValue);
if (indexValue instanceof IntConstant) {
int index = ((IntConstant) indexValue).value;
generateAction(baseVar, rvar, index, unbind);
if(unbind){
baseVar.clearElementStatus(index);
}else{
baseVar.assign(index, rvar);
}
}else if(indexValue instanceof Local){
// 存在lvar = a[i2] 这种情况,暂无法推算处i2的值是什么,存在缺陷这部分;近似处理,添加到最后一个位置上
int size = baseVar.getElements().size();
generateAction(baseVar, rvar, size, unbind);
if(!unbind){
baseVar.assign(size, rvar);
}// 忽略可控性消除
}
}
public void generateAction(TabbyVariable lvar, TabbyVariable rvar, int index, boolean unbind){
if(!reset) return; // 不记录 actions
if(unbind && lvar.isPolluted(-1)){
if(index != -1){
methodRef.addAction(lvar.getValue().getRelatedType() + "|"+index, "clear");
}else{
methodRef.addAction(lvar.getValue().getRelatedType(), "clear");
}
}else if(lvar.isPolluted(-1)){
if(rvar != null && rvar.isPolluted(-1)){
if(index != -1){
methodRef.addAction(lvar.getValue().getRelatedType() + "|"+index, rvar.getValue().getRelatedType());
}else if(!lvar.getValue().getRelatedType().equals(rvar.getValue().getRelatedType())){
methodRef.addAction(lvar.getValue().getRelatedType(), rvar.getValue().getRelatedType());
}
}else{
if(index != -1){
methodRef.addAction(lvar.getValue().getRelatedType() + "|"+index, "clear");
}else{
methodRef.addAction(lvar.getValue().getRelatedType(), "clear");
}
}
}
}
对应的解析方法为Switcher
中的parsePosition
方法,从position
中提取出污点参数的赋值情况
public static TabbyVariable parsePosition(String position,
TabbyVariable baseVar,
Map<Integer, TabbyVariable> args,
boolean created){
if(position == null) return null;
TabbyVariable retVar = null;
String[] positions = position.split("\\|");
for(String pos:positions){
if(pos.contains("&remain")){ // 通常为 xxx&remain 表示 处理时需要保留原有的污点状态
pos = pos.split("&")[0];
}
if("this".equals(pos)){ // this
retVar = baseVar;
}else if(pos.startsWith("param-")){ // param-0
int index = Integer.valueOf(pos.split("-")[1]);
retVar = args.get(index);
}else if(retVar != null && StringUtils.isNumeric(pos)){ // 后续找element 类似this|0
int index = Integer.valueOf(pos);
TabbyVariable tempVar = retVar.getElement(index);
if(created && tempVar == null){
tempVar = TabbyVariable.makeRandomInstance();
boolean isPolluted = retVar.isPolluted(-1);
tempVar.getValue().setPolluted(isPolluted);
if(isPolluted){
tempVar.getValue().setRelatedType(retVar.getValue().getRelatedType()+"|"+index);
}
retVar.addElement(index, tempVar);
}
retVar = tempVar;
}else if(retVar != null){ // 类似 this|name
TabbyVariable tempVar = retVar.getField(pos);
if(created && tempVar == null){
SootField field = retVar.getSootField(pos);
if(field != null){
tempVar = retVar.getOrAddField(retVar, field);
}
}
retVar = tempVar;
}else{
retVar = null; // 所有情况都不符合时,置为null
}
}
return retVar;
}
2.2 TabbyVariable
作为代表方法中局部变量的类型,TabbyVariable
需要提供对于污点的描述。同时实现上将其考虑为对象和数组两种类型,分别添加成员来保存相关信息。
//记录自身的污点参数情况
private TabbyValue value = null;
private TabbyVariable owner = null;
//抽取第一个污点参数作为这个变量的污点参数代表,简化分析
private String firstPollutedVarRelatedType = null;
// 保存键对应的变量
private Map<String, TabbyVariable> fieldMap = new HashMap<>();
// 保存数组对应的变量
private Map<Integer, TabbyVariable> elements = new HashMap<>();
其中TabbyValue
类型的对象内部包裹着TabbyStatus
类型的对象,保存了污点参数的信息。内部使用时通过下面的方法来获取
在一般的污点分析中,不同的源参数到不同的目标参数,都可以连接污点边,所以一个方法调用可能会产生多条污点边。这里可能是为了使得代码属性图中只保留一个调用边/污点边,所以只获取了第一个污点类型,也间接导致污点边的精度丢失。
/**
* 只获取第一个polluted type
* 当存在多个polluted type时,获取第一个,做近似化处理
* @return
*/
public String getFirstPollutedType(){
if(!isPolluted) return null;
for(String type:types){
if(type != null && (type.startsWith("this") || type.startsWith("param-"))){
return type;
}
}
return null;
}
TabbyVariable
封装了一个isPolluted
方法,用来快速判断是否是污点变量
public boolean isPolluted(int index){
if(value.isPolluted() && index == -1){
return true;
}else if(index != -1){
if(elements.size() > index){
TabbyVariable var = elements.get(index);
return var.isPolluted(-1);
}
}
return false;
}
当index
为-1
时,表示变量本身是污点变量。当index
不为-1
时,表示当前变量是一个数组,其序号为index
的成员是污点变量。
3 总结和问题讨论
3.1 总结
这篇文章从工作流程设计和soot使用两个方面介绍了tabby,理解tabby对于安全开发和漏洞挖掘具有重要意义。tabby目前还在开发过程中,从作者自己写在源代码的注释中也可以看出tabby还有很多可以改进的方向。
3.2 问题讨论
3.2.1 tabby实际的工作内容?
tabby实际的工作内容是分析类和方法中的继承、实现、重写和调用关系,并且以代码属性图的形式呈现。
3.2.2 tabby和GadgetInspector的异同?
- 同:都专注于挖掘反序列化链,都实现了污点分析功能
- 异:就JAVA反序列化来说,有两点不同。一是tabby的污点分析是基于soot框架的,它工作的语言等级是中间代码级别,更加抽象,而GadgetInpector的污点分析是自己实现的栈帧模拟,它工作的语言等级是字节码级别;二是tabby的输出是代码属性图,使用者还需要结合自己的经验进一步处理才能挖掘出反序列化链,而GadgetInpector的输出就是可能的反序列化链。