JNI安全基础

Java语言是基于C语言实现的,Java底层的很多API都是通过JNI(Java Native Interface)来实现的。通过JNI接口C/C++Java可以互相调用(存在跨平台问题)。Java可以通过JNI调用来弥补语言自身的不足(代码安全性、内存操作等)。这个看似非常炫酷的特性其实自JDK1.1开始就有了,但是我们不得不去考虑JNI调用带来的一系列的安全问题!

本章节仍以本地命令执行为例讲解如何构建动态链接库供Java调用,也许很多人是第一次接触这个概念会比较陌生但是如果你了学习过C/C++或者Android NDK那么本章节就会非常的简单了。

JNI-定义native方法

首先在Java中如果想要调用native方法那么需要在类中先定义一个native方法。

CommandExecution.java演示

package com.anbai.sec.cmd;

/**
 * 本地命令执行类
 * Creator: yz
 * Date: 2019/12/6
 */
public class CommandExecution {

    public static native String exec(String cmd);

}

如上示例代码,我们需要使用native关键字定义一个类似于接口的方法就行了,是不是感觉非常简单?

JNI-生成类头文件

如上,我们已经编写好了CommandExecution.java,现在我们需要编译并生成c语言头文件。

完整的步骤如下:

  1. cd ./javaweb-sec/javaweb-sec-source/javase/src/main/java/ (换成自己本地的地址)。
  2. vim或编辑器写入./com/anbai/sec/cmd/CommandExecution.java文件(该目录已存了一个注释掉的CommandExecution.java取消掉代码注释就可以用了)。
  3. javac -cp . com/anbai/sec/cmd/CommandExecution.java
  4. javah -d com/anbai/sec/cmd/ -cp . com.anbai.sec.cmd.CommandExecution

注意JDK版本:

JDK10移除了javah,需要改为javac-h参数的方式生产头文件,如果您的JDK版本正好>=10,那么使用如下方式可以同时编译并生成头文件。

javac -cp . com/anbai/sec/cmd/CommandExecution.java -h com/anbai/sec/cmd/

执行上面所述的命令后即可看到在com/anbai/sec/cmd/目录已经生成了CommandExecution.classcom_anbai_sec_cmd_CommandExecution.h了。

com_anbai_sec_cmd_CommandExecution.h:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_anbai_sec_cmd_CommandExecution */

#ifndef _Included_com_anbai_sec_cmd_CommandExecution
#define _Included_com_anbai_sec_cmd_CommandExecution
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_anbai_sec_cmd_CommandExecution
 * Method:    exec
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_anbai_sec_cmd_CommandExecution_exec
  (JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

您可以使用IDE或者vim完成动态链接库的编写,如果您使用MacOS+CLion可能需要把#include <jni.h>改成#include "jni.h",不改也没关系,编译的时候带上库地址就行了。

头文件命名强制性

javah生成的头文件中的函数命名方式是有非常强制性的约束的,如Java_com_anbai_sec_cmd_CommandExecution_execJava_是固定的前缀,而com_anbai_sec_cmd_CommandExecution也就代表着Java的完整包名称:com.anbai.sec.cmd.CommandExecution_exec自然是表示的方法名称了。(JNIEnv *, jclass, jstring)表示分别是JNI环境变量对象java调用的类对象参数入参类型

如果您在不希望在命令行下编译lib,可以参考:Mac IDEA+CLION jni Hello World

JNI-基础数据类型

需要特别注意的是Java和JNI定义的类型是需要转换的,不能直接使用Java里的类型,也不能直接将JNI、C/C++的类型直接返回给Java。

参考如下类型对照表:

Java类型 JNI类型 C/C++类型 大小
Boolean Jblloean unsigned char 无符号8位
Byte Jbyte char 有符号8位
Char Jchar unsigned short 无符号16位
Short Jshort short 有符号16位
Int Jint int 有符号32位
Long Jlong long long 有符号64位
Float Jfloat float 32位
Double Jdouble double 64位

jstring转char*:env->GetStringUTFChars(str, &jsCopy)

char*转jstring: env->NewStringUTF("Hello...")

字符串资源释放: env->ReleaseStringUTFChars(javaString, p);

其他知识点参考:jni中java与原生代码通信规则

JNI-编写C/C++本地命令执行实现

如上,我们已经生成好了头文件,接下来我们需要使用C/C++编写函数的最终实现代码。

com_anbai_sec_cmd_CommandExecution.cpp示例:

//
// Created by yz on 2019/12/6.
//
#include <iostream>
#include <stdlib.h>
#include <cstring>
#include <string>
#include "com_anbai_sec_cmd_CommandExecution.h"

using namespace std;

JNIEXPORT jstring

JNICALL Java_com_anbai_sec_cmd_CommandExecution_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;
}

使用vim com/anbai/sec/cmd/com_anbai_sec_cmd_CommandExecution.cpp或编辑器编写好cpp文件。

首先切换到我们的C目录:cd com/anbai/sec/cmd/然后使用g++命令编译成动态链接库,前提是您需要提前装好编译环境如:gcc/g++

MacOSX编译:

g++ -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" -shared -o libcmd.jnilib com_anbai_sec_cmd_CommandExecution.cpp

Linux编译:

g++ -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libcmd.so com_anbai_sec_cmd_CommandExecution.cpp

