当前位置:   article > 正文

安全左移利器——洞态iast调研(待完成)_洞态 iast

洞态 iast

如有描述不准确的地方,欢迎指正

1.服务端

1.1参考链接

https://zone.huoxian.cn/d/2334-iastfaq

1.2部署离线版本

1.2.1 拉取镜像

docker pull dongtai/dongtai-server:{version}

docker pull dongtai/dongtai-web:{version}

docker pull dongtai/dongtai-mysql:{version}

docker pull dongtai/dongtai-reids:{version}

docker pull dongtai/dongtai-logrotate:{version}

docker pull dongtai/dongtai-logstash:{version}

1.2.2 修改配置文件

打开文件 docker-compose.yml 将文件中的 {ChangeThisVersion} 替换为目标版本

1.2.3 启动

在DongTai/deploy/docker-compose/ 目录执行 docker-compose -p iast up -d

1.2.4 架构分析

在这里插入图片描述

1.2.5 部署问题解决

1.2.5.1 Listen queue size is greater than the system max net.core.somaxconn

原因:监听队列大小超出配置
解决方法:增大yml配置文件中net.core.somaxconn参数的大小

1.3 二开

远程调试
https://www.wangan.com/p/7fyg8k00f194df4b

2. agent

2.1 源码分析

Jna全称Java Native Access,是一个建立在 经典的JNI技术之上的Java开源框架。Jna提供工具用于调用c/c++动态库(如window的DLL已经Linux的so)而不需要编写任何 native/JNI代码。开发人员只要在一个Java接口中描述函数库的函数与结构,Java将自动实现Java接口方法到函数的映射。

2.1.1 dongtai-agent

引擎在安装时就会添加shutdownHook,会在jvm关闭时自动资源回收
运行的各类监控,心跳、熔断、性能、agent状态是在加载引擎时通过守护线程添加任务来执行

2.1.1.1 agent隔离 ⭐⭐⭐⭐

因为我们现在需要实现具有一定复杂逻辑的agent,所以我们不可避免的需要引入一些其他jar包做功能实现,而如果我们的agent与正常应用引入了同一个jar包的不同版本,这将发生jar包冲突,对正常应用产生未知影响。
agent分为三个jar包,一个是agent包,一个core包,还有一个spy包。agent包实现一个premain()方法、Tranformer类等,并且使用自定义类加载器加载core包,而core包是我们编写的修改字节码的逻辑。但这样还会有一个问题,我们修改后的正常代码,需要调用core包中的方法,而core包是自定义类加载器加载的,正常代码的类一般是应用类加载器加载的,所以正常代码这时候无法调用core中的方法。所以,还需要将正常代码调用的core方法单独拆分出一个jar包,该包使用BootstrapClassLoader加载,该类加载器是类加载器的顶层,其加载的类可以被所有的类加载器加载的类引用,所以还需要一个spy jar。
在这里插入图片描述
类加载器相关
https://juejin.cn/post/7021010821291442190
https://xie.infoq.cn/article/c5be9834709f7eb48cfa683b1
https://www.freebuf.com/articles/web/362262.html

2.1.1.2 启动时加载 premain模式

1.创建一个包含premain方法的java类
2.在配置文件中指定premain边界的agent类
通常会在MANIFEST.MF文件中配置Premain-Class或者在pom文件中配置
3.在启动Java程序时,通过-javaagent参数加载你的Agent JAR

2.1.1.3 运行时加载 agentmain模式

和premain模式区别在于方法名为agentmain,另外是需要替换目标java程序的进程pid

两者都需要使用Instrumentation来实现类的动态修改和增强
相关链接:
https://developer.aliyun.com/article/1132717
https://developer.aliyun.com/article/1093860

2.1.1.4 中间件识别

