深入分析GadgetInspector核心代码(一)

简介: 深入分析GadgetInspector核心代码

1 前言

1.1 简介

GadgetInspector是Black Hat 2018提出的一个Java反序列化利用链自动挖掘工具,核心技术的Java ASM,结合字节码的静态分析。根据输入JAR包和JDK已有类进行分析,最终得到利用链

本文的核心是:深入分析数据流模块(PassthroughDataflow)的每一句ASM代码,进而把握最底层的原理


1.2 整体流程

整个流程第一步是根据JDK和输入的Jar得到所有的字节码,然后通过MethodDiscovery分析,参考第2,3章。获取所有的方法信息,类信息和继承信息。继承关系InheritanceMap指某个类的父类和实现的接口都有哪些

e94270efaaba6355536ec66e0132e56f_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


第二步是本文的核心,数据流分析确定:方法的参数和返回值之间的关系。利用第一步获得信息得到方法中的方法调用,结合InheritanceMap的继承关系,将所有方法进行拓扑逆排序(参考8.7)实现最先调用的方法在最前端。然后利用PassthroughDiscovery得到每个方法的参数和返回值之间的关系,也就是返回值能够被哪些参数污染

而PassthroughDiscovery的底层是TaintTrakingMethodVisitor,这个类是该项目的核心,参考第5节,他模拟了JVM Stack Frame中的Operand Stack和Local Variables Array让代码“动”起来,进而根据方法调用流程拿到具体的结果passthroughDataflow,参考第6,7和8节。这个结果从一开始是最底层的调用,所以他的第一步结果可以被第二步分析使用

eb031cd1ba990c3923edd45596077d21_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


后续利用上文模拟的机制,生成调用图(CallGraph)后结合漏洞触发入口(readObject等)得到discoveredSources,主要保存了方法入口和污染参数信息。在最后一步和之前所有信息合并

c81dc4cfbfa0b49f85420a307719f383_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


1.3 加载

主要是区分了SpringBoot Fat Jar,Jar Lib,War三种方式:

  • SpringBoot Fat Jar:一个大List,加入解压后的BOOT-INF/classes路径,并加入BOOT-INF/lib下所有Jar Lib的路径,最终构造一个URLClassLoader用于获取所有字节码文件,JVM停止后自动删除解压路径
  • Jar Lib:直接将所有输入的Jar Lib加入JarClassLoader(该类继承自URLClassLoader)
  • War:一个大List,加入解压后的WEB-INF/classes路径,并加入WEB-INF/lib下所有Jar Lib的路径,最终构造一个URLClassLoader用于获取所有字节码文件,JVM停止后自动删除解压路径


加载字节码的核心方法来自guava库的ClassPath.from(classLoader).getAllClasses()


最终获得的都是ClassLoader对象,然后统一获得所有字节码文件

for (ClassPath.ClassInfo classInfo : ClassPath.from(classLoader).getAllClasses()) {
   result.add(new ClassLoaderClassResource(classLoader, classInfo.getResourceName()));
}


加载JDK的rj.jar代码如下,利用String类拿到rt.jar的路径,构造URLClassLoader然后加载

// java.lang.String类在rt.jar中,JDK不只是rt.jar,个别类并不属于rt.jar
URL stringClassUrl = Object.class.getResource("String.class");
URLConnection connection = stringClassUrl.openConnection();
Collection<ClassResource> result = new ArrayList<>();
if (connection instanceof JarURLConnection) {
   URL runtimeUrl = ((JarURLConnection) connection).getJarFileURL();
   URLClassLoader classLoader = new URLClassLoader(new URL[]{runtimeUrl});
   // 类似的操作
   for (ClassPath.ClassInfo classInfo : ClassPath.from(classLoader).getAllClasses()) {
       result.add(new ClassLoaderClassResource(classLoader, classInfo.getResourceName()));
  }
}

