Jprofile解析dump文件使用详解

  • https://www.ej-technologies.com/products/jprofiler/overview.html
  • 下载对应的系统版本即可

性能查看工具JProfiler,可用于查看java执行效率,查看线程状态,查看内存占用与内存对象,还可以分析dump日志.

  • 选择attach to a locally running jvm
  • 选择需要查看运行的jvm,双击或者点击start
  • 等待进度完成,弹出模式选择 Instrumentation模式记录所有的信息。包括方法执行次数等Sampling模式则只支持部分功能,不纪录方法调用次数等,并且更为安全由于纪录信息很多,java运行会变的比正常执行慢很多,sampling模式则不会 常规使用选择sampling模式即可,当需要调查方法执行次数才需要选择Instrumentation模式,模式切换需要重启jprofiler
  • 点击OK
  • 选择Live Momory可以查看内存中的对象和大小
  • 选择cpu views点击下图框中的按钮来纪录cpu的执行时间
  • 这时候可以在外部对需要录的jvm操作进行记录了,得出的结果可以轻松看出方法执行调用过程与消耗时间比例:
  • 根据cpu截图的信息,可以找到效率低的地方进行处理,如果是Instrumentation模式则在时间位置会显示调用次数

在Thread界面则可以实时查看线程运行状态,黄色的是wait 红色是block 绿色的是runnable蓝色是网络和I/O请求状态

选择ThreadDumps,可以录制瞬时线程的调用堆栈信息,如下图所示:

当JProfiler连接到JVM之后选择Heap Walker,选择Take snapshot图标,然后等待即可

如果内存很大,jprofiler万一参数设置的不正确打不开就需要要重新生成,内存小的时候无所谓

当JProfiler连接到JVM之后选择菜单上的Profiling->save HPROF snapshot 弹出下拉框保存即可,这时候生成的文件就可以一直保存在文件上

命令中文件名就是要保存的dump文件路径, pid就是当前jvm进程的id

在发生outofmemory的时候自动生成dump文件:

Pah后面是一个存在的可访问的路径,将改参数放入jvm启动参数可以在发生内存outofmemory的时候自动生成dump文件,但是正式环境使用的时候不要加这个参数,不然在内存快满的时候总是会生成dump而导致jvm卡半天,需要调试的时候才需要加这个参数

注意:通过WAS生成的PHD文件dump不能分析出出问题的模板,因为PHD文件不包含对象的值内容,无法根据PHD文件找到出问题的模板,所以PHD文件没有太大的参考价值

dump文件生成后,将dump压缩传输到本地,不管当前dump的后缀名是什么,直接改成*.hprof,就可以直接用jprofiler打开了

打开的过程时间可能会很长,主要是要对dump进行预处理,计算什么的,注意 这个过程不能点skip,否则就不太好定位大文件

  • 直接打开.hprof文件
  • 注意如下过程,中途可以喝一杯☕️,不要作死手滑点击了 skip!这样界面的时候下面可以开始进行操作了!

也可以使用工具栏中的“转到开始”按钮访问第一个数据集

JProfiler的内存视图部分可以提供动态的内存使用状况更新视图和显示关于内存分配状况信息的视图。所有的视图都有几个聚集层并且能够显示现有存在的对象和作为垃圾回收的对象。

  • 所有对象 All Objects显示类或在状况统计和尺码信息堆上所有对象的包。你可以标记当前值并显示差异值。
  • 记录对象 Record Objects显示类或所有已记录对象的包。你可以标记出当前值并且显示差异值。
  • 分配访问树 Allocation Call Tree显示一棵请求树或者方法、类、包或对已选择类有带注释的分配信息的J2EE组件。
  • 分配热点 Allocation Hot Spots显示一个列表,包括方法、类、包或分配已选类的J2EE组件。你可以标注当前值并且显示差异值。对于每个热点都可以显示它的跟踪记录树。
  • 类追踪器 Class Tracker类跟踪视图可以包含任意数量的图表,显示选定的类和包的实例与时间。

在视图中找到增长快速的对象类型,在memory视图中找到Concurrenthashmap—点右键—-选择“Show Selectiion In Heap Walker”,切换到HeapWarker 视图;切换前会弹出选项页面,注意一定要选择“Select recorded objects”,这样Heap Walker会在刚刚的那段记录中进行分析;否则,会分析tomcat的所有内存对象,这样既耗时又不准确;

