JNI安全基础

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

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

    CommandExecution.java演示

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

    如上,我们已经编写好了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,那么使用如下方式可以同时编译并生成头文件。

    1. 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:

    您可以使用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

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

    参考如下类型对照表:

    jstring转char*:

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

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

    其他知识点参考:

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

    com_anbai_sec_cmd_CommandExecution.cpp示例:

    1. //
    2. // Created by yz on 2019/12/6.
    3. //
    4. #include <iostream>
    5. #include <stdlib.h>
    6. #include <cstring>
    7. #include <string>
    8. #include "com_anbai_sec_cmd_CommandExecution.h"
    9. JNIEXPORT jstring
    10. JNICALL Java_com_anbai_sec_cmd_CommandExecution_exec
    11. (JNIEnv *env, jclass jclass, jstring str) {
    12. if (str != NULL) {
    13. jboolean jsCopy;
    14. // 将jstring参数转成char指针
    15. const char *cmd = env->GetStringUTFChars(str, &jsCopy);
    16. // 使用popen函数执行系统命令
    17. FILE *fd = popen(cmd, "r");
    18. if (fd != NULL) {
    19. // 返回结果字符串
    20. string result;
    21. // 定义字符串数组
    22. // 读取popen函数的执行结果
    23. while (fgets(buf, sizeof(buf), fd) != NULL) {
    24. // 拼接读取到的结果到result
    25. }
    26. // 关闭popen
    27. pclose(fd);
    28. // 返回命令执行结果给Java
    29. return env->NewStringUTF(result.c_str());
    30. }
    31. }
    32. return NULL;
    33. }

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

    MacOSX编译:

    Linux编译:

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

    Windows编译:

    1. 使用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示例:

    CommandExecutionTest执行命令演示:

    image-20191208152439368

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

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