入口 io.dongtai.iast.agent.middlewarerecognition.ServerDetect
①如果是运行时加载agent的方式,只能通过runtimeMXBean.getSystemProperties()来判断是否属于unkonwn的服务类型
②可以通过ManagementFactory.getRuntimeMXBean()获取到当前正在运行的JVM,它返回一个 RuntimeMXBean 对象,该对象提供了有关 JVM 运行时的各种信息,包括 JVM 的名称、启动时间、输入参数等。
然后通过RuntimeMXBean对象获取JVM启动参数结合当前线程的上下文类加载器来加载中间件的类来综合判断是否能匹配到各种中间件(使用 Thread.getContextClassLoader() 的主要原因是考虑到类加载器的隔离性。在 Java 的多线程环境中,不同的线程可能由不同的类加载器加载,因此线程上下文类加载器允许线程在运行时使用不同的类加载器加载类。)
https://blog.csdn.net/lsm135/article/details/78818128
③将所有中间件相关的类放到一个数组里遍历执行匹配方法(所有的类都实现了顶级接口,主要是匹配特征,不同中间件的匹配方法不一定相同)
1.dubbo是通过加载相关的类
在这里插入图片描述
2.weblogic是通过获取特定文件
在这里插入图片描述
3.tomcat多版本判断,加了一层抽象类,抽象类是通用的判断是否tomcat方法
在这里插入图片描述
具体版本的ismatch方法也是调用抽象类的ismatch方法,通过判断bootstrap.jar是否在类路径内结合启动参数中是否包括org.apache.catalina.startup.Bootstrap start来综合识别是否为tomcat服务,再和具体版本的标识比对,添加版本信息。
这三种是比较有代表性的方式,其他中间件的识别也是基本类似

2.1.1.5 agent引擎管理

引擎管理器使用了单例对象,方便对引擎的控制和管理
①安装、启动、停止agent
判断spy core api jar包的路径文件是否存在以及是否要从服务端下载相关jar包
在安装时需要对agent相关的包做类隔离,core包用自定义的类加载器加载,拆分字节码修改后正常应用需要用到的core包方法放到spy包中,并用appendToBootstrapClassLoaderSearch方法实现加载
接下来就是通过自定义的类加载器通过反射来调用core包中的install具体方法,启动和停止也是类似通过反射来执行
②agent卸载
也是先通过反射来调用具体方法卸载,同时需要管理自定义的类加载器的Closeable接口的close方法来关闭
在transform时会将原始的字节码存储到静态变量中,在卸载时可以将字节码还原(感觉放redis更好)
在这里插入图片描述

2.1.1.6 agent指标监控

性能指标主要是使用开源项目OSHI的相关方法获取系统版本、进程、内存和 CPU 使用情况、磁盘和分区、设备、传感器等
https://github.com/oshi/oshi
①性能指标
1.CPU使用率
2.堆内存使用率
②监控器相关
各类监控器都是通过守护线程来运行
1.agent状态监控
包括是否允许报告、是否熔断、预期的状态和实际状态是否相符
2.基础配置、熔断配置监控
定时向服务端发送请求读取配置并更新
心跳监控定时获取当前的内存、cpu、磁盘信息发送到服务端
3.性能监控
先收集当前的性能指标,然后更新对应的变量, 再做检查,获取熔断管理器的实例进行熔断检查,因为需要在线程中进行通信,需要把采集到的性能指标序列化成上下文来传输,再就可以比较当前性能值和临界值来做性能是否达标的判断

2.1.1.7 熔断机制

使用resilience4j相关的断路器包,熔断管理器也用到了单例对象,一方面节约资源,另一方面便于控制
https://www.cnblogs.com/hama1993/p/12019485.html
https://www.51cto.com/article/722859.html
主要维护agent的安装和暂停,
当熔断器从关闭状态转向打开状态时,暂停agent
从其他状态转向打开状态时(一般是半开转全开),卸载agent,其他状态转换也是类似逻辑,需要在状态变更时重新安装agent
检查器是类似性能监控模块,获取实时值和配置值,超过性能时触发熔断
在这里插入图片描述
监控CPU DISK 熔断器
熔断降级模块,分别有以下可参考的项
性能指标:系统内存使用率、系统内存使用值、CPU使用率
JVM指标:JVM内存使用率、JVM内存使用值、总线程数、守护线程数、洞态IAST线程数
应用指标:单请求HOOK限流、每秒限制处理请求数量(QPS)、请求响应时间

