赞
踩
如有描述不准确的地方,欢迎指正
https://zone.huoxian.cn/d/2334-iastfaq
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}
打开文件 docker-compose.yml 将文件中的 {ChangeThisVersion} 替换为目标版本
在DongTai/deploy/docker-compose/ 目录执行 docker-compose -p iast up -d
原因:监听队列大小超出配置
解决方法:增大yml配置文件中net.core.somaxconn参数的大小
远程调试
https://www.wangan.com/p/7fyg8k00f194df4b
Jna全称Java Native Access,是一个建立在 经典的JNI技术之上的Java开源框架。Jna提供工具用于调用c/c++动态库(如window的DLL已经Linux的so)而不需要编写任何 native/JNI代码。开发人员只要在一个Java接口中描述函数库的函数与结构,Java将自动实现Java接口方法到函数的映射。
引擎在安装时就会添加shutdownHook,会在jvm关闭时自动资源回收
运行的各类监控,心跳、熔断、性能、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
1.创建一个包含premain方法的java类
2.在配置文件中指定premain边界的agent类
通常会在MANIFEST.MF文件中配置Premain-Class或者在pom文件中配置
3.在启动Java程序时,通过-javaagent参数加载你的Agent JAR
和premain模式区别在于方法名为agentmain,另外是需要替换目标java程序的进程pid
两者都需要使用Instrumentation来实现类的动态修改和增强
相关链接:
https://developer.aliyun.com/article/1132717
https://developer.aliyun.com/article/1093860
入口 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服务,再和具体版本的标识比对,添加版本信息。
这三种是比较有代表性的方式,其他中间件的识别也是基本类似
引擎管理器使用了单例对象,方便对引擎的控制和管理
①安装、启动、停止agent
判断spy core api jar包的路径文件是否存在以及是否要从服务端下载相关jar包
在安装时需要对agent相关的包做类隔离,core包用自定义的类加载器加载,拆分字节码修改后正常应用需要用到的core包方法放到spy包中,并用appendToBootstrapClassLoaderSearch方法实现加载
接下来就是通过自定义的类加载器通过反射来调用core包中的install具体方法,启动和停止也是类似通过反射来执行
②agent卸载
也是先通过反射来调用具体方法卸载,同时需要管理自定义的类加载器的Closeable接口的close方法来关闭
在transform时会将原始的字节码存储到静态变量中,在卸载时可以将字节码还原(感觉放redis更好)
性能指标主要是使用开源项目OSHI的相关方法获取系统版本、进程、内存和 CPU 使用情况、磁盘和分区、设备、传感器等
https://github.com/oshi/oshi
①性能指标
1.CPU使用率
2.堆内存使用率
②监控器相关
各类监控器都是通过守护线程来运行
1.agent状态监控
包括是否允许报告、是否熔断、预期的状态和实际状态是否相符
2.基础配置、熔断配置监控
定时向服务端发送请求读取配置并更新
心跳监控定时获取当前的内存、cpu、磁盘信息发送到服务端
3.性能监控
先收集当前的性能指标,然后更新对应的变量, 再做检查,获取熔断管理器的实例进行熔断检查,因为需要在线程中进行通信,需要把采集到的性能指标序列化成上下文来传输,再就可以比较当前性能值和临界值来做性能是否达标的判断
使用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)、请求响应时间
①注册报告
主要是将安装时agent的各类相关信息同步到服务端,包括agent名称、版本、项目名称等,同时在注册之后同步agent状态
②心跳报告
心跳监控器的主要方法,实时获取当前的机器的内存、cpu、磁盘信息发送到服务端,也会在agent状态监控器执行时将agent的相关状态发送到服务端
入口 com.secnium.iast.core.engines.impl.ConfigEngine#init
主要是两点,一是加载一些配置,从服务端或者是本地文件加载(优先从本地拉取),包括Source点、Propagator点、Sink点的污点规则,主要是解析node的信息,类名、方法名、参数以及source节点和propagator节点的source和target,构造成对应节点的对象,在污点数据的流向信息在转换时使用。
入口 com.secnium.iast.core.engines.impl.TransformEngine#start
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); } }
对采集到的类通过抽象类进行配置分发
实际就是如果匹配到对应框架类名,会进入对应的访问器(ClassVisitor的子类),不同框架需要添加的字段和visitMethod方法的实现不一样,后续维护也是扩展更多的框架。这里遍历的顺序是先匹配框架类,最后匹配通用的DispatchClassPlugin,DispatchClassPlugin中会对配置中的所有定义需要hook的类做一个lazy aop,实现配置的各类型节点的onMethodEnter、onMethodExit方法。
策略域级别的维护和方法数据采集
enterScope、leaveScope里面主要是维护节点的级别、传播深度。
source节点
对于source节点的onMethodEnter方法,只是维护节点的级别、传播深度
对于source节点的onMethodExit方法,只在第一个source节点的方法退出时调用trackMethod方法,主要是填充collectMethod方法的参数并调用,然后调用leaveScope维护节点级别和传播深度
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中
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. 对于一些可以通过正则来检测的硬编码漏洞,就对关键位置的参数做正则匹配即可
字节码转换
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/
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。
ClassWriter中各类名都是以internal name的形式传参的,如java/lang/Object
描述符的对应关系
字节码介绍
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/
classfile 内容和size的计算
class的基础属性字节,再基于field_info 和 method_info来compute field和method的字节和size
创建一个对象的过程
visit方法相关参数
opcodes 常量释义
https://juejin.cn/post/6998397307913781256
出现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/
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来建立调用顺序
将正常代码调用的core方法的顶层单独拆分出一个jar包,用BootstrapClassLoader加载
美团RASP大规模研发部署实践总结 https://mp.weixin.qq.com/s/D9N09s9Ohg_u4LcMEmvSXg
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。