这项技术还是很值得学习的,尽管做不到像rebeyond师傅那样去做到内存Agent加载。

Java Agent技术

自Java 1.5后,Java Agent就被引入,这项技术允许在运行时修改字节码,主要的场景是应用于性能分析、内存分析等;而由于其可动态修改字节码的机制,也就使得我们可以对任意的类任意的方法进行Hook。

而总的来说,Java Agent有两种方式:

  • premain
  • agentmain

Premain

premain如其名,在JVM启动时时,进入main方法之前进行调用,这个适合于附加至尚未启动的Java程序使用。

具体的解释在:java.lang.instrument (Java Platform SE 6)

其中描述了premain方法签名的解释如下:

The premain method has one of two possible signatures. The JVM first attempts to invoke the following method on the agent class: public static void premain(String agentArgs, Instrumentation inst); If the agent class does not implement this method then the JVM will attempt to invoke: public static void premain(String agentArgs); The agent class may also have an agentmain method for use when the agent is started after VM startup. When the agent is started using a command-line option, the agentmain method is not invoked.

而这个Jar也是有要求的:

The manifest of the agent JAR file must contain the attribute Premain-Class. The value of this attribute is the name of the agent class. The agent class must implement a public static premain method similar in principle to the main application entry point. After the Java Virtual Machine (JVM) has initialized, each premain method will be called in the order the agents were specified, then the real application main method will be called. Each premain method must return in order for the startup sequence to proceed.

因此简单来说,我们只需要实现一个public static void premain(String agentArgs, Instrumentation inst),并且将其所在类加入META-INF/MANIFEST.MFPremain-Class属性即可。

然而对于攻防场景下来说,一般Java程序都已经启动了,并不太适合攻防利用场景(但是可以考虑攻防的持久化利用场景)。

Agentmain

agentmain则是可以在JVM启动后进行调用的方法,适合于已经启动的Java程序使用。

具体的解释同样也是在:java.lang.instrument (Java Platform SE 6)

描述agentmain方法的签名如下:

The agent JAR is appended to the system class path. This is the class loader that typically loads the class containing the application main method. The agent class is loaded and the JVM attempts to invoke the agentmain method. The JVM first attempts to invoke the following method on the agent class: public static void agentmain(String agentArgs, Instrumentation inst); If the agent class does not implement this method then the JVM will attempt to invoke: public static void agentmain(String agentArgs); The agent class may also have an premain method for use when the agent is started using a command-line option. When the agent is started after VM startup the premain method is not invoked.

连同上面的premain一起看,这里实际方法签名里都有String, InstrumentationInstrumentation将被自动传递,而String的名字叫agentArgs,显然是给agent调用的方法。

在文档中也提到:

The agent is passed its agent options via the agentArgs parameter. The agent options are passed as a single string, any additional parsing should be performed by the agent itself.

说明agent也是可以有参数的,并且是可以传递的。

Agentmain可以在已经启动的JVM上附加,适合注入内存马,因此Agent型内存马将使用该技术实现。

可卸载性

Java Agent尽管可以从另一个进程附加到当前JVM上并且动态执行一部分代码:

---
title: Agent示意图
---
stateDiagram-v2

    [*] --> AgentJVM
    state AgentJVM {
        [*] --> AgentJVM_Startup
        AgentJVM_Startup: JVM Startup
        AgentJVM_Startup --> Attach
        Attach --> JVM_Running
        JVM_Running -->  LoadAgent
        LoadAgent: Load Agent (Path and Args)
        LoadAgent --> [*]
    }
    [*] --> JVM
    state JVM {
        [*] --> JVM_Startup
        JVM_Startup: JVM Startup
        JVM_Startup --> JVM_StartAgent
        JVM_StartAgent: JVM Agent(premain) Running
        JVM_StartAgent --> JVM_Running
        JVM_Running: JVM Running
        JVM_Running --> AgentRunning
        AgentRunning: Agent Jar Running
        AgentRunning --> [*]
        JVM_Running --> [*]
        
    }

从上面的示意图可以看出,如果希望Agent是可卸载的,是不现实的;由于一次Attach,目标JVM只会执行一次代码,这是个一次性操作。

但是在红队实战场景中,如果遇到重要业务无法进行系统重启,即JVM不得间断,那么附加的Agent如何卸载,是一个必须要思考的问题。

