0x00 认识IAST

笔者初识IAST,未曾深入接触,源于兴趣开始学习,可能有理解不到位的地方,请多多包涵指导。

IAST全称Interactive Application Security Testing 交互式应用程序安全测试,它在程序运行过程中使用插桩技术监控和收集信息,根据这些信息来判断程序是否存在告警。它对来自客户端产生的请求和响应进行分析,这点类似于DAST;而它能够监控数据流信息,通过污点分析产生告警又类似于SAST。IAST有很多种模式,代理模式、流量镜像模式,插桩模式等,可参考https://www.freebuf.com/articles/web/290863.html。

本次重点介绍插桩模式,插桩实现主要通过动态hook和污点传播。
hook技术:Java最常见的hook的是JVM层,通过JVMTI接口Instrumentation在类被加载之前对类进行拦截,通过插入字节码进行修改并重载类实现hook;
污点传播:污点传播分析技术是信息流分析技术的一种实践方法,通过对系统中敏感数据进行标记跟踪数据在程序中的传播来进行安全检测。

插桩模式有主动插桩模式和被动插桩模式。
主动插桩模式:在关键函数hook到流量后,会添加payload进行扫描,包含了类似DAST的功能,主动对目标应用进行扫描,应用服务器的IAST agent不会追踪整个污点数据流,仅收集关键数据,将数据发送给IAST管理端,IAST管理端会向应用服务器发送构造好的重放流量来验证风险是否存在;

被动插桩模式:不会主动发送payload,对来自客户端的请求响应进行污点传播数据流监控,根据是否经过无害化处理判断是否存在漏洞。

本文主要针对JAVA类应用,需要用到的技术包括污点分析、插桩技术Instrumentation&agent、字节码增强技术如ASM、javassist,接下来的笔记是先把这些技术进行介绍,然后再对被动插桩式IAST demo项目进行改动,以SQL注入和log4j2 CVE-2021-44228漏洞进行深入理解并且记录遇到的问题。

0x01 污点分析:

污点分析可以抽象成一个三元组<sources,sinks,sanitizers>的形式:
1、source即污点源,代表直接引入不受信任的数据或者机密数据到系统中;
2、sink即污点汇聚点,代表直接产生安全敏感操作(违反数据完整性)或者泄露隐私数据到外界(违反数据保密性);
3、sanitizer即无害处理,代表通过数据加密或者移除危害操作等手段使数据传播不再对软件系统的信息安全产生危害。

从source到sink数据是否经过了sanitizer无害处理,如果经过则认为信息流是安全的,不经过那么认为信息流存在安全问题的。如下图是一个SQL执行的示例,我们需要通过污点分析思想判断它是否存SQL注入漏洞,假设String sql = "select * from user where id=" + value;中的value可以通过用户输入获取,我们判断它就是一个source,而执行SQL语句的代码认为是敏感操作,判断为sink,中间代码是在SQL语句后面添加'和转义处理语句中的单引号,属于sanitizer无害处理,当sql经过无害处理后,不再存在SQL注入漏洞,如果未经无害处理直接执行,则存在SQL注入漏洞。

通过上面示例,我们大概知道了通过污点分析能够发现一些安全问题,想了解更多可以看看这篇文章简单理解污点分析技术

0x02 插桩技术:

JVM不能直接执行.java 代码或者.class文件,它只能执行.class 文件中存储的指令码。class需要通过classLoader 装载以后才能被执行。如果我们想要在JVM加载class前或加载class后修改class字节码,添加埋点逻辑并重新进行加载,需要用到Instrumentation与ASM,Instrumentation可以拦截ClassLoad加载或者重新对class加载,ASM操作修改字节码 增加代码逻辑。接下来我们就Instrumentation和ASM技术进行介绍。

Instrumentation

Instrumentation主要用于类定义动态改变和操作,在JVM运行状态拦截class加载并提供类转换服务。

ClassFileTransformer

ClassFileTransformer是一个类文件转换器,提供类字节码操作服务,可以在transform方法中定义字节码的修改并返回新的字节码数组。ClassFileTransformer通常被Instrumentation用来注册转换器在类加载时进行类的转换,接口定义如下:

package java.lang.instrument;

