天天看點

Agent記憶體馬的自動分析與清除

本文首發于先知社群:https://xz.aliyun.com/t/10910

出發點是

Java Agent

記憶體馬的自動分析與清除,實際上其他記憶體馬都可以通過這種方式清除

本文主要的難點主要是以下三個,我會在文中逐個解答

  1. 如何

    dump

    JVM

    中真正的目前的位元組碼
  2. 如何解決由于

    LAMBDA

    表達式導緻非法位元組碼無法分析的問題
  3. 如何對位元組碼進行分析以确定某個類是記憶體馬

基于靜态分析動态,打破規則之道 -- Java King 對本文的評價

背景

對于Java記憶體馬的攻防一直沒有停止,是Java安全領域的重點

回顧

Tomcat

Spring

記憶體馬:

Filter

Controller

等都需要注冊新的元件

針對于需要注冊新元件的記憶體馬清除起來比較容易:

例如

c0ny1

師傅的

java-memshell-scanner

項目,利用了

Tomcat API

删除添加的元件。優點在于一個簡單的

JSP

檔案即可檢視所有的元件資訊,結合人工審查(類名和

ClassLoader

等資訊)對記憶體馬進行清除,也可以對有風險的Class進行

dump

後反編譯分析

或者

LandGrey

師傅基于Alibaba Arthas編寫的

copagent

項目,分析

JVM

中所有的Class,根據危險注解和類名等資訊

dump

可疑的元件,結合人工反編譯後進行分析

但實戰中,可能并不是以上這種注冊新元件的記憶體馬

例如師傅們常用的冰蠍記憶體馬,是

Java Agent

記憶體馬。以下這段是冰蠍記憶體馬一段代碼,簡單分析後可以發現冰蠍記憶體馬是利用

Java Agent

注入到

javax.servlet.http.HttpServlet

service

方法中,這是

JavaEE

的規範,理論上部署在

Tomcat

的都要符合這個規範,簡單來了解這是

Tomcat

處理請求最先且總是經過的地方,在該類加入記憶體馬的邏輯,可以保證穩定觸發

Agent記憶體馬的自動分析與清除

類似的邏輯,可以使用

Java Agent

将記憶體馬注入

org.apache.catalina.core.ApplicationFilterChain

類中,該類位于

Filter

鍊頭部,也就是說經過

Tomcat

的請求都會交經過該類的

doFilter

方法處理,是以在該方法中加入記憶體馬邏輯,也是一種穩定觸發的方式(據說這是老版本冰蠍記憶體馬的方式)

還可以對類似的類進行注入,例如

org.springframework.web.servlet.DispatcherServlet

類,針對于

Spring

架構的底層進行注入。或者一些巧妙的思路,比如注入

Tomcat

自帶的

Filter

之一

org.apache.tomcat.websocket.server.WsFilter

類,這也是

Java Agent

記憶體馬可以做到的

上文簡單地介紹了各種記憶體馬的利用方式與普通記憶體馬的清除,之是以最後介紹

Java Agent

記憶體馬的清除,是因為比較困難。寬位元組安全的師傅提出清除思路:基于javaAgent記憶體馬檢測清除指南

引用文章講到

Java Agent

記憶體馬檢測的難點:

調用

retransformClass

方法的時候參數中的位元組碼并不是調用

redefineClass

後被修改的類的位元組碼。對于冰蠍來講,根本無法擷取被冰蠍修改後的位元組碼。我們自己寫

Java Agent

清除記憶體馬的時候,同樣也是無法擷取到被

redefineClass

修改後的位元組碼,隻能擷取到被

retransformClass

修改後的位元組碼。通過

Javaassist

ASM

工具擷取到類的位元組碼,也隻是讀取磁盤上響應類的位元組碼,而不是

JVM

中的位元組碼

寬位元組安全的師傅找到了一種檢測手段:

sa-jdi.jar

借用公衆号師傅的圖檔,這是一個

GUI

工具,可以檢視

JVM

