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對其進行注入即可。