在JProfiler的堆遍历器(Heap Walker)中,你可以对堆的状况进行快照并且可以通过选择步骤下寻找感兴趣的对象。堆遍历器有五个视图:

  • 类 Classes显示所有类和它们的实例,可以右击具体的类\”Used Selected Instance\”实现进一步跟踪。
  • 分配 Allocations为所有记录对象显示分配树和分配热点。
  • 索引 References为单个对象和“显示到垃圾回收根目录的路径”提供索引图的显示功能。还能提供合并输入视图和输出视图的功能。
  • 时间 Time显示一个对已记录对象的解决时间的柱状图。
  • 检查 Inspections显示了一个数量的操作,将分析当前对象集在某种条件下的子集,实质是一个筛选的过程。

HeapWarker 会分析内存中的所有对象,包括对象的引用、创建、大小和数量.通过切换到References页签,可以看到这个类的具体对象实例。 为了在这些内存对象中,找到泄漏的对象(应该被回收),可以在该对象上点击右键,选择“Use Selected Instances”缩小对象范围

References 可以看到该对象的的引用关系,选项显示引用的类型

  • incoming显示这个对象被谁引用
  • outcoming显示这个对象引用的其他对象

选择“Show In Graph”将引用关系使用图形方式展现;

  • 选中该对象,点击Show Paths To GC Root,会找到引用的根节点

如果还不能定位内存泄露的地方,我们可以尝试使用Allocations页签,该页签显示对象是如何创建出来的;我们可以从创建方法开始检查,检查所有用到该对象的地方,直到找到泄漏位置;

你需要在references视图和biggest视图手动添加对象到图表,它可以显示对象的传入和传出引用,能方便的找到垃圾收集器根源。

tips:在工具栏点击\”Go To Start\”可以使堆内存重新计数,也就是回到初始状态。

  

  JProfiler 提供不同的方法来记录访问树以优化性能和细节。线程或者线程组以及线程状况可以被所有的视图选择。所有的视图都可以聚集到方法、类、包或J2EE组件等不同层上。CPU视图部分包括:

  

访问树 Call Tree显示一个积累的自顶向下的树,树中包含所有在JVM中已记录的访问队列。JDBC,JMS和JNDI服务请求都被注释在请求树中。请求树可以根据Servlet和JSP对URL的不同需要进行拆分。热点 Hot Spots显示消耗时间最多的方法的列表。对每个热点都能够显示回溯树。该热点可以按照方法请求,JDBC,JMS和JNDI服务请求以及按照URL请求来进行计算。访问图 Call Graph显示一个从已选方法、类、包或J2EE组件开始的访问队列的图。方法统计 Method Statistis显示一段时间内记录的方法的调用时间细节。

  JProfiler通过对线程历史的监控判断其运行状态,并监控是否有线程阻塞产生,还能将一个线程所管理的方法以树状形式呈现。对线程剖析,JProfiler提供以下视图:

  

线程历史 Thread History显示一个与线程活动和线程状态在一起的活动时间表。线程监控 Thread Monitor显示一个列表,包括所有的活动线程以及它们目前的活动状况。线程转储 Thread Dumps显示所有线程的堆栈跟踪。

  JProfiler提供了不同的监控器视图,如下所示:

  

当前锁定图表 Current Locking Graph显示JVM中的当前锁定情况。当前监视器 Current Monitors显示当前正在等待或阻塞中的线程操作。锁定历史图表 Locking History Graph显示记录在JVM中的锁定历史。监控器历史 Monitor History显示等待或者阻塞的历史。监控器使用统计 Monitor Usage Statistics计算统计监控器监控的数据。

  观察JVM的内部状态,JProfiler提供了不同的遥感勘测视图,如下所示:

  

内存 Memory显示堆栈的使用状况和堆栈尺寸大小活动时间表。记录的对象 Recorded Objects显示一张关于活动对象与数组的图表的活动时间表。记录的生产量 Recorded Throughput显示一段时间累计的JVM生产和释放的活动时间表。垃圾回收活动 GC Activity显示一张关于垃圾回收活动的活动时间表。类 Classes显示一个与已装载类的图表的活动时间表。线程 Threads显示一个与动态线程图表的活动时间表。CPU负载 CPU Load显示一段时间中CPU的负载图表。

