长亭百川云 - 文章详情

Linux下内存马进阶植入技术

SilverNeedleLab

49

2024-07-13

无agent文件的条件下使用Java Instrumentation API

序:Java Instrumentation API


从Java SE 5开始,可以使用Java的Instrumentation接口来编写Agent。如果需要在目标JVM启动的同时加载Agent,可以选择实现下面的方法:

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

JVM将首先寻找[1],如果没有发现[1],再寻找[2]。如果希望在目标JVM运行时加载Agent,则需要实现下面的方法:

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

我们这里只讨论运行时加载的情况。Agent需要打包成一个jar包,在ManiFest属性中指定“Premain-Class”或者“Agent-Class”:

Premain-Class: class

生成agent.jar之后,可以通过com.sun.tools.attach.VirtualMachine的loadAgent方法加载:

private void attachAgentToTargetJVM() throws Exception {

以上代码可以用反射实现,使用Java agent这种方式可以修改已有方法,java.lang.instrument.Instrumentation提供了如下方法:

public interface Instrumentation {

可以使用redefineClasses方法完成对类方法的修改,结合javassist可以说是非常方便:

public static void agentmain(String args, Instrumentation inst) throws Exception {

能否直接构造Instrumentation对象?

使用Java Instrumentation API的一个前提条件就是必须提供agent.jar,这是一个必须要放在硬盘上的文件。要解决这个问题,需要先识别问题的关键点:前面所有的编译生成agent.jar、loadagent加载最后都是为了产生Instrumentation对象,通过这个对象提供的redefineClasses方法,只需要提供字节码就可以完成类修改。java.lang.instrument.Instrumentation只是一个接口,它的实现类是:

/**

该类java.sun.instrument.InstrumentationImpl的构造函数私有,但使用反射仍然可以调用。重点关注这个参数nativeAgent,这是一个native的指针,那么如果我们能提供这个指针,就可以不通过加载agent文件的方式实现修改类代码。

如何获得nativeAgent指针?

继续翻看Hotspot代码

public class InstrumentationImpl implements Instrumentation {

可以看到mNativeAgent变量经由redefineClasses0函数,经JNI方法传递到了native层代码。

/*

可以看到这个agent指针的结构类型为JPLISAgent,它的定义如下:

struct _JPLISAgent {

redefineClasses的第一行代码是jvmtiEnv*   jvmtienv                        = jvmti(agent), 这个jvmti是个宏:

#define jvmti(a) a->mNormalEnvironment.mJVMTIEnv

在Java SE 5以前,就支持通过C/C++语言实现JVMTI agent,Java Instrumentation API的底层就是通过这种方式实现的。开发agent时,需要包含位于JDK include目录下的jvmti.h,这里面定义了使用JVMTI所用到的函数、事件、数据类型和常量,最后agent会被编译成一个动态库。JVMTI的函数调用与JNI相似,可以通过一个接口指针来访问JVMTI的函数。JVMTI的接口指针称为环境指针(environment pointer),环境指针是指向执行环境的指针,其类型为jvmtiEnv*。

jvmtiEnv *jvmti;

jvmtiEnv也同样提供了RedefineClasses函数,Java Instrumentation API同样功能就是封装于此之上。

jvmtiError RedefineClasses(jint class_count,

那么问题进一步的变为:怎样得到jvmtiEnv指针。

JPLISAgent实例是如何创建的?

继续查看Hotspot代码

/*

agent实例是通过native函数createNewJPLISAgent创建的,该函数是内部函数,没有从动态库中导出,Java层也没办法直接调用。那么思路还得回到jvmtiEnv指针上去。

*agent_ptr = NULL;

从以上代码我们可知,jvmtiEnv可以通过JavaVM对象获得。而关于JavaVM对象,在JDK的jni.h中,有定义导出方法:

_JNI_IMPORT_OR_EXPORT_ jint JNICALL JNI_GetCreatedJavaVMs(JavaVM **, jsize, jsize *);

该方法由libjvm.so中导出,即使so经过strip,符号也一定是存在的。因此我们可以通过此API获得JavaVM对象,通过JavaVM对象就能获得jvmtiEnv指针。

伪造JPLISAgent实例

JPLISAgent结构中虽然有很多成员,但分析Instrumentation对象中我们需要使用的redefineClasses等方法的native实现

public class InstrumentationImpl implements Instrumentation {

它们都只是从agent中获取jvmtiEnv指针,之后都没有再使用agent的其他成员

jobjectArray

那么我们只需要使用unsafe方法,申请一段内存,并在对应的偏移上放置jvmtiEnv指针值,就完成了JPLISAgent实例的构造。关键问题还是要解决获取jvmtiEnv指针。

如何在Java层调用native接口?

获取jvmtienv指针,可以采用暴力搜索内存的方式,但是这种方法很难做到通用。jvmtienv实例中有固定不变的4字节魔术字0x71EE,this指针就是jvmtiEnv指针。

//JVMTI_MAGIC    = 0x71EE,

稳定的办法就是上文分析的,通过JavaVM对象来获取。

struct JavaVM_ {

JavaVM对象其实也只是一个函数指针数组,不存在固定不变的魔术字。如果要通过JNI_GetCreatedJavaVMs方法获得,在Java层怎么调用它呢?
Java层想要调用native方法,常规做法是通过JNI,这种办法仍然需要提供一个so文件,然后通过dlopen的方式加载,这显然与本文初衷不符。不通过JNI能不能做到?至少在Linux是能做到的。
参考如下代码

#include <fstream>

编译执行后,得到结果

root@ecs-16:~# ./proc_mem_poc 

以上代码示例说明,Linux下进程可以通过/proc/self/mem修改自身内存,即使是只读内存也可以修改。示例代码修改了getchar函数的开头为int3,结果真的执行了。
使用Java代码读写/proc/self/mem是完全没问题的,而Java原生就有很多JNI的native方法,比如libjava.so中的

Java_java_lang_ClassLoader_registerNatives等等很多。
如果先修改Java_java_lang_ClassLoader_registerNatives的代码为我想要的,然后再主动调用ClassLoader.registerNatives,就实现了native层的任意代码执行。然后再还原代码,一切好像从未发生过!
那么关键问题就变为:如何获取

Java_java_lang_ClassLoader_registerNatives地址

Java查找ELF导出符号

再次得益于LINUX下的/proc文件系统,我们可以从/proc/self/maps轻易的获取所有已加载ELF对象的基址及文件路径

7fcbb8c0d000-7fcbb9a95000 r-xp 00000000 fc:01 1179725                    /CloudResetPwdUpdateAgent/depend/jre1.8.0_232/lib/amd64/server/libjvm.so

那么获取导出符号就变得非常简单,直接打开ELF文件解析得到对应符号地址,然后再加上库基址即可。对于x64 ELF的实例代码如下:

static long find_symbol(String elfpath, String sym, long libbase) throws IOException{

最后的步骤

为了能从native层得到返回值到java层,我们需要找一个返回值为long的native方法,把shellcode植入到它的开头。

void * shellcode()

转换为shellcode

movabs  rax, _JNI_GetCreatedJavaVMs

后来我选择了libjava.so中的Java_java_io_RandomAccessFile_length。使用unsafe申请一段内存,并在偏移8(x64下指针长度为8)的位置上放置jvmtienv指针

long JPLISAgent = unsafe.allocateMemory(0x1000);

再通过反射最终得到InstrumentationImpl对象

try {

需要注意的是,在Java11中sun.instrument包已不再可引用。这里已经可以获取所有加载的类。

意外

在正确查找得到jvmtienv指针之后,执行redefineClasses会报异常

Java_java_io_RandomAccessFile_length 0x7fb29c485e40

使用调试工具跟踪,在函数redefineClasses中会调用

void

这个(*jvmtienv)->RedefineClasses调用,暂时没找到源码,在IDA中逆向的结果如下

__int64 __fastcall jvmti_RedefineClasses(JvmtiEnvBase *this, JavaThread *a2, __int64 a3)

因此需要用unsafe设置一下

unsafe.putByte(native_jvmtienv + 361, (byte) 2);

测试

修改java.io.RandomAccessFile的getFD方法,插入打印语句

public static void main(String[] args) {

从1.txt文件夹里读取类的字节码

try {

正确输出结果

Java_java_io_RandomAccessFile_length 0x7fd720689e40

完整代码请参考:https://github.com/bigBestWay/ice

结语

要在不提供agent文件的条件下完成Java Instrument,有如下步骤:

  1. 解析ELF,得到Java_java_io_RandomAccessFile_length和JNI_GetCreatedJavaVMs

  2. 生成利用JNI_GetCreatedJavaVMs获取jvmtienv指针的shellcode

  3. 在Java_java_io_RandomAccessFile_length放置shellcode并调用

  4. 恢复Java_java_io_RandomAccessFile_length代码

  5. 利用unsafe伪造agent实例

  6. 利用反射实例化sun.instrument.InstrumentationImpl

  7. 使用此对象修改类

参考

rebeyond 《Java内存攻击技术漫谈》
https://mp.weixin.qq.com/s/JIjBjULjFnKDjEhzVAtxhw

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2