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

序:Java Instrumentation API

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

[1] public static void premain(String agentArgs, Instrumentation inst);
[2] public static void premain(String agentArgs);

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

[1] public static void agentmain(String agentArgs, Instrumentation inst);
[2] public static void agentmain(String agentArgs);

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

Premain-Class: class
Agent-Class: class

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

private void attachAgentToTargetJVM() throws Exception {
    List<VirtualMachineDescriptor> virtualMachineDescriptors = VirtualMachine.list();
    VirtualMachineDescriptor targetVM = null;
    for (VirtualMachineDescriptor descriptor : virtualMachineDescriptors) {
        if (descriptor.id().equals(configure.getPid())) {
            targetVM = descriptor;
            break;
        }
    }
    if (targetVM == null) {
        throw new IllegalArgumentException("could not find the target jvm by process id:" + configure.getPid());
    }
    VirtualMachine virtualMachine = null;
    try {
        virtualMachine = VirtualMachine.attach(targetVM);
        virtualMachine.loadAgent("{agent}", "{params}");
    } catch (Exception e) {
        if (virtualMachine != null) {
            virtualMachine.detach();
        }
    }
}

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

public interface Instrumentation {
    /**
     * 加入一个转换器Transformer,之后的所有的类加载都会被Transformer拦截。
     * ClassFileTransformer类是一个接口,使用时需要实现它,该类只有一个方法,该方法传递类的信息,返回值是转换后的类的字节码文件。
     */
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);    

    /**
     * 对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。
     * 该方法可以修改方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名
     */
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

   /**
   *此方法用于替换类的定义,而不引用现有的类文件字节,就像从源代码重新编译以进行修复和继续调试时所做的那样。
   *在要转换现有类文件字节的地方(例如在字节码插装中),应该使用retransformClasses。
   *该方法可以修改方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名
   */
    void redefineClasses(ClassDefinition... definitions)throws  ClassNotFoundException, UnmodifiableClassException;

    /**
     * 获取一个对象的大小
     */
    long getObjectSize(Object objectToSize);

    /**
     * 将一个jar加入到bootstrap classloader的 classpath里
     */
    void appendToBootstrapClassLoaderSearch(JarFile jarfile);

    /**
     * 获取当前被JVM加载的所有类对象
     */
    Class[] getAllLoadedClasses();
}

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

public static void agentmain(String args, Instrumentation inst) throws Exception {
    Class[] loadedClasses = inst.getAllLoadedClasses();
    for (int i = 0; i < loadedClasses.length; ++i) {
        Class clazz = loadedClasses[i];
        if (clazz.getName().equals("com.huawei.xxxx")) {
            try {
                ClassPool classPool = ClassPool.getDefault();
                CtClass ctClass = classPool.get(clazz.getName());
                ctClass.stopPruning(true);

                // javaassist freezes methods if their bytecode is saved
                // defrost so we can still make changes.
                if (ctClass.isFrozen()) {
                    ctClass.defrost();
                }

                CtMethod method; // populate this from ctClass however you wish

                method.insertBefore("{ System.out.println(\"Wheeeeee!\"); }");
                byte[] bytecode = ctClass.toBytecode();

                ClassDefinition definition = new ClassDefinition(Class.forName(clazz.getName()), bytecode);
                inst.redefineClasses(definition);
            } catch (Exception var9) {
                var9.printStackTrace();
            }
        }
    }
}

能否直接构造Instrumentation对象?

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

/**
 * The Java side of the JPLIS implementation. Works in concert with a native JVMTI agent
 * to implement the JPLIS API set. Provides both the Java API implementation of
 * the Instrumentation interface and utility Java routines to support the native code.
 * Keeps a pointer to the native data structure in a scalar field to allow native
 * processing behind native methods.
 */
public class InstrumentationImpl implements Instrumentation {
    private final     TransformerManager      mTransformerManager;
    private           TransformerManager      mRetransfomableTransformerManager;
    // needs to store a native pointer, so use 64 bits
    private final     long                    mNativeAgent;
    private final     boolean                 mEnvironmentSupportsRedefineClasses;
    private volatile  boolean                 mEnvironmentSupportsRetransformClassesKnown;
    private volatile  boolean                 mEnvironmentSupportsRetransformClasses;
    private final     boolean                 mEnvironmentSupportsNativeMethodPrefix;

