# 9.2 Java Native命令执行

在前面的一节我们对命令执行的hook类做了一个简单的介绍,本节将详细的介绍命令执行的native方法。

RASP实现的基本原理是修改目标方法体,将检测逻辑插入到目标方法体中。而在Java方法中有一类是没有方法体(如接口中的方法、抽象方法和native方法), 其中native方法是是Java层面向系统层面调用的入口。JRASP实现了native方法的hook。

# 9.2.1 基本原理

java.lang.instrument.Instrumentation.setNativeMethodPrefix

setNativeMethodPrefix方法的注释

void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
1
  • 开启native函数的prefix功能 native函数的prefix功能默认关闭,需要在agent包之中的MANIFEST.MF文件中设置 Can-Set-Native-Method-Prefix: true

示例如下:

Manifest-Version: 1.0
Premain-Class: com.jrasp.example.agent.Agent
Agent-Class: com.jrasp.example.agent.Agent
Can-Redefine-Classes: true
Can-Retransform-Classes: true

1
2
3
4
5
6
  • Java 方法与 native 方法的映射关系

先来看命令执行的native方法和他的实现。

java.lang.UNIXProcess#forkAndExec

private native int forkAndExec(int mode, byte[] helperpath,
                                   byte[] prog,
                                   byte[] argBlock, int argc,
                                   byte[] envBlock, int envc,
                                   byte[] dir,
                                   int[] fds,
                                   boolean redirectErrorStream)
        throws IOException;
1
2
3
4
5
6
7
8

对应的native方法(hotspot)

jdk/src/solaris/native/java/lang/UNIXProcess_md.c

JNIEXPORT jint JNICALL
Java_java_lang_UNIXProcess_forkAndExec(JNIEnv *env,
                                       jobject process,
                                       jint mode,
                                       jbyteArray helperpath,
                                       jbyteArray prog,
                                       jbyteArray argBlock, jint argc,
                                       jbyteArray envBlock, jint envc,
                                       jbyteArray dir,
                                       jintArray std_fds,
                                       jboolean redirectErrorStream){
    //...(不是本文重点,省略)
}
1
2
3
4
5
6
7
8
9
10
11
12
13

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

如果给jvm增加一个ClassTransformer并设置native prefix,jvm将使用动态解析方式。

  • 假设我们有这样一个 native 方法,标准解析下对应的native 方法实现
native boolean foo(int x);  ====> Java_somePackage_someClass_foo(JNIEnv* env, jint x);
1
  • 给jvm增加一个ClassTransformer并设置native prefix 为wrapped_,这个类在转换之后。

方法的解析规则将变成:

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

(请注意 prefix 在native函数名中出现的位置!)

img.png

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

如果没有找到,那么虚拟机将会做依次进行下面的解析工作:

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_”。 虚拟机做的解析规则为 :

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

# 9.2.2 使用ASM 修改native方法

Agent

package com.jrasp.example.agent;

import java.lang.instrument.Instrumentation;
import java.text.SimpleDateFormat;
import java.util.Date;

public class Agent {

    public static void premain(String args, Instrumentation inst) {
        main(args, inst);
    }

    public static void agentmain(String args, Instrumentation inst) {
        main(args, inst);
    }

