Java基础:初探JNI安全

文章发布时间:

最后更新时间:

文章总字数:
1.8k

预计阅读时间:
7 分钟

Java基础:初探JNI安全

Java语言是基于C语言实现的,Java底层的很多API都是通过JNI(Java Native Interface)来实现的。通过JNI接口C/C++Java可以互相调用(存在跨平台问题)。Java可以通过JNI调用来弥补语言自身的不足(代码安全性、内存操作等)。

本文属于初探,主要以本地命令执行为例讲解如何构建动态链接库供Java调用。

参考文章:https://blog.csdn.net/daiyi666/article/details/131163063

JNI-定义native方法

首先创建一个java项目,并在一个类中先定义将来要调用的native方法。

image-20230903163937504

如上代码,就是使用native关键字定义一个类似接口的方法,关键字native通常表示要调用C或C++的结构。

JNI-生成类头文件

先在终端使用javac编译CommandExecution.java为.class文件,然后使用javah来生成类头文件(等下要在C++项目中引用该头文件),具体命令如下:

1
2
javac com/anbai/sec/cmd/CommandExecution.java
javah -jni com.anbai.sec.cmd.CommandExecution

注意这里一定要是完整的软件包路径,自然要求终端的路径在软件包最外层,与com同级,否则会显示找不到类文件。

另外,需要注意JDK版本

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

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

image-20230903202331254

可以看到这个头文件命名是有强制性的,包含了完整包名和类名,点开看看里面:

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

可以看到里面有一个Java_com_anbai_sec_cmd_CommandExecution_exec的声明,等一下就需要在cpp文件中实现这个函数,这三个参数分别是JNI环境变量对象,java调用的类和参数的入参类型。

JNI-基础数据类型

可以看到上一段代码中jstring代表参数的入参类型,这个jstring就是JNI定义的数据类型,其与Java是需要转换的,不能直接用,也不能直接把JNI和C,CPP的类型直接返回Java。

image-20230903224906732

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

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

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

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

上文中我们已经编写好了头文件,下一步就是具体实现代码。

首先打开clion,新建一个项目。注意选择共享库,而不是可执行文件,不然后面构建的时候报错。

image-20230903225332286

创建项目之后需要加入的文件:刚刚生成的头文件,jni.h和jni_md.h(这两个文件可以在java的安装路径include文件夹里面找到,当然也可以全局搜索找),然后我们创建一个和头文件同名的com_anbai_sec_cmd_CommandExecution.cpp,开始实现具体代码。

具体代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#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;
const char *cmd = env->GetStringUTFChars(str,&jsCopy); //将jstring转为char*
FILE *fd = popen(cmd,"r"); //使用popen来最终实现命令执行功能,获得文件指针
string result;
if(fd != NULL){
char buf[128];
while(fgets(buf,sizeof(buf),fd) != NULL){
result += buf;
}
}
pclose(fd); //将文件,也就是命令执行的内容读出来返回
return env->NewStringUTF(result.c_str()); //将char*转换为jstring才能返回
}
return NULL;
}//对刚刚声明的函数进行实现

写完之后可以点击clion上面的那个像锤子一样的按钮(对文件进行构建),成功在cmake-build-debug中得到构建完成的动态链接库libexec.dll(这个名字不重要,可以自行更改),这个就是java调用原生语言方法的关键。

image-20230903230804551

JNI-使用动态链接库调用函数

调用System.load方法加载刚刚得到的libexec.dll动态链接库,System.load的参数是动态链接库的绝对路径,这里先做一个简单的测试调用:

image-20230903231854668

可以看到这里成功执行了命令,而我们本软件包内的CommandExecution.java是没有实现exec方法的,只是做了声明,可以得知成功load了动态链接库,并访问到了我们刚刚写的cpp文件内的函数。

这里为了方便,test代码直接放在了一开始声明方法的类下面,从而可以直接new一个对象,正常情况下需要先使用字节码注册类,然后通过反射获取exec方法。

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
package com.anbai.sec.cmd;

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

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[]{};

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);
}
}
};//重写了findClass方法来注册类

// 测试时候换成自己编译好的lib路径
File libPath = new File("");

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

// 可以用System.load也加载lib也可以用反射ClassLoader加载,如果loadLibrary0
// 也被拦截了可以换java.lang.ClassLoader$NativeLibrary类的load方法。
// System.load(""); 里面加文件路径

Method loadLibrary0Method = ClassLoader.class.getDeclaredMethod("loadLibrary0", Class.class, File.class);
loadLibrary0Method.setAccessible(true);
loadLibrary0Method.invoke(loader, commandClass, libPath);

//这里未被注释的代码展示的是通过loadLibrary0Method来反射加载lib

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

}

总流程就是注册类,然后动态链接,最后反射调用方法。

总结

本文中我们学习了如何通过JNI调用动态链接库实现本地命令执行功能,我们应该深入的认识到通过编写native方法我们可以做几乎任何事(比如不使用Java自带的FileInputStreamAPI读文件、不使用forkAndExec执行系统命令等)。JNI如此强大的功能也带来了很多安全隐患,我们以后再慢慢细说。

另外,这个JNI这种跨平台跨语言的调用方式今天还是第一次见,在配置环境和进行测试的时候出了不少小问题,所幸一下午之后还是解决了,对JNI运行方式的理解也加深了不少,看技术文章的时候一定要自己多动手操作,再多想想,不能干看也不能照抄。