    private
    InstrumentationImpl(long    nativeAgent,
                        boolean environmentSupportsRedefineClasses,
                        boolean environmentSupportsNativeMethodPrefix) {
        mTransformerManager                    = new TransformerManager(false);
        mRetransfomableTransformerManager      = null;
        mNativeAgent                           = nativeAgent;
        mEnvironmentSupportsRedefineClasses    = environmentSupportsRedefineClasses;
        mEnvironmentSupportsRetransformClassesKnown = false; // false = need to ask
        mEnvironmentSupportsRetransformClasses = false;      // don't know yet
        mEnvironmentSupportsNativeMethodPrefix = environmentSupportsNativeMethodPrefix;
    }

    ...

    public void
    redefineClasses(ClassDefinition...  definitions)
            throws  ClassNotFoundException {
        if (!isRedefineClassesSupported()) {
            throw new UnsupportedOperationException("redefineClasses is not supported in this environment");
        }
        if (definitions == null) {
            throw new NullPointerException("null passed as 'definitions' in redefineClasses");
        }
        for (int i = 0; i < definitions.length; ++i) {
            if (definitions[i] == null) {
                throw new NullPointerException("element of 'definitions' is null in redefineClasses");
            }
        }
        if (definitions.length == 0) {
            return; // short-circuit if there are no changes requested
        }

        redefineClasses0(mNativeAgent, definitions);
    }

    private native void
    redefineClasses0(long nativeAgent, ClassDefinition[]  definitions)
        throws  ClassNotFoundException;

    ...

}

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

如何获得nativeAgent指针?

继续翻看Hotspot代码

public class InstrumentationImpl implements Instrumentation {
    ...

    public void
    redefineClasses(ClassDefinition...  definitions)
            throws  ClassNotFoundException {
        if (!isRedefineClassesSupported()) {
            throw new UnsupportedOperationException("redefineClasses is not supported in this environment");
        }
        if (definitions == null) {
            throw new NullPointerException("null passed as 'definitions' in redefineClasses");
        }
        for (int i = 0; i < definitions.length; ++i) {
            if (definitions[i] == null) {
                throw new NullPointerException("element of 'definitions' is null in redefineClasses");
            }
        }
        if (definitions.length == 0) {
            return; // short-circuit if there are no changes requested
        }

        redefineClasses0(mNativeAgent, definitions);
    }

    private native void
    redefineClasses0(long nativeAgent, ClassDefinition[]  definitions)
        throws  ClassNotFoundException;

    ...
}

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

/*
 * Class:     sun_instrument_InstrumentationImpl
 * Method:    redefineClasses0
 * Signature: ([Ljava/lang/instrument/ClassDefinition;)V
 */
JNIEXPORT void JNICALL Java_sun_instrument_InstrumentationImpl_redefineClasses0
  (JNIEnv * jnienv, jobject implThis, jlong agent, jobjectArray classDefinitions) {
    redefineClasses(jnienv, (JPLISAgent*)(intptr_t)agent, classDefinitions);
}

/*
 *  Java code must not call this with a null list or a zero-length list.
 */
void
redefineClasses(JNIEnv * jnienv, JPLISAgent * agent, jobjectArray classDefinitions) {
    jvmtiEnv*   jvmtienv                        = jvmti(agent);
    jboolean    errorOccurred                   = JNI_FALSE;
    jclass      classDefClass                   = NULL;
    jmethodID   getDefinitionClassMethodID      = NULL;
    jmethodID   getDefinitionClassFileMethodID  = NULL;
    jvmtiClassDefinition* classDefs             = NULL;
    jbyteArray* targetFiles                     = NULL;
    jsize       numDefs                         = 0;

    jplis_assert(classDefinitions != NULL);

    numDefs = (*jnienv)->GetArrayLength(jnienv, classDefinitions);
    errorOccurred = checkForThrowable(jnienv);
    jplis_assert(!errorOccurred);

    if (!errorOccurred) {
        jplis_assert(numDefs > 0);
        /* get method IDs for methods to call on class definitions */
        classDefClass = (*jnienv)->FindClass(jnienv, "java/lang/instrument/ClassDefinition");
        errorOccurred = checkForThrowable(jnienv);
        jplis_assert(!errorOccurred);
    }

    ...
}

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