Windows编译:

  1. Visual Studio/cl命令编译dll。
  2. 使用min-gw/cygwin安装gcc/g++,如: x86_64-w64-mingw32-g++ -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o cmd.dll com_anbai_sec_cmd_CommandExecution.cpp

如依旧无法编译成,可参考:Java Programming Tutorial Java Native Interface (JNI),这篇文章讲解了如何在不同的操作系统中使用C/C++来编写JNI的HelloWorld。

如果您采用了C语言编写(C和C++版本基本没差别,也就在使用*env时的参数值一般会不一样)那么请用gcc编译,编译完成我们就可以使用这个动态链接库了。正常情况下我们需要严格按照JNI要求去命名文件名并且把链接库放到Java的动态链接库目录,不然会无法加载。但是这都不是什么大问题我们完全可以通过自定义库名称和路径。

com.anbai.sec.cmd.CommandExecutionTest示例:

package com.anbai.sec.cmd;

import java.io.File;
import java.lang.reflect.Method;

/**
 * Creator: yz
 * Date: 2019/12/8
 */
public class CommandExecutionTest {

    private static final String COMMAND_CLASS_NAME = "com.anbai.sec.cmd.CommandExecution";

    /**
     * JDK1.5编译的com.anbai.sec.cmd.CommandExecution类字节码,
     * 只有一个public static native String exec(String cmd);的方法
     */
    private static final byte[] COMMAND_CLASS_BYTES = new byte[]{
            -54, -2, -70, -66, 0, 0, 0, 49, 0, 15, 10, 0, 3, 0, 12, 7, 0, 13, 7, 0, 14, 1,
            0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100,
            101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108,
            101, 1, 0, 4, 101, 120, 101, 99, 1, 0, 38, 40, 76, 106, 97, 118, 97, 47, 108, 97,
            110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 76, 106, 97, 118, 97, 47, 108,
            97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114,
            99, 101, 70, 105, 108, 101, 1, 0, 21, 67, 111, 109, 109, 97, 110, 100, 69, 120,
            101, 99, 117, 116, 105, 111, 110, 46, 106, 97, 118, 97, 12, 0, 4, 0, 5, 1, 0, 34,
            99, 111, 109, 47, 97, 110, 98, 97, 105, 47, 115, 101, 99, 47, 99, 109, 100, 47, 67,
            111, 109, 109, 97, 110, 100, 69, 120, 101, 99, 117, 116, 105, 111, 110, 1, 0, 16,
            106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 0, 33, 0,
            2, 0, 3, 0, 0, 0, 0, 0, 2, 0, 1, 0, 4, 0, 5, 0, 1, 0, 6, 0, 0, 0, 29, 0, 1, 0, 1,
            0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 7, 0, 0, 0, 6, 0, 1, 0, 0, 0, 7, 1,
            9, 0, 8, 0, 9, 0, 0, 0, 1, 0, 10, 0, 0, 0, 2, 0, 11
    };

    public static void main(String[] args) {
        String cmd = "ifconfig";// 定于需要执行的cmd

        try {
            ClassLoader loader = new ClassLoader(CommandExecutionTest.class.getClassLoader()) {
                @Override
                protected Class<?> findClass(String name) throws ClassNotFoundException {
                    try {
                        return super.findClass(name);
                    } catch (ClassNotFoundException e) {
                        return defineClass(COMMAND_CLASS_NAME, COMMAND_CLASS_BYTES, 0, COMMAND_CLASS_BYTES.length);
                    }
                }
            };

            // 测试时候换成自己编译好的lib路径
            File libPath = new File("/Users/yz/IdeaProjects/javaweb-sec/javaweb-sec-source/javase/src/main/java/com/anbai/sec/cmd/libcmd.jnilib");

            // load命令执行类
            Class commandClass = loader.loadClass("com.anbai.sec.cmd.CommandExecution");

            // 可以用System.load也加载lib也可以用反射ClassLoader加载,如果loadLibrary0
            // 也被拦截了可以换java.lang.ClassLoader$NativeLibrary类的load方法。
//            System.load("/Users/yz/IdeaProjects/javaweb-sec/javaweb-sec-source/javase/src/main/java/com/anbai/sec/cmd/libcmd.jnilib/libcmd.jnilib");
            Method loadLibrary0Method = ClassLoader.class.getDeclaredMethod("loadLibrary0", Class.class, File.class);
            loadLibrary0Method.setAccessible(true);
            loadLibrary0Method.invoke(loader, commandClass, libPath);

            String content = (String) commandClass.getMethod("exec", String.class).invoke(null, cmd);
            System.out.println(content);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

CommandExecutionTest执行命令演示:

image-20191208152439368

示例代码中的CommandExecutionTest.java其实和load_library.jsp逻辑差不多,Demo实现了自定义ClassLoader重写了findClass方法来加载com.anbai.sec.cmd.CommandExecution类的字节码并实现调用,然后再通过JNI加载动态链接库并调用了链接库中的命令执行函数。

JNI安全基础总结

本章节我们学习了如何通过JNI调用动态链接库实现本地命令执行功能,我们应该深入的认识到通过编写native方法我们可以做几乎任何事(比如不使用Java自带的FileInputStreamAPI读文件、不使用forkAndExec执行系统命令等)。JNI为我们提供了如此强大的灵活性也为Java的安全性带来了非常大的挑战,所以某些情况下我们不得不考虑如何限制用户调用JNI来提升安全性。

results matching ""

    No results matching ""