前言
随着RASP技术的发展,普通webshell已经很难有用武之地,甚至是各种内存马也逐渐捉襟见肘。秉承着《JSP Webshell那些事——攻击篇(上)》中向下走的思路,存不存在一种在Java代码中执行机器码的方法呢?答案是肯定的,常见的注入方式有JNI、JNA和利用JDK自带的Native方法等,其中笔者还找到了一种鲜有文章介绍的,基于HotSpot虚拟机,且较为通用的注入方法。
基于JNI
Java底层虽然是C/C++实现的,但不能直接执行C/C++代码。若想要执行C/C++的代码,一般得通过JNI,即Java本地调用(Java Native Interface),加载JNI链接库,调用Native方法实现。
Cobalt Strike官网博客上有一篇《如何从Java注入shellcode》的文章,便是基于JNI实现,通过Native方法调用C/C++代码将shellcode注入到内存中。
1 | //C/C++代码中声明的函数对应Demo#inject本地方法 |
1 | //执行注入shellcode的代码 |
这种方法需要自行编写个链接库,并上传到受害服务器上,利用起来并不显得优雅。
还有另一种方法是利用JNA第三方库,可以直接调用内核的函数,实现Shellcode注入。在@yzddmr6师傅的Java-Shellcode-Loader项目中有实现,但JNA本质上还是基于JNI,使用时还是要加载JNA自己的链接库,并且JDK中默认不包含JNA这个类库,使用时需要想办法引入。
基于JDK自带的Native方法
第一个介绍的可能是冰蝎的作者@rebeyond师傅首先发现的方法,一种基于JDK自带的Native方法的shellcode注入,严格来说是基于HotSpot虚拟机的JDK的自带Native方法。它是sun/tools/attach/VirtualMachineImpl#enqueue
Native方法,存在于用于attach Java进程的tools.jar
包中。
当运行在Windows上时,相应的enqueue
Native方法实现在/src/jdk.attach/windows/native/libattach/VirtualMachineImpl.c中,其中Create thread in target process to execute code的操作,不能说跟前面Cobalt Strike注入shellcode的操作毫不相干,只能说是一模一样。
1 | JNIEXPORT void JNICALL Java_sun_tools_attach_VirtualMachineImpl_enqueue |
当然你不能说这个是bug,只能说是feature。
相应的Demo是比较简单,在stub
参数中传入shellcode即可,@rebeyond师傅已经给出了代码,笔者在这里做了点简化。不过实现Native方法的链接库attach.dll
默认存在,但tools.jar
这个包不一定存在,@rebeyond师傅巧妙的利用了双亲委派机制,当JVM中没有加载VirtualMachineImpl
类时,就会使用下面base64编码的类替代。当然这种方法仅适用于Windows,因为Linux下enqueue
并不是这么实现的。
1 | import java.io.ByteArrayOutputStream; |
1 | package sun.tools.attach; |
基于oop偏移
这种是基于@Ryan Wincey和@xxDark两位前辈的总结,基本原理是:多次调用某个方法,使其成为热点代码触发即时编译,然后通过oop的数据结构偏移计算出JIT地址,最后使用Unsafe写内存的功能,将shellcode写入到JIT地址。其中涉及Unsafe、Oop-Klass模型和即时编译这三个前置知识。
Unsafe类
Unsafe
类是java中非常特别的一个类,提供的操作可以直接读写内存、获得地址偏移值、锁定或释放线程。Unsafe
只有一个私有的构造方法,但在类加载时候在静态代码中会实例化一个Unsafe
对象,赋值给Unsafe
类的静态常量Unsafe
属性,我们反射获取到这个Unsafe
属性即可。
1 | Field field = Unsafe.class.getDeclaredField("theUnsafe"); |
Unsafe
读写内存的相关方法有getObject
、getAddress
、getInt
、getLong
和putByte
等。
Oop-Klass模型
HotSpot JVM 底层都是 C/C++ 实现的,Java 对象在JVM的表示模型叫做“OOP-Klass”模型,包括两部分:
OOP,即 Ordinary Object Point,普通对象指针,用来描述对象实例信息。
Klass,用来描述 Java 类,包含了元数据和方法信息等。
在Java程序运行过程中,每创建一个新的对象,在JVM内部就会相应地创建一个对应类型的OOP对象。Java类是对象,Java方法也是对象,而Java类加载完成时在JVM中的最终产物就是InstanceKlass,其中包含方法信息、字段信息等一切java 类所定义的一切元素。
即时编译(JIT)
为了优化Java的性能 ,JVM在解释器之外引入了即时(Just In Time)编译器:当程序运行时,解释器首先发挥作用,代码可以直接执行;当方法或者代码块在一段时间内的调用次数超过了JVM设定的阈值时,这些字节码就会被编译成机器码,存入codeCache中。在下次执行时,再遇到这段代码,就会从codeCache中读取机器码,直接执行,以此来提升程序运行的性能。整体的执行过程大致如下图所示:
Openjdk和Oracle JDK在默认mixed模式下会启动即时编译,即时编译的触发阈值在客户端编译器和服务端编译器上默认值分别为1500和10000。
原理分析
在JVM的本体:jvm.dll和libjvm.so中,存在这一个VMStructs的类,存储了JVM中包括oop、klass、constantPool在内的数据结构和他的属性。其中有使用JNIEXPORT
标记的VMStructs
、VMTypes
、IntConstants
和LongConstants
的入口、名称、地址等偏移的变量,借助ClassLoader
的内部类NativeLibrary
的find
或findEntry
Native方法(与JDK的版本有关),可获取到这些变量的值。
然后通过InstanceKlass
、Array<Method*>
、Method
、ConstMethod
、ConstantPool
、Symbol
这些oop数据结构中的变量偏移计算出JIT的地址。
我们要计算出的目标JIT地址是目标方法的JIT地址,这需要目标方法经多次调用触发即时编译,并自动赋值_from_compiled_entry
结构成员,然后对比方法名和Signature,从目标类众多默认方法中过滤出目标方法来,再通过Method
加上_from_compiled_entry
偏移计算出来。(这里的Signature即形如()V
、(Ljava/lang/String;)V
、()Ljava/lang/String;
的方法签名)
上图没有提到InstanceKlass
的获取,其实只要通过Target.class
获取到目标类的类实例,再用Unsafe读取类实例加上java_lang_Class
的klass
偏移即可。
JVM的JIT在内存中是一个可读可写可执行的区域,最后使用Unsafe
的putByte
方法写入shellcode,再调用目标方法即可执行。这里要注意的是,如果使用没有恢复现场,即破坏了原有栈帧的shellcode,会导致JVM奔溃,切勿在生产环境上测试。
以上的Demo代码可以@xxDark的 JavaShellcodeInjector项目中浏览。
部分问题修复及改进
在32位的JDK跑Demo,JRE会抛出个异常,调试发现从目标类实例获取InstanceKlass
的偏移:klassOffset,从内存取到的值是0,使得获取到的klass
不正确,导致Unsafe读取了一个异常的地址。
问题的原因目前还不得而知,但通过HSDB找到java.lang.Class
的InstanceKlass
就可以看到klass
的偏移,后续其他自动获取的偏移也没有出现异常。
上面自动化地计算偏移,要加载JVM的链接库,还要获取一堆JVM里的数据结构、记录一堆oop和常量池的值,这要是想将POC写成一个文件着实有点不方便啊。那有没有一种简单粗暴的方法呢?
答案是肯定的。笔者刚好装有多个版本的JDK,发现JDK大版本和位数相同的时候,上面那些偏移是不变的。翻看JDK的源码不难发现,这些offset归根结底是由offset_of
宏得出,一个与C语言offsetof
作用相同的宏,结果是一个结构成员相对于结构开头的字节偏移量。
而通过之前查阅的资料得知,不同JDK大版本之间的oop数据结构才存在差异,我们只要记录下这些相同架构和大版本的偏移,就能直接计算出JIT的地址,可以免去加载JVM链接库和收集、存储JVM里数据结构的操作。
以下是笔者收集的部分LTS版本JDK的oop相关偏移:
1 | // JDK8 x32 |
后记
笔者在JDK7也曾尝试注入shellcode,但最后还是以失败告终,不仅是因为JDK7到JDK8的oop数据结构发生了很大的变化,而且JDK7中的类示例中并没有InstanceKlass
结构成员,但java_lang_CLass
中又确确实实存在_klass_offset
这个结构成员,这点就比较奇怪。
翻看官方工具HSDB源码,发现它是通过BasicHashtable<mtInternal>
的_buckets
结构成员获取所有InstanceKlass
的。由于JDK7上POC的oop数据结构需要改动较多,且还不知道BasicHashtable<mtInternal>
要怎么获取,所以JDK7下的POC还未实现。
最后两个的shellcode注入方法基于Oracle JDK和Openjdk的默认JVM:HotSpot,其他一些的JVM的实现方法就要静待各位师傅发掘。
文中若有错误的地方,望各位师傅不吝斧正。
参考
https://www.slideshare.net/RyanWincey/java-shellcodeoffice
https://qiankunli.github.io/2014/10/27/jvm_classloader.html
https://www.sczyh30.com/posts/Java/jvm-klass-oop/
https://jishuin.proginn.com/p/763bfbd58ef3
https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html