内存马学习(文末附部分学习资源链接)

admin 2022年4月10日00:23:35评论75 views字数 11194阅读37分18秒阅读模式

0x01 前言

最近在学习java agent,以java agent注入内存马为例,在原文:https://xz.aliyun.com/t/9450基础上稍作调整。

0x01 正文

在 jdk 1.5 之后引入了 java.lang.instrument 包,该包提供了检测 java 程序的 Api,比如用于监控、收集性能信息、诊断问题,通过 java.lang.instrument 实现的工具我们称之为 Java Agent ,Java Agent 能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法。

Java agent的使用方式有两种:

  • 实现premain方法,在JVM启动前加载。
内存马学习(文末附部分学习资源链接)
image-20220107135507880
  • 实现agentmain方法,在JVM启动后加载。
内存马学习(文末附部分学习资源链接)
image-20220107135538398

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) {
    ...
}

JVM 会优先加载带 Instrumentation 签名的方法,加载成功则忽略第二种。如果第一种没有,则加载第二种方法。

  • 第一个参数String agentArgs就是Java agent的参数。
  • Inst 是一个 java.lang.instrument.Instrumentation 的实例,可以用来类定义的转换和操作等等。

premain方式

JVM启动时 会先执行 premain 方法,大部分类加载都会通过该方法,注意:是大部分,不是所有。遗漏的主要是系统类,因为很多系统类先于 agent 执行,而用户类的加载肯定是会被拦截的。也就是说,这个方法是在 main 方法启动前拦截大部分类的加载活动,既然可以拦截类的加载,就可以结合第三方的字节码编译工具,比如ASM,javassist,cglib等等来改写实现类。

1)创建应用程序Task.jar

public class Task {

    public static void main (String[] args) {
        System.out.println("task mian run");
    }
}

将Task打包成Task.jar后单独执行java -jar Task.jar

2)创建premain方式的Agent

import java.lang.instrument.Instrumentation;

public class PreDemo {
    public static void premain(String args, Instrumentation inst) throws Exception{
        for (int i = 0; i < 10; i++) {
            System.out.println("hello I'm premain agent!!!");
        }
    }
}

此时项目如果打包成jar包执行,则会因绝少入口main而报错(Java默认main为入口)。故需自定义一个MANIFEST.MF文件,用于指明premain的入口:

Manifest-Version: 1.0
Premain-Class: com.test.PreDemo

注:最后一行是空行,不能省略。以下是MANIFEST.MF的其他选项:

Premain-Class: 包含 premain 方法的类(类的全路径名)
Agent-Class: 包含 agentmain 方法的类(类的全路径名)
Boot-Class-Path: 设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件语法。如果该路径以斜杠字符(“/”)开头,则为绝对路径,否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。如果代理是在 VM 启动之后某一时刻启动的,则忽略不表示 JAR 文件的路径。(可选)
Can-Redefine-Classes: true表示能重定义此代理所需的类,默认值为 false(可选)
Can-Retransform-Classes: true 表示能重转换此代理所需的类,默认值为 false (可选)
Can-Set-Native-Method-Prefix: true表示能设置此代理所需的本机方法前缀,默认值为 false(可选)

3)使用premain进行注入

java -javaagent:PreDemo.jar -jar Task.jar

agentmain方式

写一个agentmainpremain差不多,只需要在META-INF/MANIFEST.MF中加入Agent-Class:即可。

Manifest-Version: 1.0
Agent-Class: com.test.AgentDemo

不同的是,这种方法不是通过JVM启动前的参数来指定的,官方为了实现启动后加载,提供了Attach API。Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach 包里面。着重关注的是VitualMachine这个类。

VirtualMachine

字面意义表示虚拟机,也就是Agent程序需要监控的目标JVM。它提供了获取系统信息、loadAgentAttachDetach等方法,可以实现的功能非常强大 。该类允许我们给attach方法传入一个JVM的pid,远程连接到目标JVM上 。代理类注入操作只是它众多功能中的一个,我们可以通过loadAgent方法向JVM注册一个代理程序Agent,在该Agent代理程序中将会得到一个Instrumentation实例。

VirtualMachine的用法:

// com.sun.tools.attach.VirtualMachine