中所有已加載的類。差別在于這裡擷取到的是真正的目前的位元組碼,而不是擷取到原始的,本地的位元組碼,是以是可以檢視被

Java Agent

調用

redefineClass

後被修改的類的位元組碼。進一步可以

dump

下來認為存在風險的類然後反編譯人工稽核

Agent記憶體馬的自動分析與清除

介紹

以上是背景,接下來介紹我做了些什麼,能夠實作怎樣的效果

不難看出,以上記憶體馬清除手段都是半自動結合人工稽核的方式,當檢測出記憶體馬後

是否可以找到一種方式,做到一條龍式服務:

  • 檢測(同時支援普通記憶體馬和

    Java Agent

    記憶體馬的檢測)
  • 分析(如何确定該類是記憶體馬,僅根據惡意類名和注解等資訊不完善)
  • 清除(當确定記憶體馬存在,如何自動地删除記憶體馬并恢複正常業務邏輯)

大緻看來,實作起來似乎不難,然而實際中遇到了很多坑,接下來我會逐個介紹

SA-JDI分析

我嘗試通過

Java Agent

技術來擷取目前的位元組碼,發現如師傅所說拿不到被修改的位元組碼

是以為了可以檢測

Agent

馬需要從

sa-jdi.jar

本身入手,想辦法

dump

得到目前位元組碼(這樣不止可以分析被修改了位元組碼的

Agent

馬也可以分析普通類型的記憶體馬)

注意到其中一個類:

sun.jvm.hotspot.tools.jcore.ClassDump

并通過查資料發現該類功能正是

dump

目前的Class(根據類名也可猜測出)其中的

main

方法提供一個

dump class

的指令行工具

于是我想了一些辦法,用代碼實作了指令行工具的功能,并可以設定一個

Filter

ClassDump classDump = new ClassDump();
// my filter
classDump.setClassFilter(filter);
classDump.setOutputDirectory("out");
// protected start method
Class<?> toolClass = Class.forName("sun.jvm.hotspot.tools.Tool");
Method method = toolClass.getDeclaredMethod("start", String[].class);
method.setAccessible(true);
// jvm pid
String[] params = new String[]{String.valueOf(pid)};
try {
    method.invoke(classDump, (Object) params);
} catch (Exception ignored) {
    logger.error("unknown error");
    return;
}
logger.info("dump class finish");
// detach
Field field = toolClass.getDeclaredField("agent");
field.setAccessible(true);
HotSpotAgent agent = (HotSpotAgent) field.get(classDump);
agent.detach();           

複制

上文提到設定一個

Filter

是用于确定需要對哪些類進行

dump

操作(dump過多會導緻性能等問題)

public class NameFilter implements ClassFilter {
    @Override
    public boolean canInclude(InstanceKlass instanceKlass) {
        String klassName = instanceKlass.getName().asString();
        // 在黑名單中的類需要dump
        if (blackList.contains(klassName)) {
            return true;
        }
        // 包含了關鍵字的類也需要dump
        for (String k : Constant.keyword) {
            if (klassName.contains(k)) {
                return true;
            }
        }
        return false;
    }
}           

複制

以上包含了類的黑名單和關鍵字:

  • 黑名單:Java Agent記憶體馬通常會Hook的地方,需要

    dump

    下來進行分析
  • 關鍵字:類名如果出現

    memshell

    shell

    等關鍵字認為可能是普通記憶體馬,需要分析
public class Constant {
    // BLACKLIST (Analysis Target)
    // CLASS_NAME#METHOD_NAME
    public static List<String> blackList = new ArrayList<>();
    // SHELL KEYWORD
    public static List<String> keyword = new ArrayList<>();

    static {
        blackList.add("javax/servlet/http/HttpServlet#service");
        blackList.add("org/apache/catalina/core/ApplicationFilterChain#doFilter");
        blackList.add("org/springframework/web/servlet/DispatcherServlet#doService");
        blackList.add("org/apache/tomcat/websocket/server/WsFilter#doFilter");

        keyword.add("shell");
        keyword.add("memshell");
        keyword.add("agentshell");
        keyword.add("exploit");
        keyword.add("payload");
        keyword.add("rebeyond");
        keyword.add("metasploit");
    }
}           

