长亭百川云 - 文章详情

护网专题第一篇-Java内存马(上)

零鉴科技

70

2024-07-14

Java Agent 从入门到内存马(上)

入门

介绍

注意:这里只是简短的介绍一下,想要详细了解,请看参考资料。

在JDK1.5以后,javaagent是一种能够在不影响正常编译的情况下,修改字节码。

java作为一种强类型的语言,不通过编译就不能够进行jar包的生成。而有了javaagent技术,就可以在字节码这个层面对类和方法进行修改。同时,也可以把javaagent理解成一种代码注入的方式。但是这种注入比起spring的aop更加的优美。

Java agent的使用方式有两种:

•实现premain方法,在JVM启动前加载。•实现agentmain方法,在JVM启动后加载。

premainagentmain函数声明如下,拥有Instrumentation inst参数的方法优先级更高

`public static void agentmain(String agentArgs, Instrumentation inst) {`    `...``}``   ``public static void agentmain(String agentArgs) {`    `...``}``   ``public static void premain(String agentArgs, Instrumentation inst) {`    `...``}``   ``public static void premain(String agentArgs) {`    `...``}`

第一个参数String agentArgs就是Java agent的参数。

第二个参数Instrumentaion inst相当重要,会在之后的进阶内容中提到。

premain

要做一个简单的premain需要以下几个步骤:

1.创建新项目,项目结构为:

`agent``├── agent.iml``├── pom.xml``└── src`    `├── main`    `│   ├── java`    `│   └── resources`    `└── test`        `└── java`

2.创建一个类(这里为com.shiroha.demo.PreDemo),并且实现premain方法。

`package com.shiroha.demo;``   ``import java.lang.instrument.Instrumentation;``   ``public class PreDemo {`    `public static void premain(String args, Instrumentation inst) throws Exception{`        `for (int i = 0; i < 10; i++) {`            ``System.out.println("hello I`m premain agent!!!");``        `}`    `}``}`

3.在src/main/resources/目录下创建META-INF/MANIFEST.MF,需要指定Premain-Class

`Manifest-Version: 1.0``Premain-Class: com.shiroha.demo.PreDemo`

要注意的是,最后必须多一个换行

4.打包成jar

选择Project Structure -> Artifacts -> JAR -> From modules with dependencies

默认的配置就行。

选择Build -> Build Artifacts -> Build

之后产生out/artifacts/agent_jar/agent.jar

`└── out`    `└── artifacts`        `└── agent_jar`            `└── agent.jar`

5.使用-javaagent:agent.jar参数执行hello.jar,结果如下。

可以发现在hello.jar输出hello world之前就执行了com.shiroha.demo.PreDemo$premain方法。

当使用这种方法的时候,整个流程大致如下图所示:

然而这种方法存在一定的局限性——只能在启动时使用**-javaagent参数指定**。在实际环境中,目标的JVM通常都是已经启动的状态,无法预先加载premain。相比之下,agentmain更加实用。

agentmain

写一个agentmainpremain差不多,只需要在META-INF/MANIFEST.MF中加入Agent-Class:即可。

`Manifest-Version: 1.0``Premain-Class: com.shiroha.demo.PreDemo``Agent-Class: com.shiroha.demo.AgentDemo`

不同的是,这种方法不是通过JVM启动前的参数来指定的,官方为了实现启动后加载,提供了Attach API。Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach 包里面。着重关注的是VitualMachine这个类。

VirtualMachine

字面意义表示一个Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了获取系统信息、 loadAgentAttach 和 Detach 等方法,可以实现的功能可以说非常之强大 。该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上 。代理类注入操作只是它众多功能中的一个,通过loadAgent方法向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例。

具体的用法看一下官方给的例子大概就理解了:

`// com.sun.tools.attach.VirtualMachine``   ``// 下面的示例演示如何使用VirtualMachine:`  `        // attach to target VM`        `VirtualMachine vm = VirtualMachine.attach("2177");``        // start management agent`        `Properties props = new Properties();`        `props.put("com.sun.management.jmxremote.port", "5000");`        `vm.startManagementAgent(props);`        `// detach`        `vm.detach();`  `// 在此示例中,我们附加到由进程标识符2177标识的Java虚拟机。然后,使用提供的参数在目标进程中启动JMX管理代理。最后,客户端从目标VM分离。`

下面列几个这个类提供的方法:

`public abstract class VirtualMachine {`  `// 获得当前所有的JVM列表`    `public static List<VirtualMachineDescriptor> list() { ... }`    `    // 根据pid连接到JVM`    `public static VirtualMachine attach(String id) { ... }`    `    // 断开连接`    `public abstract void detach() {}`  `    // 加载agent,agentmain方法靠的就是这个方法`  `public void loadAgent(String agent) { ... }`    `}`

根据提供的api,可以写出一个attacher,代码如下:

`import com.sun.tools.attach.AgentInitializationException;``import com.sun.tools.attach.AgentLoadException;``import com.sun.tools.attach.AttachNotSupportedException;``import com.sun.tools.attach.VirtualMachine;``   ``import java.io.IOException;``   ``public class AgentMain {`    `public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {`        `String id = args[0];`        `String jarName = args[1];``   `        `System.out.println("id ==> " + id);`        `System.out.println("jarName ==> " + jarName);``   `        `VirtualMachine virtualMachine = VirtualMachine.attach(id);`        `virtualMachine.loadAgent(jarName);`        `virtualMachine.detach();`        `        System.out.println("ends");`    `}``}`

过程非常简单:通过pid attach到目标JVM -> 加载agent -> 解除连接。

现在来测试一下agentmain:

`package com.shiroha.demo;``   ``import java.lang.instrument.Instrumentation;``   ``public class AgentDemo {`    `public static void agentmain(String agentArgs, Instrumentation inst) {`        `for (int i = 0; i < 10; i++) {`            ``System.out.println("hello I`m agentMain!!!");``        `}`    `}``}`

成功attach并加载了agent。

整个过程的流程图大致如下图所示:

进阶

Instrumentation

InstrumentationJVMTIAgent(JVM Tool Interface Agent)的一部分。Java agent通过这个类和目标JVM进行交互,从而达到修改数据的效果。

下面列出这个类的一些方法,更加详细的介绍和方法,可以参照官方文档[1]。也可以看下面的参考资料[2]。

`public interface Instrumentation {`    `    // 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。`    `void addTransformer(ClassFileTransformer transformer);``   `    `// 删除一个类转换器`    `boolean removeTransformer(ClassFileTransformer transformer);``   `    `// 在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。`    `void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;``   `    `// 判断目标类是否能够修改。`    `boolean isModifiableClass(Class<?> theClass);``   `    `// 获取目标已经加载的类。`    `@SuppressWarnings("rawtypes")`    `Class[] getAllLoadedClasses();`    `    ......``}`

由于知识点过多和篇幅限制,只先介绍getAllLoadedClassesisModifiableClasses

看名字都知道:

getAllLoadedClasses:获取所有已经加载的类。•isModifiableClasses:判断某个类是否能被修改。

修改之前写的agentmain:

`package com.shiroha.demo;``   ``import java.io.File;``import java.io.FileNotFoundException;``import java.io.FileOutputStream;``import java.io.IOException;``import java.lang.instrument.Instrumentation;``   ``public class AgentDemo {`    `public static void agentmain(String agentArgs, Instrumentation inst) throws IOException {`        `Class[] classes = inst.getAllLoadedClasses();`        `FileOutputStream fileOutputStream = new FileOutputStream(new File("/tmp/classesInfo"));`        `for (Class aClass : classes) {`            `String result = "class ==> " + aClass.getName() + "\n\t" + "Modifiable ==> " + (inst.isModifiableClass(aClass) ? "true" : "false") + "\n";`            `fileOutputStream.write(result.getBytes());`        `}`        `fileOutputStream.close();`    `}``}`

重新attach到某个JVM,在/tmp/classesInfo文件中有如下信息:

`class ==> java.lang.invoke.LambdaForm$MH/0x0000000800f06c40        ``    Modifiable ==> false``   ``class ==> java.lang.invoke.LambdaForm$DMH/0x0000000800f06840        ``    Modifiable ==> false``   ``class ==> java.lang.invoke.LambdaForm$DMH/0x0000000800f07440        ``    Modifiable ==> false``   ``class ==> java.lang.invoke.LambdaForm$DMH/0x0000000800f07040        ``    Modifiable ==> false``   ``class ==> jdk.internal.reflect.GeneratedConstructorAccessor29        ``    Modifiable ==> true``   ``........`

得到了目标JVM上所有已经加载的类,并且知道了这些类能否被修改。

至于如何修改JVM上的字节码,请听下回分解。

参考资料

1.[JVM源码分析之javaagent原理完全解读](https://developer.aliyun.com/article/2946)

2.[javaagent使用指南]

(https://www.cnblogs.com/rickiyang/p/11368932.html)

3.[Java Agent基本简介和使用]

(https://www.jianshu.com/p/de6bde2e30a2)

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2