2.1.1.8 agent注册报告及心跳报告

①注册报告
主要是将安装时agent的各类相关信息同步到服务端,包括agent名称、版本、项目名称等,同时在注册之后同步agent状态
②心跳报告
心跳监控器的主要方法,实时获取当前的机器的内存、cpu、磁盘信息发送到服务端,也会在agent状态监控器执行时将agent的相关状态发送到服务端

2.1.2 dongtai-core

2.1.2.1 获取规则

入口 com.secnium.iast.core.engines.impl.ConfigEngine#init
主要是两点,一是加载一些配置,从服务端或者是本地文件加载(优先从本地拉取),包括Source点、Propagator点、Sink点的污点规则,主要是解析node的信息,类名、方法名、参数以及source节点和propagator节点的source和target,构造成对应节点的对象,在污点数据的流向信息在转换时使用。

2.1.2.2 字节码转换

入口 com.secnium.iast.core.engines.impl.TransformEngine#start

  1. 通过Instrumentation获取所有已加载的类,遍历找出能做字节码增强的部分
  2. 筛除不能被Instrumentation修改的类、接口、特定类
    a. final类、Java虚拟机的基础类库,如java.lang.Object、java.lang.String等、java.security.SecureClassLoader 加载的类受到安全管理器的保护,不允许被修改、非 Java 语言编写的类
    b. 数组类(以[开头的,比如:int[] 的类名是:[I ;int[][] 的类名是:[[I ,数组是有对应的类,这个类是在JVM运行时创建的,所以没有对应的class 文件)
    c. aop代理类,类名中包含/$ Proxy
    d. iast agent相关的类
    e.通过CGLIB做的AOP类,类名中包含CGLIB$ $
    f.通过Lambda做的AOP类,类名中包含 $ L a m b d a Lambda Lambda
    g. 其他通过javassist做的AOP类,类名中包含_$$_jvst
    h. 将internal name转换回.class
    i. 剔除黑名单class list,包括前缀、后缀、全名匹配
  3. 获取当前类的类族
    定义一个linkedqueue,将当前类作为第一个元素,然后每次从队列中poll出一个元素,获取这个元素的继承的父类和实现的接口,再add进队列,循环操作来获取当前类的类族所有元素
    在这里插入图片描述
  4. 找到需要hook的类,在最开始获取服务端配置时,会将各个节点按类别解析source、propagator、sink节点,这些也是需要hook的类
  5. 前面这部分获取类族的异常处理在agent被动态卸载时会有问题,因为iastClassLoader继承于URLClassLoader
    当一个URLClassLoader被动态关闭之后,但JVM已经加载的类并不知情(因为没有GC),所以当尝试获取这个类更多详细信息的时候会引起关联类的ClassNotFoundException等未知的错误(取决于底层ClassLoader的实现),这里没有办法穷举出所有的异常情况,所以catch Throwable来完成异常容灾处理,当解析类出现异常的时候,直接简单粗暴的认为根本没有这个类就好了
  6. 显示调用retransformClasses方法 ⭐⭐⭐⭐
    会执行实现的transform方法,保存原始的字节码数组(为了实现对agent的一些控制,如start、stop、destroy,destroy时需要去除添加的字节码,所以在初始化时是需要储存原始的字节码,在销毁的时候可以快速还原。),在创建classreader时,填充ClassContext的基础属性如父类、访问标识、internalClassName、实现的接口、是否由BootstrapClassLoader加载,然后在创建classwriter时为了自动计算帧的大小,有时必须计算两个类共同的父类,缺省情况下,ClassWriter将会在getCommonSuperClass方法中计算这些,在加载这两个类进入虚拟机时,使用反射API来计算。但是,如果你将要生成的几个类相互之间引用,这将会带来问题,因为引用的类可能还不存在。在这种情况下,可以重写getCommonSuperClass方法来解决这个问题。通过重写 getCommonSuperClass() 方法,更正获取ClassLoader的方式,改成使用指定ClassLoader的方式进行。规避了原有代码采用Object.class.getClassLoader()的方式。实际就是将这些特殊情况的父类初始化为java/lang/Object类。
    private static String getCommonSuperClassImplByAsm(String type1, String type2, ClassLoader targetClassLoader) {
        InputStream inputStreamOfType1 = null, inputStreamOfType2 = null;
        try {
            //targetClassLoader 为null,说明是BootStrapClassLoader,不能显式引用,故使用系统类加载器间接引用
            if (null == targetClassLoader) {
                targetClassLoader = ClassLoader.getSystemClassLoader(); (通常是Application ClassLoader,它负责加载应用程序的类路径上的类和资源,例如在应用程序启动时加载的类文件和配置文件等)
            }
            if (null == targetClassLoader) {
                return "java/lang/Object";
            }
            inputStreamOfType1 = targetClassLoader.getResourceAsStream(type1 + ".class");
            if (null == inputStreamOfType1) {
                return "java/lang/Object";
            }
            inputStreamOfType2 = targetClassLoader.getResourceAsStream(type2 + ".class");
            if (null == inputStreamOfType2) {
                return "java/lang/Object";
            }
            final ClassStructure classStructureOfType1 = ClassStructureFactory.createClassStructure(inputStreamOfType1, targetClassLoader);
            final ClassStructure classStructureOfType2 = ClassStructureFactory.createClassStructure(inputStreamOfType2, targetClassLoader);
            if (classStructureOfType2.getFamilyTypeClassStructures().contains(classStructureOfType1)) {
                return type1;
            }
            if (classStructureOfType1.getFamilyTypeClassStructures().contains(classStructureOfType2)) {
                return type2;
            }
            if (classStructureOfType1.getAccess().isInterface()
                    || classStructureOfType2.getAccess().isInterface()) {
                return "java/lang/Object";
            }
            ClassStructure classStructure = classStructureOfType1;
            do {
                classStructure = classStructure.getSuperClassStructure();
                if (null == classStructure) {
                    return "java/lang/Object";
                }
            } while (!classStructureOfType2.getFamilyTypeClassStructures().contains(classStructure));
            return toInternalClassName(classStructure.getJavaClassName());
        } finally {
            IOUtils.closeQuietly(inputStreamOfType1);
            IOUtils.closeQuietly(inputStreamOfType2);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  1. 对采集到的类通过抽象类进行配置分发
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    实际就是如果匹配到对应框架类名,会进入对应的访问器(ClassVisitor的子类),不同框架需要添加的字段和visitMethod方法的实现不一样,后续维护也是扩展更多的框架。这里遍历的顺序是先匹配框架类,最后匹配通用的DispatchClassPlugin,DispatchClassPlugin中会对配置中的所有定义需要hook的类做一个lazy aop,实现配置的各类型节点的onMethodEnter、onMethodExit方法。
    在这里插入图片描述

  2. 策略域级别的维护和方法数据采集
    enterScope、leaveScope里面主要是维护节点的级别、传播深度。
    在这里插入图片描述

  3. source节点
    对于source节点的onMethodEnter方法,只是维护节点的级别、传播深度
    对于source节点的onMethodExit方法,只在第一个source节点的方法退出时调用trackMethod方法,主要是填充collectMethod方法的参数并调用,然后调用leaveScope维护节点级别和传播深度
    在这里插入图片描述

  4. collectMethod方法解析:
    在这里插入图片描述
    先判断一下当前报告最大方法池的容量,将类名、匹配到的类名、方法名、参数、返回值、signature构造成一个MethodEvent类,再根据policyNode的类型来进行对应的解析。
    对于Source节点
    a. 排除一些不需要解析的点,如返回值为空或者返回值是基础数据类型
    b. 将event标记为source点
    c. 获取堆栈hook点的应用堆栈情况填充到event中
    d. 维护invokeId、计算返回值的hash值并添加到污点池中(对于数组、迭代器、map、list等需要回调处理,对于对象类型需要拆分出字段再回调,同时设置储存值的范围 污点range)
    e. 遍历source节点的所有污点源和污点去向,给这些污点打被污染的标志,然后对event对象做一些污点数据的填充
    f. 将invokeId和event对象作为key value存储到EngineManager的TRACK_MAP中

对于Propagator节点
a. 会遍历每个污点源,如果已经在污点的hashcode的map集合中(实际就是source节点的值传递过来了),就可以把Propagator节点的event对象中的参数设置为已污染
b. 对Propagator节点的target部分也是类似操作,遍历完标记
c. 对taintRange的相关逻辑处理 ??? taintRange的作用暂时没看明白,TaintRangesBuilder中有对taintRange的命令详情
d. tag相关信息如果有也填充到返回的taintRange中
e. taintRange和tag信息都填充到event中,然后存储到TRACK_MAP

对于sink节点
在这里插入图片描述
a. 先对参数做一些静态的校验,如果发现异常的话就发送报告,填充信息到report的JSONObject中,然后丢给METHOD_REPORT_THREAD去上传到服务端
在这里插入图片描述
b. 如果污点hash池不为空,会做动态校验,相对比较复杂。
c. 遍历各种检查器,是否匹配,是否安全,fastjson相关的是检查版本,XXE相关的是各类XML解析方式的各种配置是否开启允许解析外部DTD
d. 如果匹配上是不安全的请求,再验证是否为指定的一些http请求类型,找到对应的方法,在请求头上添加traceId,和ParentKey t-parent-agent 作为跟踪符和标识符
e. 判断是危险方法后,要做多一步判断,sink方法的污点来源是否命中污点池,用于过滤未命中污点池的sink方法,避免浪费资源,设置污点源去向
f. 只有污点来源在污点池中的危险sink节点才会标记为已污染
g. 已污染的数据也会填充到event对象中,类似节点,invokeId、stacks信息也是一起封装到event中,并添加到TRACK_MAP中

2.1.2.2.1 对于service框架的适配和漏洞扫描

service框架主要是两方面的适配
a. 参数的污染判断,不同框架需要做判断的参数位置、数量不同
b. traceId 插入header ,不同框架向header插入traceId的方法不同,比如Dubbo是需要使用setAttachment,对于Feign是使用header方法

漏扫也是类似的思路
a. 通过某些漏洞的特定类来判断检测哪一类漏洞,例如用到了com.alibaba.fastjson.JSON.parseObject(java.lang.String)则检测fastjson漏洞,如果用到了javax.servlet.http.HttpServletResponse.sendRedirect(java.lang.String)之类的重定向方法,则检测重定向相关的漏洞,具体检测方法对于不同漏洞不相同但相似,即检查版本是否是安全版本或者安全配置是否开启
b. 对于一些可以通过正则来检测的硬编码漏洞,就对关键位置的参数做正则匹配即可

2.1.2.2.2 相关技术铺垫

字节码转换

  1. ASM相关技术
    https://zone.huoxian.cn/d/108-iast-java-agentaop
    https://blog.51cto.com/u_16099254/6359975
    https://nickid2018.github.io/2021/02/13/Java%20ASM%E8%AF%A6%E8%A7%A3%EF%BC%9AASM%E5%BA%93%E4%BD%BF%E7%94%A8/
    在这里插入图片描述

  2. ClassVisitor传入ClassReader的accept后会以下面顺序进行调用
    visit [ visitSource ] [ visitModule ][ visitNestHost ][ visitPermittedSubclass ][ visitOuterClass ] ( visitAnnotation | visitTypeAnnotation | visitAttribute )( visitNestMember | visitInnerClass | visitRecordComponent | visitField | visitMethod ) visitEnd
    首先访问类的信息(visit),传入的是类文件的版本(version,从V1_1到V16)、访问标志(access),类的全限定名(name),泛型签名(signature,可能为空),父类全限定名(无指定为java/lang/Object),实现接口列表(全限定名,可为空)
    之后访问注解信息(visitAnnotation),传入的是注解描述符(descriptor,这里可能包含有@Repeatable的注解类型,所以这里不是全限定名)和可见性(visible,@Retention定义的作用范围,为CLASS传入false,为RUNTIME传入true,为SOURCE不会写入类文件),该方法返回AnnotationVisitor。
    同时,访问泛型注解信息(visitTypeAnnotation),传入的是注解引用类型(typeRef,可能为TypeReference定义的几个值:CLASS_TYPE_PARAMETER <以泛型类的类型参数为目标的类型引用的类型,常量值0>,CLASS_EXTENDS <以泛型类的超类或它实现的接口之一为目标的类型引用的类型,常量值16>,CLASS_TYPE_PARAMETER_BOUND <以泛型类的类型参数的绑定为目标的类型引用的类型,常量值17>),泛型类引用路径(可为空),注解描述符和可见性,返回AnnotationVisitor。
    接着,访问字段、方法和内部类。
    字段调用visitField方法,传入访问标志,字段名,描述符,泛型签名和默认值,返回FieldVisitor。
    方法调用visitMethod方法,传入访问标志,方法名,描述符,泛型签名和异常列表(全限定名),返回MethodVisitor。
    内部类调用visitInnerClass方法,传入内部类全限定名,外部类全限定名,内部类名称(不带包路径,也就是没有“.”的名称,如果这个写错了IDE无法识别到这个类,但是不影响调用),和访问标志(这个和类声明定义的标志不同,可以有static,这样类里面就不会带有this$0)。内部类调用指的不只是类中定义了内部类,还包括引用到了其他类的内部类。
    当所有信息都访问结束,调用visitEnd。

  3. ClassWriter中各类名都是以internal name的形式传参的,如java/lang/Object

  4. 描述符的对应关系
    ](https://img-blog.csdnimg.cn/direct/f841eb5b5d9a42f7aa37d3904c4eedd6.png)

  5. 字节码介绍
    https://nickid2018.github.io/2021/04/07/Java%20ASM%E8%AF%A6%E8%A7%A3%EF%BC%9AMethodVisitor%E4%B8%8EOpcode%EF%BC%88%E4%B8%80%EF%BC%89%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C%E4%B8%8E%E8%BF%90%E7%AE%97/

  6. classfile 内容和size的计算
    class的基础属性字节,再基于field_info 和 method_info来compute field和method的字节和size

  7. 创建一个对象的过程
    在这里插入图片描述

  8. visit方法相关参数
    在这里插入图片描述

  9. opcodes 常量释义
    https://juejin.cn/post/6998397307913781256

  10. 出现NoClassDefFoundError时的处理思路
    https://ken-ljq.github.io/2018/JVM%E6%80%A7%E8%83%BD%E7%9B%91%E6%8E%A7Agent%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0-%E4%BA%8C/

  11. jna技术
    Jna全称Java Native Access,是一个建立在 经典的JNI技术之上的Java开源框架。Jna提供工具用于调用c/c++动态库(如window的DLL已经Linux的so)而不需要编写任何 native/JNI代码。开发人员只要在一个Java接口中描述函数库的函数与结构,Java将自动实现Java接口方法到函数的映射。

在以下三种情形下 ClassFileTransformer.transform() 会被执行:
新的 class 被加载。
Instrumentation.redefineClasses 显式调用。
addTransformer 第二个参数为 true 时,Instrumentation.retransformClasses 显式调用。
13. 方法的参数怎么记录和串联起来的
先通过traceId先定位请求,再通过spanId、parentId来建立调用顺序

2.1.2.3 请求重放
  1. 请求重放的逻辑基本都是类似,拆分headerstring到map中,再添加一些特殊标识到header中,判断方法类型,填充参数
  2. 还有在服务端的请求重新测试,是指在规则更新之后,将请求的整个链路重新和规则匹配,如果命中了规则则测试结果相应变化

2.1.3 dongtai-spy

将正常代码调用的core方法的顶层单独拆分出一个jar包,用BootstrapClassLoader加载

3.相关资料

美团RASP大规模研发部署实践总结 https://mp.weixin.qq.com/s/D9N09s9Ohg_u4LcMEmvSXg

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/664822
推荐阅读
相关标签
  

闽ICP备14008679号