【网络安全】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处理请求最先且总是经过的地方,在该类加入内存马的逻辑,可以保证稳定触发

img

类似的逻辑,可以使用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下来认为存在风险的类然后反编译人工审核

img

【一一帮助安全学习,所有资源获取处一一】

①网络安全学习路线

②20份渗透测试电子书

③安全攻防357页笔记

④50份安全攻防面试指南

⑤安全红队渗透工具包

⑥网络安全必备书籍

⑦100个漏洞实战案例

⑧安全大厂内部视频资源

⑨历年CTF夺旗赛题解析

介绍

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

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

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

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

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

SA-JDI分析

我尝试通过Java Agent技术来获取当前的字节码,发现如师傅所说拿不到被修改的字节码

所以为了可以检测Agent马需要从sa-jdi.jar本身入手,想办法dump得到当前字节码(这样不止可以分析被修改了字节码的Agent马也可以分析普通类型的内存马)

注意到其中一个类:sun.jvm.hotspot.tools.jcore.ClassDump并通过查资料发现该类功能正是dump当前的Class(根据类名也可猜测出)其中的main方法提供一个dump class的命令行工具

于是我想了一些办法,用代码实现了命令行工具的功能,并可以设置一个Filter

ini复制代码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过多会导致性能等问题)

java复制代码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等关键字认为可能是普通内存马,需要分析
csharp复制代码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下的依赖,需要特殊配置

xml复制代码 <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的依赖,所以需要特殊处理

xml复制代码 <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文件

xml复制代码 <!-- 省略部分 -->
<dependencySets>
    <dependencySet>
        <outputDirectory> / </outputDirectory>
        <unpack> true </unpack>
        <scope> system </scope>
    </dependencySet>
</dependencySets> 

接着就可以通过代码的方式,根据黑名单和关键字来确定需要dump哪些类然后进行dump操作了

我在测试中遇到一个小问题,值得分享:HttpServlet是正常可以dump的但是ApplicationFilterChain类没有找到。这是因为SpringBoot的懒加载问题,需要手动请求下某个接口就可以了

解决非法字节码

接下来我遇到了一个比较大的坑,通过sa-jdidump下来的字节码是非法的

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

img

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

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

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

img

不报错版本

img

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

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

img

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

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

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

根据Java基础知识可以得知:LAMBDAINVOKEDYNAMIC指令相关,于是我改了ASM的代码

(这里不解释为什么这么改了,是经过多次调试确定的)

org/objectweb/asm/ClassReader#274

ini

复制代码bootstrapMethodOffsets = null ; 

org/objectweb/asm/ClassReader#2456

arduino复制代码case Opcodes . INVOKEDYNAMIC :
  {
    return ;
  } 

改了源码后,就可以正常对非法字节码进行分析了。目前来看没有什么大问题,可以正常分析,但不确定这样的修改是否会存在一些隐患和BUG。总之目前能继续了

分析字节码

分析字节码并不需要太深入做,因为大部分可能出现的内存马都是Runtime.exec或冰蝎反射调ClassLoader.defineClass实现的,针对于这两种情况做分析,足以应对绝大多数情况

以下代码是读取dump的字节码并针对两种情况对所有方法分析

ini复制代码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的字节码中所有方法中是否存在该方法的调用即可(理论上会存在误报,但黑名单类不可能存在该方法,关键字类本身就是可疑的,所以这样做并无不妥)

sql复制代码 @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

代码不长,但对应的字节码较复杂

arduino复制代码Method m = ClassLoader . class . getDeclaredMethod ( "defineClass" , 
                                               String . class ,  ByteBuffer . class ,  ProtectionDomain . class );
m . invoke ( null ); 

对应字节码

bash复制代码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指令执行后应该在栈顶设置污点

kotlin复制代码 @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则可以确定这是内存马
  • 开头一部分代码主要是根据方法参数的实际情况对参数在操作数栈中的索引位置进行确定,是一种动态和自动的确认方式,而不是直接根据经验或者调试写死索引,算是优雅写法
ini复制代码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中(关于这个不是文章重点)

img

