0x01 前言
最近在学习java agent,以java agent注入内存马为例,在原文:https://xz.aliyun.com/t/9450基础上稍作调整。
0x01 正文
在 jdk 1.5 之后引入了 java.lang.instrument 包,该包提供了检测 java 程序的 Api,比如用于监控、收集性能信息、诊断问题,通过 java.lang.instrument 实现的工具我们称之为 Java Agent ,Java Agent 能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法。
Java agent的使用方式有两种:
-
实现 premain
方法,在JVM启动前加载。
-
实现 agentmain
方法,在JVM启动后加载。
premain和agentmain函数声明如下,方法名相同情况下,拥有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) {
...
}
JVM 会优先加载带 Instrumentation
签名的方法,加载成功则忽略第二种。如果第一种没有,则加载第二种方法。
-
第一个参数 String agentArgs
就是Java agent的参数。 -
Inst
是一个java.lang.instrument.Instrumentation
的实例,可以用来类定义的转换和操作等等。
premain方式
JVM启动时 会先执行 premain
方法,大部分类加载都会通过该方法,注意:是大部分,不是所有。遗漏的主要是系统类,因为很多系统类先于 agent 执行,而用户类的加载肯定是会被拦截的。也就是说,这个方法是在 main 方法启动前拦截大部分类的加载活动,既然可以拦截类的加载,就可以结合第三方的字节码编译工具,比如ASM,javassist,cglib等等来改写实现类。
1)创建应用程序Task.jar
public class Task {
public static void main (String[] args) {
System.out.println("task mian run");
}
}
将Task打包成Task.jar后单独执行java -jar Task.jar
2)创建premain方式的Agent
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!!!");
}
}
}
此时项目如果打包成jar包执行,则会因绝少入口main而报错(Java默认main为入口)。故需自定义一个MANIFEST.MF
文件,用于指明premain
的入口:
Manifest-Version: 1.0
Premain-Class: com.test.PreDemo
注:最后一行是空行,不能省略。以下是MANIFEST.MF的其他选项:
Premain-Class: 包含 premain 方法的类(类的全路径名)
Agent-Class: 包含 agentmain 方法的类(类的全路径名)
Boot-Class-Path: 设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件语法。如果该路径以斜杠字符(“/”)开头,则为绝对路径,否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。如果代理是在 VM 启动之后某一时刻启动的,则忽略不表示 JAR 文件的路径。(可选)
Can-Redefine-Classes: true表示能重定义此代理所需的类,默认值为 false(可选)
Can-Retransform-Classes: true 表示能重转换此代理所需的类,默认值为 false (可选)
Can-Set-Native-Method-Prefix: true表示能设置此代理所需的本机方法前缀,默认值为 false(可选)
3)使用premain进行注入
java -javaagent:PreDemo.jar -jar Task.jar
agentmain方式
写一个agentmain
和premain
差不多,只需要在META-INF/MANIFEST.MF
中加入Agent-Class:
即可。
Manifest-Version: 1.0
Agent-Class: com.test.AgentDemo
不同的是,这种方法不是通过JVM启动前的参数来指定的,官方为了实现启动后加载,提供了Attach API
。Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach
包里面。着重关注的是VitualMachine
这个类。
VirtualMachine
字面意义表示虚拟机,也就是Agent程序需要监控的目标JVM。它提供了获取系统信息、loadAgent
,Attach
和Detach
等方法,可以实现的功能非常强大 。该类允许我们给attach方法传入一个JVM的pid,远程连接到目标JVM上 。代理类注入操作只是它众多功能中的一个,我们可以通过loadAgent
方法向JVM注册一个代理程序Agent,在该Agent代理程序中将会得到一个Instrumentation
实例。
VirtualMachine的用法:
// 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分离。
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:
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!!!");
}
}
}
Instrumentation
agentmain通过instrumentation类和目标JVM进行交互,从而达到修改数据的效果。
官方文档:java.lang.instrument (Java SE 9 & JDK 9 ) (oracle.com)
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();
......
}
-
getAllLoadedClasses
:获取所有已经加载的类。 -
isModifiableClasses
:判断某个类是否能被修改。
agentmain:
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() + "nt" + "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上所有已经加载的类,并且知道了这些类能否被修改。
篡改Class的字节码:
-
addTransformer()
-
retransformClasses()
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();
......
}
在addTransformer()
方法中,有一个参数ClassFileTransformer transformer
,这个参数将帮助我们完成字节码的修改工作。
ClassFileTransformer
// 代理使用addTransformer方法注册此接口的实现,以便在加载,重新定义或重新转换类时调用转换器的transform方法。该实现应覆盖此处定义的转换方法之一。在Java虚拟机定义类之前,将调用变压器。
// 有两种转换器,由Instrumentation.addTransformer(ClassFileTransformer,boolean)的canRetransform参数确定:
// 与canRetransform一起添加的具有重转换能力的转换器为true
// 与canRetransform一起添加为false或在Instrumentation.addTransformer(ClassFileTransformer)处添加的无法重新转换的转换器
// 在addTransformer中注册了转换器后,将为每个新的类定义和每个类重新定义调用该转换器。具有重转换功能的转换器也将在每个类的重转换上被调用。使用ClassLoader.defineClass或其本机等效项来请求新的类定义。使用Instrumentation.redefineClasses或其本机等效项进行类重新定义的请求。使用Instrumentation.retransformClasses或其本机等效项进行类重新转换的请求。在验证或应用类文件字节之前,将在处理请求期间调用转换器。如果有多个转换器,则通过链接转换调用来构成转换。也就是说,一次转换所返回的字节数组成为转换的输入(通过classfileBuffer参数)。
示例:
// HelloWorld.java
public class HelloWorld {
public static void main(String[] args) {
hello h1 = new hello();
h1.hello();
// 输出当前进程的 pid
System.out.println("pid ==> " + [pid])
// 产生中断,等待注入
Scanner sc = new Scanner(System.in);
sc.nextInt();
hello h2 = new hello();
h2.hello();
System.out.println("ends...");
}
}
// hello.java
public class hello {
public void hello() {
System.out.println("hello world");
}
}
// AgentDemo.java
public class AgentDemo {
public static void agentmain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException {
Class[] classes = inst.getAllLoadedClasses();
// 判断类是否已经加载
for (Class aClass : classes) {
if (aClass.getName().equals(TransformerDemo.editClassName)) {
// 添加 Transformer
inst.addTransformer(new TransformerDemo(), true);
// 触发 Transformer
inst.retransformClasses(aClass);
}
}
}
}
// TransformerDemo.java
// 如果在使用过程中找不到javassist包中的类,那么可以使用URLCLassLoader+反射的方式调用
public class TransformerDemo implements ClassFileTransformer {
// 只需要修改这里就能修改别的函数
public static final String editClassName = "com.xxxx.hello.hello";
public static final String editClassName2 = editClassName.replace('.', '/');
public static final String editMethod = "hello";
@Override
public byte[] transform(...) throws IllegalClassFormatException {
try {
ClassPool cp = ClassPool.getDefault();
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
cp.insertClassPath(ccp);
}
CtClass ctc = cp.get(editClassName);
CtMethod method = ctc.getDeclaredMethod(editMethodName);
String source = "{System.out.println("hello transformer");}";
method.setBody(source);
byte[] bytes = ctc.toBytes();
ctc.detach();
return bytes;
} catch (Exception e){
e.printStackTrace();
}
return null;
}
}
总的来说就是利用attach.java attach到某个JVM,再利用angentdemo.java中的java.lang.instrument.Instrumentation对目标JVM中的.class进行汇总和筛选、添加(inst.addTransformer)并触发(inst.retransformClasses)Transformer,然后利用TransformerDemo.java修改字节码完成注入。
0x02 相关学习资源
filter内存马
-
中间件内存马注入&冰蝎连接(附更改部分代码) -
filter内存马技术!_localhost01-CSDN博客 -
基于Tomcat无文件Webshell研究 -
Tomcat内存马学习 一 -
Tomcat 内存马学习(二):结合反序列化注入内存马 - 木头的小屋 -
Java安全之基于Tomcat实现内存马 - nice_0e3 - 博客园
servlet内存马
listener型
-
Tomcat下基于Listener的内存Webshell分析
Spring controller内存马
-
基于内存 Webshell 的无文件攻击技术研究 - 安全客,安全资讯平台
Spring Interceptor内存马
-
利用 intercetor 注入 spring 内存 webshell
其它前提研究-获取request对象
-
基于tomcat的内存 Webshell 无文件攻击技术 -
Tomcat中一种半通用回显方法
Weblogic注入内存马
-
中间件内存马注入&冰蝎连接(附更改部分代码) -
weblogic 无文件webshell的技术研究
java agent内存马
-
利用“进程注入”实现无文件复活 WebShell
内存马查杀
-
Tomcat 内存马检测 -
查杀Java web filter型内存马 | 回忆飘如雪 -
Filter/Servlet型内存马的扫描抓捕与查杀 | 回忆飘如雪 -
基于javaAgent内存马检测查杀指南-华盟网 -
https://github.com/alibaba/arthas -
https://github.com/LandGrey/copagent -
https://github.com/c0ny1/java-memshell-scanner -
https://syst1m.com/post/memory-webshell/
汇总:GitHub - bitterzzZZ/MemoryShellLearn: 分享几个直接可用的内存马,记录一下学习过程中看过的文章
原文始发于微信公众号(XK Team):内存马学习(文末附部分学习资源链接)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论