【安全研究】JavaAgent技术在内存马中的应用

2022-03-04 13,720


JavaAgent技术简介

JDK1.5开始引入了Agent机制(即启动java程序时添加“-javaagent”参数,Java Agent机制允许用户在JVM加载class文件的时候先加载自己编写的Agent文件,通过修改JVM传入的字节码来实现注入自定义的代码采用这种方式时,必须在容器启动时添加jvm参数,所以需要重启Web容器。


JDK1.6新增了attach方式,可以对运行中的java进程附加agent,提供了动态修改运行中已经被加载的类的途径。一般通过VirtualMachine的attach(pid)方法获得VirtualMachine实例,随后调用loadagent方法将JavaAgent的jar包加载到目标JVM中。


下面一个章节笔者将通过两个demo案例说明JavaAgent技术的两种方式,让读者明白premain和agentmain的具体原理。


JavaAgent两种方式

1Premain

创建一个sayHello类,写一个say()方法。


public class sayHello {

    public String say() {

        return "hello,world!";

    }

}

创建一个People类,运行say()方法,输出结果为:hello,world!

public class People {

    public static void main(String[] args) {

        System.out.println(new sayHello().say());

    }

}

创建Transformer重写transformer方法,实现修改传入JVM的字节码。笔者这里通过javassist对类字节码进行处理。

package org.example;


import javassist.*;

import java.io.IOException;

import java.lang.instrument.ClassFileTransformer;

import java.lang.instrument.IllegalClassFormatException;

import java.security.ProtectionDomain;


public class Transformer implements ClassFileTransformer {

    @Override

    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

        System.out.println(className);

        if (className.endsWith("sayHello")){

            try {

                final ClassPool classPool = ClassPool.getDefault(); // 创建ClassPool对象

                final CtClass ctClass = classPool.get("org.example.sayHello");

                CtMethod ctMethod = ctClass.getDeclaredMethod("say"); // 获取成员方法

                String methodBody = "return \"hello premain\";";

                ctMethod.setBody(methodBody); //替换方法体中所有内容

                byte[] bytes = ctClass.toBytecode(); //使用类CtClass,生成类二进制

                //调用CtClass对象的detach()方法 CtClass对象从ClassPool移除掉减少内存消耗

                ctClass.detach();

                return bytes;

            } catch (NotFoundException e) {

                e.printStackTrace();

            } catch (CannotCompileException e) {

                e.printStackTrace();

            } catch (IOException e) {

                e.printStackTrace();

            }

        }

        return null;

    }

}

定义Premain类的premain方法

package org.example;

import java.lang.instrument.Instrumentation;

public class Premain {

    public static void premain(String agentArgs, Instrumentation inst){

        System.out.println("premain agent run!");

        inst.addTransformer(new Transformer());

    }

}

使用Maven打包成TestPremain-1.0-SNAPSHOT.jar文件,需要如下修改pom.xml文件。<Premain-class>设置为premain方法所在类。

<plugin>

  <artifactId>maven-jar-plugin</artifactId>

  <version>3.0.2</version>

  <configuration>

  <archive>

  <manifestEntries>

  <Premain-class>org.example.Premain</Premain-class>

  <Can-Redefine-Classes>true</Can-Redefine-Classes>

  <Can-Retransform-Classes>true</Can-Retransform-Classes>

  </manifestEntries>

  </archive>

  </configuration>

</plugin>

在运行配置中添加vm选项

G87FV6fW_4vqp.png

图1

运行结果如图2所示,修改了say方法。

fqFzKDWH_X8zk.png

图2

2Agentmain

同premain也创建一个People类循环打印字符串,代码如下所示。

package org.example;


public class People {

    public void sayHello(String name) {

        System.out.println(String.format("%s say hello!", name));

    }


    public static void main(String[] args) throws InterruptedException {

        People p = new People();

        for (;;){

            Thread.sleep(1000);

            p.sayHello(Thread.currentThread().getName());

        }

    }

}

重写transform方法,注入进程后打印输出代理的类,代码如下所示。

package org.example;


import java.lang.instrument.ClassFileTransformer;

import java.lang.instrument.IllegalClassFormatException;

import java.security.ProtectionDomain;


public class Transform implements ClassFileTransformer {

    @Override

    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

        System.out.println(String.format("agent run target class= %s", className));

        return classfileBuffer;

    }

}

新建Agent类实现agentmain方法,代码如下所示

public class Agent {

    public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {

        inst.addTransformer(new Transform(),true);

        inst.retransformClasses(Class.forName("org.example.People"));

    }

}

Agent设置为<Agent-Class>并打包成为jar文件。Pom.xml文件如下所示,值得注意的是如果需要修改已经被JVM加载过的类的字节码,那么还需要在MANIFEST.MF中添加Can-Retransform-Classes:true或Can-Redefine-Classes:true。

<Agent-Class>org.example.Agent</Agent-Class>

<Can-Retransform-Classes>true</Can-Retransform-Classes>

创建Attach类注入目标类的进程,代码如下所示。

public class Attach {

    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {

        String agentPath = "F:\\IdeaProjects\\TestAgent\\target\\TestAgent-1.0-SNAPSHOT.jar";

        List<VirtualMachineDescriptor> list = VirtualMachine.list(); //获取本机所有运行的Java进程

        for (VirtualMachineDescriptor desc :list){

            if (desc.displayName().endsWith("People")){

                VirtualMachine vm = VirtualMachine.attach(desc.id());

                vm.loadAgent(agentPath);

                vm.detach();

            }

        }

    }

}