// 下面的示例演示如何使用VirtualMachine:

        // attach to target VM
        VirtualMachine vm = VirtualMachine.attach("2177");  
        // start management agent
        Properties props = new Properties();
        props.put("com.sun.management.jmxremote.port""5000");
        vm.startManagementAgent(props);
        // detach
        vm.detach();

// 在此示例中,我们附加到由进程标识符2177标识的Java虚拟机。然后,使用提供的参数在目标进程中启动JMX管理代理。最后,客户端从目标VM分离。

attacher:

import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;

import java.io.IOException;

public class AgentMain {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        String id = args[0];
        String jarName = args[1];

        System.out.println("id ==> " + id);
        System.out.println("jarName ==> " + jarName);

        VirtualMachine virtualMachine = VirtualMachine.attach(id);
        virtualMachine.loadAgent(jarName);
        virtualMachine.detach();

        System.out.println("ends");
    }
}

过程非常简单:通过pid attach到目标JVM -> 加载agent -> 解除连接。

agentmain:

import java.lang.instrument.Instrumentation;

public class AgentDemo {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        for (int i = 0; i < 10; i++) {
            System.out.println("hello I'm agentMain!!!");
        }
    }
}

Instrumentation

agentmain通过instrumentation类和目标JVM进行交互,从而达到修改数据的效果。

官方文档:java.lang.instrument (Java SE 9 & JDK 9 ) (oracle.com)

public interface Instrumentation {

    // 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
    void addTransformer(ClassFileTransformer transformer);

    // 删除一个类转换器
    boolean removeTransformer(ClassFileTransformer transformer);

    // 在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

    // 判断目标类是否能够修改。
    boolean isModifiableClass(Class<?> theClass);

    // 获取目标已经加载的类。
    @SuppressWarnings("rawtypes")
    Class[] getAllLoadedClasses();

    ......
}
  • getAllLoadedClasses:获取所有已经加载的类。
  • isModifiableClasses:判断某个类是否能被修改。

agentmain:

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.instrument.Instrumentation;

public class AgentDemo {
    public static void agentmain(String agentArgs, Instrumentation inst) throws IOException {
        Class[] classes = inst.getAllLoadedClasses();
        FileOutputStream fileOutputStream = new FileOutputStream(new File("/tmp/classesInfo"));
        for (Class aClass : classes) {
            String result = "class ==> " + aClass.getName() + "nt" + "Modifiable ==> " + (inst.isModifiableClass(aClass) ? "true" : "false") + "n";
            fileOutputStream.write(result.getBytes());
        }
        fileOutputStream.close();
    }
}

重新attach到某个JVM,在/tmp/classesInfo文件中有如下信息:

class ==> java.lang.invoke.LambdaForm$MH/0x0000000800f06c40             
    Modifiable ==> false

class ==> java.lang.invoke.LambdaForm$DMH/0x0000000800f06840                
    Modifiable ==> false

class ==> java.lang.invoke.LambdaForm$DMH/0x0000000800f07440                
    Modifiable ==> false

class ==> java.lang.invoke.LambdaForm$DMH/0x0000000800f07040                
    Modifiable ==> false

class ==> jdk.internal.reflect.GeneratedConstructorAccessor29               
    Modifiable ==> true

........

得到了目标JVM上所有已经加载的类,并且知道了这些类能否被修改。

篡改Class的字节码:

  • addTransformer()

  • retransformClasses()

public interface Instrumentation {

    // 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
    void addTransformer(ClassFileTransformer transformer);

    // 删除一个类转换器
    boolean removeTransformer(ClassFileTransformer transformer);

    // 在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

    // 判断目标类是否能够修改。
    boolean isModifiableClass(Class<?> theClass);

    // 获取目标已经加载的类。
    @SuppressWarnings("rawtypes")
    Class[] getAllLoadedClasses();

    ......
}

addTransformer()方法中,有一个参数ClassFileTransformer transformer,这个参数将帮助我们完成字节码的修改工作。

ClassFileTransformer

