JavaAgent型記憶體馬基礎

xyylll發表於2021-10-27

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對傳入的位元組碼進行改寫或替換,生成新的位元組碼陣列後返回。

image

​ 總之,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程式

執行結果

image

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

相關文章