https://www.cnblogs.com/onmyway20xx/p/3963735.html

DCEVM支持热更新热部署的JDK实现原理分析

dcevm是Dynamic Code Evolution Virutal Machine的缩写,是一个能够动态进行代码变更的JVM,目前openjdk只能支持方法体的hotswap更新,dcevm在openjdk上做了一定的增强来实现当前openjdk不能实现的新增或修改字段、方法、修改类继承关系等更全面的更新能力。 项目的github地址是https://github.com/dcevm/dcevm

能够进行全面的代码热更新能力,能够让Java程序员开发效率更高,因为能够减少大量的Java进程关闭、代码编译、重启、等待启动的时间,提高迭代速度。除了本地的热更新,我们增加远程代码传输、编译能力后又能实现远程的热更新或热部署能力。在我们后续的文章中会陆续讲解如何实现。

今天我们看一下dcevm究竟做了哪些工作来实现这样的强大功能的,内容主要参考dcevm的几篇论文Wuerthinger11PhD Unrestricted and safe dynamic code evolution for Java

dcevm 的jdk11代码在 dcevm jdk 11代码,可以结合代码学习,关于jdk代码的编译、debug可以参考我之前的文章。

为了了解dcevm的实现细节,掌握一些hotspot jvm中和类更新相关的基本知识是必要的。

Java源代码会先被编译器编译成class文件,然后交给JVM执行。JVM会先解释执行,即由解释器按照字节码执行来执行代码,当方法的执行频率超过一定阈值后,会判断为热点代码然后通过JIT编译成本地代码执行,加快之后的执行速度。 但是由于Java语言的动态特性(classloader可以动态加载新的类),所以JIT编译时做的一些优化假设可能在代码执行时被破坏,所以需要一种回退机制,即把native代码删除重新解释执行,在JVM叫做deoptimize去优化。 为了能够优化方法中长时间运行的循环,JVM还实现了栈上替换(on stack replacement)。 ClassLoader负责类加载,当JVM要加载某个类时,会由classloader负责找到这个类的字节码,默认是从classpath查找,也可以实现其他的查找方案。

当我们编写完Java代码文件(.java结尾),在编译打包时(不论是通过maven编译打包还是javac jar),会由编译器编译成class文件。 字节码文件中包含字段、方法(方法体代码)、常量池等等信息。

下面是一个类编译后的字节码图示实例。

Test类中的get方法中的代码,是先拿到当前类实例对象(也就是this)的x字段,x字段是int类型,然后乘以2,作为方法返回值返回。

字节码由一些基本的字节码执行构成,按照操作数栈来实行。

aload_0是获取当前局部变量表的第一个(index为0)的引用,对于非static方法,编译成字节码后方法第一个参数其实是this,这时操作数栈上push上是this引用。

下一个指令是getfield,并且这个指令有操作参数,是包含字段x的类型和名称的符号引用,JVM在执行时会到常量池中去查找。会先从操作数栈中弹出栈顶的this引用,然后获取它的x字段的值并且放到操作数栈中。

下一个指令iconst_2表示在操作数栈中加入int类型的常量2。

imul指令会弹出操作数栈栈顶的两个int类型的栈元素,相乘并且把结果放入操作数栈。

最后的ireturn指令会把操作数栈栈顶的int值作为方法返回值返回,这个方法也就执行完成了。

更多的JVM字节码文件格式、字节码指令内容可以参考Java虚拟机规范或者《深入理解Java虚拟机》

在Java中一个类可以引用其他类的字段或方法,从而访问其他类的字段或调用其他类的方法, 这个引用使用的是常量池中的符号引用,在运行时会交由JVM进行引用解析解析成直接引用,也就是运行时内存中的真实引用信息,比如调用方法Class的内存引用。为了解析,就需要通过类的类名获取到对应的类的字节码数组来让JVM加载,通过类名得到对应类的字节码数组是通过classloader来实现的,不同的classloader可能有不同的查找机制、委托策略。

加载完字节码数组后,在交给JVM创建成Class对象之前,JVM会调用注册的所有ClassFileTransformer来进行处理,ClassFileTransformer可以对字节码数组进行修改,也就是常说的字节码增强,或者获得类加载的事件进行相应的处理。在热更新中,如果一个类出现变更,常常也需要对第三方框架内部数据进行处理才能使程序运行正确,比如清理第三方类库内部的缓存等,后续文章中我会详细讲解。