struct _JPLISAgent {
    JavaVM *                mJVM;                   /* handle to the JVM */
    JPLISEnvironment        mNormalEnvironment;     /* for every thing but retransform stuff */
    JPLISEnvironment        mRetransformEnvironment;/* for retransform stuff only */
    jobject                 mInstrumentationImpl;   /* handle to the Instrumentation instance */
    jmethodID               mPremainCaller;         /* method on the InstrumentationImpl that does the premain stuff (cached to save lots of lookups) */
    jmethodID               mAgentmainCaller;       /* method on the InstrumentationImpl for agents loaded via attach mechanism */
    jmethodID               mTransform;             /* method on the InstrumentationImpl that does the class file transform */
    jboolean                mRedefineAvailable;     /* cached answer to "does this agent support redefine" */
    jboolean                mRedefineAdded;         /* indicates if can_redefine_classes capability has been added */
    jboolean                mNativeMethodPrefixAvailable; /* cached answer to "does this agent support prefixing" */
    jboolean                mNativeMethodPrefixAdded;     /* indicates if can_set_native_method_prefix capability has been added */
    char const *            mAgentClassName;        /* agent class name */
    char const *            mOptionsString;         /* -javaagent options string */
};

struct _JPLISEnvironment {
    jvmtiEnv *              mJVMTIEnv;              /* the JVM TI environment */
    JPLISAgent *            mAgent;                 /* corresponding agent */
    jboolean                mIsRetransformer;       /* indicates if special environment */
};

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;
...
jvmtiError err = (*jvmti)->GetLoadedClasses(jvmti, &class_count, &classes);

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

jvmtiError RedefineClasses(jint class_count,
        const jvmtiClassDefinition* class_definitions) {
return functions->RedefineClasses(this, class_count, class_definitions);
}

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

JPLISAgent实例是如何创建的?

继续查看Hotspot代码

/*
 *  Creates a new JPLISAgent.
 *  Returns error if the agent cannot be created and initialized.
 *  The JPLISAgent* pointed to by agent_ptr is set to the new broker,
 *  or NULL if an error has occurred.
 */
JPLISInitializationError
createNewJPLISAgent(JavaVM * vm, JPLISAgent **agent_ptr) {
    JPLISInitializationError initerror       = JPLIS_INIT_ERROR_NONE;
    jvmtiEnv *               jvmtienv        = NULL;
    jint                     jnierror        = JNI_OK;

    *agent_ptr = NULL;
    jnierror = (*vm)->GetEnv(  vm,
                               (void **) &jvmtienv,
                               JVMTI_VERSION_1_1);
    if ( jnierror != JNI_OK ) {
        initerror = JPLIS_INIT_ERROR_CANNOT_CREATE_NATIVE_AGENT;
    } else {
        JPLISAgent * agent = allocateJPLISAgent(jvmtienv);
        if ( agent == NULL ) {
            initerror = JPLIS_INIT_ERROR_ALLOCATION_FAILURE;
        } else {
            initerror = initializeJPLISAgent(  agent,
                                               vm,
                                               jvmtienv);
            if ( initerror == JPLIS_INIT_ERROR_NONE ) {
                *agent_ptr = agent;
            } else {
                deallocateJPLISAgent(jvmtienv, agent);
            }
        }

        /* don't leak envs */
        if ( initerror != JPLIS_INIT_ERROR_NONE ) {
            jvmtiError jvmtierror = (*jvmtienv)->DisposeEnvironment(jvmtienv);
            /* can be called from any phase */
            jplis_assert(jvmtierror == JVMTI_ERROR_NONE);
        }
    }

    return initerror;
}

