【网络安全】Agent内存马的自动分析与查杀

前言

出发点是Java Agent内存马的自动分析与查杀,实际上其他内存马都可以通过这种方式查杀

本文主要的难点主要是以下三个,我会在文中逐个解答

  1. 如何dumpJVM真正的当前的字节码
  2. 如何解决由于LAMBDA表达式导致非法字节码无法分析的问题
  3. 如何对字节码进行分析以确定某个类是内存马

背景

对于Java内存马的攻防一直没有停止,是Java安全领域的重点

回顾TomcatSpring内存马:FilterController等都需要注册新的组件

针对于需要注册新组件的内存马查杀起来比较容易:

例如c0ny1师傅的java-memshell-scanner项目,利用了Tomcat API删除添加的组件。优点在于一个简单的JSP文件即可查看所有的组件信息,结合人工审查(类名和ClassLoader等信息)对内存马进行查杀,也可以对有风险的Class进行dump后反编译分析

或者LandGrey师傅基于Alibaba Arthas编写的copagent项目,分析JVM中所有的Class,根据危险注解和类名等信息dump可疑的组件,结合人工反编译后进行分析

但实战中,可能并不是以上这种注册新组件的内存马

例如师傅们常用的冰蝎内存马,是Java Agent内存马。以下这段是冰蝎内存马一段代码,简单分析后可以发现冰蝎内存马是利用Java Agent注入到javax.servlet.http.HttpServletservice方法中,这是JavaEE的规范,理论上部署在Tomcat的都要符合这个规范,简单来理解这是Tomcat处理请求最先且总是经过的地方,在该类加入内存马的逻辑,可以保证稳定触发

在这里插入图片描述

类似的逻辑,可以使用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修改后的字节码。通过JavaassistASM工具获取到类的字节码,也只是读取磁盘上响应类的字节码,而不是JVM中的字节码

宽字节安全的师傅找到了一种检测手段:sa-jdi.jar

借用公众号师傅的图片,这是一个GUI工具,可以查看JVM中所有已加载的类。区别在于这里获取到的是真正的当前的字节码,而不是获取到原始的,本地的字节码,所以是可以查看被Java Agent调用redefineClass后被修改的类的字节码。进一步可以dump下来认为存在风险的类然后反编译人工审核

在这里插入图片描述

介绍

以上是背景,接下来介绍我做了些什么,能够实现怎样的效果

不难看出,以上内存马查杀手段都是半自动结合人工审核的方式,当检测出内存马后

是否可以找到一种方式,做到一条龙式服务:

  • 检测(同时支持普通内存马和Java Agent内存马的检测)
  • 分析(如何确定该类是内存马,仅根据恶意类名和注解等信息不完善)
  • 查杀(当确定内存马存在,如何自动地删除内存马并恢复正常业务逻辑)

大致看来,实现起来似乎不难,然而实际中遇到了很多坑,接下来我会逐个介绍

→点击获取网络安全资料·攻略←

200多本网络安全系列电子书
网络安全标准题库资料
项目源码
网络安全基础入门、Linux、web安全、攻防方面的视频
网络安全学习路线图

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下来进行分析
  • 关键字:类名如果出现memshellshell等关键字认为可能是普通内存马,需要分析
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-jdidump下来的字节码是非法的

在对ApplicationFilterChain类分析的时候,会报如下的错

在这里插入图片描述

起初我怀疑是自己用了最新版ASM框架:9.2

于是逐渐降级,发现降级到7.0后不再报错,但ClassReader不报错,在分析时候会报错

经过对比,发现是以下的情况

在这里插入图片描述

不报错版本

在这里插入图片描述

稍微分析了下,发现是ApplicationFilterChain类包含了LAMBDA

不止这个类,不少的类都有可能会包含LAMBDA

在这里插入图片描述

发现通过sa-jdi获取的字节码在存在LAMBDA的情况下是非法字节码,无法进行分析