经过测试,ClassPath.from方式拿到Jar的class文件是包含rt.jar的,大概在三万多。如果jar数量多,会出现大量的重复,造成不小的性能问题,是否存在一种方式可以直接拿到rt.jar和输入jar的所有class文件的inputStream并且不重复?(已实现,后续开源)

当然,也可能是笔者本地调试的问题,由于一些特殊原因导致出现大量的重复,这点不是文章的重点,顺便提到而已


1.4 基础

基础主要是ASM技术的一些基础,需要大致明白ASM如何解析字节码

这里给出ClassVisit和MethodVisit的顺序,以便于后续的理解

ClassVisit:大体来看visit->visitAnno->visitField或visitMethod->visitEnd

visit
[visitSource][visitModule][visitNestHost][visitPermittedSubclass][visitOuterClass]
(
visitAnnotation |
visitTypeAnnotation |
visitAttribute
)*
(
visitNestMember |
visitInnerClass |
visitRecordComponent |
visitField |
visitMethod
)*
visitEnd


MethodVisit:大体来看visitParam->visitAnno->visitCode->visitFrame或visitXxxInsn->visitMax->visitEnd

(visitParameter)*
[visitAnnotationDefault]
(visitAnnotation | visitAnnotableParameterCount | visitParameterAnnotation | visitTypeAnnotation | visitAttribute)*
[
   visitCode
  (
       visitFrame |
       visitXxxInsn |
       visitLabel |
       visitInsnAnnotation |
       visitTryCatchBlock |
       visitTryCatchAnnotation |
       visitLocalVariable |
       visitLocalVariableAnnotation |
       visitLineNumber
  )*
   visitMaxs
]
visitEnd


GadgetInspector模拟了JVM Stack中Frame的Operand Stack和Local Variables Array,这一步是基础将在5.1中介绍


1.5 杂项

一些细节,比如ClassReference.Handle重写equal

可以看到判断两个类名对象Handle是否相等是根据字符串name做的,因此hashcode只需要根据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;
}


处理jar包内的class文件,比较巧妙。可以看到创建了一个临时目录,比如windows在C:\\User\\AppData\\Local\\Temp中。添加一个shutdownHook会在JVM退出的时候调用,在这里面删除这个临时目录。而临时目录中保存的是jar中的所有class文件,用于创建输入流,然后交给ClassReader做分析

final Path tmpDir = Files.createTempDirectory("exploded-jar");
// Delete the temp directory at shutdown
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
   try {
       deleteDirectory(tmpDir);
  } catch (IOException e) {
       LOGGER.error("Error cleaning up temp directory " + tmpDir.toString(), e);
  }
}));


还有一些小问题,这里就不继续写了,回到重点


2 MethodDiscoveryClassVisitor

继承自ASM的ClassVisitor,主要作用是对所有字节码中的类进行观察,下文将根据ASM定义的visit顺序进行分析


2.1 visit

// 观察到某一个类的时候会先调用visit方法
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继续
   // 类似中间人攻击,不阻断正常流程,且可以在中间做事情
   super.visit(version, access, name, signature, superName, interfaces);
}


注意到其中的(access & Opcodes.ACC_INTERFACE) != 0为什么这样可以判断是否为接口,因为Opcode中定义如下,发现每一种标识恰好二进制某一位为1,如果按位与,只要不包含该表示,那么得出结果一定是0

// 0000 0000 0000 0001
int ACC_PUBLIC = 0x0001; // class, field, method
// 0000 0000 0000 0010
int ACC_PRIVATE = 0x0002; // class, field, method
// 0000 0000 0000 0100
int ACC_PROTECTED = 0x0004; // class, field, method
// 0000 0010 0000 0000
int ACC_INTERFACE = 0x0200; // class


2.2 visitAnnotation

注解在整个流程中没有什么实际的意义

// 调用完visit后会到达visitAnnotation
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
   annotations.add(descriptor);
   // 不阻断
   return super.visitAnnotation(descriptor, visible);
}