import  java.security.ProtectionDomain;
public interface ClassFileTransformer {
  byte[] transform(  ClassLoader         loader, // 类加载器
                   String              className, // 类名
                   Class<?>            classBeingRedefined, // 类重定义
                   ProtectionDomain    protectionDomain, //保护域
                   byte[]              classfileBuffer) //类的字节码数组
    throws IllegalClassFormatException;
}

Instrumentation接口

Instrumentation定义了很多接口,常用的接口如下:

1//注册ClassFileTransformer实例,注册多个会按照注册顺序进行调用。所有的类被加载完毕之后会调用ClassFileTransformer实例,相当于它们通过了redefineClasses方法进行重定义。cransform表示是否能够通过retransformClasses方法进行回滚。
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
//添加ClassFileTransformer,默认不能回滚
void addTransformer(ClassFileTransformer transformer);

2//重新定义Class文件
void redefineClasses(ClassDefinition... definitions)

3//已加载类进行重新转换的方法,重新转换的类会被回调到ClassFileTransformer的列表中进行处理
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException

4//可以被重新定义,该方法主要作用于已经加载过的class
boolean isRetransformClassesSupported();

5//ClassFileTransformer
boolean removeTransformer(ClassFileTransformer transformer);

6//是否可以修改Class文件
boolean isModifiableClass(Class<?> theClass);

7//获取所有加载的Class
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();

//获取指定类加载器已经初始化的类
@SuppressWarnings("rawtypes")
Class[] getInitiatedClasses(ClassLoader loader);

Instrumentation有两种agent模式:premain和agentmain,premain和agentmain两种方式最终都是为了回调Instrumentation实例,激活sun.instrument.InstrumentationImpl#transform()从而回调注册到Instrumentation中的ClassFileTransformer以实现字节码修改。

agent onload-premain模式:

premain顾名思义在main前加载探针,启动时通过JVM参数加载agent。

API:

public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)

示例:

打印执行main前所有已加载的类。需要agent的jar包(包括agent和Transfromer)和测试程序。

Agent.java:

import java.lang.instrument.Instrumentation;

public class Agent {
  public static void premain(String agentOps, Instrumentation inst) {
    System.out.println("=======this is agent premain function=======");
    inst.addTransformer(new TestTransfromer());
  }

  public static void agentmain(String agentArgs, Instrumentation instrumentation) {
    System.out.println("loadagent after main run.args=" + agentArgs);
    Class<?>[] classes = instrumentation.getAllLoadedClasses();
    for (Class<?> cls : classes) {
      System.out.println(cls.getName());
    }
    System.out.println("agent run completely.");
    instrumentation.addTransformer(new DefineTransformer());
  }

}

TestTransfromer.java:

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class TestTransfromer implements ClassFileTransformer {
  @Override
  public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
    System.out.println(className.replace("/", "."));
    System.out.println(classfileBuffer.length);
    return classfileBuffer;
  }
}

在pom.xml中添加premain的配置,添加完后,package打包成jar包。

<manifestEntries>
  <!--premain-->
  <Premain-Class>com.r17a.demo.Agent</Premain-Class>
</manifestEntries>

随意写一个main打包,在命令行进行测试:java -javaagent:agent-1.0-SNAPSHOT.jar -jar test1-1.0-SNAPSHOT.jar。如下图,打印出了执行main前已加载的类。

agent onattach-agentmain模式:

agentmain在main运行时加载,是在运行态将agent加载到目标JVM中并在该JVM中执行。

API:

public static void agentmain (String agentArgs, Instrumentation inst)
public static void agentmain (String agentArgs)

示例:

在目标JVM运行时打印所有已加载的类。需要三个程序agent的jar包、正在运行的主程序、将agent加载到主程序所在JVM的程序。

Agent程序:

Agent.java

import java.lang.instrument.Instrumentation;

public class Agent {

  public static void agentmain(String agentArgs, Instrumentation instrumentation) {
    System.out.println("loadagent after main run.args=" + agentArgs);
    Class<?>[] classes = instrumentation.getAllLoadedClasses();
    for (Class<?> cls : classes) {
      System.out.println(cls.getName());
    }
    System.out.println("agent run completely.");
    instrumentation.addTransformer(new DefineTransformeAr());
  }

}

DefineTransformer.java

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class DefineTransformer implements ClassFileTransformer {
  @Override
  public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
    System.out.println("premain load Class:" + className);
    return classfileBuffer;
  }
}

pom.xml添加如下配置,并打包。

