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演示

  1. package com.anbai.sec.cmd;
  2. /**
  3. * 本地命令执行类
  4. * Creator: yz
  5. * Date: 2019/12/6
  6. */
  7. public class CommandExecution {
  8. public static native String exec(String cmd);
  9. }

如上示例代码,我们需要使用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,那么使用如下方式可以同时编译并生成头文件。

  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:

  1. /* DO NOT EDIT THIS FILE - it is machine generated */
  2. #include <jni.h>
  3. /* Header for class com_anbai_sec_cmd_CommandExecution */
  4. #ifndef _Included_com_anbai_sec_cmd_CommandExecution
  5. #define _Included_com_anbai_sec_cmd_CommandExecution
  6. #ifdef __cplusplus
  7. extern "C" {
  8. #endif
  9. /*
  10. * Class: com_anbai_sec_cmd_CommandExecution
  11. * Method: exec
  12. * Signature: (Ljava/lang/String;)Ljava/lang/String;
  13. */
  14. JNIEXPORT jstring JNICALL Java_com_anbai_sec_cmd_CommandExecution_exec
  15. (JNIEnv *, jclass, jstring);
  16. #ifdef __cplusplus
  17. }
  18. #endif
  19. #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。

参考如下类型对照表:

img

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示例:

  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. using namespace std;
  10. JNIEXPORT jstring
  11. JNICALL Java_com_anbai_sec_cmd_CommandExecution_exec
  12. (JNIEnv *env, jclass jclass, jstring str) {
  13. if (str != NULL) {
  14. jboolean jsCopy;
  15. // 将jstring参数转成char指针
  16. const char *cmd = env->GetStringUTFChars(str, &jsCopy);
  17. // 使用popen函数执行系统命令
  18. FILE *fd = popen(cmd, "r");
  19. if (fd != NULL) {
  20. // 返回结果字符串
  21. string result;
  22. // 定义字符串数组
  23. char buf[128];
  24. // 读取popen函数的执行结果
  25. while (fgets(buf, sizeof(buf), fd) != NULL) {
  26. // 拼接读取到的结果到result
  27. result +=buf;
  28. }
  29. // 关闭popen
  30. pclose(fd);
  31. // 返回命令执行结果给Java
  32. return env->NewStringUTF(result.c_str());
  33. }
  34. }
  35. return NULL;
  36. }

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

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

MacOSX编译:

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

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

  1. package com.anbai.sec.cmd;
  2. import java.io.File;
  3. import java.lang.reflect.Method;
  4. /**
  5. * Creator: yz
  6. * Date: 2019/12/8
  7. */
  8. public class CommandExecutionTest {
  9. private static final String COMMAND_CLASS_NAME = "com.anbai.sec.cmd.CommandExecution";
  10. /**
  11. * JDK1.5编译的com.anbai.sec.cmd.CommandExecution类字节码,
  12. * 只有一个public static native String exec(String cmd);的方法
  13. */
  14. private static final byte[] COMMAND_CLASS_BYTES = new byte[]{
  15. -54, -2, -70, -66, 0, 0, 0, 49, 0, 15, 10, 0, 3, 0, 12, 7, 0, 13, 7, 0, 14, 1,
  16. 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100,
  17. 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108,
  18. 101, 1, 0, 4, 101, 120, 101, 99, 1, 0, 38, 40, 76, 106, 97, 118, 97, 47, 108, 97,
  19. 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 76, 106, 97, 118, 97, 47, 108,
  20. 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114,
  21. 99, 101, 70, 105, 108, 101, 1, 0, 21, 67, 111, 109, 109, 97, 110, 100, 69, 120,
  22. 101, 99, 117, 116, 105, 111, 110, 46, 106, 97, 118, 97, 12, 0, 4, 0, 5, 1, 0, 34,
  23. 99, 111, 109, 47, 97, 110, 98, 97, 105, 47, 115, 101, 99, 47, 99, 109, 100, 47, 67,
  24. 111, 109, 109, 97, 110, 100, 69, 120, 101, 99, 117, 116, 105, 111, 110, 1, 0, 16,
  25. 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 0, 33, 0,
  26. 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,
  27. 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,
  28. 9, 0, 8, 0, 9, 0, 0, 0, 1, 0, 10, 0, 0, 0, 2, 0, 11
  29. };
  30. public static void main(String[] args) {
  31. String cmd = "ifconfig";// 定于需要执行的cmd
  32. try {
  33. ClassLoader loader = new ClassLoader(CommandExecutionTest.class.getClassLoader()) {
  34. @Override
  35. protected Class<?> findClass(String name) throws ClassNotFoundException {
  36. try {
  37. return super.findClass(name);
  38. } catch (ClassNotFoundException e) {
  39. return defineClass(COMMAND_CLASS_NAME, COMMAND_CLASS_BYTES, 0, COMMAND_CLASS_BYTES.length);
  40. }
  41. }
  42. };
  43. // 测试时候换成自己编译好的lib路径
  44. File libPath = new File("/Users/yz/IdeaProjects/javaweb-sec/javaweb-sec-source/javase/src/main/java/com/anbai/sec/cmd/libcmd.jnilib");
  45. // load命令执行类
  46. Class commandClass = loader.loadClass("com.anbai.sec.cmd.CommandExecution");
  47. // 可以用System.load也加载lib也可以用反射ClassLoader加载,如果loadLibrary0
  48. // 也被拦截了可以换java.lang.ClassLoader$NativeLibrary类的load方法。
  49. // System.load("/Users/yz/IdeaProjects/javaweb-sec/javaweb-sec-source/javase/src/main/java/com/anbai/sec/cmd/libcmd.jnilib/libcmd.jnilib");
  50. Method loadLibrary0Method = ClassLoader.class.getDeclaredMethod("loadLibrary0", Class.class, File.class);
  51. loadLibrary0Method.setAccessible(true);
  52. loadLibrary0Method.invoke(loader, commandClass, libPath);
  53. String content = (String) commandClass.getMethod("exec", String.class).invoke(null, cmd);
  54. System.out.println(content);
  55. } catch (Exception e) {
  56. e.printStackTrace();
  57. }
  58. }
  59. }

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来提升安全性。