这时候如果还想进行分析,只有两个选择:

  • 自己解析CLASS文件做分析(本末倒置)
  • 改写ASM源码使跳过LAMBDA

根据Java基础知识可以得知:LAMBDAINVOKEDYNAMIC指令相关,于是我改了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相关操作都算是比较常见的,有一定的误报可能

于是继续掏出栈帧分析大法,具体不再介绍,之前文章 已有详细解释

根据字节码,在defineClassLjava/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内存马,可以分析出
  • 发现上面还有两个内存马结果,这是我模拟的普通内存马,直接写入到代码中做测试的

在这里插入图片描述

自动修复

接下来是内存马的修复,自行写一个Java Agent即可

暂时只处理ApplicationFilterChainHttpServlet的情况(也是最常见的情况)

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这样表示,不能写reqresp
  • 这里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拿过来,做一下AtachLoad将字节码替换为正常情况即可

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 ();
    }
} 

注意使用VirtualMachineAPI需要加入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下来。但测试了常见TomcatSpringBoot等程序,发现基本没有问题

关于非法字节码

只要是包含LAMBDA的字节码都是非法字节码,无法正常处理,需要用修改源码后的ASM来做。这种方式终究不是完美的办法,是否存在能够dump下来合法字节码的方式呢(经过一些尝试没有找到办法)

关于检测

可以看到,字节码分析的过程比较简单,尤其是Runtime.exec的普通执行命令内存马,很容易绕过,但个人认为这已足够,因为之前的一些条件已经限制了分析的类是不可能包含Runtime.exec的黑名单类,且大多数用户都是脚本小子,使用免杀型内存马的可能性不大。大多数用户可能直接用了现成的工具,例如冰蝎型内存马的检测方式已完成,暂时来看这样做是足够的,没有必要加入各种免杀检测手段

关于查杀

使用Agent恢复字节码的修复方式理论上没有问题。但其中的ApplicationFilterChain类的doFilter方法中包含了LAMBDA和匿名内部类,这两者都是Javassist框架不支持的内容,可以用ASM来做,但可能难度较高

另外对于普通型内存马的修复,通过Agent技术只能覆盖方法体,不可以增加或删除方法。所以理论上可以根据方法的返回值类型,做返回NULL的处理进行修复

关于拓展

例如代码中我定义的黑名单和关键字,可以根据实战经验自行添加新的类,以实现更完善的效果。在查杀方面我做了最常见的两种,可以根据实际情况自行添加更多的逻辑

题外话

初入计算机行业的人或者大学计算机相关专业毕业生,很多因缺少实战经验,就业处处碰壁。下面我们来看两组数据:

  • 2023届全国高校毕业生预计达到1158万人,就业形势严峻;

  • 国家网络安全宣传周公布的数据显示,到2027年我国网络安全人员缺口将达327万。

一方面是每年应届毕业生就业形势严峻,一方面是网络安全人才百万缺口。

6月9日,麦可思研究2023年版就业蓝皮书(包括《2023年中国本科生就业报告》《2023年中国高职生就业报告》)正式发布。

2022届大学毕业生月收入较高的前10个专业

本科计算机类、高职自动化类专业月收入较高。2022届本科计算机类、高职自动化类专业月收入分别为6863元、5339元。其中,本科计算机类专业起薪与2021届基本持平,高职自动化类月收入增长明显,2022届反超铁道运输类专业(5295元)排在第一位。

具体看专业,2022届本科月收入较高的专业是信息安全(7579元)。对比2018届,电子科学与技术、自动化等与人工智能相关的本科专业表现不俗,较五年前起薪涨幅均达到了19%。数据科学与大数据技术虽是近年新增专业但表现亮眼,已跻身2022届本科毕业生毕业半年后月收入较高专业前三。五年前唯一进入本科高薪榜前10的人文社科类专业——法语已退出前10之列。
在这里插入图片描述

“没有网络安全就没有国家安全”。当前,网络安全已被提升到国家战略的高度,成为影响国家安全、社会稳定至关重要的因素之一。