2.3 visitField

// visitField和visitMethod调用优先级一样
// 对类属性进行观察
public FieldVisitor visitField(int access, String name, String desc,
                              String signature, Object value) {
   // 该属性非STATIC
   if ((access & Opcodes.ACC_STATIC) == 0) {
       Type type = Type.getType(desc);
       String typeName;
       if (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY) {
           // 属性类型非引用调用getInternalName得到名称
           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);
}

2.4 visitMethod

// 对类里的方法进行观察
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
    // 是否是STATIC
    boolean isStatic = (access & Opcodes.ACC_STATIC) != 0;
    //找到一个方法,添加到缓存
    discoveredMethods.add(new MethodReference(
        classHandle,
        name,
        desc,
        isStatic));
    // 传递
    return super.visitMethod(access, name, desc, signature, exceptions);
}


2.5 visitEnd

// 最后调用的一定是visitEnd方法
public void visitEnd() {
   ClassReference classReference = new ClassReference(
       name,
       superName,
       interfaces,
       isInterface,
       //把所有找到的字段(属性)封装
       members.toArray(new ClassReference.Member[members.size()]),
       annotations);
   //添加类到缓存
   discoveredClasses.add(classReference);
   super.visitEnd();
}


2.6 作用

得到所有类和方法信息后,进行分析获取进一步的信息,并保存供后续步骤操作



3 MethodCallDiscoveryClassVisitor

3.1 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);
    if (this.name != null) {
        throw new IllegalStateException("ClassVisitor already visited a class!");
    }
    // 记录当前visit的类
    this.name = name;
}


3.2 visitMethod

在Java7以前的版本会用到jsr指令,本质原因是为了程序的兼容性,兼容Jar包和JDK一些老类

public MethodVisitor visitMethod(int access, String name, String desc,
                                 String signature, String[] exceptions) {
    // 先进行正常的方法观察
    MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
    // 跟入4
    MethodCallDiscoveryMethodVisitor modelGeneratorMethodVisitor = new MethodCallDiscoveryMethodVisitor(
            api, mv, this.name, name, desc);
    // 等价于return new MethodCallDiscoveryMethodVisitor(...);
    return new JSRInlinerAdapter(modelGeneratorMethodVisitor, access, name, desc, signature, exceptions);
}

3.3 作用

与MethodCallDiscoveryMethodVisitor一起记录所有方法内的所有方法调用

public String Test(){
    A a = new A();
    a.test1();
    // static
    B.test2();
}

例如这里的test1和test2就是方法内的方法调用


4 MethodCallDiscoveryMethodVisitor

4.1 构造

// 内部类构造方法
public MethodCallDiscoveryMethodVisitor(final int api, final MethodVisitor mv,
                                   final String owner, String name, String desc) {
    super(api, mv);
    // 记录当前方法中的所有方法调用
    this.calledMethods = new HashSet<>();
    // 将当前visit的method添加到全局变量
    methodCalls.put(new MethodReference.Handle(new ClassReference.Handle(owner), name, desc), calledMethods);
}


4.2 visitMethodInsn

方法中的方法相关指令

@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
   // visit的方法中如果存在方法调用都加入全局变量(无论static,interface还是普通调用)
   calledMethods.add(new MethodReference.Handle(new ClassReference.Handle(owner), name, desc));
   super.visitMethodInsn(opcode, owner, name, desc, itf);
}


4.3 作用

与MethodCallDiscoveryClassVisitor一起记录方法内的方法调用,参考3.3


5 TaintTrackingMethodVisitor

5.1 JVM Frame

分析该类离不开JVM的原理。JVM Stack在每个线程被创建时被创建,用来存放一组栈帧(Frame)

569deb1aa5961c7d0ebb60239aa0f518_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


每次方法调用均会创建一个对应的Frame,方法执行完毕或者异常终止,Frame被销毁