获取到类字节码数组后,JVM会解析class字节码数组,并且添加JVM类对象到类型层级结构中(type hierarchy),然后类的状态变成已加载(loaded)。

类loaded完成后会进入链接阶段(linking),JVM会验证class的方法的代码合法性。下一步是类初始化,JVM会确保父类已经初始化,然后执行staic代码块(静态类字段的初始值是在static代码块中执行的)。

对于每个已经加载的类,JVM在内部会维护一个class对象,这个class镀锡通过引用连接到其他的class对象来形成类层级结构。 上图展示了JVM class对象的结构。大多数的Java类都只有很少的父类型(supertype),第一个supertype通过一个字段保存,接下来8个supertype通过一个数组保存,超过8个的supertype通过一个扩展的对象数组保存。 第一个加载的subtype子类型通过一个字段引用,其他的subtype则通过一个列表保存。 并且class对象中还保存了java.class.Class实例的引用,在反射时会用到,java.class.Class对象也保存了JVM中class对下的引用。

JVM中的class对象还内置了一些额外的数组来减少额外的内存访问,拥有了这些表在调用方法时,就可以直接找到对应的方法引用,而不用再委托给supertype查找调用,从而提高了效率。

  • Virtual method table: 虚拟方法表,包含virtual method(非static、非private、非构造函数的地方方法,包括从supertypes继承的public protected方法),这个表包含了所有可以调用的virtual method和对应的实现的引用,因为supertype的每个public方法都可能被子类覆盖,比如类B override了父类A的一个方法,那么B里面的virtual method table和A是不一样的。调用virtual method方法的字节码指令是invokevirtual。
  • Interface method table: 对于当前类继承的每个接口,都包含了这个接口的实现的方法,表的entry保存了每个接口方法到接口实现方法的引用。那么既然interface方法肯定是public的,为什么有了virtual method table还需要interface method table呢?
  • Static fields: 静态字段分为两部分,一个是static的引用,static引用会在GC的时候使用到,一个是static的基本类型。static字段的值直接保存在JVM的class对象中来加速static字段的访问。
  • Instance pointer map: 保存GC使用的bitmap,保存了heap中的哪个关于当前类的heap word是当前类的instance指针,知道对象的指针的精确位置是精确GC的前提条件。

system directory保存当前JVM所有已经加载的类的class对象,当compiler、interpreter、或者字节码验证器(verifier)找到一个常量池的类名称时,它会尝试到system dictionary中查找这个class,没有找到的话JVM会触发类加载过程。

system dictionary是一个hash table,key是class的名称和classloader,value是JVM的class对象。 不同的Java类在运行时能够并行加载,所以JVM需要能够避免同一个类被多个线程同时加载出现static代码块执行两次等并发问题,这个是通过placeholder hash table实现的,在加载一个新的类前,加载线程需要检查这个类的placeholder entry是否存在,如果不存在,这个线程需要能够先put成功对应的place holder到placeholder hash table才能继续执行类加载,如果put失败,说明其他线程正在加载,类加载完成后加载线程会从placeholder hash table中删除这个entry,然后在system dictionary中写入class object的entry。在查询和修改placeholder时需要持有system dictionary的lock。

Hotspot JVM中包含两个字节码解释器的实现,一个用C++实现字节码指令,另一个是直接生成机器码模板,C++的优势是更好的可移植性,因为利用C++已有的可移植性,但是性能不一定最优。模板解释器的优势是执行更快,因为Java字节码直接转换成人工编写的机器码指令,缺点是对于每个平台需要人工编写每个字节码的机器码模板。

JVM提供了safepoint机制,在safepoint中所有的应用线程都处于挂起状态,这样JVM就能进行一些数据操作,而不受到应用线程数据变更的影响,比如GC遍历gc root、获取线程栈(jstack命令的栈信息就是在safepoint中获取的)等。deoptimization同样也是在safepoint中执行的,JVM把正在执行的编译后的方法从当前的机器码位置换到解释器的位置,这种反向的优化叫做deoptimization。deoptimization的原因是因为编译代码时的一些假设在某些时候不再符合,比如如果一个类没有子类,那么instanceof 命令就可以优化成类的比较指令,但是如果新加载了一个这个类的子类,那么这个假设就不再满足,需要deoptimization。