<manifestEntries>
  <!--agentmain-->
  <Agent-Class>com.r17a.demo.Agent</Agent-Class>
  <Can-Redefine-Classes>true</Can-Redefine-Classes>
  <Can-Retransform-Classes>true</Can-Retransform-Classes>
  <can-redefine-classes>true</can-redefine-classes>
</manifestEntries>

Main主程序:

Main.java,直接运行

public class Main {
  public static void main(String[] args) throws InterruptedException {
    System.out.println("This is Main Program!");
    // 模拟运行状态
    Thread.sleep(10000000L);
  }
}

AttachLauncher.java:

编写attach程序,将agent.jar加载到Main程序所在JVM进行执行,注意agent.jar填写自己的路径。

import com.sun.tools.attach.*;

import java.io.IOException;
import java.util.List;

public class AttachLauncher {

  public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
    System.out.println("Hello,This is TestAgent Program!");
    System.out.println("running JVM start ");
    List<VirtualMachineDescriptor> list = VirtualMachine.list();
    System.out.println(list.size());
    for (VirtualMachineDescriptor vmd : list) {
      // 匹配目标主程序所在jvm,并加载agent
      if (vmd.displayName().endsWith("com.r17a.demo.Main")) {
        System.out.println("attaching agent to jvm:" + vmd.displayName() + ",jvmid is " + vmd.id());
        VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
        virtualMachine.loadAgent("yourpath/agent-1.0-SNAPSHOT.jar");
        virtualMachine.detach();
        System.out.println("attach finsh!");
      }
    }

  }
}

首先运行Main.java,保证主程序已启动在运行状态,执行AttachLauncher加载agent,运行结果如下:

小结

premain和agentmain两者区别如下:

  • premain在JVM启动时通过命令行加载代理,agentmain采用attach方式向目标JVM中加载代理;
  • premain是所有类首次加载并且进入程序main()方法之前,premain方法就会被激活,然后所有被加载的类都会执行ClassFileTransformer列表中的回调方式,ClassFileTransformer中的类是虚拟机加载的所有类,这个是由于代理加载的顺序比较靠前决定的。agentmain是被代理的目标JVM有可能很早之前已经启动,类已经被加载完成,这个时候需要借助Instrumentation#retransformClasses(Class<?>... classes)让对应的类可以重新转换,从而激活重新转换的类执行ClassFileTransformer列表中的回调。

ASM:

字节码结构

字段 长度 说明
魔数(magic_number) 4字节 class的标志CAFEBABE
版本(version) 4字节 次版本minor version+主版本major version,
常量池(constant_pool) 2字节+cp_info*(n-1) 常量池计数器+常量池数据区=常量数量n+cp_info*(n-1)。不同类型的常量数据结构不同,数值型结构都是tag+bytes,比如CONSTANT_Interger_info为1字节的tag值为3,4字节的bytes为int值;UTF8编码的字符串CONSTANT_Utf8_info的结构是tag(1字节,值为1)+length(2字节,字符串长度)+bytes(长度为length的字符串);ref索引型结构基本都是tag+index+index或者tag+index,比如CONSTANT_Class_info结构为tag(1字节,值为7)+index(2字节,指向类的权限定名常量项的索引),CONSTANT_Fieldref_Class_info
访问标志(access_flag) 2字节 表示类、方法等的修饰符,如ACC_PUBLIC 0x0001,ACC_STATIC 0x0008,那么public static就是ACC_PUBLIC和ACC_STATIC或运算后的结果0x0009
本类索引(this_class) 2字节 类的全限定名的索引,指向常量池该类的CONSTANT_Class_info
父类索引(super_class) 2字节 父类的全限定名的索引
接口索引(interfaces) 4字节 接口计数器+接口信息,即2字节表示实现接口的数量,n个字节是所有接口名称的常量索引值
字段表(fileds) 2字节+field_info*n 字段计数器(2字节)+field_info*n。field_info包括权限修饰符、字段名索引、描述符索引、属性个数、属性列表
方法表(methods) 2字节+method_info*n 方法计数器(2字节)+method_info*n。method_info包括权限修饰符、方法名索引、描述符索引、属性个数、属性列表
特殊属性表(attributes) 2字节+attribute_info 描述文件中类或接口所定义属性的基本信息。