而每个Frame的结构如下,主要由本地变量数组(local variables)和操作栈(operand stack)组成

2e5b3661aeea799becfc726911a175d5_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


局部变量表所需的容量大小是在编译期确定下来的,表中的变量只在当前方法调用中有效

JVM把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。比如,iadd指令就要从操作数栈中弹出两个整数,执行加法运算,其结果又压回到操作数栈中

例如:方法调用会从当前的Stack里弹出参数,而弹出的参数就到了新的局部变量表里,执行完返回的时候就得把返回值PUSH回Stack。比如5.4中的visitCode做的事就是将参数放到局部变量表

19bea79a8512b9b82116a43d77cd66b0_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


之所以介绍JVM Frame,是因为代码模拟了比较完善的Operand Stack和Local Varialbles交互,例如方法调用会从Stack中弹出参数,方法返回值会压入栈中。根据这样的规则,进而执行数据流的分析


5.2 SavedVariableState

在5.1中介绍stack和local variables因为在TaintTrackingMethodVisitor中自行实现了这样的结构

注意到这里保存的Set集合,实际上代码中要么是空Set和Null做占位,要么保存的是实际有意义的值,也就是污染点

污染点的含义是参数索引,进而分析影响返回值的参数是什么。那为什么要用Set不会数组或List呢?因为Set自带去重,分析代码中会往Stack中设置多次污染信息(见后文分析)

private static class SavedVariableState<T> {
   // [local variables]
   List<Set<T>> localVars;
   // [operand stack]
   List<Set<T>> stackVars;
   // 新建构造
   public SavedVariableState() {
       localVars = new ArrayList<>();
       stackVars = new ArrayList<>();
  }
   // 复制构造
   public SavedVariableState(SavedVariableState<T> copy) {
       this.localVars = new ArrayList<>(copy.localVars.size());
       this.stackVars = new ArrayList<>(copy.stackVars.size());
       for (Set<T> original : copy.localVars) {
           this.localVars.add(new HashSet<>(original));
      }
       for (Set<T> original : copy.stackVars) {
           this.stackVars.add(new HashSet<>(original));
      }
  }
   // 根据传入值合并
   public void combine(SavedVariableState<T> copy) {
       for (int i = 0; i < copy.localVars.size(); i++) {
           while (i >= this.localVars.size()) {
               this.localVars.add(new HashSet<T>());
          }
           this.localVars.get(i).addAll(copy.localVars.get(i));
      }
       for (int i = 0; i < copy.stackVars.size(); i++) {
           while (i >= this.stackVars.size()) {
               this.stackVars.add(new HashSet<T>());
          }
           this.stackVars.get(i).addAll(copy.stackVars.get(i));
      }
  }
}

5.3 构造

有一些变量将在后文分析

// 根据已有类信息分析生成的[子类->[祖先类,祖先的子类...父类]]
private final InheritanceMap inheritanceMap;
// 暂不分析
private final Map<MethodReference.Handle, Set<Integer>> passthroughDataflow;
// 一个工具类,暂不分析
private final AnalyzerAdapter analyzerAdapter;
// public/private...
private final int access;
// 方法名
private final String name;
// void(int a) -> (I)V
private final String desc;
// 泛型,这里没用
private final String signature;
// 异常,这里没用
private final String[] exceptions;
public TaintTrackingMethodVisitor(...) {
   super(api, new AnalyzerAdapter(owner, access, name, desc, mv));
   // 一系列赋值
   this.inheritanceMap = inheritanceMap;
  ......
}
// stack和local variables
private SavedVariableState<T> savedVariableState = new SavedVariableState<T>();
// 处理goto问题,暂不分析
private Map<Label, SavedVariableState<T>> gotoStates = new HashMap<Label, SavedVariableState<T>>();
// 处理异常问题,暂不分析
private Set<Label> exceptionHandlerLabels = new HashSet<Label>();