每个Java heap中的对象都包含对象头,对象头中包含两个heap word。 Mark word用来保存对象的所信息、identity hash code。 第二个word保存了指向对象类型的引用。

大部分的垃圾回收器都是分代收集,通过复制算法优先回收效率最高的年轻代,一个对象在年轻代存活超过一定的GC周期后会晋升到老年代,老年代常见的是标记压缩算法,标记压缩算法包含如下四个阶段。

  • Mark: JVM从gc root开始进行图遍历,标记所有存活的对象。
  • Forward Pointers: JVM开始从heap的内存起始位置开始遍历每个对象,对于每个活着的对象,计算压缩之后它的新目标内存位置。新的位置保存在对象的mark word中,如果mark word中保存了数据,则JVM会先进行备份。
  • Adjust Pointers: 调整指针,JVM需要修改各个对象的引用的地址,也就是上一步的forward pointer location,在这一步后,heap会处于不一致状态,因为pointer指针已经调整,但是对象还在原来的位置还没有移动。
  • Compact: 最后JVM会把对象从原来的位置copy到forward location,也是从heap内存的起始位置到heap内存的结束位置,然后恢复对象的mark word备份。

Hotspot VM目前支持运行时进行进行类的定义变更,有JVMTI(Java Virtual Machine Tool Interface)、JDI(Java Debug Interface)、java.lang.instrument三个接口能够实现。

在idea中通过debug运行程序后再进行类的编译后,经常会看到idea提示我们是否要进行hotswap,如果确认可以进行类的redefinition,这个就是使用的JDI接口。

JDK也提供了Java的API接口,就是java.lang.instrument.Instrumentation类中的redefineClasses方法。

Instrumentation对象可以通过premain或agentmain方法的参数获得,一个是在javaagent启动时调用,一个是在运行时attach后调用。

前面的基础知识铺垫完成,下面就是dcevm核心的class redefinition实现了。

dcevm只对GC、system dictionary、VM metadata部分进行了比较小的修改。

redefine可以批量传入多个类进行redefine,所以dcevm要对类结构变更做处理。接收到变更类后,首先要找到这些类的所有子类,并且根据类继承关系进行拓扑排序,拓扑排序的目的是让被依赖的类先更新。要找到所有子类是因为更新父类可能会影响子类的virtual method table等信息,而父类不依赖子类,所以要先更新父类。

然后新的变更类会被加载然后添加到一个side universe,type universe是JVM中的一个全局类型信息表,side universe是dcevm为了兼容新旧代码都能执行的实现,因为变更时可能还有在执行中的Java代码,还在按照旧的class hierarchy来执行代码。

接下来新变更的类会按照类加载的步骤进行verifier验证,如果加载或验证出现失败,redefinition会被拒绝并且所有的修改都会回滚。如果修改合法,在下个safepoint时,dcevm会加一个全局锁(为了防止编译和类加载的并发问题),然后遍历heap,修改所有的指向老的class的引用到新的class上。

在遍历的同时,如果对象的size增加了(新增实例字段的修改),则dcevm会进行一次自定义的full gc来调整对象的位置,来容纳新增加的字段。

JVM针对一些不一致的数据(比如常量池、JIT代码等)进行invalidate,以便这些不一致的数据能够重新计算。

最后JVM释放所有的锁并且使用新的代码版本继续执行应用程序。

修改父类可能对子类产生影响,比如父类新增一个public字段,它的所有子类也会集成这个字段;被类增加一个public方法,也会影响所有的子类的virtual method table;修改类的supertype也会影响所有子类的supertype信息。

如果因为异常(比如字节码信息非法),JVM需要能够回退到之前正常的状态。

我们需要在系统中同时保存新加的类。这样能够在类完全更新完成前能继续执行旧代码和解决循环依赖问题。

这个过程中JVM更新变更类的对象的对象头的类引用到新的类上。 并且遍历过程中,dcevm还会记录哪些对象的对象大小增加了。

dcevm目前不会重新执行类的static代码块,会把就类的static字段copy到新类上,对于新增带有默认值的static字段,hotswap-agent中提供了插件来实现。

