JavaAgent技术简介
JDK1.5开始引入了Agent机制(即启动java程序时添加"-javaagen t "参数,Java Agent机制允许用户在JVM加载class文件的时候先加载自己编写的Agent文件,通过修改JVM传入的字节码来实现注入 自定义的代码 。 采用 这种方式 时, 必须在容器启动时添加jvm参数,所以需要重启Web容器。
JDK1.6新增了attach方式,可以对运行中的jav a 进程附加agent ,提供了动态修改运行中已经被加载的类的途径。一般通过 VirtualMachine的attach(pid)方法获得VirtualMachine实例,随后 可 调用loadagent方法将JavaAgent的jar包加载到目标JVM中。
下面一个章节笔者将通过两个demo案例说明 J ava A gent技术的两种方式,让读者明白premain和agentmain的具体原理。
JavaAgent两种方式 1 Premain
创建一个 sayHello 类,写一个say () 方法。
|--------------------------------------------------------------------------| | public class sayHello { public String say() { return "hello,world!"; } } |
创建一个People类,运行say () 方法,输出结果为: hello,world!
|----------------------------------------------------------------------------------------------------------------| | public class People { public static void main(String[] args) { System.out.println(new sayHello().say()); } } |
创建 Transformer 重写t ransformer 方法,实现修改传入J VM 的字节码。笔者这里通过javassist对类字节码进行处理。
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| package org.example;
import javassist.*; import java.io.IOException; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain;
public class Transformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { System.out.println(className); if (className.endsWith("sayHello")){ try { final ClassPool classPool = ClassPool.getDefault(); // 创建ClassPool对象 final CtClass ctClass = classPool.get("org.example.sayHello"); CtMethod ctMethod = ctClass.getDeclaredMethod("say"); // 获取成员方法 String methodBody = "return \"hello premain\";"; ctMethod.setBody(methodBody); //替换方法体中所有内容 byte[] bytes = ctClass.toBytecode(); //使用类CtClass,生成类二进制 //调用CtClass对象的detach()方法 CtClass对象从ClassPool移除掉减少内存消耗 ctClass.detach(); return bytes; } catch (NotFoundException e) { e.printStackTrace(); } catch (CannotCompileException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } return null; } } |
定义 Premain 类的p remain 方法
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | package org.example; import java.lang.instrument.Instrumentation; public class Premain { public static void premain(String agentArgs, Instrumentation inst){ System.out.println("premain agent run!"); inst.addTransformer(new Transformer()); } } |
使用Maven打包成 TestPremain-1.0-SNAPSHOT.jar 文件,需要如下修改p om.xml 文件。 把 <Premain-class> 设置为p remain 方法所在类。
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | <plugin> <artifactId>maven-jar-plugin</artifactId> <version>3.0.2</version > <configuration> <archive> <manifestEntries> <Premain-class>org.example.Premain</Premain-class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> </plugin> |
在运行配置中添加vm选项
图1
运行结果如图2所示,修改了say方法。
图2 2 Agentmain
同premain也创建一个People类循环打印字符串,代码如下所示。
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| package org.example;
public class People { public void sayHello(String name) { System.out.println(String.format("%s say hello!", name)); }
public static void main(String[] args) throws InterruptedException { People p = new People(); for (;;){ Thread.sleep(1000); p.sayHello(Thread.currentThread().getName()); } } } |
重写 transform 方法,注入进程后打印输出代理的类,代码如下所示。
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| package org.example;
import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain;
public class Transform implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { System.out.println(String.format("agent run target class= %s", className)); return classfileBuffer; } } |
新建Agent类实现agentmain方法,代码如下所示
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | public class Agent { public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException { inst.addTransformer(new Transform(),true); inst.retransformClasses(Class.forName("org.example.People")); } } |
将 A gent设置为 <Agent-Class> 并打包成为jar文件。 Pom.xml 文件如下所示,值得注意的是如果需要修改已经被JVM加载过的类的字节码,那么还需要在MANIFEST.MF中添加Can-Retransform-Classes:true或Can-Redefine-Classes:true。
|--------------------------------------------------------------------------------------------------------------| | <Agent-Class>org.example.Agent</Agent-Class> <Can-Retransform-Classes>true</Can-Retransform-Classes> |
创建 Attach 类注入目标类的进程,代码如下所示。
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | public class Attach { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { String agentPath = "F:\\IdeaProjects\\TestAgent\\target\\TestAgent-1.0-SNAPSHOT.jar"; List<VirtualMachineDescriptor> list = VirtualMachine.list(); //获取本机所有运行的Java进程 for (VirtualMachineDescriptor desc :list){ if (desc.displayName().endsWith("People")){ VirtualMachine vm = VirtualMachine.attach(desc.id()); vm.loadAgent(agentPath); vm.detach(); } } } } |
A ttach捕获到类进程号如图 3 所示。
图 3
先运行people类在运行attach,运行结果如图 4 所示。
图 4
分析JavaAgent型内存马
由上文可知Agentmain 可 实现最重要的三个类Agent A ttach T ransform,来分析冰蝎作者之前写的memshell实现原理,项目地址: https://github.com/rebeyond/memShell.git M emshell中 T ransform类代码如图5所示。
图 5
不同于前面章节的demo,这里除了使用 ClassPool.getDefault() 还使用 ClassClassPath 搜索class路径其 原理 是 : ClassPool.getDefault()获取的ClassPool使用JVM的classpath 。 在Tomcat等Web服务器运行时,服务器会使用多个类加载器作为系统类加载器,这可能导致ClassPool可能无法找到用户的类 。 这时,ClassPool须添加额外的classpath才能搜索到用户的类 。
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | CtClass cc = cp.get("org.apache.catalina.core.ApplicationFilterChain"); CtMethod m = cc.getDeclaredMethod("internalDoFilter"); m.addLocalVariable("elapsedTime", CtClass.longType); m.insertBefore(readSource()); |
如上代码:作者 H ook了 ApplicationFilterChain 中的 internalDoFilter 方法,然后定义一个long类型的属性,elapsedTime , 并通过insertBefore 方法将 source.txt 中内容 插入到方法内容的开始处 。 source.txt 是url参数和agent交互的逻辑,如图6所示。
图 6
笔者之前用此内存马时发现两个特点 : 第一是 该内存马会 自己删除jar包,实现代码如下。
图 7
第二 点是 重启tomcat服务之后内存马还是存在,只有通过jps- l kill掉进程后启动服务才能删除内存马 , 其 原理 是使用了 ShutdownHook 机制。
图 8
通过使用 Runtime.addShutdownHook(Thread hook) 方法注册 JVM 关闭的勾子,调用 writeFiles 方法把jar包落地磁盘 , 再通过 Runtime.exec 启动 java-jar inject.jar 。
由于Hook的关键函数 ApplicationFilterChain .internalDoFilter 是tomcat的方法 , 导致其他中间件不适用,在冰蝎3 .0 中的内存马作者更改了Hook点。(源码版本为 V3.0 Beta11_t00ls )在 agentmain 中做了一个判断,如果是Tomcat选择hook javax.servlet.http.HttpServlet 中的 service 方法,如果是weblogic选择 hook weblogic.servlet. internal. ServletStubImpl 中的 execute 方法。
代码如图9所示。
图 9
在jdk 9及以后的版本不允许SelfAttach(即无法attach自身的进程) 。修改前面章节Attach demo,将jdk换成9之后的,attach自身的P ID 会报错提示 Can not attach to current VM 。代码如下,报错截图如图1 0 所示。
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| public class Attach { public static void main(String[] args) throws Exception { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for(VirtualMachineDescriptor desc : list){ System.out.println("进程ID:" + desc.id() + ",进程名称:" + desc.displayName()); } Scanner myObj = new Scanner(System.in); System.out.println("输入要注入的进程:"); String pid = myObj.nextLine(); String agentPath = "F:\\IdeaProjects\\TestAgent\\target\\TestAgent-1.0-SNAPSHOT.jar"; VirtualMachine vm = VirtualMachine.attach(pid); vm.loadAgent(agentPath); vm.detach(); }
} |
图 10
看到 Rebeyond 师傅在《 Java内存攻击技术漫谈 》中提出一种方法,绕过 allowAttachSelf 。首先 D ebug attch执行流程,如图1 1 所示。 可以 发现 attach的时候会创建一个HotSpotVirtualMachine的父类 对象,取键值对 jdk.attach.allowAttachSelf 的值计算后保存到 ALLOW_ATTACH_SELF 中 , 可通过反射修改该属性值。
图1 1
ALLOW_ATTACH_SELF字段有final修饰符 ,需要设置 setAccessible(true) ; 具体代码如下所示。
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Class cls=Class.forName("sun.tools.attach.HotSpotVirtualMachine"); Field field=cls.getDeclaredField("ALLOW_ATTACH_SELF"); field.setAccessible(true); Field modifiersField=Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(field,field.getModifiers()&~Modifier.FINAL); field.setBoolean(null,true); |
修改后会弹出警告信息如图1 2 所示,成功注入结果如图1 3 所示。
图1 2
图1 3
回到冰蝎3 .0 源码中,通过 setProperty 将 jdk.attach.allowAttachSelf 设置为 true ,实现绕过 SelfAttach 。
|-----------------------------------------------------------| | System.setProperty("jdk.attach.allowAttachSelf", "true"); |
总结
本文 从permain和agentmain两种实现Java A gent的原理方法引入到java agent在内存马中的应用,通过分析memshell到冰蝎3 .0 内存马源码, 加深了对 agent型内存马Hook的关键函数 、 持久化方法 以及 绕过 SelfAttach 方法等内存马技术点 的理解与学习,希望对读者有帮助 。
本文作者: 安全狗
本文为安全脉搏专栏作者发布,转载请注明: https://www.secpulse.com/archives/174266.html