Java安全2

JNI

Java Native interface,Java本地接口;是一种编程框架,使Java程序可以调用本地程序/库,也可以供本地程序调用。这里的本地程序一般指通过其他语言如C、C++、汇编等写成的,编译好的程序。是JVM规范的一部分,自JDK1.1开始就有了。为什么要使用JNI,主要有四个原因:

  1. Java天然需要JNI技术:Java是平台无关的,但JVM是平台相关的,对于调用平台API的功能,背后只能通过JNI技术在Native层分别调用不同平台的API。
  2. Java的运行效率不及C/C++:相比后者Java的运行效率要低一些,因此对于一些有密集计算需求的情况,会选择使用C/C++实现对硬件的操作,再通过JNI调用。
  3. Native层代码安全性高:反编译so的文件比反编译Class高。
  4. 复用代码:存在某些需要的功能已经用C/C++实现时,可以直接复用。

另外,总是看到native方法但不明白是什么,于是查了一下,native方法是通过关键字native声明的方法,表明该方法的实现不是使用Java编写的,而是用其他语言编写的(如C/C++),并通过JNI实现对该方法的调用。下面自己写代码练习一下。

实现一个native方法并使用java调用它

首先就是定义一个native方法先。

定义一个native方法

接下来,函数主体准备使用C++实现,接下来编译并生成C++的头文件。因为考虑到我使用的是java8且8u65,版本不算新,我一开始使用的是javah这个比较旧的方式来生成头文件,javac NativeDemo.java && javah NativeDemo,但是一直提示“错误: 找不到 ‘NativeDemo’ 的类文件。”,后来查了一下说在包里面的要带上完整的包使用全路径类名,即javah sec.learn.NativeDemo,但依然失败了。很奇怪,最后使用了javac这种被认为是更“现代”的方式解决了,javac -h . ./NativeDemo.java。得到头文件sec_learn_NativeDemo.h,内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class sec_learn_NativeDemo */

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

#ifdef __cplusplus
}
#endif
#endif

JNI的函数命名是有要求的,如上需要是Java_完整类路径_函数名,上面的JNIEnv *指对JNI环境的引用,像jcalss、jstring都是类型。JNI和Java定义的类型是需要转换的,不能直接使用java里的类型。类型对照如下图:

JNI类型对照

图源:https://www.javasec.org/javase/JNI/

使用VS Studio新建一个空项目,把所需的头文件jni.hjni_md.hsec_learn_NativeDemo.h都通过添加现有项的方式,添加到项目中,但是要注意,仅仅将头文件包含进来还不够,还需要将头文件所在的路径包含在该工程的包含目录中,具体如下图所示:

添加头文件

项目包含头文件所在的路径

编写代码如下,实现执行系统命令的功能:

 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
#include <iostream>
#include <sstream>
#include <cstdio>
#include <memory>
#include "sec_learn_NativeDemo.h"

//using namespace std;

JNIEXPORT jstring JNICALL Java_sec_learn_NativeDemo_exec(JNIEnv* env, jclass jc, jstring command) {
    // 检查命令是否为 NULL
    if (command == NULL) {
        return env->NewStringUTF("Error: Command is null");
    }

    // 将 jstring 转换为 C++ 的字符串
    const char* nativeCommand = env->GetStringUTFChars(command, NULL);
    if (nativeCommand == NULL) {
        return env->NewStringUTF("Error: Failed to convert command");
    }

    // 打开管道,执行命令
    std::unique_ptr<FILE, decltype(&_pclose)> pipe(_popen(nativeCommand, "r"), _pclose);
    env->ReleaseStringUTFChars(command, nativeCommand); // 释放资源

    if (!pipe) {
        return env->NewStringUTF("Error: Failed to execute command");
    }

    // 读取命令输出
    std::ostringstream result;
    char buffer[128];
    while (fgets(buffer, sizeof(buffer), pipe.get()) != NULL) {
        result << buffer;
    }

    // 将结果转换为 jstring 并返回
    return env->NewStringUTF(result.str().c_str());
}

点击“项目”==>“属性”==>“常规”==>“配置类型”,选择为“动态库(.dll)”,如下图所示:

设置配置类型为动态库

点击最上方菜单栏的“生成”,选择“重新生成解决方案”,就会在解决方案的x64/Debug/目录下生成动态链接库.dll文件,把它拷贝到我们的IDEA项目中(不拷贝也行,记住绝对路径就行)。接着使用System.load导入动态链接库文件,或者使用System.loadLibrary通过名称导入。不过使用后者需要该文件在java.library.path中,后面我打印了一下,发现其实这个路径也将环境变量Path包含在内的。

接着就可以使用这个native的exec函数了,如下图所示:

成功调用native方法

也就是说借助JNI,可以通过编写native方法,不使用Java的API实现任何想要的功能。

文件系统

Java语言中对文件的任何操作最终都是通过JNI调用C语言的函数实现的。一开始我先学到文件系统,苦于看不懂JNI,于是先学习铺垫了JNI的知识;有了JNI的基础,再来看文件系统就好理解多了。

首先是Java中有两类文件系统,java.iojava.nio,后者的实现是sun.nio

API实现

图源:https://www.javasec.org/javase/FileSystem/FileSystem.html

Java.io 文件系统

java.io中抽象了一个java.io.FileSystem类出来,对于不同的操作系统有不同的实现。例如Windows和Unix下分别是java.io.WinNTFileSystemjava.io.UnixFileSystem

不同平台下的FileSystem

图源:https://www.javasec.org/javase/FileSystem/FileSystem.html

java.io.FileSystem对文件的操作最终都通过调用动态链接库中C实现的native方法来实现,是一种基于阻塞模式的IO文件系统,属于Java早期的设计,在Java 7引入了NIO.2,是新设计的、更加现代化的,采用了非阻塞模式(提供异步IO的支持)的文件系统。

下面这段还看不明白,mark住

并不是所有的文件操作都在java.io.FileSystem中定义,文件的读取最终调用的是java.io.FileInputStream#read0、readBytesjava.io.RandomAccessFile#read0、readBytes,而写文件调用的是java.io.FileOutputStream#writeBytesjava.io.RandomAccessFile#write0

NIO.2文件系统

为什么称之为NIO.2?

Java1.4曾引入了java.nio,nio意味(New Input/Output),Java 7等于对其做了革命性的重大更新,因此称之为NIO.2。同时与sun.nio也是不同的,sun.nio提供的是底层的支持,是Java的内部实现包,非公开API,并不能直接调用。

NIO在不同的操作系统上最终实现的类也是不一样的,例如Mac的实现类是: sun.nio.fs.UnixNativeDispatcher,而Windows的实现类是sun.nio.fs.WindowsNativeDispatcher

纸上得来终觉浅,文件系统这块有机会还是多实践才能更了解

插曲-IDEA切换Java版本

添加了一个Java8的jdk,但是切换起来没有那么顺利,至少有三个地方需要改。

  1. 项目结构==>项目设置==>项目==>SDK和语言级别
  2. 项目结构==>项目设置==>模块==>源==>语言级别
  3. 设置==>构建、执行、部署==>编译器==>Java编译器==>目标字节码版本
  4. 如果使用了maven,且maven中有java.version的话,也需要更改(我没有遇到)

参考文献

https://zh.wikipedia.org/wiki/Java本地接口

https://zhaoshuming.github.io/2020/01/02/android-ndk-jni/

https://www.javasec.org/javase/JNI/

https://cloud.tencent.com/developer/article/2123817

https://www3.ntu.edu.sg/home/ehchua/programming/java/JavaNativeInterface.html

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计