複制

另外如果想在

Maven

項目中加入

JDK/lib

下的依賴,需要特殊配置

<dependency>
    <groupId>sun.jvm.hotspot</groupId>
    <artifactId>sa-jdi</artifactId>
    <version>jdk-8</version>
    <scope>system</scope>
    <systemPath>${env.JAVA_HOME}/lib/sa-jdi.jar</systemPath>
</dependency>           

複制

在打包成工具

Jar

包時預設情況下不會加入

system scope

的依賴,是以需要特殊處理

<artifactId>maven-assembly-plugin</artifactId>
<configuration>
    <appendAssemblyId>false</appendAssemblyId>
    <descriptors>
        <descriptor>assembly.xml</descriptor>
    </descriptors>
    <archive>
        <manifest>
            <mainClass>org.sec.Main</mainClass>
        </manifest>
    </archive>
</configuration>           

複制

編寫

assembly.xml

檔案

<!-- 省略部分 -->
<dependencySets>
    <dependencySet>
        <outputDirectory>/</outputDirectory>
        <unpack>true</unpack>
        <scope>system</scope>
    </dependencySet>
</dependencySets>           

複制

接着就可以通過代碼的方式,根據黑名單和關鍵字來确定需要

dump

哪些類然後進行

dump

操作了

我在測試中遇到一個小問題,值得分享:

HttpServlet

是正常可以

dump

的但是

ApplicationFilterChain

類沒有找到。這是因為

SpringBoot

的懶加載問題,需要手動請求下某個接口就可以了

解決非法位元組碼

接下來我遇到了一個比較大的坑,通過

sa-jdi

dump

下來的位元組碼是非法的

在對

ApplicationFilterChain

類分析的時候,會報如下的錯

Agent記憶體馬的自動分析與清除

起初我懷疑是自己用了最新版

ASM

架構:9.2

于是逐漸降級,發現降級到7.0後不再報錯,但

ClassReader

不報錯,在分析時候會報錯

經過對比,發現是以下的情況

Agent記憶體馬的自動分析與清除

不報錯版本

Agent記憶體馬的自動分析與清除

稍微分析了下,發現是

ApplicationFilterChain

類包含了

LAMBDA

不止這個類,不少的類都有可能會包含

LAMBDA

Agent記憶體馬的自動分析與清除

發現通過

sa-jdi

擷取的位元組碼在存在

LAMBDA

的情況下是非法位元組碼,無法進行分析

這時候如果還想進行分析,隻有兩個選擇:

  • 自己解析CLASS檔案做分析(本末倒置)
  • 改寫ASM源碼使跳過

    LAMBDA

根據Java基礎知識可以得知:

LAMBDA

INVOKEDYNAMIC

指令相關,于是我改了

ASM

的代碼

(這裡不解釋為什麼這麼改了,是經過多次調試确定的)

org/objectweb/asm/ClassReader#274

bootstrapMethodOffsets = null;           

複制

org/objectweb/asm/ClassReader#2456

case Opcodes.INVOKEDYNAMIC:
  {
    return;
  }           

複制

改了源碼後,就可以正常對非法位元組碼進行分析了。目前來看沒有什麼大問題,可以正常分析,但不确定這樣的修改是否會存在一些隐患和BUG。總之目前能繼續了

分析位元組碼

分析位元組碼并不需要太深入做,因為大部分可能出現的記憶體馬都是

Runtime.exec

或冰蠍反射調

ClassLoader.defineClass

實作的,針對于這兩種情況做分析,足以應對絕大多數情況

以下代碼是讀取

dump

的位元組碼并針對兩種情況對所有方法分析

