从字节码开始到ASM的gadgetinspector源码解析(二)

83 阅读30分钟

gadgetInspector分析

0x01 Intro

工具基于ASM技术来对控制字节码,从而达到对传入jar及war包的classpath下的类进行读取,并依次记录类信息、类方法信息、调用关系信息。最后基于以上收集的信息来进行反序列化链的挖掘,分别对应如下几个类:

  1. GadgetInspector:main方法,程序的入口,做一些配置以及数据的准备工作

  2. MethodDiscovery:类、方法数据以及父子类、超类关系数据的搜索

  3. PassthroughDiscovery:分析参数能影响到返回值的方法,并收集存储

  4. CallGraphDiscovery:记录调用者caller方法和被调用者target方法的参数关联

  5. SourceDiscovery:入口方法的搜索,只有具备某种特征的入口才会被标记收集

  6. GadgetChainDiscovery:整合以上数据,并通过判断调用链的最末端slink特征,从而判断出可利用的gadget chain

0x02 主入口-GadgetInspetcor

该类为整个工具的入口类,基本上是对于相关配置做出初始化处理,静态代码块中创建准备写入相关结果的文件。main中首先验证是否存在参数,若为空退出。工具在挖掘时需要我们指定不同的gadget-chain,如jdk原生反序列化、jackson等,以及指定classpath的路径。

接下来会对日志进行配置,之后是对历史dat文件(上面提到的类、方法等相关数据的本地化存储)的管理,以及反序列化链类型的指定。我们主要看这一部分是如何指定反序列化链类型的:

else if (arg.equals("--config")) {                //--config参数指定fuzz类型                config = ConfigRepository.getConfig(args[++argIndex]);                if (config == null) {                    throw new IllegalArgumentException("Invalid config name: " + args[argIndex]);                }

跟进到getConfig方法中,并且也可以看到所有的gadget-chain是通过不同的Config来实现的,并且都实现了GIConfig接口:

public interface GIConfig {​    String getName();    SerializableDecider getSerializableDecider(Map<MethodReference.Handle, MethodReference> methodMap, InheritanceMap inheritanceMap);    ImplementationFinder getImplementationFinder(        Map<Handle, MethodReference> methodMap,        Map<Handle, Set<Handle>> methodImplMap,        InheritanceMap inheritanceMap,        Map<ClassReference.Handle, Set<Handle>> methodsByClass);    SourceDiscovery getSourceDiscovery();    SlinkDiscovery getSlinkDiscovery();}

我们以Jackson的实现来看,这些被实现的方法都会在后面用到,他们都是用来对指定gadget-chain进行区分的方法,不同的gadget-chain的特征不同,因此我们可以通过这些方法来确认对应的chain。

package gadgetinspector.config;​import gadgetinspector.ImplementationFinder;import gadgetinspector.SerializableDecider;import gadgetinspector.SlinkDiscovery;import gadgetinspector.SourceDiscovery;import gadgetinspector.data.ClassReference;import gadgetinspector.data.InheritanceMap;import gadgetinspector.data.MethodReference;import gadgetinspector.data.MethodReference.Handle;import gadgetinspector.jackson.JacksonImplementationFinder;import gadgetinspector.jackson.JacksonSerializableDecider;import gadgetinspector.jackson.JacksonSourceDiscovery;​import java.util.Map;import java.util.Set;​public class JacksonDeserializationConfig implements GIConfig {​    @Override    public String getName() {        return "jackson";    }​    @Override    public SerializableDecider getSerializableDecider(Map<MethodReference.Handle, MethodReference> methodMap, InheritanceMap inheritanceMap) {        return new JacksonSerializableDecider(methodMap);    }​    @Override    public ImplementationFinder getImplementationFinder(        Map<Handle, MethodReference> methodMap,        Map<Handle, Set<Handle>> methodImplMap,        InheritanceMap inheritanceMap,        Map<ClassReference.Handle, Set<Handle>> methodsByClass) {        return new JacksonImplementationFinder(getSerializableDecider(methodMap, inheritanceMap));    }​    @Override    public SourceDiscovery getSourceDiscovery() {        return new JacksonSourceDiscovery();    }​    @Override    public SlinkDiscovery getSlinkDiscovery() {        return null;    }}​

跟进JacksonSerializableDecider,两个map中记录的是可以通过Jackson决策的类和方法:

//类是否通过决策的缓存集合    private final Map<ClassReference.Handle, Boolean> cache = new HashMap<>();    //类名-方法集合 映射集合    private final Map<ClassReference.Handle, Set<MethodReference.Handle>> methodsByClassMap;

具体的决策判断逻辑在apply中,在后面的分析中我们也可以看到会调用apply方法来判断类和方法是否通过决策。以jackson的apply来举例,由于jackson的json反序列化是需要以类的无参构造为起始,在java中如果没有显式声明无参构造器,但是显式声明了一个有参构造,那么该类是没有无参构造的,因此代表着该类不可进行jackson反序列化。

@Override    public Boolean apply(ClassReference.Handle handle) {        if (isNoGadgetClass(handle)) {            return false;        }        Boolean cached = cache.get(handle);        if (cached != null) {            return cached;        }​        Set<MethodReference.Handle> classMethods = methodsByClassMap.get(handle);        if (classMethods != null) {            for (MethodReference.Handle method : classMethods) {                //该类,只要有无参构造方法,就通过决策                if (method.getName().equals("<init>") && method.getDesc().equals("()V")) {                    cache.put(handle, Boolean.TRUE);                    return Boolean.TRUE;                }            }        }​        cache.put(handle, Boolean.FALSE);        return Boolean.FALSE;    }

接下来回到Config中,继续看InplementationFinder,在决策时由于Java的多态性,并且gadgetinspector无法在要被检测的jar运行时进行判断,因此当调用到某一接口的方法时,需要查找接口所有的实现类中的该方法,并将这些方法组成实际的调用链去进行污点分析。这些方法是否可进行当前指定的gadget-chain反序列化,还是需要通过apply方法来进行判断:

public class JacksonImplementationFinder implements ImplementationFinder {​    private final SerializableDecider serializableDecider;​    public JacksonImplementationFinder(SerializableDecider serializableDecider) {        this.serializableDecider = serializableDecider;    }​    @Override    public Set<MethodReference.Handle> getImplementations(MethodReference.Handle target) {        Set<MethodReference.Handle> allImpls = new HashSet<>();​        // For jackson search, we don't get to specify the class; it uses reflection to instantiate the        // class itself. So just add the target method if the target class is serializable.        if (Boolean.TRUE.equals(serializableDecider.apply(target.getClassReference()))) {            allImpls.add(target);        }​        return allImpls;    }}

继续看JacksonSourceDiscovery,内部只有一个discover方法,这个方法的作用就是帮我们找到可进行Jackson反序列化的入口方法,对于jackson反序列化来说,会以无参构造为入口,并依次执行setter以及getter。因此discover会查找出通过了apply决策后的类的无参构造(()V代表无参,返回值为viod),以及getter和setter。

  @Override    public void discover(Map<ClassReference.Handle, ClassReference> classMap,                         Map<MethodReference.Handle, MethodReference> methodMap,                         InheritanceMap inheritanceMap, Map<MethodReference.Handle, Set<GraphCall>> graphCallMap) {​        final JacksonSerializableDecider serializableDecider = new JacksonSerializableDecider(methodMap);​        for (MethodReference.Handle method : methodMap.keySet()) {            if (skipList.contains(method.getClassReference().getName())) {                continue;            }            if (serializableDecider.apply(method.getClassReference())) {                if (method.getName().equals("<init>") && method.getDesc().equals("()V")) {                    addDiscoveredSource(new Source(method, 0));                }                if (method.getName().startsWith("get") && method.getDesc().startsWith("()")) {                    addDiscoveredSource(new Source(method, 0));                }                if (method.getName().startsWith("set") && method.getDesc().matches("\\(L[^;]*;\\)V")) {                    addDiscoveredSource(new Source(method, 0));                }            }

继续向下看GadgetInspector,进入到initJarData方法中,通过for循环读取最后面的参数,从而指定多个jar或war包,通过URLClassLoader,根据绝对路径将这些jar或war包进行加载,并通过ClassResourceEnumerator将jar或war包中的class进行加载:

ClassLoader classLoader = initJarData(args, boot, argIndex, haveNewJar, pathList);

                for (int i = 0; i < args.length - argIndex; i++) {                    String pathStr = args[argIndex + i];                    if (!pathStr.endsWith(".jar")) {                        //todo 主要用于大批量的挖掘链                        //非.jar结尾,即目录,需要遍历目录找出所有jar文件                        File file = Paths.get(pathStr).toFile();                        if (file == null || !file.exists())                            continue;                        Files.walkFileTree(file.toPath(), new SimpleFileVisitor<Path>() {                            @Override                            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {                                if (!file.getFileName().toString().endsWith(".jar"))                                    return FileVisitResult.CONTINUE;                                File readFile = file.toFile();                                Path path = Paths.get(readFile.getAbsolutePath());                                if (Files.exists(path)) {                                    if (ConfigHelper.history) {                                        if (!scanJarHistory.contains(path.getFileName().toString())) {                                            if (jarCount.incrementAndGet() <= ConfigHelper.maxJarCount) {                                                pathList.add(path);                                            }                                        }                                    } else {                                        if (jarCount.incrementAndGet() <= ConfigHelper.maxJarCount) {                                            pathList.add(path);                                        }                                    }                                }                                return FileVisitResult.CONTINUE;                            }                        });​                        continue;                    }                    Path path = Paths.get(pathStr).toAbsolutePath();                    if (!Files.exists(path)) {                        throw new IllegalArgumentException("Invalid jar path: " + path);                    }                    pathList.add(path);                

      //类枚举加载器,具有两个方法            //getRuntimeClasses获取rt.jar的所有class            //getAllClasses获取rt.jar以及classLoader加载的class           final ClassResourceEnumerator classResourceEnumerator = new ClassResourceEnumerator(                    classLoader);

接下来进入beginDiscovery方法中,接下来我们开始分析具体的挖掘逻辑。

0x03 类、方法、继承关系数据收集-MethodDiscovery

首先进入methodDiscovery当中,可以看到如果不存在,会生成classes.dat、methods .dat、inheritanceMap.dat,分别对类数据、方法数据以及继承关系数据进行收集:

  if (!Files.exists(Paths.get("classes.dat")) || !Files.exists(Paths.get("methods.dat"))                || !Files.exists(Paths.get("inheritanceMap.dat"))) {            LOGGER.info("Running method discovery...");            MethodDiscovery methodDiscovery = new MethodDiscovery();            methodDiscovery.discover(classResourceEnumerator);            //保存了类信息、方法信息、继承实现信息            methodDiscovery.save();        }

跟进MethodDiscovery.discover,传入了上面保存了类信息的classResourceRnumerator,并且调用了getAllClasses方法,获取到了包括rt.jar和指定jar、war包中的所有类,并调用ClassReader的accept方法进行下一步,这里所用到的就是ASM。

    public void discover(final ClassResourceEnumerator classResourceEnumerator) throws Exception {        for (ClassResourceEnumerator.ClassResource classResource : classResourceEnumerator.getAllClasses()) {            try (InputStream in = classResource.getInputStream()) {                ClassReader cr = new ClassReader(in);                try {                    //使用asm的ClassVisitor、MethodVisitor,利用观察模式去扫描所有的class和method并记录                    cr.accept(new MethodDiscoveryClassVisitor(), ClassReader.EXPAND_FRAMES);                } catch (Exception e) {                    LOGGER.error("Exception analyzing: " + classResource.getName(), e);                }            } catch (Exception e) {                e.printStackTrace();            }        }    }

ASM及访问者模式

ASM的设计原理基于访问者模式,常用于类的属性无改变,在不侵入类的情况下并对属性的操作做出扩充的场景(类似于AOP)。用生活中的例子我们可以这么理解,想象你是一个导游,要带游客参观一个由多个景点(类、方法、字段等)组成的旅游区(Java类)。访问者模式的工作方式是这样的:

  1. 景点清单:旅游区有一份固定的景点清单(类的结构,比如方法、字段,并且这些不会变动)。

  2. 游客自由行动:游客(XXXVisitor)可以自由选择在每个景点做什么(比如拍照、记录日志、修改行为)。

  3. 导游协调:导游(ASM-ClassReader)负责按顺序带游客访问每个景点,并让游客在每个景点执行自己的操作。

ASM的关键思想:字节码(景点)的结构是固定的,但你可以通过"游客"灵活地定义在每个"景点"做什么,使用时我们需要先通过字节流等方式读入要控制的类,之后传入给ClassReader的accept方法,accept方法会按照JVM规定好的类文件结构来依次调用对应的方法,我们可以通过重写ClassVisitor的各个visit方法,在调用accept时传入,从而实现自己的visitXXX的逻辑。因为ASM是基于责任链的调用,并且支持visiter的嵌套包装来进行遍历调用,调用顺序为从最外层的子visitor开始调用,直到最内层的ClassVisitor,因此需要在我们的visit逻辑中处理下一层的visitor逻辑,直到将所有嵌套的visitor逻辑处理完毕(最外层也就是ASM中的ClassVisitor)否则有可能会造成visiter的逻辑断链。

1. visit()                  → 访问类的基础信息(版本、类名等)2. visitSource()            → 源码信息(可选)3. visitModule()            → 模块信息(Java 9+,可选)4. visitNestHost()          → 嵌套类宿主(Java 11+,可选)5. visitPermittedSubtype()  → sealed类的许可子类(Java 17+,可选)6. visitOuterClass()        → 外部类信息(如果是内部类)7. visitAnnotation()        → 类上的注解(可能有多个)8. visitTypeAnnotation()    → 类上的类型注解(可能有多个)9. visitAttribute()         → 类的自定义属性(可能有多个)10. visitField()            → 类的字段(按字节码中的顺序访问)11. visitMethod()           → 类的方法(按字节码中的顺序访问)12. visitEnd()              → 类访问结束

我们回到MethodDiscovery.discover,在通过cr.accept后,cr先调用visit方法,因此我们跟进传入cr的MethodDiscoveryClassVisitor的visit方法,MDCV的visit方法保存了当前观察类的信息

  1. this.name:类名

  2. this.superName:继承的父类名

  3. this.interfaces:实现的接口名

  4. this.isInterface:当前类是否接口

  5. this.members:类的字段集合

  6. this.classHandle:gadgetinspector中对于类名的封装,可以通过类名来操作类中相关属性

    public void visit ( int version, int access, String name, String signature, String superName, String[]interfaces) { this.name = name; this.superName = superName; this.interfaces = interfaces; this.isInterface = (access & Opcodes.ACC_INTERFACE) != 0; this.members = new ArrayList<>(); this.classHandle = new ClassReference.Handle(name);//类名 annotations = new HashSet<>(); super.visit(version, access, name, signature, superName, interfaces); }​

接下来我们跳过几个不太重要的visit,来到visitField,在cr的控制下,被观察的类有多少个字段,visitField就会被调用多少次,来对字段进行处理。参数列表分别代表属性访问限定符,属性名,属性类型,泛型,属性的初始值(只有静态字段生效)该方法调用时,会先判断该字段是否是静态if ((access & Opcodes.ACC_STATIC) == 0),之后会通过判断字段的类型,如果是Object或者数组类型,就获取其具体内部类型,如果是基本类型,就获取类型的原始描述符。

比如String类型是Object,String[]是Array,那么最后保存的是java/lang/String,Int类型保留原始描述符后为I。获取到类型后将数据保存到visit中初始化好的列表member中。

        @Override        public FieldVisitor visitField(int access, String name, String desc,                                       String signature, Object value) {            if ((access & Opcodes.ACC_STATIC) == 0) {                Type type = Type.getType(desc);                String typeName;                if (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY) {                    typeName = type.getInternalName();                } else {                    typeName = type.getDescriptor();                }                members.add(new ClassReference.Member(name, access, new ClassReference.Handle(typeName)));            }            return super.visitField(access, name, desc, signature, value);        }

可以看到传入的是ClassReference的内部类Member的构造函数,我们跟进ClassReference及Member的结构,可以发现在ClassReference中通过member数组来存储字段信息,内部类Member存储了字段的名字,访问限定修饰符,以及一个Handle类型的type,用来存储属性类型。Handle也是ClassReference中的一个内部类,只有一个字段,用来存储类名。大概访问流程是每个被观测的类对应一个MethodDiscoveryClassVisitor及ClassReference,当ASM观测到一个字段时调用visitField,此时visitField会new一个ClassReference.Member来存储字段信息,并将其添加到MDCV的List<ClassReference.Member> members中。当类中所有字段都被add进去之后,会调用到后续的visit,在最后调用visitEnd时,可以发现members.toArray(new ClassReference.Member[members.size()]),将member中所有被创建的ClassReference.Member转成了数组,并且初始化了一个ClassReference,将所有的字段合并到了ClassReference的Member[]数组中。

private final Member[] members;

    public static class Member {        private final String name;        private final int modifiers;        private final ClassReference.Handle type;        public Member(String name, int modifiers, Handle type) {            this.name = name;            this.modifiers = modifiers;            this.type = type;        }

    public static class Handle {        private final String name;        public Handle(String name) {            this.name = name;        }        public String getName() {            return name;        }        @Override        public boolean equals(Object o) {            if (this == o) return true;            if (o == null || getClass() != o.getClass()) return false;            Handle handle = (Handle) o;            return name != null ? name.equals(handle.name) : handle.name == null;        }        @Override        public int hashCode() {            return name != null ? name.hashCode() : 0;        }    }

接下来进行 visitMethod,依旧是观察到多少个方法就会调用多少次,初始化一个MethodReference,传入类名,方法名,方法描述(方法的返回值类型以及参数类型,需要使用Type类来进行解析),并且将方法添加到列表discoveredMethods中。

        @Override        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {            boolean isStatic = (access & Opcodes.ACC_STATIC) != 0;            //找到一个方法,添加到缓存            discoveredMethods.add(new MethodReference(                    classHandle,//类名                    name,                    desc,                    isStatic));            return super.visitMethod(access, name, desc, signature, exceptions);        }

最后进入到visitEnd,刚才也说过了会将所有字段整合到一个ClassReference中,并且将整合好的ClassReference添加到discoveredClasses中

        @Override        public void visitEnd() {            ClassReference classReference = new ClassReference(                    name,                    superName,                    interfaces,                    isInterface,                    members.toArray(new ClassReference.Member[members.size()]),                    annotations);//把所有找到的字段封装            //找到一个方法遍历完成后,添加类到缓存            discoveredClasses.add(classReference);​            super.visitEnd();        }

整个methodDiscovery.discovr执行完成,继续到下一步methodDiscovery.save();中,通过DataLoader.saveData完成。其中对于classes.dat和methods.dat分别通过ClassReference.Factory()和MethodReference.Factory()创建的factory进行序列化存储

    public static <T> void saveData(Path filePath, DataFactory<T> factory, Collection<T> values) throws IOException {        try (BufferedWriter writer = Files.newWriter(filePath.toFile(), StandardCharsets.UTF_8)) {            for (T value : values) {                final String[] fields = factory.serialize(value);                if (fields == null) {                    continue;                }                StringBuilder sb = new StringBuilder();                for (String field : fields) {                    if (field == null) {                        sb.append("\t");                    } else {                        sb.append("\t").append(field);                    }                }                writer.write(sb.substring(1));                writer.write("\n");            }        }

最终形成的文件格式如下:

classes.dat:

类名(例:java/lang/String) 父类 接口A,接口B,接口C 是否接口 字段1!字段1access!字段1类型!字段2!字段2access!字段1类型

methods.dat:

类名 方法名 方法描述 是否静态方法

在持久化相关数据后,会通过Map来整合ClassReference.Handle和ClassReference之间的映射关系

        Map<ClassReference.Handle, ClassReference> classMap = new HashMap<>();        for (ClassReference clazz : discoveredClasses) {            classMap.put(clazz.getHandle(), clazz);        }

接下来进行类的继承以及实现关系的整合分析

InheritanceDeriver.derive(classMap).save();

跟进到InheritanceDeriver.derive中,可以看到做的事就是利用Map来保存继承关系,形成了类- >(父类,接口,超类)的映射关系。

    public static InheritanceMap derive(Map<ClassReference.Handle, ClassReference> classMap) {        LOGGER.debug("Calculating inheritance for " + (classMap.size()) + " classes...");        Map<ClassReference.Handle, Set<ClassReference.Handle>> implicitInheritance = new HashMap<>();        //遍历所有类        for (ClassReference classReference : classMap.values()) {            if (implicitInheritance.containsKey(classReference.getHandle())) {                throw new IllegalStateException("Already derived implicit classes for " + classReference.getName());            }            Set<ClassReference.Handle> allParents = new HashSet<>();            //获取classReference的所有父类、超类、接口类            getAllParents(classReference, classMap, allParents);            //添加缓存:类名 -> 所有的父类、超类、接口类            implicitInheritance.put(classReference.getHandle(), allParents);        }        //InheritanceMap翻转集合,转换为{class:[subclass]}        return new InheritanceMap(implicitInheritance);    }

getAllParents方法会递归的将当前观察类的所有父类、接口的父类查找出来,并且添加到allParents集合中

    private static void getAllParents(ClassReference classReference, Map<ClassReference.Handle, ClassReference> classMap, Set<ClassReference.Handle> allParents) {        Set<ClassReference.Handle> parents = new HashSet<>();        //把当前classReference类的父类添加到parents        if (classReference.getSuperClass() != null) {            parents.add(new ClassReference.Handle(classReference.getSuperClass()));        }        //把当前classReference类实现的所有接口添加到parents        for (String iface : classReference.getInterfaces()) {            parents.add(new ClassReference.Handle(iface));        }        for (ClassReference.Handle immediateParent : parents) {            //从所有类数据集合中,遍历找出classReference的父类、接口            ClassReference parentClassReference = classMap.get(immediateParent);            if (parentClassReference == null) {                LOGGER.debug("No class id for " + immediateParent.getName());                continue;            }            //继续添加到集合中            allParents.add(parentClassReference.getHandle());            //继续递归查找,直到把classReference类的所有父类、超类、接口类都添加到allParents            getAllParents(parentClassReference, classMap, allParents);        }    }

最后将类名与整合好的allParents形成映射关系,存储到implicitInheritance中:

implicitInheritance.put(classReference.getHandle(), allParents);

接下来会用InheritanceMap构造函数将implicitInheritance的子->父的映射关系进行逆转整合。

    private final Map<ClassReference.Handle, Set<ClassReference.Handle>> inheritanceMap;    //-子关系集合    private final Map<ClassReference.Handle, Set<ClassReference.Handle>> subClassMap;    public InheritanceMap(Map<ClassReference.Handle, Set<ClassReference.Handle>> inheritanceMap) {        this.inheritanceMap = inheritanceMap;        subClassMap = new HashMap<>();        for (Map.Entry<ClassReference.Handle, Set<ClassReference.Handle>> entry : inheritanceMap.entrySet()) {            ClassReference.Handle child = entry.getKey();            for (ClassReference.Handle parent : entry.getValue()) {                subClassMap.computeIfAbsent(parent, k -> new HashSet<>()).add(child);            }        }    }

其中这一行代码会判断inheritanceMap中每个子类对应的set中的value(parent),是否在subClassMap中,如果不存在执行Lambda表达式,创建一个新的空HashSet,将parent作为key,HashSet作为value存入subClassMap,并且将child添加到HashSet中。最终subClassMap就变成了父类->子类的映射关系。

subClassMap.computeIfAbsent(parent, k -> new HashSet<>()).add(child);

举个例子:

假设 inheritanceMap 包含:

"Dog" → {"Animal", "Object"}"Cat" → {"Animal", "Object"}

subClassMap 的构建过程如下:

  1. 处理 Dog 的父类 Animal

    • subClassMap 中没有 Animal,创建HashSet → Animal: {Dog}
  2. 处理 Dog 的父类 Object

    • 没有 Object,创建HashSet → Object: {Dog}
  3. 处理 Cat 的父类 Animal

    • Animal 已存在,直接添加 → Animal: {Dog, Cat}
  4. 处理 Cat 的父类 Object

    • Object 已存在,添加 → Object: {Dog, Cat}

最终 subClassMap 结果:

"Animal" → {"Dog", "Cat"}"Object" → {"Dog", "Cat"}

最后调用save方法对继承关系进行保存,方法依旧和上面一样,会进行序列化后持久化存储:

    public void save() throws IOException {        //inheritanceMap.dat数据格式:        //类名 父类或超类或接口类1 父类或超类或接口类2 父类或超类或接口类3 ...        DataLoader.saveData(Paths.get("inheritanceMap.dat"), new InheritanceMapFactory(), inheritanceMap.entrySet());    }

最终形成的inheritanceMap.dat结构如下:

类名 父类或超类或接口类1 父类或超类或接口类2 父类或超类或接口类3 ...

0x04 入参返回值污染关系收集-PassthroughDiscovery

这一步类似于污点分析,我们对各个方法的参数对返回值的污染关系做出总结:

        if (!Files.exists(Paths.get("passthrough.dat")) && ConfigHelper.taintTrack) {            LOGGER.info("Analyzing methods for passthrough dataflow...");            PassthroughDiscovery passthroughDiscovery = new PassthroughDiscovery();            //记录参数在方法调用链中的流动关联(如:AB、C、D四个方法,调用链为A->B B->C C->D,其中参数随着调用关系从A流向B,在B调用C过程中作为入参并随着方法结束返回,最后流向D)            //该方法主要是追踪上面所说的"B调用C过程中作为入参并随着方法结束返回",入参和返回值之间的关联            passthroughDiscovery.discover(classResourceEnumerator, config);            passthroughDiscovery.save();        }

跟进passthroughDiscovery.discover当中,首先会将我们上一步MethodDiscovery所生成的类、方法、继承信息读取进来

    public void discover(final ClassResourceEnumerator classResourceEnumerator, final GIConfig config) throws IOException {        //加载文件记录的所有方法信息        Map<MethodReference.Handle, MethodReference> methodMap = DataLoader.loadMethods();        //加载文件记录的所有类信息        Map<ClassReference.Handle, ClassReference> classMap = DataLoader.loadClasses();        //加载文件记录的所有类继承、实现关联信息        InheritanceMap inheritanceMap = InheritanceMap.load();

接下来通过discoverMethodCalls,来找出所有方法间的调用关系,我们继续跟进

  //搜索方法间的调用关系,缓存至methodCalls集合,返回 类名->类资源 映射集合        Map<String, ClassResourceEnumerator.ClassResource> classResourceByName = discoverMethodCalls(classResourceEnumerator);

在该方法中,依然是通过ASM来先对所有的类进行一次观察,用到的visitor是MethodCallDiscoveryClassVisitor,并且这里的MethodCallDiscoveryClassVisitor内部是做了一些包装的,这一部分的执行顺序可能会有点乱,我会在方法分析结束后总结一下:

    private Map<String, ClassResourceEnumerator.ClassResource> discoverMethodCalls(final ClassResourceEnumerator classResourceEnumerator) throws IOException {        Map<String, ClassResourceEnumerator.ClassResource> classResourcesByName = new HashMap<>();        for (ClassResourceEnumerator.ClassResource classResource : classResourceEnumerator.getAllClasses()) {            try (InputStream in = classResource.getInputStream()) {                ClassReader cr = new ClassReader(in);                  try {                    MethodCallDiscoveryClassVisitor visitor = new MethodCallDiscoveryClassVisitor(Opcodes.ASM6);                    cr.accept(visitor, ClassReader.EXPAND_FRAMES);                    classResourcesByName.put(visitor.getName(), classResource);                } catch (Exception e) {                    LOGGER.error("Error analyzing: " + classResource.getName(), e);                }            }        }        return classResourcesByName;    }

分别跟进MCDCV的visit以及visitMethod方法,visit方法中将传入进来的classname进行记录

        @Override        public void visit(int version, int access, String name, String signature,                          String superName, String[] interfaces) {            super.visit(version, access, name, signature, superName, interfaces);            if (this.name != null) {                throw new IllegalStateException("ClassVisitor already visited a class!");            }            this.name = name;        }

visitMethod方法又创建了一个MethodCallDiscoveryMethodVisitor,并且可以看到在实例化时将上面的mv也传了进去。但其实我们观察MethodCallDiscoveryClassVisitor的构造函数,在调用父类构造函数时并没有传入任何的classvisitor,因此父类ClassVisitor的cv属性为null,最终返回的也是个null,在这里传入给MCDMV的mv也是个null:

       @Override        public MethodVisitor visitMethod(int access, String name, String desc,                                         String signature, String[] exceptions) {            MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);            //在visit每个method的时候,创建MethodVisitor对method进行观察            MethodCallDiscoveryMethodVisitor modelGeneratorMethodVisitor = new MethodCallDiscoveryMethodVisitor(                    api, mv, this.name, name, desc);            return new JSRInlinerAdapter(modelGeneratorMethodVisitor, access, name, desc, signature, exceptions);        }

 // MethodCallDiscoveryClassVisitor的构造函数 MethodCallDiscoveryClassVisitor visitor = new MethodCallDiscoveryClassVisitor(Opcodes.ASM6);  //父类ClassVisitor的构造函数  public ClassVisitor(final int api) {    this(api, null);  }    public ClassVisitor(final int api, final ClassVisitor classVisitor) {    if (api != Opcodes.ASM6        && api != Opcodes.ASM5        && api != Opcodes.ASM4        && api != Opcodes.ASM7_EXPERIMENTAL) {      throw new IllegalArgumentException();    }    this.api = api;    this.cv = classVisitor;  }  //父类ClassVisitor的visitMethod方法 public MethodVisitor visitMethod(      final int access,      final String name,      final String descriptor,      final String signature,      final String[] exceptions) {    if (cv != null) {      return cv.visitMethod(access, name, descriptor, signature, exceptions);    }    return null;  }

跟进MethodCallDiscoveryMethodVisitor,可以发现父类为MethodVisitor,并且调用父类的构造函数时传入了mv,但其实我们这里静态分析可以分析出来mv是null的,即便传入了在调用MethodVisitor.visitXXX时,最终也不会走到cv.visitXXX上,我这里推测是作者为了工具的扩充性,如果我们需要添加其他的visitor来对方法进行其他处理,那么就可以形成我们之前提到的类似于责任链的方式,来遍历的调用visitXXX:

        public MethodCallDiscoveryMethodVisitor(final int api, final MethodVisitor mv,                                           final String owner, String name, String desc) {            super(api, mv);

我们继续看,可以看到接下来会将传入的owner(此时正在观察的类名)封装到ClassReference.Handle中,并再将这个CRF.Handle和方法名、方法的相关描述封装到一个MethodReference.Handle中,calledMethods是每次观察到一个方法,都会创建的空HashSet,最终形成了观察方法:{被观察方法调用方法}的映射关系存入到methodCalls中:

          // private final Map<MethodReference.Handle, Set<MethodReference.Handle>> methodCalls = new HashMap<>();            this.calledMethods = new HashSet<>();            methodCalls.put(new MethodReference.Handle(new ClassReference.Handle(owner), name, desc), calledMethods);        }

继续向下看,类中还有一个visitMethodInsn方法,当检测到方法内部的调用时就会执行(底层原理是检查到字节码指令INVOKEVIRTUAL、INVOKESPECIAL、INVOKESTATIC、INVOKEINTERFACE),从而将正在观察的方法中调用的方法加入到calledMethods中

@Overridepublic void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {    calledMethods.add(new MethodReference.Handle(new ClassReference.Handle(owner), name, desc));    super.visitMethodInsn(opcode, owner, name, desc, itf);}

指令

调用类型

适用方法

特点

INVOKEVIRTUAL

虚方法调用(动态绑定)

普通实例方法(非私有、非构造器、非静态)

运行时根据对象实际类型选择方法,支持多态

INVOKESPECIAL

特殊方法调用(静态绑定)

构造器、私有方法、super.xxx()

编译时就决定调用哪一个,不支持多态

INVOKESTATIC

静态方法调用

static 修饰的方法

无需对象即可调用,直接通过类名调用

INVOKEINTERFACE

接口方法调用

接口定义的方法

运行时通过接口表定位目标方法,支持多态

回到visitMethod中,最后会进行return操作,并且return的是JSRInlinerAdapter。为什么要return这个类呢,因为在早期的java版本中,使用JSR和RET跳转指令来进行程序流程控制,在后续版本已废弃并使用GOTO指令,因此需要进行兼容处理。JSRInlinerAdapter会将JSR和RET指令转为GOTO指令,从而兼容了早期项目。

 return new JSRInlinerAdapter(modelGeneratorMethodVisitor, access, name, desc, signature, exceptions);

经过这些封装,调用cr.accept(visitor, ClassReader.EXPAND_FRAMES);将封装好的MethodCallDiscoveryClassVisitor传入进行方法调用关系收集。accept执行顺序如下:

  1. MethodCallDiscoveryClassVisitor.visit对类进行观察

  2. 当观察到方法时调用MethodCallDiscoveryClassVisitor.visitMethod,其中会创建一个MethodCallDiscoveryMethodVisitor实例,并包装为JSRInlinerAdapter返回,创建实例时会自动为观察到的方法添加一个映射关系,即当前观察方法->calledMethods

  3. 当触发了visitxxx时,会先把这些visitxxx发给JSRInlinerAdapter,JSRInlinerAdapter通过各个visit方法对JSR和RET跳转指令进行转换。

    JSRInlinerAdapter 本身也是一个 MethodVisitor,它的回调时机完全跟 ASM 的方法遍历流程一致,只不过它在内部额外“钩”了两个地方来做子例程(JSR/RET)内联:

    1. **visitJumpInsn**每当 ASM 在浏览方法字节码时碰到一个跳转指令(visitJumpInsn(int opcode, Label lbl)),就会调用到它的这个方法。

      • 如果 opcode == JSR,它就把这个子例程入口标签记下来,标记说“后面要做内联”
    2. **visitEnd**当 ASM 遍历完一个方法的所有指令并调用到 visitEnd() 时,JSRInlinerAdapter 会先检查在 visitJumpInsn 里有没有记录过任何 JSR

      • 如果有,就走 markSubroutines()emitCode() 的流程,把所有老版本的 JSR/RET 全部展开成 GOTO(以及必要的空值占位等)

      • 然后再把重写后的指令列表一次性转发给它下游的 MethodVisitor(通常是一个 MethodWriter代码浏览器

    换句话说:

    • 只要你把 JSRInlinerAdapter 插到你的 MethodVisitor 链上(手动 new 一个 或者在使用 ClassWriter.COMPUTE_FRAMESClassReader.EXPAND_FRAMES 时 ASM 自动给你插入),

    • 在方法遍历时遇到跳转就会进 visitJumpInsn

    • 在方法结束时(visitEnd)就会真正触发“内联 JSR→GOTO” 的逻辑。

    这样保证了旧版子例程指令在生成新的字节码之前就被全部消除,适配现代 JVM 对 StackMapFrame 的要求。

    4.JSRInlinerAdapter将指令转换并内联后,会通过其visitend方法再次通过accept将visitXXX传递给下一个visitor,也就是传入的MethodCallDiscoveryMethodVisitor的visitMethodInsn方法,从而将被调用的方法添加到当前观察方法的calledMethods中。

    accept方法结束后还剩一行,还是将类名和classResource的映射关系存储起来并return:

    classResourcesByName.put(visitor.getName(), classResource);
    

    discoverMethodCalls逻辑结束后,接下来是对methodCalls进行一次逆拓扑排序,所谓逆拓扑排序就是把拓扑排序的序列倒过来,什么你还不知道什么是拓扑排序?或许你该学一下数据结构了,或者看一下这篇文章介绍的吧

    paper.seebug.org/1034/

    List<MethodReference.Handle> sortedMethods = topologicallySortMethodCalls();
    

    为什么我们要进行逆拓扑排序,因为在方法的调用链上,假设a方法传递参数给b方法,并且b方法的返回值影响到了a方法的返回值,那么我们在判断方法链的时候就不能从a方法来入手,需要从最深处被调用的b方法来入手,观察b方法的参数与返回值之间是否存在关系,如果存在关系则证明了a方法传入b方法的参数与b方法返回值有关,此时b方法返回值影响到了a方法返回值,那么我们也就可以断定ab方法之间存在污染关系。

    在方法调用的关系中,我们可以将这些调用抽象为有向图,假设a方法内部调用了b方法,那么我们就可以将a方法对应的图节点引出一条有向边,指向b方法。最终将所有的调用关系全部依次类推,就形成了一个有向图。我们将指向其他节点的边的数量叫做一个点的出度,指向自己的边的数量叫做一个节点的入度,如果找到有向图中一个入度为0的节点,将其节点以及所有的边全部消去,并输出该节点。不断重复这一操作,直到图中所有节点和边全部被消除掉,我们就得到了一组拓扑排序序列,而这一个序列就对应了我们的方法调用顺序。

    但事情并没有想象中这么顺利,在方法调用中会出现两种情况,一个是相同的方法可能会存在重复调用,并且方法调用中由于回调等方式的存在,造成图中可能会出现环路,而环路的出现会导致拓扑排序在某一时刻无法找到一个入度为0的点,也就没有拓扑序列的产生了,解决办法上面的文章也提到了。我们用一个例子来看一下具体的执行过程:

    假设有以下方法调用关系:

    AB → CA → D
    

    对应的调用图为:

    outgoingReferences = {    A: {B, D},    B: {C},    C: {},    D: {}}
    
    1. 初始调用:从根节点 A 开始。

      dfsTsort(outgoingReferences, sortedMethods, visitedNodes, stack, A);
      
    2. 处理节点 A

      • stack 为空,visitedNodes 为空 → 继续。

      • 获取 A 的被调用方法集合 {B, D}

      • A 加入 stack(当前路径:[A])。

      • 递归处理子节点 B

        dfsTsort(outgoingReferences, sortedMethods, visitedNodes, stack, B);
        
    3. 处理节点 B

      • stack 包含 A,不包含 B → 继续。

      • 获取 B 的被调用方法集合 {C}

      • B 加入 stack(当前路径:[A, B])。

      • 递归处理子节点 C

        dfsTsort(outgoingReferences, sortedMethods, visitedNodes, stack, C);
        
    4. 处理节点 C

      • stack 包含 A, B,不包含 C → 继续。

      • 获取 C 的被调用方法集合 {}(无子节点)。

      • C 加入 visitedNodessortedMethods

        visitedNodes = {C}, sortedMethods = [C]
        
      • 返回处理 B

    5. 回溯节点 B

      • stack 中移除 B(当前路径:[A])。

      • B 加入 visitedNodessortedMethods

        visitedNodes = {C, B}, sortedMethods = [C, B]
        
      • 处理 B 的下一个子节点(无剩余节点),返回处理 A

    6. 处理节点 A 的第二个子节点 D

      • D 加入 stack(当前路径:[A, D])。

      • 递归处理 D

        dfsTsort(outgoingReferences, sortedMethods, visitedNodes, stack, D);
        
    7. 处理节点 D

      • 获取 D 的被调用方法集合 {}(无子节点)。

      • D 加入 visitedNodessortedMethods

        visitedNodes = {C, B, D}, sortedMethods = [C, B, D]
        
      • 返回处理 A

    8. 回溯节点 A

      • stack 中移除 A(当前路径:[])。

      • A 加入 visitedNodessortedMethods

        visitedNodes = {C, B, D, A}, sortedMethods = [C, B, D, A]
        
    • 污点分析顺序

      1. 先分析 C(无依赖),确定其污点传播规则。

      2. 分析 B(依赖 C),利用 C 的结果。

      3. 分析 D(无依赖)。

      4. 最后分析 A(依赖 BD),确保所有被调用方法已处理。

    若存在循环调用(如 A → B → A):

    1. 处理 ABA 时,第二次进入 A 的递归:

      • stack 包含 A → 触发 if (stack.contains(node)) return;

      • 终止递归,避免死循环。

    逆拓扑排序后,接下来就是对方法参数和返回值之间污染关系的分析:

    passthroughDataflow = calculatePassthroughDataflow(classResourceByName, classMap, inheritanceMap, sortedMethods, config.getSerializableDecider(methodMap, inheritanceMap));

跟进calculatePassthroughDataflow,首先会遍历sortedMethods,如果是静态初始化代码,即静态代码块,就直接跳过,因为静态代码块是在类加载的时候就加载到JVM当中,我们一般没有办法在程序运行中进行控制

  final Map<MethodReference.Handle, Set<Integer>> passthroughDataflow = new HashMap<>();        //遍历所有方法,然后asm观察所属类,经过前面DFS的排序,调用链最末端的方法在最前面        for (MethodReference.Handle method : sortedMethods) {            //跳过static静态初始化代码            if (method.getName().equals("<clinit>")) {                continue;            }

接下来就是对当前所遍历的方法的所属类进行ASM观察:

  ClassResourceEnumerator.ClassResource classResource = classResourceByName.get(method.getClassReference().getName());            try (InputStream inputStream = classResource.getInputStream()) {                ClassReader cr = new ClassReader(inputStream);                try {                    PassthroughDataflowClassVisitor cv = new PassthroughDataflowClassVisitor(classMap, inheritanceMap,                            passthroughDataflow, serializableDecider, Opcodes.ASM6, method);                    cr.accept(cv, ClassReader.EXPAND_FRAMES);

跟进visitor逻辑,查看visit方法,visit方法会判断当前观察的类是否是要准备观察方法的所属类

   @Override        public void visit(int version, int access, String name, String signature,                          String superName, String[] interfaces) {            super.visit(version, access, name, signature, superName, interfaces);            this.name = name;            //不是目标观察的class跳过            if (!this.name.equals(methodToVisit.getClassReference().getName())) {                throw new IllegalStateException("Expecting to visit " + methodToVisit.getClassReference().getName() + " but instead got " + this.name);            }        }

接着看visitMethod,我们需要观察的类中的方法只需要是sortedMethod中的方法即可,也就是传入进来的methodToVisit,其他方法是不存在调用关系的:

    //不是目标观察的method需要跳过,上一步得到的method都是有调用关系的method才需要数据流分析            if (!name.equals(methodToVisit.getName()) || !desc.equals(methodToVisit.getDesc())) {                return null;            }

接下来是对方法进行更细致的观察,依旧看封装后的PassthroughDataflowMethodVisitor

 MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);            passthroughDataflowMethodVisitor = new PassthroughDataflowMethodVisitor(                    classMap, inheritanceMap, this.passthroughDataflow, serializableDecider,                    api, mv, this.name, access, name, desc, signature, exceptions);

下面作者用代码模拟了方法调用的过程,从而在模拟的局部变量表(污点变量表)中对参数进行污点标记。我们先来回顾JVM在进行方法调用时都做了哪些事情。假设现在A方法中要调用B方法,那么此时我们是在A方法内部的,那么JVM中会有A方法的栈帧,栈帧中主要两部分,一个是局部变量表,一个是操作数栈,当A方法内部准备调用B方法时,会先将要传给B方法的参数保存到A方法栈帧的操作数栈上,此时JVM会为B方法创建其对应的栈帧,然后在A方法操作数栈上的参数会被弹到B方法栈帧的局部变量表中。B方法内部使用这些参数时,会通过LOAD指令将其从局部变量表加载到操作数栈上,再进行使用。这里的思想就是用代码去模仿JVM的行为,从而将JVM的方法调用流程可视化。

下面的分析过程基于如下例子,这一段代码调用包含了入参与返回结果相同,返回结果与入参有关的情况,我们分别来看:

public class Main {  public String main(String args) throws IOException {    String cmd = new A().method1(args);    return new B().method2(cmd);  }}class A {  public String method1(String param) {    return param;  }}class B {  public String method2(String param) {    return new C().method3(param);  }}class C {  public String method3(String param) {    return param;  }}

逆拓扑排序后的结果为:

A.method1C.method3B.method2main

A.method1

因此我们先从A.method1来进行分析:

这里我们看到visitCode方法,在进入方法的第一时间,ASM会先调用这个方法。对于非静态方法来说,方法参数插槽的第一个0号位位this,对于静态方法,0号位为参数,所以这里将方法内的所有参数保存在一个使用Java代码模拟的局部变量表中,localIndex为参数在局部变量表中的位置,由于参数的类型不同,所以其在局部变量表中占用的大小也不同。而argIndex对应了参数在方法中的索引,通过setLocalTaint方法,形成了局部变量表与方法参数索引之间的映射关系

        @Override        public void visitCode() {            super.visitCode();​            int localIndex = 0;            int argIndex = 0;            if ((this.access & Opcodes.ACC_STATIC) == 0) {                //非静态方法,第一个局部变量应该为对象实例this                //添加到局部变量表集合                setLocalTaint(localIndex, argIndex);                localIndex += 1;                argIndex += 1;            }            for (Type argType : Type.getArgumentTypes(desc)) {                //判断参数类型,得出变量占用空间大小,然后存储                setLocalTaint(localIndex, argIndex);                localIndex += argType.getSize();                argIndex += 1;            }        }                   protected void setLocalTaint(int index, T ... possibleValues) {        Set<T> values = new HashSet<T>();        for (T value : possibleValues) {            values.add(value);        }        savedVariableState.localVars.set(index, values);    }         

接下来执行A.method1方法内部逻辑时(即return param),要将局部变量表中的参数通过ALOAD指令读取到操作数栈上,继续模拟,在检测到ALOAD指令时(包括其他访问局部变量表的指令),会回调visitVarInsn,将参数push到模拟的污点栈上,这里的参数可以看到是列表localVars的值,也就是局部变量表中对应的参数索引

 @Override    public void visitVarInsn(int opcode, int var) {        // Extend local variable state to make sure we include the variable index        for (int i = savedVariableState.localVars.size(); i <= var; i++) {            savedVariableState.localVars.add(new HashSet<T>());        }​        //变量操作,var为操作的本地变量索引        Set<T> saved0;        switch(opcode) {            case Opcodes.ILOAD:            case Opcodes.FLOAD:                push();                break;            case Opcodes.LLOAD:            case Opcodes.DLOAD:                push();                push();                break;            case Opcodes.ALOAD:                //从局部变量表取出变量数据入操作数栈,这个变量数据可能是被污染的                push(savedVariableState.localVars.get(var));                break;            case Opcodes.ISTORE:            case Opcodes.FSTORE:                pop();                savedVariableState.localVars.set(var, new HashSet<T>());                break;            case Opcodes.DSTORE:            case Opcodes.LSTORE:                pop();                pop();                savedVariableState.localVars.set(var, new HashSet<T>());                break;            case Opcodes.ASTORE:                //从栈中取出数据存到局部变量表,这个数据可能是被污染的(主要还是得看调用的方法,返回值是否可被污染)                saved0 = pop();                savedVariableState.localVars.set(var, saved0);                break;            case Opcodes.RET:                // No effect on stack                break;            default:                throw new IllegalStateException("Unsupported opcode: " + opcode);        }​        super.visitVarInsn(opcode, var);​        sanityCheck();    }​    private void push(Set<T> possibleValues) {        // Intentionally make this a reference to the same set        savedVariableState.stackVars.add(possibleValues);    }

接下来当方法调用结束return时,由于使用了ARETURN指令,在解析到无操作数的简单指令时触发visitInsn,我们查看其具体逻辑,可以发现在方法return时,将当前栈上的值返回,即返回的是参数索引set,并将存储到了returnTaint中,代表了A.method1这个方法的调用,参数索引为1的参数param会污染返回值:

        @Override        public void visitInsn(int opcode) {            switch(opcode) {                case Opcodes.IRETURN://从当前方法返回int                case Opcodes.FRETURN://从当前方法返回float                case Opcodes.ARETURN://从当前方法返回对象引用                    returnTaint.addAll(getStackTaint(0));//栈空间从内存高位到低位分配空间                    break;                case Opcodes.LRETURN://从当前方法返回long                case Opcodes.DRETURN://从当前方法返回double                    returnTaint.addAll(getStackTaint(1));                    break;                case Opcodes.RETURN://从当前方法返回void                    break;                default:                    break;            }            super.visitInsn(opcode);        }

最后对于该方法的观察结束,将污点分析结果存到了passthroughDataflow中,可以看到形成了方法与污染参数目录集合之间的映射关系:

        final Map<MethodReference.Handle, Set<Integer>> passthroughDataflow = new HashMap<>(); passthroughDataflow.put(method, cv.getReturnTaint());

C.method3

与A.method1流程一样

B.method2

class B {  public String method2(String param) {    return new C().method3(param);  }}
  1. 进入到方法内部,触发visitCode,将参数this、param放入虚拟局部变量表,并形成与参数列表索引的映射关系。

  2. 内部方法执行,ALOAD指令触发visitVarInsn,参数this、param push到污点栈。

  3. 方法内部调用C.method3,INVOKEVIRTUAL指令触发visitMethodInsn,该方法首先将C.method3参数类型提取,并判断该方法是否是静态方法,如果不是静态方法,将this(被调用方法所在类的实例对象)存入argTypes第一个,并依次存入其他参数。之后获取了方法的返回值类型的所占大小,后面进行使用:

               Type[] argTypes = Type.getArgumentTypes(desc);            if (opcode != Opcodes.INVOKESTATIC) {                //如果执行的非静态方法,则把数组第一个元素类型设置为该实例对象的类型,类比局部变量表                Type[] extendedArgTypes = new Type[argTypes.length+1];                System.arraycopy(argTypes, 0, extendedArgTypes, 1, argTypes.length);                extendedArgTypes[0] = Type.getObjectType(owner);                argTypes = extendedArgTypes;            }
    
  4. 接下来初始化argTaint,将其内部元素设置为空,argTaint大小为参数的数量。然后将污点栈中的参数依次存放进argTaint中,对于污点栈savedVariableState.stackVars来说,list从右往左为栈底到栈顶,假设方法参数列表为abc,那么从栈底到栈顶分别为a、b、c。继续将污点栈栈顶元素取出后放在argTaint的最后一个位置,以此类推,从而保证了argTaint中存放的参数索引与C.method3的参数列表的顺序相同。

                        final List<Set<Integer>> argTaint = new ArrayList<Set<Integer>>(argTypes.length);                    for (int i = 0; i < argTypes.length; i++) {                        argTaint.add(null);                    }​                    int stackIndex = 0;                    for (int i = 0; i < argTypes.length; i++) {                        Type argType = argTypes[i];                        if (argType.getSize() > 0) {                            //根据参数类型大小,从栈顶获取入参,参数入栈是从左到右的                            argTaint.set(argTypes.length - 1 - i, getStackTaint(stackIndex + argType.getSize() - 1));                        }                        stackIndex += argType.getSize();                    }
    
  5. 接下来判断方法是否是构造器,如果是构造器的话意味着在当前调用方法(B.method2)当中会有这么一段代码:

    C c = new C();
    

    因此可以确定被调用方法(C.method3)的返回值结果受到了this(C类实例对象)的污染,那么将argTaint中的0号索引取出,即为this,并将其加入resultTaint。如果不是构造器,那么就创造一个空的HashSet来存储后面的resultTaint。

  6. 从passthroughDataflow中拿到被调用方法C.method3的参数与返回值污点分析关系,并判断污点分析关系中的参数是否在当前的argTaint中,如果在则说明被调用方法的返回值被调用者传入的参数污染,这也就是为什么要进行逆拓扑排序。

                        Set<Integer> passthrough = passthroughDataflow.get(new MethodReference.Handle(new ClassReference.Handle(owner), name, desc));                    if (passthrough != null) {                        for (Integer passthroughDataflowArg : passthrough) {                            //判断是否和同一方法体内的其它方法返回值关联,有关联则添加到栈底,等待执行return时保存                            resultTaint.addAll(argTaint.get(passthroughDataflowArg));                        }
    
  7. 最后还是return,将B.method2的结果存到passthroughDataflow中

main方法

public class Main {  public String main(String args) throws IOException {    String cmd = new A().method1(args);    return new B().method2(cmd);  }}

第一步,执行visitCode存储入参到局部变量表

第二步,执行visitVarInsn参数入栈

第三步,执行visitMethodInsn调用A.method1,A.method1被污染的返回结果,也就是参数索引会被放在栈顶

第四步,执行visitVarInsn把放在栈顶的污染参数索引,放入到本地变量表

第五步,执行visitVarInsn参数入栈

第六步,执行visitMethodInsn调用B.method2,被污染的返回结果会被放在栈顶

第七步,执行visitInsn,返回栈顶数据,缓存到passthroughDataflow,也就是main方法的污点分析结果

最后通过passthroughDiscovery.save方法保存分析数据

public static class PassThroughFactory implements DataFactory<Map.Entry<MethodReference.Handle, Set<Integer>>> {    ...    @Override    public String[] serialize(Map.Entry<MethodReference.Handle, Set<Integer>> entry) {        if (entry.getValue().size() == 0) {            return null;        }        final String[] fields = new String[4];        fields[0] = entry.getKey().getClassReference().getName();        fields[1] = entry.getKey().getName();        fields[2] = entry.getKey().getDesc();        StringBuilder sb = new StringBuilder();        for (Integer arg : entry.getValue()) {            sb.append(Integer.toString(arg));            sb.append(",");        }        fields[3] = sb.toString();        return fields;    }}

最后持久化的passthrough.dat文件的数据格式如下:

类名 方法名 方法描述 能污染返回值的参数索引1,能污染返回值的参数索引2,能污染返回值的参数索引3...

0x05 方法调用污染关联-CallGraphDiscovery

我们用这个例子进行分析:

public class Main {  private String name;  public void main(String args) throws IOException {    new A().method1(args, name);  }}class A {  public String method1(String param, String param2) {    return param + param2;  }}

跟进callGraphDiscovery.discover,读取前面收集的数据,然后使用ModelGeneratorClassVisitor进行观察,visitCode观察每一个类,visitMethod观察类中的每一个方法,继续跟进ModelGeneratorMethodVisitor

        @Override        public MethodVisitor visitMethod(int access, String name, String desc,                                         String signature, String[] exceptions) {            MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);            ModelGeneratorMethodVisitor modelGeneratorMethodVisitor = new ModelGeneratorMethodVisitor(classMap,                    inheritanceMap, passthroughDataflow, serializableDecider, api, mv, this.name, access, name, desc, signature, exceptions);            return new JSRInlinerAdapter(modelGeneratorMethodVisitor, access, name, desc, signature, exceptions);        }

进入main方法内部,触发visitCode,main方法不是静态,将this以及参数args存入局部变量表,此处与前面不同的是会在参数索引前加一个arg前缀来进行标识:

        public void visitCode() {            super.visitCode();            int localIndex = 0;            int argIndex = 0;            //使用arg前缀来表示方法入参,后续用于判断是否为目标调用方法的入参            if ((this.access & Opcodes.ACC_STATIC) == 0) {                setLocalTaint(localIndex, "arg" + argIndex);                localIndex += 1;                argIndex += 1;            }            for (Type argType : Type.getArgumentTypes(desc)) {                setLocalTaint(localIndex, "arg" + argIndex);                localIndex += argType.getSize();                argIndex += 1;            }        }

我在写到这里的时候有一点疑问,对于visitVarInsn的调用时机。我们来看如下两个例子:

// example 1A a = new A();a.method1(args);//example 2new A().method1(args);

我们先来看第一个例子,new A()的字节码指令大概如下,可以看到是没有LOAD指令的,在调用构造方法时直接消费的是操作数栈上的A对象引用:

NEW A //创建A类实例DUP   //创建对象引用INVOKESPECIAL A.<init>()V //调用构造方法

接下来由于要把对象引用存到a中,因此会把对象引用存储到局部变量表中(假设在局部变量表2号位,局部变量表1号位存储args),即ASTORE指令,此时会触发一次visitVarInsn。那么接下来在调用a.method1(args)时需要进行两次ALOAD,首先把a的对象引用加载到操作数栈上,再把args加载到操作数栈上,从而接着触发了两次visitVarInsn

NEW ADUPINVOKESPECIAL A.<init>()VASTORE 2        // 存到局部槽 2 —> visitVarInsn(ASTORE,2)ALOAD 2         // 再加载回来 —> visitVarInsn(ALOAD,2)ALOAD 1         // 加载 args  —> visitVarInsn(ALOAD,1)INVOKEVIRTUAL A.method1…

继续我们看第二个例子,当构造函数执行完毕后,不需要进行ASTORE,并且再调用method1时也不需要从局部变量表中加载a的对象引用,因此最终只有加载args时才会调用一次visitVarInsn

NEW ADUPINVOKESPECIAL A.<init>()V// 上一步执行完 new A(),操作数栈上已经有了 A 的实例ALOAD 1          // 将 args(槽 1)加载到栈顶 — 触发一次 visitVarInsn(AL OAD,1)INVOKEVIRTUAL A.method1:(Ljava/lang/String;)Ljava/lang/Strin

检测到字节码指令new,触发visitTypeInsn,会push一个空的HashSet到污点栈中:

   @Override    public void visitTypeInsn(int opcode, String type) {        switch(opcode) {            case Opcodes.NEW:                push();                break;            case Opcodes.ANEWARRAY:                pop();                push();                break;            case Opcodes.CHECKCAST:                // No-op                break;            case Opcodes.INSTANCEOF:                pop();                push();                break;            default:                throw new IllegalStateException("Unsupported opcode: " + opcode);        }

字节码指令INVOKESPECIALA.<init>()V,调用A的构造器,触发visitMethodInsn,判断是否是构造器,被调用方法为构造器,将this设置为argTypes第一个参数:

            Type[] argTypes = Type.getArgumentTypes(desc);            if (opcode != Opcodes.INVOKESTATIC) {                Type[] extendedArgTypes = new Type[argTypes.length+1];                System.arraycopy(argTypes, 0, extendedArgTypes, 1, argTypes.length);                extendedArgTypes[0] = Type.getObjectType(owner);                argTypes = extendedArgTypes;            }

jiee下来检测启动工具时参数是否要进行污点分析,如果不进行污点分析,则直接把调用方法以及被调用方法封装为GraphCall,加入discoveredCalls中:

                    if (!ConfigHelper.taintTrack) {                        //不进行污点分析,全部调用关系都记录                        discoveredCalls.add(new GraphCall(                            new MethodReference.Handle(new ClassReference.Handle(this.owner), this.name, this.desc),                            new MethodReference.Handle(new ClassReference.Handle(owner), name, desc),                            0,                            "",                            0));                        break;                    }

启动污点分析后的逻辑接着往下看,会从污点栈中取出对应的参数,但我们这里由于没有进入到visitVarInsn,因此污点栈目前只有一个在visitInsn中push进去的一个空的set,这一步不会对discoverdCalls做任何事情

接着我们分析method1(args,name)的调用情况,首先需要加载args,触发visitVarInsn,ALOAD指令,将args(arg1)推入污点栈,然后调用visitMethodInsn。由于要传递的参数name是a的属性,因此需要加载this,从this中拿到name属性。触发ALOAD指令,将this(arg0)推入污点栈。此时污点栈中为如下内容:

stackVars[{}, {"arg1"}, {"arg0"} ]

接下来需要读入实例a的name字段,检测到字节码指令GETFIELD,触发visitFieldInsn,首先在ClassReference中不断遍历,直到找到该字段,判断该字段是否是transient,如果是transient就没必要加入污点栈。如果是非transient属性,就把栈顶当前的arg0修改为arg0.name加入污点栈中

                        Set<String> newTaint = new HashSet<>();                        if (!Boolean.TRUE.equals(isTransient)) {                            for (String s : getStackTaint(0)) {                                newTaint.add(s + "." + name);                            }                        }                        super.visitFieldInsn(opcode, owner, name, desc);                        //在调用方法前,都会先入栈,作为参数                        setStackTaint(0, newTaint);

非静态方法,argTypes第一个为A(this),第二个为String(args),第三个为String(name),对应了污点栈上的[{},{"arg1"}, {"arg0"} ](从左到右为栈底到栈顶),for循环i从0到2,分别从污点栈中拿到了arg0.name,arg1和空set。首先对arg0.name进行拆解,最终拆解出来dotIndex为4,srcArgIndex为0,srcArgPath为name,并记录到了discoverdCalls当中。继续拆解arg1,dotindex为-1,srcArgIndexn为1,srcArgPath为null,记录到discoverdCalls中。

  int stackIndex = 0;                    for (int i = 0; i < argTypes.length; i++) {                        //最右边的参数,就是最后入栈,即在栈顶                        int argIndex = argTypes.length-1-i;                        Type type = argTypes[argIndex];                        //操作数栈出栈,调用方法前,参数都已入栈                        Set<String> taint = getStackTaint(stackIndex);                        if (taint.size() > 0) {                            for (String argSrc : taint) {                                //取出出栈的参数,判断是否为当前方法的入参,arg前缀                                if (!argSrc.substring(0, 3).equals("arg")) {                                    throw new IllegalStateException("Invalid taint arg: " + argSrc);                                }                                int dotIndex = argSrc.indexOf('.');                                int srcArgIndex;                                String srcArgPath;                                if (dotIndex == -1) {                                    srcArgIndex = Integer.parseInt(argSrc.substring(3));                                    srcArgPath = null;                                } else {                                    srcArgIndex = Integer.parseInt(argSrc.substring(3, dotIndex));                                    srcArgPath = argSrc.substring(dotIndex+1);                                }                                //记录参数流动关系                                //argIndex:当前方法参数索引,srcArgIndex:对应上一级方法的参数索引                                discoveredCalls.add(new GraphCall(                                        new MethodReference.Handle(new ClassReference.Handle(this.owner), this.name, this.desc),                                        new MethodReference.Handle(new ClassReference.Handle(owner), name, desc),                                        srcArgIndex,                                        srcArgPath,                                        argIndex));                            }                        }                        stackIndex += type.getSize();                    }

最后save保存数据,持久化后的callgraph.dat格式如下:

调用者类名 调用者方法caller 调用者方法描述 被调用者类名 被调用者方法target 被调用者方法描述 调用者方法参数索引 调用者字段名 被调用者方法参数索引

0x06 利用链入口搜索-SourceDiscovery

在一开始我们也说到了,在挖掘反序列化链的时候需要指定类型,所以此处先获得对应的sourceDiscovery,我们这里以Jackson反序列化分析

        if (!Files.exists(Paths.get("sources.dat"))) {            LOGGER.info("Discovering gadget chain source methods...");            SourceDiscovery sourceDiscovery = config.getSourceDiscovery();            //查找利用链的入口(例:java原生反序列化的readObject)            sourceDiscovery.discover();            sourceDiscovery.save();        }

跟进SourceDiscovery.discover在jackson中的实现,可以发现对于Jackson反序列化来说,source需要判断方法是否是无参构造、setter和getter,只有这些方法才能作为jackson反序列化的入口:

    @Override    public void discover(Map<ClassReference.Handle, ClassReference> classMap,                         Map<MethodReference.Handle, MethodReference> methodMap,                         InheritanceMap inheritanceMap, Map<MethodReference.Handle, Set<GraphCall>> graphCallMap) {        final JacksonSerializableDecider serializableDecider = new JacksonSerializableDecider(methodMap);        for (MethodReference.Handle method : methodMap.keySet()) {            if (skipList.contains(method.getClassReference().getName())) {                continue;            }            if (serializableDecider.apply(method.getClassReference())) {                if (method.getName().equals("<init>") && method.getDesc().equals("()V")) {                    addDiscoveredSource(new Source(method, 0));                }                if (method.getName().startsWith("get") && method.getDesc().startsWith("()")) {                    addDiscoveredSource(new Source(method, 0));                }                if (method.getName().startsWith("set") && method.getDesc().matches("\\(L[^;]*;\\)V")) {                    addDiscoveredSource(new Source(method, 0));                }            }        }    }

最后还是将方法保存持久化为sources.dat,格式如下:

类名 方法名 方法描述 污染参数索引

0x07 gadgetChain挖掘-GadgetChainDiscovery

跟进GadgetChainDiscovery.discover,首先进行所有重写方法的扫描,在一开始我们也说了工具没有办法在运行时进行扫描,所以对于各种方法的重写我们没有办法确定到底调用的是哪个方法

Map<MethodReference.Handle, MethodReference> methodMap = DataLoader.loadMethods();    InheritanceMap inheritanceMap = InheritanceMap.load();    Map<MethodReference.Handle, Set<MethodReference.Handle>> methodImplMap = InheritanceDeriver        .getAllMethodImplementations(            inheritanceMap, methodMap);    Map<ClassReference.Handle, Set<MethodReference.Handle>> methodsByClass = InheritanceDeriver.getMethodsByClass(methodMap);

跟进InheritanceDeriver.getAllMethodImplementations,获取之前收集到的method的类,并通过之前收集到的继承关系来获取类的所有子孙类,最终形成类->子孙类的映射关系:

Map<Handle, Set<MethodReference.Handle>> methodsByClass = getMethodsByClass(methodMap);  Map<ClassReference.Handle, Set<ClassReference.Handle>> subClassMap = new HashMap<>();        for (Map.Entry<ClassReference.Handle, Set<ClassReference.Handle>> entry : inheritanceMap.entrySet()) {            for (ClassReference.Handle parent : entry.getValue()) {                if (!subClassMap.containsKey(parent)) {                    Set<ClassReference.Handle> subClasses = new HashSet<>();                    subClasses.add(entry.getKey());                    subClassMap.put(parent, subClasses);                } else {                    subClassMap.get(parent).add(entry.getKey());                }            }        }

接下来遍历所有的方法,并遍历subclasses,如果某一个subclass中存在与当前遍历的方法名和返回值一致的方法,就将其加入overridingMethods,最后整合所有重写的方法,形成方法名到重写方法之间的映射关系,由于静态方法不可重写,因此遇到静态方法直接跳过:

        //遍历所有方法,根据父类->子孙类集合,找到所有的override的方法,记录下来(某个类的方法->所有的override方法)        Map<MethodReference.Handle, Set<MethodReference.Handle>> methodImplMap = new HashMap<>();        for (MethodReference method : methodMap.values()) {            // Static methods cannot be overriden            if (method.isStatic()) {                continue;            }            Set<MethodReference.Handle> overridingMethods = new HashSet<>();            Set<ClassReference.Handle> subClasses = subClassMap.get(method.getClassReference());            if (subClasses != null) {                for (ClassReference.Handle subClass : subClasses) {                    // This class extends ours; see if it has a matching method                    Set<MethodReference.Handle> subClassMethods = methodsByClass.get(subClass);                    if (subClassMethods != null) {                        for (MethodReference.Handle subClassMethod : subClassMethods) {                            if (subClassMethod.getName().equals(method.getName()) && subClassMethod.getDesc().equals(method.getDesc())) {                                overridingMethods.add(subClassMethod);                            }                        }                    }                }            }            if (overridingMethods.size() > 0) {                methodImplMap.put(method.getHandle(), overridingMethods);            }        }

然后下面的一大堆逻辑就是对重写方法关系的持久化存储,最终的methodimpl.dat格式如下:

类名 方法名 方法描述\t重写方法的类名 方法名 方法描述\t重写方法的类名 方法名 方法描述\t重写方法的类名 方法名 方法描述\t重写方法的类名 方法名 方法描述类名 方法名 方法描述\t重写方法的类名 方法名 方法描述\t重写方法的类名 方法名 方法描述

接下来对callgraph.dat的调用关系进行整合,对于同一个方法发起的调用,整合成caller->被调用方法集合之间的映射关系:

    Map<MethodReference.Handle, Set<GraphCall>> graphCallMap = new HashMap<>();    for (GraphCall graphCall : DataLoader        .loadData(Paths.get("callgraph.dat"), new GraphCall.Factory())) {      MethodReference.Handle caller = graphCall.getCallerMethod();      if (!graphCallMap.containsKey(caller)) {        Set<GraphCall> graphCalls = new HashSet<>();        graphCalls.add(graphCall);        graphCallMap.put(caller, graphCalls);      } else {        graphCallMap.get(caller).add(graphCall);      }    }

剩下的挖掘逻辑我们用一个例子来分析:

设我们有如下方法间调用:

  1. A.sources() 污染参数 0

  2. A.sources(0) → 调用 B.load(0)

  3. B.load(0) → 调用接口方法 C.handle(0)

  4. C.handle(0) 在实现类 CImpl 中有实现 CImpl.handle(0)

  5. CImpl.handle(0) → 调用 D.sink(1)(这里假设它把参数 1 污染到 sink)

  6. D.sink(1) 是最终的 sink

对应的数据结构:

  • sources.dat 只包含一个 Source(A.sources, taintedArgIndex=0)

  • graphCallMap

    A.sources → { GraphCall(callerArgIndex=0, targetMethod=B.load,   targetArgIndex=0) }B.load    → { GraphCall(callerArgIndex=0, targetMethod=C.handle, targetArgIndex=0) }C.handle  → { GraphCall(callerArgIndex=0, targetMethod=C.handle, targetArgIndex=0) } // interfaceCImpl.handle → { GraphCall(callerArgIndex=0, targetMethod=D.sink, targetArgIndex=1) }
    
  • implementationFinder.getImplementations(C.handle){ CImpl.handle }

  • isSink(D.sink,1)true

对于是否为sink点的判断逻辑如下:

 private boolean isSink(MethodReference.Handle method, int argIndex,      InheritanceMap inheritanceMap) {    if (!customSlinks.isEmpty()) {      for (CustomSlink customSlink:customSlinks) {        boolean flag = false;        if (customSlink.getClassName() != null)          flag &= customSlink.getClassName().equals(method.getClassReference().getName());        if (customSlink.getMethod() != null)          flag &= customSlink.getMethod().equals(method.getName());        if (customSlink.getDesc() != null)          flag &= customSlink.getDesc().equals(method.getDesc());        if (flag)          return flag;      }      return false;    }    if (config.getName().equals("sqlinject")) {      //SQLInject只能检测注入      return isSQLInjectSink(method, argIndex, inheritanceMap);    }    if (config.getName().equals("hessian")) {      //仅hessian可选BCEL slink      if (ConfigHelper.slinks.contains("BCEL") && BCELSlink(method, argIndex, inheritanceMap)) {        return true;      }    }​    //通用slink,不设定slink则全部都挖掘    if ((ConfigHelper.slinks.isEmpty() || ConfigHelper.slinks.contains("JNDI")) && JNDISlink(method, inheritanceMap)) {      return true;    }    if ((ConfigHelper.slinks.isEmpty() || ConfigHelper.slinks.contains("CLASSLOADER")) && ClassLoaderlink(method, argIndex, inheritanceMap)) {      return true;    }​    if ((ConfigHelper.slinks.isEmpty() || ConfigHelper.slinks.contains("SSRFAndXXE")) && SSRFAndXXESlink(method, inheritanceMap)) {      return true;    }    if ((ConfigHelper.slinks.isEmpty() || ConfigHelper.slinks.contains("EXEC")) && EXECSlink(method, argIndex)) {      return true;    }    if ((ConfigHelper.slinks.isEmpty() || ConfigHelper.slinks.contains("FileIO")) && FileIOSlink(method)) {      return true;    }    if ((ConfigHelper.slinks.isEmpty() || ConfigHelper.slinks.contains("Reflect")) && ReflectSlink(method, argIndex, inheritanceMap)) {      return true;    }    if ((ConfigHelper.slinks.isEmpty() || ConfigHelper.slinks.contains("JDBC")) && JDBCSlink(method, argIndex, inheritanceMap)) {      return true;    }    if ((ConfigHelper.slinks.isEmpty() || ConfigHelper.slinks.contains("EL")) && ELSlink(method, argIndex, inheritanceMap)) {      return true;    }    if ((ConfigHelper.slinks.isEmpty() || ConfigHelper.slinks.contains("SQLInject")) && isSQLInjectSink(method, argIndex, inheritanceMap)) {      return true;    }    return false;  }

配置参数:

maxChainLength = 10opLevel = 2taintTrack = true

1️⃣ 初始化

for each Source:  srcLink = (A.sources, 0)  methodsToExplore  = [ [ A.sources(0) ] ]  exploredMethods   = { A.sources(0) }discoveredGadgets = { }

2️⃣ 第一次迭代

  • iteration=0 → pop first chain

    chain = [ A.sources(0) ]lastLink = (A.sources,0)
    
  • 长度检查1 < maxChainLength → 通过

  • 取出 graphCallMap.get(A.sources){ GC1 }

    • GC1: (callerArgIndex=0 → targetMethod=B.load, targetArgIndex=0)
  • taintTrackGC1.callerArgIndex(0) == lastLink.taintedArgIndex(0) → 通过

  • 找实现allImpls = getImpls(B.load){ B.load }(普通方法)

  • 遍历 impls

    • methodImpl = B.load

    • newLink = (B.load,0)

    • 去重exploredMethods 不含 → 继续

    • 新链newChain = [ A.sources(0), B.load(0) ]

    • sink 检测isSink(B.load,0)false

    • 加入队列

      methodsToExplore = [ [A.sources(0),B.load(0)] ]exploredMethods.add(B.load(0))
      

3️⃣ 第二次迭代

  • iteration=1 → pop

    chain = [A.sources(0),B.load(0)]lastLink = (B.load,0)
    
  • graphCallMap.get(B.load){ GC2 }

    • GC2: (callerArgIndex=0 → targetMethod=C.handle, targetArgIndex=0)
  • taintTrack:匹配 → 通过

  • implsgetImpls(C.handle){ },fallback 父类查找也无(接口),所以按注释 “GadgetInspector bug”,跳到父类去搜,依次找到 C.handle 本身,加入。

  • impls 变为{ C.handle }

  • for each impl

    • newLink = (C.handle,0)

    • 去重通过

    • newChain = [A.sources(0),B.load(0),C.handle(0)]

    • isSink(C.handle,0)false

    • 加入:

      methodsToExplore = [ [A.sources(0),B.load(0),C.handle(0)] ]exploredMethods.add(C.handle(0))
      

4️⃣ 第三次迭代

  • chain = [A.sources(0),B.load(0),C.handle(0)]

  • graphCallMap.get(C.handle){ GC3 }

    • GC3: (callerArgIndex=0 → targetMethod=C.handle, targetArgIndex=0) // 发自实现类
  • taintTrack:匹配

  • implsgetImpls(C.handle){ CImpl.handle }

  • for each

    • newLink = (CImpl.handle,0)

    • 去重通过

    • newChain = [A.sources(0),B.load(0),C.handle(0),CImpl.handle(0)]

    • isSink(CImpl.handle,0)false

    • 入队 & 加入 exploredMethods

5️⃣ 第四次迭代

  • chain = [ …, CImpl.handle(0)]

  • graphCallMap.get(CImpl.handle){ GC4 }

    • GC4: (callerArgIndex=0 → targetMethod=D.sink, targetArgIndex=1)
  • taintTrack:匹配

  • implsgetImpls(D.sink){ D.sink }

  • for each

    • newLink = (D.sink,1)

    • 去重通过

    • newChain = [ …, CImpl.handle(0), D.sink(1)]

    • isSink(D.sink,1)true

    • 加入 discoveredGadgets

此时 methodsToExplore 可能为空,循环结束。

接下来进行链路聚合优化

java复制编辑for (GadgetChain shortChain : methodsToExploreRepeat) {  for (GadgetChain fullChain : discoveredGadgets) {    if (shortChain.lastLink  出现在 fullChain 里) {      // 把 fullChain 从 shortChain.lastLink 之后的部分拼过来      tmpDiscoveredGadgets.add( 拼合后的链 );    }  }}discoveredGadgets.addAll(tmpDiscoveredGadgets);
  • 比如如果我们因为 opLevel 限制,把某条中间链放进了 methodsToExploreRepeat 而没展开到 sink,那么这段逻辑就能 把这些中途链 自动补全到 已知的完整 Chain,得到更多发现。