尽管Agent附加是一个一次性操作,但是我们可以通过多次附加,多次执行agentmain方法,来尝试在目标JVM中多次执行我们的代码;利用这一性质,配合Agent Args,我们可以在附加时传递参数,即卸载安装,来动态控制Agent附加后的影响;对于安装操作,则通过Java代码造成指定效果,对于卸载操作,则恢复原样。

Agent马

开始前必须要对Instrument的相关API有所了解。

Instrument

java.lang.instrument是Java里的一个标准包,可用于Java运行时代码注入;下面是核心的接口:

  • Instrumentation
  • ClassFileTransformer

其中Instrumentation会由JVM自动传递给AgentmainPremain方法,接口具体的方法参考文档在:Instrumentation (Java Platform SE 6)

addTransformer

具体在Agent马中需要使用的是:addTransformer(ClassFileTransformer transformer, boolean canRetransform),参考文档:Instrumentation (Java Platform SE 6)#addTransformer(java.lang.instrument.ClassFileTransformer, boolean)

文档里的解释是:

Registers the supplied transformer. All future class definitions will be seen by the transformer, except definitions of classes upon which any registered transformer is dependent. The transformer is called when classes are loaded, when they are redefined. and if canRetransform is true, when they are retransformed. See ClassFileTransformer.transform for the order of transform calls. If a transformer throws an exception during execution, the JVM will still call the other registered transformers in order. The same transformer may be added more than once, but it is strongly discouraged — avoid this by creating a new instance of tranformer class.

这个方法用于向系统注册类转换器,这里只需要注意它的三个调用时机,即loadredefinedretransform。这里最方便的自然是retransform,文档里提到,如果传入的canRetransform为真,那么当一个类被retransform时,就会将所有的类定义经过transformer得到最终的类定义,值得注意的是,这里如果transformer中出异常,transformer也会继续下去,并不会直接返回。

ClassFileTransformer

而系统转换器接口只有一个方法需要实现:

 byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)

这个方法用于对类进行转换,其中ClassLoader是类的加载器;className则是类名,注意类名并非是a.b.c这样的标准package命名,而是a/b/c这样的;protectionDomain不具体介绍;classfileBuffer则是原本的类字节码。

实现

代码已放至:GitHub - evalexp/AMI: Java Agent Memshell Injector

src目录已设置为私有项目,具体源码不公开,防止被杀。

Manifest

推荐使用GradleShadow Jar,配置可以如下:

plugins {
    id("java")
    id("com.github.johnrengelman.shadow") version "8.1.1"
}
// ...
tasks.jar {
    manifest {
        attributes["Main-Class"] = mainClass
        attributes["Agent-Class"] = mainClass
        attributes["Can-Redefine-Classes"] = "true"
        attributes["Can-Retransform-Classes"] = "true"
        attributes["Implementation-Version"] = archiveVersion
    }
}

可卸载性实现

在注册的ClassFileTransformer中添加启用标志即可:

public class CoreTransformer implements ClassFileTransformer {
    private boolean enable = true;
    // ... 
 
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        className = className.replace('/', '.');
        if (this.enable) {
            // transform here
        }
 
        return classfileBuffer;
    }
    
    /**
     * enable core transformer
     */
    public void enable() {
        this.enable = true;
    }
 
    /**
     * disable core transformer
     */
    public void disable() {
        this.enable = false;
    }
}

触发

可以主动触发,调用instrumentation.restansformClass(Class clazz)即可。

那么将此方法可以考虑放到CoreTransformer中:

public class CoreTransformer implements ClassFileTransformer {
	public void retransform(Class clazz) {
		this.instrumentation.retransformClasses(clazz);
	}
}

坑点

  1. 注意不可以transform时对类进行新方法成员的添加,否则将造成异常,无法完成转换。
  2. 如果使用javassist进行hook代码的添加,不要在hook的代码中通过反射调用defineClass方法,会产生java.lang.VerifyError

Docker下失败

在Docker中可能出现无法Attach的情况,自己模拟即可:

# Template
touch /proc/${PID}/cwd/.attach_pid${PID}
kill -SIGQUIT ${PID}

例如PID为1时,如下:

touch /proc/1/cwd/.attach_pid1
kill -SIGQUIT 1