List<Result> results = new ArrayList<>();
int api = Opcodes.ASM9;
int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
for (String fileName : files) {
    byte[] bytes = Files.readAllBytes(Paths.get(fileName));
    if (bytes.length == 0) {
        continue;
    }
    ClassReader cr;
    ClassVisitor cv;
    try {
        // runtime exec analysis
        cr = new ClassReader(bytes);
        cv = new ShellClassVisitor(api, results);
        cr.accept(cv, parsingOptions);
        // classloader defineClass analysis
        cr = new ClassReader(bytes);
        cv = new DefineClassVisitor(api, results);
        cr.accept(cv, parsingOptions);
    } catch (Exception ignored) {
    }
}
for (Result r : results) {
    logger.info(r.getKey() + " -> " + r.getTypeWord());
}           

複制

對于

Runtime.exec

型的分析最為簡單,僅判斷已

dump

的位元組碼中所有方法中是否存在該方法的調用即可(理論上會存在誤報,但黑名單類不可能存在該方法,關鍵字類本身就是可疑的,是以這樣做并無不妥)

@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
    boolean runtimeCondition = owner.equals("java/lang/Runtime") && 
        name.equals("exec") &&
        descriptor.equals("(Ljava/lang/String;)Ljava/lang/Process;");
    if (runtimeCondition) {
        Result result = new Result();
        result.setKey(this.owner);
        result.setType(Result.RUNTIME_EXEC_TIME);
        results.add(result);
    }
    super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}           

複制

但這種情況不适用于冰蠍反射調

ClassLoader.defineClass

代碼不長,但對應的位元組碼較複雜

Method m = ClassLoader.class.getDeclaredMethod("defineClass", 
                                               String.class, ByteBuffer.class, ProtectionDomain.class);
m.invoke(null);           

複制

對應位元組碼