Attach捕获到类进程号如图3所示。

EZ94SKDZ_OnME.png 

3


先运行people类在运行attach,运行结果如图4所示。

Zaw52RET_Z32r.png

4


分析JavaAgent型内存马

由上文可知Agentmain实现最重要的三个类Agent Attach Transform,来分析冰蝎作者之前写的memshell实现原理,项目地址:https://github.com/rebeyond/memShell.gitMemshell中Transform类代码如图5所示。

COh3Lh2k_mEqe.png

5

不同于前面章节的demo,这里除了使用ClassPool.getDefault()还使用ClassClassPath搜索class路径其原理ClassPool.getDefault()获取的ClassPool使用JVM的classpath在Tomcat等Web服务器运行时,服务器会使用多个类加载器作为系统类加载器,这可能导致ClassPool可能无法找到用户的类这时,ClassPool须添加额外的classpath才能搜索到用户的类

CtClass cc = cp.get("org.apache.catalina.core.ApplicationFilterChain");

CtMethod m = cc.getDeclaredMethod("internalDoFilter");

m.addLocalVariable("elapsedTime", CtClass.longType);

m.insertBefore(readSource());

如上代码:作者Hook了ApplicationFilterChain中的internalDoFilter方法,然后定义一个long类型的属性,elapsedTime并通过insertBefore方法将source.txt中内容插入到方法内容的开始处source.txt是url参数和agent交互的逻辑,如图6所示。

vEkzd47B_udch.png

6


笔者之前用此内存马时发现两个特点第一是该内存马会自己删除jar包,实现代码如下。

V2I8gRb9_wCv2.png

7


第二点是重启tomcat服务之后内存马还是存在,只有通过jps-l kill掉进程后启动服务才能删除内存马原理是使用了ShutdownHook机制。

deW8QeSA_qfKF.png

8


通过使用Runtime.addShutdownHook(Thread hook)方法注册JVM关闭的勾子,调用writeFiles方法把jar包落地磁盘再通过Runtime.exec启动java-jar inject.jar


由于Hook的关键函数ApplicationFilterChain.internalDoFilter是tomcat的方法导致其他中间件不适用,在冰蝎3.0中的内存马作者更改了Hook点。(源码版本为V3.0 Beta11_t00ls)在agentmain中做了一个判断,如果是Tomcat选择hook javax.servlet.http.HttpServlet中的service方法,如果是weblogic选择hookweblogic.servlet.internal.ServletStubImpl中的execute方法。


代码如图9所示。

gLeWaLaH_tePs.png

9


在jdk9及以后的版本不允许SelfAttach(即无法attach自身的进程)。修改前面章节Attach demo,将jdk换成9之后的,attach自身的PID会报错提示Can not attach to current VM。代码如下,报错截图如图10所示。

public class Attach {

    public static void main(String[] args) throws Exception {

        List<VirtualMachineDescriptor> list = VirtualMachine.list();

        for(VirtualMachineDescriptor desc : list){

            System.out.println("进程ID:" + desc.id() + ",进程名称:" + desc.displayName());

        }

        Scanner myObj = new Scanner(System.in);

        System.out.println("输入要注入的进程:");

        String pid = myObj.nextLine();

        String agentPath = "F:\\IdeaProjects\\TestAgent\\target\\TestAgent-1.0-SNAPSHOT.jar";

        VirtualMachine vm = VirtualMachine.attach(pid);

        vm.loadAgent(agentPath);

        vm.detach();

    }


}

4WhQHz9R_w3ab.png

10


看到Rebeyond师傅在《Java内存攻击技术漫谈》中提出一种方法,绕过allowAttachSelf。首先Debug attch执行流程,如图11所示。可以发现attach的时候会创建一个HotSpotVirtualMachine的父类对象,取键值对jdk.attach.allowAttachSelf的值计算后保存到ALLOW_ATTACH_SELF可通过反射修改该属性值。


McSzCgTd_QGCZ.png

图11


ALLOW_ATTACH_SELF字段有final修饰符,需要设置setAccessible(true)具体代码如下所示。

  Class cls=Class.forName("sun.tools.attach.HotSpotVirtualMachine");

        Field field=cls.getDeclaredField("ALLOW_ATTACH_SELF");

        field.setAccessible(true);

        Field modifiersField=Field.class.getDeclaredField("modifiers");

        modifiersField.setAccessible(true);

        modifiersField.setInt(field,field.getModifiers()&~Modifier.FINAL);

        field.setBoolean(null,true);

修改后会弹出警告信息如图12所示,成功注入结果如图13所示。

KXe99kOy_EXGt.png

图12


JaCnSTrd_MXWB.png

图13


回到冰蝎3.0源码中,通过setPropertyjdk.attach.allowAttachSelf设置为true,实现绕过SelfAttach

System.setProperty("jdk.attach.allowAttachSelf", "true");


总结

本文从permain和agentmain两种实现JavaAgent的原理方法引入到java agent在内存马中的应用,通过分析memshell到冰蝎3.0内存马源码,加深了对agent型内存马Hook的关键函数持久化方法以及绕过SelfAttach方法等内存马技术点的理解与学习,希望对读者有帮助



本文作者:安全狗

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/174266.html

Tags:
评论  (0)
快来写下你的想法吧!

安全狗

文章数:32 积分: 180

基于智能驱动的新一代云安全公司

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号