如果类的实例字段发生了变化,则需要修改heap中这个类的每个对象的字段值。 在更新对象的之后,会从旧对象复制不变的字段到新的对象中。

java.lang.Class中保存了一个classRedefinedCount字段,当redefine之后,jvm会修改这个类的classRedefinedCount,然后应用程序再调用反射方法时,比如getMethods(),都会调用到reflectionData(),来实现读取到最新的类变更。不过如果在引用程序内对Method对象进行了缓存,Method对象的数据是不能及时变更的,需要在agent内添加数据清理逻辑。

如果类的实例对象的大小变大了,我们需要使用一个full gc来调整对象的位置好让变大的地方能够放下。这是通过一个修改过的mark-and-compact GC算法实现的。

对象大小变大意味着compact的时候,可能会移动到比当前对象内存offset更高的位置,所以为了避免数据冲突,还增加了一个side buffer来保存中间溢出的对象。

下图是计算forward pointer的算法,forwardTop指向当前已经压缩的heap的end。在遍历heap对象的时候,每遍历到一个live object,forward pointer就加上这个object的新的size,如果forward pointer加上新的size大于当前对象的end,说明可能会覆盖后面的对象,则需要把当前对象放到side buffer中临时保存起来。如果没有放到side buffer中,则会保存forwardTop到object header中。

dcevm没有修改update pointer步骤,compact步骤完成后,会把side buffer中的对象复制到当前压缩后的heap的结尾。

类更新后,还需要对JIT编译的代码(compiled code)和常量池缓存(Constant Pool Cache)进行清理。 compiled code可以通过deoptimization机制进行清理,清理之后执行一定次数再通过JIT编译。 Constant Pool Cache清理之后也会重新进行符号引用到metadata的直接引用的解析。

8K字详解Java安全之动态加载字节码

简单说,Java字节码就是.class后缀的文件,里面存放Java虚拟机执行的指令。由于Java是一门跨平台的编译型语言,所以可以适用于不同平台,不同CPU的计算机,开发者只需要将自己的代码编译一次,就可以运行在不同平台的JVM中。甚至,开发者可以用类似Scala、Kotlin这样的语言编写代码,只要你的编译器能够将代码编译成.class文件,都可以在JVM虚拟机中运行:uploading-image-878441.png

ClassLoader是一个加载器,就是用来告诉JVM虚拟机如何去加载这个类,默认的就是根据类名来加载类,这个类名需要是完整路径,比如说java.lang.Runtime

URLClassLoader 实际上是我们平时默认使用的 AppClassLoader 的父类,所以,我们解释URLClassLoader 的工作过程实际上就是在解释默认的Java类加载器的工作流程

正常情况下,Java会根据配置项 sun.boot.class.path 和 java.class.path 中列举到的基础路径(这些路径是经过处理后的 java.net.URL 类)来寻找.class文件来加载,而这个基础路径有分为三种情况:

  • URL未以斜杠 / 结尾,则认为是一个JA件,使用 JarLoader 来寻找类,即为在Jar包中寻找.class文件
  • URL以斜杠 / 结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻找.class文件
  • URL以斜杠 / 结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类

使用Http协议测试,看Java是否能从远程HTTP服务器上加载.class文件:

先编译一个.class文件放在服务器上

这里用python起一个微型服务器

注意:我这里是换了端口,且绑定了127.0.0.1,

我使用第二种是报错的,问了很多人,原因如下:第二种默认启动的服务器是IPV6地址,Java是解析不了的。这种情况在JNDI注入中也有出现。如果有懂的大佬,欢迎留言。

其实,不管是加载远程class文件,还是本地的class或jar文件,Java都经历的是下面这三个方法调用

  • loadClass 的作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机制),在前面没有找到的情况下,执行 findClass
  • findClass 的作用是根据基础URL指定的方式来加载类的字节码,就像上面说到的,可能会在本地文件系统、jar包或远程http服务器上读取字节码,然后交给 defineClass
  • defineClass 的作用是处理前面传入的字节码,将其处理成真正的Java类

所以真正核心的部分其实是 defineClass ,他决定了如何将一段字节流转变成一个Java类,Java默认的 ClassLoader#defineClass 是一个native方法,逻辑在JVM的C语言代码中