JPLISInitializationError
initializeJPLISAgent(   JPLISAgent *    agent,
                        JavaVM *        vm,
                        jvmtiEnv *      jvmtienv) {
    jvmtiError      jvmtierror = JVMTI_ERROR_NONE;
    jvmtiPhase      phase;

    agent->mJVM                                      = vm;
    agent->mNormalEnvironment.mJVMTIEnv              = jvmtienv;
    agent->mNormalEnvironment.mAgent                 = agent;
    agent->mNormalEnvironment.mIsRetransformer       = JNI_FALSE;
    agent->mRetransformEnvironment.mJVMTIEnv         = NULL;        /* NULL until needed */
    agent->mRetransformEnvironment.mAgent            = agent;
    agent->mRetransformEnvironment.mIsRetransformer  = JNI_FALSE;   /* JNI_FALSE until mJVMTIEnv is set */
    agent->mAgentmainCaller                          = NULL;
    agent->mInstrumentationImpl                      = NULL;
    agent->mPremainCaller                            = NULL;
    agent->mTransform                                = NULL;
    agent->mRedefineAvailable                        = JNI_FALSE;   /* assume no for now */
    agent->mRedefineAdded                            = JNI_FALSE;
    agent->mNativeMethodPrefixAvailable              = JNI_FALSE;   /* assume no for now */
    agent->mNativeMethodPrefixAdded                  = JNI_FALSE;
    agent->mAgentClassName                           = NULL;
    agent->mOptionsString                            = NULL;
    ...
}

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

*agent_ptr = NULL;
    jnierror = (*vm)->GetEnv(  vm,
                               (void **) &jvmtienv,
                               JVMTI_VERSION_1_1);

从以上代码我们可知,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 {
    private native void
    redefineClasses0(long nativeAgent, ClassDefinition[]  definitions)
        throws  ClassNotFoundException;

    @SuppressWarnings("rawtypes")
    private native Class[]
    getAllLoadedClasses0(long nativeAgent);

    ...
}

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