5.4 visitCode

最先调用visitCode,在进入方法体的时候

这是数据流动的起始位置,注意到根据实际情况在局部变量表里设置参数,这是模拟JVM真实的情况,以便于后续数据流分析

// 首先执行的方法
public void visitCode() {
    super.visitCode();
    // 清空stack和local variables
    savedVariableState.localVars.clear();
    savedVariableState.stackVars.clear();
    if ((this.access & Opcodes.ACC_STATIC) == 0) {
        // 如果方法非static那么local variables[0]=this
        // 这里没有给出实际的值,而是直接占了一位
        savedVariableState.localVars.add(new HashSet<T>());
    }
    // 方法参数类型
    for (Type argType : Type.getArgumentTypes(desc)) {
        // size只会是1或2
        for (int i = 0; i < argType.getSize(); i++) {
            // 根据size将所有参数都占位
            savedVariableState.localVars.add(new HashSet<T>());
        }
    }
}



5.5 push & pop & get

模拟Stack的push和pop操作

// 模拟stack的push
private void push(T ... possibleValues) {
   Set<T> vars = new HashSet<>();
   for (T s : possibleValues) {
       vars.add(s);
  }
   savedVariableState.stackVars.add(vars);
}
// 模拟stack的push,直接设置Set
private void push(Set<T> possibleValues) {
   savedVariableState.stackVars.add(possibleValues);
}
// 模拟stack的pop
private Set<T> pop() {
   // 注意stack后入先出,所以是最后一个
   return savedVariableState.stackVars.remove(savedVariableState.stackVars.size()-1);
}
private Set<T> get(int stackIndex) {
   // 假设stack大小为10,传入index是3,那么get的索引为6,是第7个
   return savedVariableState.stackVars.get(savedVariableState.stackVars.size()-1-stackIndex);
}


5.6 visitFrame

visitFrame在visitCode后调用

主要作用是根据ASM给出“正确”的Frame计算方法同步当前模拟的Stack和局部变量表,确保不出现问题

第一步判断的F_NEW原因可以参考ASM源码:Must be Opcodes.F_NEW for expanded frames


几个参数的意义参考ASM文档:

  • type:the type of this stack map frame
  • nLocal:the number of local variables in the visited frame
  • local:the local variable types in this frame.This array must not be modified
  • nStack:the number of operand stack elements in the visited frame
  • stack:the operand stack types in this frame.This array must not be modified
public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) {
   if (type != Opcodes.F_NEW) {
       throw new IllegalStateException("Compressed frame encountered; class reader should use accept() with EXPANDED_FRAMES option.");
  }
   int stackSize = 0;
   for (int i = 0; i < nStack; i++) {
       Object typ = stack[i];
       int objectSize = 1;
       // long和double的大小为2
       if (typ.equals(Opcodes.LONG) || typ.equals(Opcodes.DOUBLE)) {
           objectSize = 2;
      }
       for (int j = savedVariableState.stackVars.size(); j < stackSize+objectSize; j++) {
           // 根据size在模拟stack中占位
           savedVariableState.stackVars.add(new HashSet<T>());
      }
       // 统计总stack大小
       stackSize += objectSize;
  }
   int localSize = 0;
   for (int i = 0; i < nLocal; i++) {
       Object typ = local[i];
       int objectSize = 1;
       // 类似
       if (typ.equals(Opcodes.LONG) || typ.equals(Opcodes.DOUBLE)) {
           objectSize = 2;
      }
       // 类似,占位
       for (int j = savedVariableState.localVars.size(); j < localSize+objectSize; j++) {
           savedVariableState.localVars.add(new HashSet<T>());
      }
       // 统计
       localSize += objectSize;
  }
   // 根据统计出的真实size进行缩容,达到一致
   for (int i = savedVariableState.stackVars.size() - stackSize; i > 0; i--) {
       savedVariableState.stackVars.remove(savedVariableState.stackVars.size()-1);
  }
   // 根据统计出的真实size进行缩容,达到一致
   for (int i = savedVariableState.localVars.size() - localSize; i > 0; i--) {
       savedVariableState.localVars.remove(savedVariableState.localVars.size()-1);
  }
   // 传递
   super.visitFrame(type, nLocal, local, nStack, stack);
   // 验证
   sanityCheck();
}
private void sanityCheck() {
   // 利用analyzerAdapter计算和验证stack的size
   if (analyzerAdapter.stack != null && savedVariableState.stackVars.size() != analyzerAdapter.stack.size()) {
       throw new IllegalStateException("Bad stack size.");
  }
}