方法表是我们字节码增强操作非常关注的一部分,所以特别说明下。方法表的属性部分包括不限于

  • Code区:JVM指令操作码区,在进行字节码增强时重点操作的就是Code区这一部分。
  • LineNumberTable:行号表,Java源码的行号与Code区字节码指令的对应关系,将Code区的操作码和源代码中的行号对应,Debug时会起到作用(源代码走一行,需要走多少个JVM指令操作码)。
  • LocalVariableTable:本地变量表,包含This和局部变量,之所以可以在每一个方法内部都可以调用This,是因为JVM将This作为每一个方法的第一个参数隐式进行传入。当然,这是针对非Static方法而言。

字节码操作指令(opcode)类型包括加载存储、运算和类型转换、控制转移、对象操作、方法调用、线程同步、异常处理等,具体可以参考:https://segmentfault.com/a/1190000008722128。

ASM字节码增强技术

ASM工具可以修改字节码,核心包有Core API、Tree API、Commons和Util等,ASM转换类的两种方法就是基于事件触发的Core API和基于对象的Tree API。Core API采用事件驱动方法,按照class内容顺序解析文档,当解析触发事件则回调函数处理事件;而Tree API是采用树型结构先将class解析成树结构的数据然后进行处理。本文关注的主要是Core API,想要了解更多可以阅读:asm4-guide。Core API最重要的就是ClassReader、ClassVisitor和ClassWriter这三个类。

ClassReader:字节码读取和分析引擎,负责解析class文件,将class内容解析成对应上面字节码结构的各个节点。当有事件发生,触发相应的ClassVisitor、MethodVisitor进行处理,这个类可以看作一个事件产生器。

ClassVisitor:是一个抽象类 使用时需要继承,ClassVisitor定义解析class字节码时想要触发的事件,可以通过ClassVisitor的visit方法修改原始的字节码,ClassVisitor的每个visitXxx方法都对应于同名的类文件结构节点,比如visitAttribute、visitField、visitMethod等。这样当ClassReader.accept()传入ClassVisitor实例时,ClassVisitor定义ClassReader在解析class的不同节点时需要触发的事件,然后调用ClassVisitor中对应节点的方法。 这个类可以看作一个事件筛选器。

ClassWriter:继承了ClassVisitor接口,可以拼接生成字节码,调用toByteArray将字节码byte数组形式返回。code

这里要特别强调下MethodVisitor,它有非常多用来操作code区(上面字节码部分提到的方法表属性中的)指令码的方法,也是经常会用到的:

方法 描述
visitCode() 开始解析Code属性
visitInsn(int opcode) 访问一个零参数要求的字节码指令,如ACONST_NULL
visitIntInsn(int opcode, int operand) 访问一个零操作栈要求但需要有一个int参数的字节码指令,如BIPUSH
visitVarInsn(int opcode, int var) 访问一个关于局部变量的字节码指令,如ALOAD
visitTypeInsn(int opcode, String type) 访问一个关于类型的字节码指令,如CHECKCAST
visitFieldInsn(int opcode, String owner, String name, String desc) 访问一个有关于字段的字节码,如PUTFIELD
visitMethodInsn(int opcode, String owner, String name, String desc) 访问一个有关于方法调用的字节码,如INVOKESPECIAL
visitJumpInsn(int opcode, Label label) 访问跳转字节码,如IFEQ
visitInvokeDynamicInsn(String name, String desc, Handle bsm,Object... bsmArgs) 基于INVOKEDYNAMIC,动态方法调用,会在lambda表达式和方法引用里用到
visitLdcInsn(Object cst) 基于LDC、LDC_W和LDC2_W,将一个常量加载到操作栈用
visitIincInsn(int var, int increment) 基于IINC、IINC_W,自增/减表达式
visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) 基于TABLESWITCH,用于进行table-switch操作

ASM操作示例:

package com.r17a.demo.asm;

public class Test {
  public void test(){
    System.out.println("the program is  running!");
  }
}

我们尝试将一个Test.class文件读取,然后将test方法的开始和结束加上字符串输出,将其增强为:

package com.r17a.demo.asm;

public class Test {
  public void test(){
    System.out.println("here enhanced: enter");
    System.out.println("the program is  running!");
    System.out.println("here enhanced: leave");
  }
}

首先我们自定义一个ClassVisitor用来定义事件,也就是来修改test方法:

package com.r17a.demo.asm;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class TestClassVisitor extends ClassVisitor implements Opcodes {

  public TestClassVisitor(ClassVisitor cv) {
    super(Opcodes.ASM5, cv);
  }

  @Override
  public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
    super.visit(version, access, name, signature, superName, interfaces);
  }

  @Override
  public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
    MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
    // 当方法名为test时候进行修改
    if(name.equals("test")){
      mv = new TestMethodVisitor(mv);
    }
    return mv;
  }

  class TestMethodVisitor extends MethodVisitor implements Opcodes{

    public TestMethodVisitor(MethodVisitor mv) {
      super(Opcodes.ASM5, mv);
    }

    @Override
    public void visitCode() {
      super.visitCode();
      // 在开始扫描code区时 即方法开始时添加方法调用System.out.println("here enhanced: enter");
      // 首先System.out是一个field: public final static PrintStream out = null;
      mv.visitFieldInsn(Opcodes.GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream;");
      // 将字符串常量加载到栈
      mv.visitLdcInsn("here enhanced: enter");
      // 调用println:  public void println(String x)
      mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,"java/io/PrintStream","println","(Ljava/lang/String;)V",false);
    }

    @Override
    public void visitInsn(int opcode) {
      //    int IRETURN = 172; // visitInsn
      //    int LRETURN = 173; // -
      //    int FRETURN = 174; // -
      //    int DRETURN = 175; // -
      //    int ARETURN = 176; // -
      //    int RETURN = 177; // -
      // 判断opcode是否处于结束状态,return或者抛出异常的情况,在结束前添加字节码
      if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW){
        mv.visitFieldInsn(Opcodes.GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream;");
        mv.visitLdcInsn("here enhanced: leave");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,"java/io/PrintStream","println","(Ljava/lang/String;)V",false);

      }
      super.visitInsn(opcode);
    }
  }
}

通过ClassReader、ClassVisitor、ClassWriter来实现具体的修改,并通过自定义TestClassLoader重新加载类,在调用test方法:

package com.r17a.demo.asm;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.ClassVisitor;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;

public class AsmTest {
    public static void main(String[] args) throws Exception {
        // 自定义ClassLoader重新加载Test,方便调用增强后的test方法
        TestClassLoader testClassLoader = new TestClassLoader();
        Class<?> aClass = testClassLoader.findClass("com.r17a.demo.asm.Test");
        Object test = aClass.newInstance();
        Method method = aClass.getMethod("test");
        method.invoke(test);
    }

    public static byte[] getClassBuffer() throws IOException{
        // 读取class
        ClassReader classReader = new ClassReader("com/r17a/demo/asm/Test");
        // classWriter提供一个编写器
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        // 调用自定义的ClassVisitor定义事件,即增强字节码
        ClassVisitor classVisitor = new TestClassVisitor(classWriter);
        // 通知触发事件
        classReader.accept(classVisitor,ClassReader.SKIP_DEBUG);
        // 获取字节码的byte数组
        byte[] bytes = classWriter.toByteArray();
        FileOutputStream fileOutputStream = new FileOutputStream(new File("/Users/R17a/网安/代码审计/JAVA代码审计/项目/tmp/tmp111/src/main/java/com/r17a/demo/asm/Test.class"));
        fileOutputStream.write(bytes);
        fileOutputStream.close();
        return bytes;
    }

    static class TestClassLoader extends ClassLoader{
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {

            if (name.endsWith("com.r17a.demo.asm.Test")){
                try {
                    // 重定义Test为增强后的类
                    byte[] bytes = getClassBuffer();
                    return defineClass(name,bytes,0,bytes.length);
                }catch (IOException e){
                    return super.findClass(name);
                }
            }
            return super.findClass(name);
        }
    }
}

执行AsmTest后的结果:

AdviceAdapter

AdviceAdapter是MethodVisitor的子类,onMethodEnter、onMethodExit是在AdviceAdapter中定义的两个接口,分别在方法开始和方法结束时修改代码,onMethodEnter和onMethodExit的本质还是调用visitCode或者visitInsn方法来实现的。

protected void onMethodEnter() {}
protected void onMethodExit(int opcode) {}

我们尝试将上面的例子做修改,通过AdviceAdapter在方法开始和结束增加代码逻辑,最终运行效果一样。

public class TestClassVisitorByAdviceAdapter extends ClassVisitor implements Opcodes {