jobjectArray
commonGetClassList( JNIEnv *            jnienv,
                    JPLISAgent *        agent,
                    jobject             classLoader,
                    ClassListFetcher    fetcher) {
    jvmtiEnv *      jvmtienv        = jvmti(agent);

...
void
redefineClasses(JNIEnv * jnienv, JPLISAgent * agent, jobjectArray classDefinitions) {
    jvmtiEnv*   jvmtienv                        = jvmti(agent);

...

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

如何在Java层调用native接口?

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

//JVMTI_MAGIC    = 0x71EE,
bool __fastcall JvmtiEnvBase::is_valid(JvmtiEnvBase *this)
{
  return *((_DWORD *)this + 2) == 0x71EE;
}

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

struct JavaVM_ {
    const struct JNIInvokeInterface_ *functions;
#ifdef __cplusplus

    jint DestroyJavaVM() {
        return functions->DestroyJavaVM(this);
    }
    jint AttachCurrentThread(void **penv, void *args) {
        return functions->AttachCurrentThread(this, penv, args);
    }
    jint DetachCurrentThread() {
        return functions->DetachCurrentThread(this);
    }

    jint GetEnv(void **penv, jint version) {
        return functions->GetEnv(this, penv, version);
    }
    jint AttachCurrentThreadAsDaemon(void **penv, void *args) {
        return functions->AttachCurrentThreadAsDaemon(this, penv, args);
    }
#endif
};

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

#include <fstream>
#include <iostream>
#include <sys/mman.h>

/* Write @len bytes at @ptr to @addr in this address space using
 * /proc/self/mem.
 */
void memwrite(void *addr, char *ptr, size_t len) {
  std::ofstream ff("/proc/self/mem");
  ff.seekp(reinterpret_cast<size_t>(addr));
  ff.write(ptr, len);
  ff.flush();
}

int main(int argc, char **argv) {
  // Map an unwritable page. (read-only)
  auto mymap =
      (int *)mmap(NULL, 0x9000,
                  PROT_READ, // <<<<<<<<<<<<<<<<<<<<< READ ONLY <<<<<<<<
                  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

  if (mymap == MAP_FAILED) {
    std::cout << "FAILED\n";
    return 1;
  }

  std::cout << "Allocated PROT_READ only memory: " << mymap << "\n";
  getchar();

  // Try to write to the unwritable page.
  memwrite(mymap, "\x40\x41\x41\x41", 4);
  std::cout << "did mymap[0] = 0x41414140 via proc self mem..";
  getchar();
  std::cout << "mymap[0] = 0x" << std::hex << mymap[0] << "\n";
  getchar();

  // Try to writ to the text segment (executable code) of libc.
  auto getchar_ptr = (char *)getchar;
  memwrite(getchar_ptr, "\xcc", 1);

  // Run the libc function whose code we modified. If the write worked,
  // we will get a SIGTRAP when the 0xcc executes.
  getchar();
}

编译执行后,得到结果

root@ecs-16:~# ./proc_mem_poc 
Allocated PROT_READ only memory: 0x7f4390429000

did mymap[0] = 0x41414140 via proc self mem..
mymap[0] = 0x41414140

Trace/breakpoint trap (core dumped)

以上代码示例说明,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
7fcbb9a95000-7fcbb9c95000 ---p 00e88000 fc:01 1179725                    /CloudResetPwdUpdateAgent/depend/jre1.8.0_232/lib/amd64/server/libjvm.so
7fcbb9c95000-7fcbb9d33000 r--p 00e88000 fc:01 1179725                    /CloudResetPwdUpdateAgent/depend/jre1.8.0_232/lib/amd64/server/libjvm.so
7fcbb9d33000-7fcbb9d5c000 rw-p 00f26000 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{
    long func_ptr = 0;
    RandomAccessFile fin = new RandomAccessFile(elfpath, "r");

    byte[] e_ident = new byte[16];
    fin.read(e_ident);
    short e_type = Short.reverseBytes(fin.readShort());
    short e_machine = Short.reverseBytes(fin.readShort());
    int e_version = Integer.reverseBytes(fin.readInt());
    long e_entry = Long.reverseBytes(fin.readLong());
    long e_phoff = Long.reverseBytes(fin.readLong());
    long e_shoff = Long.reverseBytes(fin.readLong());
    int e_flags = Integer.reverseBytes(fin.readInt());
    short e_ehsize = Short.reverseBytes(fin.readShort());
    short e_phentsize = Short.reverseBytes(fin.readShort());
    short e_phnum = Short.reverseBytes(fin.readShort());
    short e_shentsize = Short.reverseBytes(fin.readShort());
    short e_shnum = Short.reverseBytes(fin.readShort());
    short e_shstrndx = Short.reverseBytes(fin.readShort());

    int sh_name = 0;
    int sh_type = 0;
    long sh_flags = 0;
    long sh_addr = 0;
    long sh_offset = 0;
    long sh_size = 0;
    int sh_link = 0;
    int sh_info = 0;
    long sh_addralign = 0;
    long sh_entsize = 0;

    for(int i = 0; i < e_shnum; ++i) {
        fin.seek(e_shoff + i*64);
        sh_name = Integer.reverseBytes(fin.readInt());
        sh_type = Integer.reverseBytes(fin.readInt());
        sh_flags = Long.reverseBytes(fin.readLong());
        sh_addr = Long.reverseBytes(fin.readLong());
        sh_offset = Long.reverseBytes(fin.readLong());
        sh_size = Long.reverseBytes(fin.readLong());
        sh_link = Integer.reverseBytes(fin.readInt());
        sh_info = Integer.reverseBytes(fin.readInt());
        sh_addralign = Long.reverseBytes(fin.readLong());
        sh_entsize = Long.reverseBytes(fin.readLong());
        if(sh_type == SHT_DYNSYM) {
            break;
        }
    }

    int symtab_shdr_sh_link = sh_link;
    long symtab_shdr_sh_size = sh_size;
    long symtab_shdr_sh_entsize = sh_entsize;
    long symtab_shdr_sh_offset = sh_offset;

    fin.seek(e_shoff + symtab_shdr_sh_link * e_shentsize);
    sh_name = Integer.reverseBytes(fin.readInt());
    sh_type = Integer.reverseBytes(fin.readInt());
    sh_flags = Long.reverseBytes(fin.readLong());
    sh_addr = Long.reverseBytes(fin.readLong());
    sh_offset = Long.reverseBytes(fin.readLong());
    sh_size = Long.reverseBytes(fin.readLong());
    sh_link = Integer.reverseBytes(fin.readInt());
    sh_info = Integer.reverseBytes(fin.readInt());
    sh_addralign = Long.reverseBytes(fin.readLong());
    sh_entsize = Long.reverseBytes(fin.readLong());

    long symstr_shdr_sh_offset = sh_offset;

    long cnt = symtab_shdr_sh_entsize > 0 ? symtab_shdr_sh_size/symtab_shdr_sh_entsize : 0;
    for(long i = 0; i < cnt; ++i) {
        fin.seek(symtab_shdr_sh_offset + symtab_shdr_sh_entsize*i);
        int st_name = Integer.reverseBytes(fin.readInt());
        byte st_info = fin.readByte();
        byte st_other = fin.readByte();
        short st_shndx = Short.reverseBytes(fin.readShort());
        long st_value = Long.reverseBytes(fin.readLong());
        long st_size = Long.reverseBytes(fin.readLong());
        if(st_value == 0
            || st_name == 0
            || (ELF_ST_TYPE(st_info) != STT_FUNC && ELF_ST_TYPE(st_info) != STT_GNU_IFUNC))
        {
            continue;
        }

        fin.seek(symstr_shdr_sh_offset + st_name);
        String name = "";
        byte ch = 0;
        while((ch = fin.readByte()) != 0)
        {
            name += (char)ch;
        }

        if(sym.equals(name))
        {
            func_ptr = libbase + st_value;
            break;
        }
    }

    fin.close();

    return func_ptr;
}

最后的步骤

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

void * shellcode()
{
    struct JavaVM_ * vm;
    jsize count;
    JNI_GetCreatedJavaVMs(&vm, 1, &count);
    struct jvmtiEnv_ * _jvmti_env; 
    vm->functions->GetEnv(vm, (void **)&_jvmti_env, JVMTI_VERSION_1_2);
    return _jvmti_env;
}

转换为shellcode

movabs  rax, _JNI_GetCreatedJavaVMs
sub     rsp, 20h
xor     rsi, rsi
inc     rsi
lea     rdx, [rsp+4]
lea     rdi, [rsp+8]
call    rax
mov     rdi, [rsp+8]
lea     rsi, [rsp+10h]
mov     edx, 30010200h
mov     rax, [rdi]
call    qword ptr [rax+30h]
mov     rax, [rsp+10h]
add     rsp, 20h
ret

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

long JPLISAgent = unsafe.allocateMemory(0x1000);
unsafe.putLong(JPLISAgent + 8, native_jvmtienv);

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

try {
    Class<?> instrument_clazz = Class.forName("sun.instrument.InstrumentationImpl");
    Constructor<?> constructor = instrument_clazz.getDeclaredConstructor(long.class, boolean.class, boolean.class);
    constructor.setAccessible(true);
    Object insn = constructor.newInstance(JPLISAgent, true, false);
    Method getAllLoadedClasses = instrument_clazz.getMethod("getAllLoadedClasses");
    Class<?>[] clazzes = (Class<?>[]) getAllLoadedClasses.invoke(insn);

    for(Class<?> cls : clazzes) {
        System.out.println(cls.getName());
    }

}catch(Exception e) {
    System.out.println("Exception: " + e.getMessage());
}

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

意外

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

Java_java_io_RandomAccessFile_length 0x7fb29c485e40
JNI_GetCreatedJavaVMs 0x7fb29d52b650
native_jvmtienv 7fb2980ef070

Exception: null

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

void
redefineClasses(JNIEnv * jnienv, JPLISAgent * agent, jobjectArray classDefinitions) {
    jvmtiEnv*   jvmtienv                        = jvmti(agent);
    jboolean    errorOccurred                   = JNI_FALSE;
    jclass      classDefClass                   = NULL;
    jmethodID   getDefinitionClassMethodID      = NULL;
    ...
    if (!errorOccurred) {
                    jvmtiError  errorCode = JVMTI_ERROR_NONE;
                    errorCode = (*jvmtienv)->RedefineClasses(jvmtienv, numDefs, classDefs);
                    if (errorCode == JVMTI_ERROR_WRONG_PHASE) {
                        /* insulate caller from the wrong phase error */
                        errorCode = JVMTI_ERROR_NONE;
                    } else {
                        errorOccurred = (errorCode != JVMTI_ERROR_NONE);
                        if ( errorOccurred ) {
                            createAndThrowThrowableFromJVMTIErrorCode(jnienv, errorCode);
                        }
                    }
                }
    ...
}

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

__int64 __fastcall jvmti_RedefineClasses(JvmtiEnvBase *this, JavaThread *a2, __int64 a3)
{
  int v4; // er15
  unsigned int v5; // er12
  void *v7; // rax
  unsigned __int64 v8; // r14
  unsigned __int64 v9; // rsi
  __int64 v10; // rbx
  _QWORD *v11; // rax
  _QWORD *v12; // r13
  signed __int32 v13; // [rsp+0h] [rbp-60h] BYREF
  CautiouslyPreserveExceptionMark *v14; // [rsp+8h] [rbp-58h]
  char v15[40]; // [rsp+10h] [rbp-50h] BYREF

  v4 = (int)a2;
  v5 = 112;
  if ( JvmtiEnvBase::_phase == 4 )
  {
    v7 = pthread_getspecific(ThreadLocalStorage::_thread_index);
    v8 = (unsigned __int64)v7;
    if ( v7 && (*(unsigned __int8 (__fastcall **)(void *))(*(_QWORD *)v7 + 40LL))(v7) )
    {
      *(_DWORD *)(v8 + 624) = 5;
      if ( os::_processor_count != 1 || AssumeMP )
      {
        if ( UseMembar )
        {
          if ( os::_processor_count != 1 || AssumeMP )
            _InterlockedAdd(&v13, 0);
        }
        else
        {
          *(_DWORD *)((char *)os::_mem_serialize_page
                    + ((unsigned int)(v8 >> 4) & (unsigned int)os::_serialize_page_mask)) = 1;
        }
      }
      if ( SafepointSynchronize::_state || (*(_DWORD *)(v8 + 48) & 0x30000000) != 0 )
        JavaThread::check_safepoint_and_suspend_for_native_trans((JavaThread *)v8, a2);
      *(_DWORD *)(v8 + 624) = 6;
      v9 = v8;
      v5 = 116;
      v14 = (CautiouslyPreserveExceptionMark *)v15;
      CautiouslyPreserveExceptionMark::CautiouslyPreserveExceptionMark(
        (CautiouslyPreserveExceptionMark *)v15,
        (Thread *)v8);
      if ( (unsigned __int8)JvmtiEnvBase::is_valid(this) )
      {
        v5 = 99;
        if ( (*((_BYTE *)this + 361) & 2) != 0 )   <--- 这个位置校验不过,需要令它为2
        {
          v5 = 103;
          if ( v4 >= 0 )
          {
            v5 = 100;
            if ( a3 )
            {
              v9 = (unsigned int)v4;
              v5 = JvmtiEnv::RedefineClasses(this, (unsigned int)v4, a3);
            }
          }
        }
    ...
}

因此需要用unsafe设置一下

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

测试

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

public static void main(String[] args) {
    ClassPool pool = ClassPool.getDefault();
    CtClass string_clazz = null;
    try {
        string_clazz = pool.get("java.io.RandomAccessFile");
        CtMethod method_getname = string_clazz.getDeclaredMethod("getFD");
        method_getname.insertBefore("System.out.println(\"hi, from java instrucment api\");");
        string_clazz.writeFile("D:\\1.txt");
    } catch (NotFoundException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } catch (CannotCompileException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}

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

try {
    Class<?> instrument_clazz = Class.forName("sun.instrument.InstrumentationImpl");
    Constructor<?> constructor = instrument_clazz.getDeclaredConstructor(long.class, boolean.class, boolean.class);
    constructor.setAccessible(true);
    Object inst = constructor.newInstance(JPLISAgent, true, false);

    //修改过的java.io.RandomAccessFile
    byte hexData[] = {
            ... //太长省略 
    };

    ClassDefinition definition = new ClassDefinition(Class.forName("java.io.RandomAccessFile"), hexData);
    Method redefineClazz = instrument_clazz.getMethod("redefineClasses", ClassDefinition[].class);
    redefineClazz.invoke(inst, new Object[] {
            new ClassDefinition[] {
                    definition
                    }
            });
}catch(Exception e) {
    System.out.println("Exception: " + e.getMessage());
}

fout.getFD();

正确输出结果

Java_java_io_RandomAccessFile_length 0x7fd720689e40
JNI_GetCreatedJavaVMs 0x7fd72172f650
native_jvmtienv 7fd71c0e71d0

hi, from java instrucment api

完整代码请参考: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

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