网络安全行业特点

1、就业薪资非常高,涨薪快 2021年猎聘网发布网络安全行业就业薪资行业最高人均33.77万!

2、人才缺口大,就业机会多

2019年9月18日《中华人民共和国中央人民政府》官方网站发表:我国网络空间安全人才 需求140万人,而全国各大学校每年培养的人员不到1.5W人。猎聘网《2021年上半年网络安全报告》预测2027年网安人才需求300W,现在从事网络安全行业的从业人员只有10W人。

行业发展空间大,岗位非常多

网络安全行业产业以来,随即新增加了几十个网络安全行业岗位︰网络安全专家、网络安全分析师、安全咨询师、网络安全工程师、安全架构师、安全运维工程师、渗透工程师、信息安全管理员、数据安全工程师、网络安全运营工程师、网络安全应急响应工程师、数据鉴定师、网络安全产品经理、网络安全服务工程师、网络安全培训师、网络安全审计员、威胁情报分析工程师、灾难恢复专业人员、实战攻防专业人员…

职业增值潜力大

网络安全专业具有很强的技术特性,尤其是掌握工作中的核心网络架构、安全技术,在职业发展上具有不可替代的竞争优势。

随着个人能力的不断提升,所从事工作的职业价值也会随着自身经验的丰富以及项目运作的成熟,升值空间一路看涨,这也是为什么受大家欢迎的主要原因。

从某种程度来讲,在网络安全领域,跟医生职业一样,越老越吃香,因为技术愈加成熟,自然工作会受到重视,升职加薪则是水到渠成之事。

黑客&网络安全如何学习

今天只要你给我的文章点赞,我私藏的网安学习资料一样免费共享给你们,来看看有哪些东西。

1.学习路线图

攻击和防守要学的东西也不少,具体要学的东西我都写在了上面的路线图,如果你能学完它们,你去就业和接私活完全没有问题。

2.视频教程

网上虽然也有很多的学习资源,但基本上都残缺不全的,这是我自己录的网安视频教程,上面路线图的每一个知识点,我都有配套的视频讲解。

内容涵盖了网络安全法学习、网络安全运营等保测评、渗透测试基础、漏洞详解、计算机基础知识等,都是网络安全入门必知必会的学习内容。
在这里插入图片描述

(都打包成一块的了,不能一一展开,总共300多集)

因篇幅有限,仅展示部分资料,需要保存下方图片,微信扫码即可前往获取

3.技术文档和电子书

技术文档也是我自己整理的,包括我参加大型网安行动、CTF和挖SRC漏洞的经验和技术要点,电子书也有200多本,由于内容的敏感性,我就不一一展示了。

在这里插入图片描述

因篇幅有限,仅展示部分资料,需要保存下方图片,微信扫码即可前往获取

### 4.工具包、面试题和源码

“工欲善其事必先利其器”我为大家总结出了最受欢迎的几十款款黑客工具。涉及范围主要集中在 信息收集、Android黑客工具、自动化工具、网络钓鱼等,感兴趣的同学不容错过。

还有我视频里讲的案例源码和对应的工具包,需要的话也可以拿走。

因篇幅有限,仅展示部分资料,需要保存下方图片,微信扫码即可前往获取

最后就是我这几年整理的网安方面的面试题,如果你是要找网安方面的工作,它们绝对能帮你大忙。

这些题目都是大家在面试深信服、奇安信、腾讯或者其它大厂面试时经常遇到的,如果大家有好的题目或者好的见解欢迎分享。

参考解析:深信服官网、奇安信官网、Freebuf、csdn等

内容特点:条理清晰,含图像化表示更加易懂。

内容概要:包括 内网、操作系统、协议、渗透测试、安服、漏洞、注入、XSS、CSRF、SSRF、文件上传、文件下载、文件包含、XXE、逻辑漏洞、工具、SQLmap、NMAP、BP、MSF…

因篇幅有限,仅展示部分资料,需要保存下方图片,微信扫码即可前往获取

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值