然后跑起来我写的工具

  • 其中红色框内是注入的Agent内存马,可以分析出
  • 发现上面还有两个内存马结果,这是我模拟的普通内存马,直接写入到代码中做测试的

img

自动修复

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

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

vbnet复制代码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下的类都需要完整类名
swift复制代码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后,需要加入自动修复的逻辑

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

如果分析出了结果,且用户选择了修复功能,才会进入修复逻辑(暂只修复这两个最常见的类)

sql复制代码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将字节码替换为正常情况即可

ini复制代码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这样会比较方便

xml复制代码 <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的处理进行修复

关于拓展

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

题外话

网络安全行业特点

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

img

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

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

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

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

职业增值潜力大

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

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

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

关于网络安全学习指南

学习网络安全技术的方法无非三种:

第一种是报网络安全专业,现在叫网络空间安全专业,主要专业课程:程序设计、计算机组成原理原理、数据结构、操作系统原理、数据库系统、 计算机网络、人工智能、自然语言处理、社会计算、网络安全法律法规、网络安全、内容安全、数字取证、机器学习,多媒体技术,信息检索、舆情分析等。

第二种是自学,就是在网上找资源、找教程,或者是想办法认识一-些大佬,抱紧大腿,不过这种方法很耗时间,而且学习没有规划,可能很长一段时间感觉自己没有进步,容易劝退。

第三种就是去找培训。

image.png

接下来,我会教你零基础入门快速入门上手网络安全。

网络安全入门到底是先学编程还是先学计算机基础?这是一个争议比较大的问题,有的人会建议先学编程,而有的人会建议先学计算机基础,其实这都是要学的。而且这些对学习网络安全来说非常重要。但是对于完全零基础的人来说又或者急于转行的人来说,学习编程或者计算机基础对他们来说都有一定的难度,并且花费时间太长。

第一阶段:基础准备 4周~6周

这个阶段是所有准备进入安全行业必学的部分,俗话说:基础不劳,地动山摇
image.png

第二阶段:web渗透

学习基础 时间:1周 ~ 2周:

① 了解基本概念:(SQL注入、XSS、上传、CSRF、一句话木马、等)为之后的WEB渗透测试打下基础。
② 查看一些论坛的一些Web渗透,学一学案例的思路,每一个站点都不一样,所以思路是主要的。
③ 学会提问的艺术,如果遇到不懂得要善于提问。
image.png

配置渗透环境 时间:3周 ~ 4周:

① 了解渗透测试常用的工具,例如(AWVS、SQLMAP、NMAP、BURP、中国菜刀等)。
② 下载这些工具无后门版本并且安装到计算机上。
③ 了解这些工具的使用场景,懂得基本的使用,推荐在Google上查找。

渗透实战操作 时间:约6周:

① 在网上搜索渗透实战案例,深入了解SQL注入、文件上传、解析漏洞等在实战中的使用。
② 自己搭建漏洞环境测试,推荐DWVA,SQLi-labs,Upload-labs,bWAPP。
③ 懂得渗透测试的阶段,每一个阶段需要做那些动作:例如PTES渗透测试执行标准。
④ 深入研究手工SQL注入,寻找绕过waf的方法,制作自己的脚本。
⑤ 研究文件上传的原理,如何进行截断、双重后缀欺骗(IIS、PHP)、解析漏洞利用(IIS、Nignix、Apache)等,参照:上传攻击框架。
⑥ 了解XSS形成原理和种类,在DWVA中进行实践,使用一个含有XSS漏洞的cms,安装安全狗等进行测试。
⑦ 了解一句话木马,并尝试编写过狗一句话。
⑧ 研究在Windows和Linux下的提升权限,Google关键词:提权
image.png
以上就是入门阶段

第三阶段:进阶

已经入门并且找到工作之后又该怎么进阶?详情看下图
image.png

给新手小白的入门建议:
新手入门学习最好还是从视频入手进行学习,视频的浅显易懂相比起晦涩的文字而言更容易吸收,这里我给大家准备了一套网络安全从入门到精通的视频学习资料包免费领取哦!

如果你对网络安全入门感兴趣,那么你需要的话可以在下方扫码领取!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值