    public static void main(String args, Instrumentation inst) {
        System.out.println(String.format("%s  INFO [rasp] %s ",
                new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.sss").format(new Date()), "enter agent"));
        RaspClassFileTransformer raspClassFileTransformer = new RaspClassFileTransformer(inst);
        inst.addTransformer(raspClassFileTransformer);
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

RaspClassVisitor

package com.jrasp.example.agent;

import org.objectweb.asm.*;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.JSRInlinerAdapter;

import java.lang.instrument.Instrumentation;

public class RaspClassVisitor extends ClassVisitor {

    private final static String NATIVE_PREFIX = "RASP";

    private RaspMethod method = null;

    private final String targetClassInternalName;

    private final Instrumentation inst;

    private RaspClassFileTransformer raspClassFileTransformer;

    public RaspClassVisitor(final int api, final ClassVisitor cv,
                            String targetClassInternalName, Instrumentation inst,
                            RaspClassFileTransformer raspClassFileTransformer) {
        super(api, cv);
        this.targetClassInternalName = targetClassInternalName;
        this.inst = inst;
        this.raspClassFileTransformer = raspClassFileTransformer;
    }
    
    @Override
    public MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) {
        if ("forkAndExec".equals(name)) {
            if (inst.isNativeMethodPrefixSupported()) {
                inst.setNativeMethodPrefix(raspClassFileTransformer, NATIVE_PREFIX);
            } else {
                throw new UnsupportedOperationException("Native Method Prefix Unspported");
            }
            int newAccess = access & ~Opcodes.ACC_NATIVE;
            method = new RaspMethod(access, NATIVE_PREFIX + name, desc);
            final MethodVisitor mv = super.visitMethod(newAccess, name, desc, signature, exceptions);
            return new AdviceAdapter(api, new JSRInlinerAdapter(mv, newAccess, name, desc, signature, exceptions), newAccess, name, desc) {
                @Override
                public void visitEnd() {
                    loadThis();
                    loadArgs();
                    mv.visitMethodInsn(Opcodes.INVOKESPECIAL, targetClassInternalName, method.getName(), method.getDescriptor(), false);
                    returnValue();
                    super.visitEnd();
                }
            };
        }
        return super.visitMethod(access, name, desc, signature, exceptions);
    }

    @Override
    public void visitEnd() {
        if (method != null) {
            int newAccess = (Opcodes.ACC_PRIVATE | Opcodes.ACC_NATIVE | Opcodes.ACC_FINAL);
            MethodVisitor mv = cv.visitMethod(newAccess, method.getName(), method.getDescriptor(), null, null);
            mv.visitEnd();
        }
        super.visitEnd();
    }

}
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

RaspClassFileTransformer

package com.jrasp.example.agent;

import java.io.File;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

import static org.objectweb.asm.ClassReader.EXPAND_FRAMES;
import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES;
import static org.objectweb.asm.ClassWriter.COMPUTE_MAXS;
import static org.objectweb.asm.Opcodes.ASM9;

public class RaspClassFileTransformer implements ClassFileTransformer {

    private final Instrumentation inst;

    public RaspClassFileTransformer(Instrumentation inst) {
        this.inst = inst;
    }

    @Override
    public byte[] transform(ClassLoader loader, String className,
                            Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) throws IllegalClassFormatException {
        try {
            if ("java/lang/UNIXProcess".equals(className) || "java.lang.ProcessImpl".equals(className)) {
                final ClassReader cr = new ClassReader(classfileBuffer);
                final ClassWriter cw = new ClassWriter(cr, COMPUTE_FRAMES | COMPUTE_MAXS);
                cr.accept(new RaspClassVisitor(ASM9, cw, cr.getClassName(), inst, this), EXPAND_FRAMES);
                return dumpClassIfNecessary(cr.getClassName(), cw.toByteArray());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    private static byte[] dumpClassIfNecessary(String className, byte[] data) {
        final File dumpClassFile = new File("./rasp-class-dump/" + className + ".class");
        final File classPath = new File(dumpClassFile.getParent());
        if (!classPath.mkdirs()
                && !classPath.exists()) {
            return data;
        }
        try {
            FileUtils.writeByteArrayToFile(dumpClassFile, data);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return data;
    }

}
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

RaspMethod

package com.jrasp.example.agent;

import org.objectweb.asm.commons.Method;

public class RaspMethod extends Method {

    public int access;

    public RaspMethod(int access, String name, String descriptor) {
        super(name, descriptor);
        this.access = access;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

POM.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>java-agent</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <shade-prefix>rasp</shade-prefix>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm</artifactId>
            <version>9.2</version>
        </dependency>
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm-commons</artifactId>
            <version>9.2</version>
        </dependency>
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm-util</artifactId>
            <version>9.2</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.11.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.0</version>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                    <encoding>UTF-8</encoding>
                    <showDeprecation>true</showDeprecation>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.2.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Premain-Class>com.jrasp.example.agent.Agent</Premain-Class>
                            <Agent-Class>com.jrasp.example.agent.Agent</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                            <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.1.0</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <relocations>
                                <relocation>
                                    <pattern>org.objectweb.asm</pattern>
                                    <shadedPattern>${shade-prefix}.org.objectweb.asm</shadedPattern>
                                </relocation>
                                <relocation>
                                    <pattern>org.apache.commons.io</pattern>
                                    <shadedPattern>${shade-prefix}.org.apache.commons.io</shadedPattern>
                                </relocation>
                            </relocations>
                            <filters>
                                <filter>
                                    <artifact>*:*</artifact>
                                    <excludes>
                                        <exclude>META-INF/*.SF</exclude>
                                        <exclude>META-INF/*.DSA</exclude>
                                        <exclude>META-INF/*.RSA</exclude>
                                    </excludes>
                                </filter>
                            </filters>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
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

# 9.2.3 修改后的native方法

java.lang.UNIXProcessforkAndExec为例子说明修改前后的字节码。

原始字节码:

private final native int forkAndExec(int var1, byte[] var2, byte[] var3, byte[] var4, int var5, byte[] var6, int var7, byte[] var8, int[] var9, boolean var10);
1

修改后的字节码:

private int forkAndExec(int var1, byte[] var2, byte[] var3, byte[] var4, int var5, byte[] var6, int var7, byte[] var8, int[] var9, boolean var10) throws IOException {
    return this.RASPforkAndExec(var1, var2, var3, var4, var5, var6, var7, var8, var9, var10);
}
// 新增的方法
private final native int RASPforkAndExec(int var1, byte[] var2, byte[] var3, byte[] var4, int var5, byte[] var6, int var7, byte[] var8, int[] var9, boolean var10);
1
2
3
4
5

修改后的forkAndExec有方法体了,就可以在执行该方法时增加RASP检测逻辑了。