这项技术还是很值得学习的,尽管做不到像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 anagentmain
method for use when the agent is started after VM startup. When the agent is started using a command-line option, theagentmain
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 staticpremain
method similar in principle to themain
application entry point. After the Java Virtual Machine (JVM) has initialized, eachpremain
method will be called in the order the agents were specified, then the real applicationmain
method will be called. Eachpremain
method must return in order for the startup sequence to proceed.
因此简单来说,我们只需要实现一个public static void premain(String agentArgs, Instrumentation inst)
,并且将其所在类加入META-INF/MANIFEST.MF
的Premain-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 theagentmain
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 anpremain
method for use when the agent is started using a command-line option. When the agent is started after VM startup thepremain
method is not invoked.
连同上面的premain
一起看,这里实际方法签名里都有String, Instrumentation
,Instrumentation
将被自动传递,而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自动传递给Agentmain
或Premain
方法,接口具体的方法参考文档在: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. SeeClassFileTransformer.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.
这个方法用于向系统注册类转换器,这里只需要注意它的三个调用时机,即load
、redefined
、retransform
。这里最方便的自然是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
推荐使用Gradle
的Shadow 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);
}
}
坑点
- 注意不可以在
transform
时对类进行新方法成员的添加,否则将造成异常,无法完成转换。 - 如果使用
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