// 代理使用addTransformer方法注册此接口的实现,以便在加载,重新定义或重新转换类时调用转换器的transform方法。该实现应覆盖此处定义的转换方法之一。在Java虚拟机定义类之前,将调用变压器。
// 有两种转换器,由Instrumentation.addTransformer(ClassFileTransformer,boolean)的canRetransform参数确定:
// 与canRetransform一起添加的具有重转换能力的转换器为true
// 与canRetransform一起添加为false或在Instrumentation.addTransformer(ClassFileTransformer)处添加的无法重新转换的转换器
// 在addTransformer中注册了转换器后,将为每个新的类定义和每个类重新定义调用该转换器。具有重转换功能的转换器也将在每个类的重转换上被调用。使用ClassLoader.defineClass或其本机等效项来请求新的类定义。使用Instrumentation.redefineClasses或其本机等效项进行类重新定义的请求。使用Instrumentation.retransformClasses或其本机等效项进行类重新转换的请求。在验证或应用类文件字节之前,将在处理请求期间调用转换器。如果有多个转换器,则通过链接转换调用来构成转换。也就是说,一次转换所返回的字节数组成为转换的输入(通过classfileBuffer参数)。

示例:

// HelloWorld.java
public class HelloWorld {
    public static void main(String[] args) {
        hello h1 = new hello();
        h1.hello();
        // 输出当前进程的 pid
        System.out.println("pid ==> " + [pid])
        // 产生中断,等待注入
        Scanner sc = new Scanner(System.in);
        sc.nextInt();

        hello h2 = new hello();
        h2.hello();
        System.out.println("ends...");
    }
}

// hello.java
public class hello {
    public void hello() {
        System.out.println("hello world");
    }
}
// AgentDemo.java
public class AgentDemo {
    public static void agentmain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException {
        Class[] classes = inst.getAllLoadedClasses();
        // 判断类是否已经加载
        for (Class aClass : classes) {      
            if (aClass.getName().equals(TransformerDemo.editClassName)) { 
                // 添加 Transformer
                inst.addTransformer(new TransformerDemo(), true);
                // 触发 Transformer
                inst.retransformClasses(aClass);
            }
        }
    }
}

// TransformerDemo.java
// 如果在使用过程中找不到javassist包中的类,那么可以使用URLCLassLoader+反射的方式调用
public class TransformerDemo implements ClassFileTransformer {
    // 只需要修改这里就能修改别的函数
    public static final String editClassName = "com.xxxx.hello.hello";
    public static final String editClassName2 = editClassName.replace('.''/');
    public static final String editMethod = "hello";

    @Override
    public byte[] transform(...) 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 source = "{System.out.println("hello transformer");}";
            method.setBody(source);
            byte[] bytes = ctc.toBytes();
            ctc.detach();
            return bytes;
        } catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}

总的来说就是利用attach.java attach到某个JVM,再利用angentdemo.java中的java.lang.instrument.Instrumentation对目标JVM中的.class进行汇总和筛选、添加(inst.addTransformer)并触发(inst.retransformClasses)Transformer,然后利用TransformerDemo.java修改字节码完成注入。

0x02 相关学习资源

filter内存马

servlet内存马

listener型

  • Tomcat下基于Listener的内存Webshell分析

Spring controller内存马

  • 基于内存 Webshell 的无文件攻击技术研究 - 安全客,安全资讯平台

Spring Interceptor内存马

  • 利用 intercetor 注入 spring 内存 webshell

其它前提研究-获取request对象

  • 基于tomcat的内存 Webshell 无文件攻击技术
  • Tomcat中一种半通用回显方法

Weblogic注入内存马

java agent内存马

  • 利用“进程注入”实现无文件复活 WebShell

内存马查杀

  • Tomcat 内存马检测
  • 查杀Java web filter型内存马 | 回忆飘如雪
  • Filter/Servlet型内存马的扫描抓捕与查杀 | 回忆飘如雪
  • 基于javaAgent内存马检测查杀指南-华盟网
  • https://github.com/alibaba/arthas
  • https://github.com/LandGrey/copagent
  • https://github.com/c0ny1/java-memshell-scanner
  • https://syst1m.com/post/memory-webshell/

汇总:GitHub - bitterzzZZ/MemoryShellLearn: 分享几个直接可用的内存马,记录一下学习过程中看过的文章


原文始发于微信公众号(XK Team):内存马学习(文末附部分学习资源链接)

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年4月10日00:23:35
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   内存马学习(文末附部分学习资源链接)http://cn-sec.com/archives/880172.html

发表评论

匿名网友 填写信息