LDC Ljava/lang/ClassLoader;.class // 重點關注
LDC "defineClass" // 重點關注
ICONST_3
ANEWARRAY java/lang/Class
DUP
ICONST_0
LDC Ljava/lang/String;.class
AASTORE
DUP
ICONST_1
LDC Ljava/nio/ByteBuffer;.class
AASTORE
DUP
ICONST_2
LDC Ljava/security/ProtectionDomain;.class
AASTORE
INVOKEVIRTUAL java/lang/Class.getDeclaredMethod (Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method; // 重點關注
ASTORE 1
L1
LINENUMBER 11 L1
ALOAD 1
ACONST_NULL
ICONST_0
ANEWARRAY java/lang/Object
INVOKEVIRTUAL java/lang/reflect/Method.invoke (Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; // 重點關注
POP           

複制

這種操作需要多個步驟,并不是簡單的一個

INVOKE

那麼簡單,不特殊處理的話,由于反射和

ClassLoader

相關操作都算是比較常見的,有一定的誤報可能

于是繼續掏出棧幀分析大法,具體不再介紹,之前文章 已有詳細解釋

根據位元組碼,在

defineClass

Ljava/lang/ClassLoader;

通過

LDC

指令入棧之前,應該認為這是惡意操作,模拟JVM指令執行後應該在棧頂設定污點

@Override
public void visitLdcInsn(Object value) {
    if (value instanceof String) {
        if (value.equals("defineClass")) {
            super.visitLdcInsn(value);
            this.operandStack.set(0, "LDC_STRING");
            return;
        }
    } else {
        if (value.equals(Type.getType("Ljava/lang/ClassLoader;"))) {
            super.visitLdcInsn(value);
            this.operandStack.set(0, "LDC_CL");
            return;
        }
    }
    super.visitLdcInsn(value);
}           

複制

後續主要是對于兩個

INVOKE

進行分析

  • 如果

    getDeclaredMethod

    傳入的是上文

    LDC

    處設定的污點,認為方法傳回值也是污點,給棧頂的傳回值設定

    REFLECTION_METHOD

    标志
  • 如果

    Method.invoke

    方法中的

    Method

    被标記了

    REFLECTION_METHOD

    則可以确定這是記憶體馬
  • 開頭一部分代碼主要是根據方法參數的實際情況對參數在操作數棧中的索引位置進行确定,是一種動态和自動的确認方式,而不是直接根據經驗或者調試寫死索引,算是優雅寫法
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
    Type[] argTypes = Type.getArgumentTypes(descriptor);
    if (opcode != Opcodes.INVOKESTATIC) {
        Type[] extendedArgTypes = new Type[argTypes.length + 1];
        System.arraycopy(argTypes, 0, extendedArgTypes, 1, argTypes.length);
        extendedArgTypes[0] = Type.getObjectType(owner);
        argTypes = extendedArgTypes;
    }
    boolean reflectionMethod = owner.equals("java/lang/Class") &&
        opcode == Opcodes.INVOKEVIRTUAL && name.equals("getDeclaredMethod");
    boolean methodInvoke = owner.equals("java/lang/reflect/Method") &&
        opcode == Opcodes.INVOKEVIRTUAL && name.equals("invoke");
    if (reflectionMethod) {
        int targetIndex = 0;
        for (int i = 0; i < argTypes.length; i++) {
            if (argTypes[i].getClassName().equals("java.lang.String")) {
                targetIndex = i;
                break;
            }
        }
        if (operandStack.get(argTypes.length - targetIndex - 1).contains("LDC_STRING")) {
            super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
            operandStack.set(TOP, "REFLECTION_METHOD");
            return;
        }
    }
    if (methodInvoke) {
        int targetIndex = 0;
        for (int i = 0; i < argTypes.length; i++) {
            if (argTypes[i].getClassName().equals("java.lang.reflect.Method")) {
                targetIndex = i;
                break;
            }
        }
        if (operandStack.get(argTypes.length - targetIndex - 1).contains("REFLECTION_METHOD")) {
            super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
            Result result = new Result();
            result.setKey(owner);
            result.setType(Result.CLASSLOADER_DEFINE);
            results.add(result);
            return;
        }
    }
    super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}           

複制

檢測效果如下:

先寫個記憶體馬注入的

Agent

注入到

HttpServlet

中(關于這個不是文章重點)

Agent記憶體馬的自動分析與清除

然後跑起來我寫的工具

  • 其中紅色框内是注入的

    Agent

    記憶體馬,可以分析出
  • 發現上面還有兩個記憶體馬結果,這是我模拟的普通記憶體馬,直接寫入到代碼中做測試的
Agent記憶體馬的自動分析與清除

自動修複

接下來是記憶體馬的修複,自行寫一個

Java Agent

即可

暫時隻處理

ApplicationFilterChain

HttpServlet

的情況(也是最常見的情況)

public class RepairAgent {
    public static void agentmain(String agentArgs, Instrumentation ins) {
        ClassFileTransformer transformer = new RepairTransformer();
        ins.addTransformer(transformer, true);
        Class<?>[] classes = ins.getAllLoadedClasses();
        for (Class<?> clas : classes) {
            if (clas.getName().equals("org.apache.catalina.core.ApplicationFilterChain")
                    || clas.getName().equals("javax.servlet.http.HttpServlet")) {
                try {
                    ins.retransformClasses(clas);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}           

複制

處理的邏輯并不複雜

  • 由于ApplicationFilterChain中包含了LAMBDA是以我直接簡化了代碼,變成簡單的一句internalDoFilter(1,2)做修複(慎重選擇,為什麼這樣做我将在總結裡解釋)
  • 修改方法的參數需要用1 2這樣表示,不能寫req和resp
  • 這裡

    HttpServlet

    的情況稍複雜,其中有兩個

    service

    方法,實際上對任何一個進行修改都可以導緻記憶體馬的效果,是以我要做的事情是恢複這兩個方法,而不是隻針對某一個
  • 注意任何非

    java.lang

    下的類都需要完整類名
public class RepairTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader,
                            String className,
                            Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) {
        className = className.replace("/", ".");
        ClassPool pool = ClassPool.getDefault();
        if (className.equals("org.apache.catalina.core.ApplicationFilterChain")) {
            try {
                CtClass c = pool.getCtClass(className);
                CtMethod m = c.getDeclaredMethod("doFilter");
                m.setBody("{internalDoFilter($1,$2);}");
                byte[] bytes = c.toBytecode();
                c.detach();
                return bytes;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        if (className.equals("javax.servlet.http.HttpServlet")) {
            try {
                CtClass c = pool.getCtClass(className);
                CtClass[] params = new CtClass[]{
                        pool.getCtClass("javax.servlet.ServletRequest"),
                        pool.getCtClass("javax.servlet.ServletResponse"),
                };
                CtMethod m = c.getDeclaredMethod("service", params);
                m.setBody("{" +
                        "        javax.servlet.http.HttpServletRequest  request;\n" +
                        "        javax.servlet.http.HttpServletResponse response;\n" +
                        "\n" +
                        "        try {\n" +
                        "            request = (javax.servlet.http.HttpServletRequest) $1;\n" +
                        "            response = (javax.servlet.http.HttpServletResponse) $2;\n" +
                        "        } catch (ClassCastException e) {\n" +
                        "            throw new javax.servlet.ServletException(lStrings.getString(\"http.non_http\"));\n" +
                        "        }\n" +
                        "        service(request, response);" +
                        "}");

                CtClass[] paramsProtected = new CtClass[]{
                        pool.getCtClass("javax.servlet.http.HttpServletRequest"),
                        pool.getCtClass("javax.servlet.http.HttpServletResponse"),
                };
                CtMethod mProtected = c.getDeclaredMethod("service", paramsProtected);
                mProtected.setBody("{" +
                        "String method = $1.getMethod();\n" +
                        "\n" +
                        "        if (method.equals(METHOD_GET)) {\n" +
                        "            long lastModified = getLastModified($1);\n" +
                        "            if (lastModified == -1) {\n" +
                        "                doGet($1, $2);\n" +
                        "            } else {\n" +
                        "                long ifModifiedSince;\n" +
                        "                try {\n" +
                        "                    ifModifiedSince = $1.getDateHeader(HEADER_IFMODSINCE);\n" +
                        "                } catch (IllegalArgumentException iae) {\n" +
                        "                    ifModifiedSince = -1;\n" +
                        "                }\n" +
                        "                if (ifModifiedSince < (lastModified / 1000 * 1000)) {\n" +
                        "                    maybeSetLastModified($2, lastModified);\n" +
                        "                    doGet($1, $2);\n" +
                        "                } else {\n" +
                        "                    $2.setStatus(javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED);\n" +
                        "                }\n" +
                        "            }\n" +
                        "\n" +
                        "        } else if (method.equals(METHOD_HEAD)) {\n" +
                        "            long lastModified = getLastModified($1);\n" +
                        "            maybeSetLastModified($2, lastModified);\n" +
                        "            doHead($1, $2);\n" +
                        "\n" +
                        "        } else if (method.equals(METHOD_POST)) {\n" +
                        "            doPost($1, $2);\n" +
                        "\n" +
                        "        } else if (method.equals(METHOD_PUT)) {\n" +
                        "            doPut($1, $2);\n" +
                        "\n" +
                        "        } else if (method.equals(METHOD_DELETE)) {\n" +
                        "            doDelete($1, $2);\n" +
                        "\n" +
                        "        } else if (method.equals(METHOD_OPTIONS)) {\n" +
                        "            doOptions($1, $2);\n" +
                        "\n" +
                        "        } else if (method.equals(METHOD_TRACE)) {\n" +
                        "            doTrace($1, $2);\n" +
                        "\n" +
                        "        } else {\n" +
                        "            String errMsg = lStrings.getString(\"http.method_not_implemented\");\n" +
                        "            Object[] errArgs = new Object[1];\n" +
                        "            errArgs[0] = method;\n" +
                        "            errMsg = java.text.MessageFormat.format(errMsg, errArgs);\n" +
                        "\n" +
                        "            $2.sendError(javax.servlet.http.HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);\n" +
                        "        }"
                        + "}");

                byte[] bytes = c.toBytecode();
                c.detach();
                return bytes;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return new byte[0];
    }
}           

複制

當我們寫好了

Agent

後,需要加入自動修複的邏輯

List<Result> results = Analysis.doAnalysis(files);
if (command.repair) {
    RepairService.start(results, pid);
}           

複制

如果分析出了結果,且使用者選擇了修複功能,才會進入修複邏輯(暫隻修複這兩個最常見的類)

public static void start(List<Result> resultList, int pid) {
    logger.info("try repair agent memshell");
    for (Result result : resultList) {
        String className = result.getKey().replace("/", ".");
        if (className.equals("org.apache.catalina.core.ApplicationFilterChain") ||
            className.equals("javax/servlet/http/HttpServlet")) {
            try {
                start(pid);
                return;
            } catch (Exception ignored) {
            }
        }
    }
}           

複制

修複的核心代碼:把打包好的

Agent

拿過來,做一下

Atach

Load

将位元組碼替換為正常情況即可

public static void start(int pid) {
    try {
        String agent = Paths.get("RepairAgent.jar").toAbsolutePath().toString();
        VirtualMachine vm = VirtualMachine.attach(String.valueOf(pid));
        logger.info("load agent...");
        vm.loadAgent(agent);
        logger.info("repair...");
        vm.detach();
        logger.info("detach agent...");
    } catch (Exception e) {
        e.printStackTrace();
    }
}           

複制

注意使用

VirtualMachine

API

需要加入

tools.jar

,由于上文已經配置了打包插件,是以可以直接打入

Jar

包,使用時候

java -jar xxx.jar --pid 000

這樣會比較友善

<dependency>
    <groupId>com.sun.tools</groupId>
    <artifactId>tools</artifactId>
    <version>jdk-8</version>
    <scope>system</scope>
    <systemPath>${env.JAVA_HOME}/lib/tools.jar</systemPath>
</dependency>           

複制

通過以上這些修複手段可以做到的效果:

  • 啟動某SpringBoot應用
  • 通過

    Agent

    注入記憶體馬,通路後記憶體馬可用
  • 通過工具檢測到記憶體馬,嘗試修改,使位元組碼被還原
  • 再次通路後記憶體馬失效,不需要重新開機

總結

關于Dump位元組碼

經過我的一些測試,使用

sa-jdi

庫不能保證

dump

所有的位元組碼,會出現莫名其妙的異常,猜測是某些位元組碼不允許被

dump

下來。但測試了常見

Tomcat

SpringBoot

等程式,發現基本沒有問題

關于非法位元組碼

隻要是包含

LAMBDA

的位元組碼都是非法位元組碼,無法正常處理,需要用修改源碼後的

ASM

來做。這種方式終究不是完美的辦法,是否存在能夠

dump

下來合法位元組碼的方式呢(經過一些嘗試沒有找到辦法)

關于檢測

可以看到,位元組碼分析的過程比較簡單,尤其是

Runtime.exec

的普通執行指令記憶體馬,很容易繞過,但個人認為這已足夠,因為之前的一些條件已經限制了分析的類是不可能包含

Runtime.exec

的黑名單類,且大多數使用者都是腳本小子,使用免殺型記憶體馬的可能性不大。大多數使用者可能直接用了現成的工具,例如冰蠍型記憶體馬的檢測方式已完成,暫時來看這樣做是足夠的,沒有必要加入各種免殺檢測手段

關于清除

使用Agent恢複位元組碼的修複方式理論上沒有問題。但其中的

ApplicationFilterChain

類的

doFilter

方法中包含了

LAMBDA

和匿名内部類,這兩者都是

Javassist

架構不支援的内容,可以用

ASM

來做,但可能難度較高

另外對于普通型記憶體馬的修複,通過Agent技術隻能覆寫方法體,不可以增加或删除方法。是以理論上可以根據方法的傳回值類型,做傳回

NULL

的處理進行修複

關于拓展

例如代碼中我定義的黑名單和關鍵字,可以根據實戰經驗自行添加新的類,以實作更完善的效果。在清除方面我做了最常見的兩種,可以根據實際情況自行添加更多的邏輯

最後

代碼位址:https://github.com/4ra1n/FindShell