相关文章
|
4月前
|
运维 自然语言处理 监控
软件研发核心问题之在需求拆解过程中,“需要多少UI”的问题如何解决
软件研发核心问题之在需求拆解过程中,“需要多少UI”的问题如何解决
|
5月前
|
C++
C++核心技术要点《异常处理详解》
C++核心技术要点《try-throw-catch异常处理详解》
50 2
|
5月前
|
数据采集 存储 监控
构建高效爬虫系统:设计思路与案例分析
构建高效爬虫系统涉及关键模块如爬虫引擎、链接存储、内容处理器等,以及用户代理池、IP代理池等反反爬策略。评估项目复杂性考虑数据规模、网站结构、反爬虫机制等因素。案例分析展示了电子商务价格比较爬虫的设计,强调了系统模块化、错误处理和合规性的重要性。爬虫技术需要不断进化以应对复杂网络环境的挑战。
125 1
|
6月前
|
存储 算法 编译器
C++性能调优:从代码层面提升程序效率
本文探讨了C++程序性能调优的关键点:选择合适的数据结构和算法,例如用哈希表(如`std::unordered_map`)替换低效的数组或链表;减少不必要的内存分配和释放,利用智能指针和容器如`std::vector`自动管理内存;优化循环和条件语句,例如在循环外存储数组大小;利用编译器优化如`-O2`或`-O3`;以及使用性能分析工具如`gprof`、`callgrind`和`perf`识别并解决性能瓶颈。通过这些方法,可以有效提升C++程序的运行效率。
|
6月前
|
资源调度 供应链 监控
深入探究:ERP系统的核心模块解析
深入探究:ERP系统的核心模块解析
320 0
|
XML 开发框架 安全
C#学习核心知识总结
C#学习核心知识总结
53 1
|
消息中间件 缓存 NoSQL
程序员快来学习缓存层场景实战数据收集—技术选型思路及整体方案
根据以上业务场景,项目组提炼出了6点业务需求,并针对业务需求梳理了技术选型相关思路。 1)原始数据海量:对于这一点,初步考虑使用HBase进行持久化。 2)对于埋点记录的请求响应要快:埋点记录服务会把原始埋点记录存放在一个缓存层,以此保证响应快速。关于这一点有多个缓存方案,稍后展开讨论。 3)可通过后台查询原始数据:如果直接使用HBase作为查询引擎,查询速度太慢,所以还需要使用Elasticsearch来保存查询页面上作为查询条件的字段和活动ID。
|
SQL 监控 NoSQL
技术组件优化分析:原理、方法与实战分享
对一个固定的技术组件的分析优化思路,即组件不是我们开发的,但又要分析优化它,怎么办? 当数据库的CPU并没有全部用完,而是只用了几颗的时候,如何具体定向?将用到查看数据库本身线程栈的方法,这和前面直接看trx表有所不同。
130 0
|
存储 安全 Java
深入分析GadgetInspector核心代码(二)
深入分析GadgetInspector核心代码
102 0
深入分析GadgetInspector核心代码(二)
|
缓存 安全 Java
深入分析GadgetInspector核心代码(三)
深入分析GadgetInspector核心代码
173 0
深入分析GadgetInspector核心代码(三)