native方法称为本地方法。在java源程序中以关键字“native”声明,不提供函数体。其实现使用C/C++语言在另外的文件中编写,编写的规则遵循Java本地接口的规范(简称JNI)。简而言就是Java中声明的可调用的使用C/C++实现的方法。

例子:

里面是Hello.class的base64编码

注意:在 defineClass 被调用的时候,类对象是不会被初始化的,只有这个对象显式地调用其构造函数,初始化代码才能被执行。而且,即使我们将初始化代码放在类的static块中,在 defineClass 时也无法被直接调用到。所以,如果我们要使用 defineClass 在目标机器上执行任意代码,需要想办法调用构造函数。

因为系统的 ClassLoader#defineClass 是一个保护属性,所以我们无法直接在外部访问,不得不使用反射的形式来调用。在实际场景中,因为defineClass方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我们常用的一个攻击链 TemplatesImpl 的基石。

前面分析了defineClass方法并不好直接利用,但是Java底层还是有一些类用到了它,这就是 TemplatesImpl,com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 这个类中定义了一个内部类TransletClassLoader :

这个类里重写了 defineClass 方法,并且这里没有显式地声明其定义域。Java中默认情况下,如果一个方法没有显式声明作用域,其作用域为default。所以也就是说这里的defineClass 由其父类的protected类型变成了一个default类型的方法,可以被类外部调用。

从 TransletClassLoader#defineClass() 向前追溯一下调用链:

先看TemplatesImpl#defineTransletClasses()方法:

但它是一个private方法,还是不能直接调用,继续往上看到getTransletInstance(),

还是private方法,继续找到newTransformer()方法

是public方法了,可以直接调用,所以一条调用链就出来了

首先得设置TemplatesImpl对象的三个私有属性,这里我们用反射设置就行,三个属性:_bytecodes、 _name 和 _tfactory

  • _name:为任意字符串,只要不是null才可以进入defineTransletClasses()
  • _bytecodes:由字节码组成的数组,用来存放恶意代码,其值不能为null
  • _tfactory 需要是一个 TransformerFactoryImpl 对象,因为TemplatesImpl#defineTransletClasses() 方法里有调用_tfactory.getExternalExtensionsMap() ,如果是null会出错

另外TemplatesImpl 中对加载的字节码是有一定要求的:这个字节码对应的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类所以,我们需要构造一个特殊的类:

解释以下为什么多了两个transform方法

同样将其编译为class文件,然后base64编码最后就是写poc了,就新建一个TemplatesImpl对象,把属性设置进去然后执行newTransformer方法触发,主要是咱得先写一个利用反射给私有属性赋值的一个方法setFieldValue:

关于BCEL先看看p神的:https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html#0x01-bcel

被Apache Xalan所使用,而Apache Xalan又是Java内部对于JAXP的实现,所以BCEL也被包含在了JDK的原生库中。

BCEL属于Apache Commons项目下的一个子项目,全名应Apache Commons BCE,它提供了一系列用于分析、修改和创建Java Class文件的API,从库功能来看,使用性远不及其他库,但被Apache Xalan所使用,而Apache Xalan又是Java内部对于JAXP的实现,所以BCEL也被包含在了JDK的原生库中位com.sun.org.apache.bcel。

BCEL包中有com.sun.org.apache.bcel.internal.util.ClassLoader类,它是一个ClassLoader,但重写了Java内置的ClassLoader#LoadClass方法

在LoadClass中,会判断类名是否是$BCEL$开头,如果是的话,将会对这个字符串进行decode来看一下decode的具体算法:

可以理解为是传统字节码的16进制编码,然后将 \\ 替换为 $ ,默认还会在最外层加上 GZip 压缩

边写恶意类

然后通过BCEL提供的两个类Repository和utility来利用:

最后用BCEL ClassLoader加载这串特殊的字节码,并执行里面的代码:

代码进行简化后如下

BCEL ClassLoader类和前面的TemplatesImpl 都出自于同一个第三方库,Apache Xalan,在Fastjson等漏洞的利用链构造时都有被用到还有一个重要的利用条件就是在Java 8u251的更新中,这个ClassLoader被移除了,所以之后只能在这个之前的版本才可以利用。

本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com

点赞 0
收藏 0

文章为作者独立观点不代本网立场,未经允许不得转载。