    public TestClassVisitorByAdviceAdapter(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        // 当方法名为test时候进行修改
        if (name.equals("test")) {
            mv = new TestMethodVisitor(Opcodes.ASM5, mv, access, name, desc);
        }
        return mv;
    }

    class TestMethodVisitor extends AdviceAdapter {

        protected TestMethodVisitor(int api, MethodVisitor mv, int access, String name, String desc) {
            super(api, mv, access, name, desc);
        }

        @Override
        protected void onMethodEnter() {
            super.onMethodEnter();
            mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            // 将字符串常量加载到栈
            mv.visitLdcInsn("here enhanced: enter");
            // 调用println:  public void println(String x)
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }

        @Override
        protected void onMethodExit(int opcode) {
            super.onMethodExit(opcode);
            mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("here enhanced: leave");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }
    }
}

0x03 IAST demo

确定IAST思路

我们在完成一个被动插桩式IAST时,必须考虑的肯定有以下部分:

  1. hook方法:在source、sink、propagator等添加方法,用于埋点和监控,
  2. agent和插桩:premain或者agentmain方式加载agent,实现ClassFileTransformer,用ASM添加埋点逻辑
  3. context全局共享:记录调用图等信息

确定污点传播及修改代码

在阅读了sky师傅的浅谈被动式IAST产品与技术实现-代码实现Demo篇,对其代码进行了稍微的改动,用一个JDBC SQL注入和log4j2 jndi注入漏洞示例进行理解,修改后的代码https://github.com/R17a-17/java_iast_example/tree/iast_demo。

SQL注入

首先确定污点分析的source、sink、propagator,source和sink很好确认,很明显就是source可以是客户端传递参数的入口Request.getParameter(),sink是SQL执行的地方即Statement.executeQuery(),但是这里需要注意,实际执行SQL查询的是StatementImpl.executeQuery(),Statement.executeQuery()是一个接口,所以sink是StatementImpl.executeQuery(),那么中间传播propagator可能是一些String的相关操作,SQL注入最常见的是做拼接,Java的string直接+最终还是使用StringBuilder.append(),那么我们尝试将StringBuilder.append()作为propagator。

String id = request.getParameter("id");
...
Statement statement = connection.createStatement();
String sql = "select * from user where id=" + value;
ResultSet resultSet = statement.executeQuery(sql);

这一步我们确定了:

  1. source:Request.getParameter()
  2. sink:StatementImpl.executeQuery()
  3. propagator:StringBuilder.append()

那么我们可以对https://github.com/iiiusky/java_rasp_example进行修改,已知原代码本身:已经对source Request.getParameter()添加了代码逻辑,所以我们不需要再进行添加。先对sink进行添加在cn.org.javaweb.iast.visitor.handler.SinkClassVisitorHandler的ClassVisitorHandler方法中,添加判断条件className.equals("com.mysql.cj.jdbc.StatementImpl")&&name.equals("executeQuery"),意味着对com.mysql.cj.jdbc.StatementImpl.executeQuery()进行埋点:

public MethodVisitor ClassVisitorHandler(MethodVisitor mv, final String className, int access,
                                             final String name, final String desc, String signature, String[] exceptions) {
        if (("start".equals(name) && METHOD_DESC.equals(desc))||(className.equals("com.mysql.cj.jdbc.StatementImpl")&&name.equals("executeQuery"))) {
            ...

同理在cn.org.javaweb.iast.visitor.handler.PropagatorClassVisitorHandlerStringBuilder.append()埋点

public MethodVisitor ClassVisitorHandler(MethodVisitor mv, final String className, int access, final String name, final String desc, String signature, String[] exceptions) {
  if ((name.contains("decode") && METHOD_DESC.equals(desc)) || CLASS_NAME.equals(className) || (className.equals("java.lang.StringBuilder") && name.equals("append")&&desc.equals("(Ljava/lang/String;)Ljava/lang/StringBuilder;"))){

运行后的结果如下:

Log4j2

那么log4j2漏洞我们以同样的思路进行修改,关于漏洞细节可以阅读之前的文章log4j2 漏洞分析与思考。首先确认污点传播:

  1. Source:Request.getParameter()
  2. Sink:可以考虑lookup方法,即javax.naming.InitialContext.lookup()
  3. Propagator:传播过程一般是string的操作,为了防备,这里不额外做埋点了。

修改代码不再赘述,运行结果如下:

问题记录

1、tomcat加载agent遇到NoClassDefFoundError问题:

遇到这个问题找了好久的解决方法,最后发现跟类加载的双亲委派机制有关。

先来回顾下类生命周期:加载、链接(验证、准备、解析)、初始化、使用、卸载,其中类的加载采用的双亲委派模型。JVM 启动默认使用如下三种类型类装入器:BootstrapClassLoader启动类加载器、ExtensionClassLoader扩展类加载器和SystemClassLoader系统类加载器,具体每个类加载器加载的类库可以看下图。

我们知道双亲委派模型如下:

  1. 本加载器需要加载类时,检查该类是否被加载,未被加载先交由父加载器加载类
  2. 当父加载器不能加载时,再交给其父加载器
  3. 如果所有的父类加载器都不能加载,则由本加载器进行加载,但是本加载器也不能加载时就会出现ClassNotFoundError。

如下图,通常我们的Javaagent会被SystemClassLoader加载,tomcat有自定义的加载器。当我们的agent用到了web应用中的某个类com.r17a.xxx时,com.r17a.xxx本在tomcat自定义的加载器中加载,但是javaagent用到了该类尝试在SystemClassLoader中加载,相当于父加载器直接加载本来是由子加载器才能加载的类,就会出现ClassNotFoundError。所以我们要解决该问题有两种方法,一是让SystemClassLoader能够加载到我们需要用到的类,二是将agent中涉及到的类分离由tomcat自定义加载器中加载不被SystemClassLoader加载。本实验是通过-Xbootclasspath:/.../agent.jar解决的。

2、添加埋点逻辑的时候注意是不是接口,接口不能增强,比如java.sql.Statement.executeQuery()本身是一个接口,不能添加代码,所以对com.mysql.cj.jdbc.StatementImpl.executeQuery()即子类进行hook,算是一个小注意点。

3、遍历list的时候无意间添加了元素,但又没有报错

问题:遍历callChain时候,明明有sink元素却一直不打印,一到StringBuilder.append()就不打印之后的call信息了。

问题定位:

通过一步一步调试发现,在HTTP.leaveHttp方法中,打印信息时调用了Arrays.asList(item.getArgumentArray()),这个方法最终会调用StringBuilder.append(),已知我们在append中添加了代码逻辑及Propagator.enterPropagator()Propagator.leavePropagator(),在这两个方法中会将调用方法添加到CallChain,CallChain本身就在HTTP.leaveHttp()中在进行遍历RequestContext.getHttpReques;tContextThreadLocal().getCallChain().forEach,这个问题相当于在遍历list的时候给该list添加元素,一般采用迭代器来解决。

解决方法:

我这里是直接避免再次添加无用的StringBuilder.append()的callChain,根据条件直接在HttpRequestContext.addCallChain()x中不再添加则可解决:

public void addCallChain(CallChain callChain) {
        // 遍历之前的元素,如果有元素是enterPropagator或者leavePropagator,并且是append方法,就不添加append元素
        // 这样可以解决遍历时候还添加元素从而导致出错
        for (CallChain item: this.callChain) {
            if (item.getChainType().equals("enterPropagator") && item.getJavaMethodName().equals("append") && callChain.getJavaMethodName().equals("append"))
                return;
            if (item.getChainType().equals("leavePropagator") && item.getJavaMethodName().equals("append") && callChain.getJavaMethodName().equals("append"))
                return;
        }
        this.callChain.add(callChain);
    }

0x04 参考链接

https://www.k0rz3n.com/2019/03/01/%E7%AE%80%E5%8D%95%E7%90%86%E8%A7%A3%E6%B1%A1%E7%82%B9%E5%88%86%E6%9E%90%E6%8A%80%E6%9C%AF/
https://www.freebuf.com/sectool/290671.html
《深入理解JVM字节码》
https://blog.csdn.net/weixin_29306011/article/details/114449863
https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html
https://www.cnblogs.com/lzmrex/articles/12888619.html
https://dzone.com/refcardz/introduction-to-iast
https://www.bilibili.com/read/cv10794772/
https://blog.51cto.com/lsieun/2974297
https://cloud.tencent.com/developer/article/1650113
https://blog.csdn.net/weixin_39602737/article/details/114069325

点击收藏 | 4 关注 | 1 打赏
登录 后跟帖